Disable mouse capture, improve SSH error handling, add log labels
This commit is contained in:
parent
bafce8e49c
commit
4b7389cfe7
7 changed files with 155 additions and 18 deletions
64
Cargo.lock
generated
64
Cargo.lock
generated
|
|
@ -148,6 +148,15 @@ dependencies = [
|
|||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block2"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.3"
|
||||
|
|
@ -353,6 +362,17 @@ dependencies = [
|
|||
"phf",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctrlc"
|
||||
version = "3.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162"
|
||||
dependencies = [
|
||||
"dispatch2",
|
||||
"nix 0.31.3",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.23.0"
|
||||
|
|
@ -452,6 +472,18 @@ dependencies = [
|
|||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dispatch2"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
|
||||
dependencies = [
|
||||
"bitflags 2.13.0",
|
||||
"block2",
|
||||
"libc",
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "document-features"
|
||||
version = "0.2.12"
|
||||
|
|
@ -850,6 +882,7 @@ dependencies = [
|
|||
"chrono",
|
||||
"clap",
|
||||
"crossterm",
|
||||
"ctrlc",
|
||||
"directories",
|
||||
"fuzzy-matcher",
|
||||
"ratatui",
|
||||
|
|
@ -865,7 +898,7 @@ version = "1.1.8"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303"
|
||||
dependencies = [
|
||||
"nix",
|
||||
"nix 0.29.0",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
|
|
@ -921,6 +954,18 @@ dependencies = [
|
|||
"memoffset",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.31.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
|
||||
dependencies = [
|
||||
"bitflags 2.13.0",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
|
|
@ -966,6 +1011,21 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
|
||||
dependencies = [
|
||||
"objc2-encode",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-encode"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
|
|
@ -1582,7 +1642,7 @@ dependencies = [
|
|||
"libc",
|
||||
"log",
|
||||
"memmem",
|
||||
"nix",
|
||||
"nix 0.29.0",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"ordered-float",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ categories = ["command-line-utilities"]
|
|||
[dependencies]
|
||||
chrono = "0.4.45"
|
||||
clap = { version = "4.6.1", features = ["derive"] }
|
||||
ctrlc = "3.4.6"
|
||||
crossterm = "0.29.0"
|
||||
directories = "6.0.0"
|
||||
fuzzy-matcher = "0.3.7"
|
||||
|
|
|
|||
11
src/app.rs
11
src/app.rs
|
|
@ -28,6 +28,7 @@ pub struct LogViewer {
|
|||
pub max_lines: usize,
|
||||
pub search: SearchState,
|
||||
pub search_cursor: Option<usize>,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
impl LogViewer {
|
||||
|
|
@ -41,6 +42,7 @@ impl LogViewer {
|
|||
max_lines,
|
||||
search: SearchState::default(),
|
||||
search_cursor: None,
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -49,6 +51,10 @@ impl LogViewer {
|
|||
}
|
||||
|
||||
pub fn push_line(&mut self, line: String) {
|
||||
if let Some(err) = line.strip_prefix("STDERR:") {
|
||||
self.error = Some(err.to_string());
|
||||
return;
|
||||
}
|
||||
if self.buffer.len() >= self.max_lines {
|
||||
self.buffer.pop_front();
|
||||
if self.scroll > 0 {
|
||||
|
|
@ -189,7 +195,7 @@ impl App {
|
|||
.enumerate()
|
||||
.filter_map(|(i, log)| {
|
||||
self.matcher
|
||||
.fuzzy_match(&log.to_string(), &self.picker_filter)
|
||||
.fuzzy_match(&log.search_text(), &self.picker_filter)
|
||||
.map(|score| (score, i))
|
||||
})
|
||||
.collect();
|
||||
|
|
@ -244,6 +250,9 @@ impl App {
|
|||
while let Ok(line) = viewer.receiver.try_recv() {
|
||||
viewer.push_line(line);
|
||||
}
|
||||
if let Some(err) = viewer.error.take() {
|
||||
self.message = Some(format!("SSH error: {}", err));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,8 +36,36 @@ impl LogSource {
|
|||
}
|
||||
}
|
||||
|
||||
/// Human-readable label shown in the log picker.
|
||||
pub fn display_name(&self) -> String {
|
||||
match self {
|
||||
LogSource::File { path } => {
|
||||
let basename = std::path::Path::new(path)
|
||||
.file_name()
|
||||
.map(|s| s.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| path.clone());
|
||||
if let Some(parent) = std::path::Path::new(path).parent() {
|
||||
let parent_str = parent.to_string_lossy();
|
||||
if parent_str == "/var/log" {
|
||||
return format!("{} (system)", basename);
|
||||
} else if parent_str.starts_with("/var/log/") || parent_str.starts_with("/opt/") {
|
||||
return format!("{} ({})", basename, parent_str);
|
||||
}
|
||||
}
|
||||
format!("{} ({})", basename, path)
|
||||
}
|
||||
LogSource::Docker { container, image } => format!("{} (docker: {})", container, image),
|
||||
LogSource::Journal { unit } => format!("{} (journal)", unit),
|
||||
}
|
||||
}
|
||||
|
||||
/// Text used for fuzzy searching (label + raw source).
|
||||
pub fn search_text(&self) -> String {
|
||||
format!("{} {}", self.display_name(), self)
|
||||
}
|
||||
|
||||
pub fn sort_key(&self) -> String {
|
||||
format!("{}:{}", self.category(), self)
|
||||
format!("{}:{}", self.category(), self.display_name())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
41
src/main.rs
41
src/main.rs
|
|
@ -8,11 +8,12 @@ mod ssh;
|
|||
mod ui;
|
||||
|
||||
use std::io;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use crossterm::{
|
||||
event::{DisableMouseCapture, EnableMouseCapture, poll, read},
|
||||
event::{poll, read},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
|
|
@ -48,6 +49,12 @@ enum Command {
|
|||
},
|
||||
}
|
||||
|
||||
fn restore_terminal() {
|
||||
let _ = disable_raw_mode();
|
||||
let _ = execute!(io::stdout(), LeaveAlternateScreen);
|
||||
let _ = execute!(io::stdout(), crossterm::event::DisableMouseCapture);
|
||||
}
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
let args = Args::parse();
|
||||
let command = args.command.unwrap_or_else(|| {
|
||||
|
|
@ -74,30 +81,43 @@ fn main() -> io::Result<()> {
|
|||
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
execute!(stdout, EnterAlternateScreen)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
let result = run_app(&mut terminal, app, &rt);
|
||||
// Ensure terminal is restored on panic or Ctrl-C
|
||||
std::panic::set_hook(Box::new(|info| {
|
||||
restore_terminal();
|
||||
eprintln!("Panic: {}", info);
|
||||
}));
|
||||
let running = Arc::new(std::sync::atomic::AtomicBool::new(true));
|
||||
let r = running.clone();
|
||||
ctrlc::set_handler(move || {
|
||||
r.store(false, std::sync::atomic::Ordering::SeqCst);
|
||||
})
|
||||
.expect("failed to set Ctrl-C handler");
|
||||
|
||||
let result = run_app(&mut terminal, app, &rt, running);
|
||||
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App, rt: &Runtime) -> io::Result<()>
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
rt: &Runtime,
|
||||
running: Arc<std::sync::atomic::AtomicBool>,
|
||||
) -> io::Result<()>
|
||||
where
|
||||
io::Error: From<<B as Backend>::Error>,
|
||||
{
|
||||
let tick_rate = Duration::from_millis(100);
|
||||
|
||||
loop {
|
||||
while running.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
app.update_stream();
|
||||
|
||||
if app.loading {
|
||||
|
|
@ -113,4 +133,5 @@ where
|
|||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
24
src/ssh.rs
24
src/ssh.rs
|
|
@ -52,8 +52,10 @@ impl SshSession {
|
|||
|
||||
fn base_cmd(&self) -> Command {
|
||||
let mut cmd = Command::new(&self.command);
|
||||
// Non-interactive mode: do not prompt for host keys or passwords.
|
||||
cmd.arg("-o").arg("BatchMode=yes");
|
||||
// Avoid interactive prompts from ssh itself. Passphrase prompts from the
|
||||
// SSH agent may still appear in the terminal, but password prompts are
|
||||
// disabled and unknown host keys are accepted automatically.
|
||||
cmd.arg("-o").arg("PasswordAuthentication=no");
|
||||
cmd.arg("-o").arg("StrictHostKeyChecking=accept-new");
|
||||
cmd.args(&self.target.args);
|
||||
cmd.stdin(Stdio::null());
|
||||
|
|
@ -104,20 +106,36 @@ impl SshSession {
|
|||
let mut cmd = self.base_cmd();
|
||||
cmd.arg("--").arg(remote_command);
|
||||
cmd.stdout(Stdio::piped());
|
||||
cmd.stderr(Stdio::null());
|
||||
cmd.stderr(Stdio::piped());
|
||||
cmd.kill_on_drop(true);
|
||||
cmd.spawn()
|
||||
}
|
||||
|
||||
/// Spawn a remote command and return a channel of lines from stdout.
|
||||
/// Lines coming from stderr are prefixed with `STDERR:` so the UI can
|
||||
/// surface ssh errors without mixing them into the log output.
|
||||
pub fn stream_lines(&self, remote_command: &str) -> io::Result<mpsc::UnboundedReceiver<String>> {
|
||||
let mut child = self.spawn_stream(remote_command)?;
|
||||
let stdout = child.stdout.take().ok_or_else(|| {
|
||||
io::Error::other("failed to capture stdout")
|
||||
})?;
|
||||
let stderr = child.stderr.take().ok_or_else(|| {
|
||||
io::Error::other("failed to capture stderr")
|
||||
})?;
|
||||
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
tokio::spawn(async move {
|
||||
let tx_err = tx.clone();
|
||||
tokio::spawn(async move {
|
||||
let reader = BufReader::new(stderr);
|
||||
let mut lines = reader.lines();
|
||||
while let Ok(Some(line)) = lines.next_line().await {
|
||||
if tx_err.send(format!("STDERR:{}", line)).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let reader = BufReader::new(stdout);
|
||||
let mut lines = reader.lines();
|
||||
while let Ok(Some(line)) = lines.next_line().await {
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ fn draw_log_list(f: &mut Frame, app: &App, area: ratatui::layout::Rect) {
|
|||
} else {
|
||||
Style::default()
|
||||
};
|
||||
ListItem::new(format!("[{}] {}", log.category(), log)).style(style)
|
||||
ListItem::new(format!("[{}] {}", log.category(), log.display_name())).style(style)
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue