feat(sync): use real text inputs for group editing

This commit is contained in:
Kang
2026-07-27 17:07:19 +08:00
parent 3db088a244
commit 2d2abd5571
6 changed files with 112 additions and 270 deletions
@@ -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,
@@ -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<String>,
pub(in crate::features) sync_groups_delete_pending: Option<String>,
/// Broadcast keyboard input to every live session (Tauri broadcastToAll).
@@ -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(
@@ -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>) {
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<Self>,
) {
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<Self>,
) {
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<Self>) {
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<Self>,
) -> 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<Self>,
@@ -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()
));
}
}
@@ -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(),
@@ -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