From 8d92110c581abe07c5a409dfbf08433930a182fd Mon Sep 17 00:00:00 2001 From: SW Date: Mon, 15 Jun 2026 13:39:10 +0200 Subject: [PATCH] Add history.log for tracking command executions --- Cargo.lock | 128 ++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + README.md | 9 ++++ src/app.rs | 10 +++- src/commands.rs | 14 ++++++ src/events.rs | 28 +++++++++-- src/logger.rs | 38 ++++++++++++++ src/main.rs | 13 ++++- 8 files changed, 233 insertions(+), 8 deletions(-) create mode 100644 src/logger.rs diff --git a/Cargo.lock b/Cargo.lock index a943594..58f5510 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,15 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "1.0.0" @@ -166,6 +175,16 @@ dependencies = [ "rustversion", ] +[[package]] +name = "cc" +version = "1.2.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -178,6 +197,19 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "clap" version = "4.6.1" @@ -247,6 +279,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -475,6 +513,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + [[package]] name = "finl_unicode" version = "1.4.0" @@ -618,6 +662,30 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "id-arena" version = "2.3.0" @@ -760,6 +828,7 @@ name = "lines" version = "0.1.0" dependencies = [ "base64", + "chrono", "clap", "crossterm", "directories", @@ -1410,6 +1479,12 @@ dependencies = [ "digest", ] +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + [[package]] name = "signal-hook" version = "0.3.18" @@ -1973,12 +2048,65 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.61.2" diff --git a/Cargo.toml b/Cargo.toml index 8331bf8..ca76f2f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ edition = "2024" [dependencies] base64 = "0.22.1" +chrono = "0.4.45" clap = { version = "4.6.1", features = ["derive"] } crossterm = "0.29.0" directories = "6.0.0" diff --git a/README.md b/README.md index 72a45bd..7c89f45 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,15 @@ The default terminal is `Terminal` (macOS Terminal.app). The default shell is de | `r` | reload data | | `q` / `Esc` | quit | +## History + +Every command run through `lines` is appended to `~/.lines/history.log`: + +``` +2026-06-15 08:30:15 [Servers/web.md] Main server: ssh admin@web.example.com +2026-06-15 08:31:02 [Scripts/update.md] Run all: ssh admin@host1.example.com /opt/app/update.sh; ssh admin@host2.example.com /opt/app/update.sh +``` + ## Backup The entire `~/.lines` folder is plain text. Copy it to a disk, Git repo, or iCloud. On a new Mac, move it to the same location and `lines` works immediately. diff --git a/src/app.rs b/src/app.rs index e01997c..b4e9e68 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,6 +3,14 @@ use std::path::PathBuf; use crate::config::Terminal; use crate::parser::{load_groups, CommandFile, Group, Section}; +#[derive(Debug, Clone)] +pub struct PendingCommand { + pub group: String, + pub file: String, + pub section: String, + pub commands: Vec, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Focus { Groups, @@ -19,7 +27,7 @@ pub struct App { pub file_index: usize, pub section_index: usize, pub focus: Focus, - pub pending_command: Option>, + pub pending_command: Option, pub message: Option, } diff --git a/src/commands.rs b/src/commands.rs index 21ad2a1..1fa3c22 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -7,14 +7,19 @@ use std::process::Command; use base64::{engine::general_purpose::STANDARD, Engine}; use crate::config::Terminal; +use crate::logger; use crate::parser::{CommandFile, Section}; +#[allow(clippy::too_many_arguments)] pub fn run_section( section: &Section, file: &CommandFile, terminal: &Terminal, shell: &str, data_dir: &Path, + group_name: &str, + file_name: &str, + section_name: &str, ) { let commands = if section.is_run_all() { file.sections @@ -30,6 +35,15 @@ pub fn run_section( return; } + logger::log_run( + data_dir, + group_name, + file_name, + section_name, + &commands, + true, + ); + match terminal { Terminal::Ghostty => run_in_ghostty(&commands, shell), _ => run_via_script(&commands, terminal, shell, data_dir), diff --git a/src/events.rs b/src/events.rs index d6404f3..2eaef12 100644 --- a/src/events.rs +++ b/src/events.rs @@ -2,7 +2,7 @@ use crossterm::event::{self, Event, KeyCode, KeyEventKind}; use std::io; use std::time::Duration; -use crate::app::App; +use crate::app::{App, PendingCommand}; use crate::commands::{edit_file, run_section}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -42,12 +42,25 @@ pub fn handle_event(app: &mut App, event: AppEvent) -> bool { } } KeyCode::Char('o') => { - if let (Some(section), Some(file)) = (app.current_section(), app.current_file()) { - run_section(section, file, &app.terminal, &app.shell, &app.data_dir); + if let (Some(section), Some(file), Some(group)) = + (app.current_section(), app.current_file(), app.current_group()) + { + run_section( + section, + file, + &app.terminal, + &app.shell, + &app.data_dir, + &group.name, + &file.name, + §ion.title, + ); } } KeyCode::Enter => { - if let (Some(section), Some(file)) = (app.current_section(), app.current_file()) { + if let (Some(section), Some(file), Some(group)) = + (app.current_section(), app.current_file(), app.current_group()) + { let commands = if section.is_run_all() { file.sections .iter() @@ -58,7 +71,12 @@ pub fn handle_event(app: &mut App, event: AppEvent) -> bool { section.commands.clone() }; if !commands.is_empty() { - app.pending_command = Some(commands); + app.pending_command = Some(PendingCommand { + group: group.name.clone(), + file: file.name.clone(), + section: section.title.clone(), + commands, + }); return false; } } diff --git a/src/logger.rs b/src/logger.rs new file mode 100644 index 0000000..759b6fb --- /dev/null +++ b/src/logger.rs @@ -0,0 +1,38 @@ +use std::fs::{self, OpenOptions}; +use std::io::Write; +use std::path::Path; + +pub fn log_run( + data_dir: &Path, + group: &str, + file: &str, + section: &str, + commands: &[String], + in_new_window: bool, +) { + let log_path = data_dir.join("history.log"); + + let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S"); + let window_note = if in_new_window { " [new-window]" } else { "" }; + let commands_str = commands.join("; "); + + let line = format!( + "{} [{}/{}] {}{}: {}\n", + timestamp, group, file, section, window_note, commands_str + ); + + if let Some(parent) = log_path.parent() { + let _ = fs::create_dir_all(parent); + } + + let mut file = match OpenOptions::new() + .create(true) + .append(true) + .open(&log_path) + { + Ok(f) => f, + Err(_) => return, + }; + + let _ = file.write_all(line.as_bytes()); +} diff --git a/src/main.rs b/src/main.rs index 4c4d287..7200b4b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ mod app; mod commands; mod config; mod events; +mod logger; mod parser; mod ui; @@ -66,8 +67,16 @@ fn main() -> io::Result<()> { )?; terminal.show_cursor()?; - if let Some(commands) = app.pending_command { - run_in_current_terminal(&commands, &app.shell); + if let Some(pending) = app.pending_command { + logger::log_run( + &app.data_dir, + &pending.group, + &pending.file, + &pending.section, + &pending.commands, + false, + ); + run_in_current_terminal(&pending.commands, &app.shell); } Ok(())