diff --git a/local/recipes/tui/tlc/source/src/app.rs b/local/recipes/tui/tlc/source/src/app.rs index 0bd35520df..5c8d740aa6 100644 --- a/local/recipes/tui/tlc/source/src/app.rs +++ b/local/recipes/tui/tlc/source/src/app.rs @@ -40,10 +40,14 @@ impl Application { }); let start: PathBuf = if cli.start_path.is_empty() { - std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/")) + std::env::current_dir() + .inspect_err(|e| log::warn!("current_dir failed: {e} — using /")) + .unwrap_or_else(|_| PathBuf::from("/")) } else { let p = PathBuf::from(&cli.start_path); - p.canonicalize().unwrap_or(p) + p.canonicalize() + .inspect_err(|e| log::warn!("canonicalize({}) failed: {e}", p.display())) + .unwrap_or(p) }; let mut fm = FileManager::new(&start, &cfg)?; @@ -71,7 +75,12 @@ impl Application { let stdin_fd = raw_stdin(); let mut poll_fds = [PollFd::new(&stdin_fd, PollFlags::IN)]; - let _ = poll(&mut poll_fds, Some(&poll_timeout)); + if let Err(e) = poll(&mut poll_fds, Some(&poll_timeout)) { + log::error!("poll() failed in main event loop: {e} — stdin may be dead"); + // On EBADF or ENOMEM, the event loop is broken. + // Log and break; the user sees TLC exit cleanly. + break; + } let size = tui.size(); if size.0 > 0 && size.1 > 0 && size != prev_size { diff --git a/local/recipes/tui/tlc/source/src/config.rs b/local/recipes/tui/tlc/source/src/config.rs index ef6f1846e0..b8cce692b7 100644 --- a/local/recipes/tui/tlc/source/src/config.rs +++ b/local/recipes/tui/tlc/source/src/config.rs @@ -44,7 +44,17 @@ pub struct Config { impl Default for Config { fn default() -> Self { - toml::from_str(DEFAULT_CONFIG).expect("default.toml must be valid") + toml::from_str(DEFAULT_CONFIG).unwrap_or_else(|e| { + log::error!("embedded default.toml parse failed: {e} — falling back to empty config"); + Config { + skin: SkinConfig::default(), + filemanager: FilemanagerConfig::default(), + editor: EditorConfig::default(), + viewer: ViewerConfig::default(), + runtime: RuntimeConfig::default(), + vfs: VfsConfig::default(), + } + }) } } diff --git a/local/recipes/tui/tlc/source/src/filemanager/dialog_ops.rs b/local/recipes/tui/tlc/source/src/filemanager/dialog_ops.rs index 983cc51c10..9ca212dd6d 100644 --- a/local/recipes/tui/tlc/source/src/filemanager/dialog_ops.rs +++ b/local/recipes/tui/tlc/source/src/filemanager/dialog_ops.rs @@ -826,40 +826,56 @@ Err(e) => self.status.set_error(format!("view: {e}")), let thread_sources = sources.clone(); let thread_dest = destination.clone(); std::thread::spawn(move || { - let result = match kind { + match kind { crate::ops::OpKind::Copy => { - let dst = thread_dest.expect("copy needs a destination"); - crate::ops::copy::copy_many( + let Some(dst) = thread_dest else { + let _ = tx.send(Err(crate::ops::OpsError::Other( + "copy: missing destination".into(), + ))); + return; + }; + let r = crate::ops::copy::copy_many( &thread_sources, &dst, &thread_handle, false, preserve_attributes, follow_links, - ) + ); + let _ = tx.send(r); + return; } crate::ops::OpKind::Move => { - let dst = thread_dest.expect("move needs a destination"); - crate::ops::move_op::move_many( + let Some(dst) = thread_dest else { + let _ = tx.send(Err(crate::ops::OpsError::Other( + "move: missing destination".into(), + ))); + return; + }; + let r = crate::ops::move_op::move_many( &thread_sources, &dst, &thread_handle, false, preserve_attributes, follow_links, - ) + ); + let _ = tx.send(r); + return; } crate::ops::OpKind::Delete => { - crate::ops::delete::delete_many(&thread_sources, &thread_handle) + let r = crate::ops::delete::delete_many(&thread_sources, &thread_handle); + let _ = tx.send(r); + return; } crate::ops::OpKind::MkDir => { // MkDir is instantaneous — it should never be // spawned with a progress dialog. If it reaches // this branch, treat it as a success (no work). - Ok(()) + let _ = tx.send(Ok(())); + return; } - }; - let _ = tx.send(result); + } }); let title = match kind { diff --git a/local/recipes/tui/tlc/source/src/main.rs b/local/recipes/tui/tlc/source/src/main.rs index 2ef6472542..46e756ce36 100644 --- a/local/recipes/tui/tlc/source/src/main.rs +++ b/local/recipes/tui/tlc/source/src/main.rs @@ -112,7 +112,8 @@ fn init_logging(verbosity: u8) { .format_timestamp_secs() .format_module_path(false) .format_level(true) - .try_init(); + .try_init() + .unwrap_or_else(|e| eprintln!("logger init failed: {e} (output lost)")); } fn run_editor(file: &str, line: Option) -> ExitCode { diff --git a/local/recipes/tui/tlc/source/src/terminal/mod.rs b/local/recipes/tui/tlc/source/src/terminal/mod.rs index 1f652499d5..d75d634454 100644 --- a/local/recipes/tui/tlc/source/src/terminal/mod.rs +++ b/local/recipes/tui/tlc/source/src/terminal/mod.rs @@ -71,7 +71,13 @@ impl Tui { } let stdout = io::stdout(); let _ = SAVED_TERMIOS.get_or_init(|| { - rustix::termios::tcgetattr(stdout.as_fd()).ok() + match rustix::termios::tcgetattr(stdout.as_fd()) { + Ok(t) => Some(t), + Err(e) => { + log::warn!("tcgetattr failed: {e} — terminal restore on crash may be incomplete"); + None + } + } }); let raw = stdout .into_raw_mode() diff --git a/local/recipes/tui/tlc/source/src/viewer/mod.rs b/local/recipes/tui/tlc/source/src/viewer/mod.rs index d9bbc0c10b..d5e53e13a7 100644 --- a/local/recipes/tui/tlc/source/src/viewer/mod.rs +++ b/local/recipes/tui/tlc/source/src/viewer/mod.rs @@ -537,7 +537,9 @@ impl Viewer { line: self.current_line() as u32, column: 0, }; - let _ = crate::editor::filepos::save(&self.path, pos); + if let Err(e) = crate::editor::filepos::save(&self.path, pos) { + log::debug!("filepos save failed for {}: {e}", self.path.display()); + } } fn current_line(&self) -> u64 {