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",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "block2"
|
||||||
|
version = "0.6.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
|
||||||
|
dependencies = [
|
||||||
|
"objc2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.20.3"
|
version = "3.20.3"
|
||||||
|
|
@ -353,6 +362,17 @@ dependencies = [
|
||||||
"phf",
|
"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]]
|
[[package]]
|
||||||
name = "darling"
|
name = "darling"
|
||||||
version = "0.23.0"
|
version = "0.23.0"
|
||||||
|
|
@ -452,6 +472,18 @@ dependencies = [
|
||||||
"windows-sys",
|
"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]]
|
[[package]]
|
||||||
name = "document-features"
|
name = "document-features"
|
||||||
version = "0.2.12"
|
version = "0.2.12"
|
||||||
|
|
@ -850,6 +882,7 @@ dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
|
"ctrlc",
|
||||||
"directories",
|
"directories",
|
||||||
"fuzzy-matcher",
|
"fuzzy-matcher",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
|
|
@ -865,7 +898,7 @@ version = "1.1.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303"
|
checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"nix",
|
"nix 0.29.0",
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -921,6 +954,18 @@ dependencies = [
|
||||||
"memoffset",
|
"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]]
|
[[package]]
|
||||||
name = "nom"
|
name = "nom"
|
||||||
version = "7.1.3"
|
version = "7.1.3"
|
||||||
|
|
@ -966,6 +1011,21 @@ dependencies = [
|
||||||
"libc",
|
"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]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.4"
|
version = "1.21.4"
|
||||||
|
|
@ -1582,7 +1642,7 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"memmem",
|
"memmem",
|
||||||
"nix",
|
"nix 0.29.0",
|
||||||
"num-derive",
|
"num-derive",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"ordered-float",
|
"ordered-float",
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ categories = ["command-line-utilities"]
|
||||||
[dependencies]
|
[dependencies]
|
||||||
chrono = "0.4.45"
|
chrono = "0.4.45"
|
||||||
clap = { version = "4.6.1", features = ["derive"] }
|
clap = { version = "4.6.1", features = ["derive"] }
|
||||||
|
ctrlc = "3.4.6"
|
||||||
crossterm = "0.29.0"
|
crossterm = "0.29.0"
|
||||||
directories = "6.0.0"
|
directories = "6.0.0"
|
||||||
fuzzy-matcher = "0.3.7"
|
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 max_lines: usize,
|
||||||
pub search: SearchState,
|
pub search: SearchState,
|
||||||
pub search_cursor: Option<usize>,
|
pub search_cursor: Option<usize>,
|
||||||
|
pub error: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LogViewer {
|
impl LogViewer {
|
||||||
|
|
@ -41,6 +42,7 @@ impl LogViewer {
|
||||||
max_lines,
|
max_lines,
|
||||||
search: SearchState::default(),
|
search: SearchState::default(),
|
||||||
search_cursor: None,
|
search_cursor: None,
|
||||||
|
error: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -49,6 +51,10 @@ impl LogViewer {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn push_line(&mut self, line: String) {
|
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 {
|
if self.buffer.len() >= self.max_lines {
|
||||||
self.buffer.pop_front();
|
self.buffer.pop_front();
|
||||||
if self.scroll > 0 {
|
if self.scroll > 0 {
|
||||||
|
|
@ -189,7 +195,7 @@ impl App {
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.filter_map(|(i, log)| {
|
.filter_map(|(i, log)| {
|
||||||
self.matcher
|
self.matcher
|
||||||
.fuzzy_match(&log.to_string(), &self.picker_filter)
|
.fuzzy_match(&log.search_text(), &self.picker_filter)
|
||||||
.map(|score| (score, i))
|
.map(|score| (score, i))
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
@ -244,6 +250,9 @@ impl App {
|
||||||
while let Ok(line) = viewer.receiver.try_recv() {
|
while let Ok(line) = viewer.receiver.try_recv() {
|
||||||
viewer.push_line(line);
|
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 {
|
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;
|
mod ui;
|
||||||
|
|
||||||
use std::io;
|
use std::io;
|
||||||
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
event::{DisableMouseCapture, EnableMouseCapture, poll, read},
|
event::{poll, read},
|
||||||
execute,
|
execute,
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
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<()> {
|
fn main() -> io::Result<()> {
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
let command = args.command.unwrap_or_else(|| {
|
let command = args.command.unwrap_or_else(|| {
|
||||||
|
|
@ -74,30 +81,43 @@ fn main() -> io::Result<()> {
|
||||||
|
|
||||||
enable_raw_mode()?;
|
enable_raw_mode()?;
|
||||||
let mut stdout = io::stdout();
|
let mut stdout = io::stdout();
|
||||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
execute!(stdout, EnterAlternateScreen)?;
|
||||||
let backend = CrosstermBackend::new(stdout);
|
let backend = CrosstermBackend::new(stdout);
|
||||||
let mut terminal = Terminal::new(backend)?;
|
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()?;
|
disable_raw_mode()?;
|
||||||
execute!(
|
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
||||||
terminal.backend_mut(),
|
|
||||||
LeaveAlternateScreen,
|
|
||||||
DisableMouseCapture
|
|
||||||
)?;
|
|
||||||
terminal.show_cursor()?;
|
terminal.show_cursor()?;
|
||||||
|
|
||||||
result
|
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
|
where
|
||||||
io::Error: From<<B as Backend>::Error>,
|
io::Error: From<<B as Backend>::Error>,
|
||||||
{
|
{
|
||||||
let tick_rate = Duration::from_millis(100);
|
let tick_rate = Duration::from_millis(100);
|
||||||
|
|
||||||
loop {
|
while running.load(std::sync::atomic::Ordering::SeqCst) {
|
||||||
app.update_stream();
|
app.update_stream();
|
||||||
|
|
||||||
if app.loading {
|
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 {
|
fn base_cmd(&self) -> Command {
|
||||||
let mut cmd = Command::new(&self.command);
|
let mut cmd = Command::new(&self.command);
|
||||||
// Non-interactive mode: do not prompt for host keys or passwords.
|
// Avoid interactive prompts from ssh itself. Passphrase prompts from the
|
||||||
cmd.arg("-o").arg("BatchMode=yes");
|
// 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.arg("-o").arg("StrictHostKeyChecking=accept-new");
|
||||||
cmd.args(&self.target.args);
|
cmd.args(&self.target.args);
|
||||||
cmd.stdin(Stdio::null());
|
cmd.stdin(Stdio::null());
|
||||||
|
|
@ -104,20 +106,36 @@ impl SshSession {
|
||||||
let mut cmd = self.base_cmd();
|
let mut cmd = self.base_cmd();
|
||||||
cmd.arg("--").arg(remote_command);
|
cmd.arg("--").arg(remote_command);
|
||||||
cmd.stdout(Stdio::piped());
|
cmd.stdout(Stdio::piped());
|
||||||
cmd.stderr(Stdio::null());
|
cmd.stderr(Stdio::piped());
|
||||||
cmd.kill_on_drop(true);
|
cmd.kill_on_drop(true);
|
||||||
cmd.spawn()
|
cmd.spawn()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spawn a remote command and return a channel of lines from stdout.
|
/// 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>> {
|
pub fn stream_lines(&self, remote_command: &str) -> io::Result<mpsc::UnboundedReceiver<String>> {
|
||||||
let mut child = self.spawn_stream(remote_command)?;
|
let mut child = self.spawn_stream(remote_command)?;
|
||||||
let stdout = child.stdout.take().ok_or_else(|| {
|
let stdout = child.stdout.take().ok_or_else(|| {
|
||||||
io::Error::other("failed to capture stdout")
|
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();
|
let (tx, rx) = mpsc::unbounded_channel();
|
||||||
tokio::spawn(async move {
|
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 reader = BufReader::new(stdout);
|
||||||
let mut lines = reader.lines();
|
let mut lines = reader.lines();
|
||||||
while let Ok(Some(line)) = lines.next_line().await {
|
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 {
|
} else {
|
||||||
Style::default()
|
Style::default()
|
||||||
};
|
};
|
||||||
ListItem::new(format!("[{}] {}", log.category(), log)).style(style)
|
ListItem::new(format!("[{}] {}", log.category(), log.display_name())).style(style)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue