rss attach: mini TUI with fixed input line, utf-8, history, resize handling
This commit is contained in:
parent
e4aea785ee
commit
4ff0e65e91
4 changed files with 659 additions and 58 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -342,7 +342,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rss"
|
name = "rss"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
|
|
|
||||||
|
|
@ -8,5 +8,5 @@ clap = { version = "4", features = ["derive"] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
nix = { version = "0.29", default-features = false, features = ["signal", "fs"] }
|
nix = { version = "0.29", default-features = false, features = ["signal", "fs", "term"] }
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
|
|
|
||||||
655
src/attach.rs
Normal file
655
src/attach.rs
Normal file
|
|
@ -0,0 +1,655 @@
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
use std::io::{BufRead, BufReader, Read, Seek, SeekFrom, Write};
|
||||||
|
use std::os::unix::net::UnixStream;
|
||||||
|
use std::sync::mpsc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use nix::libc;
|
||||||
|
|
||||||
|
const RED: &str = "\x1b[31m";
|
||||||
|
const RESET: &str = "\x1b[0m";
|
||||||
|
const GRAY_BG: &str = "\x1b[48;5;236m";
|
||||||
|
const REVERSE: &str = "\x1b[7m";
|
||||||
|
const SHOW_CURSOR: &str = "\x1b[?25h";
|
||||||
|
|
||||||
|
struct TerminalGuard {
|
||||||
|
orig: nix::sys::termios::Termios,
|
||||||
|
stdin: std::io::Stdin,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TerminalGuard {
|
||||||
|
fn new() -> Self {
|
||||||
|
let stdin = std::io::stdin();
|
||||||
|
let orig = set_raw_mode(&stdin);
|
||||||
|
TerminalGuard { orig, stdin }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for TerminalGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = write!(std::io::stdout(), "{}\r\n", SHOW_CURSOR);
|
||||||
|
let _ = std::io::stdout().flush();
|
||||||
|
let _ = nix::sys::termios::tcsetattr(
|
||||||
|
&self.stdin,
|
||||||
|
nix::sys::termios::SetArg::TCSANOW,
|
||||||
|
&self.orig,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_raw_mode(stdin: &std::io::Stdin) -> nix::sys::termios::Termios {
|
||||||
|
let mut termios = nix::sys::termios::tcgetattr(stdin).expect("tcgetattr");
|
||||||
|
let orig = termios.clone();
|
||||||
|
|
||||||
|
termios.local_flags.remove(
|
||||||
|
nix::sys::termios::LocalFlags::ECHO
|
||||||
|
| nix::sys::termios::LocalFlags::ICANON
|
||||||
|
| nix::sys::termios::LocalFlags::ISIG
|
||||||
|
| nix::sys::termios::LocalFlags::IEXTEN,
|
||||||
|
);
|
||||||
|
termios.input_flags.remove(
|
||||||
|
nix::sys::termios::InputFlags::IXON
|
||||||
|
| nix::sys::termios::InputFlags::BRKINT
|
||||||
|
| nix::sys::termios::InputFlags::INPCK
|
||||||
|
| nix::sys::termios::InputFlags::ISTRIP
|
||||||
|
| nix::sys::termios::InputFlags::ICRNL,
|
||||||
|
);
|
||||||
|
termios.output_flags.remove(nix::sys::termios::OutputFlags::OPOST);
|
||||||
|
termios.control_flags.insert(nix::sys::termios::ControlFlags::CS8);
|
||||||
|
|
||||||
|
nix::sys::termios::tcsetattr(stdin, nix::sys::termios::SetArg::TCSANOW, &termios)
|
||||||
|
.expect("tcsetattr");
|
||||||
|
|
||||||
|
orig
|
||||||
|
}
|
||||||
|
|
||||||
|
fn terminal_size() -> (usize, usize) {
|
||||||
|
unsafe {
|
||||||
|
let mut ws: libc::winsize = std::mem::zeroed();
|
||||||
|
if libc::ioctl(libc::STDOUT_FILENO, libc::TIOCGWINSZ, &mut ws) == 0 {
|
||||||
|
(ws.ws_row as usize, ws.ws_col as usize)
|
||||||
|
} else {
|
||||||
|
(24, 80)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn safe_truncate(s: &str, max: usize) -> &str {
|
||||||
|
if s.len() <= max {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
// Find the nearest char boundary at or before max
|
||||||
|
let mut idx = max;
|
||||||
|
while idx > 0 && !s.is_char_boundary(idx) {
|
||||||
|
idx -= 1;
|
||||||
|
}
|
||||||
|
&s[..idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn colorize(line: &str) -> String {
|
||||||
|
let use_color =
|
||||||
|
std::env::var("NO_COLOR").is_err() && nix::unistd::isatty(1).unwrap_or(false);
|
||||||
|
if !use_color {
|
||||||
|
return line.to_string();
|
||||||
|
}
|
||||||
|
if line.contains("E: ") || line.starts_with("E:") {
|
||||||
|
format!("{}{}{}", RED, line, RESET)
|
||||||
|
} else {
|
||||||
|
line.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn follow_log(path: &str, tx: mpsc::Sender<String>) {
|
||||||
|
let file = match std::fs::File::open(path) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(_) => return,
|
||||||
|
};
|
||||||
|
let mut reader = BufReader::new(file);
|
||||||
|
let _ = reader.seek(SeekFrom::End(0));
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let mut line = String::new();
|
||||||
|
match reader.read_line(&mut line) {
|
||||||
|
Ok(0) => {
|
||||||
|
std::thread::sleep(Duration::from_millis(100));
|
||||||
|
}
|
||||||
|
Ok(_) => {
|
||||||
|
let trimmed = line.trim_end_matches('\n').trim_end_matches('\r').to_string();
|
||||||
|
if !trimmed.is_empty() && tx.send(trimmed).is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn adjust_scroll(input: &str, cursor: usize, scroll: &mut usize, cols: usize) {
|
||||||
|
let max_visible = cols.saturating_sub(2);
|
||||||
|
if max_visible == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let threshold = (max_visible as f64 * 0.9) as usize;
|
||||||
|
|
||||||
|
if cursor > *scroll + threshold {
|
||||||
|
*scroll = cursor - threshold;
|
||||||
|
}
|
||||||
|
if cursor < *scroll {
|
||||||
|
*scroll = cursor;
|
||||||
|
}
|
||||||
|
if *scroll + max_visible > input.len() {
|
||||||
|
*scroll = input.len().saturating_sub(max_visible);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn draw_all(
|
||||||
|
stdout: &mut std::io::Stdout,
|
||||||
|
rows: usize,
|
||||||
|
cols: usize,
|
||||||
|
log_lines: &VecDeque<String>,
|
||||||
|
input: &str,
|
||||||
|
cursor: usize,
|
||||||
|
scroll: usize,
|
||||||
|
name: &str,
|
||||||
|
started: bool,
|
||||||
|
) {
|
||||||
|
let mut out = String::new();
|
||||||
|
|
||||||
|
if !started {
|
||||||
|
out.push_str("\x1b[2J");
|
||||||
|
}
|
||||||
|
|
||||||
|
let header = format!(" Attached to {} - Ctrl+D detach | Ctrl+C quit ", name);
|
||||||
|
out.push_str("\x1b[1;1H");
|
||||||
|
out.push_str(REVERSE);
|
||||||
|
out.push_str(&header);
|
||||||
|
out.push_str("\x1b[K");
|
||||||
|
out.push_str(RESET);
|
||||||
|
|
||||||
|
let max_log_rows = rows.saturating_sub(2);
|
||||||
|
for i in 0..max_log_rows {
|
||||||
|
out.push_str(&format!("\x1b[{};1H", i + 2));
|
||||||
|
out.push_str("\x1b[2K");
|
||||||
|
if let Some(line) = log_lines.get(i) {
|
||||||
|
out.push_str(safe_truncate(line, cols));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let prompt = "> ";
|
||||||
|
let max_visible = cols.saturating_sub(prompt.len());
|
||||||
|
let visible_start = scroll;
|
||||||
|
let visible_end = (scroll + max_visible).min(input.len());
|
||||||
|
let visible = if scroll < input.len() {
|
||||||
|
&input[visible_start..visible_end]
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
|
||||||
|
out.push_str(&format!("\x1b[{};1H", rows));
|
||||||
|
out.push_str(GRAY_BG);
|
||||||
|
out.push_str(prompt);
|
||||||
|
out.push_str(visible);
|
||||||
|
out.push_str("\x1b[K");
|
||||||
|
out.push_str(RESET);
|
||||||
|
|
||||||
|
let cursor_col = prompt.len() + cursor.saturating_sub(scroll);
|
||||||
|
out.push_str(&format!("\x1b[{};{}H", rows, cursor_col + 1));
|
||||||
|
|
||||||
|
let _ = stdout.write_all(out.as_bytes());
|
||||||
|
let _ = stdout.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
enum InputAction {
|
||||||
|
Send,
|
||||||
|
Exit,
|
||||||
|
SignalChild,
|
||||||
|
HistoryPrev,
|
||||||
|
HistoryNext,
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
enum EscState {
|
||||||
|
None,
|
||||||
|
Escaped,
|
||||||
|
Bracket,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn utf8_char_len(first: u8) -> Option<usize> {
|
||||||
|
if first & 0x80 == 0 {
|
||||||
|
Some(1)
|
||||||
|
} else if first & 0xE0 == 0xC0 {
|
||||||
|
Some(2)
|
||||||
|
} else if first & 0xF0 == 0xE0 {
|
||||||
|
Some(3)
|
||||||
|
} else if first & 0xF8 == 0xF0 {
|
||||||
|
Some(4)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_attach(name: &str, full: bool, stream: UnixStream) {
|
||||||
|
if !nix::unistd::isatty(0).unwrap_or(false) {
|
||||||
|
run_attach_simple(name, full, stream);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let _guard = TerminalGuard::new();
|
||||||
|
let mut stdout = std::io::stdout();
|
||||||
|
|
||||||
|
let (stdin_tx, stdin_rx) = mpsc::channel();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let mut buf = [0u8; 1];
|
||||||
|
loop {
|
||||||
|
match std::io::stdin().read(&mut buf) {
|
||||||
|
Ok(0) => {
|
||||||
|
let _ = stdin_tx.send(None);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Ok(_) => {
|
||||||
|
let _ = stdin_tx.send(Some(buf[0]));
|
||||||
|
}
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let (log_tx, log_rx) = mpsc::channel();
|
||||||
|
let log_path = crate::state::log_path(name);
|
||||||
|
if log_path.exists() {
|
||||||
|
let path_str = log_path.to_string_lossy().to_string();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
follow_log(&path_str, log_tx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut stream = stream;
|
||||||
|
let mut input = String::new();
|
||||||
|
let mut cursor = 0usize;
|
||||||
|
let mut scroll = 0usize;
|
||||||
|
let mut log_lines: VecDeque<String> = VecDeque::new();
|
||||||
|
let mut history: VecDeque<String> = VecDeque::new();
|
||||||
|
let mut history_idx: Option<usize> = None;
|
||||||
|
let mut started = false;
|
||||||
|
let mut utf8_buf: Vec<u8> = Vec::new();
|
||||||
|
let mut utf8_expected: Option<usize> = None;
|
||||||
|
let mut esc_state = EscState::None;
|
||||||
|
let mut prev_rows = 0usize;
|
||||||
|
let mut prev_cols = 0usize;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let (rows, cols) = terminal_size();
|
||||||
|
if rows != prev_rows || cols != prev_cols {
|
||||||
|
prev_rows = rows;
|
||||||
|
prev_cols = cols;
|
||||||
|
started = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut changed = false;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match stdin_rx.try_recv() {
|
||||||
|
Ok(byte_opt) => {
|
||||||
|
changed = true;
|
||||||
|
match byte_opt {
|
||||||
|
None => {
|
||||||
|
let _ = writeln!(std::io::stdout(), "\r");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Some(b) => {
|
||||||
|
let action = {
|
||||||
|
if b == 0x1b {
|
||||||
|
esc_state = EscState::Escaped;
|
||||||
|
changed = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match esc_state {
|
||||||
|
EscState::Escaped => {
|
||||||
|
esc_state = if b == b'[' {
|
||||||
|
EscState::Bracket
|
||||||
|
} else {
|
||||||
|
EscState::None
|
||||||
|
};
|
||||||
|
if esc_state == EscState::Bracket {
|
||||||
|
InputAction::None
|
||||||
|
} else {
|
||||||
|
process_input_byte(
|
||||||
|
b,
|
||||||
|
&mut input,
|
||||||
|
&mut cursor,
|
||||||
|
&mut scroll,
|
||||||
|
cols,
|
||||||
|
&mut utf8_buf,
|
||||||
|
&mut utf8_expected,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EscState::Bracket => {
|
||||||
|
esc_state = EscState::None;
|
||||||
|
process_escape(
|
||||||
|
b,
|
||||||
|
&mut input,
|
||||||
|
&mut cursor,
|
||||||
|
&mut scroll,
|
||||||
|
cols,
|
||||||
|
&mut history,
|
||||||
|
&mut history_idx,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
EscState::None => process_input_byte(
|
||||||
|
b,
|
||||||
|
&mut input,
|
||||||
|
&mut cursor,
|
||||||
|
&mut scroll,
|
||||||
|
cols,
|
||||||
|
&mut utf8_buf,
|
||||||
|
&mut utf8_expected,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match action {
|
||||||
|
InputAction::Send => {
|
||||||
|
if !input.is_empty() {
|
||||||
|
let _ = writeln!(stream, "{}", input);
|
||||||
|
let _ = stream.flush();
|
||||||
|
history.push_back(input.clone());
|
||||||
|
if history.len() > 100 {
|
||||||
|
history.pop_front();
|
||||||
|
}
|
||||||
|
history_idx = None;
|
||||||
|
}
|
||||||
|
input.clear();
|
||||||
|
cursor = 0;
|
||||||
|
scroll = 0;
|
||||||
|
}
|
||||||
|
InputAction::Exit => {
|
||||||
|
let _ = writeln!(std::io::stdout(), "\r");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
InputAction::SignalChild => {
|
||||||
|
if full {
|
||||||
|
let _ =
|
||||||
|
crate::ipc::send_cmd(&format!("signal {} INT", name));
|
||||||
|
}
|
||||||
|
let _ = writeln!(std::io::stdout(), "\r");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
InputAction::HistoryPrev => {
|
||||||
|
if history.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if history_idx.is_none() {
|
||||||
|
history_idx = Some(history.len() - 1);
|
||||||
|
} else if history_idx.unwrap() > 0 {
|
||||||
|
history_idx = Some(history_idx.unwrap() - 1);
|
||||||
|
}
|
||||||
|
if let Some(idx) = history_idx {
|
||||||
|
input = history[idx].clone();
|
||||||
|
cursor = input.len();
|
||||||
|
scroll = input.len().saturating_sub(cols.saturating_sub(2));
|
||||||
|
}
|
||||||
|
adjust_scroll(&input, cursor, &mut scroll, cols);
|
||||||
|
}
|
||||||
|
InputAction::HistoryNext => {
|
||||||
|
if let Some(idx) = history_idx {
|
||||||
|
if idx + 1 < history.len() {
|
||||||
|
history_idx = Some(idx + 1);
|
||||||
|
input = history[idx + 1].clone();
|
||||||
|
cursor = input.len();
|
||||||
|
} else {
|
||||||
|
history_idx = None;
|
||||||
|
input.clear();
|
||||||
|
cursor = 0;
|
||||||
|
}
|
||||||
|
adjust_scroll(&input, cursor, &mut scroll, cols);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
InputAction::None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(mpsc::TryRecvError::Disconnected) => {
|
||||||
|
let _ = writeln!(std::io::stdout(), "\r");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Err(mpsc::TryRecvError::Empty) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while let Ok(line) = log_rx.try_recv() {
|
||||||
|
changed = true;
|
||||||
|
let max_log_area = rows.saturating_sub(2);
|
||||||
|
if log_lines.len() >= max_log_area {
|
||||||
|
log_lines.pop_front();
|
||||||
|
}
|
||||||
|
log_lines.push_back(colorize(&line));
|
||||||
|
}
|
||||||
|
|
||||||
|
if changed || !started {
|
||||||
|
draw_all(
|
||||||
|
&mut stdout,
|
||||||
|
rows,
|
||||||
|
cols,
|
||||||
|
&log_lines,
|
||||||
|
&input,
|
||||||
|
cursor,
|
||||||
|
scroll,
|
||||||
|
name,
|
||||||
|
started,
|
||||||
|
);
|
||||||
|
started = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::thread::sleep(Duration::from_millis(16));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_input_byte(
|
||||||
|
b: u8,
|
||||||
|
input: &mut String,
|
||||||
|
cursor: &mut usize,
|
||||||
|
scroll: &mut usize,
|
||||||
|
cols: usize,
|
||||||
|
utf8_buf: &mut Vec<u8>,
|
||||||
|
utf8_expected: &mut Option<usize>,
|
||||||
|
) -> InputAction {
|
||||||
|
match b {
|
||||||
|
0x0d | 0x0a => return InputAction::Send,
|
||||||
|
0x03 => return InputAction::SignalChild,
|
||||||
|
0x04 => return InputAction::Exit,
|
||||||
|
0x10 => return InputAction::HistoryPrev,
|
||||||
|
0x0e => return InputAction::HistoryNext,
|
||||||
|
0x08 | 0x7f => {
|
||||||
|
if *cursor > 0 {
|
||||||
|
*cursor -= 1;
|
||||||
|
input.remove(*cursor);
|
||||||
|
}
|
||||||
|
adjust_scroll(input, *cursor, scroll, cols);
|
||||||
|
return InputAction::None;
|
||||||
|
}
|
||||||
|
0x01 => {
|
||||||
|
*cursor = 0;
|
||||||
|
*scroll = 0;
|
||||||
|
return InputAction::None;
|
||||||
|
}
|
||||||
|
0x05 => {
|
||||||
|
*cursor = input.len();
|
||||||
|
adjust_scroll(input, *cursor, scroll, cols);
|
||||||
|
return InputAction::None;
|
||||||
|
}
|
||||||
|
0x15 => {
|
||||||
|
input.clear();
|
||||||
|
*cursor = 0;
|
||||||
|
*scroll = 0;
|
||||||
|
return InputAction::None;
|
||||||
|
}
|
||||||
|
0x0b => {
|
||||||
|
input.truncate(*cursor);
|
||||||
|
return InputAction::None;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if b & 0x80 != 0 {
|
||||||
|
if utf8_buf.is_empty() {
|
||||||
|
if let Some(len) = utf8_char_len(b) {
|
||||||
|
utf8_buf.push(b);
|
||||||
|
*utf8_expected = Some(len);
|
||||||
|
}
|
||||||
|
} else if b & 0xC0 == 0x80 {
|
||||||
|
utf8_buf.push(b);
|
||||||
|
if Some(utf8_buf.len()) == *utf8_expected {
|
||||||
|
if let Ok(s) = std::str::from_utf8(&utf8_buf) {
|
||||||
|
for c in s.chars() {
|
||||||
|
let n = c.len_utf8();
|
||||||
|
input.insert(*cursor, c);
|
||||||
|
*cursor += n;
|
||||||
|
}
|
||||||
|
adjust_scroll(input, *cursor, scroll, cols);
|
||||||
|
}
|
||||||
|
utf8_buf.clear();
|
||||||
|
*utf8_expected = None;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
utf8_buf.clear();
|
||||||
|
*utf8_expected = None;
|
||||||
|
}
|
||||||
|
} else if b >= 0x20 {
|
||||||
|
input.insert(*cursor, b as char);
|
||||||
|
*cursor += 1;
|
||||||
|
adjust_scroll(input, *cursor, scroll, cols);
|
||||||
|
}
|
||||||
|
|
||||||
|
InputAction::None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_escape(
|
||||||
|
b: u8,
|
||||||
|
input: &mut String,
|
||||||
|
cursor: &mut usize,
|
||||||
|
scroll: &mut usize,
|
||||||
|
cols: usize,
|
||||||
|
history: &mut VecDeque<String>,
|
||||||
|
history_idx: &mut Option<usize>,
|
||||||
|
) -> InputAction {
|
||||||
|
match b {
|
||||||
|
b'A' => {
|
||||||
|
// Up arrow - history prev
|
||||||
|
if history.is_empty() {
|
||||||
|
return InputAction::None;
|
||||||
|
}
|
||||||
|
if history_idx.is_none() {
|
||||||
|
*history_idx = Some(history.len() - 1);
|
||||||
|
} else if history_idx.unwrap() > 0 {
|
||||||
|
*history_idx = Some(history_idx.unwrap() - 1);
|
||||||
|
}
|
||||||
|
if let Some(idx) = *history_idx {
|
||||||
|
*input = history[idx].clone();
|
||||||
|
*cursor = input.len();
|
||||||
|
}
|
||||||
|
adjust_scroll(input, *cursor, scroll, cols);
|
||||||
|
InputAction::None
|
||||||
|
}
|
||||||
|
b'B' => {
|
||||||
|
// Down arrow - history next
|
||||||
|
if let Some(idx) = *history_idx {
|
||||||
|
if idx + 1 < history.len() {
|
||||||
|
*history_idx = Some(idx + 1);
|
||||||
|
*input = history[idx + 1].clone();
|
||||||
|
*cursor = input.len();
|
||||||
|
} else {
|
||||||
|
*history_idx = None;
|
||||||
|
input.clear();
|
||||||
|
*cursor = 0;
|
||||||
|
}
|
||||||
|
adjust_scroll(input, *cursor, scroll, cols);
|
||||||
|
}
|
||||||
|
InputAction::None
|
||||||
|
}
|
||||||
|
b'C' => {
|
||||||
|
// Right arrow
|
||||||
|
if *cursor < input.len() {
|
||||||
|
*cursor += 1;
|
||||||
|
}
|
||||||
|
adjust_scroll(input, *cursor, scroll, cols);
|
||||||
|
InputAction::None
|
||||||
|
}
|
||||||
|
b'D' => {
|
||||||
|
// Left arrow
|
||||||
|
*cursor = cursor.saturating_sub(1);
|
||||||
|
adjust_scroll(input, *cursor, scroll, cols);
|
||||||
|
InputAction::None
|
||||||
|
}
|
||||||
|
b'H' => {
|
||||||
|
// Home
|
||||||
|
*cursor = 0;
|
||||||
|
*scroll = 0;
|
||||||
|
InputAction::None
|
||||||
|
}
|
||||||
|
b'F' => {
|
||||||
|
// End
|
||||||
|
*cursor = input.len();
|
||||||
|
adjust_scroll(input, *cursor, scroll, cols);
|
||||||
|
InputAction::None
|
||||||
|
}
|
||||||
|
_ => InputAction::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_attach_simple(name: &str, full: bool, stream: UnixStream) {
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
static ATTACH_INTERRUPT: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
|
extern "C" fn handle_attach_int(_: i32) {
|
||||||
|
ATTACH_INTERRUPT.store(true, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
let sig_action = nix::sys::signal::SigAction::new(
|
||||||
|
nix::sys::signal::SigHandler::Handler(handle_attach_int),
|
||||||
|
nix::sys::signal::SaFlags::empty(),
|
||||||
|
nix::sys::signal::SigSet::empty(),
|
||||||
|
);
|
||||||
|
unsafe {
|
||||||
|
let _ = nix::sys::signal::sigaction(nix::sys::signal::Signal::SIGINT, &sig_action);
|
||||||
|
}
|
||||||
|
|
||||||
|
let log_path = crate::state::log_path(name);
|
||||||
|
if log_path.exists() {
|
||||||
|
let log_path_str = log_path.to_string_lossy().to_string();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let _ = crate::log::show_logs(&log_path_str, Some(0), true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let stdin = std::io::stdin();
|
||||||
|
loop {
|
||||||
|
if ATTACH_INTERRUPT.load(Ordering::Relaxed) {
|
||||||
|
if full {
|
||||||
|
let _ = crate::ipc::send_cmd(&format!("signal {} INT", name));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let mut line = String::new();
|
||||||
|
match stdin.read_line(&mut line) {
|
||||||
|
Ok(0) => break,
|
||||||
|
Ok(_) => {
|
||||||
|
let trimmed = line.trim_end_matches('\n').trim_end_matches('\r');
|
||||||
|
let bytes = trimmed.as_bytes();
|
||||||
|
if bytes.len() >= 2 && bytes[0] == 0x10 && (bytes[1] == b'd' || bytes[1] == b'D') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if !trimmed.is_empty() {
|
||||||
|
let _ = writeln!(&stream, "{}", trimmed);
|
||||||
|
let _ = (&stream).flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
58
src/main.rs
58
src/main.rs
|
|
@ -1,3 +1,4 @@
|
||||||
|
mod attach;
|
||||||
mod cli;
|
mod cli;
|
||||||
mod config;
|
mod config;
|
||||||
mod daemon;
|
mod daemon;
|
||||||
|
|
@ -8,8 +9,6 @@ mod supervisor;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use cli::{Cli, Commands};
|
use cli::{Cli, Commands};
|
||||||
use std::io::Write;
|
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
|
||||||
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
|
@ -201,22 +200,7 @@ fn cmd_send(name: &str, message: &[String]) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static ATTACH_INTERRUPT: AtomicBool = AtomicBool::new(false);
|
|
||||||
|
|
||||||
extern "C" fn handle_attach_int(_: i32) {
|
|
||||||
ATTACH_INTERRUPT.store(true, Ordering::Relaxed);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cmd_attach(name: &str, full: bool) {
|
fn cmd_attach(name: &str, full: bool) {
|
||||||
let sig_action = nix::sys::signal::SigAction::new(
|
|
||||||
nix::sys::signal::SigHandler::Handler(handle_attach_int),
|
|
||||||
nix::sys::signal::SaFlags::empty(),
|
|
||||||
nix::sys::signal::SigSet::empty(),
|
|
||||||
);
|
|
||||||
unsafe {
|
|
||||||
let _ = nix::sys::signal::sigaction(nix::sys::signal::Signal::SIGINT, &sig_action);
|
|
||||||
}
|
|
||||||
|
|
||||||
let stream = match ipc::send_cmd_get_stream(&format!("stdin {}", name)) {
|
let stream = match ipc::send_cmd_get_stream(&format!("stdin {}", name)) {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
@ -225,43 +209,5 @@ fn cmd_attach(name: &str, full: bool) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Tail the log file in a background thread
|
attach::run_attach(name, full, stream);
|
||||||
let log_path = crate::state::log_path(name);
|
|
||||||
if log_path.exists() {
|
|
||||||
let log_path_str = log_path.to_string_lossy().to_string();
|
|
||||||
std::thread::spawn(move || {
|
|
||||||
let _ = crate::log::show_logs(&log_path_str, Some(0), true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main loop: read stdin line by line, send to daemon
|
|
||||||
let stdin = std::io::stdin();
|
|
||||||
loop {
|
|
||||||
if ATTACH_INTERRUPT.load(Ordering::Relaxed) {
|
|
||||||
if full {
|
|
||||||
let _ = ipc::send_cmd(&format!("signal {} INT", name));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let mut line = String::new();
|
|
||||||
match stdin.read_line(&mut line) {
|
|
||||||
Ok(0) => break,
|
|
||||||
Ok(_) => {
|
|
||||||
let trimmed = line.trim_end_matches('\n').trim_end_matches('\r');
|
|
||||||
let bytes = trimmed.as_bytes();
|
|
||||||
if bytes.len() >= 2 && bytes[0] == 0x10 && (bytes[1] == b'd' || bytes[1] == b'D')
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if !trimmed.is_empty() {
|
|
||||||
let _ = writeln!(&stream, "{}", trimmed);
|
|
||||||
let _ = (&stream).flush();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Err(_) => break,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue