feat(history): Add new flag to allow custom output format (#662)
* feat(history): Add new flag to allow custom output format * more efficient formatting * add user and host * docs Co-authored-by: Conrad Ludgate <conrad.ludgate@truelayer.com>
This commit is contained in:
parent
a8ed8f1325
commit
893a395f12
5 changed files with 124 additions and 36 deletions
18
Cargo.lock
generated
18
Cargo.lock
generated
|
@ -91,6 +91,7 @@ dependencies = [
|
||||||
"itertools",
|
"itertools",
|
||||||
"log",
|
"log",
|
||||||
"rpassword",
|
"rpassword",
|
||||||
|
"runtime-format",
|
||||||
"semver",
|
"semver",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
@ -1647,6 +1648,15 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "runtime-format"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b035308411b1af4683acc4fc928366443f08b893bb73e235c85de4c2be572495"
|
||||||
|
dependencies = [
|
||||||
|
"tinyvec",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "0.36.5"
|
version = "0.36.5"
|
||||||
|
@ -2104,18 +2114,18 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.34"
|
version = "1.0.38"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8c1b05ca9d106ba7d2e31a9dab4a64e7be2cce415321966ea3132c49a656e252"
|
checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl",
|
"thiserror-impl",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror-impl"
|
name = "thiserror-impl"
|
||||||
version = "1.0.34"
|
version = "1.0.38"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e8f2591983642de85c921015f3f070c665a197ed69e417af436115e3a1407487"
|
checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
|
@ -72,6 +72,7 @@ fs-err = "2.7"
|
||||||
whoami = "1.1.2"
|
whoami = "1.1.2"
|
||||||
rpassword = "7.0"
|
rpassword = "7.0"
|
||||||
semver = "1.0.14"
|
semver = "1.0.14"
|
||||||
|
runtime-format = "0.1.2"
|
||||||
|
|
||||||
[dependencies.tracing-subscriber]
|
[dependencies.tracing-subscriber]
|
||||||
version = "0.3"
|
version = "0.3"
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
use std::{
|
use std::{
|
||||||
env,
|
env,
|
||||||
|
fmt::{self, Display},
|
||||||
io::{StdoutLock, Write},
|
io::{StdoutLock, Write},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
use clap::Subcommand;
|
use clap::Subcommand;
|
||||||
use eyre::Result;
|
use eyre::Result;
|
||||||
|
use runtime_format::{FormatKey, FormatKeyError, ParsedFmt};
|
||||||
|
|
||||||
use atuin_client::{
|
use atuin_client::{
|
||||||
database::{current_context, Database},
|
database::{current_context, Database},
|
||||||
|
@ -17,7 +19,7 @@ use atuin_client::{
|
||||||
use atuin_client::sync;
|
use atuin_client::sync;
|
||||||
use log::debug;
|
use log::debug;
|
||||||
|
|
||||||
use super::search::format_duration;
|
use super::search::format_duration_into;
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
#[command(infer_subcommands = true)]
|
#[command(infer_subcommands = true)]
|
||||||
|
@ -46,6 +48,11 @@ pub enum Cmd {
|
||||||
/// Show only the text of the command
|
/// Show only the text of the command
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
cmd_only: bool,
|
cmd_only: bool,
|
||||||
|
|
||||||
|
/// Available variables: {command}, {directory}, {duration}, {user}, {host} and {time}.
|
||||||
|
/// Example: --format "{time} - [{duration}] - {directory}$\t{command}"
|
||||||
|
#[arg(long, short)]
|
||||||
|
format: Option<String>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Get the last command ran
|
/// Get the last command ran
|
||||||
|
@ -56,6 +63,11 @@ pub enum Cmd {
|
||||||
/// Show only the text of the command
|
/// Show only the text of the command
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
cmd_only: bool,
|
cmd_only: bool,
|
||||||
|
|
||||||
|
/// Available variables: {command}, {directory}, {duration}, {user}, {host} and {time}.
|
||||||
|
/// Example: --format "{time} - [{duration}] - {directory}$\t{command}"
|
||||||
|
#[arg(long, short)]
|
||||||
|
format: Option<String>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,43 +91,76 @@ impl ListMode {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::cast_sign_loss)]
|
#[allow(clippy::cast_sign_loss)]
|
||||||
pub fn print_list(h: &[History], list_mode: ListMode) {
|
pub fn print_list(h: &[History], list_mode: ListMode, format: Option<&str>) {
|
||||||
let w = std::io::stdout();
|
let w = std::io::stdout();
|
||||||
let mut w = w.lock();
|
let mut w = w.lock();
|
||||||
|
|
||||||
match list_mode {
|
match list_mode {
|
||||||
ListMode::Human => print_human_list(&mut w, h),
|
ListMode::Human => print_human_list(&mut w, h, format),
|
||||||
ListMode::CmdOnly => print_cmd_only(&mut w, h),
|
ListMode::CmdOnly => print_cmd_only(&mut w, h),
|
||||||
ListMode::Regular => print_regular(&mut w, h),
|
ListMode::Regular => print_regular(&mut w, h, format),
|
||||||
}
|
}
|
||||||
|
|
||||||
w.flush().expect("failed to flush history");
|
w.flush().expect("failed to flush history");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::cast_sign_loss)]
|
/// type wrapper around `History` so we can implement traits
|
||||||
pub fn print_human_list(w: &mut StdoutLock, h: &[History]) {
|
struct FmtHistory<'a>(&'a History);
|
||||||
for h in h.iter().rev() {
|
|
||||||
let duration = format_duration(Duration::from_nanos(std::cmp::max(h.duration, 0) as u64));
|
|
||||||
|
|
||||||
let time = h.timestamp.format("%Y-%m-%d %H:%M:%S");
|
/// defines how to format the history
|
||||||
let cmd = h.command.trim();
|
impl FormatKey for FmtHistory<'_> {
|
||||||
|
#[allow(clippy::cast_sign_loss)]
|
||||||
writeln!(w, "{time} · {duration}\t{cmd}").expect("failed to write history");
|
fn fmt(&self, key: &str, f: &mut fmt::Formatter<'_>) -> Result<(), FormatKeyError> {
|
||||||
|
match key {
|
||||||
|
"command" => f.write_str(self.0.command.trim())?,
|
||||||
|
"directory" => f.write_str(self.0.cwd.trim())?,
|
||||||
|
"duration" => {
|
||||||
|
let dur = Duration::from_nanos(std::cmp::max(self.0.duration, 0) as u64);
|
||||||
|
format_duration_into(dur, f)?;
|
||||||
|
}
|
||||||
|
"time" => self.0.timestamp.format("%Y-%m-%d %H:%M:%S").fmt(f)?,
|
||||||
|
"host" => f.write_str(
|
||||||
|
self.0
|
||||||
|
.hostname
|
||||||
|
.split_once(':')
|
||||||
|
.map_or(&self.0.hostname, |(host, _)| host),
|
||||||
|
)?,
|
||||||
|
"user" => f.write_str(self.0.hostname.split_once(':').map_or("", |(_, user)| user))?,
|
||||||
|
_ => return Err(FormatKeyError::UnknownKey),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::cast_sign_loss)]
|
fn print_list_with(w: &mut StdoutLock, h: &[History], format: &str) {
|
||||||
pub fn print_regular(w: &mut StdoutLock, h: &[History]) {
|
let fmt = match ParsedFmt::new(format) {
|
||||||
|
Ok(fmt) => fmt,
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("ERROR: History formatting failed with the following error: {err}");
|
||||||
|
println!("If your formatting string contains curly braces (eg: {{var}}) you need to escape them this way: {{{{var}}.");
|
||||||
|
std::process::exit(1)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
for h in h.iter().rev() {
|
for h in h.iter().rev() {
|
||||||
let duration = format_duration(Duration::from_nanos(std::cmp::max(h.duration, 0) as u64));
|
writeln!(w, "{}", fmt.with_args(&FmtHistory(h))).expect("failed to write history");
|
||||||
|
|
||||||
let time = h.timestamp.format("%Y-%m-%d %H:%M:%S");
|
|
||||||
let cmd = h.command.trim();
|
|
||||||
|
|
||||||
writeln!(w, "{time}\t{cmd}\t{duration}").expect("failed to write history");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn print_human_list(w: &mut StdoutLock, h: &[History], format: Option<&str>) {
|
||||||
|
let format = format
|
||||||
|
.unwrap_or("{time} · {duration}\t{command}")
|
||||||
|
.replace("\\t", "\t");
|
||||||
|
print_list_with(w, h, &format);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_regular(w: &mut StdoutLock, h: &[History], format: Option<&str>) {
|
||||||
|
let format = format
|
||||||
|
.unwrap_or("{time}\t{command}\t{duration}")
|
||||||
|
.replace("\\t", "\t");
|
||||||
|
print_list_with(w, h, &format);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn print_cmd_only(w: &mut StdoutLock, h: &[History]) {
|
pub fn print_cmd_only(w: &mut StdoutLock, h: &[History]) {
|
||||||
for h in h.iter().rev() {
|
for h in h.iter().rev() {
|
||||||
writeln!(w, "{}", h.command.trim()).expect("failed to write history");
|
writeln!(w, "{}", h.command.trim()).expect("failed to write history");
|
||||||
|
@ -187,6 +232,7 @@ impl Cmd {
|
||||||
cwd,
|
cwd,
|
||||||
human,
|
human,
|
||||||
cmd_only,
|
cmd_only,
|
||||||
|
format,
|
||||||
} => {
|
} => {
|
||||||
let session = if *session {
|
let session = if *session {
|
||||||
Some(env::var("ATUIN_SESSION")?)
|
Some(env::var("ATUIN_SESSION")?)
|
||||||
|
@ -218,14 +264,26 @@ impl Cmd {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
print_list(&history, ListMode::from_flags(*human, *cmd_only));
|
print_list(
|
||||||
|
&history,
|
||||||
|
ListMode::from_flags(*human, *cmd_only),
|
||||||
|
format.as_deref(),
|
||||||
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
Self::Last { human, cmd_only } => {
|
Self::Last {
|
||||||
|
human,
|
||||||
|
cmd_only,
|
||||||
|
format,
|
||||||
|
} => {
|
||||||
let last = db.last().await?;
|
let last = db.last().await?;
|
||||||
print_list(&[last], ListMode::from_flags(*human, *cmd_only));
|
print_list(
|
||||||
|
&[last],
|
||||||
|
ListMode::from_flags(*human, *cmd_only),
|
||||||
|
format.as_deref(),
|
||||||
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ mod duration;
|
||||||
mod event;
|
mod event;
|
||||||
mod history_list;
|
mod history_list;
|
||||||
mod interactive;
|
mod interactive;
|
||||||
pub use duration::format_duration;
|
pub use duration::{format_duration, format_duration_into};
|
||||||
|
|
||||||
#[allow(clippy::struct_excessive_bools)]
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
|
@ -74,6 +74,11 @@ pub struct Cmd {
|
||||||
/// Show only the text of the command
|
/// Show only the text of the command
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
cmd_only: bool,
|
cmd_only: bool,
|
||||||
|
|
||||||
|
/// Available variables: {command}, {directory}, {duration}, {user}, {host} and {time}.
|
||||||
|
/// Example: --format "{time} - [{duration}] - {directory}$\t{command}"
|
||||||
|
#[arg(long, short)]
|
||||||
|
format: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Cmd {
|
impl Cmd {
|
||||||
|
@ -97,6 +102,7 @@ impl Cmd {
|
||||||
self.exit,
|
self.exit,
|
||||||
self.exclude_exit,
|
self.exclude_exit,
|
||||||
self.exclude_cwd,
|
self.exclude_cwd,
|
||||||
|
self.format,
|
||||||
self.before,
|
self.before,
|
||||||
self.after,
|
self.after,
|
||||||
self.limit,
|
self.limit,
|
||||||
|
@ -122,6 +128,7 @@ async fn run_non_interactive(
|
||||||
exit: Option<i64>,
|
exit: Option<i64>,
|
||||||
exclude_exit: Option<i64>,
|
exclude_exit: Option<i64>,
|
||||||
exclude_cwd: Option<String>,
|
exclude_cwd: Option<String>,
|
||||||
|
format: Option<String>,
|
||||||
before: Option<String>,
|
before: Option<String>,
|
||||||
after: Option<String>,
|
after: Option<String>,
|
||||||
limit: Option<i64>,
|
limit: Option<i64>,
|
||||||
|
@ -202,6 +209,6 @@ async fn run_non_interactive(
|
||||||
.map(std::borrow::ToOwned::to_owned)
|
.map(std::borrow::ToOwned::to_owned)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
super::history::print_list(&results, list_mode);
|
super::history::print_list(&results, list_mode, format.as_deref());
|
||||||
Ok(results.len())
|
Ok(results.len())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
|
use core::fmt;
|
||||||
use std::{ops::ControlFlow, time::Duration};
|
use std::{ops::ControlFlow, time::Duration};
|
||||||
|
|
||||||
#[allow(clippy::module_name_repetitions)]
|
#[allow(clippy::module_name_repetitions)]
|
||||||
pub fn format_duration(f: Duration) -> String {
|
pub fn format_duration_into(dur: Duration, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
fn item(name: &str, value: u64) -> ControlFlow<String> {
|
fn item(unit: &'static str, value: u64) -> ControlFlow<(&'static str, u64)> {
|
||||||
if value > 0 {
|
if value > 0 {
|
||||||
ControlFlow::Break(format!("{value}{name}"))
|
ControlFlow::Break((unit, value))
|
||||||
} else {
|
} else {
|
||||||
ControlFlow::Continue(())
|
ControlFlow::Continue(())
|
||||||
}
|
}
|
||||||
|
@ -13,7 +14,7 @@ pub fn format_duration(f: Duration) -> String {
|
||||||
// impl taken and modified from
|
// impl taken and modified from
|
||||||
// https://github.com/tailhook/humantime/blob/master/src/duration.rs#L295-L331
|
// https://github.com/tailhook/humantime/blob/master/src/duration.rs#L295-L331
|
||||||
// Copyright (c) 2016 The humantime Developers
|
// Copyright (c) 2016 The humantime Developers
|
||||||
fn fmt(f: Duration) -> ControlFlow<String, ()> {
|
fn fmt(f: Duration) -> ControlFlow<(&'static str, u64), ()> {
|
||||||
let secs = f.as_secs();
|
let secs = f.as_secs();
|
||||||
let nanos = f.subsec_nanos();
|
let nanos = f.subsec_nanos();
|
||||||
|
|
||||||
|
@ -43,8 +44,19 @@ pub fn format_duration(f: Duration) -> String {
|
||||||
ControlFlow::Continue(())
|
ControlFlow::Continue(())
|
||||||
}
|
}
|
||||||
|
|
||||||
match fmt(f) {
|
match fmt(dur) {
|
||||||
ControlFlow::Break(b) => b,
|
ControlFlow::Break((unit, value)) => write!(f, "{value}{unit}"),
|
||||||
ControlFlow::Continue(()) => String::from("0s"),
|
ControlFlow::Continue(()) => write!(f, "0s"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::module_name_repetitions)]
|
||||||
|
pub fn format_duration(f: Duration) -> String {
|
||||||
|
struct F(Duration);
|
||||||
|
impl fmt::Display for F {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
format_duration_into(self.0, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
F(f).to_string()
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue