use std::{ fs::File, io::{BufRead, BufReader}, path::{Path, PathBuf}, }; use chrono::{TimeZone, Utc}; use directories::UserDirs; use eyre::{eyre, Result}; use serde::Deserialize; use atuin_common::utils::uuid_v4; use super::{count_lines, Importer}; use crate::history::History; #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct ReshEntry { pub cmd_line: String, pub exit_code: i64, pub shell: String, pub uname: String, pub session_id: String, pub home: String, pub lang: String, pub lc_all: String, pub login: String, pub pwd: String, pub pwd_after: String, pub shell_env: String, pub term: String, pub real_pwd: String, pub real_pwd_after: String, pub pid: i64, pub session_pid: i64, pub host: String, pub hosttype: String, pub ostype: String, pub machtype: String, pub shlvl: i64, pub timezone_before: String, pub timezone_after: String, pub realtime_before: f64, pub realtime_after: f64, pub realtime_before_local: f64, pub realtime_after_local: f64, pub realtime_duration: f64, pub realtime_since_session_start: f64, pub realtime_since_boot: f64, pub git_dir: String, pub git_real_dir: String, pub git_origin_remote: String, pub git_dir_after: String, pub git_real_dir_after: String, pub git_origin_remote_after: String, pub machine_id: String, pub os_release_id: String, pub os_release_version_id: String, pub os_release_id_like: String, pub os_release_name: String, pub os_release_pretty_name: String, pub resh_uuid: String, pub resh_version: String, pub resh_revision: String, pub parts_merged: bool, pub recalled: bool, pub recall_last_cmd_line: String, pub cols: String, pub lines: String, } #[derive(Debug)] pub struct Resh { file: BufReader, strbuf: String, loc: usize, } impl Importer for Resh { const NAME: &'static str = "resh"; fn histpath() -> Result { let user_dirs = UserDirs::new().unwrap(); let home_dir = user_dirs.home_dir(); Ok(home_dir.join(".resh_history.json")) } fn parse(path: impl AsRef) -> Result { let file = File::open(path)?; let mut buf = BufReader::new(file); let loc = count_lines(&mut buf)?; Ok(Self { file: buf, strbuf: String::new(), loc, }) } } impl Iterator for Resh { type Item = Result; fn next(&mut self) -> Option { self.strbuf.clear(); match self.file.read_line(&mut self.strbuf) { Ok(0) => return None, Ok(_) => (), Err(e) => return Some(Err(eyre!("failed to read line: {}", e))), // we can skip past things like invalid utf8 } // .resh_history.json lies about being a json. It is actually a file containing valid json // on every line. This means that the last line will throw an error, as it is just an EOF. // Without the special case here, that will crash the importer. let entry = match serde_json::from_str::(&self.strbuf) { Ok(e) => e, Err(e) if e.is_eof() => return None, Err(e) => { return Some(Err(eyre!( "Invalid entry found in resh_history file: {}", e ))) } }; #[allow(clippy::cast_possible_truncation)] #[allow(clippy::cast_sign_loss)] let timestamp = { let secs = entry.realtime_before.floor() as i64; let nanosecs = (entry.realtime_before.fract() * 1_000_000_000_f64).round() as u32; Utc.timestamp(secs, nanosecs) }; #[allow(clippy::cast_possible_truncation)] #[allow(clippy::cast_sign_loss)] let duration = { let secs = entry.realtime_after.floor() as i64; let nanosecs = (entry.realtime_after.fract() * 1_000_000_000_f64).round() as u32; let difference = Utc.timestamp(secs, nanosecs) - timestamp; difference.num_nanoseconds().unwrap_or(0) }; Some(Ok(History { id: uuid_v4(), timestamp, duration, exit: entry.exit_code, command: entry.cmd_line, cwd: entry.pwd, session: uuid_v4(), hostname: entry.host, })) } fn size_hint(&self) -> (usize, Option) { (self.loc, Some(self.loc)) } }