feat: scaffold Rabby Rust Tauri terminal
test / core (push) Successful in 1m55s

This commit is contained in:
Tom You
2026-07-06 10:44:47 +00:00
commit 04ef2fe57d
21 changed files with 9460 additions and 0 deletions
+246
View File
@@ -0,0 +1,246 @@
//! Core feature model for Rabby, a Rust/Tauri terminal workspace inspired by Tabby.
//!
//! This crate is intentionally dependency-light so terminal/profile/session behavior can be
//! unit-tested without starting the desktop shell.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ConnectionKind {
LocalShell,
Ssh,
Telnet,
Serial,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Profile {
pub id: String,
pub name: String,
pub kind: ConnectionKind,
pub command: String,
pub color_scheme: String,
}
impl Profile {
pub fn new(id: &str, name: &str, kind: ConnectionKind, command: &str) -> Result<Self, String> {
if id.trim().is_empty() {
return Err("profile id is required".into());
}
if name.trim().is_empty() {
return Err("profile name is required".into());
}
if command.trim().is_empty() {
return Err("profile command is required".into());
}
Ok(Self {
id: slug(id),
name: name.trim().to_string(),
kind,
command: command.trim().to_string(),
color_scheme: "Rabby Dark".to_string(),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PaneNode {
Leaf {
profile_id: String,
},
Split {
axis: SplitAxis,
ratio_percent: u8,
first: Box<PaneNode>,
second: Box<PaneNode>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SplitAxis {
Horizontal,
Vertical,
}
impl PaneNode {
pub fn leaf(profile_id: impl Into<String>) -> Self {
Self::Leaf {
profile_id: profile_id.into(),
}
}
pub fn split(
self,
axis: SplitAxis,
other: PaneNode,
ratio_percent: u8,
) -> Result<Self, String> {
if !(10..=90).contains(&ratio_percent) {
return Err("split ratio must be between 10 and 90 percent".into());
}
Ok(Self::Split {
axis,
ratio_percent,
first: Box::new(self),
second: Box::new(other),
})
}
pub fn leaf_count(&self) -> usize {
match self {
PaneNode::Leaf { .. } => 1,
PaneNode::Split { first, second, .. } => first.leaf_count() + second.leaf_count(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Tab {
pub title: String,
pub root: PaneNode,
pub progress: Option<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Workspace {
pub profiles: Vec<Profile>,
pub tabs: Vec<Tab>,
pub active_tab: usize,
pub quake_mode: bool,
}
impl Workspace {
pub fn default_linux() -> Self {
let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".to_string());
let profile = Profile::new("local", "Local Shell", ConnectionKind::LocalShell, &shell)
.expect("static default profile is valid");
Self {
profiles: vec![profile.clone()],
tabs: vec![Tab {
title: profile.name.clone(),
root: PaneNode::leaf(profile.id),
progress: None,
}],
active_tab: 0,
quake_mode: false,
}
}
pub fn add_tab(&mut self, profile_id: &str) -> Result<(), String> {
let profile = self
.profiles
.iter()
.find(|p| p.id == profile_id)
.ok_or_else(|| format!("unknown profile: {profile_id}"))?;
self.tabs.push(Tab {
title: profile.name.clone(),
root: PaneNode::leaf(profile.id.clone()),
progress: None,
});
self.active_tab = self.tabs.len() - 1;
Ok(())
}
pub fn feature_matrix() -> Vec<(&'static str, &'static str)> {
vec![
(
"Terminal",
"VT-style terminal surface, tabs, nested split panes, Unicode-first rendering",
),
(
"Connections",
"Local shell, SSH, Telnet, and Serial profile model",
),
(
"UX",
"Command palette, configurable shortcuts, quake-mode window, remembered sessions",
),
(
"Transfers",
"Reserved Zmodem/SFTP transfer surface for SSH sessions",
),
(
"Security",
"Encrypted secret-store boundary in the Tauri/Rust backend",
),
(
"Footprint",
"Rust core + Tauri shell instead of Electron/Angular runtime",
),
]
}
}
pub fn slug(input: &str) -> String {
let mut out = String::new();
let mut last_dash = false;
for ch in input.trim().chars() {
if ch.is_ascii_alphanumeric() {
out.push(ch.to_ascii_lowercase());
last_dash = false;
} else if !last_dash {
out.push('-');
last_dash = true;
}
}
out.trim_matches('-').to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn slug_normalizes_profile_ids() {
assert_eq!(slug(" SSH: Prod Box "), "ssh-prod-box");
}
#[test]
fn profile_validation_rejects_empty_command() {
let err = Profile::new("p", "Prod", ConnectionKind::Ssh, " ").unwrap_err();
assert!(err.contains("command"));
}
#[test]
fn split_panes_count_nested_leaves() {
let root = PaneNode::leaf("local")
.split(SplitAxis::Horizontal, PaneNode::leaf("ssh"), 50)
.unwrap()
.split(SplitAxis::Vertical, PaneNode::leaf("serial"), 65)
.unwrap();
assert_eq!(root.leaf_count(), 3);
}
#[test]
fn split_ratio_has_sane_bounds() {
assert!(PaneNode::leaf("a")
.split(SplitAxis::Horizontal, PaneNode::leaf("b"), 5)
.is_err());
assert!(PaneNode::leaf("a")
.split(SplitAxis::Horizontal, PaneNode::leaf("b"), 50)
.is_ok());
}
#[test]
fn workspace_adds_tabs_for_known_profiles() {
let mut ws = Workspace::default_linux();
ws.profiles
.push(Profile::new("ssh-prod", "Prod SSH", ConnectionKind::Ssh, "ssh prod").unwrap());
ws.add_tab("ssh-prod").unwrap();
assert_eq!(ws.active_tab, 1);
assert_eq!(ws.tabs[1].title, "Prod SSH");
}
#[test]
fn feature_matrix_covers_tabby_readme_capabilities() {
let text = Workspace::feature_matrix()
.iter()
.map(|(a, b)| format!("{a} {b}"))
.collect::<Vec<_>>()
.join("\n")
.to_lowercase();
for needle in [
"terminal", "ssh", "telnet", "serial", "split", "unicode", "quake", "zmodem",
] {
assert!(text.contains(needle), "missing {needle}");
}
}
}