feat(security): use real masked password inputs

This commit is contained in:
Kang
2026-07-27 17:07:19 +08:00
parent 300e789885
commit eebb7e50e5
11 changed files with 106 additions and 219 deletions
@@ -581,7 +581,6 @@ impl NyaTermApp {
multi_line_paste_focus: cx.focus_handle(),
lock_focus: cx.focus_handle(),
lock_password_draft: String::new(),
lock_password_marked_text: String::new(),
lock_status: String::new(),
pending_terminal_frame_events: VecDeque::new(),
pending_session_events: VecDeque::new(),
@@ -266,7 +266,6 @@ pub struct NyaTermApp {
pub(in crate::features) multi_line_paste_focus: FocusHandle,
pub(in crate::features) lock_focus: FocusHandle,
pub(in crate::features) lock_password_draft: String,
pub(in crate::features) lock_password_marked_text: String,
pub(in crate::features) lock_status: String,
pub(in crate::features) pending_terminal_frame_events: VecDeque<TerminalFrameEvent>,
pub(in crate::features) pending_session_events: VecDeque<SessionEvent>,
@@ -88,14 +88,13 @@ impl NyaTermApp {
cx: &mut Context<Self>,
) -> impl IntoElement {
let palette = self.theme_palette();
let draft_length = self.security.unlock.draft.chars().count()
+ self.security.unlock.marked_text.chars().count();
let draft = if draft_length == 0 {
" ".to_string()
} else {
"•".repeat(draft_length.min(32))
};
let input_entity = cx.entity();
let password_input = self.text_input(
"security.unlock.password",
&self.security.unlock.draft.clone(),
TextInputSetup::masked(),
cx,
);
let password_focus = password_input.read(cx).focus_handle();
div()
.absolute()
.inset_0()
@@ -146,25 +145,16 @@ impl NyaTermApp {
.font_family(crate::features::gpui_code_font_family())
.text_xs()
.text_color(rgb(palette.text))
.child(draft)
.cursor_text()
.on_mouse_down(MouseButton::Left, move |_, window, _| {
window.focus(&password_focus);
})
.child(
gpui::canvas(
|_bounds, _window, _cx| {},
move |bounds, _state, window, cx| {
let focus =
input_entity.read(cx).security.unlock.focus.clone();
window.handle_input(
&focus,
gpui::ElementInputHandler::new(
bounds,
input_entity.clone(),
),
cx,
);
},
)
.absolute()
.inset_0(),
div()
.min_w_0()
.flex_1()
.overflow_hidden()
.child(password_input),
),
)
.when_some(self.security.unlock.error.clone(), |this, error| {
@@ -7,13 +7,17 @@ impl NyaTermApp {
cx: &mut Context<Self>,
) -> impl IntoElement {
let palette = self.theme_palette();
let input_entity = cx.entity();
let password_length = self.lock_password_draft.chars().count()
+ self.lock_password_marked_text.chars().count();
let password_display = if password_length == 0 {
" ".to_string()
let password_input = self.text_input(
"lock-screen.password",
&self.lock_password_draft.clone(),
TextInputSetup::masked(),
cx,
);
let password_focus = password_input.read(cx).focus_handle();
let overlay_focus = if self.settings.has_master_password {
password_focus.clone()
} else {
"•".repeat(password_length.min(32))
self.lock_focus.clone()
};
let lock_status = if self.lock_status.trim().is_empty() {
if self.settings.has_master_password {
@@ -39,10 +43,7 @@ impl NyaTermApp {
.bg(rgba(0x000000d9))
.text_color(rgb(0xffffff))
.track_focus(&self.lock_focus)
.on_click(cx.listener(|this, _, window, cx| {
window.focus(&this.lock_focus);
cx.notify();
}))
.on_click(move |_, window, _| window.focus(&overlay_focus))
.on_key_down(cx.listener(|this, event: &KeyDownEvent, _, cx| {
cx.stop_propagation();
this.handle_lock_key_down(event, cx);
@@ -98,10 +99,12 @@ impl NyaTermApp {
.justify_center()
.child(
div()
.id("lock-screen-content")
.flex()
.flex_col()
.items_center()
.gap_5()
.on_click(|_, _, cx| cx.stop_propagation())
.child(
div()
.relative()
@@ -185,33 +188,19 @@ impl NyaTermApp {
)
.text_sm()
.text_color(rgb(palette.text))
.cursor_pointer()
.track_focus(&self.lock_focus)
.on_click(cx.listener(|this, _, window, cx| {
window.focus(&this.lock_focus);
cx.notify();
}))
.child(password_display)
.cursor_text()
.on_mouse_down(
MouseButton::Left,
move |_, window, _| {
window.focus(&password_focus);
},
)
.child(
gpui::canvas(
|_bounds, _window, _cx| {},
move |bounds, _state, window, cx| {
let focus = input_entity
.read(cx)
.lock_focus
.clone();
window.handle_input(
&focus,
gpui::ElementInputHandler::new(
bounds,
input_entity.clone(),
),
cx,
);
},
)
.absolute()
.inset_0(),
div()
.min_w_0()
.flex_1()
.overflow_hidden()
.child(password_input),
),
)
.child(
@@ -7,21 +7,26 @@ impl NyaTermApp {
pub(in crate::features) fn lock_app(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.is_locked = true;
self.lock_password_draft.clear();
self.lock_password_marked_text.clear();
self.forget_text_inputs("lock-screen.password");
self.lock_status = if self.settings.has_master_password {
self.tr("lockScreen.passwordPlaceholder").to_string()
} else {
String::new()
};
self.terminal.view.status = "screen locked".to_string();
window.focus(&self.lock_focus);
if self.settings.has_master_password {
let field = self.text_input("lock-screen.password", "", TextInputSetup::masked(), cx);
window.focus(&field.read(cx).focus_handle());
} else {
window.focus(&self.lock_focus);
}
cx.notify();
}
pub(in crate::features) fn unlock_app(&mut self, cx: &mut Context<Self>) {
self.is_locked = false;
self.lock_password_draft.clear();
self.lock_password_marked_text.clear();
self.forget_text_inputs("lock-screen.password");
self.lock_status.clear();
self.last_user_activity_at = Instant::now();
self.terminal.view.status = "screen unlocked".to_string();
@@ -43,14 +48,14 @@ impl NyaTermApp {
Ok(true) => self.unlock_app(cx),
Ok(false) => {
self.lock_password_draft.clear();
self.lock_password_marked_text.clear();
self.reset_text_input("lock-screen.password", "", cx);
self.lock_status = self.tr("lockScreen.wrongPassword").to_string();
self.terminal.view.status = "screen unlock rejected".to_string();
cx.notify();
}
Err(error) => {
self.lock_password_draft.clear();
self.lock_password_marked_text.clear();
self.reset_text_input("lock-screen.password", "", cx);
self.lock_status = format!("{}: {error}", self.tr("lockScreen.unlockFailed"));
self.terminal.view.status = "screen unlock failed".to_string();
cx.notify();
@@ -73,30 +78,24 @@ impl NyaTermApp {
"escape" if !self.settings.has_master_password => self.unlock_app(cx),
"escape" => {
self.lock_password_draft.clear();
self.reset_text_input("lock-screen.password", "", cx);
self.lock_status = self.tr("lockScreen.passwordPlaceholder").to_string();
cx.notify();
}
"backspace" => {
self.lock_password_draft.pop();
self.lock_password_marked_text.clear();
cx.notify();
}
_ if self.settings.has_master_password => {
if let Some(value) = keystroke
.key_char
.as_deref()
.filter(|value| !value.is_empty())
{
self.lock_password_draft.push_str(value);
self.lock_password_marked_text.clear();
self.lock_status = self.tr("lockScreen.passwordPlaceholder").to_string();
cx.notify();
}
}
_ => {}
}
}
pub(in crate::features) fn apply_lock_password_input(
&mut self,
text: String,
cx: &mut Context<Self>,
) {
self.lock_password_draft = text;
self.lock_status = self.tr("lockScreen.passwordPlaceholder").to_string();
cx.notify();
}
pub(in crate::features) fn reveal_log_dir(&mut self, cx: &mut Context<Self>) {
match std::fs::create_dir_all(self.runtime.log_dir()) {
Ok(()) => {
@@ -31,7 +31,7 @@ impl NyaTermApp {
self.security.unlock.prompt_open = false;
self.security.unlock.master_required_prompt_open = true;
self.security.unlock.draft.clear();
self.security.unlock.marked_text.clear();
self.forget_text_inputs("security.unlock.password");
self.security.unlock.error = None;
self.security.status = "master password required".to_string();
cx.notify();
@@ -40,15 +40,17 @@ impl NyaTermApp {
self.security.unlock.master_required_prompt_open = false;
self.security.unlock.prompt_open = true;
self.security.unlock.draft.clear();
self.security.unlock.marked_text.clear();
self.forget_text_inputs("security.unlock.password");
let field = self.text_input("security.unlock.password", "", TextInputSetup::masked(), cx);
self.security.unlock.error = None;
self.security.status = "enter master password to unlock secrets".to_string();
window.focus(&self.security.unlock.focus);
window.focus(&field.read(cx).focus_handle());
cx.notify();
}
pub(in crate::features) fn close_security_unlock_prompt(&mut self, cx: &mut Context<Self>) {
self.security.close_unlock_prompt();
self.forget_text_inputs("security.unlock.password");
cx.notify();
}
@@ -78,6 +80,7 @@ impl NyaTermApp {
pub(in crate::features) fn lock_security_secrets(&mut self, cx: &mut Context<Self>) {
self.security.lock_secrets();
self.forget_text_inputs("security.unlock.password");
cx.notify();
}
@@ -91,7 +94,7 @@ impl NyaTermApp {
self.security.unlock.prompt_open = false;
self.security.unlock.master_required_prompt_open = true;
self.security.unlock.draft.clear();
self.security.unlock.marked_text.clear();
self.forget_text_inputs("security.unlock.password");
self.security.unlock.error = None;
self.security.status = "master password required".to_string();
cx.notify();
@@ -114,7 +117,7 @@ impl NyaTermApp {
}
Ok(false) => {
self.security.unlock.draft.clear();
self.security.unlock.marked_text.clear();
self.reset_text_input("security.unlock.password", "", cx);
self.security.unlock.error =
Some(self.tr("secretUnlock.wrongPassword").to_string());
self.security.status = "unlock rejected".to_string();
@@ -122,7 +125,7 @@ impl NyaTermApp {
}
Err(error) => {
self.security.unlock.draft.clear();
self.security.unlock.marked_text.clear();
self.reset_text_input("security.unlock.password", "", cx);
self.security.unlock.error = Some(error.to_string());
self.security.status = "unlock failed".to_string();
cx.notify();
@@ -144,30 +147,20 @@ impl NyaTermApp {
match keystroke.key.as_str() {
"enter" => self.submit_security_unlock(window, cx),
"escape" => self.cancel_security_unlock_prompt(cx),
"backspace" => {
if self.security.unlock.marked_text.is_empty() {
self.security.unlock.draft.pop();
} else {
self.security.unlock.marked_text.clear();
}
self.security.unlock.error = None;
cx.notify();
}
_ => {
if let Some(value) = keystroke
.key_char
.as_deref()
.filter(|value| !value.is_empty())
{
self.security.unlock.marked_text.clear();
self.security.unlock.draft.push_str(value);
self.security.unlock.error = None;
cx.notify();
}
}
_ => {}
}
}
pub(in crate::features) fn apply_security_unlock_password_input(
&mut self,
text: String,
cx: &mut Context<Self>,
) {
self.security.unlock.draft = text;
self.security.unlock.error = None;
cx.notify();
}
fn execute_security_unlock_action(
&mut self,
action: SecurityUnlockAction,
@@ -59,7 +59,6 @@ pub(in crate::features) struct SecurityUnlockState {
pub prompt_open: bool,
pub master_required_prompt_open: bool,
pub draft: String,
pub marked_text: String,
pub error: Option<String>,
pub pending_action: Option<SecurityUnlockAction>,
pub focus: FocusHandle,
@@ -96,7 +95,6 @@ impl SecurityFeatureState {
prompt_open: false,
master_required_prompt_open: false,
draft: String::new(),
marked_text: String::new(),
error: None,
pending_action: None,
focus: focus.unlock,
@@ -147,7 +145,6 @@ impl SecurityFeatureState {
pub(in crate::features) fn close_unlock_prompt(&mut self) {
self.unlock.prompt_open = false;
self.unlock.draft.clear();
self.unlock.marked_text.clear();
self.unlock.error = None;
}
@@ -243,7 +243,11 @@ impl NyaTermApp {
})
}
pub(in crate::features) fn drive_idle_lock(&mut self) -> bool {
pub(in crate::features) fn drive_idle_lock(
&mut self,
window: &mut Window,
cx: &mut Context<Self>,
) -> bool {
if self.is_locked
|| !self.settings.enable_screen_lock
|| self.settings.idle_lock_minutes == 0
@@ -257,6 +261,7 @@ impl NyaTermApp {
}
self.is_locked = true;
self.lock_password_draft.clear();
self.forget_text_inputs("lock-screen.password");
self.lock_status = if self.settings.has_master_password {
"Enter the master password to unlock.".to_string()
} else {
@@ -266,6 +271,12 @@ impl NyaTermApp {
"screen locked after {} minute(s) idle",
self.settings.idle_lock_minutes
);
if self.settings.has_master_password {
let field = self.text_input("lock-screen.password", "", TextInputSetup::masked(), cx);
window.focus(&field.read(cx).focus_handle());
} else {
window.focus(&self.lock_focus);
}
true
}
@@ -644,7 +644,7 @@ impl NyaTermApp {
result.remote_refresh = stage_started_at.elapsed();
let stage_started_at = Instant::now();
dirty |= self.drive_idle_lock();
dirty |= self.drive_idle_lock(window, cx);
result.idle_lock = stage_started_at.elapsed();
result.dirty = dirty;
result
@@ -86,14 +86,6 @@ impl EntityInputHandler for NyaTermApp {
*adjusted_range = Some(utf16_range_from_bytes(text, &byte_range));
return Some(text[byte_range].to_string());
}
if self.security.unlock.prompt_open && !self.security.unlock.marked_text.is_empty() {
let marked = &self.security.unlock.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());
}
let quick_switch = self.quick_switch_state(cx);
if quick_switch.is_open() && !quick_switch.marked_text().is_empty() {
let marked = quick_switch.marked_text();
@@ -103,14 +95,6 @@ impl EntityInputHandler for NyaTermApp {
*adjusted_range = Some(start..end);
return Some(marked.to_string());
}
if self.is_locked && !self.lock_password_marked_text.is_empty() {
let marked = &self.lock_password_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.terminal.input.ime_marked_text.is_empty() {
return None;
}
@@ -127,13 +111,6 @@ impl EntityInputHandler for NyaTermApp {
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<UTF16Selection> {
if self.security.unlock.prompt_open {
let cursor = self.security.unlock.draft.encode_utf16().count();
return Some(UTF16Selection {
range: cursor..cursor,
reversed: false,
});
}
let quick_switch = self.quick_switch_state(cx);
if quick_switch.is_open() {
let cursor = quick_switch.query().encode_utf16().count();
@@ -142,13 +119,6 @@ impl EntityInputHandler for NyaTermApp {
reversed: false,
});
}
if self.is_locked {
let cursor = self.lock_password_draft.encode_utf16().count();
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();
@@ -171,19 +141,11 @@ impl EntityInputHandler for NyaTermApp {
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<Range<usize>> {
if self.security.unlock.prompt_open {
let len = self.security.unlock.marked_text.encode_utf16().count();
return (len > 0).then_some(0..len);
}
let quick_switch = self.quick_switch_state(cx);
if quick_switch.is_open() {
let len = quick_switch.marked_text().encode_utf16().count();
return (len > 0).then_some(0..len);
}
if self.is_locked {
let len = self.lock_password_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
@@ -195,18 +157,10 @@ impl EntityInputHandler for NyaTermApp {
}
fn unmark_text(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.security.unlock.prompt_open {
self.security.unlock.marked_text.clear();
return;
}
if self.quick_switch_open(cx) {
self.update_quick_switch_state(cx, |store| store.clear_quick_switch_marked_text());
return;
}
if self.is_locked {
self.lock_password_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;
@@ -222,27 +176,11 @@ impl EntityInputHandler for NyaTermApp {
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.security.unlock.prompt_open {
self.security.unlock.marked_text.clear();
self.security.unlock.draft.push_str(text);
self.security.unlock.error = None;
cx.notify();
return;
}
if self.quick_switch_open(cx) {
self.update_quick_switch_state(cx, |store| store.replace_quick_switch_text(text));
cx.notify();
return;
}
if self.is_locked {
self.lock_password_marked_text.clear();
if !text.is_empty() {
self.lock_password_draft.push_str(text);
self.lock_status = self.tr("lockScreen.passwordPlaceholder").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()
@@ -278,11 +216,6 @@ impl EntityInputHandler for NyaTermApp {
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.security.unlock.prompt_open {
self.security.unlock.marked_text = new_text.to_string();
cx.notify();
return;
}
if self.quick_switch_open(cx) {
self.update_quick_switch_state(cx, |store| {
store.set_quick_switch_marked_text(new_text)
@@ -290,11 +223,6 @@ impl EntityInputHandler for NyaTermApp {
cx.notify();
return;
}
if self.is_locked {
self.lock_password_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()
@@ -326,23 +254,8 @@ impl EntityInputHandler for NyaTermApp {
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<Bounds<Pixels>> {
if self.security.unlock.prompt_open {
return Some(gpui::bounds(
Point {
x: element_bounds.left(),
y: element_bounds.bottom() - px(18.),
},
Size {
width: px(1.),
height: px(18.),
},
));
}
if self.quick_switch_open(cx)
|| self.is_locked
|| (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))
{
return Some(element_bounds);
}
@@ -384,28 +297,16 @@ impl EntityInputHandler for NyaTermApp {
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<usize> {
if self.security.unlock.prompt_open {
return Some(self.security.unlock.draft.encode_utf16().count());
}
let quick_switch = self.quick_switch_state(cx);
if quick_switch.is_open() {
return Some(quick_switch.query().encode_utf16().count());
}
if self.is_locked {
return Some(self.lock_password_draft.encode_utf16().count());
}
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(),
self.multi_line_paste_cursor,
));
}
if self.rename_session_id.is_some() && self.rename_focus.is_focused(window) {
return Some(self.rename_draft.encode_utf16().count());
}
if self.startup_command_open && self.startup_command_focus.is_focused(window) {
return Some(self.startup_command_draft.encode_utf16().count());
}
Some(0)
}
}
@@ -15,7 +15,7 @@
use std::collections::HashMap;
use gpui::{
App, AppContext as _, Context, Entity, InteractiveElement as _, IntoElement, MouseButton,
AppContext, Context, Entity, InteractiveElement as _, IntoElement, MouseButton,
ParentElement as _, SharedString, Styled as _, Subscription, div, prelude::FluentBuilder as _,
px, rgb,
};
@@ -197,7 +197,12 @@ impl NyaTermApp {
}
/// Push a value the runtime changed back into its input.
pub(in crate::features) fn reset_text_input(&mut self, id: &str, text: &str, cx: &mut App) {
pub(in crate::features) fn reset_text_input(
&mut self,
id: &str,
text: &str,
cx: &mut impl AppContext,
) {
if let Some(field) = self.text_inputs.fields.get(id) {
field.update(cx, |field, cx| field.set_content(text, cx));
}
@@ -277,6 +282,10 @@ impl NyaTermApp {
self.apply_temporary_ssh_link(text, cx);
} else if let Some(field) = id.strip_prefix("session.") {
self.apply_session_text_input(field, text, cx);
} else if id.as_ref() == "lock-screen.password" {
self.apply_lock_password_input(text, cx);
} else if id.as_ref() == "security.unlock.password" {
self.apply_security_unlock_password_input(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