Add history.log for tracking command executions
This commit is contained in:
parent
f15be840ca
commit
8d92110c58
8 changed files with 233 additions and 8 deletions
128
Cargo.lock
generated
128
Cargo.lock
generated
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
10
src/app.rs
10
src/app.rs
|
|
@ -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>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
§ion.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
38
src/logger.rs
Normal 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());
|
||||||
|
}
|
||||||
13
src/main.rs
13
src/main.rs
|
|
@ -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(())
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue