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]]
|
||||
name = "rss"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
|
|
|
|||
|
|
@ -8,5 +8,5 @@ clap = { version = "4", features = ["derive"] }
|
|||
serde = { version = "1", features = ["derive"] }
|
||||
toml = "0.8"
|
||||
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"
|
||||
|
|
|
|||
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 config;
|
||||
mod daemon;
|
||||
|
|
@ -8,8 +9,6 @@ mod supervisor;
|
|||
|
||||
use clap::Parser;
|
||||
use cli::{Cli, Commands};
|
||||
use std::io::Write;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
|
||||
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) {
|
||||
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)) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
|
|
@ -225,43 +209,5 @@ fn cmd_attach(name: &str, full: bool) {
|
|||
}
|
||||
};
|
||||
|
||||
// Tail the log file in a background thread
|
||||
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,
|
||||
}
|
||||
}
|
||||
attach::run_attach(name, full, stream);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue