mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
fix(tui): handle EINTR and cleanup stale worktrees
This commit is contained in:
+62
-41
@@ -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
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user