aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-04-03 17:33:47 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-04-03 17:33:47 +0300
commite82ae0b32981076ec7360e8d4dd5294a7abc05cf (patch)
treef91cc8eeba8074315482aed84f5bcf6b4b31dbdb /src
parent5ed262a43ac7f264a1a7523b6d4507ba4a976604 (diff)
feat: reuse rename modal for new tab naming
Diffstat (limited to 'src')
-rw-r--r--src/app/input.rs113
-rw-r--r--src/app/mod.rs20
-rw-r--r--src/app/state.rs2
-rw-r--r--src/ui.rs25
4 files changed, 111 insertions, 49 deletions
diff --git a/src/app/input.rs b/src/app/input.rs
index 7191a8fa..73366576 100644
--- a/src/app/input.rs
+++ b/src/app/input.rs
@@ -825,8 +825,7 @@ fn execute_navigate_action(state: &mut AppState, action: NavigateAction) {
}
NavigateAction::RenameWorkspace => {
if !state.workspaces.is_empty() {
- state.name_input = state.workspaces[state.selected].display_name();
- state.mode = Mode::RenameWorkspace;
+ open_rename_workspace(state, state.selected);
}
}
NavigateAction::CloseWorkspace => {
@@ -851,14 +850,7 @@ fn execute_navigate_action(state: &mut AppState, action: NavigateAction) {
state.request_new_tab = true;
leave_navigate_mode(state);
}
- NavigateAction::RenameTab => {
- if let Some(ws) = state.active.and_then(|i| state.workspaces.get(i)) {
- if let Some(name) = ws.active_tab_display_name() {
- state.name_input = name;
- state.mode = Mode::RenameTab;
- }
- }
- }
+ NavigateAction::RenameTab => open_rename_active_tab(state, false),
NavigateAction::PreviousTab => {
state.previous_tab();
leave_navigate_mode(state);
@@ -905,6 +897,23 @@ fn leave_navigate_mode(state: &mut AppState) {
}
}
+fn open_rename_workspace(state: &mut AppState, ws_idx: usize) {
+ state.selected = ws_idx;
+ state.name_input = state.workspaces[ws_idx].display_name();
+ state.name_input_replace_on_type = false;
+ state.mode = Mode::RenameWorkspace;
+}
+
+fn open_rename_active_tab(state: &mut AppState, replace_on_type: bool) {
+ if let Some(ws) = state.active.and_then(|i| state.workspaces.get(i)) {
+ if let Some(name) = ws.active_tab_display_name() {
+ state.name_input = name;
+ state.name_input_replace_on_type = replace_on_type;
+ state.mode = Mode::RenameTab;
+ }
+ }
+}
+
/// Return to the appropriate mode after completing a modal action.
/// Goes to Terminal if a workspace is active, otherwise Navigate.
fn leave_modal(state: &mut AppState) {
@@ -1008,12 +1017,17 @@ fn apply_rename_action(state: &mut AppState, action: ModalAction) {
}
}
state.name_input.clear();
- state.mode = Mode::Navigate;
+ state.name_input_replace_on_type = false;
+ leave_modal(state);
+ }
+ ModalAction::Clear => {
+ state.name_input.clear();
+ state.name_input_replace_on_type = false;
}
- ModalAction::Clear => state.name_input.clear(),
ModalAction::Cancel => {
state.name_input.clear();
- state.mode = Mode::Navigate;
+ state.name_input_replace_on_type = false;
+ leave_modal(state);
}
_ => {}
}
@@ -1027,9 +1041,18 @@ fn handle_rename_key(state: &mut AppState, key: KeyEvent) {
match key.code {
KeyCode::Backspace => {
- state.name_input.pop();
+ if state.name_input_replace_on_type {
+ state.name_input.clear();
+ state.name_input_replace_on_type = false;
+ } else {
+ state.name_input.pop();
+ }
}
KeyCode::Char(c) => {
+ if state.name_input_replace_on_type {
+ state.name_input.clear();
+ state.name_input_replace_on_type = false;
+ }
state.name_input.push(c);
}
_ => {}
@@ -1091,9 +1114,7 @@ fn apply_context_menu_action(state: &mut AppState, menu: ContextMenuState, idx:
let item = menu.items().get(idx).copied();
match (menu.kind, item) {
(ContextMenuKind::Workspace { ws_idx }, Some("Rename")) => {
- state.selected = ws_idx;
- state.name_input = state.workspaces[ws_idx].display_name();
- state.mode = Mode::RenameWorkspace;
+ open_rename_workspace(state, ws_idx);
}
(ContextMenuKind::Workspace { ws_idx }, Some("Close")) => {
state.selected = ws_idx;
@@ -1115,12 +1136,7 @@ fn apply_context_menu_action(state: &mut AppState, menu: ContextMenuState, idx:
state.selected = ws_idx;
state.active = Some(ws_idx);
state.switch_tab(tab_idx);
- if let Some(ws) = state.workspaces.get(ws_idx) {
- if let Some(name) = ws.active_tab_display_name() {
- state.name_input = name;
- state.mode = Mode::RenameTab;
- }
- }
+ open_rename_active_tab(state, false);
}
(ContextMenuKind::Tab { ws_idx, tab_idx }, Some("Close")) => {
state.selected = ws_idx;
@@ -2724,7 +2740,7 @@ mod tests {
&mut state,
KeyEvent::new(KeyCode::Enter, KeyModifiers::empty()),
);
- assert_eq!(state.mode, Mode::Navigate);
+ assert_eq!(state.mode, Mode::Terminal);
assert_eq!(state.workspaces[0].display_name(), "renamed");
state.view.sidebar_rect = Rect::new(0, 0, 26, 20);
@@ -2738,6 +2754,55 @@ mod tests {
}
#[test]
+ fn rename_cancel_returns_to_terminal_when_workspace_is_active() {
+ let mut state = state_with_workspaces(&["test"]);
+ state.mode = Mode::RenameTab;
+ state.name_input = "test".into();
+
+ handle_rename_key(
+ &mut state,
+ KeyEvent::new(KeyCode::Esc, KeyModifiers::empty()),
+ );
+
+ assert_eq!(state.mode, Mode::Terminal);
+ assert!(state.name_input.is_empty());
+ }
+
+ #[test]
+ fn rename_modal_replaces_prefilled_text_on_first_type() {
+ let mut state = state_with_workspaces(&["test"]);
+ state.mode = Mode::RenameTab;
+ state.name_input = "2".into();
+ state.name_input_replace_on_type = true;
+
+ handle_rename_key(
+ &mut state,
+ KeyEvent::new(KeyCode::Char('n'), KeyModifiers::empty()),
+ );
+ assert_eq!(state.name_input, "n");
+ assert!(!state.name_input_replace_on_type);
+
+ handle_rename_key(
+ &mut state,
+ KeyEvent::new(KeyCode::Char('e'), KeyModifiers::empty()),
+ );
+ assert_eq!(state.name_input, "ne");
+ }
+
+ #[test]
+ fn open_rename_active_tab_can_prefill_default_new_tab_name() {
+ let mut state = state_with_workspaces(&["test"]);
+ state.workspaces[0].test_add_tab(None);
+ state.workspaces[0].switch_tab(1);
+
+ open_rename_active_tab(&mut state, true);
+
+ assert_eq!(state.mode, Mode::RenameTab);
+ assert_eq!(state.name_input, "2");
+ assert!(state.name_input_replace_on_type);
+ }
+
+ #[test]
fn confirm_close_keyboard_actions_are_direct_not_focused() {
let mut state = state_with_workspaces(&["a", "b"]);
state.mode = Mode::ConfirmClose;
diff --git a/src/app/mod.rs b/src/app/mod.rs
index 3b744d02..17fc1b5d 100644
--- a/src/app/mod.rs
+++ b/src/app/mod.rs
@@ -193,6 +193,7 @@ impl App {
request_new_tab: false,
request_complete_onboarding: false,
name_input: String::new(),
+ name_input_replace_on_type: false,
onboarding_step: 0,
onboarding_list: state::SelectionListState::new(1),
release_notes: startup_release_notes.map(|notes| state::ReleaseNotesState {
@@ -1822,8 +1823,23 @@ impl App {
.and_then(|ws_idx| self.seed_cwd_from_workspace(ws_idx))
.or_else(|| std::env::current_dir().ok())
.unwrap_or_else(|| std::path::PathBuf::from("/"));
- if let Err(e) = self.create_tab_with_options(initial_cwd, true) {
- error!(err = %e, "failed to create tab");
+ match self.create_tab_with_options(initial_cwd, true) {
+ Ok(_) => {
+ if let Some(ws) = self
+ .state
+ .active
+ .and_then(|ws_idx| self.state.workspaces.get(ws_idx))
+ {
+ if let Some(name) = ws.active_tab_display_name() {
+ self.state.name_input = name;
+ self.state.name_input_replace_on_type = true;
+ self.state.mode = Mode::RenameTab;
+ }
+ }
+ }
+ Err(e) => {
+ error!(err = %e, "failed to create tab");
+ }
}
}
diff --git a/src/app/state.rs b/src/app/state.rs
index dee805cd..43174081 100644
--- a/src/app/state.rs
+++ b/src/app/state.rs
@@ -539,6 +539,7 @@ pub struct AppState {
pub request_new_tab: bool,
pub request_complete_onboarding: bool,
pub name_input: String,
+ pub name_input_replace_on_type: bool,
pub onboarding_step: usize,
pub onboarding_list: SelectionListState,
pub release_notes: Option<ReleaseNotesState>,
@@ -632,6 +633,7 @@ impl AppState {
request_new_tab: false,
request_complete_onboarding: false,
name_input: String::new(),
+ name_input_replace_on_type: false,
onboarding_step: 0,
onboarding_list: SelectionListState::new(1),
release_notes: None,
diff --git a/src/ui.rs b/src/ui.rs
index 7ea3afd0..210b221e 100644
--- a/src/ui.rs
+++ b/src/ui.rs
@@ -98,7 +98,6 @@ pub fn render(app: &AppState, frame: &mut Frame) {
Mode::Resize => render_resize_overlay(app, frame, terminal_area),
Mode::ConfirmClose => render_confirm_close_overlay(app, frame, terminal_area),
Mode::ContextMenu => {
- render_navigate_overlay(app, frame, terminal_area);
render_context_menu(app, frame);
}
Mode::Settings => render_settings_overlay(app, frame, frame.area()),
@@ -326,17 +325,7 @@ fn compute_sidebar_width(app: &AppState) -> u16 {
/// Collapsed sidebar: pure glance mode.
fn render_sidebar_collapsed(app: &AppState, frame: &mut Frame, area: Rect) {
- let is_navigating = matches!(
- app.mode,
- Mode::Navigate
- | Mode::RenameWorkspace
- | Mode::Resize
- | Mode::ConfirmClose
- | Mode::ContextMenu
- | Mode::Settings
- | Mode::GlobalMenu
- | Mode::KeybindHelp
- );
+ let is_navigating = matches!(app.mode, Mode::Navigate);
let p = &app.palette;
let sep_style = if is_navigating {
@@ -395,17 +384,7 @@ fn render_sidebar_collapsed(app: &AppState, frame: &mut Frame, area: Rect) {
fn render_sidebar(app: &AppState, frame: &mut Frame, area: Rect) {
let p = &app.palette;
- let is_navigating = matches!(
- app.mode,
- Mode::Navigate
- | Mode::RenameWorkspace
- | Mode::Resize
- | Mode::ConfirmClose
- | Mode::ContextMenu
- | Mode::Settings
- | Mode::GlobalMenu
- | Mode::KeybindHelp
- );
+ let is_navigating = matches!(app.mode, Mode::Navigate);
let sep_style = if is_navigating {
Style::default().fg(p.accent)
} else {