diff --git a/design/prompt.md b/design/prompt.md new file mode 100644 index 00000000..393fb349 --- /dev/null +++ b/design/prompt.md @@ -0,0 +1,81 @@ + +# Prompt Design Document + +Goal: Provide an easy and *rusty* way of building a prompt. + +## TODO Design 1: Derive Macro + +Inspired by clap derive builder +``` +struct Prompt { + +} +``` + +## Design 2: Builder Pattern + +More standard rust builder pattern + +Section trait, each type of section implements it. Display trait is +automatically implemented for each section. +``` +trait Section { + pub fn render(&self); +} + +enum PathMode { + Full, + Top +} + +struct Path { + mode: PathMode +} +impl Section for Path { + fn render(&self) { + ... + } +} +``` + +Final prompt could be built using string formatting +``` +let path = Path { mode: PathMode::Top }; +let username = Username::default(); +let hostname = Hostname::default(); +prompt!(" {username}@{hostname} {path} >"); +``` + +or proper builder pattern +``` +Prompt::builder() + .section(Username::default()) + .text("@") + .section(Hostname::default()) + .text(" ") + .section(Path::top()) + .build(); + +// or with macro +prompt!( + Username::default(), "@", Hostname::default(), " ", Path::top(), " > " +) +``` + +Considerations +- how to color sections +- conditional sections (show git dir when in repository) + - can just return empty string + +Possible sections +- Hostname +- Username +- Path (working directory) +- Indicator (vi mode, completion mode etc) +- Time +- Git info +- Project info (rust, python) +- Last command exit statu +- Time to run last command +- Custom hooks similar to [starship custom commands](https://starship.rs/config/#custom-commands) + diff --git a/src/prompt.rs b/src/prompt.rs index 4cf5987d..79f6c84d 100644 --- a/src/prompt.rs +++ b/src/prompt.rs @@ -1,85 +1,144 @@ //! Collection of utility functions for building a prompt -use std::{borrow::Cow, ffi::OsString, process::Command}; +use std::{borrow::Cow, ffi::OsString, fmt::Display, process::Command}; use anyhow::anyhow; use reedline::{ Prompt, PromptEditMode, PromptHistorySearch, PromptHistorySearchStatus, Reedline, Signal, }; -/// Get the full working directory -pub fn full_pwd() -> String { - std::env::current_dir() - .unwrap() - .into_os_string() - .into_string() - .unwrap() -} +// pub trait Section { +// } + +// pub struct ShowSection(pub T); +// impl std::fmt::Display for ShowSection +// where T: Section +// { +// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +// write!(f, "{}", self.0.render()) +// } +// } -/// Get the top level working directory -pub fn top_pwd() -> String { - std::env::current_dir() - .unwrap() - .file_name() - .unwrap() - .to_os_string() - .into_string() - .unwrap() +pub enum WorkDir { + Full, + Top, } -// TODO this is very linux specific, could use crate that abstracts -// TODO this function is disgusting -pub fn username() -> anyhow::Result { - let username = Command::new("whoami").output()?.stdout; - let encoded = std::str::from_utf8(&username)? - .strip_suffix("\n") - .unwrap() - .to_string(); - Ok(encoded) +impl Display for WorkDir { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let str = match self { + WorkDir::Full => std::env::current_dir() + .unwrap() + .into_os_string() + .into_string() + .unwrap(), + WorkDir::Top => std::env::current_dir() + .unwrap() + .file_name() + .unwrap() + .to_os_string() + .into_string() + .unwrap(), + }; + write!(f, "{}", str) + } } -pub fn hostname() -> anyhow::Result { - let username = Command::new("hostname").output()?.stdout; - let encoded = std::str::from_utf8(&username)? - .strip_suffix("\n") - .unwrap() - .to_string(); - Ok(encoded) +// TODO could technically cache username and hostname as they are unlikely to change +pub struct Username; + +impl Display for Username { + // TODO this is very linux specific, could use crate that abstracts + // TODO this function is disgusting + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let username = Command::new("whoami").output().unwrap().stdout; + let str = std::str::from_utf8(&username) + .unwrap() + .strip_suffix("\n") + .unwrap() + .to_string(); + write!(f, "{}", str) + } } -pub struct CustomPrompt { - pub prompt_indicator: String, - pub prompt_left: String, - pub prompt_right: String, - pub multiline_indicator: String, +pub struct Hostname; + +impl Display for Hostname { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let username = Command::new("hostname").output().unwrap().stdout; + let str = std::str::from_utf8(&username) + .unwrap() + .strip_suffix("\n") + .unwrap() + .to_string(); + write!(f, "{}", str) + } } -impl Default for CustomPrompt { - fn default() -> Self { - CustomPrompt { - prompt_indicator: "> ".into(), - prompt_left: "sh".into(), - prompt_right: "shrs".into(), - multiline_indicator: "| ".into(), +pub struct Rust; + +impl Display for Rust { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // TODO grab info from Cargo.toml + + // look for Cargo.toml + for path in std::fs::read_dir("./").unwrap() { + if path.unwrap().file_name() == "Cargo.toml" { + return write!(f, "rust"); + } } + write!(f, "") + } +} + +// prompt for reedline + +pub trait CustomPrompt: Send { + fn prompt_indicator(&self) -> String; + fn prompt_left(&self) -> String; + fn prompt_right(&self) -> String; + fn multiline_indicator(&self) -> String; +} + +pub struct SimplePrompt; + +impl CustomPrompt for SimplePrompt { + fn prompt_indicator(&self) -> String { + "> ".into() + } + fn prompt_left(&self) -> String { + let username = Username; + let hostname = Hostname; + let workdir = WorkDir::Top; + format!("{username}@{hostname} {workdir} > ") + } + fn prompt_right(&self) -> String { + "".into() + } + fn multiline_indicator(&self) -> String { + "| ".into() } } -impl Prompt for CustomPrompt { +pub struct PromptWrapper(pub T); +impl Prompt for PromptWrapper +where + T: CustomPrompt, +{ fn render_prompt_left(&self) -> Cow { - Cow::Owned(self.prompt_left.clone()) + Cow::Owned(self.0.prompt_left()) } fn render_prompt_right(&self) -> Cow { - Cow::Owned(self.prompt_right.clone()) + Cow::Owned(self.0.prompt_right()) } fn render_prompt_indicator(&self, _edit_mode: PromptEditMode) -> Cow { - Cow::Owned(self.prompt_indicator.clone()) + Cow::Owned(self.0.prompt_indicator()) } fn render_prompt_multiline_indicator(&self) -> Cow { - Cow::Borrowed(&self.multiline_indicator) + Cow::Owned(self.0.multiline_indicator()) } fn render_prompt_history_search_indicator( @@ -97,3 +156,22 @@ impl Prompt for CustomPrompt { )) } } + +#[cfg(test)] +mod tests { + use crate::prompt::*; + + #[test] + fn simple_prompt() { + let username = Username; + let hostname = Hostname; + let workdir = WorkDir::Top; + println!("{username}@{hostname} {workdir} > "); + } + + #[test] + fn rust_prompt() { + let rust = Rust; + println!("{rust} > "); + } +} diff --git a/src/shell.rs b/src/shell.rs index c17a924f..18182628 100644 --- a/src/shell.rs +++ b/src/shell.rs @@ -8,6 +8,7 @@ use std::{ }; use anyhow::anyhow; +use reedline::Prompt; use crate::{ alias::Alias, @@ -16,7 +17,7 @@ use crate::{ env::Env, history::History, parser, - prompt::CustomPrompt, + prompt::{CustomPrompt, PromptWrapper, SimplePrompt}, signal::sig_handler, }; @@ -48,11 +49,20 @@ impl Default for Hooks { } } -#[derive(Default)] pub struct Shell { pub hooks: Hooks, pub builtins: Builtins, - pub prompt: CustomPrompt, + pub prompt: Box, +} + +impl Default for Shell { + fn default() -> Self { + Shell { + hooks: Hooks::default(), + builtins: Builtins::default(), + prompt: Box::new(PromptWrapper(SimplePrompt)), + } + } } // Runtime context for the shell @@ -93,7 +103,7 @@ impl Shell { loop { // (self.hooks.prompt_command)(); - let sig = line_editor.read_line(&self.prompt); + let sig = line_editor.read_line(&*self.prompt); let line = match sig { Ok(Signal::Success(buffer)) => buffer, x => {