AIF5IVL7B5S2X3N4RLS6GNKUCASQZPOH6NCVGECUFHQ5ZUJVDU6QC TD5C7JSE4WCYXSLG7GW6GQU2M6O5GALQAVOPNT7A4WUHXRHPUQYAC WFOZ5OXZGGWJYA7LUVY7EV45MNG477SIOWYWIJ6VGUMNBSHDJGKAC 2SE6RZTNFW52MEWZXMQCVHXF3TSAYOOVVBU2JMAQPQJ4C3DWYAKQC 7V5FJVDUULMBA6XTRS2C625QWWSP3L27QPIINWQHAH2SEFWTTWHQC F2QYIRKBFYFWSTB7Z5CNGSZVYI6XQO3MHPUHJLBHPGLIOG7UNVNQC B2MSSEJB4GIBFA6F2HRMK7FXUHJBZIZFK6NOHY7YZUSHRB53URGQC FKTISISENWXRFIQHUBCSWQAAN7OWUWIOD6R64YEQOBZ6VENZUWYQC YQSLDBVSLMXMWYHDASV2QOSYZMMLV2T2QT2IWJT6WYWQKOL2MKFQC 42MNZNAH4QWQPYFUEMXD2OCDH5PRANP4IXEIK3SBYEOMJXTOKQ7QC RAVOIZIUDSG2A44PKQP3KY7IVBWBYSQY4I744B6I7QZFRL3AE4EQC I5WVRUHGAQGWFZGX7YBKCGQKLHXZHZTCBWFIUCXVGY2WVFB77VQQC FTI67CGF4MMPDFA6YJN6UKOADQLFAECKGYPTWSPSALVQK76BJMJAC EDCVYHTHMBCI3VIBGMA35H7MFYK3MTEJH36ET36PQBB5WGKDXQ5QC VK3SMPPCGTZANUZKMXBUMIZWXQDNR3PZU3DE7SRWXTMXNSI7XBJAC V74ZMPLHZQYD4MXOYTT6SLVV5AY2B3MAAS27L2M7PIM3D3RZZKCQC 5PDXNCBBHM3UXTGZNX76FUMWUHBL6BGB7MFBXFGVN5TJECMBYUJAC MIIKFTYUUN5UC74HIC5SMCPAWU7ZYOHWANZLCPWREKSPD4UE65JQC PPVF6CAUFB6GIKSYLQ2IYIOPSRRNNBQUG5CLXJDMO6PRD23M4F3AC ZNNEYXCD2DFYUQTIP5NC3YEYVVBBKG32OUWDKM7REM7BL5OPFTDAC T2HK3ZSDLLLGHROM77MND4UZF663WSHT5J2CT7ZMXDH6MMIZFOAQC use crate::{client,message,message::{CommandFinished,CommandStart,Message,},server,store,};use chrono::{DateTime,Local,Utc,};use comfy_table::{Attribute,Cell,Table,};
use crate::run;
#[derive(Error, Debug)]pub enum Error {#[error("{0}")]ClientError(#[from] client::Error),#[error("{0}")]MessageError(#[from] message::Error),#[error("{0}")]ServerError(#[from] server::Error),
#[error("{0}")]StoreError(#[from] store::Error),#[error("can not get hostname: {0}")]GetHostname(std::io::Error),#[error("can not open sqlite database: {0}")]OpenSqliteDatabase(rusqlite::Error),#[error("can not prepare sqlite query to get entries: {0}")]PrepareSqliteQuery(rusqlite::Error),#[error("can not convert sqlite row: {0}")]ConvertSqliteRow(rusqlite::Error),#[error("can not collect entries from sqlite query: {0}")]CollectEntries(rusqlite::Error),#[error("can not convert exit status from sqlite: {0}")]ConvertExitStatus(std::num::TryFromIntError),#[error("can not get base directories")]GetBaseDirectories,#[error("can not get current directory: {0}")]GetCurrentDir(std::io::Error),#[error("can not convert chrono milliseconds: {0}")]ConvertDuration(std::num::TryFromIntError),}
SubCommand::Stop(o) => Self::run_stop(o.socket_path),SubCommand::PreCmd(o) => Self::run_precmd(o.socket_path),SubCommand::SessionID => Self::run_session_id(),SubCommand::Running(o) => Self::run_running(o.socket_path),SubCommand::Import(o) => Self::run_import(o.import_file, o.data_dir.data_dir),
SubCommand::Stop(o) => run::stop(o.socket_path),SubCommand::PreCmd(o) => run::precmd(o.socket_path),SubCommand::SessionID => run::session_id(),SubCommand::Running(o) => run::running(o.socket_path),SubCommand::Import(o) => run::import(o.import_file, o.data_dir.data_dir),
None => Self::run_default(self.default_args),}}fn run_default(args: DefaultArgs) -> Result<(), Error> {let dir_filter = if args.in_current {Some(std::env::current_dir().map_err(Error::GetCurrentDir)?)} else {args.folder};let hostname = hostname::get().map_err(Error::GetHostname)?.to_string_lossy().to_string();let hostname_filter = if args.all_hosts {None} else {Some(args.hostname.unwrap_or(hostname))};let entries = store::new(args.data_dir.data_dir).get_entries(hostname_filter,args.entries_count,args.command,dir_filter,args.no_subdirs,args.command_text,)?;
let mut table = Table::new();table.load_preset(" ");table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic);if args.no_format {table.force_no_tty();}
None => {let in_current = self.default_args.in_current;let folder = self.default_args.folder;let all_hosts = self.default_args.all_hosts;let hostname = self.default_args.hostname;let data_dir = self.default_args.data_dir.data_dir;let entries_count = self.default_args.entries_count;let command = self.default_args.command;let no_subdirs = self.default_args.no_subdirs;let command_text = self.default_args.command_text;let no_format = self.default_args.no_format;let host = self.default_args.host;let duration = self.default_args.duration;let status = self.default_args.status;
let mut header = vec![Cell::new("tmn").add_attribute(Attribute::Bold)];if args.host {header.push(Cell::new("host").add_attribute(Attribute::Bold))};if args.duration {header.push(Cell::new("duration").add_attribute(Attribute::Bold))};if args.status {header.push(Cell::new("res").add_attribute(Attribute::Bold))};header.push(Cell::new("ses").add_attribute(Attribute::Bold));header.push(Cell::new("pwd").add_attribute(Attribute::Bold));header.push(Cell::new("cmd").add_attribute(Attribute::Bold));table.set_header(header);for entry in entries.into_iter() {let mut row = vec![format_timestamp(entry.time_finished)];if args.host {row.push(entry.hostname)
run::default(in_current,folder,all_hosts,hostname,data_dir,entries_count,command,no_subdirs,command_text,no_format,host,duration,status,)
if args.duration {row.push(format_duration(entry.time_start, entry.time_finished)?)}if args.status {row.push(format!("{}", entry.result))}row.push(format_uuid(entry.session_id));row.push(format_pwd(entry.pwd)?);row.push(format_command(entry.command, args.no_format));table.add_row(row);
println!("{}", table);Ok(())}fn run_zsh_add_history(command: String, socket_path: PathBuf) -> Result<(), Error> {let data = CommandStart::from_env(command)?;client::new(socket_path).send(Message::CommandStart(data))?;Ok(())
fn run_server(cache_path: PathBuf,socket_path: PathBuf,data_dir: PathBuf,) -> Result<(), Error> {let server = server::new(cache_path, data_dir)?;server.start(socket_path)?;Ok(())}fn run_stop(socket_path: PathBuf) -> Result<(), Error> {client::new(socket_path).send(Message::Stop)?;Ok(())}fn run_precmd(socket_path: PathBuf) -> Result<(), Error> {let data = CommandFinished::from_env()?;client::new(socket_path).send(Message::CommandFinished(data))?;Ok(())}fn run_session_id() -> Result<(), Error> {println!("{}", Uuid::new_v4());Ok(())}fn run_running(socket_path: PathBuf) -> Result<(), Error> {client::new(socket_path).send(Message::Running)?;Ok(())}fn run_import(import_file: PathBuf, data_dir: PathBuf) -> Result<(), Error> {let db = rusqlite::Connection::open(&import_file).map_err(Error::OpenSqliteDatabase)?;let mut stmt = db.prepare("select * from history left join places on places.id=history.place_idleft join commands on history.command_id=commands.id",).map_err(Error::PrepareSqliteQuery)?;#[derive(Debug, Ord, PartialOrd, Eq, PartialEq)]struct DBEntry {session: i64,start_time: i64,duration: Option<i64>,exit_status: Option<i64>,hostname: String,pwd: String,command: String,}let entries = stmt.query_map(params![], |row| {Ok(DBEntry {session: row.get(1)?,exit_status: row.get(4)?,start_time: row.get(5)?,duration: row.get(6)?,hostname: row.get(8)?,pwd: row.get(9)?,command: row.get(11)?,})}).map_err(Error::ConvertSqliteRow)?.collect::<Result<std::collections::BTreeSet<_>, _>>().map_err(Error::CollectEntries)?;info!("importing {:?} entries", entries.len());let mut session_ids = std::collections::HashMap::new();let store = crate::store::new(data_dir);for entry in entries {if entry.duration.is_none()|| entry.exit_status.is_none()|| entry.command.trim().is_empty(){continue;}let session_id = session_ids.entry((entry.session, entry.hostname.clone())).or_insert_with(Uuid::new_v4);let start_time = entry.start_time;let time_start = chrono::DateTime::<Utc>::from_utc(chrono::NaiveDateTime::from_timestamp(start_time, 0),Utc,);let time_finished = chrono::DateTime::<Utc>::from_utc(chrono::NaiveDateTime::from_timestamp(start_time+ entry.duration.expect("save as we already checked if duration is some earlier"),0,),Utc,);let hostname = entry.hostname;let pwd = PathBuf::from(entry.pwd);let result = entry.exit_status.expect("save as we already checked if status is some earlier").try_into().map_err(Error::ConvertExitStatus)?;let user = String::new();let command = entry.command;let entry = crate::entry::Entry {time_finished,time_start,hostname,pwd,result,session_id: *session_id,user,command,};store.add_entry(&entry)?;}let hostname = hostname::get().map_err(Error::GetHostname)?.to_string_lossy().to_string();store.commit(format!("imported histdb file from {:?}", &hostname))?;Ok(())}}fn format_timestamp(timestamp: DateTime<Utc>) -> String {let today = Local::now().date();let local = timestamp.with_timezone(&chrono::offset::Local);let date = local.date().with_timezone(&chrono::offset::Local);if date == today {local.format("%H:%M").to_string()} else {local.date().format("%Y-%m-%d").to_string()}
fn format_uuid(uuid: Uuid) -> String {let chars = uuid.to_string().chars().collect::<Vec<_>>();vec![chars[0], chars[1], chars[2], chars[3]].into_iter().collect()}fn format_pwd(pwd: PathBuf) -> Result<String, Error> {let base_dirs = directories::BaseDirs::new().ok_or(Error::GetBaseDirectories)?;let home = base_dirs.home_dir();if pwd.starts_with(home) {let mut without_home = PathBuf::from("~");let pwd_components = pwd.components().skip(3);pwd_components.for_each(|component| without_home.push(component));Ok(without_home.to_string_lossy().to_string())} else {Ok(pwd.to_string_lossy().to_string())}}fn format_duration(time_start: DateTime<Utc>,time_finished: DateTime<Utc>,) -> Result<String, Error> {let duration = time_finished - time_start;let duration_ms = duration.num_milliseconds();let duration_std =std::time::Duration::from_millis(duration_ms.try_into().map_err(Error::ConvertDuration)?);Ok(humantime::format_duration(duration_std).to_string().replace(" ", ""))}fn format_command(command: String, no_format: bool) -> String {if no_format {command.trim().replace("\n", "\\n")} else {command.trim().to_string()}}
use crate::{client,message,message::{CommandFinished,CommandStart,Message,},server,store,};use chrono::{DateTime,Local,Utc,};use comfy_table::{Attribute,Cell,Table,};use log::info;use regex::Regex;use rusqlite::params;use std::{convert::TryInto,path::PathBuf,};use thiserror::Error;use uuid::Uuid;#[derive(Error, Debug)]pub enum Error {#[error("{0}")]ClientError(#[from] client::Error),#[error("{0}")]MessageError(#[from] message::Error),#[error("{0}")]ServerError(#[from] server::Error),#[error("{0}")]StoreError(#[from] store::Error),#[error("can not get hostname: {0}")]GetHostname(std::io::Error),#[error("can not open sqlite database: {0}")]OpenSqliteDatabase(rusqlite::Error),#[error("can not prepare sqlite query to get entries: {0}")]PrepareSqliteQuery(rusqlite::Error),#[error("can not convert sqlite row: {0}")]ConvertSqliteRow(rusqlite::Error),#[error("can not collect entries from sqlite query: {0}")]CollectEntries(rusqlite::Error),#[error("can not convert exit status from sqlite: {0}")]ConvertExitStatus(std::num::TryFromIntError),#[error("can not get base directories")]GetBaseDirectories,#[error("can not get current directory: {0}")]GetCurrentDir(std::io::Error),#[error("can not convert chrono milliseconds: {0}")]ConvertDuration(std::num::TryFromIntError),}pub fn default(in_current: bool,folder: Option<PathBuf>,all_hosts: bool,hostname: Option<String>,data_dir: PathBuf,entries_count: usize,command: Option<String>,no_subdirs: bool,command_text: Option<Regex>,no_format: bool,host: bool,duration: bool,status: bool,) -> Result<(), Error> {let dir_filter = if in_current {Some(std::env::current_dir().map_err(Error::GetCurrentDir)?)} else {folder};let current_hostname = hostname::get().map_err(Error::GetHostname)?.to_string_lossy().to_string();let hostname_filter = if all_hosts {None} else {Some(hostname.unwrap_or(current_hostname))};let entries = store::new(data_dir).get_entries(hostname_filter,entries_count,command,dir_filter,no_subdirs,command_text,)?;let mut table = Table::new();table.load_preset(" ");table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic);if no_format {table.force_no_tty();}let mut header = vec![Cell::new("tmn").add_attribute(Attribute::Bold)];if host {header.push(Cell::new("host").add_attribute(Attribute::Bold))};if duration {header.push(Cell::new("duration").add_attribute(Attribute::Bold))};if status {header.push(Cell::new("res").add_attribute(Attribute::Bold))};header.push(Cell::new("ses").add_attribute(Attribute::Bold));header.push(Cell::new("pwd").add_attribute(Attribute::Bold));header.push(Cell::new("cmd").add_attribute(Attribute::Bold));table.set_header(header);for entry in entries.into_iter() {let mut row = vec![format_timestamp(entry.time_finished)];if host {row.push(entry.hostname)}if duration {row.push(format_duration(entry.time_start, entry.time_finished)?)}if status {row.push(format!("{}", entry.result))}row.push(format_uuid(entry.session_id));row.push(format_pwd(entry.pwd)?);row.push(format_command(entry.command, no_format));table.add_row(row);}println!("{}", table);Ok(())}pub fn zsh_add_history(command: String, socket_path: PathBuf) -> Result<(), Error> {let data = CommandStart::from_env(command)?;client::new(socket_path).send(Message::CommandStart(data))?;Ok(())}pub fn server(cache_path: PathBuf, socket_path: PathBuf, data_dir: PathBuf) -> Result<(), Error> {let server = server::new(cache_path, data_dir)?;server.start(socket_path)?;Ok(())}pub fn stop(socket_path: PathBuf) -> Result<(), Error> {client::new(socket_path).send(Message::Stop)?;Ok(())}pub fn precmd(socket_path: PathBuf) -> Result<(), Error> {let data = CommandFinished::from_env()?;client::new(socket_path).send(Message::CommandFinished(data))?;Ok(())}pub fn session_id() -> Result<(), Error> {println!("{}", Uuid::new_v4());Ok(())}pub fn running(socket_path: PathBuf) -> Result<(), Error> {client::new(socket_path).send(Message::Running)?;Ok(())}pub fn import(import_file: PathBuf, data_dir: PathBuf) -> Result<(), Error> {let db = rusqlite::Connection::open(&import_file).map_err(Error::OpenSqliteDatabase)?;let mut stmt = db.prepare("select * from history left join places on places.id=history.place_idleft join commands on history.command_id=commands.id",).map_err(Error::PrepareSqliteQuery)?;#[derive(Debug, Ord, PartialOrd, Eq, PartialEq)]struct DBEntry {session: i64,start_time: i64,duration: Option<i64>,exit_status: Option<i64>,hostname: String,pwd: String,command: String,}let entries = stmt.query_map(params![], |row| {Ok(DBEntry {session: row.get(1)?,exit_status: row.get(4)?,start_time: row.get(5)?,duration: row.get(6)?,hostname: row.get(8)?,pwd: row.get(9)?,command: row.get(11)?,})}).map_err(Error::ConvertSqliteRow)?.collect::<Result<std::collections::BTreeSet<_>, _>>().map_err(Error::CollectEntries)?;info!("importing {:?} entries", entries.len());let mut session_ids = std::collections::HashMap::new();let store = crate::store::new(data_dir);for entry in entries {if entry.duration.is_none()|| entry.exit_status.is_none()|| entry.command.trim().is_empty(){continue;}let session_id = session_ids.entry((entry.session, entry.hostname.clone())).or_insert_with(Uuid::new_v4);let start_time = entry.start_time;let time_start = chrono::DateTime::<Utc>::from_utc(chrono::NaiveDateTime::from_timestamp(start_time, 0),Utc,);let time_finished = chrono::DateTime::<Utc>::from_utc(chrono::NaiveDateTime::from_timestamp(start_time+ entry.duration.expect("save as we already checked if duration is some earlier"),0,),Utc,);let hostname = entry.hostname;let pwd = PathBuf::from(entry.pwd);let result = entry.exit_status.expect("save as we already checked if status is some earlier").try_into().map_err(Error::ConvertExitStatus)?;let user = String::new();let command = entry.command;let entry = crate::entry::Entry {time_finished,time_start,hostname,pwd,result,session_id: *session_id,user,command,};store.add_entry(&entry)?;}let hostname = hostname::get().map_err(Error::GetHostname)?.to_string_lossy().to_string();store.commit(format!("imported histdb file from {:?}", &hostname))?;Ok(())}fn format_timestamp(timestamp: DateTime<Utc>) -> String {let today = Local::now().date();let local = timestamp.with_timezone(&chrono::offset::Local);let date = local.date().with_timezone(&chrono::offset::Local);if date == today {local.format("%H:%M").to_string()} else {local.date().format("%Y-%m-%d").to_string()}}fn format_uuid(uuid: uuid::Uuid) -> String {let chars = uuid.to_string().chars().collect::<Vec<_>>();vec![chars[0], chars[1], chars[2], chars[3]].into_iter().collect()}fn format_pwd(pwd: PathBuf) -> Result<String, Error> {let base_dirs = directories::BaseDirs::new().ok_or(Error::GetBaseDirectories)?;let home = base_dirs.home_dir();if pwd.starts_with(home) {let mut without_home = PathBuf::from("~");let pwd_components = pwd.components().skip(3);pwd_components.for_each(|component| without_home.push(component));Ok(without_home.to_string_lossy().to_string())} else {Ok(pwd.to_string_lossy().to_string())}}fn format_duration(time_start: DateTime<Utc>,time_finished: DateTime<Utc>,) -> Result<String, Error> {let duration = time_finished - time_start;let duration_ms = duration.num_milliseconds();let duration_std =std::time::Duration::from_millis(duration_ms.try_into().map_err(Error::ConvertDuration)?);Ok(humantime::format_duration(duration_std).to_string().replace(" ", ""))}fn format_command(command: String, no_format: bool) -> String {if no_format {command.trim().replace("\n", "\\n")} else {command.trim().to_string()}}