diff --git a/AGENTS.md b/AGENTS.md index d00c180..0fd0493 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -147,5 +147,6 @@ Edit `FONT_SIZE_PT` in `src/config.rs`. The renderer scales by the window's DPI - **DPI**: `Renderer` stores physical-pixel sizes. Window resize events from winit give physical pixels. `LogicalSize` is only used for the initial window creation. - **PTY slave lifetime**: drop `pair.slave` after `spawn_command` — keeping it open prevents the reader thread from ever seeing EOF when the shell exits. - **Alternate screen**: `alt_grid` and `alt_saved_cursor` are separate from the normal grid. Operations on the live grid (scrollback push, viewport snap) must check `alt_screen` and skip when true. +- **tjson cwd**: `CommandBuilder::new()` defaults to the process's home directory, not `current_dir()`. Always call `cmd.cwd(std::env::current_dir()?)` so the spawned command runs in the same directory as the shell invoking `tjson`. - **UTF-8 in tjson**: `BufReader::lines()` requires valid UTF-8. If the PTY output contains raw C1 bytes (0x80–0x9F) that aren't part of a multi-byte sequence, `lines()` will error and the loop will break. Be aware of this if expanding `run_pty`. - **GPU buffer growth**: `rect_buf` and `glyph_buf` are grown on demand by reallocating. The capacity is tracked in `rect_buf_cap` / `glyph_buf_cap`. Don't assume a fixed size. diff --git a/src/bin/tjson.rs b/src/bin/tjson.rs index 4d03b83..3268cd7 100644 --- a/src/bin/tjson.rs +++ b/src/bin/tjson.rs @@ -14,7 +14,7 @@ //! pnpm dev | tjson //! json pnpm dev # via the shell alias (preferred — preserves TTY) -use std::io::{self, BufRead, BufReader, Write}; +use std::io::{self, BufRead, Read, Write}; use portable_pty::{native_pty_system, CommandBuilder, PtySize}; use syntect::easy::HighlightLines; @@ -140,29 +140,62 @@ fn run_pty( for arg in &args[1..] { cmd.arg(arg); } + if let Ok(cwd) = std::env::current_dir() { + cmd.cwd(cwd); + } + // Force UTF-8 locale so multi-byte characters aren't re-encoded via Mac Roman. + cmd.env("LANG", "en_US.UTF-8"); + cmd.env("LC_ALL", "en_US.UTF-8"); let mut child = pair.slave.spawn_command(cmd).expect("spawn failed"); drop(pair.slave); // child owns the slave end // PTY master gives us combined stdout+stderr from the child. - let reader = pair.master.try_clone_reader().expect("clone reader"); + let mut reader = pair.master.try_clone_reader().expect("clone reader"); let stdout = io::stdout(); - let mut out = io::BufWriter::new(stdout.lock()); + let mut out = stdout.lock(); let mut drain = false; - for line in BufReader::new(reader).lines() { - let line = match line { - Ok(l) => l, - Err(_) => break, + // Read raw bytes to avoid String round-trips that can corrupt multi-byte + // sequences when the locale isn't UTF-8. + let mut buf = Vec::new(); + let mut chunk = [0u8; 4096]; + loop { + let n = match reader.read(&mut chunk) { + Ok(0) | Err(_) => break, + Ok(n) => n, }; - if drain { continue; } - // PTY line endings are \r\n; BufRead::lines strips \n but leaves \r. - let line = line.strip_suffix('\r').unwrap_or(&line).to_owned(); - process_line(&line, &mut out, ps, syntax, theme, &mut drain); + buf.extend_from_slice(&chunk[..n]); + + // Process all complete lines (ending with \n) in the buffer. + while let Some(pos) = buf.iter().position(|&b| b == b'\n') { + let mut line_bytes = buf.drain(..=pos).collect::>(); + // Strip trailing \n and \r\n. + if line_bytes.last() == Some(&b'\n') { line_bytes.pop(); } + if line_bytes.last() == Some(&b'\r') { line_bytes.pop(); } + + if drain { continue; } + + // Try to interpret as UTF-8 for JSON detection; fall back to raw write. + match std::str::from_utf8(&line_bytes) { + Ok(line) => process_line(line, &mut out, ps, syntax, theme, &mut drain), + Err(_) => { + // Not valid UTF-8 — write raw bytes unchanged. + let _ = out.write_all(&line_bytes); + let _ = out.write_all(b"\n"); + } + } + } } - let _ = out.flush(); + // Flush any remaining bytes that had no trailing newline. + if !buf.is_empty() && !drain { + match std::str::from_utf8(&buf) { + Ok(line) => process_line(line, &mut out, ps, syntax, theme, &mut drain), + Err(_) => { let _ = out.write_all(&buf); } + } + } let exit_code = match child.wait() { Ok(status) => if status.success() { 0 } else { 1 },