fix(tui): handle EINTR and cleanup stale worktrees

This commit is contained in:
James Peter
2025-09-26 21:28:38 +10:00
parent 12dea0b860
commit da2840b8cc
2 changed files with 118 additions and 48 deletions
+62 -41
View File
@@ -243,52 +243,73 @@ impl App<'_> {
// back off to reduce CPU when idle.
let hot_typing = Instant::now().duration_since(last_key_time) <= Duration::from_millis(250);
let poll_timeout = if hot_typing { Duration::from_millis(2) } else { Duration::from_millis(10) };
if let Ok(true) = crossterm::event::poll(poll_timeout) {
if let Ok(event) = crossterm::event::read() {
match event {
crossterm::event::Event::Key(key_event) => {
// Some Windows terminals (e.g., legacy conhost) only report
// `Release` events when keyboard enhancement flags are not
// supported. Preserve those events so onboarding works there.
if !drop_release_events
|| matches!(key_event.kind, KeyEventKind::Press | KeyEventKind::Repeat)
{
last_key_time = Instant::now();
app_event_tx.send(AppEvent::KeyEvent(key_event));
match crossterm::event::poll(poll_timeout) {
Ok(true) => match crossterm::event::read() {
Ok(event) => {
match event {
crossterm::event::Event::Key(key_event) => {
// Some Windows terminals (e.g., legacy conhost) only report
// `Release` events when keyboard enhancement flags are not
// supported. Preserve those events so onboarding works there.
if !drop_release_events
|| matches!(key_event.kind, KeyEventKind::Press | KeyEventKind::Repeat)
{
last_key_time = Instant::now();
app_event_tx.send(AppEvent::KeyEvent(key_event));
}
}
crossterm::event::Event::Resize(_, _) => {
app_event_tx.send(AppEvent::RequestRedraw);
}
// When the terminal/tab regains focus, issue a redraw.
// Some terminals clear the alt‑screen buffer on focus switches,
// which can leave the status bar and inline images blank until
// the next resize. A focus‑gain repaint fixes this immediately.
crossterm::event::Event::FocusGained => {
app_event_tx.send(AppEvent::RequestRedraw);
}
crossterm::event::Event::FocusLost => {
// No action needed; keep state as‑is.
}
crossterm::event::Event::Paste(pasted) => {
// Many terminals convert newlines to \r when pasting (e.g., iTerm2),
// but tui-textarea expects \n. Normalize CR to LF.
// [tui-textarea]: https://github.com/rhysd/tui-textarea/blob/4d18622eeac13b309e0ff6a55a46ac6706da68cf/src/textarea.rs#L782-L783
// [iTerm2]: https://github.com/gnachman/iTerm2/blob/5d0c0d9f68523cbd0494dad5422998964a2ecd8d/sources/iTermPasteHelper.m#L206-L216
let pasted = pasted.replace("\r", "\n");
app_event_tx.send(AppEvent::Paste(pasted));
}
crossterm::event::Event::Mouse(mouse_event) => {
app_event_tx.send(AppEvent::MouseEvent(mouse_event));
}
// All other event variants are explicitly handled above.
}
crossterm::event::Event::Resize(_, _) => {
app_event_tx.send(AppEvent::RequestRedraw);
}
Err(err) => {
if err.kind() == std::io::ErrorKind::Interrupted {
continue;
}
// When the terminal/tab regains focus, issue a redraw.
// Some terminals clear the alt‑screen buffer on focus switches,
// which can leave the status bar and inline images blank until
// the next resize. A focus‑gain repaint fixes this immediately.
crossterm::event::Event::FocusGained => {
app_event_tx.send(AppEvent::RequestRedraw);
}
crossterm::event::Event::FocusLost => {
// No action needed; keep state as‑is.
}
crossterm::event::Event::Paste(pasted) => {
// Many terminals convert newlines to \r when pasting (e.g., iTerm2),
// but tui-textarea expects \n. Normalize CR to LF.
// [tui-textarea]: https://github.com/rhysd/tui-textarea/blob/4d18622eeac13b309e0ff6a55a46ac6706da68cf/src/textarea.rs#L782-L783
// [iTerm2]: https://github.com/gnachman/iTerm2/blob/5d0c0d9f68523cbd0494dad5422998964a2ecd8d/sources/iTermPasteHelper.m#L206-L216
let pasted = pasted.replace("\r", "\n");
app_event_tx.send(AppEvent::Paste(pasted));
}
crossterm::event::Event::Mouse(mouse_event) => {
app_event_tx.send(AppEvent::MouseEvent(mouse_event));
}
// All other event variants are explicitly handled above.
tracing::error!("input thread failed to read event: {err}");
input_running_thread.store(false, Ordering::Release);
app_event_tx.send(AppEvent::ExitRequest);
break;
}
},
Ok(false) => {
// Timeout expired, no `Event` is available. If the user is typing
// keep the loop hot; otherwise sleep briefly to cut idle CPU.
if !hot_typing {
std::thread::sleep(Duration::from_millis(5));
}
}
} else {
// Timeout expired, no `Event` is available. If the user is typing
// keep the loop hot; otherwise sleep briefly to cut idle CPU.
if !hot_typing {
std::thread::sleep(Duration::from_millis(5));
Err(err) => {
if err.kind() == std::io::ErrorKind::Interrupted {
continue;
}
tracing::error!("input thread failed to poll events: {err}");
input_running_thread.store(false, Ordering::Release);
app_event_tx.send(AppEvent::ExitRequest);
break;
}
}
}
+56 -7
View File
@@ -403,12 +403,58 @@ fn print_timing_summary(summary: &str) {
#[allow(clippy::print_stdout, clippy::print_stderr)]
fn cleanup_session_worktrees_and_print() {
use std::process::Command;
let pid = std::process::id();
let home = match std::env::var_os("HOME") { Some(h) => std::path::PathBuf::from(h), None => return };
let session_dir = home.join(".code").join("working").join("_session");
cleanup_stale_session_files(&session_dir, pid);
let file = session_dir.join(format!("pid-{}.txt", pid));
let Ok(data) = std::fs::read_to_string(&file) else { return };
reclaim_worktrees_from_file(&file, "current session");
}
fn cleanup_stale_session_files(session_dir: &std::path::Path, current_pid: u32) {
let Ok(entries) = std::fs::read_dir(session_dir) else { return; };
for entry in entries.flatten() {
let path = entry.path();
if !path.is_file() { continue; }
let filename = match entry.file_name().into_string() {
Ok(name) => name,
Err(_) => continue,
};
let Some(pid_str) = filename.strip_prefix("pid-").and_then(|s| s.strip_suffix(".txt")) else { continue; };
let Ok(pid) = pid_str.parse::<u32>() else { continue; };
if pid == current_pid { continue; }
#[cfg(unix)]
let still_running = unsafe {
if libc::kill(pid as libc::pid_t, 0) == 0 {
true
} else {
matches!(
std::io::Error::last_os_error().raw_os_error(),
Some(code) if code != libc::ESRCH
)
}
};
#[cfg(not(unix))]
let still_running = true;
if still_running {
continue;
}
reclaim_worktrees_from_file(&path, &format!("stale pid {pid}"));
}
}
fn reclaim_worktrees_from_file(path: &std::path::Path, label: &str) {
use std::process::Command;
let Ok(data) = std::fs::read_to_string(path) else {
let _ = std::fs::remove_file(path);
return;
};
let mut entries: Vec<(std::path::PathBuf, std::path::PathBuf)> = Vec::new();
for line in data.lines() {
if line.trim().is_empty() { continue; }
@@ -416,22 +462,25 @@ fn cleanup_session_worktrees_and_print() {
entries.push((std::path::PathBuf::from(root_s), std::path::PathBuf::from(path_s)));
}
}
// Deduplicate paths in case of retries
use std::collections::HashSet;
let mut seen = HashSet::new();
entries.retain(|(_, p)| seen.insert(p.clone()));
if entries.is_empty() { let _ = std::fs::remove_file(&file); return; }
if entries.is_empty() {
let _ = std::fs::remove_file(path);
return;
}
eprintln!("Cleaning remaining worktrees ({}).", entries.len());
eprintln!("Cleaning remaining worktrees for {} ({}).", label, entries.len());
for (git_root, worktree) in entries {
let wt_str = match worktree.to_str() { Some(s) => s, None => continue };
let Some(wt_str) = worktree.to_str() else { continue };
let _ = Command::new("git")
.current_dir(&git_root)
.args(["worktree", "remove", wt_str, "--force"])
.output();
let _ = std::fs::remove_dir_all(&worktree);
}
let _ = std::fs::remove_file(&file);
let _ = std::fs::remove_file(path);
}
fn maybe_apply_terminal_theme_detection(config: &mut Config, theme_configured_explicitly: bool) {