From d05ba2ea9f1ab2379f5be5ef7fc3898a03f5dce1 Mon Sep 17 00:00:00 2001 From: Ogulcan Celik Date: Sat, 28 Mar 2026 16:38:04 +0300 Subject: [PATCH] feat: restore workspace numbers in expanded sidebar --- src/app/input.rs | 43 +++++++++++---- src/config.rs | 135 +++++++++++++++++++++++++++++++++++++++-------- src/persist.rs | 5 +- src/ui.rs | 56 +++++++++++++++----- src/workspace.rs | 3 +- 5 files changed, 196 insertions(+), 46 deletions(-) diff --git a/src/app/input.rs b/src/app/input.rs index 7e7f5877..590e9548 100644 --- a/src/app/input.rs +++ b/src/app/input.rs @@ -249,7 +249,11 @@ fn handle_rename_key(state: &mut AppState, key: KeyEvent) { fn handle_resize_key(state: &mut AppState, key: KeyEvent) { if key.code == KeyCode::Esc || key.code == KeyCode::Enter - || key_matches(&key, state.keybinds.resize_mode.0, state.keybinds.resize_mode.1) + || key_matches( + &key, + state.keybinds.resize_mode.0, + state.keybinds.resize_mode.1, + ) { if state.active.is_some() { state.mode = Mode::Terminal; @@ -599,8 +603,8 @@ impl AppState { #[cfg(test)] mod tests { use super::*; - use crossterm::event::KeyModifiers; use crate::workspace::Workspace; + use crossterm::event::KeyModifiers; fn state_with_workspaces(names: &[&str]) -> AppState { let mut state = AppState::test_new(); @@ -619,7 +623,10 @@ mod tests { state.keybinds.rename_workspace = (KeyCode::Char('g'), KeyModifiers::empty()); state.keybinds.rename_workspace_label = "g".into(); - handle_navigate_key(&mut state, KeyEvent::new(KeyCode::Char('g'), KeyModifiers::empty())); + handle_navigate_key( + &mut state, + KeyEvent::new(KeyCode::Char('g'), KeyModifiers::empty()), + ); assert_eq!(state.mode, Mode::RenameSession); assert_eq!(state.name_input, "test"); @@ -631,7 +638,10 @@ mod tests { state.keybinds.new_workspace = (KeyCode::Char('g'), KeyModifiers::empty()); state.keybinds.new_workspace_label = "g".into(); - handle_navigate_key(&mut state, KeyEvent::new(KeyCode::Char('g'), KeyModifiers::empty())); + handle_navigate_key( + &mut state, + KeyEvent::new(KeyCode::Char('g'), KeyModifiers::empty()), + ); assert!(state.request_new_workspace); assert_eq!(state.mode, Mode::Terminal); @@ -644,7 +654,10 @@ mod tests { state.keybinds.toggle_sidebar_label = "g".into(); assert!(!state.sidebar_collapsed); - handle_navigate_key(&mut state, KeyEvent::new(KeyCode::Char('g'), KeyModifiers::empty())); + handle_navigate_key( + &mut state, + KeyEvent::new(KeyCode::Char('g'), KeyModifiers::empty()), + ); assert!(state.sidebar_collapsed); assert_eq!(state.mode, Mode::Terminal); @@ -656,7 +669,10 @@ mod tests { state.keybinds.resize_mode = (KeyCode::Char('g'), KeyModifiers::empty()); state.keybinds.resize_mode_label = "g".into(); - handle_navigate_key(&mut state, KeyEvent::new(KeyCode::Char('g'), KeyModifiers::empty())); + handle_navigate_key( + &mut state, + KeyEvent::new(KeyCode::Char('g'), KeyModifiers::empty()), + ); assert_eq!(state.mode, Mode::Resize); } @@ -666,7 +682,10 @@ mod tests { let mut state = state_with_workspaces(&["a", "b"]); state.selected = 0; - handle_navigate_key(&mut state, KeyEvent::new(KeyCode::Down, KeyModifiers::empty())); + handle_navigate_key( + &mut state, + KeyEvent::new(KeyCode::Down, KeyModifiers::empty()), + ); assert_eq!(state.selected, 1); assert_eq!(state.mode, Mode::Navigate); @@ -679,7 +698,10 @@ mod tests { state.keybinds.fullscreen = (KeyCode::Char('g'), KeyModifiers::empty()); state.keybinds.fullscreen_label = "g".into(); - handle_navigate_key(&mut state, KeyEvent::new(KeyCode::Char('g'), KeyModifiers::empty())); + handle_navigate_key( + &mut state, + KeyEvent::new(KeyCode::Char('g'), KeyModifiers::empty()), + ); assert!(state.workspaces[0].zoomed); assert_eq!(state.mode, Mode::Terminal); @@ -692,7 +714,10 @@ mod tests { state.keybinds.resize_mode = (KeyCode::Char('g'), KeyModifiers::empty()); state.keybinds.resize_mode_label = "g".into(); - handle_resize_key(&mut state, KeyEvent::new(KeyCode::Char('g'), KeyModifiers::empty())); + handle_resize_key( + &mut state, + KeyEvent::new(KeyCode::Char('g'), KeyModifiers::empty()), + ); assert_eq!(state.mode, Mode::Terminal); } diff --git a/src/config.rs b/src/config.rs index e4899248..8df9d6b5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -181,7 +181,10 @@ impl Config { Ok(content) => match toml::from_str::(&content) { Ok(config) => { let diagnostics = config.collect_diagnostics(); - return LoadedConfig { config, diagnostics }; + return LoadedConfig { + config, + diagnostics, + }; } Err(e) => { warn!(err = %e, "config parse error, using defaults"); @@ -220,7 +223,14 @@ impl Config { prefix_diag.into_iter().chain(keybind_diags).collect() } - fn validated_keybinds(&self) -> (Option, (KeyCode, KeyModifiers), Vec, Keybinds) { + fn validated_keybinds( + &self, + ) -> ( + Option, + (KeyCode, KeyModifiers), + Vec, + Keybinds, + ) { #[derive(Clone)] struct Binding<'a> { field: &'a str, @@ -264,15 +274,69 @@ impl Config { } let mut bindings = vec![ - binding("keys.new_workspace", &self.keys.new_workspace, "n", (KeyCode::Char('n'), KeyModifiers::empty()), &mut diagnostics), - binding("keys.rename_workspace", &self.keys.rename_workspace, "shift+n", (KeyCode::Char('n'), KeyModifiers::SHIFT), &mut diagnostics), - binding("keys.close_workspace", &self.keys.close_workspace, "d", (KeyCode::Char('d'), KeyModifiers::empty()), &mut diagnostics), - binding("keys.split_vertical", &self.keys.split_vertical, "v", (KeyCode::Char('v'), KeyModifiers::empty()), &mut diagnostics), - binding("keys.split_horizontal", &self.keys.split_horizontal, "-", (KeyCode::Char('-'), KeyModifiers::empty()), &mut diagnostics), - binding("keys.close_pane", &self.keys.close_pane, "x", (KeyCode::Char('x'), KeyModifiers::empty()), &mut diagnostics), - binding("keys.fullscreen", &self.keys.fullscreen, "f", (KeyCode::Char('f'), KeyModifiers::empty()), &mut diagnostics), - binding("keys.resize_mode", &self.keys.resize_mode, "r", (KeyCode::Char('r'), KeyModifiers::empty()), &mut diagnostics), - binding("keys.toggle_sidebar", &self.keys.toggle_sidebar, "b", (KeyCode::Char('b'), KeyModifiers::empty()), &mut diagnostics), + binding( + "keys.new_workspace", + &self.keys.new_workspace, + "n", + (KeyCode::Char('n'), KeyModifiers::empty()), + &mut diagnostics, + ), + binding( + "keys.rename_workspace", + &self.keys.rename_workspace, + "shift+n", + (KeyCode::Char('n'), KeyModifiers::SHIFT), + &mut diagnostics, + ), + binding( + "keys.close_workspace", + &self.keys.close_workspace, + "d", + (KeyCode::Char('d'), KeyModifiers::empty()), + &mut diagnostics, + ), + binding( + "keys.split_vertical", + &self.keys.split_vertical, + "v", + (KeyCode::Char('v'), KeyModifiers::empty()), + &mut diagnostics, + ), + binding( + "keys.split_horizontal", + &self.keys.split_horizontal, + "-", + (KeyCode::Char('-'), KeyModifiers::empty()), + &mut diagnostics, + ), + binding( + "keys.close_pane", + &self.keys.close_pane, + "x", + (KeyCode::Char('x'), KeyModifiers::empty()), + &mut diagnostics, + ), + binding( + "keys.fullscreen", + &self.keys.fullscreen, + "f", + (KeyCode::Char('f'), KeyModifiers::empty()), + &mut diagnostics, + ), + binding( + "keys.resize_mode", + &self.keys.resize_mode, + "r", + (KeyCode::Char('r'), KeyModifiers::empty()), + &mut diagnostics, + ), + binding( + "keys.toggle_sidebar", + &self.keys.toggle_sidebar, + "b", + (KeyCode::Char('b'), KeyModifiers::empty()), + &mut diagnostics, + ), ]; use std::collections::HashMap; @@ -477,7 +541,6 @@ fn parse_key_combo_with_diagnostic( } } - #[cfg(test)] mod tests { use super::*; @@ -569,7 +632,10 @@ mod tests { let config = Config::default(); let kb = config.keybinds(); assert_eq!(kb.new_workspace.0, KeyCode::Char('n')); - assert_eq!(kb.rename_workspace, (KeyCode::Char('n'), KeyModifiers::SHIFT)); + assert_eq!( + kb.rename_workspace, + (KeyCode::Char('n'), KeyModifiers::SHIFT) + ); assert_eq!(kb.close_workspace.0, KeyCode::Char('d')); assert_eq!(kb.split_vertical.0, KeyCode::Char('v')); assert_eq!(kb.split_horizontal.0, KeyCode::Char('-')); @@ -600,9 +666,18 @@ toggle_sidebar = "tab" assert_eq!(mods, KeyModifiers::CONTROL); let kb = config.keybinds(); - assert_eq!(kb.new_workspace, (KeyCode::Char('c'), KeyModifiers::empty())); - assert_eq!(kb.rename_workspace, (KeyCode::Char('r'), KeyModifiers::SHIFT)); - assert_eq!(kb.close_workspace, (KeyCode::Char('d'), KeyModifiers::CONTROL)); + assert_eq!( + kb.new_workspace, + (KeyCode::Char('c'), KeyModifiers::empty()) + ); + assert_eq!( + kb.rename_workspace, + (KeyCode::Char('r'), KeyModifiers::SHIFT) + ); + assert_eq!( + kb.close_workspace, + (KeyCode::Char('d'), KeyModifiers::CONTROL) + ); assert_eq!(kb.split_vertical.0, KeyCode::Char('s')); assert_eq!( kb.split_horizontal, @@ -622,7 +697,10 @@ split_horizontal = "D" "#; let config: Config = toml::from_str(toml).unwrap(); let kb = config.keybinds(); - assert_eq!(kb.split_horizontal, (KeyCode::Char('d'), KeyModifiers::SHIFT)); + assert_eq!( + kb.split_horizontal, + (KeyCode::Char('d'), KeyModifiers::SHIFT) + ); } #[test] @@ -635,8 +713,13 @@ rename_workspace = "wat" let diagnostics = config.collect_diagnostics(); let kb = config.keybinds(); - assert!(diagnostics.iter().any(|d| d.contains("keys.rename_workspace"))); - assert_eq!(kb.rename_workspace, (KeyCode::Char('n'), KeyModifiers::SHIFT)); + assert!(diagnostics + .iter() + .any(|d| d.contains("keys.rename_workspace"))); + assert_eq!( + kb.rename_workspace, + (KeyCode::Char('n'), KeyModifiers::SHIFT) + ); assert_eq!(kb.rename_workspace_label, "shift+n"); } @@ -651,9 +734,17 @@ rename_workspace = "g" let diagnostics = config.collect_diagnostics(); let kb = config.keybinds(); - assert!(diagnostics.iter().any(|d| d.contains("duplicate keybinding"))); - assert_eq!(kb.new_workspace, (KeyCode::Char('g'), KeyModifiers::empty())); - assert_eq!(kb.rename_workspace, (KeyCode::Char('n'), KeyModifiers::SHIFT)); + assert!(diagnostics + .iter() + .any(|d| d.contains("duplicate keybinding"))); + assert_eq!( + kb.new_workspace, + (KeyCode::Char('g'), KeyModifiers::empty()) + ); + assert_eq!( + kb.rename_workspace, + (KeyCode::Char('n'), KeyModifiers::SHIFT) + ); assert_eq!(kb.rename_workspace_label, "shift+n"); } diff --git a/src/persist.rs b/src/persist.rs index 2660cc8f..9e211730 100644 --- a/src/persist.rs +++ b/src/persist.rs @@ -406,7 +406,10 @@ mod tests { let restored: SessionSnapshot = serde_json::from_str(&json).unwrap(); assert_eq!(restored.workspaces.len(), 1); - assert_eq!(restored.workspaces[0].custom_name.as_deref(), Some("pi-mono")); + assert_eq!( + restored.workspaces[0].custom_name.as_deref(), + Some("pi-mono") + ); assert_eq!(restored.workspaces[0].panes.len(), 2); assert_eq!( restored.workspaces[0].panes[&0].cwd, diff --git a/src/ui.rs b/src/ui.rs index a4d88b1b..235be9e3 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -140,14 +140,16 @@ fn compute_sidebar_width(app: &AppState) -> u16 { let max_line = app .workspaces .iter() - .map(|ws| { + .enumerate() + .map(|(i, ws)| { let name_len = ws.display_name().len(); + let number_len = (i + 1).to_string().len(); let pane_count = if ws.layout.pane_count() > 1 { ws.layout.pane_count() } else { 0 }; - let line1 = 4 + name_len + pane_count; // marker + dot + spaces + pane dots + let line1 = 5 + name_len + number_len + pane_count; // marker + number + name + spaces + pane dots let line2 = ws.agent_summary().map(|s| s.len() + 2).unwrap_or(0); line1.max(line2) }) @@ -160,7 +162,11 @@ fn compute_sidebar_width(app: &AppState) -> u16 { fn render_sidebar_collapsed(app: &AppState, frame: &mut Frame, area: Rect) { let is_navigating = matches!( app.mode, - Mode::Navigate | Mode::RenameSession | Mode::Resize | Mode::ConfirmClose | Mode::ContextMenu + Mode::Navigate + | Mode::RenameSession + | Mode::Resize + | Mode::ConfirmClose + | Mode::ContextMenu ); let sep_style = if is_navigating { @@ -208,7 +214,14 @@ fn render_sidebar_collapsed(app: &AppState, frame: &mut Frame, area: Rect) { Paragraph::new(Line::from(vec![ Span::styled(format!("{}", i + 1), num_style), Span::styled(" ", row_style), - Span::styled(icon, if is_selected { icon_style.bg(app.accent) } else { icon_style }), + Span::styled( + icon, + if is_selected { + icon_style.bg(app.accent) + } else { + icon_style + }, + ), ])), Rect::new(area.x, y, content_w, 1), ); @@ -220,7 +233,11 @@ fn render_sidebar_collapsed(app: &AppState, frame: &mut Frame, area: Rect) { fn render_sidebar(app: &AppState, frame: &mut Frame, area: Rect) { let is_navigating = matches!( app.mode, - Mode::Navigate | Mode::RenameSession | Mode::Resize | Mode::ConfirmClose | Mode::ContextMenu + Mode::Navigate + | Mode::RenameSession + | Mode::Resize + | Mode::ConfirmClose + | Mode::ContextMenu ); let highlight = Style::default().bg(app.accent).fg(Color::Black); let sep_style = if is_navigating { @@ -249,7 +266,11 @@ fn render_sidebar(app: &AppState, frame: &mut Frame, area: Rect) { let marker = if Some(i) == app.active { "▸" } else { " " }; let (agg_state, agg_seen) = ws.aggregate_state(); let (icon, icon_style) = state_icon_style(agg_state, agg_seen); - let text_style = if selected { highlight } else { Style::default() }; + let text_style = if selected { + highlight + } else { + Style::default() + }; let dim_style = if selected { highlight } else { @@ -257,24 +278,30 @@ fn render_sidebar(app: &AppState, frame: &mut Frame, area: Rect) { }; let mut line1 = vec![ Span::styled(marker, text_style), - Span::styled( - ws.display_name(), - text_style.add_modifier(Modifier::BOLD), - ), + Span::styled(format!("{} ", i + 1), dim_style), + Span::styled(ws.display_name(), text_style.add_modifier(Modifier::BOLD)), Span::styled(" ", dim_style), ]; if ws.layout.pane_count() == 1 { line1.push(Span::styled( icon, - if selected { icon_style.bg(app.accent) } else { icon_style }, + if selected { + icon_style.bg(app.accent) + } else { + icon_style + }, )); } else { for (pane_state, pane_seen) in ws.pane_states() { let (pane_icon, pane_style) = state_icon_style(pane_state, pane_seen); line1.push(Span::styled( pane_icon, - if selected { pane_style.bg(app.accent) } else { pane_style }, + if selected { + pane_style.bg(app.accent) + } else { + pane_style + }, )); } } @@ -288,7 +315,10 @@ fn render_sidebar(app: &AppState, frame: &mut Frame, area: Rect) { } } - frame.render_widget(Paragraph::new(Line::from(line1)), Rect::new(content.x, row_y, content.width, 1)); + frame.render_widget( + Paragraph::new(Line::from(line1)), + Rect::new(content.x, row_y, content.width, 1), + ); if app.mode == Mode::RenameSession && i == app.selected { let text = format!(" {}\u{2588}", app.name_input); diff --git a/src/workspace.rs b/src/workspace.rs index 0e296639..78992a73 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -61,7 +61,8 @@ impl Workspace { cwd: Option, ) -> std::io::Result { let new_id = self.layout.split_focused(direction); - let actual_cwd = cwd.unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| "/".into())); + let actual_cwd = + cwd.unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| "/".into())); let runtime = PaneRuntime::spawn(new_id, rows, cols, actual_cwd, self.events.clone())?; self.panes.insert(new_id, PaneState::new()); self.runtimes.insert(new_id, runtime);