Skip to content

Commit 7e8e2a6

Browse files
authored
feat: append shell execute command to history file (#1026)
1 parent efdec34 commit 7e8e2a6

File tree

5 files changed

+88
-7
lines changed

5 files changed

+88
-7
lines changed

config.example.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ right_prompt:
8383
# ---- misc ----
8484
serve_addr: 127.0.0.1:8000 # Default serve listening address
8585
user_agent: null # Set User-Agent HTTP header, use `auto` for aichat/<current-version>
86+
append_command_to_history_file: true # Weather to append shell execute command to the history file
8687

8788
# ---- clients ----
8889
clients:

src/config/mod.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ pub struct Config {
137137

138138
pub serve_addr: Option<String>,
139139
pub user_agent: Option<String>,
140+
pub append_command_to_history_file: bool,
140141

141142
pub clients: Vec<ClientConfig>,
142143

@@ -206,6 +207,7 @@ impl Default for Config {
206207

207208
serve_addr: None,
208209
user_agent: None,
210+
append_command_to_history_file: true,
209211

210212
clients: vec![],
211213

@@ -1950,7 +1952,7 @@ impl Config {
19501952
if output.is_empty() || !self.save {
19511953
return Ok(());
19521954
}
1953-
let timestamp = now();
1955+
let now = now();
19541956
let summary = input.summary();
19551957
let raw_input = input.raw();
19561958
let scope = if self.agent.is_none() {
@@ -1983,7 +1985,7 @@ impl Config {
19831985
None => String::new(),
19841986
};
19851987
let output = format!(
1986-
"# CHAT: {summary} [{timestamp}]{scope}\n{raw_input}\n--------\n{tool_calls}{output}\n--------\n\n",
1988+
"# CHAT: {summary} [{now}]{scope}\n{raw_input}\n--------\n{tool_calls}{output}\n--------\n\n",
19871989
);
19881990
file.write_all(output.as_bytes())
19891991
.with_context(|| "Failed to save message")
@@ -2225,6 +2227,9 @@ impl Config {
22252227
if let Some(v) = read_env_value::<String>(&get_env_name("user_agent")) {
22262228
self.user_agent = v;
22272229
}
2230+
if let Some(Some(v)) = read_env_bool(&get_env_name("append_command_to_history_file")) {
2231+
self.append_command_to_history_file = v;
2232+
}
22282233
}
22292234

22302235
fn load_functions(&mut self) -> Result<()> {

src/main.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -259,9 +259,10 @@ async fn shell_execute(
259259
"e" => {
260260
debug!("{} {:?}", shell.cmd, &[&shell.arg, &eval_str]);
261261
let code = run_command(&shell.cmd, &[&shell.arg, &eval_str], None)?;
262-
if code != 0 {
263-
process::exit(code);
262+
if code == 0 && config.read().append_command_to_history_file {
263+
let _ = append_to_shell_history(&shell.name, &eval_str, code);
264264
}
265+
process::exit(code);
265266
}
266267
"r" => {
267268
let revision = Text::new("Enter your revision:").prompt()?;

src/utils/command.rs

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
use super::*;
22

3-
use std::{collections::HashMap, env, ffi::OsStr, path::Path, process::Command};
3+
use std::{
4+
collections::HashMap,
5+
env,
6+
ffi::OsStr,
7+
fs::OpenOptions,
8+
io::{self, Write},
9+
path::{Path, PathBuf},
10+
process::Command,
11+
};
412

513
use anyhow::{anyhow, bail, Context, Result};
14+
use dirs::home_dir;
615

716
lazy_static::lazy_static! {
817
pub static ref SHELL: Shell = detect_shell();
@@ -152,3 +161,65 @@ pub fn edit_file(editor: &str, path: &Path) -> Result<()> {
152161
child.wait()?;
153162
Ok(())
154163
}
164+
165+
pub fn append_to_shell_history(shell: &str, command: &str, exit_code: i32) -> io::Result<()> {
166+
if let Some(history_file) = get_history_file(shell) {
167+
let command = command.replace('\n', " ");
168+
let now = now_timestamp();
169+
let history_txt = if shell == "fish" {
170+
format!("- cmd: {command}\n when: {now}")
171+
} else if shell == "zsh" {
172+
format!(": {now}:{exit_code};{command}",)
173+
} else {
174+
command
175+
};
176+
let mut file = OpenOptions::new()
177+
.create(true)
178+
.append(true)
179+
.open(&history_file)?;
180+
writeln!(file, "{}", history_txt)?;
181+
}
182+
Ok(())
183+
}
184+
185+
fn get_history_file(shell: &str) -> Option<PathBuf> {
186+
match shell {
187+
"bash" | "sh" => Some(home_dir()?.join(".bash_history")),
188+
"zsh" => Some(home_dir()?.join(".zsh_history")),
189+
"nushell" => Some(dirs::config_dir()?.join("nushell").join("history.txt")),
190+
"fish" => Some(
191+
home_dir()?
192+
.join(".local")
193+
.join("share")
194+
.join("fish")
195+
.join("fish_history"),
196+
),
197+
"powershell" | "pwsh" => {
198+
#[cfg(not(windows))]
199+
{
200+
Some(
201+
home_dir()?
202+
.join(".local")
203+
.join("share")
204+
.join("powershell")
205+
.join("PSReadLine")
206+
.join("ConsoleHost_history.txt"),
207+
)
208+
}
209+
#[cfg(windows)]
210+
{
211+
Some(
212+
dirs::data_dir()?
213+
.join("Microsoft")
214+
.join("Windows")
215+
.join("PowerShell")
216+
.join("PSReadLine")
217+
.join("ConsoleHost_history.txt"),
218+
)
219+
}
220+
}
221+
"ksh" => Some(home_dir()?.join(".ksh_history")),
222+
"tcsh" => Some(home_dir()?.join(".history")),
223+
_ => None,
224+
}
225+
}

src/utils/mod.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,11 @@ lazy_static::lazy_static! {
3535
}
3636

3737
pub fn now() -> String {
38-
let now = chrono::Local::now();
39-
now.to_rfc3339_opts(chrono::SecondsFormat::Secs, false)
38+
chrono::Local::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, false)
39+
}
40+
41+
pub fn now_timestamp() -> i64 {
42+
chrono::Local::now().timestamp()
4043
}
4144

4245
pub fn get_env_name(key: &str) -> String {

0 commit comments

Comments
 (0)