use omp_core::{Str, fmts};
use omp_tui::{
Color, Dim, Key, Layer, Mouse, OverlayAnchor, OverlayOptions, Prop, Size, Ui, UiContext,
UiEvent, dom,
};
use crate::demo::demo_commands;
const CYAN: Color = Color::Rgb(62, 190, 203);
const TEXT: Color = Color::Rgb(194, 198, 204);
const DIM: Color = Color::Rgb(110, 116, 124);
const HINT: &str = "↑/↓ commands · Enter run · type to search · Esc close";
const FRAME_ROWS: u16 = 4;
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum PaletteEvent {
Consumed,
Close,
Run(PaletteAction),
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum PaletteAction {
SwitchModel,
ToggleSidebar,
Quit,
Insert(Str),
}
const SWITCH_MODEL: &str = "switch-model";
const TOGGLE_SIDEBAR: &str = "toggle-sidebar";
const QUIT: &str = "quit";
pub struct CommandPalette {
ui: Ui,
ctx: UiContext,
options: OverlayOptions,
query: Str,
rows: u16,
}
impl CommandPalette {
pub fn open(ctx: &UiContext) -> Self {
let options = OverlayOptions::default()
.anchor(OverlayAnchor::Top)
.offset_y(1)
.z(10);
Self { ui: build("", 8, 100, ctx), ctx: ctx.clone(), options, query: Str::default(), rows: 8 }
}
pub fn handle_key(&mut self, key: Key) -> PaletteEvent {
let event = self.ui.handle_key(key);
self.route(event)
}
pub fn handle_paste(&mut self, text: &str) -> PaletteEvent {
let event = self.ui.handle_paste(text);
self.route(event)
}
pub fn handle_mouse(&mut self, col: u16, row: u16, kind: Mouse, viewport: Size) -> PaletteEvent {
match self
.ui
.handle_mouse_as_layer(&self.options, viewport, col, row, kind)
{
Some(event) => self.route(event),
None if kind == Mouse::Click => PaletteEvent::Close,
None => PaletteEvent::Consumed,
}
}
pub fn layer(&mut self, viewport: Size) -> Layer<'_> {
let width = (viewport.width * 3 / 5).max(48).min(viewport.width);
let rows = (viewport.height / 2).saturating_sub(FRAME_ROWS).max(5);
if rows != self.rows {
self.rows = rows;
self
.ui
.set_prop("commands", Prop::H, rows.saturating_add(1));
}
if self.ui.frame().size().width != width {
self.ui = build(&self.query, self.rows, width, &self.ctx);
}
self.options = self.options.width(Dim::Cells(width));
Layer { frame: self.ui.frame(), options: &self.options, active: true }
}
fn route(&mut self, event: UiEvent) -> PaletteEvent {
match event {
UiEvent::Cancel => PaletteEvent::Close,
UiEvent::Changed { value, .. } => match value.as_str() {
SWITCH_MODEL => PaletteEvent::Run(PaletteAction::SwitchModel),
TOGGLE_SIDEBAR => PaletteEvent::Run(PaletteAction::ToggleSidebar),
QUIT => PaletteEvent::Run(PaletteAction::Quit),
slash => PaletteEvent::Run(PaletteAction::Insert(fmts!("{slash} "))),
},
UiEvent::Filtered { query, .. } => {
self.query = query;
PaletteEvent::Consumed
},
UiEvent::None | UiEvent::Submit | UiEvent::Highlighted { .. } | UiEvent::Pressed(_) => {
PaletteEvent::Consumed
},
}
}
}
struct EntrySpec {
value: Str,
label: Str,
name: Str,
name_fg: Color,
detail: Str,
key: Str,
}
impl EntrySpec {
const fn action(
value: &'static str,
name: &'static str,
detail: &'static str,
key: &'static str,
) -> Self {
Self {
value: Str::new_static(value),
label: Str::new_static(name),
name: Str::new_static(name),
name_fg: TEXT,
detail: Str::new_static(detail),
key: Str::new_static(key),
}
}
}
fn entries() -> Vec<EntrySpec> {
let commands = demo_commands();
let mut list = Vec::with_capacity(commands.len() + 3);
list.push(EntrySpec::action(
SWITCH_MODEL,
"Switch Model",
"Pick the model for this session",
"ctrl+p",
));
list.push(EntrySpec::action(
TOGGLE_SIDEBAR,
"Toggle Sidebar",
"Show or hide the session rail",
"ctrl+b",
));
list.push(EntrySpec::action(QUIT, "Quit", "Exit the demo", "ctrl+c"));
list.extend(commands.iter().map(|command| {
let name = fmts!("/{}", command.name());
EntrySpec {
value: name.clone(),
label: name.clone(),
name,
name_fg: CYAN,
detail: Str::from(command.description()),
key: Str::default(),
}
}));
list
}
fn build(query: &str, rows: u16, width: u16, ctx: &UiContext) -> Ui {
let list = entries();
let seed = Str::from(query);
let height = rows.saturating_add(1);
Ui::from_root(
dom! {
<box border=round title="Commands" pad-x=1>
<col>
<select id="commands" filter={seed} h={height}>
for entry in list {
<option value={entry.value} label={entry.label}>
<td><pre fg={entry.name_fg}>{entry.name}</pre></td>
<td truncate grow><pre fg={DIM}>{entry.detail}</pre></td>
if !entry.key.is_empty() {
<td align=end><pre fg={DIM}>{entry.key}</pre></td>
}
</option>
}
</select>
<text dim truncate>{HINT}</text>
</col>
</box>
},
width,
ctx.clone(),
)
}