Show available key bindings with F1/?
This commit is contained in:
parent
a51bb60342
commit
6c1ce49236
6 changed files with 355 additions and 87 deletions
82
src/ui.rs
82
src/ui.rs
|
|
@ -5,6 +5,7 @@ mod rooms;
|
||||||
mod util;
|
mod util;
|
||||||
mod widgets;
|
mod widgets;
|
||||||
|
|
||||||
|
use std::convert::Infallible;
|
||||||
use std::sync::{Arc, Weak};
|
use std::sync::{Arc, Weak};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
|
@ -20,8 +21,10 @@ use crate::vault::Vault;
|
||||||
|
|
||||||
pub use self::chat::ChatMsg;
|
pub use self::chat::ChatMsg;
|
||||||
use self::chat::ChatState;
|
use self::chat::ChatState;
|
||||||
use self::input::{key, KeyEvent};
|
use self::input::{key, KeyBindingsList, KeyEvent};
|
||||||
use self::rooms::Rooms;
|
use self::rooms::Rooms;
|
||||||
|
use self::widgets::layer::Layer;
|
||||||
|
use self::widgets::list::ListState;
|
||||||
use self::widgets::BoxedWidget;
|
use self::widgets::BoxedWidget;
|
||||||
|
|
||||||
/// Time to spend batch processing events before redrawing the screen.
|
/// Time to spend batch processing events before redrawing the screen.
|
||||||
|
|
@ -50,6 +53,7 @@ pub struct Ui {
|
||||||
|
|
||||||
rooms: Rooms,
|
rooms: Rooms,
|
||||||
log_chat: ChatState<LogMsg, Logger>,
|
log_chat: ChatState<LogMsg, Logger>,
|
||||||
|
key_bindings_list: Option<ListState<Infallible>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Ui {
|
impl Ui {
|
||||||
|
|
@ -85,6 +89,7 @@ impl Ui {
|
||||||
mode: Mode::Main,
|
mode: Mode::Main,
|
||||||
rooms: Rooms::new(vault, event_tx.clone()),
|
rooms: Rooms::new(vault, event_tx.clone()),
|
||||||
log_chat: ChatState::new(logger),
|
log_chat: ChatState::new(logger),
|
||||||
|
key_bindings_list: None,
|
||||||
};
|
};
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
e = ui.run_main(terminal, event_rx, crossterm_lock) => Ok(e),
|
e = ui.run_main(terminal, event_rx, crossterm_lock) => Ok(e),
|
||||||
|
|
@ -183,9 +188,34 @@ impl Ui {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn widget(&mut self) -> BoxedWidget {
|
async fn widget(&mut self) -> BoxedWidget {
|
||||||
match self.mode {
|
let widget = match self.mode {
|
||||||
Mode::Main => self.rooms.widget().await,
|
Mode::Main => self.rooms.widget().await,
|
||||||
Mode::Log => self.log_chat.widget(String::new()).into(),
|
Mode::Log => self.log_chat.widget(String::new()).into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(key_bindings_list) = &self.key_bindings_list {
|
||||||
|
let mut bindings = KeyBindingsList::new(key_bindings_list);
|
||||||
|
self.list_key_bindings(&mut bindings).await;
|
||||||
|
Layer::new(vec![widget, bindings.widget()]).into()
|
||||||
|
} else {
|
||||||
|
widget
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_key_bindings(&mut self) {
|
||||||
|
if self.key_bindings_list.is_none() {
|
||||||
|
self.key_bindings_list = Some(ListState::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_key_bindings(&self, bindings: &mut KeyBindingsList) {
|
||||||
|
bindings.binding("ctrl+c", "quit cove");
|
||||||
|
bindings.binding("F1, ?", "show this menu");
|
||||||
|
bindings.binding("F12", "toggle log");
|
||||||
|
bindings.empty();
|
||||||
|
match self.mode {
|
||||||
|
Mode::Main => self.rooms.list_key_bindings(bindings).await,
|
||||||
|
Mode::Log => self.log_chat.list_key_bindings(bindings, false).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -195,26 +225,58 @@ impl Ui {
|
||||||
terminal: &mut Terminal,
|
terminal: &mut Terminal,
|
||||||
crossterm_lock: &Arc<FairMutex<()>>,
|
crossterm_lock: &Arc<FairMutex<()>>,
|
||||||
) -> EventHandleResult {
|
) -> EventHandleResult {
|
||||||
match event {
|
if let key!(Ctrl + 'c') = event {
|
||||||
// Exit unconditionally on ctrl+c. Previously, shift+q would also
|
// Exit unconditionally on ctrl+c. Previously, shift+q would also
|
||||||
// unconditionally exit, but that interfered with typing text in
|
// unconditionally exit, but that interfered with typing text in
|
||||||
// inline editors.
|
// inline editors.
|
||||||
key!(Ctrl + 'c') => return EventHandleResult::Stop,
|
return EventHandleResult::Stop;
|
||||||
key!(F 1) => self.mode = Mode::Main,
|
}
|
||||||
key!(F 2) => self.mode = Mode::Log,
|
|
||||||
|
// Key bindings list overrides any other bindings if visible
|
||||||
|
if let Some(key_bindings_list) = &mut self.key_bindings_list {
|
||||||
|
match event {
|
||||||
|
key!(Esc) | key!(F 1) | key!('?') => self.key_bindings_list = None,
|
||||||
|
key!('k') | key!(Up) => key_bindings_list.scroll_up(1),
|
||||||
|
key!('j') | key!(Down) => key_bindings_list.scroll_down(1),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
return EventHandleResult::Continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match event {
|
||||||
|
key!(F 1) => {
|
||||||
|
self.key_bindings_list = Some(ListState::new());
|
||||||
|
return EventHandleResult::Continue;
|
||||||
|
}
|
||||||
|
key!(F 12) => {
|
||||||
|
self.mode = match self.mode {
|
||||||
|
Mode::Main => Mode::Log,
|
||||||
|
Mode::Log => Mode::Main,
|
||||||
|
};
|
||||||
|
return EventHandleResult::Continue;
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
match self.mode {
|
let handled = match self.mode {
|
||||||
Mode::Main => {
|
Mode::Main => {
|
||||||
self.rooms
|
self.rooms
|
||||||
.handle_key_event(terminal, crossterm_lock, event)
|
.handle_key_event(terminal, crossterm_lock, event)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
Mode::Log => {
|
Mode::Log => self
|
||||||
self.log_chat
|
.log_chat
|
||||||
.handle_key_event(terminal, crossterm_lock, event, false)
|
.handle_key_event(terminal, crossterm_lock, event, false)
|
||||||
.await;
|
.await
|
||||||
|
.handled(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pressing '?' should only open the key bindings list if it doesn't
|
||||||
|
// interfere with any part of the main UI, such as entering text in a
|
||||||
|
// text editor.
|
||||||
|
if !handled {
|
||||||
|
if let key!('?') = event {
|
||||||
|
self.show_key_bindings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ use crate::store::{Msg, MsgStore};
|
||||||
|
|
||||||
use self::tree::{TreeView, TreeViewState};
|
use self::tree::{TreeView, TreeViewState};
|
||||||
|
|
||||||
use super::input::KeyEvent;
|
use super::input::{KeyBindingsList, KeyEvent};
|
||||||
use super::widgets::Widget;
|
use super::widgets::Widget;
|
||||||
|
|
||||||
///////////
|
///////////
|
||||||
|
|
@ -84,6 +84,12 @@ impl<M: Msg> Reaction<M> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<M: Msg, S: MsgStore<M>> ChatState<M, S> {
|
impl<M: Msg, S: MsgStore<M>> ChatState<M, S> {
|
||||||
|
pub async fn list_key_bindings(&self, bindings: &mut KeyBindingsList, can_compose: bool) {
|
||||||
|
match self.mode {
|
||||||
|
Mode::Tree => self.tree.list_key_bindings(bindings, can_compose).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn handle_key_event(
|
pub async fn handle_key_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
terminal: &mut Terminal,
|
terminal: &mut Terminal,
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ use toss::frame::{Frame, Pos, Size};
|
||||||
use toss::terminal::Terminal;
|
use toss::terminal::Terminal;
|
||||||
|
|
||||||
use crate::store::{Msg, MsgStore};
|
use crate::store::{Msg, MsgStore};
|
||||||
use crate::ui::input::{key, KeyEvent};
|
use crate::ui::input::{key, KeyBindingsList, KeyEvent};
|
||||||
use crate::ui::widgets::editor::EditorState;
|
use crate::ui::widgets::editor::EditorState;
|
||||||
use crate::ui::widgets::Widget;
|
use crate::ui::widgets::Widget;
|
||||||
|
|
||||||
|
|
@ -59,6 +59,107 @@ impl<M: Msg, S: MsgStore<M>> InnerTreeViewState<M, S> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn list_movement_key_bindings(&self, bindings: &mut KeyBindingsList) {
|
||||||
|
bindings.binding("j/k, ↓/↑", "move cursor up/down");
|
||||||
|
bindings.binding("h/l, ←/→", "move cursor chronologically");
|
||||||
|
bindings.binding("g, home", "move cursor to top");
|
||||||
|
bindings.binding("G, end", "move cursor to bottom");
|
||||||
|
bindings.binding("ctrl+y/e", "scroll up/down a line");
|
||||||
|
bindings.binding("ctrl+u/d", "scroll up/down half a screen");
|
||||||
|
bindings.binding("ctrl+b/f", "scroll up/down one screen");
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_movement_key_event(&mut self, frame: &mut Frame, event: KeyEvent) -> bool {
|
||||||
|
let chat_height = frame.size().height - 3;
|
||||||
|
|
||||||
|
match event {
|
||||||
|
key!('k') | key!(Up) => self.move_cursor_up().await,
|
||||||
|
key!('j') | key!(Down) => self.move_cursor_down().await,
|
||||||
|
key!('h') | key!(Left) => self.move_cursor_older().await,
|
||||||
|
key!('l') | key!(Right) => self.move_cursor_newer().await,
|
||||||
|
key!('g') | key!(Home) => self.move_cursor_to_top().await,
|
||||||
|
key!('G') | key!(End) => self.move_cursor_to_bottom().await,
|
||||||
|
key!(Ctrl + 'y') => self.scroll_up(1),
|
||||||
|
key!(Ctrl + 'e') => self.scroll_down(1),
|
||||||
|
key!(Ctrl + 'u') => self.scroll_up((chat_height / 2).into()),
|
||||||
|
key!(Ctrl + 'd') => self.scroll_down((chat_height / 2).into()),
|
||||||
|
key!(Ctrl + 'b') => self.scroll_up(chat_height.saturating_sub(1).into()),
|
||||||
|
key!(Ctrl + 'f') => self.scroll_down(chat_height.saturating_sub(1).into()),
|
||||||
|
_ => return false,
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_edit_initiating_key_bindings(&self, bindings: &mut KeyBindingsList) {
|
||||||
|
bindings.empty();
|
||||||
|
bindings.binding("r", "reply to message");
|
||||||
|
bindings.binding_ctd("(inline if possible, otherwise directly)");
|
||||||
|
bindings.binding("R", "reply to message (opposite of R)");
|
||||||
|
bindings.binding("t", "start a new thread");
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_edit_initiating_key_event(
|
||||||
|
&mut self,
|
||||||
|
event: KeyEvent,
|
||||||
|
id: Option<M::Id>,
|
||||||
|
) -> bool {
|
||||||
|
match event {
|
||||||
|
key!('r') => {
|
||||||
|
if let Some(parent) = self.parent_for_normal_reply().await {
|
||||||
|
self.cursor = Cursor::editor(id, parent);
|
||||||
|
self.correction = Some(Correction::MakeCursorVisible);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
key!('R') => {
|
||||||
|
if let Some(parent) = self.parent_for_alternate_reply().await {
|
||||||
|
self.cursor = Cursor::editor(id, parent);
|
||||||
|
self.correction = Some(Correction::MakeCursorVisible);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
key!('t') | key!('T') => {
|
||||||
|
self.cursor = Cursor::editor(id, None);
|
||||||
|
self.correction = Some(Correction::MakeCursorVisible);
|
||||||
|
}
|
||||||
|
_ => return false,
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_normal_key_bindings(&self, bindings: &mut KeyBindingsList, can_compose: bool) {
|
||||||
|
self.list_movement_key_bindings(bindings);
|
||||||
|
if can_compose {
|
||||||
|
self.list_edit_initiating_key_bindings(bindings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_normal_key_event(
|
||||||
|
&mut self,
|
||||||
|
frame: &mut Frame,
|
||||||
|
event: KeyEvent,
|
||||||
|
can_compose: bool,
|
||||||
|
id: Option<M::Id>,
|
||||||
|
) -> bool {
|
||||||
|
if self.handle_movement_key_event(frame, event).await {
|
||||||
|
true
|
||||||
|
} else if can_compose {
|
||||||
|
self.handle_edit_initiating_key_event(event, id).await
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_editor_key_bindings(&self, bindings: &mut KeyBindingsList) {
|
||||||
|
bindings.binding("esc", "close editor");
|
||||||
|
bindings.binding("enter", "send message");
|
||||||
|
bindings.binding("←/→", "move cursor left/right");
|
||||||
|
bindings.binding("backspace", "delete before cursor");
|
||||||
|
bindings.binding("delete", "delete after cursor");
|
||||||
|
bindings.binding("ctrl+e", "edit in $EDITOR");
|
||||||
|
bindings.binding("ctrl+l", "clear editor contents");
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_editor_key_event(
|
fn handle_editor_key_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
terminal: &mut Terminal,
|
terminal: &mut Terminal,
|
||||||
|
|
@ -93,9 +194,9 @@ impl<M: Msg, S: MsgStore<M>> InnerTreeViewState<M, S> {
|
||||||
} => self.editor.insert_char('\n'),
|
} => self.editor.insert_char('\n'),
|
||||||
|
|
||||||
key!(Char ch) => self.editor.insert_char(ch),
|
key!(Char ch) => self.editor.insert_char(ch),
|
||||||
key!(Backspace) => self.editor.backspace(),
|
|
||||||
key!(Left) => self.editor.move_cursor_left(),
|
key!(Left) => self.editor.move_cursor_left(),
|
||||||
key!(Right) => self.editor.move_cursor_right(),
|
key!(Right) => self.editor.move_cursor_right(),
|
||||||
|
key!(Backspace) => self.editor.backspace(),
|
||||||
key!(Delete) => self.editor.delete(),
|
key!(Delete) => self.editor.delete(),
|
||||||
key!(Ctrl + 'e') => self.editor.edit_externally(terminal, crossterm_lock),
|
key!(Ctrl + 'e') => self.editor.edit_externally(terminal, crossterm_lock),
|
||||||
key!(Ctrl + 'l') => self.editor.clear(),
|
key!(Ctrl + 'l') => self.editor.clear(),
|
||||||
|
|
@ -106,69 +207,16 @@ impl<M: Msg, S: MsgStore<M>> InnerTreeViewState<M, S> {
|
||||||
Reaction::Handled
|
Reaction::Handled
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_movement_key_event(&mut self, frame: &mut Frame, event: KeyEvent) -> bool {
|
pub fn list_key_bindings(&self, bindings: &mut KeyBindingsList, can_compose: bool) {
|
||||||
let chat_height = frame.size().height - 3;
|
bindings.heading("Chat");
|
||||||
|
match &self.cursor {
|
||||||
match event {
|
Cursor::Bottom | Cursor::Msg(_) => {
|
||||||
key!('k') | key!(Up) => self.move_cursor_up().await,
|
self.list_normal_key_bindings(bindings, can_compose);
|
||||||
key!('j') | key!(Down) => self.move_cursor_down().await,
|
|
||||||
key!('h') | key!(Left) => self.move_cursor_older().await,
|
|
||||||
key!('l') | key!(Right) => self.move_cursor_newer().await,
|
|
||||||
key!('g') | key!(Home) => self.move_cursor_to_top().await,
|
|
||||||
key!('G') | key!(End) => self.move_cursor_to_bottom().await,
|
|
||||||
key!(Ctrl + 'y') => self.scroll_up(1),
|
|
||||||
key!(Ctrl + 'e') => self.scroll_down(1),
|
|
||||||
key!(Ctrl + 'u') => self.scroll_up((chat_height / 2).into()),
|
|
||||||
key!(Ctrl + 'd') => self.scroll_down((chat_height / 2).into()),
|
|
||||||
key!(Ctrl + 'b') => self.scroll_up(chat_height.saturating_sub(1).into()),
|
|
||||||
key!(Ctrl + 'f') => self.scroll_down(chat_height.saturating_sub(1).into()),
|
|
||||||
_ => return false,
|
|
||||||
}
|
}
|
||||||
|
Cursor::Editor { .. } => self.list_editor_key_bindings(bindings),
|
||||||
true
|
Cursor::Pseudo { .. } => {
|
||||||
|
self.list_movement_key_bindings(bindings);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_edit_initiating_key_event(
|
|
||||||
&mut self,
|
|
||||||
event: KeyEvent,
|
|
||||||
id: Option<M::Id>,
|
|
||||||
) -> bool {
|
|
||||||
match event {
|
|
||||||
key!('r') => {
|
|
||||||
if let Some(parent) = self.parent_for_normal_reply().await {
|
|
||||||
self.cursor = Cursor::editor(id, parent);
|
|
||||||
self.correction = Some(Correction::MakeCursorVisible);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
key!('R') => {
|
|
||||||
if let Some(parent) = self.parent_for_alternate_reply().await {
|
|
||||||
self.cursor = Cursor::editor(id, parent);
|
|
||||||
self.correction = Some(Correction::MakeCursorVisible);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
key!('t') | key!('T') => {
|
|
||||||
self.cursor = Cursor::editor(id, None);
|
|
||||||
self.correction = Some(Correction::MakeCursorVisible);
|
|
||||||
}
|
|
||||||
_ => return false,
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_normal_key_event(
|
|
||||||
&mut self,
|
|
||||||
frame: &mut Frame,
|
|
||||||
event: KeyEvent,
|
|
||||||
can_compose: bool,
|
|
||||||
id: Option<M::Id>,
|
|
||||||
) -> bool {
|
|
||||||
if self.handle_movement_key_event(frame, event).await {
|
|
||||||
true
|
|
||||||
} else if can_compose {
|
|
||||||
self.handle_edit_initiating_key_event(event, id).await
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -254,6 +302,10 @@ impl<M: Msg, S: MsgStore<M>> TreeViewState<M, S> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn list_key_bindings(&self, bindings: &mut KeyBindingsList, can_compose: bool) {
|
||||||
|
self.0.lock().await.list_key_bindings(bindings, can_compose);
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn handle_key_event(
|
pub async fn handle_key_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
terminal: &mut Terminal,
|
terminal: &mut Terminal,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,20 @@
|
||||||
|
use std::convert::Infallible;
|
||||||
|
|
||||||
use crossterm::event::{KeyCode, KeyModifiers};
|
use crossterm::event::{KeyCode, KeyModifiers};
|
||||||
|
use crossterm::style::{ContentStyle, Stylize};
|
||||||
|
use toss::styled::Styled;
|
||||||
|
|
||||||
|
use super::widgets::background::Background;
|
||||||
|
use super::widgets::border::Border;
|
||||||
|
use super::widgets::empty::Empty;
|
||||||
|
use super::widgets::float::Float;
|
||||||
|
use super::widgets::join::{HJoin, Segment};
|
||||||
|
use super::widgets::layer::Layer;
|
||||||
|
use super::widgets::list::{List, ListState};
|
||||||
|
use super::widgets::padding::Padding;
|
||||||
|
use super::widgets::resize::Resize;
|
||||||
|
use super::widgets::text::Text;
|
||||||
|
use super::widgets::BoxedWidget;
|
||||||
|
|
||||||
/// A key event data type that is a bit easier to pattern match on than
|
/// A key event data type that is a bit easier to pattern match on than
|
||||||
/// [`crossterm::event::KeyEvent`].
|
/// [`crossterm::event::KeyEvent`].
|
||||||
|
|
@ -46,3 +62,62 @@ macro_rules! key {
|
||||||
( Alt + $key:ident ) => { KeyEvent { code: KeyCode::$key, shift: false, ctrl: false, alt: true, } };
|
( Alt + $key:ident ) => { KeyEvent { code: KeyCode::$key, shift: false, ctrl: false, alt: true, } };
|
||||||
}
|
}
|
||||||
pub(crate) use key;
|
pub(crate) use key;
|
||||||
|
|
||||||
|
/// Helper wrapper around a list widget for a more consistent key binding style.
|
||||||
|
pub struct KeyBindingsList(List<Infallible>);
|
||||||
|
|
||||||
|
impl KeyBindingsList {
|
||||||
|
pub fn new(state: &ListState<Infallible>) -> Self {
|
||||||
|
Self(state.widget())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn binding_style() -> ContentStyle {
|
||||||
|
ContentStyle::default().cyan()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn widget(self) -> BoxedWidget {
|
||||||
|
let binding_style = Self::binding_style();
|
||||||
|
Float::new(Layer::new(vec![
|
||||||
|
Border::new(Background::new(Padding::new(self.0).horizontal(1))).into(),
|
||||||
|
Float::new(
|
||||||
|
Padding::new(Text::new(
|
||||||
|
Styled::new("jk/↓↑", binding_style)
|
||||||
|
.then_plain(" to scroll, ")
|
||||||
|
.then("esc", binding_style)
|
||||||
|
.then_plain(" to close"),
|
||||||
|
))
|
||||||
|
.horizontal(1),
|
||||||
|
)
|
||||||
|
.horizontal(0.5)
|
||||||
|
.into(),
|
||||||
|
]))
|
||||||
|
.horizontal(0.5)
|
||||||
|
.vertical(0.5)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn empty(&mut self) {
|
||||||
|
self.0.add_unsel(Empty::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn heading(&mut self, name: &str) {
|
||||||
|
self.0
|
||||||
|
.add_unsel(Text::new((name, ContentStyle::default().bold())));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn binding(&mut self, binding: &str, description: &str) {
|
||||||
|
let widget = HJoin::new(vec![
|
||||||
|
Segment::new(Resize::new(Text::new((binding, Self::binding_style()))).min_width(16)),
|
||||||
|
Segment::new(Text::new(description)),
|
||||||
|
]);
|
||||||
|
self.0.add_unsel(widget);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn binding_ctd(&mut self, description: &str) {
|
||||||
|
let widget = HJoin::new(vec![
|
||||||
|
Segment::new(Resize::new(Empty::new()).min_width(16)),
|
||||||
|
Segment::new(Text::new(description)),
|
||||||
|
]);
|
||||||
|
self.0.add_unsel(widget);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ use crate::euph::{self, Joined, Status};
|
||||||
use crate::vault::EuphVault;
|
use crate::vault::EuphVault;
|
||||||
|
|
||||||
use super::chat::{ChatState, Reaction};
|
use super::chat::{ChatState, Reaction};
|
||||||
use super::input::{key, KeyEvent};
|
use super::input::{key, KeyBindingsList, KeyEvent};
|
||||||
use super::widgets::background::Background;
|
use super::widgets::background::Background;
|
||||||
use super::widgets::border::Border;
|
use super::widgets::border::Border;
|
||||||
use super::widgets::editor::EditorState;
|
use super::widgets::editor::EditorState;
|
||||||
|
|
@ -302,6 +302,37 @@ impl EuphRoom {
|
||||||
list.into()
|
list.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn list_key_bindings(&self, bindings: &mut KeyBindingsList) {
|
||||||
|
bindings.heading("Room");
|
||||||
|
|
||||||
|
match &self.state {
|
||||||
|
State::Normal => {
|
||||||
|
// TODO Use if-let chain
|
||||||
|
bindings.binding("esc", "leave room");
|
||||||
|
let can_compose = if let Some(room) = &self.room {
|
||||||
|
if let Ok(Some(Status::Joined(_))) = room.status().await {
|
||||||
|
bindings.binding("n", "change nick");
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
bindings.empty();
|
||||||
|
self.chat.list_key_bindings(bindings, can_compose).await;
|
||||||
|
}
|
||||||
|
State::ChooseNick(_) => {
|
||||||
|
bindings.binding("esc", "abort");
|
||||||
|
bindings.binding("enter", "set nick");
|
||||||
|
bindings.binding("←/→", "move cursor left/right");
|
||||||
|
bindings.binding("backspace", "delete before cursor");
|
||||||
|
bindings.binding("delete", "delete after cursor");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn handle_key_event(
|
pub async fn handle_key_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
terminal: &mut Terminal,
|
terminal: &mut Terminal,
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ use crate::euph::api::SessionType;
|
||||||
use crate::euph::{Joined, Status};
|
use crate::euph::{Joined, Status};
|
||||||
use crate::vault::Vault;
|
use crate::vault::Vault;
|
||||||
|
|
||||||
use super::input::{key, KeyEvent};
|
use super::input::{key, KeyBindingsList, KeyEvent};
|
||||||
use super::room::EuphRoom;
|
use super::room::EuphRoom;
|
||||||
use super::widgets::background::Background;
|
use super::widgets::background::Background;
|
||||||
use super::widgets::border::Border;
|
use super::widgets::border::Border;
|
||||||
|
|
@ -199,19 +199,54 @@ impl Rooms {
|
||||||
list.into()
|
list.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn list_key_bindings(&self, bindings: &mut KeyBindingsList) {
|
||||||
|
match &self.state {
|
||||||
|
State::ShowList => {
|
||||||
|
bindings.heading("Rooms");
|
||||||
|
bindings.binding("j/k, ↓/↑", "move cursor up/down");
|
||||||
|
bindings.binding("g, home", "move cursor to top");
|
||||||
|
bindings.binding("G, end", "move cursor to bottom");
|
||||||
|
bindings.binding("ctrl+y/e", "scroll up/down");
|
||||||
|
bindings.empty();
|
||||||
|
bindings.binding("enter", "enter selected room");
|
||||||
|
bindings.binding("c", "connect to selected room");
|
||||||
|
bindings.binding("C", "connect to new room");
|
||||||
|
bindings.binding("d", "disconnect from selected room");
|
||||||
|
bindings.binding("D", "delete room");
|
||||||
|
}
|
||||||
|
State::ShowRoom(name) => {
|
||||||
|
// Key bindings for leaving the room are a part of the room's
|
||||||
|
// list_key_bindings function since they may be shadowed by the
|
||||||
|
// nick selector or message editor.
|
||||||
|
if let Some(room) = self.euph_rooms.get(name) {
|
||||||
|
room.list_key_bindings(bindings).await;
|
||||||
|
} else {
|
||||||
|
// There should always be a room here already but I don't
|
||||||
|
// really want to panic in case it is not. If I show a
|
||||||
|
// message like this, it'll hopefully be reported if
|
||||||
|
// somebody ever encounters it.
|
||||||
|
bindings.binding_ctd("oops, this text should never be visible")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
State::Connect(_) => {
|
||||||
|
bindings.heading("Rooms");
|
||||||
|
bindings.binding("esc", "abort");
|
||||||
|
bindings.binding("enter", "connect to room");
|
||||||
|
bindings.binding("←/→", "move cursor left/right");
|
||||||
|
bindings.binding("backspace", "delete before cursor");
|
||||||
|
bindings.binding("delete", "delete after cursor");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn handle_key_event(
|
pub async fn handle_key_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
terminal: &mut Terminal,
|
terminal: &mut Terminal,
|
||||||
crossterm_lock: &Arc<FairMutex<()>>,
|
crossterm_lock: &Arc<FairMutex<()>>,
|
||||||
event: KeyEvent,
|
event: KeyEvent,
|
||||||
) {
|
) -> bool {
|
||||||
match &self.state {
|
match &self.state {
|
||||||
State::ShowList => match event {
|
State::ShowList => match event {
|
||||||
key!(Enter) => {
|
|
||||||
if let Some(name) = self.list.cursor() {
|
|
||||||
self.state = State::ShowRoom(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
key!('k') | key!(Up) => self.list.move_cursor_up(),
|
key!('k') | key!(Up) => self.list.move_cursor_up(),
|
||||||
key!('j') | key!(Down) => self.list.move_cursor_down(),
|
key!('j') | key!(Down) => self.list.move_cursor_down(),
|
||||||
key!('g') | key!(Home) => self.list.move_cursor_to_top(),
|
key!('g') | key!(Home) => self.list.move_cursor_to_top(),
|
||||||
|
|
@ -219,6 +254,11 @@ impl Rooms {
|
||||||
key!(Ctrl + 'y') => self.list.scroll_up(1),
|
key!(Ctrl + 'y') => self.list.scroll_up(1),
|
||||||
key!(Ctrl + 'e') => self.list.scroll_down(1),
|
key!(Ctrl + 'e') => self.list.scroll_down(1),
|
||||||
|
|
||||||
|
key!(Enter) => {
|
||||||
|
if let Some(name) = self.list.cursor() {
|
||||||
|
self.state = State::ShowRoom(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
key!('c') => {
|
key!('c') => {
|
||||||
if let Some(name) = self.list.cursor() {
|
if let Some(name) = self.list.cursor() {
|
||||||
self.get_or_insert_room(name).connect();
|
self.get_or_insert_room(name).connect();
|
||||||
|
|
@ -237,7 +277,7 @@ impl Rooms {
|
||||||
self.vault.euph(name.clone()).delete();
|
self.vault.euph(name.clone()).delete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => return false,
|
||||||
},
|
},
|
||||||
State::ShowRoom(name) => {
|
State::ShowRoom(name) => {
|
||||||
if self
|
if self
|
||||||
|
|
@ -245,7 +285,7 @@ impl Rooms {
|
||||||
.handle_key_event(terminal, crossterm_lock, event)
|
.handle_key_event(terminal, crossterm_lock, event)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let key!(Esc) = event {
|
if let key!(Esc) = event {
|
||||||
|
|
@ -262,12 +302,14 @@ impl Rooms {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
key!(Char ch) if ch.is_ascii_alphanumeric() || ch == '_' => ed.insert_char(ch),
|
key!(Char ch) if ch.is_ascii_alphanumeric() || ch == '_' => ed.insert_char(ch),
|
||||||
key!(Backspace) => ed.backspace(),
|
|
||||||
key!(Left) => ed.move_cursor_left(),
|
key!(Left) => ed.move_cursor_left(),
|
||||||
key!(Right) => ed.move_cursor_right(),
|
key!(Right) => ed.move_cursor_right(),
|
||||||
|
key!(Backspace) => ed.backspace(),
|
||||||
key!(Delete) => ed.delete(),
|
key!(Delete) => ed.delete(),
|
||||||
_ => {}
|
_ => return false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue