fix: some tui work in layout
This commit is contained in:
@@ -33,8 +33,10 @@ editor, or form. A paste containing control characters is rejected and never
|
||||
submits a command or creates extra entry lines. Losing terminal ownership
|
||||
(including terminal suspension/focus loss) immediately revokes the lease,
|
||||
cancels Git and clipboard work, removes plaintext presentation state, and
|
||||
returns to the locked screen. `NO_COLOR` or `TERM=dumb` selects the monochrome
|
||||
fallback while preserving focused selections through reverse-video attributes.
|
||||
returns to the locked screen. A native authentication prompt may temporarily
|
||||
own focus; its result stays hidden until terminal focus returns. `NO_COLOR` or
|
||||
`TERM=dumb` selects the monochrome fallback while preserving focused selections
|
||||
through reverse-video attributes.
|
||||
Selected rows override nested field colors so labels, masked values, metadata,
|
||||
and OTP codes keep one high-contrast foreground across the complete selection.
|
||||
|
||||
|
||||
@@ -87,6 +87,7 @@ pub fn run(terminal: &mut DefaultTerminal) -> io::Result<()> {
|
||||
let mut authentication = None;
|
||||
let mut git_control = None;
|
||||
let mut authentication_initialized = false;
|
||||
let mut terminal_focused = true;
|
||||
let mut key_resolver = KeyResolver::default();
|
||||
let color_capability = ui::ColorCapability::detect();
|
||||
let startup = app.begin_latest_request();
|
||||
@@ -120,7 +121,8 @@ pub fn run(terminal: &mut DefaultTerminal) -> io::Result<()> {
|
||||
Err(error) => app.authentication_failed(error),
|
||||
}
|
||||
}
|
||||
if let Some(coordinator) = authentication.as_mut()
|
||||
if terminal_focused
|
||||
&& let Some(coordinator) = authentication.as_mut()
|
||||
&& let Some(event) = coordinator.completion()
|
||||
{
|
||||
apply_authentication_event(
|
||||
@@ -252,12 +254,18 @@ pub fn run(terminal: &mut DefaultTerminal) -> io::Result<()> {
|
||||
}
|
||||
pasted.zeroize();
|
||||
}
|
||||
Event::FocusGained => terminal_focused = true,
|
||||
Event::FocusLost => {
|
||||
handle_terminal_ownership_lost(
|
||||
terminal_focused = false;
|
||||
let native_prompt_pending = authentication
|
||||
.as_ref()
|
||||
.is_some_and(AuthenticationCoordinator::native_prompt_pending);
|
||||
handle_terminal_focus_lost(
|
||||
&mut app,
|
||||
&mut authentication,
|
||||
&mut git_control,
|
||||
&mut clipboard_cancellations,
|
||||
native_prompt_pending,
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
@@ -266,6 +274,19 @@ pub fn run(terminal: &mut DefaultTerminal) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_terminal_focus_lost(
|
||||
app: &mut App,
|
||||
authentication: &mut Option<AuthenticationCoordinator>,
|
||||
git_control: &mut Option<ironstorage::git::GitOperationControl>,
|
||||
clipboard_cancellations: &mut ClipboardCancellations,
|
||||
native_prompt_pending: bool,
|
||||
) {
|
||||
if native_prompt_pending {
|
||||
return;
|
||||
}
|
||||
handle_terminal_ownership_lost(app, authentication, git_control, clipboard_cancellations);
|
||||
}
|
||||
|
||||
fn current_unix_seconds() -> Result<u64, std::time::SystemTimeError> {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
@@ -1620,6 +1641,24 @@ mod tests {
|
||||
assert_eq!(presentation.0, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_authentication_focus_handoff_waits_without_relocking() {
|
||||
let mut app = App::new();
|
||||
let mut authentication = None;
|
||||
let mut git_control = None;
|
||||
let mut clipboard = ClipboardCancellations::default();
|
||||
|
||||
handle_terminal_focus_lost(
|
||||
&mut app,
|
||||
&mut authentication,
|
||||
&mut git_control,
|
||||
&mut clipboard,
|
||||
true,
|
||||
);
|
||||
|
||||
assert_eq!(app.mode(), crate::app::Mode::Browser);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authentication_expiry_cancels_the_active_clipboard_presentation() {
|
||||
let mut app = App::new();
|
||||
|
||||
@@ -53,6 +53,7 @@ pub struct AuthenticationCoordinator {
|
||||
sender: Sender<AuthenticationCompletion>,
|
||||
receiver: Receiver<AuthenticationCompletion>,
|
||||
generation: u64,
|
||||
native_prompt_pending: bool,
|
||||
}
|
||||
|
||||
impl AuthenticationCoordinator {
|
||||
@@ -71,6 +72,7 @@ impl AuthenticationCoordinator {
|
||||
sender,
|
||||
receiver,
|
||||
generation: 0,
|
||||
native_prompt_pending: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -96,6 +98,7 @@ impl AuthenticationCoordinator {
|
||||
|
||||
fn request(&mut self, target: AuthenticationTarget) {
|
||||
self.generation = self.generation.wrapping_add(1);
|
||||
self.native_prompt_pending = self.handle.is_none();
|
||||
let generation = self.generation;
|
||||
let session = self.session.clone();
|
||||
let key = self.key.clone();
|
||||
@@ -118,6 +121,7 @@ impl AuthenticationCoordinator {
|
||||
.try_iter()
|
||||
.filter(|completion| completion.generation == self.generation)
|
||||
.last()?;
|
||||
self.native_prompt_pending = false;
|
||||
match completion.result {
|
||||
Ok(handle) => {
|
||||
self.handle = Some(handle);
|
||||
@@ -172,8 +176,13 @@ impl AuthenticationCoordinator {
|
||||
self.handle.clone()
|
||||
}
|
||||
|
||||
pub const fn native_prompt_pending(&self) -> bool {
|
||||
self.native_prompt_pending
|
||||
}
|
||||
|
||||
pub fn lock(&mut self) -> Result<(), String> {
|
||||
self.generation = self.generation.wrapping_add(1);
|
||||
self.native_prompt_pending = false;
|
||||
self.handle = None;
|
||||
self.session
|
||||
.manual_lock()
|
||||
|
||||
@@ -419,14 +419,7 @@ fn render_large_otp(
|
||||
let inner_width = usize::from(area.width.saturating_sub(2));
|
||||
let mut lines = glyphs
|
||||
.iter()
|
||||
.map(|row| {
|
||||
Line::styled(
|
||||
row.as_str(),
|
||||
Style::default()
|
||||
.fg(Color::Green)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)
|
||||
})
|
||||
.map(|row| large_otp_line(row))
|
||||
.collect::<Vec<_>>();
|
||||
lines.push(Line::styled(
|
||||
countdown_bar(remaining, period, inner_width),
|
||||
@@ -444,6 +437,26 @@ fn render_large_otp(
|
||||
);
|
||||
}
|
||||
|
||||
fn large_otp_line(row: &str) -> Line<'_> {
|
||||
let digit_style = Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Color::Green)
|
||||
.add_modifier(Modifier::BOLD);
|
||||
Line::from(
|
||||
row.split(' ')
|
||||
.enumerate()
|
||||
.flat_map(|(index, digits)| {
|
||||
[
|
||||
(index != 0).then(|| Span::raw(" ")),
|
||||
(!digits.is_empty()).then(|| Span::styled(digits, digit_style)),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_compact_otp(
|
||||
frame: &mut Frame,
|
||||
display: &crate::app::OtpDisplay,
|
||||
@@ -1149,6 +1162,21 @@ mod tests {
|
||||
assert_eq!(layout_class(Rect::new(0, 0, 140, 20)), LayoutClass::Wide);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_otp_inverts_only_the_digits_that_form_each_glyph() {
|
||||
let line = large_otp_line("1 22");
|
||||
assert_eq!(line.to_string(), "1 22");
|
||||
for span in line.spans {
|
||||
if span.content == " " {
|
||||
assert_eq!(span.style, Style::default());
|
||||
} else {
|
||||
assert_eq!(span.style.fg, Some(Color::Black));
|
||||
assert_eq!(span.style.bg, Some(Color::Green));
|
||||
assert!(span.style.add_modifier.contains(Modifier::BOLD));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn color_capability_has_a_complete_monochrome_fallback() {
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user