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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ because `src/main.rs` is **~3000 lines** — reading it linearly is wasteful.
<!-- gitnexus:start -->
# GitNexus — Code Intelligence

This project is indexed by GitNexus as **LibreCommander** (3149 symbols, 11728 relationships, 276 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
This project is indexed by GitNexus as **LibreCommander** (3153 symbols, 11739 relationships, 277 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.

> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.

Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Read AGENTS.md before proceeding.
<!-- gitnexus:start -->
# GitNexus — Code Intelligence

This project is indexed by GitNexus as **LibreCommander** (3149 symbols, 11728 relationships, 276 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
This project is indexed by GitNexus as **LibreCommander** (3153 symbols, 11739 relationships, 277 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.

> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.

Expand Down
117 changes: 78 additions & 39 deletions src/ui/viewer/loader.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use std::io::Read;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Output, Stdio};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, mpsc};
use std::thread::{self, JoinHandle};
Expand All @@ -9,6 +11,8 @@ use ratatui::text::Text;

use super::open::ViewerState;

const CHAFA_TIMEOUT: Duration = Duration::from_secs(10);

pub struct ViewerLoader {
pub receiver: mpsc::Receiver<std::io::Result<ViewerState>>,
pub cancel: Arc<AtomicBool>,
Expand Down Expand Up @@ -47,60 +51,95 @@ impl Drop for ViewerLoader {
}
}

const CHAFA_TIMEOUT: Duration = Duration::from_secs(10);

pub fn run_chafa(path: &Path, width: u16, height: u16) -> Text<'static> {
let size_str = format!("{}x{}", width, height);
let mut child = match std::process::Command::new("chafa")
// Keep terminal probing and passthrough disabled. If chafa talks directly
// to the terminal, crossterm can read those responses as viewer input and
// open the search dialog instead of showing the image preview.
let child = Command::new("chafa")
.arg("-f")
.arg("symbols")
.arg("--probe")
.arg("off")
.arg("--passthrough")
.arg("none")
.arg("--polite")
.arg("on")
.arg("--size")
.arg(&size_str)
.arg("--")
.arg(path)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
{
Ok(c) => c,
Err(e) => return Text::raw(format!("Failed to execute chafa (is it installed?): {}", e)),
};
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn();

match child.and_then(wait_for_chafa_output) {
Ok(out) if out.status.success() => match out.stdout.into_text() {
Ok(text) => text,
Err(e) => Text::raw(format!("Failed to parse ANSI: {}", e)),
},
Ok(out) => {
let err_msg = String::from_utf8_lossy(&out.stderr);
Text::raw(format!("Chafa error: {}", err_msg))
}
Err(e) => Text::raw(format!("Failed to execute chafa (is it installed?): {}", e)),
}
Comment on lines 54 to 87
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Missing timeout — background threads can block indefinitely

The old implementation used CHAFA_TIMEOUT (10 s) with an explicit child.kill() + child.wait() fallback. The new code calls .output(), which blocks until the child exits. If chafa hangs on a malformed or very large image, the ImagePreviewLoader background thread never unblocks. The Drop impl drops the JoinHandle without joining it (let _ = self._handle.take()), so the thread is simply orphaned. Rapid file-panel navigation across many images could accumulate tens of stuck threads, each holding OS resources until the chafa child eventually exits (or the process does).

}

fn wait_for_chafa_output(mut child: Child) -> std::io::Result<Output> {
let stdout = child.stdout.take();
let stderr = child.stderr.take();
let stdout_reader = read_pipe_in_background(stdout);
let stderr_reader = read_pipe_in_background(stderr);
let deadline = Instant::now() + CHAFA_TIMEOUT;

loop {
match child.try_wait() {
Ok(Some(status)) => {
let out = child
.wait_with_output()
.unwrap_or_else(|_| std::process::Output {
status,
stdout: Vec::new(),
stderr: Vec::new(),
});
if out.status.success() {
return match out.stdout.into_text() {
Ok(text) => text,
Err(e) => Text::raw(format!("Failed to parse ANSI: {}", e)),
};
}
let err_msg = String::from_utf8_lossy(&out.stderr);
return Text::raw(format!("Chafa error: {}", err_msg));
}
Ok(None) => {
if Instant::now() >= deadline {
let _ = child.kill();
let _ = child.wait();
return Text::raw("Chafa timed out".to_string());
}
thread::sleep(Duration::from_millis(50));
}
Err(e) => {
return Text::raw(format!("Chafa wait error: {}", e));
}
if let Some(status) = child.try_wait()? {
let stdout = join_pipe_reader(stdout_reader);
let stderr = join_pipe_reader(stderr_reader);
return Ok(Output {
status,
stdout,
stderr,
});
}
if Instant::now() >= deadline {
let _ = child.kill();
let status = child.wait()?;
let stdout = join_pipe_reader(stdout_reader);
let stderr = join_pipe_reader(stderr_reader);
return Ok(Output {
status,
stdout,
stderr: if stderr.is_empty() {
b"Chafa timed out".to_vec()
} else {
stderr
},
});
}
thread::sleep(Duration::from_millis(50));
}
}

fn read_pipe_in_background<R>(pipe: Option<R>) -> JoinHandle<Vec<u8>>
where
R: Read + Send + 'static,
{
thread::spawn(move || {
let mut bytes = Vec::new();
if let Some(mut pipe) = pipe {
let _ = pipe.read_to_end(&mut bytes);
}
bytes
})
}

fn join_pipe_reader(handle: JoinHandle<Vec<u8>>) -> Vec<u8> {
handle.join().unwrap_or_default()
}

pub struct ImagePreviewLoader {
pub file_path: PathBuf,
pub(crate) receiver: mpsc::Receiver<(u16, u16, Text<'static>)>,
Expand Down
Loading