pub mod command_registry;
mod completer;
mod highlighter;
mod prompt;
pub use completer::SelfwareCompleter;
pub use highlighter::SelfwareHighlighter;
pub use prompt::SelfwarePrompt;
use anyhow::Result;
use reedline::{
default_emacs_keybindings, ColumnarMenu, DefaultHinter, DefaultValidator, EditCommand, Emacs,
FileBackedHistory, KeyCode, KeyModifiers, Keybindings, MenuBuilder, Reedline, ReedlineEvent,
ReedlineMenu, Signal, Vi,
};
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum InputMode {
#[default]
Emacs,
Vi,
}
#[derive(Debug, Clone)]
pub struct InputConfig {
pub mode: InputMode,
pub history_path: Option<PathBuf>,
pub max_history: usize,
pub syntax_highlight: bool,
pub show_hints: bool,
pub tool_names: Vec<String>,
pub commands: Vec<String>,
}
impl Default for InputConfig {
fn default() -> Self {
Self {
mode: InputMode::Emacs,
history_path: dirs_history_path(),
max_history: 10000,
syntax_highlight: true,
show_hints: true,
tool_names: vec![],
commands: command_registry::command_names(),
}
}
}
fn dirs_history_path() -> Option<PathBuf> {
dirs::data_local_dir().map(|p| p.join("selfware").join("history.txt"))
}
pub struct SelfwareEditor {
editor: Reedline,
prompt: SelfwarePrompt,
config: InputConfig,
}
impl SelfwareEditor {
pub fn new(config: InputConfig) -> Result<Self> {
let history = if let Some(path) = &config.history_path {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
Box::new(FileBackedHistory::with_file(
config.max_history,
path.clone(),
)?)
} else {
Box::new(FileBackedHistory::new(config.max_history)?)
};
let completer = Box::new(SelfwareCompleter::new(
config.tool_names.clone(),
config.commands.clone(),
));
let highlighter = Box::new(SelfwareHighlighter::new());
let hinter = Box::new(DefaultHinter::default());
let completion_menu = Box::new(
ColumnarMenu::default()
.with_name("completion_menu")
.with_columns(1) .with_column_padding(2)
.with_marker(" > "), );
let keybindings = Self::build_keybindings(config.mode);
let edit_mode: Box<dyn reedline::EditMode> = match config.mode {
InputMode::Emacs => Box::new(Emacs::new(keybindings)),
InputMode::Vi => Box::new(Vi::default()),
};
let validator = Box::new(DefaultValidator);
let editor_cmd = std::env::var("VISUAL")
.or_else(|_| std::env::var("EDITOR"))
.unwrap_or_else(|_| "vi".to_string());
let temp_file =
std::env::temp_dir().join(format!("selfware_edit_{}.tmp", std::process::id()));
let buffer_editor = std::process::Command::new(editor_cmd);
let mut editor = Reedline::create()
.with_history(history)
.with_completer(completer)
.with_quick_completions(true)
.with_partial_completions(true)
.with_hinter(hinter)
.with_highlighter(highlighter)
.with_validator(validator)
.with_menu(ReedlineMenu::EngineCompleter(completion_menu))
.with_edit_mode(edit_mode)
.with_buffer_editor(buffer_editor, temp_file);
editor = editor.with_history_exclusion_prefix(Some(" ".into()));
let prompt = SelfwarePrompt::new();
Ok(Self {
editor,
prompt,
config,
})
}
fn build_keybindings(mode: InputMode) -> Keybindings {
let mut keybindings = match mode {
InputMode::Emacs => default_emacs_keybindings(),
InputMode::Vi => Keybindings::default(),
};
keybindings.add_binding(
KeyModifiers::NONE,
KeyCode::Tab,
ReedlineEvent::UntilFound(vec![
ReedlineEvent::HistoryHintComplete, ReedlineEvent::Edit(vec![EditCommand::Complete]), ReedlineEvent::Menu("completion_menu".to_string()), ReedlineEvent::MenuNext, ]),
);
keybindings.add_binding(
KeyModifiers::NONE,
KeyCode::Char('/'),
ReedlineEvent::Multiple(vec![
ReedlineEvent::Edit(vec![EditCommand::InsertChar('/')]),
ReedlineEvent::Menu("completion_menu".to_string()),
]),
);
keybindings.add_binding(
KeyModifiers::SHIFT,
KeyCode::BackTab,
ReedlineEvent::ExecuteHostCommand("__cycle_mode__".to_string()),
);
keybindings.add_binding(KeyModifiers::NONE, KeyCode::Esc, ReedlineEvent::Esc);
keybindings.add_binding(
KeyModifiers::NONE,
KeyCode::Right,
ReedlineEvent::UntilFound(vec![
ReedlineEvent::HistoryHintComplete,
ReedlineEvent::Edit(vec![EditCommand::MoveRight { select: false }]),
]),
);
keybindings.add_binding(
KeyModifiers::CONTROL,
KeyCode::Char('j'),
ReedlineEvent::Edit(vec![EditCommand::InsertNewline]),
);
keybindings.add_binding(
KeyModifiers::CONTROL,
KeyCode::Char('y'),
ReedlineEvent::ExecuteHostCommand("__toggle_yolo__".to_string()),
);
keybindings.add_binding(
KeyModifiers::CONTROL,
KeyCode::Char('x'),
ReedlineEvent::OpenEditor,
);
keybindings.add_binding(
KeyModifiers::CONTROL,
KeyCode::Char(' '),
ReedlineEvent::Edit(vec![EditCommand::InsertString("".into())]),
);
keybindings
}
pub fn read_line(&mut self) -> Result<ReadlineResult> {
match self.editor.read_line(&self.prompt) {
Ok(Signal::Success(line)) => {
if line.starts_with("__") && line.ends_with("__") {
Ok(ReadlineResult::HostCommand(line))
} else {
Ok(ReadlineResult::Line(line))
}
}
Ok(Signal::CtrlC) => Ok(ReadlineResult::Interrupt),
Ok(Signal::CtrlD) => Ok(ReadlineResult::Eof),
Err(e) => Err(e.into()),
}
}
pub fn set_prompt_full_context(&mut self, model: &str, step: usize, context_pct: f64) {
self.prompt = SelfwarePrompt::with_full_context(model, step, context_pct);
}
pub fn toggle_vim_mode(&mut self) -> Result<InputMode> {
let new_mode = match self.config.mode {
InputMode::Emacs => InputMode::Vi,
InputMode::Vi => InputMode::Emacs,
};
self.config.mode = new_mode;
let new_editor = SelfwareEditor::new(self.config.clone())?;
self.editor = new_editor.editor;
Ok(new_mode)
}
}
#[derive(Debug)]
pub enum ReadlineResult {
Line(String),
Interrupt,
Eof,
HostCommand(String),
}
#[cfg(test)]
#[path = "../../tests/unit/input/mod_test.rs"]
mod tests;