From 5e5b9a8e72e32f924c98fd278998124d162c8cdb Mon Sep 17 00:00:00 2001 From: ai Date: Fri, 3 Apr 2026 13:13:25 +0000 Subject: [PATCH] fix: draw TUI to /dev/tty instead of stdout for subshell compat --- rust/ais/src/main.rs | 50 +++++++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/rust/ais/src/main.rs b/rust/ais/src/main.rs index dd0203e..14440c5 100644 --- a/rust/ais/src/main.rs +++ b/rust/ais/src/main.rs @@ -4,7 +4,7 @@ use std::collections::HashSet; use std::env; -use std::io::{Read, Write, stdout, stdin}; +use std::io::{Read, Write, stdin}; use std::process::Command; fn main() { @@ -261,25 +261,37 @@ fn fuzzy_match(item: &str, query: &str) -> bool { .all(|w| lower.contains(&w.to_lowercase())) } +fn open_tty() -> Option { + std::fs::OpenOptions::new() + .read(true) + .write(true) + .open("/dev/tty") + .ok() +} + fn fuzzy_select(items: &[String], label: &str) -> Option { + let mut tty = match open_tty() { + Some(f) => f, + None => return None, + }; let mut query = String::new(); let mut cursor: usize = 0; - // save terminal state and enter raw mode + // save terminal state and enter raw mode via /dev/tty let _ = Command::new("stty") .args(["-echo", "raw"]) - .stdin(std::process::Stdio::inherit()) + .stdin(std::process::Stdio::from(tty.try_clone().unwrap())) .status(); - let result = run_select_loop(items, label, &mut query, &mut cursor); + let result = run_select_loop(items, label, &mut query, &mut cursor, &mut tty); // restore terminal let _ = Command::new("stty") .args(["echo", "-raw"]) - .stdin(std::process::Stdio::inherit()) + .stdin(std::process::Stdio::from(tty.try_clone().unwrap())) .status(); - print!("\x1b[2J\x1b[H"); - let _ = stdout().flush(); + let _ = write!(tty, "\x1b[2J\x1b[H"); + let _ = tty.flush(); result } @@ -289,6 +301,7 @@ fn run_select_loop( label: &str, query: &mut String, cursor: &mut usize, + tty: &mut std::fs::File, ) -> Option { loop { let filtered: Vec<&String> = items.iter().filter(|i| fuzzy_match(i, query)).collect(); @@ -299,12 +312,11 @@ fn run_select_loop( *cursor = filtered.len() - 1; } - // draw - let mut out = stdout(); - let _ = write!(out, "\x1b[2J\x1b[H"); - let _ = write!(out, "\x1b[33m{}\x1b[0m> {}\r\n", label, query); + // draw to /dev/tty (not stdout, which may be captured by subshell) + let _ = write!(tty, "\x1b[2J\x1b[H"); + let _ = write!(tty, "\x1b[33m{}\x1b[0m> {}\r\n", label, query); let _ = write!( - out, + tty, "\x1b[90m{} items\x1b[0m\r\n", filtered.len() ); @@ -312,25 +324,21 @@ fn run_select_loop( let max_show = 20; for (i, item) in filtered.iter().enumerate().take(max_show) { if i == *cursor { - let _ = write!(out, "\x1b[7m {}\x1b[0m\r\n", item); + let _ = write!(tty, "\x1b[7m {}\x1b[0m\r\n", item); } else { - let _ = write!(out, " {}\r\n", item); + let _ = write!(tty, " {}\r\n", item); } } if filtered.len() > max_show { let _ = write!( - out, + tty, " \x1b[90m+{} more\x1b[0m\r\n", filtered.len() - max_show ); } - let _ = out.flush(); + let _ = tty.flush(); - // read key from /dev/tty (works even when stdin is piped) - let mut tty = match std::fs::File::open("/dev/tty") { - Ok(f) => f, - Err(_) => return None, - }; + // read key from /dev/tty let mut buf = [0u8; 3]; let n = tty.read(&mut buf).unwrap_or(0); if n == 0 {