Skip to content

Commit

Permalink
chore: add keymap help
Browse files Browse the repository at this point in the history
  • Loading branch information
covercash2 committed Nov 26, 2024
1 parent f2ef84a commit 86175c3
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 34 deletions.
1 change: 1 addition & 0 deletions ollama-cli/default_keymap.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ b = "left_word"
enter = "enter"
backspace = "left"
space = "popup"
"?" = "help"

[edit]
esc = "escape"
Expand Down
9 changes: 3 additions & 6 deletions ollama-cli/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use ollama_rs::error::OllamaError;
use thiserror::Error;
use tokio::sync::mpsc::error::SendError;

use crate::lm::Response;
use crate::{lm::Response, tui::event::InputMode};

pub type Result<T> = std::result::Result<T, Error>;

Expand All @@ -17,11 +17,8 @@ pub enum Error {
path: PathBuf,
},

#[error("error in linemux: {source}")]
LineMux {
source: std::io::Error,
msg: &'static str,
},
#[error("keymap should have all modes defined by default. missing {0}")]
MissingKeymap(InputMode),

#[error("error indexing collection at index {index}: {msg}")]
BadIndex { index: usize, msg: &'static str },
Expand Down
27 changes: 21 additions & 6 deletions ollama-cli/src/tui/event/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ use std::collections::HashMap;
use crossterm::event::{Event, KeyCode, KeyEvent};
use keymap::KeyMap;
use serde::{Deserialize, Serialize};
use strum::EnumIter;

const DEFAULTS: &str = include_str!("../../../default_keymap.toml");

#[derive(Debug, PartialEq, Deserialize)]
pub struct EventProcessor {
input_mode: InputMode,
definitions: EventDefinitions,
pub input_mode: InputMode,
pub definitions: EventDefinitions,
}

impl EventProcessor {
Expand All @@ -19,6 +20,7 @@ impl EventProcessor {
definitions,
}
}

pub fn input_mode(&mut self, input_mode: InputMode) {
self.input_mode = input_mode;
}
Expand Down Expand Up @@ -47,7 +49,19 @@ impl EventProcessor {
}
}

#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[derive(
Default,
Debug,
Clone,
Copy,
PartialEq,
Eq,
Hash,
Serialize,
Deserialize,
EnumIter,
strum::Display,
)]
#[serde(rename_all = "snake_case")]
pub enum InputMode {
#[default]
Expand All @@ -56,7 +70,7 @@ pub enum InputMode {
}

#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct EventDefinitions(HashMap<InputMode, ActionMap>);
pub struct EventDefinitions(pub HashMap<InputMode, ActionMap>);

impl Default for EventDefinitions {
fn default() -> Self {
Expand All @@ -80,9 +94,9 @@ impl From<Action> for ActionDefinition {
}

#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct ActionMap(HashMap<KeyMap, Action>);
pub struct ActionMap(pub HashMap<KeyMap, Action>);

#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, strum::Display)]
#[serde(rename_all = "snake_case")]
pub enum Action {
Beginning,
Expand All @@ -96,6 +110,7 @@ pub enum Action {
RightWord,
Refresh,
Popup,
Help,
Enter,
Escape,
Backspace,
Expand Down
16 changes: 10 additions & 6 deletions ollama-cli/src/tui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,11 @@ impl AppContext {
}
}
AppEvent::Deactivate => {
self.view = View::Nav(Default::default());
if self.popup.is_some() {
self.popup = None;
} else {
self.view = View::Nav(Default::default());
}
Ok(true)
}
AppEvent::InputMode(input_mode) => {
Expand All @@ -195,16 +199,16 @@ impl AppContext {
async fn handle_input(&mut self, event: Event) -> anyhow::Result<Option<AppEvent>> {
let action = self.event_processor.process(event);

if let Some(_popup) = &self.popup {
if action == Action::Popup {
self.popup = None;
}
return Ok(None);
if let Some(ref mut popup) = self.popup {
return Ok(popup.handle_action(action)?);
}

if action == Action::Popup {
self.popup = Some(PopupViewModel::log_popup(&self.config.log_file)?);
Ok(None)
} else if action == Action::Help {
self.popup = Some(PopupViewModel::keymap_popup(&self.event_processor));
Ok(None)
} else {
let app_event = match &mut self.view {
View::Chat(ref mut chat_view_model) => {
Expand Down
140 changes: 124 additions & 16 deletions ollama-cli/src/tui/popup.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,69 @@
use std::path::Path;
use std::{path::Path, sync::Arc};

use itertools::Itertools;
use ratatui::{
layout::{Constraint, Flex, Layout, Rect},
style::Style,
widgets::{Block, Clear, Paragraph, Wrap},
widgets::{Block, Clear, Padding, Paragraph, Wrap},
Frame,
};
use strum::IntoEnumIterator as _;

use crate::{
error::{Error, Result},
fs_ext::read_file_to_string,
tui::event::InputMode,
};

use super::{
event::{Action, EventProcessor},
AppEvent,
};

#[derive(Debug, Clone)]
pub struct PopupViewModel {
title: String,
content: String,
content: PopupContent,
scroll_offset: u16,
}

#[derive(Debug, Clone)]
pub struct PopupContent {
columns: Arc<[String]>,
}

impl<S: AsRef<str>> From<S> for PopupContent {
fn from(value: S) -> Self {
PopupContent {
columns: [value.as_ref().to_string()].into(),
}
}
}

impl<S: AsRef<str>> FromIterator<S> for PopupContent {
fn from_iter<T: IntoIterator<Item = S>>(iter: T) -> Self {
let columns: Arc<[String]> = iter.into_iter().map(|s| s.as_ref().into()).collect();

Self { columns }
}
}

impl PopupContent {
pub fn line_count(&self) -> usize {
self.columns
.iter()
.map(|column| column.lines().count())
.max()
.expect("should have a non-zero number of columns")
}
}

impl PopupViewModel {
pub fn new(title: impl ToString, content: impl ToString) -> Self {
pub fn new(title: impl ToString, content: impl Into<PopupContent>) -> Self {
PopupViewModel {
title: title.to_string(),
content: content.to_string(),
content: content.into(),
scroll_offset: 0,
}
}

Expand All @@ -32,10 +72,64 @@ impl PopupViewModel {

let content = logs.lines().rev().take(10).join("\n");

Ok(PopupViewModel {
title: "logs".to_string(),
content,
})
Ok(PopupViewModel::new("logs".to_string(), content))
}

pub fn keymap_popup(event_processor: &EventProcessor) -> Self {
let keymaps = &event_processor.definitions.0;
let keymap_help: PopupContent = InputMode::iter()
.map(|mode| {
let keymap = keymaps
.get(&mode)
.ok_or(Error::MissingKeymap(mode))
.expect("should be able to get the keymap");

std::iter::once(mode.to_string())
.chain(
keymap
.0
.iter()
.map(|(key, action)| format!("{key}: {action}")),
)
.join("\n")
})
.collect();

PopupViewModel::new("help", keymap_help)
}

fn max_scroll(&self) -> u16 {
(self.content.line_count())
.try_into()
.expect("should be able to fit popup content into u16")
}

pub fn handle_action(&mut self, action: Action) -> Result<Option<AppEvent>> {
match action {
Action::Up => {
self.scroll_offset = self.scroll_offset.saturating_sub(1);
Ok(None)
}
Action::Down => {
self.scroll_offset = self.scroll_offset.saturating_add(1);
if self.scroll_offset > self.max_scroll() {
self.scroll_offset = self.max_scroll();
}
Ok(None)
}
Action::Beginning => {
self.scroll_offset = 0;
Ok(None)
}
Action::End => {
self.scroll_offset = self.max_scroll();
Ok(None)
}
Action::Popup | Action::Help | Action::Quit | Action::Enter | Action::Escape => {
Ok(Some(AppEvent::Deactivate))
}
_ => Ok(None),
}
}
}

Expand All @@ -52,14 +146,28 @@ fn popup_area(parent: Rect, percent_x: u16, percent_y: u16) -> Rect {
#[extend::ext(name = PopupView)]
pub impl<'a> Frame<'a> {
fn popup(&mut self, parent: Rect, style: Style, view_model: &mut PopupViewModel) {
let block = Block::bordered().title("popup");
let content = Paragraph::new(view_model.content.as_str())
.wrap(Wrap { trim: true })
.block(block)
.style(style);

let area = popup_area(parent, 60, 60);
self.render_widget(Clear, area);
self.render_widget(content, area);

let block = Block::bordered().title(view_model.title.as_str());

let layout = Layout::horizontal(
view_model
.content
.columns
.iter()
.map(|_| Constraint::Fill(1)),
);

for (i, column_area) in layout.split(area).iter().enumerate() {
let content = Paragraph::new(view_model.content.columns[i].as_str())
.wrap(Wrap { trim: true })
.scroll((view_model.scroll_offset, 0))
.block(Block::new().padding(Padding::proportional(2)))
.style(style);
self.render_widget(content, *column_area);
}

self.render_widget(block, area);
}
}

0 comments on commit 86175c3

Please sign in to comment.