diff --git a/config.example.yaml b/config.example.yaml index f46cddd1..e50b7ed8 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -83,6 +83,7 @@ right_prompt: # ---- misc ---- serve_addr: 127.0.0.1:8000 # Default serve listening address user_agent: null # Set User-Agent HTTP header, use `auto` for aichat/ +append_command_to_history_file: true # Weather to append shell execute command to the history file # ---- clients ---- clients: diff --git a/src/config/mod.rs b/src/config/mod.rs index 39c63a32..e0e1efa8 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -137,6 +137,7 @@ pub struct Config { pub serve_addr: Option, pub user_agent: Option, + pub append_command_to_history_file: bool, pub clients: Vec, @@ -206,6 +207,7 @@ impl Default for Config { serve_addr: None, user_agent: None, + append_command_to_history_file: true, clients: vec![], @@ -1950,7 +1952,7 @@ impl Config { if output.is_empty() || !self.save { return Ok(()); } - let timestamp = now(); + let now = now(); let summary = input.summary(); let raw_input = input.raw(); let scope = if self.agent.is_none() { @@ -1983,7 +1985,7 @@ impl Config { None => String::new(), }; let output = format!( - "# CHAT: {summary} [{timestamp}]{scope}\n{raw_input}\n--------\n{tool_calls}{output}\n--------\n\n", + "# CHAT: {summary} [{now}]{scope}\n{raw_input}\n--------\n{tool_calls}{output}\n--------\n\n", ); file.write_all(output.as_bytes()) .with_context(|| "Failed to save message") @@ -2225,6 +2227,9 @@ impl Config { if let Some(v) = read_env_value::(&get_env_name("user_agent")) { self.user_agent = v; } + if let Some(Some(v)) = read_env_bool(&get_env_name("append_command_to_history_file")) { + self.append_command_to_history_file = v; + } } fn load_functions(&mut self) -> Result<()> { diff --git a/src/main.rs b/src/main.rs index d8f66f86..2cefaf31 100644 --- a/src/main.rs +++ b/src/main.rs @@ -259,9 +259,10 @@ async fn shell_execute( "e" => { debug!("{} {:?}", shell.cmd, &[&shell.arg, &eval_str]); let code = run_command(&shell.cmd, &[&shell.arg, &eval_str], None)?; - if code != 0 { - process::exit(code); + if code == 0 && config.read().append_command_to_history_file { + let _ = append_to_shell_history(&shell.name, &eval_str, code); } + process::exit(code); } "r" => { let revision = Text::new("Enter your revision:").prompt()?; diff --git a/src/utils/command.rs b/src/utils/command.rs index 9ec929c1..32e470d3 100644 --- a/src/utils/command.rs +++ b/src/utils/command.rs @@ -1,8 +1,17 @@ use super::*; -use std::{collections::HashMap, env, ffi::OsStr, path::Path, process::Command}; +use std::{ + collections::HashMap, + env, + ffi::OsStr, + fs::OpenOptions, + io::{self, Write}, + path::{Path, PathBuf}, + process::Command, +}; use anyhow::{anyhow, bail, Context, Result}; +use dirs::home_dir; lazy_static::lazy_static! { pub static ref SHELL: Shell = detect_shell(); @@ -152,3 +161,65 @@ pub fn edit_file(editor: &str, path: &Path) -> Result<()> { child.wait()?; Ok(()) } + +pub fn append_to_shell_history(shell: &str, command: &str, exit_code: i32) -> io::Result<()> { + if let Some(history_file) = get_history_file(shell) { + let command = command.replace('\n', " "); + let now = now_timestamp(); + let history_txt = if shell == "fish" { + format!("- cmd: {command}\n when: {now}") + } else if shell == "zsh" { + format!(": {now}:{exit_code};{command}",) + } else { + command + }; + let mut file = OpenOptions::new() + .create(true) + .append(true) + .open(&history_file)?; + writeln!(file, "{}", history_txt)?; + } + Ok(()) +} + +fn get_history_file(shell: &str) -> Option { + match shell { + "bash" | "sh" => Some(home_dir()?.join(".bash_history")), + "zsh" => Some(home_dir()?.join(".zsh_history")), + "nushell" => Some(dirs::config_dir()?.join("nushell").join("history.txt")), + "fish" => Some( + home_dir()? + .join(".local") + .join("share") + .join("fish") + .join("fish_history"), + ), + "powershell" | "pwsh" => { + #[cfg(not(windows))] + { + Some( + home_dir()? + .join(".local") + .join("share") + .join("powershell") + .join("PSReadLine") + .join("ConsoleHost_history.txt"), + ) + } + #[cfg(windows)] + { + Some( + dirs::data_dir()? + .join("Microsoft") + .join("Windows") + .join("PowerShell") + .join("PSReadLine") + .join("ConsoleHost_history.txt"), + ) + } + } + "ksh" => Some(home_dir()?.join(".ksh_history")), + "tcsh" => Some(home_dir()?.join(".history")), + _ => None, + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 873c5c88..52715718 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -35,8 +35,11 @@ lazy_static::lazy_static! { } pub fn now() -> String { - let now = chrono::Local::now(); - now.to_rfc3339_opts(chrono::SecondsFormat::Secs, false) + chrono::Local::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, false) +} + +pub fn now_timestamp() -> i64 { + chrono::Local::now().timestamp() } pub fn get_env_name(key: &str) -> String {