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

refactor(gdtk-cli, cliui): Better version resolving, introduce cliui. #106

Merged
merged 21 commits into from
Sep 25, 2024
Merged
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
1,815 changes: 763 additions & 1,052 deletions Cargo.lock

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ strip = false
debug = true

[patch.crates-io]
# Contains unstable fixes that we need.
logos = { git = "https://github.com/elenakrittik/logos", rev = "d79f8e736239fb5e6b972a54463095d44bc494bc" }
# This fork of logos includes some unstable fixes that we need.
logos = { git = "https://github.com/elenakrittik/logos", rev = "d835bae8e28829710f44fcc679770c3f0ada87d3" }
# logos = { path = "../logos" } # you didn't see this, okay?
# ureq 3.x, currently unreleased. Much better than 2.x.
ureq = { git = "https://github.com/algesten/ureq", rev = "bc6665047304bb839193f39ece4021d3981cdcc8" }
18 changes: 18 additions & 0 deletions crates/cliui/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "cliui"
description = "Good-looking prompts and whatnot for your command-line applications."
version = "0.1.0"
edition = "2021"
authors = ["Elena Krittik <dev.elenakrittik@gmail.com>"]
keywords = ["cli", "menu", "prompt"]
categories = ["command-line-interface"]
license = "MIT"
homepage = "https://github.com/elenakrittik/gdtk/tree/master/crates/cliui"
repository = "https://github.com/elenakrittik/gdtk/tree/master/crates/cliui"
documentation = "https://docs.rs/cliui"

[dependencies]
console = "0.15.0"
thiserror = "1.0.61"
yansi = "1.0.1"
ahash = "0.8.11"
34 changes: 34 additions & 0 deletions crates/cliui/examples/basic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use cliui::{
prompt::{Action, Key},
Prompt,
};

struct MyState {
only_3x: bool,
}

fn main() -> cliui::Result<()> {
const ITEMS: [&str; 7] = ["4.3", "4.2", "4.1", "4.0", "3.6", "3.5", "3.4"];

Prompt::builder()
.with_question("Choose Godot version")
.with_items(ITEMS.into_iter().collect::<Vec<_>>())
.with_state(MyState { only_3x: false })
.with_action(
Key::Char('b'),
Action {
description: "Toggle something idk",
callback: |prompt| {
prompt.state.only_3x = !prompt.state.only_3x;

// TODO: implement filtering

Ok(())
},
},
)
.build()
.interact()?;

Ok(())
}
File renamed without changes.
9 changes: 9 additions & 0 deletions crates/cliui/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//! Good-looking prompts and whatnot for your command-line applications.

#![feature(type_changing_struct_update, int_roundings)]

pub use crate::error::{Error, Result};
pub use crate::prompt::{Action, Key, Prompt};

mod error;
pub mod prompt;
187 changes: 187 additions & 0 deletions crates/cliui/src/prompt.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
use std::{
fmt::{Display, Write},
io::Write as IOWrite,
};

use ahash::AHashMap;
use console::Term;
use yansi::Paint;

use crate::prompt::vecview::VecView;

mod action;
mod builders;
mod sentinel;
mod vecview;

const QUESTION_MARK_STYLE: yansi::Style = yansi::Style::new().bright_yellow().bold();
const ARROW_STYLE: yansi::Style = yansi::Style::new().bright_green().bold();
const ACTION_STYLE: yansi::Style = yansi::Style::new().bright_black().bold().dim();
const CHOICE_STYLE: yansi::Style = ARROW_STYLE;
const NO_CHOICE_STYLE: yansi::Style = yansi::Style::new().bright_red().bold();

pub use action::Action;
pub use console::Key;

/// A prompt.
pub struct Prompt<Item, State> {
question: &'static str,
term: Term,
allow_esc: bool,
view: VecView<Item>,
pub actions: AHashMap<Key, Action<Item, State>>,
pub state: State,
}

impl<Item: Display, State> Prompt<Item, State> {
pub fn interact(mut self) -> crate::Result<(Option<Item>, State)> {
let mut choice = None;

self.term.hide_cursor()?;
self.draw_question()?;

let mut lines_previously_drawn = self.draw_items()?;

loop {
match self.term.read_key()? {
Key::ArrowUp => {
self.view.move_up();
self.term.clear_last_lines(lines_previously_drawn)?;

lines_previously_drawn = self.draw_items()?;
}
Key::ArrowDown => {
self.view.move_down();
self.term.clear_last_lines(lines_previously_drawn)?;

lines_previously_drawn = self.draw_items()?;
}
Key::Enter => {
choice.replace(self.view.current_idx);
break;
}
Key::Escape if self.allow_esc => break,
other => {
if let Some(action) = self.actions.get(&other) {
(action.callback)(&mut self)?;
}

self.term.clear_last_lines(lines_previously_drawn)?;
lines_previously_drawn = self.draw_items()?;
}
}
}

self.term.clear_last_lines(lines_previously_drawn)?;
self.term.clear_last_lines(1)?;
self.draw_choice(choice)?;
self.term.show_cursor()?;

let item = choice.map(|idx| self.view.items.swap_remove(idx));

Ok((item, self.state))
}

fn draw_question(&mut self) -> crate::Result<usize> {
writeln!(
self.term,
"{} {}",
'?'.paint(QUESTION_MARK_STYLE),
self.question
)?;

Ok(1)
}

fn draw_items(&mut self) -> crate::Result<usize> {
let view = self.view.view();
let mut idx = self.view.range_start();

for item in view.items {
let arrow = if idx == self.view.current_idx {
">"
} else {
" "
};

writeln!(self.term, "{} {}", arrow.paint(ARROW_STYLE), item)?;

idx += 1;
}

let lines_drawn = view.items.len() + self.draw_actions()?;

Ok(lines_drawn)
}

fn draw_choice(&mut self, choice: Option<usize>) -> crate::Result {
if let Some(choice) = choice {
writeln!(
self.term,
"{} {}: {}",
'?'.paint(CHOICE_STYLE),
self.question,
&self.view.items[choice],
)?;
} else {
writeln!(
self.term,
"{} {}",
'x'.paint(NO_CHOICE_STYLE),
self.question,
)?;
}

Ok(())
}

fn draw_actions(&mut self) -> crate::Result<usize> {
writeln!(self.term)?;

for (key, action) in &self.actions {
writeln!(
self.term,
"{}{}{} {}",
'['.paint(ACTION_STYLE),
display_key(key).paint(ACTION_STYLE),
']'.paint(ACTION_STYLE),
action.description.paint(ACTION_STYLE)
)?;
}

Ok(self.actions.len() + 1)
}
}

fn display_key(key: &console::Key) -> impl Display + '_ {
struct DisplayKey<'a>(&'a console::Key);

impl Display for DisplayKey<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.0 {
Key::ArrowLeft => todo!(),
Key::ArrowRight => todo!(),
Key::ArrowUp => todo!(),
Key::ArrowDown => todo!(),
Key::Enter => todo!(),
Key::Escape => todo!(),
Key::Backspace => todo!(),
Key::Home => todo!(),
Key::End => todo!(),
Key::Tab => todo!(),
Key::BackTab => todo!(),
Key::Alt => todo!(),
Key::Del => todo!(),
Key::Shift => todo!(),
Key::Insert => todo!(),
Key::PageUp => todo!(),
Key::PageDown => todo!(),
Key::Char(c) => f.write_char(c.to_ascii_uppercase()),
Key::CtrlC => f.write_str("Ctrl-C"),
_ => f.write_str("unknown"),
}
}
}

DisplayKey(key)
}
9 changes: 9 additions & 0 deletions crates/cliui/src/prompt/action.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
use crate::Prompt;

type ActionCallback<Item, State> = fn(&mut Prompt<Item, State>) -> crate::Result;

/// An action of a [Prompt].
pub struct Action<Item, State> {
pub description: &'static str,
pub callback: ActionCallback<Item, State>,
}
Loading