summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/cli.rs51
-rw-r--r--src/client/message.rs6
-rw-r--r--src/client/model.rs26
-rw-r--r--src/config/agent.rs2
-rw-r--r--src/config/input.rs6
-rw-r--r--src/config/mod.rs310
-rw-r--r--src/config/session.rs3
-rw-r--r--src/main.rs41
-rw-r--r--src/repl/mod.rs671
-rw-r--r--src/utils/mod.rs25
10 files changed, 685 insertions, 456 deletions
diff --git a/src/cli.rs b/src/cli.rs
index 73fede2..de88776 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -1,4 +1,7 @@
+use anyhow::{Context, Result};
use clap::Parser;
+use is_terminal::IsTerminal;
+use std::io::{stdin, Read};
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
@@ -33,6 +36,9 @@ pub struct Cli {
/// Rebuild the RAG to sync document changes
#[clap(long)]
pub rebuild_rag: bool,
+ /// Execute a macro
+ #[clap(long = "macro", value_name = "MACRO")]
+ pub macro_name: Option<String>,
/// Serve the LLM API and WebAPP
#[clap(long, value_name = "ADDRESS")]
pub serve: Option<Option<String>>,
@@ -69,17 +75,52 @@ pub struct Cli {
/// List all RAGs
#[clap(long)]
pub list_rags: bool,
+ /// List all macros
+ #[clap(long)]
+ pub list_macros: bool,
/// Input text
#[clap(trailing_var_arg = true)]
text: Vec<String>,
}
impl Cli {
- pub fn text(&self) -> Option<String> {
- let text = self.text.to_vec().join(" ");
- if text.is_empty() {
- return None;
+ pub fn text(&self) -> Result<Option<String>> {
+ let mut stdin_text = String::new();
+ if !stdin().is_terminal() {
+ let _ = stdin()
+ .read_to_string(&mut stdin_text)
+ .context("Invalid stdin pipe")?;
+ };
+ match self.text.is_empty() {
+ true => {
+ if stdin_text.is_empty() {
+ Ok(None)
+ } else {
+ Ok(Some(stdin_text))
+ }
+ }
+ false => {
+ if self.macro_name.is_some() {
+ let text = self
+ .text
+ .iter()
+ .map(|v| shell_words::quote(v))
+ .collect::<Vec<_>>()
+ .join(" ");
+ if stdin_text.is_empty() {
+ Ok(Some(text))
+ } else {
+ Ok(Some(format!("{} -- {}", text, stdin_text)))
+ }
+ } else {
+ let text = self.text.join(" ");
+ if stdin_text.is_empty() {
+ Ok(Some(text))
+ } else {
+ Ok(Some(format!("{}\n{}", text, stdin_text)))
+ }
+ }
+ }
}
- Some(text)
}
}
diff --git a/src/client/message.rs b/src/client/message.rs
index f6b7f6d..2cfc183 100644
--- a/src/client/message.rs
+++ b/src/client/message.rs
@@ -1,4 +1,4 @@
-use crate::{function::ToolResult, utils::dimmed_text};
+use crate::{function::ToolResult, multiline_text, utils::dimmed_text};
use serde::{Deserialize, Serialize};
@@ -83,7 +83,7 @@ impl MessageContent {
agent_info: &Option<(String, Vec<String>)>,
) -> String {
match self {
- MessageContent::Text(text) => text.to_string(),
+ MessageContent::Text(text) => multiline_text(text),
MessageContent::Array(list) => {
let (mut concated_text, mut files) = (String::new(), vec![]);
for item in list {
@@ -97,7 +97,7 @@ impl MessageContent {
}
}
if !concated_text.is_empty() {
- concated_text = format!(" -- {concated_text}")
+ concated_text = format!(" -- {}", multiline_text(&concated_text))
}
format!(".file {}{}", files.join(" "), concated_text)
}
diff --git a/src/client/model.rs b/src/client/model.rs
index 47f6598..60320cb 100644
--- a/src/client/model.rs
+++ b/src/client/model.rs
@@ -5,7 +5,7 @@ use super::{
};
use crate::config::Config;
-use crate::utils::{estimate_token_length, format_option_value};
+use crate::utils::estimate_token_length;
use anyhow::{bail, Result};
use serde::{Deserialize, Serialize};
@@ -133,10 +133,10 @@ impl Model {
supports_function_calling,
..
} = &self.data;
- let max_input_tokens = format_option_value(max_input_tokens);
- let max_output_tokens = format_option_value(max_output_tokens);
- let input_price = format_option_value(input_price);
- let output_price = format_option_value(output_price);
+ let max_input_tokens = stringify_option_value(max_input_tokens);
+ let max_output_tokens = stringify_option_value(max_output_tokens);
+ let input_price = stringify_option_value(input_price);
+ let output_price = stringify_option_value(output_price);
let mut capabilities = vec![];
if *supports_vision {
capabilities.push('👁');
@@ -161,9 +161,9 @@ impl Model {
max_batch_size,
..
} = &self.data;
- let max_tokens = format_option_value(max_tokens_per_chunk);
- let max_batch = format_option_value(max_batch_size);
- let price = format_option_value(input_price);
+ let max_tokens = stringify_option_value(max_tokens_per_chunk);
+ let max_batch = stringify_option_value(max_batch_size);
+ let price = stringify_option_value(input_price);
format!("max-tokens:{max_tokens};max-batch:{max_batch};price:{price}")
}
ModelType::Reranker => String::new(),
@@ -366,3 +366,13 @@ impl ModelType {
}
}
}
+
+fn stringify_option_value<T>(value: &Option<T>) -> String
+where
+ T: std::fmt::Display,
+{
+ match value {
+ Some(value) => value.to_string(),
+ None => "-".to_string(),
+ }
+}
diff --git a/src/config/agent.rs b/src/config/agent.rs
index 731dd91..533e7a9 100644
--- a/src/config/agent.rs
+++ b/src/config/agent.rs
@@ -68,7 +68,7 @@ impl Agent {
let rag = if rag_path.exists() {
Some(Arc::new(Rag::load(config, DEFAULT_AGENT_NAME, &rag_path)?))
- } else if !definition.documents.is_empty() && !config.read().cli_info_flag {
+ } else if !definition.documents.is_empty() && !config.read().info_flag {
let mut ans = false;
if *IS_STDOUT_TERMINAL {
ans = Confirm::new("The agent has the documents, init RAG?")
diff --git a/src/config/input.rs b/src/config/input.rs
index 70a01cf..c21bc62 100644
--- a/src/config/input.rs
+++ b/src/config/input.rs
@@ -27,8 +27,8 @@ pub struct Input {
medias: Vec<String>,
data_urls: HashMap<String, String>,
tool_calls: Option<MessageContentToolCalls>,
- rag_name: Option<String>,
role: Role,
+ rag_name: Option<String>,
with_session: bool,
with_agent: bool,
}
@@ -47,8 +47,8 @@ impl Input {
medias: Default::default(),
data_urls: Default::default(),
tool_calls: None,
- rag_name: None,
role,
+ rag_name: None,
with_session,
with_agent,
}
@@ -128,8 +128,8 @@ impl Input {
medias,
data_urls,
tool_calls: Default::default(),
- rag_name: None,
role,
+ rag_name: None,
with_session,
with_agent,
})
diff --git a/src/config/mod.rs b/src/config/mod.rs
index 975c8ea..7e2f275 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -17,6 +17,7 @@ use crate::client::{
use crate::function::{FunctionDeclaration, Functions, ToolResult};
use crate::rag::Rag;
use crate::render::{MarkdownRender, RenderOptions};
+use crate::repl::{run_repl_command, split_params_text};
use crate::utils::*;
use anyhow::{anyhow, bail, Context, Result};
@@ -49,6 +50,7 @@ const LIGHT_THEME: &[u8] = include_bytes!("../../assets/monokai-extended-light.t
const CONFIG_FILE_NAME: &str = "config.yaml";
const ROLES_DIR_NAME: &str = "roles";
+const MACROS_DIR_NAME: &str = "macros";
const ENV_FILE_NAME: &str = ".env";
const MESSAGES_FILE_NAME: &str = "messages.md";
const SESSIONS_DIR_NAME: &str = "sessions";
@@ -142,13 +144,12 @@ pub struct Config {
pub clients: Vec<ClientConfig>,
#[serde(skip)]
- pub role: Option<Role>,
- #[serde(skip)]
- pub session: Option<Session>,
+ pub macro_flag: bool,
#[serde(skip)]
- pub rag: Option<Arc<Rag>>,
+ pub info_flag: bool,
#[serde(skip)]
- pub agent: Option<Agent>,
+ pub cli_agent_variables: Option<AgentVariables>,
+
#[serde(skip)]
pub model: Model,
#[serde(skip)]
@@ -159,9 +160,13 @@ pub struct Config {
pub last_message: Option<LastMessage>,
#[serde(skip)]
- pub cli_info_flag: bool,
+ pub role: Option<Role>,
#[serde(skip)]
- pub cli_agent_variables: Option<AgentVariables>,
+ pub session: Option<Session>,
+ #[serde(skip)]
+ pub rag: Option<Arc<Rag>>,
+ #[serde(skip)]
+ pub agent: Option<Agent>,
}
impl Default for Config {
@@ -212,17 +217,19 @@ impl Default for Config {
clients: vec![],
- role: None,
- session: None,
- rag: None,
- agent: None,
+ macro_flag: false,
+ info_flag: false,
+ cli_agent_variables: None,
+
model: Default::default(),
functions: Default::default(),
working_mode: WorkingMode::Cmd,
last_message: None,
- cli_info_flag: false,
- cli_agent_variables: None,
+ role: None,
+ session: None,
+ rag: None,
+ agent: None,
}
}
}
@@ -296,6 +303,17 @@ impl Config {
Self::roles_dir().join(format!("{name}.md"))
}
+ pub fn macros_dir() -> PathBuf {
+ match env::var(get_env_name("macros_dir")) {
+ Ok(value) => PathBuf::from(value),
+ Err(_) => Self::local_path(MACROS_DIR_NAME),
+ }
+ }
+
+ pub fn macro_file(name: &str) -> PathBuf {
+ Self::macros_dir().join(format!("{name}.yaml"))
+ }
+
pub fn env_file() -> PathBuf {
match env::var(get_env_name("env_file")) {
Ok(value) => PathBuf::from(value),
@@ -537,23 +555,16 @@ impl Config {
let role = self.extract_role();
let mut items = vec![
("model", role.model().id()),
+ ("temperature", format_option_value(&role.temperature())),
+ ("top_p", format_option_value(&role.top_p())),
+ ("use_tools", format_option_value(&role.use_tools())),
(
"max_output_tokens",
self.model
.max_tokens_param()
.map(|v| format!("{v} (current model)"))
- .unwrap_or_else(|| "-".into()),
+ .unwrap_or_else(|| "null".into()),
),
- ("temperature", format_option_value(&role.temperature())),
- ("top_p", format_option_value(&role.top_p())),
- ("dry_run", self.dry_run.to_string()),
- ("stream", self.stream.to_string()),
- ("save", self.save.to_string()),
- ("keybindings", self.keybindings.clone()),
- ("wrap", wrap),
- ("wrap_code", self.wrap_code.to_string()),
- ("function_calling", self.function_calling.to_string()),
- ("use_tools", format_option_value(&role.use_tools())),
("save_session", format_option_value(&self.save_session)),
("compress_threshold", self.compress_threshold.to_string()),
(
@@ -561,6 +572,13 @@ impl Config {
format_option_value(&rag_reranker_model),
),
("rag_top_k", rag_top_k.to_string()),
+ ("dry_run", self.dry_run.to_string()),
+ ("function_calling", self.function_calling.to_string()),
+ ("stream", self.stream.to_string()),
+ ("save", self.save.to_string()),
+ ("keybindings", self.keybindings.clone()),
+ ("wrap", wrap),
+ ("wrap_code", self.wrap_code.to_string()),
("highlight", self.highlight.to_string()),
("light_theme", self.light_theme.to_string()),
("config_file", display_path(&Self::config_file())),
@@ -568,6 +586,7 @@ impl Config {
("roles_dir", display_path(&Self::roles_dir())),
("sessions_dir", display_path(&self.sessions_dir())),
("rags_dir", display_path(&Self::rags_dir())),
+ ("macros_dir", display_path(&Self::macros_dir())),
("functions_dir", display_path(&Self::functions_dir())),
("messages_file", display_path(&self.messages_file())),
];
@@ -590,10 +609,6 @@ impl Config {
let key = parts[0];
let value = parts[1];
match key {
- "max_output_tokens" => {
- let value = parse_value(value)?;
- config.write().set_max_output_tokens(value);
- }
"temperature" => {
let value = parse_value(value)?;
config.write().set_temperature(value);
@@ -602,29 +617,14 @@ impl Config {
let value = parse_value(value)?;
config.write().set_top_p(value);
}
- "dry_run" => {
- let value = value.parse().with_context(|| "Invalid value")?;
- config.write().dry_run = value;
- }
- "stream" => {
- let value = value.parse().with_context(|| "Invalid value")?;
- config.write().stream = value;
- }
- "save" => {
- let value = value.parse().with_context(|| "Invalid value")?;
- config.write().save = value;
- }
- "function_calling" => {
- let value = value.parse().with_context(|| "Invalid value")?;
- if value && config.write().functions.is_empty() {
- bail!("Function calling cannot be enabled because no functions are installed.")
- }
- config.write().function_calling = value;
- }
"use_tools" => {
let value = parse_value(value)?;
config.write().set_use_tools(value);
}
+ "max_output_tokens" => {
+ let value = parse_value(value)?;
+ config.write().set_max_output_tokens(value);
+ }
"save_session" => {
let value = parse_value(value)?;
config.write().set_save_session(value);
@@ -641,6 +641,25 @@ impl Config {
let value = value.parse().with_context(|| "Invalid value")?;
Self::set_rag_top_k(config, value)?;
}
+ "dry_run" => {
+ let value = value.parse().with_context(|| "Invalid value")?;
+ config.write().dry_run = value;
+ }
+ "function_calling" => {
+ let value = value.parse().with_context(|| "Invalid value")?;
+ if value && config.write().functions.is_empty() {
+ bail!("Function calling cannot be enabled because no functions are installed.")
+ }
+ config.write().function_calling = value;
+ }
+ "stream" => {
+ let value = value.parse().with_context(|| "Invalid value")?;
+ config.write().stream = value;
+ }
+ "save" => {
+ let value = value.parse().with_context(|| "Invalid value")?;
+ config.write().save = value;
+ }
"highlight" => {
let value = value.parse().with_context(|| "Invalid value")?;
config.write().highlight = value;
@@ -655,6 +674,7 @@ impl Config {
"role" => (Self::roles_dir(), Some(".md")),
"session" => (config.read().sessions_dir(), Some(".yaml")),
"rag" => (Self::rags_dir(), Some(".yaml")),
+ "macro" => (Self::macros_dir(), Some(".yaml")),
"agent-data" => (Self::agents_data_dir(), None),
_ => bail!("Unknown kind '{kind}'"),
};
@@ -887,31 +907,38 @@ impl Config {
}
pub fn new_role(&mut self, name: &str) -> Result<()> {
+ if self.macro_flag {
+ bail!("No role");
+ }
let ans = Confirm::new("Create a new role?")
.with_default(true)
.prompt()?;
if ans {
self.upsert_role(name)?;
+ } else {
+ bail!("No role");
}
Ok(())
}
pub fn edit_role(&mut self) -> Result<()> {
+ let role_name;
if let Some(session) = self.session.as_ref() {
if let Some(name) = session.role_name().map(|v| v.to_string()) {
if session.is_empty() {
- self.upsert_role(&name)
+ role_name = Some(name);
} else {
bail!("Cannot perform this operation because you are in a non-empty session")
}
} else {
bail!("No role")
}
- } else if let Some(name) = self.role.as_ref().map(|v| v.name().to_string()) {
- self.upsert_role(&name)
} else {
- bail!("No role")
+ role_name = self.role.as_ref().map(|v| v.name().to_string());
}
+ let name = role_name.ok_or_else(|| anyhow!("No role"))?;
+ self.upsert_role(&name)?;
+ self.use_role(&name)
}
pub fn upsert_role(&mut self, name: &str) -> Result<()> {
@@ -921,7 +948,6 @@ impl Config {
ensure_parent_exists(&role_path)?;
let editor = self.editor()?;
edit_file(&editor, &role_path)?;
- self.use_role(name)?;
Ok(())
}
@@ -1444,9 +1470,13 @@ impl Config {
bail!("Already in a agent, please run '.exit agent' first to exit the current agent.");
}
let agent = Agent::init(config, agent_name, abort_signal).await?;
- let session = session_name
- .map(|v| v.to_string())
- .or_else(|| agent.agent_prelude().map(|v| v.to_string()));
+ let session = session_name.map(|v| v.to_string()).or_else(|| {
+ if config.read().macro_flag {
+ None
+ } else {
+ agent.agent_prelude().map(|v| v.to_string())
+ }
+ });
config.write().rag = agent.rag();
config.write().agent = Some(agent);
if let Some(session) = session {
@@ -1516,8 +1546,43 @@ impl Config {
Ok(())
}
+ pub fn list_macros() -> Vec<String> {
+ list_file_names(Self::macros_dir(), ".yaml")
+ }
+
+ pub fn load_macro(name: &str) -> Result<Macro> {
+ let path = Self::macro_file(name);
+ let err = || format!("Failed to load macro '{name}' at '{}'", path.display());
+ let content = read_to_string(&path).with_context(err)?;
+ let value: Macro = serde_yaml::from_str(&content).with_context(err)?;
+ Ok(value)
+ }
+
+ pub fn has_macro(name: &str) -> bool {
+ let names = Self::list_macros();
+ names.contains(&name.to_string())
+ }
+
+ pub fn new_macro(&mut self, name: &str) -> Result<()> {
+ if self.macro_flag {
+ bail!("No macro");
+ }
+ let ans = Confirm::new("Create a new macro?")
+ .with_default(true)
+ .prompt()?;
+ if ans {
+ let macro_path = Self::macro_file(name);
+ ensure_parent_exists(&macro_path)?;
+ let editor = self.editor()?;
+ edit_file(&editor, &macro_path)?;
+ } else {
+ bail!("No macro");
+ }
+ Ok(())
+ }
+
pub fn apply_prelude(&mut self) -> Result<()> {
- if !self.state().is_empty() {
+ if self.macro_flag || !self.state().is_empty() {
return Ok(());
}
let prelude = match self.working_mode {
@@ -1672,6 +1737,7 @@ impl Config {
}
".rag" => map_completion_values(Self::list_rags()),
".agent" => map_completion_values(list_agents()),
+ ".macro" => map_completion_values(Self::list_macros()),
".starter" => match &self.agent {
Some(agent) => map_completion_values(agent.conversation_staters().to_vec()),
None => vec![],
@@ -1686,18 +1752,18 @@ impl Config {
},
".set" => {
let mut values = vec![
- "max_output_tokens",
"temperature",
"top_p",
- "dry_run",
- "stream",
- "save",
- "function_calling",
"use_tools",
"save_session",
"compress_threshold",
"rag_reranker_model",
"rag_top_k",
+ "max_output_tokens",
+ "dry_run",
+ "function_calling",
+ "stream",
+ "save",
"highlight",
];
values.sort_unstable();
@@ -1706,7 +1772,9 @@ impl Config {
.map(|v| (format!("{v} "), None))
.collect()
}
- ".delete" => map_completion_values(vec!["role", "session", "rag", "agent-data"]),
+ ".delete" => {
+ map_completion_values(vec!["role", "session", "rag", "macro", "agent-data"])
+ }
_ => vec![],
};
filter = args[0]
@@ -2009,11 +2077,11 @@ impl Config {
let new_variables = Agent::init_agent_variables(
agent.defined_variables(),
&config_variables,
- self.cli_info_flag,
+ self.info_flag,
)?;
agent.set_shared_variables(new_variables);
}
- if !self.cli_info_flag {
+ if !self.info_flag {
agent.update_shared_dynamic_instructions(false)?;
}
Ok(())
@@ -2035,7 +2103,7 @@ impl Config {
let new_variables = Agent::init_agent_variables(
agent.defined_variables(),
&config_variables,
- self.cli_info_flag,
+ self.info_flag,
)?;
agent.set_shared_variables(new_variables.clone());
new_variables
@@ -2043,7 +2111,7 @@ impl Config {
shared_variables
};
agent.set_session_variables(session_variables);
- if !self.cli_info_flag {
+ if !self.info_flag {
agent.update_session_dynamic_instructions(None)?;
}
session.sync_agent(agent);
@@ -2319,6 +2387,106 @@ impl WorkingMode {
}
}
+#[async_recursion::async_recursion]
+pub async fn macro_execute(
+ config: &GlobalConfig,
+ name: &str,
+ args: Option<&str>,
+ abort_signal: AbortSignal,
+) -> Result<()> {
+ let macro_value = Config::load_macro(name)?;
+ let (mut new_args, text) = split_params_text(args.unwrap_or_default(), cfg!(windows));
+ if !text.is_empty() {
+ new_args.push(text.to_string());
+ }
+ let variables = macro_value
+ .resolve_variables(&new_args)
+ .map_err(|err| anyhow!("{err}. Usage: {}", macro_value.usage(name)))?;
+ let role = config.read().extract_role();
+ let mut config = config.read().clone();
+ config.temperature = role.temperature();
+ config.top_p = role.top_p();
+ config.use_tools = role.use_tools().clone();
+ config.macro_flag = true;
+ config.model = role.model().clone();
+ config.role = None;
+ config.session = None;
+ config.rag = None;
+ config.agent = None;
+ config.discontinuous_last_message();
+ let config = Arc::new(RwLock::new(config));
+ config.write().macro_flag = true;
+ for step in &macro_value.steps {
+ let command = Macro::interpolate_command(step, &variables);
+ println!(">> {}", multiline_text(&command));
+ run_repl_command(&config, abort_signal.clone(), &command).await?;
+ }
+ Ok(())
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct Macro {
+ #[serde(default)]
+ pub variables: Vec<MacroVariable>,
+ pub steps: Vec<String>,
+}
+
+impl Macro {
+ pub fn resolve_variables(&self, args: &[String]) -> Result<IndexMap<String, String>> {
+ let mut output = IndexMap::new();
+ for (i, variable) in self.variables.iter().enumerate() {
+ let value = if variable.rest && i == self.variables.len() - 1 {
+ if args.len() > i {
+ Some(args[i..].join(" "))
+ } else {
+ variable.default.clone()
+ }
+ } else {
+ args.get(i)
+ .map(|v| v.to_string())
+ .or_else(|| variable.default.clone())
+ };
+ let value =
+ value.ok_or_else(|| anyhow!("Missing value for variable '{}'", variable.name))?;
+ output.insert(variable.name.clone(), value);
+ }
+ Ok(output)
+ }
+
+ pub fn usage(&self, name: &str) -> String {
+ let mut parts = vec![name.to_string()];
+ for (i, variable) in self.variables.iter().enumerate() {
+ let part = match (
+ variable.rest && i == self.variables.len() - 1,
+ variable.default.is_some(),
+ ) {
+ (true, true) => format!("[{}]...", variable.name),
+ (true, false) => format!("<{}>...", variable.name),
+ (false, true) => format!("[{}]", variable.name),
+ (false, false) => format!("<{}>", variable.name),
+ };
+ parts.push(part);
+ }
+ parts.join(" ")
+ }
+
+ pub fn interpolate_command(command: &str, variables: &IndexMap<String, String>) -> String {
+ let mut output = command.to_string();
+ for (key, value) in variables {
+ output = output.replace(&format!("{{{{{key}}}}}"), value);
+ }
+ output
+ }
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct MacroVariable {
+ pub name: String,
+ #[serde(default)]
+ pub rest: bool,
+ pub default: Option<String>,
+}
+
#[derive(Debug, Clone)]
pub struct LastMessage {
pub input: Input,
@@ -2487,3 +2655,13 @@ where
config.write().rag = Some(Arc::new(rag));
Ok(())
}
+
+fn format_option_value<T>(value: &Option<T>) -> String
+where
+ T: std::fmt::Display,
+{
+ match value {
+ Some(value) => value.to_string(),
+ None => "null".to_string(),
+ }
+}
diff --git a/src/config/session.rs b/src/config/session.rs
index 227a23c..13af751 100644
--- a/src/config/session.rs
+++ b/src/config/session.rs
@@ -234,8 +234,7 @@ impl Session {
}
MessageRole::User => {
lines.push(format!(
- "{}){}",
- self.name,
+ ">> {}",
message.content.render_input(resolve_url_fn, agent_info)
));
}
diff --git a/src/main.rs b/src/main.rs
index 056133c..48eb2b9 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -17,8 +17,8 @@ use crate::client::{
call_chat_completions, call_chat_completions_streaming, list_models, ModelType,
};
use crate::config::{
- ensure_parent_exists, list_agents, load_env_file, Config, GlobalConfig, Input, WorkingMode,
- CODE_ROLE, EXPLAIN_SHELL_ROLE, SHELL_ROLE, TEMP_SESSION_NAME,
+ ensure_parent_exists, list_agents, load_env_file, macro_execute, Config, GlobalConfig, Input,
+ WorkingMode, CODE_ROLE, EXPLAIN_SHELL_ROLE, SHELL_ROLE, TEMP_SESSION_NAME,
};
use crate::render::render_error;
use crate::repl::Repl;
@@ -31,19 +31,13 @@ use inquire::Text;
use is_terminal::IsTerminal;
use parking_lot::RwLock;
use simplelog::{format_description, ConfigBuilder, LevelFilter, SimpleLogger, WriteLogger};
-use std::{
- env,
- io::{stdin, Read},
- process,
- sync::Arc,
-};
+use std::{env, io::stdin, process, sync::Arc};
#[tokio::main]
async fn main() -> Result<()> {
load_env_file()?;
let cli = Cli::parse();
- let text = cli.text();
- let text = aggregate_text(text)?;
+ let text = cli.text()?;
let working_mode = if cli.serve.is_some() {
WorkingMode::Serve
} else if text.is_none() && cli.file.is_empty() {
@@ -67,7 +61,7 @@ async fn run(config: GlobalConfig, cli: Cli, text: Option<String>) -> Result<()>
return serve::run(config, addr).await;
}
if cli.info {
- config.write().cli_info_flag = true;
+ config.write().info_flag = true;
}
if cli.list_models {
@@ -91,6 +85,12 @@ async fn run(config: GlobalConfig, cli: Cli, text: Option<String>) -> Result<()>
println!("{rags}");
return Ok(());
}
+ if cli.list_macros {
+ let macros = Config::list_macros().join("\n");
+ println!("{macros}");
+ return Ok(());
+ }
+
if cli.dry_run {
config.write().dry_run = true;
}
@@ -158,6 +158,10 @@ async fn run(config: GlobalConfig, cli: Cli, text: Option<String>) -> Result<()>
return Ok(());
}
}
+ if let Some(name) = &cli.macro_name {
+ macro_execute(&config, name, text.as_deref(), abort_signal.clone()).await?;
+ return Ok(());
+ }
if cli.execute && !is_repl {
if cfg!(target_os = "macos") && !stdin().is_terminal() {
bail!("Unable to read the pipe for shell execution on MacOS")
@@ -318,21 +322,6 @@ async fn shell_execute(
Ok(())
}
-fn aggregate_text(text: Option<String>) -> Result<Option<String>> {
- let text = if stdin().is_terminal() {
- text
- } else {
- let mut stdin_text = String::new();
- stdin().read_to_string(&mut stdin_text)?;
- if let Some(text) = text {
- Some(format!("{text}\n{stdin_text}"))
- } else {
- Some(stdin_text)
- }
- };
- Ok(text)
-}
-
async fn create_input(
config: &GlobalConfig,
text: Option<String>,
diff --git a/src/repl/mod.rs b/src/repl/mod.rs
index 136d113..87befd3 100644
--- a/src/repl/mod.rs
+++ b/src/repl/mod.rs
@@ -7,7 +7,9 @@ use self::highlighter::ReplHighlighter;
use self::prompt::ReplPrompt;
use crate::client::{call_chat_completions, call_chat_completions_streaming};
-use crate::config::{AssertState, Config, GlobalConfig, Input, LastMessage, StateFlags};
+use crate::config::{
+ macro_execute, AssertState, Config, GlobalConfig, Input, LastMessage, StateFlags,
+};
use crate::render::render_error;
use crate::utils::{
abortable_run_with_spinner, create_abort_signal, set_text, temp_file, AbortSignal,
@@ -26,7 +28,7 @@ use std::{env, process};
const MENU_NAME: &str = "completion_menu";
lazy_static::lazy_static! {
- static ref REPL_COMMANDS: [ReplCommand; 34] = [
+ static ref REPL_COMMANDS: [ReplCommand; 35] = [
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()),
@@ -147,6 +149,11 @@ lazy_static::lazy_static! {
AssertState::TrueFalse(StateFlags::RAG, StateFlags::AGENT),
),
ReplCommand::new(
+ ".macro",
+ "Execute a macro",
+ AssertState::pass()
+ ),
+ ReplCommand::new(
".file",
"Include files, directories, URLs or commands",
AssertState::pass()
@@ -192,7 +199,13 @@ impl Repl {
if AssertState::False(StateFlags::AGENT | StateFlags::RAG)
.assert(self.config.read().state())
{
- self.banner();
+ print!(
+ r#"Welcome to {} {}
+Type ".help" for additional help.
+"#,
+ env!("CARGO_CRATE_NAME"),
+ env!("CARGO_PKG_VERSION"),
+ )
}
loop {
@@ -203,7 +216,7 @@ impl Repl {
match sig {
Ok(Signal::Success(line)) => {
self.abort_signal.reset();
- match self.handle(&line).await {
+ match run_repl_command(&self.config, self.abort_signal.clone(), &line).await {
Ok(exit) => {
if exit {
break;
@@ -230,313 +243,6 @@ impl Repl {
Ok(())
}
- async fn handle(&self, mut line: &str) -> Result<bool> {
- if let Ok(Some(captures)) = MULTILINE_RE.captures(line) {
- if let Some(text_match) = captures.get(1) {
- line = text_match.as_str();
- }
- }
- match parse_command(line) {
- Some((cmd, args)) => match cmd {
- ".help" => {
- dump_repl_help();
- }
- ".info" => match args {
- Some("role") => {
- let info = self.config.read().role_info()?;
- print!("{}", info);
- }
- Some("session") => {
- let info = self.config.read().session_info()?;
- print!("{}", info);
- }
- Some("rag") => {
- let info = self.config.read().rag_info()?;
- print!("{}", info);
- }
- Some("agent") => {
- let info = self.config.read().agent_info()?;
- print!("{}", info);
- }
- Some(_) => unknown_command()?,
- None => {
- let output = self.config.read().sysinfo()?;
- print!("{}", output);
- }
- },
- ".model" => match args {
- Some(name) => {
- self.config.write().set_model(name)?;
- }
- None => println!("Usage: .model <name>"),
- },
- ".prompt" => match args {
- Some(text) => {
- self.config.write().use_prompt(text)?;
- }
- None => println!("Usage: .prompt <text>..."),
- },
- ".role" => match args {
- Some(args) => match args.split_once(['\n', ' ']) {
- Some((name, text)) => {
- let role = self.config.read().retrieve_role(name.trim())?;
- let input = Input::from_str(&self.config, text, Some(role));
- ask(&self.config, self.abort_signal.clone(), input, false).await?;
- }
- None => {
- let name = args;
- if Config::has_role(name) {
- self.config.write().use_role(name)?;
- } else {
- self.config.write().new_role(name)?;
- }
- }
- },
- None => println!(
- r#"Usage:
- .role <name> # If the role exists, switch to it; otherwise, create a new role
- .role <name> [text]... # Temporarily switch to the role, send the text, and switch back"#
- ),
- },
- ".session" => {
- self.config.write().use_session(args)?;
- Config::maybe_autoname_session(self.config.clone());
- }
- ".rag" => {
- Config::use_rag(&self.config, args, self.abort_signal.clone()).await?;
- }
- ".agent" => match split_args(args) {
- Some((agent_name, session_name)) => {
- Config::use_agent(
- &self.config,
- agent_name,
- session_name,
- self.abort_signal.clone(),
- )
- .await?;
- }
- None => println!(r#"Usage: .agent <agent-name> [session-name]"#),
- },
- ".starter" => match args {
- Some(value) => {
- let input = Input::from_str(&self.config, value, None);
- ask(&self.config, self.abort_signal.clone(), input, true).await?;
- }
- None => {
- let banner = self.config.read().agent_banner()?;
- self.config.read().print_markdown(&banner)?;
- }
- },
- ".variable" => match args {
- Some(args) => {
- self.config.write().set_agent_variable(args)?;
- }
- _ => {
- println!("Usage: .variable <key> <value>")
- }
- },
- ".save" => match split_args(args) {
- Some(("role", name)) => {
- self.config.write().save_role(name)?;
- }
- Some(("session", name)) => {
- self.config.write().save_session(name)?;
- }
- _ => {
- println!(r#"Usage: .save <role|session> [name]"#)
- }
- },
- ".edit" => match args {
- Some("role") => {
- self.config.write().edit_role()?;
- }
- Some("session") => {
- self.config.write().edit_session()?;
- }
- Some("rag-docs") => {
- Config::edit_rag_docs(&self.config, self.abort_signal.clone()).await?;
- }
- _ => {
- println!(r#"Usage: .edit <role|session|rag-docs>"#)
- }
- },
- ".compress" => match args {
- Some("session") => {
- abortable_run_with_spinner(
- Config::compress_session(&self.config),
- "Compressing",
- self.abort_signal.clone(),
- )
- .await?;
- println!("✓ Successfully compressed the session.");
- }
- _ => {
- println!(r#"Usage: .compress session"#)
- }
- },
- ".empty" => match args {
- Some("session") => {
- self.config.write().empty_session()?;
- }
- _ => {
- println!(r#"Usage: .empty session"#)
- }
- },
- ".rebuild" => match args {
- Some("rag") => {
- Config::rebuild_rag(&self.config, self.abort_signal.clone()).await?;
- }
- _ => {
- println!(r#"Usage: .rebuild rag"#)
- }
- },
- ".sources" => match args {
- Some("rag") => {
- let output = Config::rag_sources(&self.config)?;
- println!("{}", output);
- }
- _ => {
- println!(r#"Usage: .sources rag"#)
- }
- },
- ".file" => match args {
- Some(args) => {
- let (files, text) = split_files_text(args, cfg!(windows));
- let input = Input::from_files_with_spinner(
- &self.config,
- text,
- files,
- None,
- self.abort_signal.clone(),
- )
- .await?;
- ask(&self.config, self.abort_signal.clone(), input, true).await?;
- }
- None => println!(
- r#"Usage: .file <file|dir|url|%%|cmd>... [-- <text>...]
-
-.file /tmp/file.txt
-.file src/ Cargo.toml -- analyze
-.file https://example.com/file.txt -- summarize
-.file https://example.com/image.png -- recognize text
-.file %% -- translate last reply to english
-.file `git diff` -- Generate git commit message"#
- ),
- },
- ".continue" => {
- let LastMessage {
- mut input, output, ..
- } = match self
- .config
- .read()
- .last_message
- .as_ref()
- .filter(|v| v.continuous && !v.output.is_empty())
- .cloned()
- {
- Some(v) => v,
- None => bail!("Unable to continue the response"),
- };
- input.set_continue_output(&output);
- ask(&self.config, self.abort_signal.clone(), input, true).await?;
- }
- ".regenerate" => {
- let LastMessage { mut input, .. } = match self
- .config
- .read()
- .last_message
- .as_ref()
- .filter(|v| v.continuous)
- .cloned()
- {
- Some(v) => v,
- None => bail!("Unable to regenerate the response"),
- };
- input.set_regenerate();
- ask(&self.config, self.abort_signal.clone(), input, true).await?;
- }
- ".set" => match args {
- Some(args) => {
- Config::update(&self.config, args)?;
- }
- _ => {
- println!("Usage: .set <key> <value>...")
- }
- },
- ".delete" => match args {
- Some(args) => {
- Config::delete(&self.config, args)?;
- }
- _ => {
- println!("Usage: .delete <role|session|rag|agent-data>")
- }
- },
- ".copy" => {
- let output = match self
- .config
- .read()
- .last_message
- .as_ref()
- .filter(|v| v.continuous && !v.output.is_empty())
- .map(|v| v.output.clone())
- {
- Some(v) => v,
- None => bail!("No chat response to copy"),
- };
- self.copy(&output)
- .with_context(|| "Failed to copy the last chat response")?;
- }
- ".exit" => match args {
- Some("role") => {
- self.config.write().exit_role()?;
- }
- Some("session") => {
- if self.config.read().agent.is_some() {
- self.config.write().exit_agent_session()?;
- } else {
- self.config.write().exit_session()?;
- }
- }
- Some("rag") => {
- self.config.write().exit_rag()?;
- }
- Some("agent") => {
- self.config.write().exit_agent()?;
- }
- Some(_) => unknown_command()?,
- None => {
- return Ok(true);
- }
- },
- ".clear" => match args {
- Some("messages") => {
- bail!("Use '.empty session' instead");
- }
- _ => unknown_command()?,
- },
- _ => unknown_command()?,
- },
- None => {
- let input = Input::from_str(&self.config, line, None);
- ask(&self.config, self.abort_signal.clone(), input, true).await?;
- }
- }
-
- println!();
-
- Ok(false)
- }
-
- fn banner(&self) {
- let name = env!("CARGO_CRATE_NAME");
- let version = env!("CARGO_PKG_VERSION");
- print!(
- r#"Welcome to {name} {version}
-Type ".help" for additional help.
-"#
- )
- }
-
fn create_editor(config: &GlobalConfig) -> Result<Reedline> {
let completer = ReplCompleter::new(config);
let highlighter = ReplHighlighter::new(config);
@@ -602,14 +308,6 @@ Type ".help" for additional help.
let completion_menu = ColumnarMenu::default().with_name(MENU_NAME);
ReedlineMenu::EngineCompleter(Box::new(completion_menu))
}
-
- fn copy(&self, text: &str) -> Result<()> {
- if text.is_empty() {
- bail!("No text to copy")
- }
- set_text(text)?;
- Ok(())
- }
}
#[derive(Debug, Clone)]
@@ -647,6 +345,314 @@ impl Validator for ReplValidator {
}
}
+pub async fn run_repl_command(
+ config: &GlobalConfig,
+ abort_signal: AbortSignal,
+ mut line: &str,
+) -> Result<bool> {
+ if let Ok(Some(captures)) = MULTILINE_RE.captures(line) {
+ if let Some(text_match) = captures.get(1) {
+ line = text_match.as_str();
+ }
+ }
+ match parse_command(line) {
+ Some((cmd, args)) => match cmd {
+ ".help" => {
+ dump_repl_help();
+ }
+ ".info" => match args {
+ Some("role") => {
+ let info = config.read().role_info()?;
+ print!("{}", info);
+ }
+ Some("session") => {
+ let info = config.read().session_info()?;
+ print!("{}", info);
+ }
+ Some("rag") => {
+ let info = config.read().rag_info()?;
+ print!("{}", info);
+ }
+ Some("agent") => {
+ let info = config.read().agent_info()?;
+ print!("{}", info);
+ }
+ Some(_) => unknown_command()?,
+ None => {
+ let output = config.read().sysinfo()?;
+ print!("{}", output);
+ }
+ },
+ ".model" => match args {
+ Some(name) => {
+ config.write().set_model(name)?;
+ }
+ None => println!("Usage: .model <name>"),
+ },
+ ".prompt" => match args {
+ Some(text) => {
+ config.write().use_prompt(text)?;
+ }
+ None => println!("Usage: .prompt <text>..."),
+ },
+ ".role" => match args {
+ Some(args) => match args.split_once(['\n', ' ']) {
+ Some((name, text)) => {
+ let role = config.read().retrieve_role(name.trim())?;
+ let input = Input::from_str(config, text, Some(role));
+ ask(config, abort_signal.clone(), input, false).await?;
+ }
+ None => {
+ let name = args;
+ if !Config::has_role(name) {
+ config.write().new_role(name)?;
+ }
+ config.write().use_role(name)?;
+ }
+ },
+ None => println!(
+ r#"Usage:
+ .role <name> # If the role exists, switch to it; otherwise, create a new role
+ .role <name> [text]... # Temporarily switch to the role, send the text, and switch back"#
+ ),
+ },
+ ".session" => {
+ config.write().use_session(args)?;
+ Config::maybe_autoname_session(config.clone());
+ }
+ ".rag" => {
+ Config::use_rag(config, args, abort_signal.clone()).await?;
+ }
+ ".agent" => match split_first_arg(args) {
+ Some((agent_name, session_name)) => {
+ Config::use_agent(config, agent_name, session_name, abort_signal.clone())
+ .await?;
+ }
+ None => println!(r#"Usage: .agent <agent-name> [session-name]"#),
+ },
+ ".starter" => match args {
+ Some(value) => {
+ let input = Input::from_str(config, value, None);
+ ask(config, abort_signal.clone(), input, true).await?;
+ }
+ None => {
+ let banner = config.read().agent_banner()?;
+ config.read().print_markdown(&banner)?;
+ }
+ },
+ ".variable" => match args {
+ Some(args) => {
+ config.write().set_agent_variable(args)?;
+ }
+ _ => {
+ println!("Usage: .variable <key> <value>")
+ }
+ },
+ ".save" => match split_first_arg(args) {
+ Some(("role", name)) => {
+ config.write().save_role(name)?;
+ }
+ Some(("session", name)) => {
+ config.write().save_session(name)?;
+ }
+ _ => {
+ println!(r#"Usage: .save <role|session> [name]"#)
+ }
+ },
+ ".edit" => {
+ if config.read().macro_flag {
+ bail!("Cannot perform this operation because you are in a macro")
+ }
+ match args {
+ Some("role") => {
+ config.write().edit_role()?;
+ }
+ Some("session") => {
+ config.write().edit_session()?;
+ }
+ Some("rag-docs") => {
+ Config::edit_rag_docs(config, abort_signal.clone()).await?;
+ }
+ _ => {
+ println!(r#"Usage: .edit <role|session|rag-docs>"#)
+ }
+ }
+ }
+ ".compress" => match args {
+ Some("session") => {
+ abortable_run_with_spinner(
+ Config::compress_session(config),
+ "Compressing",
+ abort_signal.clone(),
+ )
+ .await?;
+ println!("✓ Successfully compressed the session.");
+ }
+ _ => {
+ println!(r#"Usage: .compress session"#)
+ }
+ },
+ ".empty" => match args {
+ Some("session") => {
+ config.write().empty_session()?;
+ }
+ _ => {
+ println!(r#"Usage: .empty session"#)
+ }
+ },
+ ".rebuild" => match args {
+ Some("rag") => {
+ Config::rebuild_rag(config, abort_signal.clone()).await?;
+ }
+ _ => {
+ println!(r#"Usage: .rebuild rag"#)
+ }
+ },
+ ".sources" => match args {
+ Some("rag") => {
+ let output = Config::rag_sources(config)?;
+ println!("{}", output);
+ }
+ _ => {
+ println!(r#"Usage: .sources rag"#)
+ }
+ },
+ ".macro" => match split_first_arg(args) {
+ Some((name, extra)) => {
+ if !Config::has_macro(name) && extra.is_none() {
+ config.write().new_macro(name)?;
+ } else {
+ macro_execute(config, name, extra, abort_signal.clone()).await?;
+ }
+ }
+ None => println!("Usage: .macro <name> <text>..."),
+ },
+ ".file" => match args {
+ Some(args) => {
+ let (files, text) = split_params_text(args, cfg!(windows));
+ let input = Input::from_files_with_spinner(
+ config,
+ text,
+ files,
+ None,
+ abort_signal.clone(),
+ )
+ .await?;
+ ask(config, abort_signal.clone(), input, true).await?;
+ }
+ None => println!(
+ r#"Usage: .file <file|dir|url|%%|cmd>... [-- <text>...]
+
+.file /tmp/file.txt
+.file src/ Cargo.toml -- analyze
+.file https://example.com/file.txt -- summarize
+.file https://example.com/image.png -- recognize text
+.file %% -- translate last reply to english
+.file `git diff` -- Generate git commit message"#
+ ),
+ },
+ ".continue" => {
+ let LastMessage {
+ mut input, output, ..
+ } = match config
+ .read()
+ .last_message
+ .as_ref()
+ .filter(|v| v.continuous && !v.output.is_empty())
+ .cloned()
+ {
+ Some(v) => v,
+ None => bail!("Unable to continue the response"),
+ };
+ input.set_continue_output(&output);
+ ask(config, abort_signal.clone(), input, true).await?;
+ }
+ ".regenerate" => {
+ let LastMessage { mut input, .. } = match config
+ .read()
+ .last_message
+ .as_ref()
+ .filter(|v| v.continuous)
+ .cloned()
+ {
+ Some(v) => v,
+ None => bail!("Unable to regenerate the response"),
+ };
+ input.set_regenerate();
+ ask(config, abort_signal.clone(), input, true).await?;
+ }
+ ".set" => match args {
+ Some(args) => {
+ Config::update(config, args)?;
+ }
+ _ => {
+ println!("Usage: .set <key> <value>...")
+ }
+ },
+ ".delete" => match args {
+ Some(args) => {
+ Config::delete(config, args)?;
+ }
+ _ => {
+ println!("Usage: .delete <role|session|rag|macro|agent-data>")
+ }
+ },
+ ".copy" => {
+ let output = match config
+ .read()
+ .last_message
+ .as_ref()
+ .filter(|v| v.continuous && !v.output.is_empty())
+ .map(|v| v.output.clone())
+ {
+ Some(v) => v,
+ None => bail!("No chat response to copy"),
+ };
+ set_text(&output).context("Failed to copy the last chat response")?;
+ }
+ ".exit" => match args {
+ Some("role") => {
+ config.write().exit_role()?;
+ }
+ Some("session") => {
+ if config.read().agent.is_some() {
+ config.write().exit_agent_session()?;
+ } else {
+ config.write().exit_session()?;
+ }
+ }
+ Some("rag") => {
+ config.write().exit_rag()?;
+ }
+ Some("agent") => {
+ config.write().exit_agent()?;
+ }
+ Some(_) => unknown_command()?,
+ None => {
+ return Ok(true);
+ }
+ },
+ ".clear" => match args {
+ Some("messages") => {
+ bail!("Use '.empty session' instead");
+ }
+ _ => unknown_command()?,
+ },
+ _ => unknown_command()?,
+ },
+ None => {
+ let input = Input::from_str(config, line, None);
+ ask(config, abort_signal.clone(), input, true).await?;
+ }
+ }
+
+ if !config.read().macro_flag {
+ println!();
+ }
+
+ Ok(false)
+}
+
#[async_recursion::async_recursion]
async fn ask(
config: &GlobalConfig,
@@ -720,14 +726,14 @@ fn parse_command(line: &str) -> Option<(&str, Option<&str>)> {
}
}
-fn split_args(args: Option<&str>) -> Option<(&str, Option<&str>)> {
+fn split_first_arg(args: Option<&str>) -> Option<(&str, Option<&str>)> {
args.map(|v| match v.split_once(' ') {
Some((subcmd, args)) => (subcmd, Some(args.trim())),
None => (v, None),
})
}
-fn split_files_text(line: &str, is_win: bool) -> (Vec<String>, &str) {
+pub fn split_params_text(line: &str, is_win: bool) -> (Vec<String>, &str) {
let mut words = Vec::new();
let mut word = String::new();
let mut unbalance: Option<char> = None;
@@ -823,44 +829,45 @@ mod tests {
}
#[test]
- fn test_split_files_text() {
+ fn test_split_params_text() {
+ assert_eq!(split_params_text("", false), (vec![], ""));
assert_eq!(
- split_files_text("file.txt", false),
+ split_params_text("file.txt", false),
(vec!["file.txt".into()], "")
);
assert_eq!(
- split_files_text("file.txt --", false),
+ split_params_text("file.txt --", false),
(vec!["file.txt".into()], "")
);
assert_eq!(
- split_files_text("file.txt -- hello", false),
+ split_params_text("file.txt -- hello", false),
(vec!["file.txt".into()], "hello")
);
assert_eq!(
- split_files_text("file.txt -- \thello", false),
+ split_params_text("file.txt -- \thello", false),
(vec!["file.txt".into()], "\thello")
);
assert_eq!(
- split_files_text("file.txt --\nhello", false),
+ split_params_text("file.txt --\nhello", false),
(vec!["file.txt".into()], "hello")
);
assert_eq!(
- split_files_text("file.txt --\r\nhello", false),
+ split_params_text("file.txt --\r\nhello", false),
(vec!["file.txt".into()], "hello")
);
assert_eq!(
- split_files_text("file.txt --\rhello", false),
+ split_params_text("file.txt --\rhello", false),
(vec!["file.txt".into()], "hello")
);
assert_eq!(
- split_files_text(r#"file1.txt 'file2.txt' "file3.txt""#, false),
+ split_params_text(r#"file1.txt 'file2.txt' "file3.txt""#, false),
(
vec!["file1.txt".into(), "file2.txt".into(), "file3.txt".into()],
""
)
);
assert_eq!(
- split_files_text(r#"./file1.txt 'file1 - Copy.txt' file\ 2.txt"#, false),
+ split_params_text(r#"./file1.txt 'file1 - Copy.txt' file\ 2.txt"#, false),
(
vec![
"./file1.txt".into(),
@@ -871,7 +878,7 @@ mod tests {
)
);
assert_eq!(
- split_files_text(r#".\file.txt C:\dir\file.txt"#, true),
+ split_params_text(r#".\file.txt C:\dir\file.txt"#, true),
(vec![".\\file.txt".into(), "C:\\dir\\file.txt".into()], "")
);
}
diff --git a/src/utils/mod.rs b/src/utils/mod.rs
index c880252..ecc81aa 100644
--- a/src/utils/mod.rs
+++ b/src/utils/mod.rs
@@ -112,16 +112,6 @@ pub fn extract_block(input: &str) -> String {
}
}
-pub fn format_option_value<T>(value: &Option<T>) -> String
-where
- T: std::fmt::Display,
-{
- match value {
- Some(value) => value.to_string(),
- None => "-".to_string(),
- }
-}
-
pub fn convert_option_string(value: &str) -> Option<String> {
if value.is_empty() {
None
@@ -199,6 +189,21 @@ pub fn dimmed_text(input: &str) -> String {
nu_ansi_term::Style::new().dimmed().paint(input).to_string()
}
+pub fn multiline_text(input: &str) -> String {
+ input
+ .split('\n')
+ .enumerate()
+ .map(|(i, v)| {
+ if i == 0 {
+ v.to_string()
+ } else {
+ format!(".. {v}")
+ }
+ })
+ .collect::<Vec<String>>()
+ .join("\n")
+}
+
pub fn temp_file(prefix: &str, suffix: &str) -> PathBuf {
env::temp_dir().join(format!(
"{}-{}{prefix}{}{suffix}",