feat: add optional workspace name prompt
This commit is contained in:
parent
13ce20c4f5
commit
e298e45890
|
|
@ -640,6 +640,12 @@
|
|||
"default": "true",
|
||||
"description": "Ask for a tab name before creating a new tab."
|
||||
},
|
||||
{
|
||||
"key": "ui.prompt_new_workspace_name",
|
||||
"type": "boolean",
|
||||
"default": "false",
|
||||
"description": "Ask for a workspace name before interactive TUI creation."
|
||||
},
|
||||
{
|
||||
"key": "ui.pane_borders",
|
||||
"type": "boolean",
|
||||
|
|
|
|||
|
|
@ -76,6 +76,33 @@ impl App {
|
|||
})
|
||||
}
|
||||
|
||||
pub(super) fn begin_tui_workspace_create(&mut self, request_id: &'static str) {
|
||||
if self.state.prompt_new_workspace_name {
|
||||
let follow_cwd = self.workspace_creation_source().and_then(|ws_idx| {
|
||||
self.focused_pane_cwd_in_workspace(ws_idx)
|
||||
.or_else(|| self.seed_cwd_from_workspace(ws_idx))
|
||||
});
|
||||
let cwd = self.resolve_new_terminal_cwd(follow_cwd);
|
||||
super::input::open_new_workspace_dialog(&mut self.state, cwd);
|
||||
return;
|
||||
}
|
||||
|
||||
self.runtime_workspace_create(
|
||||
request_id,
|
||||
crate::api::schema::WorkspaceCreateParams {
|
||||
cwd: None,
|
||||
focus: true,
|
||||
label: None,
|
||||
env: Default::default(),
|
||||
},
|
||||
);
|
||||
self.state.mode = if self.state.active.is_some() {
|
||||
Mode::Terminal
|
||||
} else {
|
||||
Mode::Navigate
|
||||
};
|
||||
}
|
||||
|
||||
/// Create a workspace with a real PTY (needs event_tx).
|
||||
#[cfg(test)]
|
||||
pub(crate) fn create_workspace(&mut self) {
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ mod terminal;
|
|||
pub(crate) use self::{
|
||||
modal::{
|
||||
handle_global_menu_key, handle_keybind_help_key, handle_navigator_key,
|
||||
insert_navigator_search_text, insert_rename_input_text,
|
||||
insert_navigator_search_text, insert_rename_input_text, open_new_workspace_dialog,
|
||||
},
|
||||
navigate::{
|
||||
terminal_direct_indexed_navigation_action, terminal_direct_non_indexed_navigation_action,
|
||||
|
|
@ -313,6 +313,9 @@ impl App {
|
|||
}
|
||||
if let Some(action) = self.state.handle_mouse(&mut self.terminal_runtimes, mouse) {
|
||||
match action {
|
||||
MouseAction::NewWorkspace => {
|
||||
self.begin_tui_workspace_create("tui.mouse.workspace.create")
|
||||
}
|
||||
MouseAction::Settings(action) => match action {
|
||||
SettingsAction::SaveTheme(name) => self.save_theme(&name),
|
||||
SettingsAction::SaveSound(enabled) => self.save_sound(enabled),
|
||||
|
|
|
|||
|
|
@ -302,6 +302,7 @@ pub(super) fn open_rename_workspace(
|
|||
terminal_runtimes: &crate::terminal::TerminalRuntimeRegistry,
|
||||
ws_idx: usize,
|
||||
) {
|
||||
state.pending_workspace_create_cwd = None;
|
||||
state.selected = ws_idx;
|
||||
state.rename_pane_target = None;
|
||||
state.name_input =
|
||||
|
|
@ -310,9 +311,21 @@ pub(super) fn open_rename_workspace(
|
|||
state.mode = Mode::RenameWorkspace;
|
||||
}
|
||||
|
||||
pub(crate) fn open_new_workspace_dialog(state: &mut AppState, cwd: std::path::PathBuf) {
|
||||
let suggested_name = crate::workspace::derive_label_from_cwd(&cwd);
|
||||
state.creating_new_tab = false;
|
||||
state.requested_new_tab_name = None;
|
||||
state.pending_workspace_create_cwd = Some(cwd);
|
||||
state.rename_pane_target = None;
|
||||
state.name_input = suggested_name;
|
||||
state.name_input_replace_on_type = true;
|
||||
state.mode = Mode::RenameWorkspace;
|
||||
}
|
||||
|
||||
pub(super) fn open_rename_active_tab(state: &mut AppState, replace_on_type: bool) {
|
||||
state.creating_new_tab = false;
|
||||
state.requested_new_tab_name = None;
|
||||
state.pending_workspace_create_cwd = None;
|
||||
state.rename_pane_target = None;
|
||||
if let Some(ws) = state.active.and_then(|i| state.workspaces.get(i)) {
|
||||
if let Some(name) = ws.active_tab_display_name() {
|
||||
|
|
@ -333,6 +346,7 @@ pub(super) fn open_rename_pane(state: &mut AppState, pane_id: crate::layout::Pan
|
|||
let terminal = state.terminals.get(&pane.attached_terminal_id);
|
||||
state.creating_new_tab = false;
|
||||
state.requested_new_tab_name = None;
|
||||
state.pending_workspace_create_cwd = None;
|
||||
state.rename_pane_target = Some(pane_id);
|
||||
state.name_input = terminal
|
||||
.and_then(|t| t.manual_label.clone())
|
||||
|
|
@ -341,6 +355,11 @@ pub(super) fn open_rename_pane(state: &mut AppState, pane_id: crate::layout::Pan
|
|||
state.mode = Mode::RenamePane;
|
||||
}
|
||||
|
||||
fn workspace_create_label(input: &str, suggested_name: &str) -> Option<String> {
|
||||
let name = input.trim();
|
||||
(!name.is_empty() && name != suggested_name).then(|| name.to_string())
|
||||
}
|
||||
|
||||
fn next_new_tab_default_name(state: &AppState) -> String {
|
||||
state
|
||||
.active
|
||||
|
|
@ -352,6 +371,7 @@ fn next_new_tab_default_name(state: &AppState) -> String {
|
|||
pub(super) fn open_new_tab_dialog(state: &mut AppState) {
|
||||
state.creating_new_tab = true;
|
||||
state.requested_new_tab_name = None;
|
||||
state.pending_workspace_create_cwd = None;
|
||||
state.rename_pane_target = None;
|
||||
state.name_input = next_new_tab_default_name(state);
|
||||
state.name_input_replace_on_type = true;
|
||||
|
|
@ -423,7 +443,11 @@ pub(super) fn apply_rename_action(state: &mut AppState, action: ModalAction) {
|
|||
state.name_input.trim().to_string()
|
||||
};
|
||||
match state.mode {
|
||||
Mode::RenameWorkspace if !state.workspaces.is_empty() && !new_name.is_empty() => {
|
||||
Mode::RenameWorkspace
|
||||
if state.pending_workspace_create_cwd.is_none()
|
||||
&& !state.workspaces.is_empty()
|
||||
&& !new_name.is_empty() =>
|
||||
{
|
||||
let workspace_id = state.workspaces[state.selected].id.clone();
|
||||
state.workspaces[state.selected].set_custom_name(new_name);
|
||||
crate::logging::workspace_renamed(&workspace_id);
|
||||
|
|
@ -487,6 +511,7 @@ pub(super) fn apply_rename_action(state: &mut AppState, action: ModalAction) {
|
|||
_ => {}
|
||||
}
|
||||
state.creating_new_tab = false;
|
||||
state.pending_workspace_create_cwd = None;
|
||||
state.rename_pane_target = None;
|
||||
state.name_input.clear();
|
||||
state.name_input_replace_on_type = false;
|
||||
|
|
@ -499,6 +524,7 @@ pub(super) fn apply_rename_action(state: &mut AppState, action: ModalAction) {
|
|||
ModalAction::Cancel => {
|
||||
state.creating_new_tab = false;
|
||||
state.requested_new_tab_name = None;
|
||||
state.pending_workspace_create_cwd = None;
|
||||
state.rename_pane_target = None;
|
||||
state.name_input.clear();
|
||||
state.name_input_replace_on_type = false;
|
||||
|
|
@ -916,15 +942,29 @@ impl App {
|
|||
};
|
||||
|
||||
match self.state.mode {
|
||||
Mode::RenameWorkspace if !self.state.workspaces.is_empty() && !new_name.is_empty() => {
|
||||
let workspace_id = self.public_workspace_id(self.state.selected);
|
||||
self.runtime_workspace_rename(
|
||||
"tui.workspace.rename",
|
||||
crate::api::schema::WorkspaceRenameParams {
|
||||
workspace_id,
|
||||
label: new_name,
|
||||
},
|
||||
);
|
||||
Mode::RenameWorkspace => {
|
||||
if let Some(cwd) = self.state.pending_workspace_create_cwd.take() {
|
||||
let suggested_name = crate::workspace::derive_label_from_cwd(&cwd);
|
||||
let label = workspace_create_label(&new_name, &suggested_name);
|
||||
self.runtime_workspace_create(
|
||||
"tui.workspace.create_named",
|
||||
crate::api::schema::WorkspaceCreateParams {
|
||||
cwd: Some(cwd.display().to_string()),
|
||||
focus: true,
|
||||
label,
|
||||
env: Default::default(),
|
||||
},
|
||||
);
|
||||
} else if !self.state.workspaces.is_empty() && !new_name.is_empty() {
|
||||
let workspace_id = self.public_workspace_id(self.state.selected);
|
||||
self.runtime_workspace_rename(
|
||||
"tui.workspace.rename",
|
||||
crate::api::schema::WorkspaceRenameParams {
|
||||
workspace_id,
|
||||
label: new_name,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Mode::RenameTab if self.state.creating_new_tab => {
|
||||
let default_name = next_new_tab_default_name(&self.state);
|
||||
|
|
@ -1256,6 +1296,7 @@ impl App {
|
|||
fn cancel_rename_modal(state: &mut AppState) {
|
||||
state.creating_new_tab = false;
|
||||
state.requested_new_tab_name = None;
|
||||
state.pending_workspace_create_cwd = None;
|
||||
state.rename_pane_target = None;
|
||||
state.name_input.clear();
|
||||
state.name_input_replace_on_type = false;
|
||||
|
|
@ -1318,6 +1359,17 @@ mod tests {
|
|||
app
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_create_label_preserves_auto_name_for_suggestion_or_blank() {
|
||||
assert_eq!(workspace_create_label("project", "project"), None);
|
||||
assert_eq!(workspace_create_label("", "project"), None);
|
||||
assert_eq!(workspace_create_label(" ", "project"), None);
|
||||
assert_eq!(
|
||||
workspace_create_label(" logs ", "project").as_deref(),
|
||||
Some("logs")
|
||||
);
|
||||
}
|
||||
|
||||
fn mark_worktree_space_member(state: &mut AppState, ws_idx: usize, key: &str) {
|
||||
state.workspaces[ws_idx].worktree_space = Some(crate::workspace::WorktreeSpaceMembership {
|
||||
key: key.into(),
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ use super::{
|
|||
};
|
||||
|
||||
pub(super) enum MouseAction {
|
||||
NewWorkspace,
|
||||
Settings(SettingsAction),
|
||||
FocusWorkspace {
|
||||
ws_idx: usize,
|
||||
|
|
@ -535,8 +536,7 @@ impl AppState {
|
|||
&& mouse.column >= new_button.x
|
||||
&& mouse.column < new_button.x + new_button.width;
|
||||
if on_new_button {
|
||||
self.request_new_workspace = true;
|
||||
return None;
|
||||
return Some(MouseAction::NewWorkspace);
|
||||
}
|
||||
|
||||
if let Some(target) =
|
||||
|
|
@ -1152,7 +1152,7 @@ impl AppState {
|
|||
|
||||
match crate::ui::mobile_switcher_target_at(self, mouse.column, mouse.row) {
|
||||
Some(crate::ui::MobileSwitcherTarget::NewWorkspace) => {
|
||||
self.request_new_workspace = true;
|
||||
return MobileMouseResult::Action(MouseAction::NewWorkspace);
|
||||
}
|
||||
Some(crate::ui::MobileSwitcherTarget::Workspace(ws_idx)) => {
|
||||
self.mode = Mode::Terminal;
|
||||
|
|
@ -3628,7 +3628,82 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn mobile_switcher_action_rows_create_workspace_and_open_tab_dialog() {
|
||||
fn mobile_switcher_new_workspace_opens_prompt_when_enabled() {
|
||||
let mut app = app_for_mouse_test();
|
||||
app.state.workspaces = vec![Workspace::test_new("one")];
|
||||
app.state.active = Some(0);
|
||||
app.state.selected = 0;
|
||||
app.state.mode = Mode::Terminal;
|
||||
app.state.prompt_new_workspace_name = true;
|
||||
|
||||
crate::ui::compute_view(&mut app.state, Rect::new(0, 0, 44, 20));
|
||||
let switch = app.state.view.mobile_menu_hit_area;
|
||||
app.handle_mouse(mouse(
|
||||
MouseEventKind::Down(MouseButton::Left),
|
||||
switch.x + 1,
|
||||
switch.y + 1,
|
||||
));
|
||||
let viewport = crate::ui::mobile_switcher_areas(&app.state).viewport;
|
||||
app.handle_mouse(mouse(
|
||||
MouseEventKind::Down(MouseButton::Left),
|
||||
viewport.x + 2,
|
||||
viewport.y + 1,
|
||||
));
|
||||
|
||||
assert_eq!(app.state.mode, Mode::RenameWorkspace);
|
||||
assert!(app.state.pending_workspace_create_cwd.is_some());
|
||||
assert!(app.state.name_input_replace_on_type);
|
||||
assert_eq!(app.state.workspaces.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn desktop_new_workspace_opens_prompt_when_enabled() {
|
||||
let mut app = app_for_mouse_test();
|
||||
app.state.workspaces = vec![Workspace::test_new("one")];
|
||||
app.state.active = Some(0);
|
||||
app.state.selected = 0;
|
||||
app.state.mode = Mode::Terminal;
|
||||
app.state.prompt_new_workspace_name = true;
|
||||
|
||||
crate::ui::compute_view(&mut app.state, Rect::new(0, 0, 120, 40));
|
||||
let new_workspace = app.state.sidebar_new_button_rect();
|
||||
app.handle_mouse(mouse(
|
||||
MouseEventKind::Down(MouseButton::Left),
|
||||
new_workspace.x + 1,
|
||||
new_workspace.y,
|
||||
));
|
||||
|
||||
assert_eq!(app.state.mode, Mode::RenameWorkspace);
|
||||
assert!(app.state.pending_workspace_create_cwd.is_some());
|
||||
assert!(app.state.name_input_replace_on_type);
|
||||
assert_eq!(app.state.workspaces.len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn desktop_new_workspace_creates_immediately_by_default() {
|
||||
let mut app = app_for_mouse_test();
|
||||
app.state.workspaces = vec![Workspace::test_new("one")];
|
||||
app.state.ensure_test_terminals();
|
||||
app.state.active = Some(0);
|
||||
app.state.selected = 0;
|
||||
app.state.mode = Mode::Terminal;
|
||||
|
||||
crate::ui::compute_view(&mut app.state, Rect::new(0, 0, 120, 40));
|
||||
let new_workspace = app.state.sidebar_new_button_rect();
|
||||
app.handle_mouse(mouse(
|
||||
MouseEventKind::Down(MouseButton::Left),
|
||||
new_workspace.x + 1,
|
||||
new_workspace.y,
|
||||
));
|
||||
|
||||
assert_eq!(app.state.workspaces.len(), 2);
|
||||
assert_eq!(app.state.mode, Mode::Terminal);
|
||||
assert!(app.state.pending_workspace_create_cwd.is_none());
|
||||
crate::app::api::test_support::shutdown_test_runtimes(&mut app);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mobile_switcher_new_tab_opens_dialog_when_enabled() {
|
||||
let mut app = app_for_mouse_test();
|
||||
let mut ws = Workspace::test_new("one");
|
||||
ws.test_add_tab(Some("logs"));
|
||||
|
|
@ -3645,21 +3720,12 @@ mod tests {
|
|||
switch.y + 1,
|
||||
));
|
||||
let viewport = crate::ui::mobile_switcher_areas(&app.state).viewport;
|
||||
|
||||
app.handle_mouse(mouse(
|
||||
MouseEventKind::Down(MouseButton::Left),
|
||||
viewport.x + 2,
|
||||
viewport.y + 1,
|
||||
));
|
||||
assert!(app.state.request_new_workspace);
|
||||
|
||||
app.state.request_new_workspace = false;
|
||||
app.state.mode = Mode::Navigate;
|
||||
app.handle_mouse(mouse(
|
||||
MouseEventKind::Down(MouseButton::Left),
|
||||
viewport.x + 2,
|
||||
viewport.y + 5,
|
||||
));
|
||||
|
||||
assert_eq!(app.state.mode, Mode::RenameTab);
|
||||
assert!(app.state.creating_new_tab);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -184,16 +184,7 @@ impl App {
|
|||
let previous_mode = self.state.mode;
|
||||
match action {
|
||||
NavigateAction::NewWorkspace => {
|
||||
self.runtime_workspace_create(
|
||||
"tui.key.workspace.create",
|
||||
crate::api::schema::WorkspaceCreateParams {
|
||||
cwd: None,
|
||||
focus: true,
|
||||
label: None,
|
||||
env: Default::default(),
|
||||
},
|
||||
);
|
||||
leave_navigate_mode(&mut self.state);
|
||||
self.begin_tui_workspace_create("tui.key.workspace.create");
|
||||
}
|
||||
NavigateAction::NewWorktree => {
|
||||
if let Some(ws_idx) = workspace_action_target(&self.state, context).filter(|idx| {
|
||||
|
|
@ -1999,6 +1990,74 @@ mod tests {
|
|||
assert_eq!(state.mode, Mode::Terminal);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn new_workspace_key_opens_prefilled_prompt_and_preserves_captured_cwd() {
|
||||
let cwd = unique_temp_path("workspace-name-suggestion");
|
||||
std::fs::create_dir_all(&cwd).unwrap();
|
||||
let suggested_name = crate::workspace::derive_label_from_cwd(&cwd);
|
||||
let mut app = app_with_test_workspaces(&["test"]);
|
||||
app.state.new_terminal_cwd =
|
||||
crate::config::NewTerminalCwdConfig::Path(cwd.display().to_string());
|
||||
app.state.prompt_new_workspace_name = true;
|
||||
app.state.mode = Mode::Navigate;
|
||||
app.state.keybinds.new_workspace = crate::config::ActionKeybinds::prefix("g");
|
||||
|
||||
app.handle_navigate_key(TerminalKey::new(KeyCode::Char('g'), KeyModifiers::empty()));
|
||||
|
||||
assert_eq!(app.state.mode, Mode::RenameWorkspace);
|
||||
assert_eq!(app.state.name_input, suggested_name);
|
||||
assert!(app.state.name_input_replace_on_type);
|
||||
assert_eq!(app.state.pending_workspace_create_cwd.as_ref(), Some(&cwd));
|
||||
assert_eq!(app.state.workspaces.len(), 1);
|
||||
|
||||
app.state.new_terminal_cwd =
|
||||
crate::config::NewTerminalCwdConfig::Path("/tmp/changed-after-prompt".into());
|
||||
app.handle_rename_key_via_api(KeyEvent::new(KeyCode::Enter, KeyModifiers::empty()));
|
||||
|
||||
assert_eq!(app.state.workspaces.len(), 2);
|
||||
assert_eq!(app.state.workspaces[1].identity_cwd, cwd);
|
||||
assert!(app.state.workspaces[1].custom_name.is_none());
|
||||
assert!(app.state.pending_workspace_create_cwd.is_none());
|
||||
assert_eq!(app.state.mode, Mode::Terminal);
|
||||
crate::app::api::test_support::shutdown_test_runtimes(&mut app);
|
||||
let _ = std::fs::remove_dir_all(&cwd);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn new_workspace_prompt_saves_custom_name_atomically() {
|
||||
let cwd = unique_temp_path("workspace-custom-name");
|
||||
std::fs::create_dir_all(&cwd).unwrap();
|
||||
let mut app = app_with_test_workspaces(&["test"]);
|
||||
app.state.new_terminal_cwd =
|
||||
crate::config::NewTerminalCwdConfig::Path(cwd.display().to_string());
|
||||
app.state.prompt_new_workspace_name = true;
|
||||
app.state.mode = Mode::Navigate;
|
||||
|
||||
app.execute_tui_navigate_action(NavigateAction::NewWorkspace, ActionContext::Navigate);
|
||||
app.state.name_input = " logs ".into();
|
||||
app.handle_rename_key_via_api(KeyEvent::new(KeyCode::Enter, KeyModifiers::empty()));
|
||||
|
||||
assert_eq!(app.state.workspaces.len(), 2);
|
||||
assert_eq!(app.state.workspaces[1].custom_name.as_deref(), Some("logs"));
|
||||
assert_eq!(app.state.workspaces[1].identity_cwd, cwd);
|
||||
crate::app::api::test_support::shutdown_test_runtimes(&mut app);
|
||||
let _ = std::fs::remove_dir_all(&cwd);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancelling_new_workspace_prompt_creates_nothing() {
|
||||
let mut app = app_with_test_workspaces(&["test"]);
|
||||
app.state.prompt_new_workspace_name = true;
|
||||
app.state.mode = Mode::Navigate;
|
||||
|
||||
app.execute_tui_navigate_action(NavigateAction::NewWorkspace, ActionContext::Navigate);
|
||||
app.handle_rename_key_via_api(KeyEvent::new(KeyCode::Esc, KeyModifiers::empty()));
|
||||
|
||||
assert_eq!(app.state.workspaces.len(), 1);
|
||||
assert!(app.state.pending_workspace_create_cwd.is_none());
|
||||
assert_eq!(app.state.mode, Mode::Terminal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_new_worktree_key_requests_selected_workspace() {
|
||||
let mut state = state_with_workspaces(&["main", "scratch"]);
|
||||
|
|
|
|||
|
|
@ -534,6 +534,7 @@ impl App {
|
|||
request_clipboard_write: None,
|
||||
creating_new_tab: false,
|
||||
requested_new_tab_name: None,
|
||||
pending_workspace_create_cwd: None,
|
||||
rename_pane_target: None,
|
||||
worktree_create: None,
|
||||
worktree_open: None,
|
||||
|
|
@ -617,6 +618,7 @@ impl App {
|
|||
mouse_scroll_lines: config.ui.mouse_scroll_lines(),
|
||||
confirm_close: config.ui.confirm_close,
|
||||
prompt_new_tab_name: config.ui.prompt_new_tab_name,
|
||||
prompt_new_workspace_name: config.ui.prompt_new_workspace_name,
|
||||
pane_borders: config.ui.pane_borders,
|
||||
pane_gaps: config.ui.pane_gaps,
|
||||
show_agent_labels_on_pane_borders: config.ui.show_agent_labels_on_pane_borders,
|
||||
|
|
@ -1151,7 +1153,10 @@ impl App {
|
|||
}
|
||||
|
||||
pub(crate) fn ensure_default_workspace(&mut self) -> bool {
|
||||
if !self.state.workspaces.is_empty() || self.state.mode == Mode::Onboarding {
|
||||
if !self.state.workspaces.is_empty()
|
||||
|| self.state.mode == Mode::Onboarding
|
||||
|| self.state.pending_workspace_create_cwd.is_some()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -1405,6 +1410,7 @@ impl App {
|
|||
config.ui.right_click_passthrough_modifiers();
|
||||
self.state.confirm_close = config.ui.confirm_close;
|
||||
self.state.prompt_new_tab_name = config.ui.prompt_new_tab_name;
|
||||
self.state.prompt_new_workspace_name = config.ui.prompt_new_workspace_name;
|
||||
self.state.pane_borders = config.ui.pane_borders;
|
||||
self.state.pane_gaps = config.ui.pane_gaps;
|
||||
self.state.show_agent_labels_on_pane_borders =
|
||||
|
|
@ -2385,6 +2391,34 @@ mod tests {
|
|||
assert!(!app.state.redraw_on_focus_gained);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_name_prompt_suppresses_default_creation_while_pending() {
|
||||
let mut app = test_app();
|
||||
app.state.prompt_new_workspace_name = true;
|
||||
|
||||
app.begin_tui_workspace_create("test.workspace.create");
|
||||
|
||||
assert_eq!(app.state.mode, Mode::RenameWorkspace);
|
||||
assert!(app.state.pending_workspace_create_cwd.is_some());
|
||||
assert!(!app.ensure_default_workspace());
|
||||
assert!(app.state.workspaces.is_empty());
|
||||
|
||||
app.handle_rename_key_via_api(KeyEvent::new(KeyCode::Esc, KeyModifiers::empty()));
|
||||
assert!(app.state.workspaces.is_empty());
|
||||
assert!(app.state.pending_workspace_create_cwd.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_uses_workspace_name_prompt_config() {
|
||||
let mut config = Config::default();
|
||||
config.ui.prompt_new_workspace_name = true;
|
||||
let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
|
||||
let app = App::new(&config, true, None, api_rx, crate::api::EventHub::default());
|
||||
|
||||
assert!(app.state.prompt_new_workspace_name);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn theme_auto_switch_is_opt_in_and_preserves_manual_default() {
|
||||
let mut config = Config::default();
|
||||
|
|
@ -2571,7 +2605,7 @@ mod tests {
|
|||
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
std::fs::write(
|
||||
&path,
|
||||
"[terminal]\ndefault_shell = \"nu\"\nshell_mode = \"non_login\"\nnew_cwd = \"home\"\n[keys]\nnew_workspace = \"prefix+m\"\nprefix = \"ctrl+a\"\n[update]\nversion_check = false\nmanifest_check = false\n[ui]\nagent_panel_scope = \"current\"\nagent_panel_sort = \"priority\"\nredraw_on_focus_gained = false\ncopy_on_select = false\nright_click_passthrough_modifier = \"ctrl\"\n[ui.toast]\ndelivery = \"herdr\"\n[experimental]\nswitch_ascii_input_source_in_prefix = true\n",
|
||||
"[terminal]\ndefault_shell = \"nu\"\nshell_mode = \"non_login\"\nnew_cwd = \"home\"\n[keys]\nnew_workspace = \"prefix+m\"\nprefix = \"ctrl+a\"\n[update]\nversion_check = false\nmanifest_check = false\n[ui]\nagent_panel_scope = \"current\"\nagent_panel_sort = \"priority\"\nredraw_on_focus_gained = false\ncopy_on_select = false\nright_click_passthrough_modifier = \"ctrl\"\nprompt_new_workspace_name = true\n[ui.toast]\ndelivery = \"herdr\"\n[experimental]\nswitch_ascii_input_source_in_prefix = true\n",
|
||||
)
|
||||
.unwrap();
|
||||
std::env::set_var(crate::config::CONFIG_PATH_ENV_VAR, &path);
|
||||
|
|
@ -2619,6 +2653,7 @@ mod tests {
|
|||
assert_eq!(app.state.agent_panel_sort, state::AgentPanelSort::Priority);
|
||||
assert!(!app.state.redraw_on_focus_gained);
|
||||
assert!(!app.state.copy_on_select);
|
||||
assert!(app.state.prompt_new_workspace_name);
|
||||
assert!(app.state.selection.is_some());
|
||||
assert!(app.state.selection_autoscroll.is_some());
|
||||
assert_eq!(app.selection_autoscroll_deadline, Some(selection_deadline));
|
||||
|
|
|
|||
|
|
@ -1394,6 +1394,7 @@ pub struct AppState {
|
|||
pub request_clipboard_write: Option<Vec<u8>>,
|
||||
pub creating_new_tab: bool,
|
||||
pub requested_new_tab_name: Option<String>,
|
||||
pub pending_workspace_create_cwd: Option<std::path::PathBuf>,
|
||||
pub rename_pane_target: Option<PaneId>,
|
||||
pub worktree_create: Option<WorktreeCreateState>,
|
||||
pub worktree_open: Option<WorktreeOpenState>,
|
||||
|
|
@ -1461,6 +1462,7 @@ pub struct AppState {
|
|||
pub mouse_scroll_lines: usize,
|
||||
pub confirm_close: bool,
|
||||
pub prompt_new_tab_name: bool,
|
||||
pub prompt_new_workspace_name: bool,
|
||||
pub pane_borders: bool,
|
||||
pub pane_gaps: bool,
|
||||
pub show_agent_labels_on_pane_borders: bool,
|
||||
|
|
@ -1760,6 +1762,7 @@ impl AppState {
|
|||
request_clipboard_write: None,
|
||||
creating_new_tab: false,
|
||||
requested_new_tab_name: None,
|
||||
pending_workspace_create_cwd: None,
|
||||
rename_pane_target: None,
|
||||
worktree_create: None,
|
||||
worktree_open: None,
|
||||
|
|
@ -1834,6 +1837,7 @@ impl AppState {
|
|||
mouse_scroll_lines: crate::config::DEFAULT_MOUSE_SCROLL_LINES,
|
||||
confirm_close: true,
|
||||
prompt_new_tab_name: true,
|
||||
prompt_new_workspace_name: false,
|
||||
pane_borders: true,
|
||||
pane_gaps: false,
|
||||
show_agent_labels_on_pane_borders: false,
|
||||
|
|
|
|||
|
|
@ -802,6 +802,8 @@ pub struct UiConfig {
|
|||
pub confirm_close: bool,
|
||||
/// Ask for a tab name before creating a new tab. Default: true.
|
||||
pub prompt_new_tab_name: bool,
|
||||
/// Ask for a workspace name before interactive creation. Default: false.
|
||||
pub prompt_new_workspace_name: bool,
|
||||
/// Draw borders around split panes. Default: true.
|
||||
pub pane_borders: bool,
|
||||
/// Keep split panes visually separated instead of sharing divider borders. Default: true.
|
||||
|
|
@ -1003,6 +1005,7 @@ impl Default for UiConfig {
|
|||
mouse_scroll_lines: None,
|
||||
confirm_close: true,
|
||||
prompt_new_tab_name: true,
|
||||
prompt_new_workspace_name: false,
|
||||
pane_borders: true,
|
||||
pane_gaps: true,
|
||||
show_agent_labels_on_pane_borders: false,
|
||||
|
|
@ -1279,6 +1282,19 @@ prompt_new_tab_name = false
|
|||
assert!(!config.ui.prompt_new_tab_name);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_new_workspace_name_defaults_off_and_parses() {
|
||||
let default_config = Config::default();
|
||||
assert!(!default_config.ui.prompt_new_workspace_name);
|
||||
|
||||
let toml = r#"
|
||||
[ui]
|
||||
prompt_new_workspace_name = true
|
||||
"#;
|
||||
let config: Config = toml::from_str(toml).unwrap();
|
||||
assert!(config.ui.prompt_new_workspace_name);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reveal_hidden_cursor_for_cjk_ime_default_off_and_parse() {
|
||||
let default_config = Config::default();
|
||||
|
|
|
|||
|
|
@ -297,6 +297,9 @@ const DEFAULT_CONFIG: &str = r##"# herdr configuration
|
|||
# Set false to create tabs immediately with generated names.
|
||||
# prompt_new_tab_name = true
|
||||
|
||||
# Ask for a workspace name before interactive creation.
|
||||
# prompt_new_workspace_name = false
|
||||
|
||||
# Draw borders around split panes.
|
||||
# pane_borders = true
|
||||
|
||||
|
|
|
|||
20
src/ui.rs
20
src/ui.rs
|
|
@ -624,6 +624,26 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_creation_dialog_renders_new_workspace_title() {
|
||||
let mut app = crate::app::state::AppState::test_new();
|
||||
app.mode = Mode::RenameWorkspace;
|
||||
app.pending_workspace_create_cwd = Some("/tmp/project".into());
|
||||
app.name_input = "project".into();
|
||||
|
||||
let area = Rect::new(0, 0, 80, 20);
|
||||
compute_view(&mut app, area);
|
||||
let mut terminal = Terminal::new(TestBackend::new(area.width, area.height)).unwrap();
|
||||
terminal.draw(|frame| render(&app, frame)).unwrap();
|
||||
let screen = (0..area.height)
|
||||
.map(|row| buffer_row_text(terminal.backend().buffer(), area, row))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
assert!(screen.contains("new workspace"), "{screen}");
|
||||
assert!(screen.contains("project"), "{screen}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn focused_pane_cursor_wins_during_terminal_render() {
|
||||
let mut app = crate::app::state::AppState::test_new();
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ pub(super) fn render_rename_overlay(app: &AppState, frame: &mut Frame, area: Rec
|
|||
super::dim_background(frame, area);
|
||||
|
||||
let title = match app.mode {
|
||||
Mode::RenameWorkspace if app.pending_workspace_create_cwd.is_some() => "new workspace",
|
||||
Mode::RenameWorkspace => "rename workspace",
|
||||
Mode::RenameTab if app.creating_new_tab => "new tab",
|
||||
Mode::RenameTab => "rename tab",
|
||||
|
|
|
|||
Loading…
Reference in New Issue