Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

better prompt builder #23

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions design/prompt.md
Original file line number Diff line number Diff line change
@@ -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)

180 changes: 129 additions & 51 deletions src/prompt.rs
Original file line number Diff line number Diff line change
@@ -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<T>(pub T);
// impl<T> std::fmt::Display for ShowSection<T>
// 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<String> {
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<String> {
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<T>(pub T);
impl<T> Prompt for PromptWrapper<T>
where
T: CustomPrompt,
{
fn render_prompt_left(&self) -> Cow<str> {
Cow::Owned(self.prompt_left.clone())
Cow::Owned(self.0.prompt_left())
}

fn render_prompt_right(&self) -> Cow<str> {
Cow::Owned(self.prompt_right.clone())
Cow::Owned(self.0.prompt_right())
}

fn render_prompt_indicator(&self, _edit_mode: PromptEditMode) -> Cow<str> {
Cow::Owned(self.prompt_indicator.clone())
Cow::Owned(self.0.prompt_indicator())
}

fn render_prompt_multiline_indicator(&self) -> Cow<str> {
Cow::Borrowed(&self.multiline_indicator)
Cow::Owned(self.0.multiline_indicator())
}

fn render_prompt_history_search_indicator(
Expand All @@ -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} > ");
}
}
18 changes: 14 additions & 4 deletions src/shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use std::{
};

use anyhow::anyhow;
use reedline::Prompt;

use crate::{
alias::Alias,
Expand All @@ -16,7 +17,7 @@ use crate::{
env::Env,
history::History,
parser,
prompt::CustomPrompt,
prompt::{CustomPrompt, PromptWrapper, SimplePrompt},
signal::sig_handler,
};

Expand Down Expand Up @@ -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<dyn Prompt>,
}

impl Default for Shell {
fn default() -> Self {
Shell {
hooks: Hooks::default(),
builtins: Builtins::default(),
prompt: Box::new(PromptWrapper(SimplePrompt)),
}
}
}

// Runtime context for the shell
Expand Down Expand Up @@ -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 => {
Expand Down