From 9db7571542c9ade772033fcfbef6663452177fd7 Mon Sep 17 00:00:00 2001 From: Skylar Saveland Date: Sat, 23 Nov 2024 18:28:50 -0800 Subject: [PATCH] feat(cli): use editor for workon and chat on rs CLI (#63) --- docker-compose.yaml | 1 + rs/core/corpora_cli/src/commands/chat.rs | 41 +++++-------- rs/core/corpora_cli/src/commands/workon.rs | 63 ++++++------------- rs/core/corpora_cli/src/context/mod.rs | 71 +++++++++++++++++++++- 4 files changed, 103 insertions(+), 73 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 46c305c..7fa0250 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -78,6 +78,7 @@ services: CORPORA_CLIENT_SECRET: "${CORPORA_CLIENT_SECRET}" GITHUB_TOKEN: "${GITHUB_TOKEN}" # OPENAI_API_KEY: "${OPENAI_API_KEY}" + # EDITOR: "code" command: sleep infinity working_dir: /workspace networks: diff --git a/rs/core/corpora_cli/src/commands/chat.rs b/rs/core/corpora_cli/src/commands/chat.rs index 118b85a..e586c98 100644 --- a/rs/core/corpora_cli/src/commands/chat.rs +++ b/rs/core/corpora_cli/src/commands/chat.rs @@ -1,23 +1,17 @@ use crate::context::Context; use corpora_client::models::{CorpusChatSchema, MessageSchema}; -use dialoguer::{theme::ColorfulTheme, Input}; use std::fs; use termimad::MadSkin; -/// The `workon` command operation +/// Executes the chat command operation pub fn run(ctx: &Context) { - let mut messages: Vec = Vec::new(); + let mut messages = Vec::new(); - // REPL loop loop { - // TODO: multiline input - let user_input: String = Input::with_theme(&ColorfulTheme::default()) - .with_prompt("Can I help you?") - .allow_empty(false) - .interact_text() - .unwrap(); + let user_input = ctx + .get_user_input_via_editor("Put your prompt here and close") + .expect("Failed to obtain user input"); - // Add the user's input as a new message messages.push(MessageSchema { role: "user".to_string(), text: user_input.trim().to_string(), @@ -30,11 +24,7 @@ pub fn run(ctx: &Context) { let structure = fs::read_to_string(root_path.join(".corpora/STRUCTURE.md")).unwrap_or_default(); - // println!("Voice: {}", voice); - // println!("Purpose: {}", purpose); - // println!("Structure: {}", structure); - - let response = match corpora_client::apis::corpus_api::chat( + match corpora_client::apis::corpus_api::chat( &ctx.api_config, CorpusChatSchema { messages: messages.clone(), @@ -49,21 +39,18 @@ pub fn run(ctx: &Context) { directions: None, }, ) { - Ok(response) => response, + Ok(response) => { + let skin = MadSkin::default(); + skin.print_text(&response); + messages.push(MessageSchema { + role: "assistant".to_string(), + text: response.clone(), + }); + } Err(err) => { ctx.error(&format!("Failed to get response: {:?}", err)); continue; } }; - - // ctx.print(&response, dialoguer::console::Style::new().dim()); - let skin = MadSkin::default(); - skin.print_text(&response); - - // println!("{}", relative_path.display()); - messages.push(MessageSchema { - role: "assistant".to_string(), - text: response.clone(), - }); } } diff --git a/rs/core/corpora_cli/src/commands/workon.rs b/rs/core/corpora_cli/src/commands/workon.rs index 2eb8898..da0172e 100644 --- a/rs/core/corpora_cli/src/commands/workon.rs +++ b/rs/core/corpora_cli/src/commands/workon.rs @@ -1,7 +1,7 @@ use crate::context::Context; use clap::Args; use corpora_client::models::{CorpusFileChatSchema, MessageSchema}; -use dialoguer::{theme::ColorfulTheme, Confirm, Input}; +use dialoguer::{theme::ColorfulTheme, Confirm}; use std::fs::{self, File}; use std::io::Write; use std::path::Path; @@ -14,32 +14,20 @@ pub struct WorkonArgs { /// The `workon` command operation pub fn run(ctx: &Context, args: WorkonArgs) { - // get path as a path from the string let path = Path::new(&args.path); - // get cwd - let cwd = std::env::current_dir().unwrap(); - ctx.print( - &format!("Current working directory: {}", cwd.display()), - dialoguer::console::Style::new().dim(), - ); - // add cwd and args.path - let absolute_path = Path::join(&cwd, path); - // rm the config.root_path to get the relative path + let cwd = std::env::current_dir().expect("Failed to get current directory"); + ctx.dim(&format!("Current working directory: {}", cwd.display())); + let absolute_path = cwd.join(path); let relative_path = absolute_path .strip_prefix(&ctx.corpora_config.root_path) - .unwrap(); - // ctx.success(&format!("Working on file: {}", relative_path.display())); - ctx.print( - &format!("Working on file: {}", relative_path.display()), - dialoguer::console::Style::new().dim().green(), - ); + .expect("Failed to get relative path"); + + ctx.magenta(&format!("Working on file: {}", relative_path.display())); let ext = relative_path .extension() - .unwrap_or_default() - .to_str() + .and_then(|s| s.to_str()) .unwrap_or(""); - // Show current file content, load the content from the let current_file_content = match fs::read_to_string(path) { Ok(content) => content, Err(_) => { @@ -47,15 +35,10 @@ pub fn run(ctx: &Context, args: WorkonArgs) { String::new() } }; - ctx.success("Current file content:"); - ctx.print( - ¤t_file_content, - dialoguer::console::Style::new().dim(), - ); - - let mut messages: Vec = Vec::new(); + ctx.dim(¤t_file_content); + let mut messages: Vec = vec![]; if !current_file_content.is_empty() { messages.push(MessageSchema { role: "user".to_string(), @@ -67,25 +50,19 @@ pub fn run(ctx: &Context, args: WorkonArgs) { }); } - // REPL loop loop { - let user_input: String = Input::with_theme(&ColorfulTheme::default()) - .with_prompt(if messages.is_empty() { - "What to do?" - } else { - "How to revise?" - }) - .allow_empty(false) - .interact_text() - .unwrap(); + let user_input = ctx + .get_user_input_via_editor(&format!( + "Put your prompt here for {}, save and close", + path.display() + )) + .expect("Failed to get user input"); - // Add the user's input as a new message messages.push(MessageSchema { role: "user".to_string(), text: user_input.trim().to_string(), }); - // Generate revision ctx.success("Generating revision..."); let root_path = &ctx.corpora_config.root_path; @@ -120,12 +97,8 @@ pub fn run(ctx: &Context, args: WorkonArgs) { } }; - ctx.print(&revision, dialoguer::console::Style::new().dim()); - // println!("{}", relative_path.display()); - ctx.print( - &format!("Revision for `{}`:", relative_path.display()), - dialoguer::console::Style::new().dim().magenta().on_green(), - ); + ctx.dim(&revision); + ctx.highlight(&format!("^^Revision for `{}`^^", relative_path.display())); messages.push(MessageSchema { role: "assistant".to_string(), text: revision.clone(), diff --git a/rs/core/corpora_cli/src/context/mod.rs b/rs/core/corpora_cli/src/context/mod.rs index 6e6ebcd..8bbbfd8 100644 --- a/rs/core/corpora_cli/src/context/mod.rs +++ b/rs/core/corpora_cli/src/context/mod.rs @@ -2,9 +2,14 @@ pub mod auth; pub mod collector; pub mod config; +use std::sync::Arc; + use console::{Style, Term}; +// use dialoguer::Editor; +use std::process::Command; +use tempfile::NamedTempFile; + use indicatif::{ProgressBar, ProgressStyle}; -use std::sync::Arc; use corpora_client::apis::configuration::Configuration; @@ -151,4 +156,68 @@ impl Context { let dim_style = Style::new().dim(); self.print(message, dim_style); } + + /// Print a magenta message + /// Magenta messages are used for highlighting information + /// # Arguments + /// * `message` - The message to print. + pub fn magenta(&self, message: &str) { + let magenta_style = Style::new().magenta(); + self.print(message, magenta_style); + } + + /// Print a highlighted message + /// Highlighted messages are used for important information + /// # Arguments + /// * `message` - The message to print. + pub fn highlight(&self, message: &str) { + let style = Style::new().bold().green().on_magenta(); + self.print(message, style); + } + + /// Get user input via an editor + /// Opens the user's default editor with the provided content + /// # Arguments + /// * `initial_content` - The initial content to display in the editor. + /// # Returns + /// The edited content as a `String`. + pub fn get_user_input_via_editor(&self, initial_content: &str) -> Result { + get_user_input_via_editor(initial_content) + } +} + +pub fn get_user_input_via_editor(initial_content: &str) -> Result { + // Create a temporary file + let temp_file = + NamedTempFile::new().map_err(|e| format!("Failed to create temp file: {}", e))?; + + // Write the initial content to the temporary file + std::fs::write(temp_file.path(), initial_content) + .map_err(|e| format!("Failed to write to temp file: {}", e))?; + + // Get the editor from $EDITOR or default to 'vim' + let editor = std::env::var("EDITOR").unwrap_or_else(|_| "vim".to_string()); + let mut command = Command::new(&editor); + + // Add flags for non-blocking editors + if editor.contains("code") || editor.contains("subl") { + command.arg("--wait"); + } else if editor.contains("gedit") { + command.arg("--standalone"); + } + + // Open the file in the editor and wait for it to close + let status = command + .arg(temp_file.path()) + .status() + .map_err(|e| format!("Failed to launch editor '{}': {}", editor, e))?; + + // Check if the editor exited successfully + if !status.success() { + return Err(format!("Editor '{}' exited with a non-zero status", editor)); + } + + // Read the edited content back from the file + std::fs::read_to_string(temp_file.path()) + .map_err(|e| format!("Failed to read edited file: {}", e)) }