diff options
| -rw-r--r-- | Cargo.lock | 1 | ||||
| -rw-r--r-- | crates/atuin-ai/src/commands/inline.rs | 2 | ||||
| -rw-r--r-- | crates/atuin/Cargo.toml | 5 | ||||
| -rw-r--r-- | crates/atuin/src/command/client/search.rs | 3 | ||||
| -rw-r--r-- | crates/atuin/src/command/client/search/engines/daemon.rs | 2 | ||||
| -rw-r--r-- | crates/atuin/src/command/client/search/interactive.rs | 119 |
6 files changed, 96 insertions, 36 deletions
@@ -272,6 +272,7 @@ dependencies = [ "tracing-tree", "unicode-width 0.2.2", "uuid", + "windows-sys 0.61.2", ] [[package]] diff --git a/crates/atuin-ai/src/commands/inline.rs b/crates/atuin-ai/src/commands/inline.rs index af1d2137..fe6327a5 100644 --- a/crates/atuin-ai/src/commands/inline.rs +++ b/crates/atuin-ai/src/commands/inline.rs @@ -457,7 +457,7 @@ async fn run_inline_tui( #[cfg(unix)] let mut popup_state = crate::tui::popup::try_setup_popup(); #[cfg(not(unix))] - let mut popup_state: Option<()> = None; + let popup_state: Option<()> = None; let popup_mode = popup_state.is_some(); diff --git a/crates/atuin/Cargo.toml b/crates/atuin/Cargo.toml index b7950faa..c01d5e22 100644 --- a/crates/atuin/Cargo.toml +++ b/crates/atuin/Cargo.toml @@ -99,12 +99,15 @@ arboard = { version = "3.4", optional = true } [target.'cfg(target_os = "linux")'.dependencies] arboard = { version = "3.4", optional = true, features = [ - "wayland-data-control", + "wayland-data-control", ] } [target.'cfg(unix)'.dependencies] daemonize = "0.5.0" +[target.'cfg(windows)'.dependencies] +windows-sys = { version = "0.61.2", features = ["Win32_System_Console"] } + [dev-dependencies] tracing-tree = "0.4" diff --git a/crates/atuin/src/command/client/search.rs b/crates/atuin/src/command/client/search.rs index d05b1c24..7c72e13d 100644 --- a/crates/atuin/src/command/client/search.rs +++ b/crates/atuin/src/command/client/search.rs @@ -228,7 +228,8 @@ impl Cmd { write!(file, "{item}")?; } else if !stdout().is_terminal() { // stdout is not a terminal - likely command substitution like VAR=$(atuin search -i) - // Write to stdout so it gets captured + // Write to stdout so it gets captured. This requires some care on Windows, as the current + // console code page or `[Console]::OutputEncoding` on PowerShell may be different from UTF-8. println!("{item}"); } else if stderr().is_terminal() { eprintln!("{}", item.escape_control()); diff --git a/crates/atuin/src/command/client/search/engines/daemon.rs b/crates/atuin/src/command/client/search/engines/daemon.rs index 9518fcb2..c5de39ab 100644 --- a/crates/atuin/src/command/client/search/engines/daemon.rs +++ b/crates/atuin/src/command/client/search/engines/daemon.rs @@ -18,6 +18,7 @@ use super::{SearchEngine, SearchState}; pub struct Search { client: Option<SearchClient>, query_id: u64, + #[cfg(unix)] socket_path: String, #[cfg(not(unix))] tcp_port: u64, @@ -28,6 +29,7 @@ impl Search { Search { client: None, query_id: 0, + #[cfg(unix)] socket_path: settings.daemon.socket_path.clone(), #[cfg(not(unix))] tcp_port: settings.daemon.tcp_port, diff --git a/crates/atuin/src/command/client/search/interactive.rs b/crates/atuin/src/command/client/search/interactive.rs index bba64d78..74600520 100644 --- a/crates/atuin/src/command/client/search/interactive.rs +++ b/crates/atuin/src/command/client/search/interactive.rs @@ -52,6 +52,9 @@ use ratatui::crossterm::event::{ KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags, }; +#[cfg(windows)] +use windows_sys::Win32::System::Console::{GetConsoleOutputCP, SetConsoleOutputCP}; + const TAB_TITLES: [&str; 2] = ["Search", "Inspect"]; pub enum InputAction { @@ -1268,6 +1271,62 @@ enum TerminalWriter { Stdout(std::io::Stdout), #[cfg(unix)] Tty(std::fs::File), + #[cfg(windows)] + ConOut(std::io::LineWriter<std::fs::File>, u32), +} + +impl TerminalWriter { + #[cfg(windows)] + const CP_UTF8: u32 = 65001; + + fn new() -> std::io::Result<Self> { + let stdout = stdout(); + if stdout.is_terminal() { + return Ok(TerminalWriter::Stdout(stdout)); + } + + // If stdout is not a terminal (e.g., captured by command substitution), + // fall back to /dev/tty so the TUI can still render. + // This allows usage like: VAR=$(atuin search -i) + #[cfg(unix)] + { + Ok(TerminalWriter::Tty( + std::fs::File::options() + .read(true) + .write(true) + .open("/dev/tty")?, + )) + } + + // On Windows, use CONOUT$ which is the equivalent of /dev/tty, but this + // requires setting the current console output code page to UTF-8 for the + // TUI to render properly. We'll set it back to its previous value upon exit. + #[cfg(windows)] + { + let file = std::fs::File::options() + .read(true) + .write(true) + .open("CONOUT$")?; + + let initial_console_output_cp = unsafe { GetConsoleOutputCP() }; + if initial_console_output_cp != Self::CP_UTF8 { + unsafe { + SetConsoleOutputCP(Self::CP_UTF8); + } + } + + Ok(TerminalWriter::ConOut( + std::io::LineWriter::new(file), + initial_console_output_cp, + )) + } + + #[cfg(not(any(unix, windows)))] + Err(std::io::Error::new( + std::io::ErrorKind::Unsupported, + "Interactive mode requires a terminal", + )) + } } impl Write for TerminalWriter { @@ -1276,6 +1335,8 @@ impl Write for TerminalWriter { TerminalWriter::Stdout(stdout) => stdout.write(buf), #[cfg(unix)] TerminalWriter::Tty(file) => file.write(buf), + #[cfg(windows)] + TerminalWriter::ConOut(writer, _) => writer.write(buf), } } @@ -1284,6 +1345,21 @@ impl Write for TerminalWriter { TerminalWriter::Stdout(stdout) => stdout.flush(), #[cfg(unix)] TerminalWriter::Tty(file) => file.flush(), + #[cfg(windows)] + TerminalWriter::ConOut(writer, _) => writer.flush(), + } + } +} + +impl Drop for TerminalWriter { + fn drop(&mut self) { + #[cfg(windows)] + if let TerminalWriter::ConOut(_, initial_console_output_cp) = self + && *initial_console_output_cp != Self::CP_UTF8 + { + unsafe { + SetConsoleOutputCP(*initial_console_output_cp); + } } } } @@ -1406,32 +1482,10 @@ struct Stdout { } impl Stdout { - pub fn new(inline_mode: bool, stdout_is_terminal: bool) -> std::io::Result<Self> { + pub fn new(inline_mode: bool) -> std::io::Result<Self> { terminal::enable_raw_mode()?; - // If stdout is not a terminal (e.g., captured by command substitution), - // fall back to /dev/tty so the TUI can still render. - // This allows usage like: VAR=$(atuin search -i) - let mut writer = if stdout_is_terminal { - TerminalWriter::Stdout(stdout()) - } else { - #[cfg(unix)] - { - TerminalWriter::Tty( - std::fs::File::options() - .read(true) - .write(true) - .open("/dev/tty")?, - ) - } - #[cfg(not(unix))] - { - return Err(std::io::Error::new( - std::io::ErrorKind::Unsupported, - "Interactive mode requires a terminal", - )); - } - }; + let mut writer = TerminalWriter::new()?; if !inline_mode { execute!(writer, terminal::EnterAlternateScreen)?; @@ -1497,6 +1551,7 @@ impl Write for Stdout { /// of lines the caller should scroll the terminal up before rendering. /// /// This function performs no I/O — it is a pure computation. +#[cfg(unix)] fn compute_popup_placement( cursor_row: u16, term_rows: u16, @@ -1545,15 +1600,13 @@ pub async fn history( settings.inline_height }; - // Check if stdout is a terminal - if not (e.g., command substitution like VAR=$(atuin search -i)), - // we need to use /dev/tty for the TUI and force fullscreen mode (inline mode requires - // cursor position queries that don't work when stdout is captured) - let stdout_is_terminal = stdout().is_terminal(); - // Use fullscreen mode if the inline height doesn't fit in the terminal, // this will preserve the scroll position upon exit. - // Also force fullscreen when stdout isn't a terminal (inline mode won't work). - let inline_height = if !stdout_is_terminal { + // Also force fullscreen when stdout isn't a terminal (e.g., command substitution + // like VAR=$(atuin search -i)). In that case, we need to use /dev/tty for the TUI and force + // fullscreen mode (inline mode won't work as it requires cursor position queries + // that don't work when stdout is captured). + let inline_height = if !stdout().is_terminal() { 0 } else if let Ok(size) = terminal::size() && inline_height >= size.1 @@ -1598,12 +1651,12 @@ pub async fn history( }; #[cfg(not(unix))] - let (saved_screen, popup_rect, popup_scroll_offset): (Option<()>, Rect, u16) = + let (saved_screen, popup_rect, _popup_scroll_offset): (Option<()>, Rect, u16) = (None, Rect::default(), 0); let popup_mode = saved_screen.is_some(); - let stdout = Stdout::new(inline_height > 0, stdout_is_terminal)?; + let stdout = Stdout::new(inline_height > 0)?; // In popup mode, clear the popup region on the physical terminal before // ratatui takes over. Ratatui's diff-based rendering compares against an |
