Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 16 additions & 16 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,9 @@ pub struct App {
/// Id of the attached task, if any: by id (not index) so it survives the
/// task list changing underneath it.
pub focused_id: Option<u64>,
/// Whether the host terminal window has focus
/// While unfocused, highlight rows mute to a bright-black background
pub terminal_focused: bool,
pub rows: u16,
pub cols: u16,
/// Bytes of the last painted frame; the renderer skips the write when the
Expand Down Expand Up @@ -424,6 +427,7 @@ impl App {
spawn_cwd: invocation_dir.clone(),
spawn_group: None,
focused_id: None,
terminal_focused: true,
rows,
cols,
last_frame: Vec::new(),
Expand Down Expand Up @@ -881,6 +885,8 @@ impl App {
CtEvent::Resize(cols, rows) => self.on_resize(rows, cols),
CtEvent::Paste(s) => self.on_paste(&s),
CtEvent::Mouse(m) => self.on_mouse(m),
CtEvent::FocusGained => self.terminal_focused = true,
CtEvent::FocusLost => self.terminal_focused = false,
_ => {}
}
}
Expand Down
36 changes: 36 additions & 0 deletions src/app_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2436,6 +2436,42 @@ fn painted(app: &mut App) -> String {
String::from_utf8_lossy(&out).into_owned()
}

/// Highlight rows swap reverse video for a bright-black background while the
/// host terminal is unfocused
#[test]
fn unfocused_terminal_mutes_the_highlight_rows() {
use crossterm::style::{Attribute, Color, SetAttribute, SetBackgroundColor};

fn sgr(cmd: impl crossterm::Command) -> String {
let mut s = String::new();
cmd.write_ansi(&mut s).unwrap();
s
}
let reverse = sgr(SetAttribute(Attribute::Reverse));
let muted = sgr(SetBackgroundColor(Color::DarkGrey));

let mut app = App::new_local(30, 100);
let inv = app.invocation_dir.clone();
app.spawn_in("sleep 5", inv);
app.pump();
app.selected_id = Some(1);

// The dashboard's only reverse-video line is the selected row, so its
// presence tracks `rev` exactly.
let focused = painted(&mut app);
assert!(focused.contains(&reverse), "{focused:?}");
assert!(!focused.contains(&muted), "{focused:?}");

app.terminal_focused = false;
let away = painted(&mut app);
assert!(away.contains(&muted), "{away:?}");
assert!(!away.contains(&reverse), "{away:?}");

// Regaining focus restores the live highlight.
app.terminal_focused = true;
assert!(painted(&mut app).contains(&reverse));
}

/// `?` opens the overlay; `?`, `Esc`, and `q` each close it.
#[test]
fn controls_overlay_opens_on_question_and_closes_on_peeks_key_set() {
Expand Down
16 changes: 11 additions & 5 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,8 +242,8 @@ fn run() -> io::Result<()> {
// Keyboard enhancement distinguishes modified Enter; bracketed paste
// delivers the clipboard as one event. Keyboard flags are screen-specific,
// so enable them after entering the alternate screen. Mouse capture is
// managed by `App::sync_input_modes`. Save and enable alternate scroll;
// restoration occurs in `restore_terminal`.
// managed by `App::sync_input_modes`. Save and enable alternate scroll
// and focus reporting; restoration occurs in `restore_terminal`.
if kitty {
execute!(
out,
Expand All @@ -253,7 +253,11 @@ fn run() -> io::Result<()> {
// setup actually did, not what it attempted.
KITTY_PUSHED.store(true, Ordering::Relaxed);
}
execute!(out, EnableBracketedPaste, Print("\x1b[?1007s\x1b[?1007h"))?;
execute!(
out,
EnableBracketedPaste,
Print("\x1b[?1007s\x1b[?1007h\x1b[?1004s\x1b[?1004h")
)?;
// `fleetcom [--foreground] <session>` loads that session at startup; the
// result shows in the status line.
if let Some(name) = &session {
Expand Down Expand Up @@ -303,7 +307,7 @@ fn emit_restore_sequences(out: &mut impl io::Write, kitty_pushed: bool) -> io::R
out,
DisableMouseCapture,
DisableBracketedPaste,
Print("\x1b[?1007r"),
Print("\x1b[?1007r\x1b[?1004r"),
Show,
LeaveAlternateScreen
)
Expand Down Expand Up @@ -484,8 +488,10 @@ mod tests {
for out in [&pushed, &unpushed] {
assert!(contains(out, &leave));
assert!(contains(out, &show));
// Alternate-scroll restore is a raw Print, not a crossterm command.
// Alternate-scroll and focus-reporting restores are raw Prints,
// not crossterm commands.
assert!(contains(out, b"\x1b[?1007r"));
assert!(contains(out, b"\x1b[?1004r"));
}
}
}
35 changes: 21 additions & 14 deletions src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use std::{
use crossterm::{
cursor::{Hide, MoveTo, Show},
queue,
style::{Attribute, Print, SetAttribute},
style::{Attribute, Color, Print, SetAttribute, SetBackgroundColor},
terminal::{BeginSynchronizedUpdate, EndSynchronizedUpdate},
};

Expand Down Expand Up @@ -87,15 +87,16 @@ fn dim(out: &mut impl Write, y: u16, s: &str, cols: usize) -> io::Result<()> {
)
}

/// Paint a full-width reverse-video line: selection and focused-field styling.
fn rev(out: &mut impl Write, y: u16, s: &str, cols: usize) -> io::Result<()> {
queue!(
out,
MoveTo(0, y),
SetAttribute(Attribute::Reverse),
Print(pad(s, cols)),
SetAttribute(Attribute::Reset)
)
/// Paint a full-width highlight line: selection and focused-field styling.
fn rev(out: &mut impl Write, y: u16, s: &str, cols: usize, focused: bool) -> io::Result<()> {
queue!(out, MoveTo(0, y))?;
// When the host terminal is unfocused, the highlight is muted to a dark-grey
if focused {
queue!(out, SetAttribute(Attribute::Reverse))?;
} else {
queue!(out, SetBackgroundColor(Color::DarkGrey))?;
}
queue!(out, Print(pad(s, cols)), SetAttribute(Attribute::Reset))
}

fn render_dashboard(out: &mut impl Write, app: &App) -> io::Result<()> {
Expand Down Expand Up @@ -183,7 +184,7 @@ fn render_dashboard(out: &mut impl Write, app: &App) -> io::Result<()> {
Row::Task(ti) => {
let v = &app.views[*ti];
if app.selected_id == Some(v.id) {
rev(out, y, &task_row(v, cols), cols)?;
rev(out, y, &task_row(v, cols), cols, app.terminal_focused)?;
} else if v.preview.source == PreviewSource::Marker {
// The marker is a placeholder, not output: dim the
// preview cell so it reads as metadata.
Expand Down Expand Up @@ -666,7 +667,7 @@ fn render_panel(out: &mut impl Write, app: &App, p: &Panel) -> io::Result<()> {
let panel_h = (body + 2) as u16;
let top = rows.saturating_sub(panel_h).max(2);

rev(out, top, &p.header, cols)?;
rev(out, top, &p.header, cols, app.terminal_focused)?;

if total == 0 {
if let Some(msg) = p.empty {
Expand All @@ -679,7 +680,7 @@ fn render_panel(out: &mut impl Write, app: &App, p: &Panel) -> io::Result<()> {
let marker = if idx == p.sel { "▸ " } else { " " };
let line = format!(" {marker}{}", p.labels[idx]);
if idx == p.sel {
rev(out, y, &line, cols)?;
rev(out, y, &line, cols, app.terminal_focused)?;
} else {
put(out, y, &line, cols)?;
}
Expand Down Expand Up @@ -929,7 +930,13 @@ fn render_attached(out: &mut impl Write, app: &App) -> io::Result<()> {
let cols = app.cols as usize;
let title = attached_title(v);
let bar = attached_bar(&title, screen.map_or(0, |s| s.scrollback), app.notice());
rev(out, app.rows.saturating_sub(1), &bar, cols)?;
rev(
out,
app.rows.saturating_sub(1),
&bar,
cols,
app.terminal_focused,
)?;

// Place the real cursor where the child's is, so typing feels native.
match screen {
Expand Down