From 2d2abd55713a088b5833e2f2126d8ea16366dfcf Mon Sep 17 00:00:00 2001 From: Kang Date: Mon, 27 Jul 2026 13:30:06 +0800 Subject: [PATCH] feat(sync): use real text inputs for group editing --- .../src/features/app_state/construct.rs | 4 - .../src/features/app_state/mod.rs | 4 - .../features/panels/sync_groups_overlay.rs | 131 ++++++---------- .../src/features/sync_input.rs | 141 ++++++++---------- .../terminal_selection_runtime/helpers.rs | 98 ------------ .../src/features/text_inputs.rs | 4 + 6 files changed, 112 insertions(+), 270 deletions(-) diff --git a/crates/nyaterm-desktop/src/features/app_state/construct.rs b/crates/nyaterm-desktop/src/features/app_state/construct.rs index 1e5bf3f4d..d680c75b7 100644 --- a/crates/nyaterm-desktop/src/features/app_state/construct.rs +++ b/crates/nyaterm-desktop/src/features/app_state/construct.rs @@ -444,10 +444,6 @@ impl NyaTermApp { sync_groups_open: false, sync_groups_focus: cx.focus_handle(), sync_groups_search_draft: String::new(), - sync_groups_search_marked_text: String::new(), - sync_groups_name_marked_text: String::new(), - sync_groups_search_focus: cx.focus_handle(), - sync_groups_name_focus: cx.focus_handle(), sync_groups_selected_id: None, sync_groups_delete_pending: None, broadcast_to_all: false, diff --git a/crates/nyaterm-desktop/src/features/app_state/mod.rs b/crates/nyaterm-desktop/src/features/app_state/mod.rs index a851c45ec..1a70c3036 100644 --- a/crates/nyaterm-desktop/src/features/app_state/mod.rs +++ b/crates/nyaterm-desktop/src/features/app_state/mod.rs @@ -114,10 +114,6 @@ pub struct NyaTermApp { pub(in crate::features) sync_groups_open: bool, pub(in crate::features) sync_groups_focus: FocusHandle, pub(in crate::features) sync_groups_search_draft: String, - pub(in crate::features) sync_groups_search_marked_text: String, - pub(in crate::features) sync_groups_name_marked_text: String, - pub(in crate::features) sync_groups_search_focus: FocusHandle, - pub(in crate::features) sync_groups_name_focus: FocusHandle, pub(in crate::features) sync_groups_selected_id: Option, pub(in crate::features) sync_groups_delete_pending: Option, /// Broadcast keyboard input to every live session (Tauri broadcastToAll). diff --git a/crates/nyaterm-desktop/src/features/panels/sync_groups_overlay.rs b/crates/nyaterm-desktop/src/features/panels/sync_groups_overlay.rs index 6bef814b2..68214beeb 100644 --- a/crates/nyaterm-desktop/src/features/panels/sync_groups_overlay.rs +++ b/crates/nyaterm-desktop/src/features/panels/sync_groups_overlay.rs @@ -10,24 +10,27 @@ impl NyaTermApp { let dialog_width = (viewport_w - 24.).clamp(280., 900.); let dialog_height = (viewport_h - 56.).clamp(280., 500.); let groups_width = (dialog_width * 0.23).clamp(160., 208.); - let search_input_entity = cx.entity(); - let name_input_entity = search_input_entity.clone(); let selected_group = self.selected_sync_group().cloned(); let selected_group_id = selected_group.as_ref().map(|group| group.id.clone()); - let selected_group_name = selected_group - .as_ref() - .map(|group| format!("{}{}", group.name, self.sync_groups_name_marked_text)) - .unwrap_or_default(); - let search_display = if self.sync_groups_search_draft.is_empty() - && self.sync_groups_search_marked_text.is_empty() - { - self.tr("syncGroup.searchPlaceholder").to_string() + let (group_name_input, group_name_focus) = if let Some(group) = selected_group.as_ref() { + let field = self.text_input( + format!("sync.group-name.{}", group.id), + &group.name, + TextInputSetup::default(), + cx, + ); + let focus = field.read(cx).focus_handle(); + (Some(field), Some(focus)) } else { - format!( - "{}{}", - self.sync_groups_search_draft, self.sync_groups_search_marked_text - ) + (None, None) }; + let search_input = self.text_input( + "sync.groups.search", + &self.sync_groups_search_draft.clone(), + TextInputSetup::placeholder(self.tr("syncGroup.searchPlaceholder")), + cx, + ); + let search_focus = search_input.read(cx).focus_handle(); let pending_delete_name = self .sync_groups_delete_pending .as_deref() @@ -327,9 +330,14 @@ impl NyaTermApp { window.focus(&this.sync_groups_focus); cx.notify(); })) - .on_key_down(cx.listener(|this, event: &KeyDownEvent, window, cx| { + .on_key_down(cx.listener(|this, event: &KeyDownEvent, _, cx| { cx.stop_propagation(); - if this.handle_sync_groups_key_down(event, window, cx) { + if this.sync_groups_delete_pending.is_some() { + match event.keystroke.key.as_str() { + "escape" => this.cancel_delete_sync_group(cx), + "enter" => this.confirm_delete_sync_group(cx), + _ => {} + } return; } match event.keystroke.key.as_str() { @@ -457,7 +465,6 @@ impl NyaTermApp { .child( div() .id(SharedString::from("sync-group-name-input")) - .relative() .h(px(34.)) .flex_1() .min_w_0() @@ -470,31 +477,19 @@ impl NyaTermApp { .bg(rgb(palette.input)) .text_sm() .text_color(rgb(palette.text)) - .track_focus(&self.sync_groups_name_focus) - .on_click(cx.listener(|this, _, window, cx| { - window.focus(&this.sync_groups_name_focus); - cx.notify(); - })) - .child(selected_group_name) - .child( - gpui::canvas( - |_bounds, _window, _cx| {}, - move |bounds, _state, window, cx| { - window.handle_input( - &name_input_entity - .read(cx) - .sync_groups_name_focus, - gpui::ElementInputHandler::new( - bounds, - name_input_entity.clone(), - ), - cx, - ); - }, - ) - .absolute() - .inset_0(), - ), + .cursor_text() + .when_some( + group_name_focus, + |this, focus| { + this.on_mouse_down( + MouseButton::Left, + move |_, window, _| { + window.focus(&focus); + }, + ) + }, + ) + .children(group_name_input), ), ) .child( @@ -520,9 +515,8 @@ impl NyaTermApp { .id(SharedString::from( "sync-group-search-input", )) - .relative() - .flex_1() - .min_w(px(140.)) + .flex_1() + .min_w(px(140.)) .h(px(30.)) .px_2() .flex() @@ -532,46 +526,15 @@ impl NyaTermApp { .border_color(rgb(palette.border)) .bg(rgb(palette.input)) .text_xs() - .text_color(if self - .sync_groups_search_draft - .is_empty() - { - rgb(palette.text_muted) - } else { - rgb(palette.text) - }) - .track_focus( - &self.sync_groups_search_focus, - ) - .on_click(cx.listener( - |this, _, window, cx| { - window.focus( - &this.sync_groups_search_focus, - ); - cx.notify(); + .text_color(rgb(palette.text)) + .cursor_text() + .on_mouse_down( + MouseButton::Left, + move |_, window, _| { + window.focus(&search_focus); }, - )) - .child(search_display) - .child( - gpui::canvas( - |_bounds, _window, _cx| {}, - move |bounds, _state, window, cx| { - window.handle_input( - &search_input_entity - .read(cx) - .sync_groups_search_focus, - gpui::ElementInputHandler::new( - bounds, - search_input_entity - .clone(), - ), - cx, - ); - }, - ) - .absolute() - .inset_0(), - ), + ) + .child(search_input), ), ) .child( diff --git a/crates/nyaterm-desktop/src/features/sync_input.rs b/crates/nyaterm-desktop/src/features/sync_input.rs index 4906718a8..57e5931e8 100644 --- a/crates/nyaterm-desktop/src/features/sync_input.rs +++ b/crates/nyaterm-desktop/src/features/sync_input.rs @@ -10,8 +10,8 @@ impl NyaTermApp { ) { self.sync_groups_open = true; self.sync_groups_search_draft.clear(); - self.sync_groups_search_marked_text.clear(); - self.sync_groups_name_marked_text.clear(); + self.forget_text_inputs("sync.groups.search"); + self.forget_text_inputs("sync.group-name."); self.sync_groups_delete_pending = None; if self.sync_groups_selected_id.is_none() { self.sync_groups_selected_id = self.sync_groups.first().map(|group| group.id.clone()); @@ -24,8 +24,8 @@ impl NyaTermApp { pub(in crate::features) fn close_sync_groups(&mut self, cx: &mut Context) { self.sync_groups_open = false; self.sync_groups_search_draft.clear(); - self.sync_groups_search_marked_text.clear(); - self.sync_groups_name_marked_text.clear(); + self.forget_text_inputs("sync.groups.search"); + self.forget_text_inputs("sync.group-name."); self.sync_groups_delete_pending = None; self.terminal.view.status = "sync groups closed".to_string(); cx.notify(); @@ -86,24 +86,26 @@ impl NyaTermApp { self.delete_selected_sync_group(cx); } - pub(in crate::features) fn set_selected_sync_group_name( + pub(in crate::features) fn apply_sync_groups_search( &mut self, - name: String, + text: String, cx: &mut Context, ) { - let Some(group_id) = self.sync_groups_selected_id.clone() else { - return; - }; - if let Some(group) = self - .sync_groups - .iter_mut() - .find(|group| group.id == group_id) - { - group.name = name; - } + self.sync_groups_search_draft = text; cx.notify(); } + pub(in crate::features) fn apply_sync_group_name( + &mut self, + group_id: &str, + text: String, + cx: &mut Context, + ) { + if update_sync_group_name(&mut self.sync_groups, group_id, text) { + cx.notify(); + } + } + pub(in crate::features) fn select_all_sync_group_sessions(&mut self, cx: &mut Context) { let session_ids = self .ordered_sessions() @@ -234,70 +236,6 @@ impl NyaTermApp { } } - pub(in crate::features) fn handle_sync_groups_key_down( - &mut self, - event: &KeyDownEvent, - window: &mut Window, - cx: &mut Context, - ) -> bool { - if self.sync_groups_delete_pending.is_some() { - match event.keystroke.key.as_str() { - "escape" => self.cancel_delete_sync_group(cx), - "enter" => self.confirm_delete_sync_group(cx), - _ => {} - } - return true; - } - let search_focused = self.sync_groups_search_focus.is_focused(window); - let name_focused = self.sync_groups_name_focus.is_focused(window); - if !search_focused && !name_focused { - return false; - } - if event.keystroke.modifiers.platform - || event.keystroke.modifiers.alt - || event.keystroke.modifiers.control - { - return true; - } - match event.keystroke.key.as_str() { - "escape" => self.close_sync_groups(cx), - "backspace" => { - if search_focused { - self.sync_groups_search_draft.pop(); - self.sync_groups_search_marked_text.clear(); - } else if let Some(group) = self.selected_sync_group_mut() { - group.name.pop(); - self.sync_groups_name_marked_text.clear(); - } - cx.notify(); - } - "delete" => { - if search_focused { - self.sync_groups_search_draft.pop(); - self.sync_groups_search_marked_text.clear(); - cx.notify(); - } - } - "enter" => {} - _ => { - if let Some(input) = event - .keystroke - .key_char - .as_deref() - .filter(|text| !text.is_empty()) - { - if search_focused { - self.sync_groups_search_draft.push_str(input); - } else if let Some(group) = self.selected_sync_group_mut() { - group.name.push_str(input); - } - cx.notify(); - } - } - } - true - } - pub(in crate::features) fn toggle_selected_sync_group_enabled( &mut self, cx: &mut Context, @@ -558,3 +496,46 @@ impl NyaTermApp { .unwrap_or(SYNC_GROUP_COLORS[self.sync_groups.len() % SYNC_GROUP_COLORS.len()]) } } + +fn update_sync_group_name(groups: &mut [SyncInputGroup], group_id: &str, text: String) -> bool { + let Some(group) = groups.iter_mut().find(|group| group.id == group_id) else { + return false; + }; + group.name = text; + true +} + +#[cfg(test)] +mod tests { + use super::update_sync_group_name; + use crate::models::SyncInputGroup; + + fn group(id: &str, name: &str) -> SyncInputGroup { + SyncInputGroup { + id: id.to_string(), + name: name.to_string(), + color: 0, + session_ids: Vec::new(), + paused_session_ids: Vec::new(), + enabled: true, + } + } + + #[test] + fn group_name_input_updates_only_its_addressed_group() { + let mut groups = vec![group("one", "One"), group("two", "Two")]; + + assert!(update_sync_group_name( + &mut groups, + "two", + "Renamed".to_string() + )); + assert_eq!(groups[0].name, "One"); + assert_eq!(groups[1].name, "Renamed"); + assert!(!update_sync_group_name( + &mut groups, + "missing", + "Ignored".to_string() + )); + } +} diff --git a/crates/nyaterm-desktop/src/features/terminal/terminal_selection_runtime/helpers.rs b/crates/nyaterm-desktop/src/features/terminal/terminal_selection_runtime/helpers.rs index 0e6b59d83..817153da0 100644 --- a/crates/nyaterm-desktop/src/features/terminal/terminal_selection_runtime/helpers.rs +++ b/crates/nyaterm-desktop/src/features/terminal/terminal_selection_runtime/helpers.rs @@ -122,28 +122,6 @@ impl EntityInputHandler for NyaTermApp { *adjusted_range = Some(start..end); return Some(marked.clone()); } - if self.sync_groups_open - && self.sync_groups_search_focus.is_focused(window) - && !self.sync_groups_search_marked_text.is_empty() - { - let marked = &self.sync_groups_search_marked_text; - let len = marked.encode_utf16().count(); - let start = range.start.min(len); - let end = range.end.min(len).max(start); - *adjusted_range = Some(start..end); - return Some(marked.clone()); - } - if self.sync_groups_open - && self.sync_groups_name_focus.is_focused(window) - && !self.sync_groups_name_marked_text.is_empty() - { - let marked = &self.sync_groups_name_marked_text; - let len = marked.encode_utf16().count(); - let start = range.start.min(len); - let end = range.end.min(len).max(start); - *adjusted_range = Some(start..end); - return Some(marked.clone()); - } if self.rename_session_id.is_some() && self.rename_focus.is_focused(window) && !self.rename_marked_text.is_empty() @@ -211,23 +189,6 @@ impl EntityInputHandler for NyaTermApp { reversed: false, }); } - if self.sync_groups_open && self.sync_groups_search_focus.is_focused(window) { - let cursor = self.sync_groups_search_draft.encode_utf16().count(); - return Some(UTF16Selection { - range: cursor..cursor, - reversed: false, - }); - } - if self.sync_groups_open && self.sync_groups_name_focus.is_focused(window) { - let cursor = self - .selected_sync_group() - .map(|group| group.name.encode_utf16().count()) - .unwrap_or_default(); - return Some(UTF16Selection { - range: cursor..cursor, - reversed: false, - }); - } if self.multi_line_paste_focus.is_focused(window) { let text = self.multi_line_paste_text(); let range = self.multi_line_paste_selected_byte_range(); @@ -277,14 +238,6 @@ impl EntityInputHandler for NyaTermApp { let len = self.lock_password_marked_text.encode_utf16().count(); return (len > 0).then_some(0..len); } - if self.sync_groups_open && self.sync_groups_search_focus.is_focused(window) { - let len = self.sync_groups_search_marked_text.encode_utf16().count(); - return (len > 0).then_some(0..len); - } - if self.sync_groups_open && self.sync_groups_name_focus.is_focused(window) { - let len = self.sync_groups_name_marked_text.encode_utf16().count(); - return (len > 0).then_some(0..len); - } if self.multi_line_paste.is_some() && self.multi_line_paste_focus.is_focused(window) { return self .multi_line_paste_marked_range @@ -320,14 +273,6 @@ impl EntityInputHandler for NyaTermApp { self.lock_password_marked_text.clear(); return; } - if self.sync_groups_open && self.sync_groups_search_focus.is_focused(window) { - self.sync_groups_search_marked_text.clear(); - return; - } - if self.sync_groups_open && self.sync_groups_name_focus.is_focused(window) { - self.sync_groups_name_marked_text.clear(); - return; - } if self.multi_line_paste.is_some() && self.multi_line_paste_focus.is_focused(window) { self.multi_line_paste_marked_text.clear(); self.multi_line_paste_marked_range = None; @@ -376,26 +321,6 @@ impl EntityInputHandler for NyaTermApp { cx.notify(); return; } - if self.sync_groups_open && self.sync_groups_search_focus.is_focused(window) { - self.sync_groups_search_marked_text.clear(); - if !text.is_empty() { - self.sync_groups_search_draft.push_str(text); - } - cx.notify(); - return; - } - if self.sync_groups_open && self.sync_groups_name_focus.is_focused(window) { - self.sync_groups_name_marked_text.clear(); - if !text.is_empty() { - let mut name = self - .selected_sync_group() - .map(|group| group.name.clone()) - .unwrap_or_default(); - name.push_str(text); - self.set_selected_sync_group_name(name, cx); - } - return; - } if self.multi_line_paste.is_some() && self.multi_line_paste_focus.is_focused(window) { let range = range .as_ref() @@ -474,16 +399,6 @@ impl EntityInputHandler for NyaTermApp { cx.notify(); return; } - if self.sync_groups_open && self.sync_groups_search_focus.is_focused(window) { - self.sync_groups_search_marked_text = new_text.to_string(); - cx.notify(); - return; - } - if self.sync_groups_open && self.sync_groups_name_focus.is_focused(window) { - self.sync_groups_name_marked_text = new_text.to_string(); - cx.notify(); - return; - } if self.multi_line_paste.is_some() && self.multi_line_paste_focus.is_focused(window) { let range = range .as_ref() @@ -544,9 +459,6 @@ impl EntityInputHandler for NyaTermApp { } if self.quick_switch_open(cx) || self.is_locked - || (self.sync_groups_open - && (self.sync_groups_search_focus.is_focused(window) - || self.sync_groups_name_focus.is_focused(window))) || (self.multi_line_paste.is_some() && self.multi_line_paste_focus.is_focused(window)) || (self.rename_session_id.is_some() && self.rename_focus.is_focused(window)) || (self.startup_command_open && self.startup_command_focus.is_focused(window)) @@ -601,16 +513,6 @@ impl EntityInputHandler for NyaTermApp { if self.is_locked { return Some(self.lock_password_draft.encode_utf16().count()); } - if self.sync_groups_open && self.sync_groups_search_focus.is_focused(window) { - return Some(self.sync_groups_search_draft.encode_utf16().count()); - } - if self.sync_groups_open && self.sync_groups_name_focus.is_focused(window) { - return Some( - self.selected_sync_group() - .map(|group| group.name.encode_utf16().count()) - .unwrap_or_default(), - ); - } if self.multi_line_paste.is_some() && self.multi_line_paste_focus.is_focused(window) { return Some(utf16_offset_for_byte( self.multi_line_paste_text(), diff --git a/crates/nyaterm-desktop/src/features/text_inputs.rs b/crates/nyaterm-desktop/src/features/text_inputs.rs index abd44ae7c..7cf1200dd 100644 --- a/crates/nyaterm-desktop/src/features/text_inputs.rs +++ b/crates/nyaterm-desktop/src/features/text_inputs.rs @@ -269,6 +269,10 @@ impl NyaTermApp { self.apply_recording_search(text, cx); } else if id.as_ref() == "settings.keybindings.search" { self.apply_keybinding_search(text, cx); + } else if id.as_ref() == "sync.groups.search" { + self.apply_sync_groups_search(text, cx); + } else if let Some(group_id) = id.strip_prefix("sync.group-name.") { + self.apply_sync_group_name(group_id, text, cx); } else if let Some(rest) = id.strip_prefix("ai.credential.") { self.apply_ai_credential_input(rest, text, cx); } else if let Some(field) = id