F2QYIRKBFYFWSTB7Z5CNGSZVYI6XQO3MHPUHJLBHPGLIOG7UNVNQC A4JWJ2GZLECVXZYT24PX2FNNXERZG5QDFNUZDT7NGA22QXBJHLDAC 54NTBFDPH7QNK7J5ACSLOIU7B5ELJ2FJUJYWW4LTCXZAS6NE227QC NXADNFPSCM7ETIGZ676I7X3WENZPRBEZGIOLDWC2FSY652QK2UBAC T2HK3ZSDLLLGHROM77MND4UZF663WSHT5J2CT7ZMXDH6MMIZFOAQC B2MSSEJB4GIBFA6F2HRMK7FXUHJBZIZFK6NOHY7YZUSHRB53URGQC LSRFYRWWQXJ2LVGZ7FWF5O57E4RK45EE6NWLNTRSRO5M7TFW7PZAC VEN5WJYRT23IT77JAAZ5CJRSW3GUTTNMAECT3WVTHQA34HI4646AC F6L2P7VTKQUV64O66XPEIRG46ZO36E4FWFN3KNMBPRHQLQWL6HSAC K7M77GF5ILC4KKKYPTLZRZ2OND7DOQDQNRYKM3N6XV2DMJURYA3QC B2BZZXWMRR2FYPBMSSQ6WBCOWX5R3JOXSESX4LFUAQF3FOS4APBQC 5WTKSBFRO522ILOHTH5OII4EES3DMLWMTA47PXCVWCIGMXIS77KAC TXF4WACU3YJSUMTXJ2VZ3K7OXL2Z564R5KGMVMAFIVXPCLYCMD2AC EEBZMKMLEADO3ODXOQ7VNZNT36L7BYFRQUS2S2HLDVBZI7FHZQFQC [[package]]name = "arrayref"version = "0.3.6"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544"[[package]]name = "arrayvec"version = "0.5.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8"[[package]]name = "atty"version = "0.2.14"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"dependencies = ["hermit-abi","libc","winapi",]
][[package]]name = "constant_time_eq"version = "0.1.5"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"[[package]]name = "crossbeam-utils"version = "0.7.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8"dependencies = ["autocfg","cfg-if","lazy_static",
][[package]]name = "directories"version = "3.0.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "f8fed639d60b58d0f53498ab13d26f621fd77569cc6edb031f4cc36a2ad9da0f"dependencies = ["dirs-sys",][[package]]name = "dirs-sys"version = "0.3.5"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "8e93d7f5705de3e49895a2b5e0b8855a1c27f080192ae9c32a6432d50741a57a"dependencies = ["libc","redox_users","winapi",
[[package]]name = "proc-macro-error"version = "1.0.4"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"dependencies = ["proc-macro-error-attr","proc-macro2","quote","syn","version_check",][[package]]name = "proc-macro-error-attr"version = "1.0.4"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"dependencies = ["proc-macro2","quote","version_check",]
name = "structopt"version = "0.3.18"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "a33f6461027d7f08a13715659b2948e1602c31a3756aeae9378bfe7518c72e82"dependencies = ["clap","lazy_static","structopt-derive",][[package]]name = "structopt-derive"version = "0.4.11"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "c92e775028122a4b3dd55d58f14fc5120289c69bee99df1d117ae30f84b225c9"dependencies = ["heck","proc-macro-error","proc-macro2","quote","syn",][[package]]
name = "vec_map"version = "0.8.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"[[package]]name = "version_check"version = "0.9.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"[[package]]
use chrono::{DateTime,Utc,};use comfy_table::{Attribute,Cell,Table,};use message::{CommandFinished,CommandStart,Message,};use rusqlite::params;use std::{collections::{BTreeSet,HashMap,},convert::TryInto,path::PathBuf,};use uuid::Uuid;
use opt::Opt;use structopt::StructOpt;
let command = std::env::args().into_iter().nth(1).unwrap_or_default();match command.as_str() {"zshaddhistory" => {let data = CommandStart::from_env()?;client::new().send(Message::CommandStart(data))?;Ok(())}"precmd" => {let data = CommandFinished::from_env()?;client::new().send(Message::CommandFinished(data))?;Ok(())}"session_id" => {println!("{}", Uuid::new_v4());Ok(())}"stop" => {client::new().send(Message::Stop)?;Ok(())}"running" => {client::new().send(Message::Running)?;Ok(())}"import" => {let path = std::env::args().into_iter().nth(2).unwrap_or_default();dbg!(&path);let db = rusqlite::Connection::open(&path)?;let mut stmt = db.prepare("select * from history left join places on places.id=history.place_id left join \commands on history.command_id=commands.id",)?;#[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)?,})})?.collect::<Result<BTreeSet<_>, _>>()?;println!("{:?}", entries.len());let mut session_ids: HashMap<(i64, String), Uuid> = HashMap::new();let store = crate::store::new();let xdg_dirs = xdg::BaseDirectories::with_prefix("histdb-rs").unwrap();let datadir_path = xdg_dirs.get_data_home();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(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.unwrap(), 0),Utc,);let hostname = entry.hostname;let pwd = PathBuf::from(entry.pwd);let result = entry.exit_status.unwrap().try_into().unwrap();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, &datadir_path).unwrap();}let hostname = hostname::get()?.to_string_lossy().to_string();store.commit(&hostname)?;Ok(())}"server" => {let xdg_dirs = xdg::BaseDirectories::with_prefix("histdb-rs")?;let server = match xdg_dirs.find_cache_file("server.json") {None => server::new(),Some(path) => {let file = std::fs::File::open(path).unwrap();let reader = std::io::BufReader::new(file);serde_json::from_reader(reader).unwrap()}};let server = server.start()?;let path = xdg_dirs.place_cache_file("server.json").unwrap();let file = std::fs::File::create(path).unwrap();let writer = std::io::BufWriter::new(file);serde_json::to_writer(writer, &server).unwrap();Ok(())}_ => {let hostname = hostname::get()?.to_string_lossy().to_string();let entries = store::new().get_nth_entries(Some(&hostname), 25)?;let mut table = Table::new();table.load_preset(" ");table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic);table.set_header(vec![Cell::new("tmn").add_attribute(Attribute::Bold),Cell::new("ses").add_attribute(Attribute::Bold),Cell::new("res").add_attribute(Attribute::Bold),Cell::new("pwd").add_attribute(Attribute::Bold),Cell::new("cmd").add_attribute(Attribute::Bold),]);for entry in entries.into_iter() {table.add_row(vec![format_timestamp(entry.time_finished),format_uuid(entry.session_id),format!("{}", entry.result),format_pwd(entry.pwd),entry.command.trim().to_string(),]);}println!("{}", table);Ok(())}}}fn format_timestamp(timestamp: DateTime<Utc>) -> String {let today = Utc::now().date();let date = timestamp.date();if date == today {timestamp.format("%H:%M").to_string()} else {timestamp.date().format("%Y-%m-%d").to_string()}}
let opt = Opt::from_args();
fn format_pwd(pwd: PathBuf) -> String {let home = std::env::var("HOME").unwrap();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));without_home.to_string_lossy().to_string()} else {pwd.to_string_lossy().to_string()}}
pub fn from_env() -> Result<Self, Error> {let command = env::args().into_iter().nth(2).unwrap_or_default().trim().to_string();
pub fn from_env(command: String) -> Result<Self, Error> {
use crate::{client,message,message::{CommandFinished,CommandStart,Message,},server,store,};use anyhow::Result;use chrono::{DateTime,Utc,};use comfy_table::{Attribute,Cell,Table,};use directories::ProjectDirs;use rusqlite::params;use std::{convert::TryInto,path::PathBuf,};use structopt::{clap::AppSettings::*,StructOpt,};use thiserror::Error;use uuid::Uuid;macro_rules! into_str {($x:expr) => {{structopt::lazy_static::lazy_static! {static ref DATA: String = $x.to_string();}DATA.as_str()}};}fn project_dir() -> ProjectDirs {ProjectDirs::from("com", "histdb-rs", "histdb-rs").unwrap()}fn default_data_dir() -> String {let project_dir = project_dir();let data_dir = project_dir.data_dir();data_dir.to_string_lossy().to_string()}fn default_cache_path() -> String {let project_dir = project_dir();let cache_path = project_dir.cache_dir().join("server.json");cache_path.to_string_lossy().to_string()}fn default_socket_path() -> String {let project_dir = project_dir();let socket_path = project_dir.runtime_dir().unwrap().join("server_socket");socket_path.to_string_lossy().to_string()}#[derive(StructOpt, Debug)]struct ZSHAddHistory {#[structopt(index = 1)]command: String,}#[derive(StructOpt, Debug)]struct Server {#[structopt(short, long, default_value = into_str!(default_cache_path()))]cache_path: PathBuf,}#[derive(StructOpt, Debug)]struct Import {#[structopt(short, long, default_value = into_str!(default_cache_path()))]import_file: PathBuf,}#[derive(StructOpt, Debug)]enum SubCommand {#[structopt(name = "zshaddhistory")]ZSHAddHistory(ZSHAddHistory),#[structopt(name = "server")]Server(Server),#[structopt(name = "stop")]Stop,#[structopt(name = "precmd")]PreCmd,#[structopt(name = "session_id")]SessionID,#[structopt(name = "running")]Running,#[structopt(name = "import")]Import(Import),}#[derive(StructOpt, Debug)]#[structopt(global_settings = &[ColoredHelp, VersionlessSubcommands, NextLineHelp, GlobalVersion])]pub struct Opt {/// Path to folder in which to store the history files.#[structopt(global = true,short,long,default_value = into_str!(default_data_dir()))]data_dir: PathBuf,/// Path to the socket for communication with the server#[structopt(global = true, short, long, default_value = into_str!(default_socket_path()))]socket_path: PathBuf,#[structopt(subcommand)]sub_command: Option<SubCommand>,}#[derive(Error, Debug)]pub enum Error {#[error("{0}")]ClientError(client::Error),#[error("{0}")]MessageError(message::Error),#[error("{0}")]ServerError(server::Error),#[error("{0}")]StoreError(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),}impl From<client::Error> for Error {fn from(err: client::Error) -> Self {Error::ClientError(err)}}impl From<message::Error> for Error {fn from(err: message::Error) -> Self {Error::MessageError(err)}}impl From<server::Error> for Error {fn from(err: server::Error) -> Self {Error::ServerError(err)}}impl From<store::Error> for Error {fn from(err: store::Error) -> Self {Error::StoreError(err)}}impl Opt {pub fn run(self) -> Result<(), Error> {let sub_command = self.sub_command;match sub_command {Some(sub_command) => match sub_command {SubCommand::ZSHAddHistory(o) => {Self::run_zsh_add_history(o.command, self.socket_path)}SubCommand::Server(o) => {Self::run_server(o.cache_path, self.socket_path, self.data_dir)}SubCommand::Stop => Self::run_stop(self.socket_path),SubCommand::PreCmd => Self::run_precmd(self.socket_path),SubCommand::SessionID => Self::run_session_id(),SubCommand::Running => Self::run_running(self.socket_path),SubCommand::Import(o) => Self::run_import(o.import_file, self.data_dir),},None => Self::run_default(self.data_dir),}}fn run_default(data_dir: PathBuf) -> Result<(), Error> {let hostname = hostname::get().map_err(Error::GetHostname)?.to_string_lossy().to_string();let entries = store::new(data_dir).get_nth_entries(Some(&hostname), 25)?;let mut table = Table::new();table.load_preset(" ");table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic);table.set_header(vec![Cell::new("tmn").add_attribute(Attribute::Bold),Cell::new("ses").add_attribute(Attribute::Bold),Cell::new("res").add_attribute(Attribute::Bold),Cell::new("pwd").add_attribute(Attribute::Bold),Cell::new("cmd").add_attribute(Attribute::Bold),]);for entry in entries.into_iter() {table.add_row(vec![format_timestamp(entry.time_finished),format_uuid(entry.session_id),format!("{}", entry.result),format_pwd(entry.pwd),entry.command.trim().to_string(),]);}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)?;println!("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.unwrap(), 0),Utc,);let hostname = entry.hostname;let pwd = PathBuf::from(entry.pwd);let result = entry.exit_status.unwrap().try_into().unwrap();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).unwrap();}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 = Utc::now().date();let date = timestamp.date();if date == today {timestamp.format("%H:%M").to_string()} else {timestamp.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) -> String {let home = std::env::var("HOME").unwrap();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));without_home.to_string_lossy().to_string()} else {pwd.to_string_lossy().to_string()}}
pub fn new() -> Server {Server {entries: HashMap::new(),
pub fn new(cache_path: PathBuf, data_dir: PathBuf) -> Result<Server, Error> {if cache_path.exists() {from_cachefile(cache_path, data_dir)} else {Ok(Server {entries: HashMap::new(),store: store::new(data_dir),cache_path,})
impl Server {pub fn start(mut self) -> Result<Self, Error> {let xdg_dirs = xdg::BaseDirectories::with_prefix("histdb-rs").unwrap();let socket_path = xdg_dirs.place_runtime_file("socket").unwrap();
fn from_cachefile(cache_path: PathBuf, data_dir: PathBuf) -> Result<Server, Error> {let file = std::fs::File::open(&cache_path).unwrap();let reader = std::io::BufReader::new(file);let entries = serde_json::from_reader(reader).unwrap();let store = store::new(data_dir);Ok(Server {entries,store,cache_path,})}