Add history.log for tracking command executions

This commit is contained in:
SW 2026-06-15 13:39:10 +02:00
parent f15be840ca
commit 8d92110c58
8 changed files with 233 additions and 8 deletions

128
Cargo.lock generated
View file

@ -17,6 +17,15 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 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]] [[package]]
name = "anstream" name = "anstream"
version = "1.0.0" version = "1.0.0"
@ -166,6 +175,16 @@ dependencies = [
"rustversion", "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]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.4" version = "1.0.4"
@ -178,6 +197,19 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 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]] [[package]]
name = "clap" name = "clap"
version = "4.6.1" version = "4.6.1"
@ -247,6 +279,12 @@ dependencies = [
"unicode-segmentation", "unicode-segmentation",
] ]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]] [[package]]
name = "cpufeatures" name = "cpufeatures"
version = "0.2.17" version = "0.2.17"
@ -475,6 +513,12 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]] [[package]]
name = "finl_unicode" name = "finl_unicode"
version = "1.4.0" version = "1.4.0"
@ -618,6 +662,30 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 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]] [[package]]
name = "id-arena" name = "id-arena"
version = "2.3.0" version = "2.3.0"
@ -760,6 +828,7 @@ name = "lines"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"base64", "base64",
"chrono",
"clap", "clap",
"crossterm", "crossterm",
"directories", "directories",
@ -1410,6 +1479,12 @@ dependencies = [
"digest", "digest",
] ]
[[package]]
name = "shlex"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]] [[package]]
name = "signal-hook" name = "signal-hook"
version = "0.3.18" version = "0.3.18"
@ -1973,12 +2048,65 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 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]] [[package]]
name = "windows-link" name = "windows-link"
version = "0.2.1" version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 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]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.61.2" version = "0.61.2"

View file

@ -5,6 +5,7 @@ edition = "2024"
[dependencies] [dependencies]
base64 = "0.22.1" base64 = "0.22.1"
chrono = "0.4.45"
clap = { version = "4.6.1", features = ["derive"] } clap = { version = "4.6.1", features = ["derive"] }
crossterm = "0.29.0" crossterm = "0.29.0"
directories = "6.0.0" directories = "6.0.0"

View file

@ -73,6 +73,15 @@ The default terminal is `Terminal` (macOS Terminal.app). The default shell is de
| `r` | reload data | | `r` | reload data |
| `q` / `Esc` | quit | | `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 ## 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. 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.

View file

@ -3,6 +3,14 @@ use std::path::PathBuf;
use crate::config::Terminal; use crate::config::Terminal;
use crate::parser::{load_groups, CommandFile, Group, Section}; 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<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Focus { pub enum Focus {
Groups, Groups,
@ -19,7 +27,7 @@ pub struct App {
pub file_index: usize, pub file_index: usize,
pub section_index: usize, pub section_index: usize,
pub focus: Focus, pub focus: Focus,
pub pending_command: Option<Vec<String>>, pub pending_command: Option<PendingCommand>,
pub message: Option<String>, pub message: Option<String>,
} }

View file

@ -7,14 +7,19 @@ use std::process::Command;
use base64::{engine::general_purpose::STANDARD, Engine}; use base64::{engine::general_purpose::STANDARD, Engine};
use crate::config::Terminal; use crate::config::Terminal;
use crate::logger;
use crate::parser::{CommandFile, Section}; use crate::parser::{CommandFile, Section};
#[allow(clippy::too_many_arguments)]
pub fn run_section( pub fn run_section(
section: &Section, section: &Section,
file: &CommandFile, file: &CommandFile,
terminal: &Terminal, terminal: &Terminal,
shell: &str, shell: &str,
data_dir: &Path, data_dir: &Path,
group_name: &str,
file_name: &str,
section_name: &str,
) { ) {
let commands = if section.is_run_all() { let commands = if section.is_run_all() {
file.sections file.sections
@ -30,6 +35,15 @@ pub fn run_section(
return; return;
} }
logger::log_run(
data_dir,
group_name,
file_name,
section_name,
&commands,
true,
);
match terminal { match terminal {
Terminal::Ghostty => run_in_ghostty(&commands, shell), Terminal::Ghostty => run_in_ghostty(&commands, shell),
_ => run_via_script(&commands, terminal, shell, data_dir), _ => run_via_script(&commands, terminal, shell, data_dir),

View file

@ -2,7 +2,7 @@ use crossterm::event::{self, Event, KeyCode, KeyEventKind};
use std::io; use std::io;
use std::time::Duration; use std::time::Duration;
use crate::app::App; use crate::app::{App, PendingCommand};
use crate::commands::{edit_file, run_section}; use crate::commands::{edit_file, run_section};
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -42,12 +42,25 @@ pub fn handle_event(app: &mut App, event: AppEvent) -> bool {
} }
} }
KeyCode::Char('o') => { KeyCode::Char('o') => {
if let (Some(section), Some(file)) = (app.current_section(), app.current_file()) { if let (Some(section), Some(file), Some(group)) =
run_section(section, file, &app.terminal, &app.shell, &app.data_dir); (app.current_section(), app.current_file(), app.current_group())
{
run_section(
section,
file,
&app.terminal,
&app.shell,
&app.data_dir,
&group.name,
&file.name,
&section.title,
);
} }
} }
KeyCode::Enter => { 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() { let commands = if section.is_run_all() {
file.sections file.sections
.iter() .iter()
@ -58,7 +71,12 @@ pub fn handle_event(app: &mut App, event: AppEvent) -> bool {
section.commands.clone() section.commands.clone()
}; };
if !commands.is_empty() { 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; return false;
} }
} }

38
src/logger.rs Normal file
View file

@ -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());
}

View file

@ -2,6 +2,7 @@ mod app;
mod commands; mod commands;
mod config; mod config;
mod events; mod events;
mod logger;
mod parser; mod parser;
mod ui; mod ui;
@ -66,8 +67,16 @@ fn main() -> io::Result<()> {
)?; )?;
terminal.show_cursor()?; terminal.show_cursor()?;
if let Some(commands) = app.pending_command { if let Some(pending) = app.pending_command {
run_in_current_terminal(&commands, &app.shell); logger::log_run(
&app.data_dir,
&pending.group,
&pending.file,
&pending.section,
&pending.commands,
false,
);
run_in_current_terminal(&pending.commands, &app.shell);
} }
Ok(()) Ok(())