diff --git a/src/config/mod.rs b/src/config/mod.rs index efbf984..6cbbc05 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -713,17 +713,42 @@ impl Config { Ok(()) } - pub fn save_session(&mut self, name: &str) -> Result<()> { - let sessions_dir = self.sessions_dir()?; + pub fn save_session(&mut self, name: Option<&str>) -> Result<()> { + let name = match &self.session { + Some(session) => match name { + Some(v) => v.to_string(), + None => session.name().to_string(), + }, + None => bail!("No session"), + }; + let session_path = self.session_file(&name)?; if let Some(session) = self.session.as_mut() { - if !name.is_empty() { - session.set_name(name); - } - session.save(&sessions_dir)?; + session.save(&session_path)?; } Ok(()) } + pub fn edit_session(&mut self) -> Result<()> { + let name = match &self.session { + Some(session) => session.name().to_string(), + None => bail!("No session"), + }; + let editor = match self.buffer_editor() { + Some(editor) => editor, + None => bail!("No editor, please set $EDITOR/$VISUAL."), + }; + let session_path = self.session_file(&name)?; + self.save_session(Some(&name))?; + edit_file(&editor, &session_path).with_context(|| { + format!( + "Failed to edit '{}' with '{editor}'", + session_path.display() + ) + })?; + self.session = Some(Session::load(self, &name, &session_path)?); + Ok(()) + } + pub fn clear_session_messages(&mut self) -> Result<()> { if let Some(session) = self.session.as_mut() { session.clear_messages(); diff --git a/src/config/session.rs b/src/config/session.rs index 7400f30..b95fcc5 100644 --- a/src/config/session.rs +++ b/src/config/session.rs @@ -240,10 +240,6 @@ impl Session { (tokens, percent) } - pub fn set_name(&mut self, name: &str) { - self.name = name.to_string(); - } - pub fn set_role(&mut self, role: Role) { self.model_id = role.model().id(); self.temperature = role.temperature(); @@ -290,7 +286,7 @@ impl Session { self.dirty = true; } - pub fn exit(&mut self, sessions_dir: &Path, is_repl: bool) -> Result<()> { + pub fn exit(&mut self, session_dir: &Path, is_repl: bool) -> Result<()> { let save_session = self.save_session(); if self.dirty && save_session != Some(false) { if save_session.is_none() { @@ -315,25 +311,26 @@ impl Session { .prompt()?; } } - self.save(sessions_dir)?; + let session_path = session_dir.join(format!("{}.yaml", self.name())); + self.save(&session_path)?; } Ok(()) } - pub fn save(&mut self, sessions_dir: &Path) -> Result<()> { - let mut session_path = sessions_dir.to_path_buf(); - session_path.push(format!("{}.yaml", self.name())); - if !sessions_dir.exists() { - create_dir_all(sessions_dir).with_context(|| { - format!("Failed to create session_dir '{}'", sessions_dir.display()) - })?; + pub fn save(&mut self, session_path: &Path) -> Result<()> { + if let Some(sessions_dir) = session_path.parent() { + if !sessions_dir.exists() { + create_dir_all(sessions_dir).with_context(|| { + format!("Failed to create session_dir '{}'", sessions_dir.display()) + })?; + } } self.path = Some(session_path.display().to_string()); let content = serde_yaml::to_string(&self) .with_context(|| format!("Failed to serde session {}", self.name))?; - fs::write(&session_path, content).with_context(|| { + fs::write(session_path, content).with_context(|| { format!( "Failed to write session {} to {}", self.name, diff --git a/src/repl/mod.rs b/src/repl/mod.rs index 48d8224..5b37d36 100644 --- a/src/repl/mod.rs +++ b/src/repl/mod.rs @@ -33,7 +33,7 @@ lazy_static! { const MENU_NAME: &str = "completion_menu"; lazy_static! { - static ref REPL_COMMANDS: [ReplCommand; 23] = [ + static ref REPL_COMMANDS: [ReplCommand; 24] = [ ReplCommand::new(".help", "Show this help message", AssertState::pass()), ReplCommand::new(".info", "View system info", AssertState::pass()), ReplCommand::new(".model", "Change the current LLM", AssertState::pass()), @@ -72,6 +72,11 @@ lazy_static! { "Save the chat to file", AssertState::True(StateFlags::SESSION_EMPTY | StateFlags::SESSION) ), + ReplCommand::new( + ".edit session", + "Edit the current session", + AssertState::True(StateFlags::SESSION_EMPTY | StateFlags::SESSION) + ), ReplCommand::new( ".clear messages", "Erase messages in the current session", @@ -279,8 +284,8 @@ Tips: use to autocomplete conversation starter text. }, ".save" => { match args.map(|v| match v.split_once(' ') { - Some((subcmd, args)) => (subcmd, args.trim()), - None => (v, ""), + Some((subcmd, args)) => (subcmd, Some(args.trim())), + None => (v, None), }) { Some(("session", name)) => { self.config.write().save_session(name)?; @@ -290,6 +295,19 @@ Tips: use to autocomplete conversation starter text. } } } + ".edit" => { + match args.map(|v| match v.split_once(' ') { + Some((subcmd, args)) => (subcmd, Some(args.trim())), + None => (v, None), + }) { + Some(("session", _)) => { + self.config.write().edit_session()?; + } + _ => { + println!(r#"Usage: .edit session"#) + } + } + } ".set" => match args { Some(args) => { self.config.write().update(args)?; diff --git a/src/utils/command.rs b/src/utils/command.rs index 873e40b..557649a 100644 --- a/src/utils/command.rs +++ b/src/utils/command.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, env, ffi::OsStr, process::Command}; +use std::{collections::HashMap, env, ffi::OsStr, path::Path, process::Command}; use anyhow::{Context, Result}; @@ -90,3 +90,9 @@ pub fn run_command_with_output>( let stderr = std::str::from_utf8(&output.stderr).context("Invalid UTF-8 in stderr")?; Ok((status.success(), stdout.to_string(), stderr.to_string())) } + +pub fn edit_file(editor: &str, path: &Path) -> Result<()> { + let mut child = Command::new(editor).arg(path).spawn()?; + child.wait()?; + Ok(()) +}