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
|
submits a command or creates extra entry lines. Losing terminal ownership
|
||||||
(including terminal suspension/focus loss) immediately revokes the lease,
|
(including terminal suspension/focus loss) immediately revokes the lease,
|
||||||
cancels Git and clipboard work, removes plaintext presentation state, and
|
cancels Git and clipboard work, removes plaintext presentation state, and
|
||||||
returns to the locked screen. `NO_COLOR` or `TERM=dumb` selects the monochrome
|
returns to the locked screen. A native authentication prompt may temporarily
|
||||||
fallback while preserving focused selections through reverse-video attributes.
|
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,
|
Selected rows override nested field colors so labels, masked values, metadata,
|
||||||
and OTP codes keep one high-contrast foreground across the complete selection.
|
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 authentication = None;
|
||||||
let mut git_control = None;
|
let mut git_control = None;
|
||||||
let mut authentication_initialized = false;
|
let mut authentication_initialized = false;
|
||||||
|
let mut terminal_focused = true;
|
||||||
let mut key_resolver = KeyResolver::default();
|
let mut key_resolver = KeyResolver::default();
|
||||||
let color_capability = ui::ColorCapability::detect();
|
let color_capability = ui::ColorCapability::detect();
|
||||||
let startup = app.begin_latest_request();
|
let startup = app.begin_latest_request();
|
||||||
@@ -120,7 +121,8 @@ pub fn run(terminal: &mut DefaultTerminal) -> io::Result<()> {
|
|||||||
Err(error) => app.authentication_failed(error),
|
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()
|
&& let Some(event) = coordinator.completion()
|
||||||
{
|
{
|
||||||
apply_authentication_event(
|
apply_authentication_event(
|
||||||
@@ -252,12 +254,18 @@ pub fn run(terminal: &mut DefaultTerminal) -> io::Result<()> {
|
|||||||
}
|
}
|
||||||
pasted.zeroize();
|
pasted.zeroize();
|
||||||
}
|
}
|
||||||
|
Event::FocusGained => terminal_focused = true,
|
||||||
Event::FocusLost => {
|
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 app,
|
||||||
&mut authentication,
|
&mut authentication,
|
||||||
&mut git_control,
|
&mut git_control,
|
||||||
&mut clipboard_cancellations,
|
&mut clipboard_cancellations,
|
||||||
|
native_prompt_pending,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -266,6 +274,19 @@ pub fn run(terminal: &mut DefaultTerminal) -> io::Result<()> {
|
|||||||
Ok(())
|
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> {
|
fn current_unix_seconds() -> Result<u64, std::time::SystemTimeError> {
|
||||||
std::time::SystemTime::now()
|
std::time::SystemTime::now()
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
@@ -1620,6 +1641,24 @@ mod tests {
|
|||||||
assert_eq!(presentation.0, 0);
|
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]
|
#[test]
|
||||||
fn authentication_expiry_cancels_the_active_clipboard_presentation() {
|
fn authentication_expiry_cancels_the_active_clipboard_presentation() {
|
||||||
let mut app = App::new();
|
let mut app = App::new();
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ pub struct AuthenticationCoordinator {
|
|||||||
sender: Sender<AuthenticationCompletion>,
|
sender: Sender<AuthenticationCompletion>,
|
||||||
receiver: Receiver<AuthenticationCompletion>,
|
receiver: Receiver<AuthenticationCompletion>,
|
||||||
generation: u64,
|
generation: u64,
|
||||||
|
native_prompt_pending: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AuthenticationCoordinator {
|
impl AuthenticationCoordinator {
|
||||||
@@ -71,6 +72,7 @@ impl AuthenticationCoordinator {
|
|||||||
sender,
|
sender,
|
||||||
receiver,
|
receiver,
|
||||||
generation: 0,
|
generation: 0,
|
||||||
|
native_prompt_pending: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,6 +98,7 @@ impl AuthenticationCoordinator {
|
|||||||
|
|
||||||
fn request(&mut self, target: AuthenticationTarget) {
|
fn request(&mut self, target: AuthenticationTarget) {
|
||||||
self.generation = self.generation.wrapping_add(1);
|
self.generation = self.generation.wrapping_add(1);
|
||||||
|
self.native_prompt_pending = self.handle.is_none();
|
||||||
let generation = self.generation;
|
let generation = self.generation;
|
||||||
let session = self.session.clone();
|
let session = self.session.clone();
|
||||||
let key = self.key.clone();
|
let key = self.key.clone();
|
||||||
@@ -118,6 +121,7 @@ impl AuthenticationCoordinator {
|
|||||||
.try_iter()
|
.try_iter()
|
||||||
.filter(|completion| completion.generation == self.generation)
|
.filter(|completion| completion.generation == self.generation)
|
||||||
.last()?;
|
.last()?;
|
||||||
|
self.native_prompt_pending = false;
|
||||||
match completion.result {
|
match completion.result {
|
||||||
Ok(handle) => {
|
Ok(handle) => {
|
||||||
self.handle = Some(handle);
|
self.handle = Some(handle);
|
||||||
@@ -172,8 +176,13 @@ impl AuthenticationCoordinator {
|
|||||||
self.handle.clone()
|
self.handle.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const fn native_prompt_pending(&self) -> bool {
|
||||||
|
self.native_prompt_pending
|
||||||
|
}
|
||||||
|
|
||||||
pub fn lock(&mut self) -> Result<(), String> {
|
pub fn lock(&mut self) -> Result<(), String> {
|
||||||
self.generation = self.generation.wrapping_add(1);
|
self.generation = self.generation.wrapping_add(1);
|
||||||
|
self.native_prompt_pending = false;
|
||||||
self.handle = None;
|
self.handle = None;
|
||||||
self.session
|
self.session
|
||||||
.manual_lock()
|
.manual_lock()
|
||||||
|
|||||||
@@ -419,14 +419,7 @@ fn render_large_otp(
|
|||||||
let inner_width = usize::from(area.width.saturating_sub(2));
|
let inner_width = usize::from(area.width.saturating_sub(2));
|
||||||
let mut lines = glyphs
|
let mut lines = glyphs
|
||||||
.iter()
|
.iter()
|
||||||
.map(|row| {
|
.map(|row| large_otp_line(row))
|
||||||
Line::styled(
|
|
||||||
row.as_str(),
|
|
||||||
Style::default()
|
|
||||||
.fg(Color::Green)
|
|
||||||
.add_modifier(Modifier::BOLD),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
lines.push(Line::styled(
|
lines.push(Line::styled(
|
||||||
countdown_bar(remaining, period, inner_width),
|
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(
|
fn render_compact_otp(
|
||||||
frame: &mut Frame,
|
frame: &mut Frame,
|
||||||
display: &crate::app::OtpDisplay,
|
display: &crate::app::OtpDisplay,
|
||||||
@@ -1149,6 +1162,21 @@ mod tests {
|
|||||||
assert_eq!(layout_class(Rect::new(0, 0, 140, 20)), LayoutClass::Wide);
|
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]
|
#[test]
|
||||||
fn color_capability_has_a_complete_monochrome_fallback() {
|
fn color_capability_has_a_complete_monochrome_fallback() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
Reference in New Issue
Block a user