fix: some tui work in layout

This commit is contained in:
2026-08-11 14:44:16 +02:00
parent 9e0e37b875
commit 64c90aa1d2
4 changed files with 90 additions and 12 deletions

View File

@@ -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.

View File

@@ -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();

View File

@@ -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()

View File

@@ -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!(