aboutsummaryrefslogtreecommitdiffstats
path: root/crates
diff options
context:
space:
mode:
authorLucas Trzesniewski <lucas.trzesniewski@gmail.com>2026-03-20 08:11:45 +0100
committerGitHub <noreply@github.com>2026-03-20 07:11:45 +0000
commitde5ec0bec09a3e50b193c937f71388a737cb582a (patch)
treea008d972f61a58eec2263d37701c7255615aa64d /crates
parentdocs: add inline_height_shell_up_key_binding (#3270) (diff)
downloadatuin-de5ec0bec09a3e50b193c937f71388a737cb582a.zip
feat: Allow running `atuin search -i` as subcommand on Windows (#3250)
This is the equivalent of #3208, but for Windows. ~The rendering performance is noticeably slower in this mode when refreshing a large part of the screen, but it's better than not having the feature at all.~ Fixed 🙂 The second commit fixes some unrelated warnings. /cc @BinaryMuse FYI ## Checks - [x] I am happy for maintainers to push small adjustments to this PR, to speed up the review cycle - [x] I have checked that there are no existing pull requests for the same thing
Diffstat (limited to 'crates')
-rw-r--r--crates/atuin-ai/src/commands/inline.rs2
-rw-r--r--crates/atuin/Cargo.toml5
-rw-r--r--crates/atuin/src/command/client/search.rs3
-rw-r--r--crates/atuin/src/command/client/search/engines/daemon.rs2
-rw-r--r--crates/atuin/src/command/client/search/interactive.rs119
5 files changed, 95 insertions, 36 deletions
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