diff --git a/README.md b/README.md index 423869d..84faf13 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ - Vim keybinding (most common ops) - Copy text from/to clipboard (works only on the prompt) - Multiple backends +- Automatically load the last saved chat into history
@@ -84,14 +85,12 @@ tenere -c ~/path/to/custom/config.toml Here are the available general settings: -- `archive_file_name`: the file name where the chat will be saved. By default it is set to `tenere.archive` - `llm`: the llm model name. Possible values are: - `chatgpt` - `llamacpp` - `ollama` ```toml -archive_file_name = "tenere.archive" llm = "chatgpt" ``` @@ -106,7 +105,6 @@ Here is an example with the default key bindings show_help = '?' show_history = 'h' new_chat = 'n' -save_chat = 's' ``` ℹ️ Note @@ -185,9 +183,7 @@ More infos about ollama api [here](https://github.com/ollama/ollama/blob/main/do These are the default key bindings regardless of the focused block. -`ctrl + n`: Start a new chat and save the previous one in history. - -`ctrl + s`: Save the current chat or chat history (history pop-up should be visible first) to `tenere.archive` file in the current directory. +`ctrl + n`: Start a new chat and save the previous one in history and save it to `tenere.archive-i` file in `data directory`. `Tab`: Switch the focus. diff --git a/src/config.rs b/src/config.rs index f3a608a..00cb77e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -7,9 +7,6 @@ use std::path::PathBuf; #[derive(Deserialize, Debug)] pub struct Config { - #[serde(default = "default_archive_file_name")] - pub archive_file_name: String, - #[serde(default)] pub key_bindings: KeyBindings, @@ -24,10 +21,6 @@ pub struct Config { pub ollama: Option, } -pub fn default_archive_file_name() -> String { - String::from("tenere.archive") -} - pub fn default_llm_backend() -> LLMBackend { LLMBackend::ChatGPT } @@ -91,9 +84,6 @@ pub struct KeyBindings { #[serde(default = "KeyBindings::default_new_chat")] pub new_chat: char, - #[serde(default = "KeyBindings::default_save_chat")] - pub save_chat: char, - #[serde(default = "KeyBindings::default_stop_stream")] pub stop_stream: char, } @@ -104,7 +94,6 @@ impl Default for KeyBindings { show_help: '?', show_history: 'h', new_chat: 'n', - save_chat: 's', stop_stream: 't', } } @@ -123,10 +112,6 @@ impl KeyBindings { 'n' } - fn default_save_chat() -> char { - 's' - } - fn default_stop_stream() -> char { 't' } diff --git a/src/handler.rs b/src/handler.rs index 0a2453f..91c864c 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -11,7 +11,6 @@ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use ratatui::text::Line; -use crate::notification::{Notification, NotificationLevel}; use std::sync::Arc; use tokio::sync::Mutex; @@ -111,6 +110,8 @@ pub async fn handle_key_events( .push(app.chat.formatted_chat.clone()); app.history.text.push(app.chat.plain_chat.clone()); + // after adding to history, save the chat in file + app.history.save(app.history.text.len() - 1, sender.clone()); app.chat = Chat::default(); @@ -123,40 +124,6 @@ pub async fn handle_key_events( app.chat.scroll = 0; } - // Save chat - KeyCode::Char(c) - if c == app.config.key_bindings.save_chat - && key_event.modifiers == KeyModifiers::CONTROL => - { - match app.focused_block { - FocusedBlock::History | FocusedBlock::Preview => { - app.history - .save(app.config.archive_file_name.as_str(), sender.clone()); - } - FocusedBlock::Chat | FocusedBlock::Prompt => { - match std::fs::write( - app.config.archive_file_name.clone(), - app.chat.plain_chat.join(""), - ) { - Ok(_) => { - let notif = Notification::new( - format!("Chat saved to `{}` file", app.config.archive_file_name), - NotificationLevel::Info, - ); - - sender.send(Event::Notification(notif)).unwrap(); - } - Err(e) => { - let notif = Notification::new(e.to_string(), NotificationLevel::Error); - - sender.send(Event::Notification(notif)).unwrap(); - } - } - } - _ => (), - } - } - // Switch the focus KeyCode::Tab => match app.focused_block { FocusedBlock::Chat => { diff --git a/src/help.rs b/src/help.rs index f53b195..537d8b5 100644 --- a/src/help.rs +++ b/src/help.rs @@ -24,10 +24,6 @@ impl Default for Help { Cell::from("ctrl + n").bold().yellow(), "Start new chat and save the previous one to the history", ), - ( - Cell::from("ctrl + s").bold().yellow(), - "Save the chat to file in the current directory", - ), (Cell::from("ctrl + h").bold().yellow(), "Show history"), ( Cell::from("ctrl + t").bold().yellow(), diff --git a/src/history.rs b/src/history.rs index 1e9180e..9510e5b 100644 --- a/src/history.rs +++ b/src/history.rs @@ -1,3 +1,6 @@ +use core::str; +use std::{fs, path::PathBuf}; + use tokio::sync::mpsc::UnboundedSender; use ratatui::{ @@ -81,17 +84,60 @@ impl History<'_> { self.state.select(Some(i)); } - pub fn save(&mut self, archive_file_name: &str, sender: UnboundedSender) { + // check if data directory for the application exists, else it will create it + pub fn check_data_directory_exists(&self, sender: UnboundedSender) { + if let Some(data_directory) = dirs::data_dir() { + let target_directory = data_directory.join("tenere"); + + if !target_directory.exists() { + if let Err(e) = fs::create_dir_all(target_directory) { + let notif = Notification::new(e.to_string(), NotificationLevel::Error); + sender.send(Event::Notification(notif)).unwrap(); + } + } + } + } + + // load chat in the history from data directory + pub fn load_history(&mut self, sender: UnboundedSender) { + let directory_path: PathBuf = dirs::data_dir().unwrap().join("tenere"); + + if let Ok(paths) = fs::read_dir(directory_path.clone()) { + // foreach archive file we add it to history + for path in paths { + if path.as_ref().unwrap().file_type().unwrap().is_file() { + self.load_chat_from_file(path.unwrap().path().to_str().unwrap()); + } + } + + let notif = Notification::new("History loaded".to_string(), NotificationLevel::Info); + + sender.send(Event::Notification(notif)).unwrap(); + } + } + + /// Add to history the archive file if exists + pub fn load_chat_from_file(&mut self, archive_file_name: &str) { + if let Ok(text) = std::fs::read_to_string(archive_file_name) { + // push full conversation in preview + self.preview.text.push(Text::from(text.clone())); + // get first line of the conversation + let first_line: String = text.lines().next().unwrap_or("").to_string(); + self.text.push(vec![first_line]); + } + } + + // call after adding new chat in history (Starting a new chat) + // with the index of the chat in history to save + pub fn save(&mut self, chat_index_in_history: usize, sender: UnboundedSender) { + let file_name = format!("tenere.archive-{}", chat_index_in_history); + let file_path: PathBuf = dirs::data_dir().unwrap().join("tenere").join(file_name); + if !self.text.is_empty() { - match std::fs::write( - archive_file_name, - self.text[self.state.selected().unwrap_or(0)].join(""), - ) { + match std::fs::write(file_path.clone(), self.text[chat_index_in_history].join("")) { Ok(_) => { - let notif = Notification::new( - format!("Chat saved to `{}` file", archive_file_name), - NotificationLevel::Info, - ); + let notif = + Notification::new("Chat saved".to_string(), NotificationLevel::Info); sender.send(Event::Notification(notif)).unwrap(); } diff --git a/src/main.rs b/src/main.rs index 680f452..96fe544 100644 --- a/src/main.rs +++ b/src/main.rs @@ -48,6 +48,13 @@ async fn main() -> AppResult<()> { let mut tui = Tui::new(terminal, events); tui.init()?; + // create data directory if not exists + app.history + .check_data_directory_exists(tui.events.sender.clone()); + + // load potential history data from archive files + app.history.load_history(tui.events.sender.clone()); + while app.running { tui.draw(&mut app)?; match tui.events.next().await? {