Feat(App): implemented vim like modes (Normal/Insert)

This commit is contained in:
antifallobst 2023-07-22 16:00:52 +02:00
parent 4856ecc582
commit d447eb2312
Signed by: antifallobst
GPG Key ID: 2B4F402172791BAF
8 changed files with 113 additions and 47 deletions

View File

@ -1,7 +1,10 @@
// FIXME: This file needs documentation with examples of how the proc macros work. // FIXME: This file needs documentation with examples of how the proc macros work.
// for now use `cargo expand app::command_interface` for an overview // for now use `cargo expand app::command_interface` for an overview
use std::{io::{Error, ErrorKind}, sync::Arc}; use std::{
io::{Error, ErrorKind},
sync::Arc,
};
use lua_macros::{ci_command, turn_struct_to_ci_command_enum}; use lua_macros::{ci_command, turn_struct_to_ci_command_enum};
@ -43,6 +46,11 @@ struct Commands {
/// Go to the previous plane /// Go to the previous plane
cycle_planes_rev: fn(), cycle_planes_rev: fn(),
/// Sets the current app mode to Normal / navigation mode
set_mode_normal: fn(),
/// Sets the current app mode to Insert / editing mode
set_mode_insert: fn(),
/// Send a message to the current room /// Send a message to the current room
/// The send message is interpreted literally. /// The send message is interpreted literally.
room_message_send: fn(String) -> String, room_message_send: fn(String) -> String,

View File

@ -1,4 +1,7 @@
use crate::app::{command_interface::Command, events::event_types::EventStatus, App}; use crate::{
app::{command_interface::Command, events::event_types::EventStatus, status::State, App},
ui::central::InputPosition,
};
use anyhow::Result; use anyhow::Result;
use cli_log::info; use cli_log::info;
@ -49,6 +52,18 @@ pub async fn handle(
(EventStatus::Ok, "".to_owned()) (EventStatus::Ok, "".to_owned())
} }
Command::SetModeNormal => {
app.status.set_state(State::Normal);
set_status_output!("Set input mode to Normal");
(EventStatus::Ok, "".to_owned())
}
Command::SetModeInsert => {
app.status.set_state(State::Insert);
app.ui.set_input_position(InputPosition::MessageCompose);
set_status_output!("Set input mode to Insert");
(EventStatus::Ok, "".to_owned())
}
Command::RoomMessageSend(msg) => { Command::RoomMessageSend(msg) => {
if let Some(room) = app.status.room_mut() { if let Some(room) = app.status.room_mut() {
room.send(msg.clone()).await?; room.send(msg.clone()).await?;

View File

@ -10,7 +10,7 @@ use crate::{
ui::central, ui::central,
}; };
pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result<EventStatus> { pub async fn handle_normal(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result<EventStatus> {
match input_event { match input_event {
CrosstermEvent::Key(KeyEvent { CrosstermEvent::Key(KeyEvent {
code: KeyCode::Esc, .. code: KeyCode::Esc, ..
@ -35,37 +35,22 @@ pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result<E
.await?; .await?;
} }
CrosstermEvent::Key(KeyEvent { CrosstermEvent::Key(KeyEvent {
code: KeyCode::Char('c'), code: KeyCode::Char(':'),
modifiers: KeyModifiers::CONTROL,
.. ..
}) => { }) => {
app.tx app.tx
.send(Event::CommandEvent(Command::CommandLineShow, None)) .send(Event::CommandEvent(Command::CommandLineShow, None))
.await?; .await?;
} }
input => match app.ui.input_position() {
central::InputPosition::MessageCompose => {
match input {
CrosstermEvent::Key(KeyEvent { CrosstermEvent::Key(KeyEvent {
code: KeyCode::Enter, code: KeyCode::Char('i'),
modifiers: KeyModifiers::ALT,
.. ..
}) => { }) => {
app.tx app.tx
.send(Event::CommandEvent( .send(Event::CommandEvent(Command::SetModeInsert, None))
Command::RoomMessageSend(app.ui.message_compose.lines().join("\n")),
None,
))
.await?; .await?;
app.ui.message_compose_clear();
}
_ => {
app.ui
.message_compose
.input(tui_textarea::Input::from(input.to_owned()));
}
};
} }
input => match app.ui.input_position() {
central::InputPosition::Rooms => { central::InputPosition::Rooms => {
match input { match input {
CrosstermEvent::Key(KeyEvent { CrosstermEvent::Key(KeyEvent {
@ -186,3 +171,35 @@ pub async fn handle(app: &mut App<'_>, input_event: &CrosstermEvent) -> Result<E
}; };
Ok(EventStatus::Ok) Ok(EventStatus::Ok)
} }
pub async fn handle_insert(app: &mut App<'_>, event: &CrosstermEvent) -> Result<EventStatus> {
match event {
CrosstermEvent::Key(KeyEvent {
code: KeyCode::Esc, ..
}) => {
app.tx
.send(Event::CommandEvent(Command::SetModeNormal, None))
.await?;
}
CrosstermEvent::Key(KeyEvent {
code: KeyCode::Enter,
modifiers: KeyModifiers::ALT,
..
}) => {
app.tx
.send(Event::CommandEvent(
Command::RoomMessageSend(app.ui.message_compose.lines().join("\n")),
None,
))
.await?;
app.ui.message_compose_clear();
}
_ => {
app.ui
.message_compose
.input(tui_textarea::Input::from(event.to_owned()));
}
};
Ok(EventStatus::Ok)
}

View File

@ -45,13 +45,10 @@ impl Event {
.with_context(|| format!("Failed to handle lua code: `{:#?}`", lua_code)), .with_context(|| format!("Failed to handle lua code: `{:#?}`", lua_code)),
Event::InputEvent(event) => match app.status.state() { Event::InputEvent(event) => match app.status.state() {
State::None => unreachable!( State::Normal => main::handle_normal(app, event)
"This state should not be available, when we are in the input handling"
),
State::Main => main::handle(app, event)
.await .await
.with_context(|| format!("Failed to handle input event: `{:#?}`", event)), .with_context(|| format!("Failed to handle input event: `{:#?}`", event)),
State::Setup => setup::handle(app, event) State::Insert => main::handle_insert(app, event)
.await .await
.with_context(|| format!("Failed to handle input event: `{:#?}`", event)), .with_context(|| format!("Failed to handle input event: `{:#?}`", event)),
}, },

View File

@ -82,7 +82,6 @@ impl App<'_> {
} }
loop { loop {
self.status.set_state(State::Main);
self.ui.update(&self.status).await?; self.ui.update(&self.status).await?;
let event = self.rx.recv().await.context("Failed to get next event")?; let event = self.rx.recv().await.context("Failed to get next event")?;
@ -102,7 +101,6 @@ impl App<'_> {
self.ui.setup_ui = Some(setup::UI::new()); self.ui.setup_ui = Some(setup::UI::new());
loop { loop {
self.status.set_state(State::Setup);
self.ui.update_setup().await?; self.ui.update_setup().await?;
let event = self.rx.recv().await.context("Failed to get next event")?; let event = self.rx.recv().await.context("Failed to get next event")?;

View File

@ -1,3 +1,5 @@
use core::fmt;
use anyhow::{Error, Result}; use anyhow::{Error, Result};
use cli_log::warn; use cli_log::warn;
use indexmap::IndexMap; use indexmap::IndexMap;
@ -11,9 +13,8 @@ use matrix_sdk::{
}; };
pub enum State { pub enum State {
None, Normal,
Main, Insert,
Setup,
} }
pub struct Room { pub struct Room {
@ -35,6 +36,15 @@ pub struct Status {
current_room_id: String, current_room_id: String,
} }
impl fmt::Display for State {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Normal => write!(f, "Normal"),
Self::Insert => write!(f, "Insert"),
}
}
}
impl Room { impl Room {
pub fn new(matrix_room: matrix_sdk::room::Joined) -> Self { pub fn new(matrix_room: matrix_sdk::room::Joined) -> Self {
Self { Self {
@ -124,7 +134,7 @@ impl Status {
}; };
Self { Self {
state: State::None, state: State::Normal,
account_name: "".to_string(), account_name: "".to_string(),
account_user_id: "".to_string(), account_user_id: "".to_string(),
client, client,

View File

@ -112,6 +112,10 @@ impl UI<'_> {
}; };
} }
pub fn set_input_position(&mut self, position: InputPosition) {
self.input_position = position;
}
pub fn input_position(&self) -> &InputPosition { pub fn input_position(&self) -> &InputPosition {
&self.input_position &self.input_position
} }

View File

@ -3,7 +3,8 @@ use std::cmp;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use tui::{ use tui::{
layout::{Constraint, Direction, Layout}, layout::{Constraint, Direction, Layout},
style::Color, style::{Color, Style},
widgets::{Block, Borders, Paragraph},
}; };
use crate::{ use crate::{
@ -19,13 +20,21 @@ pub mod widgets;
impl UI<'_> { impl UI<'_> {
pub async fn update(&mut self, status: &Status) -> Result<()> { pub async fn update(&mut self, status: &Status) -> Result<()> {
let chunks = match self.cli { let chunks = Layout::default()
Some(_) => Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints([Constraint::Min(10), Constraint::Length(3)].as_ref()) .constraints([Constraint::Min(10), Constraint::Length(3)].as_ref())
.split(self.terminal.size()?), .split(self.terminal.size()?);
None => vec![self.terminal.size()?],
}; let bottom_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints(
[
Constraint::Length((status.state().to_string().len() + 2) as u16),
Constraint::Min(16),
]
.as_ref(),
)
.split(chunks[1]);
let main_chunks = Layout::default() let main_chunks = Layout::default()
.direction(Direction::Horizontal) .direction(Direction::Horizontal)
@ -143,6 +152,13 @@ impl UI<'_> {
}; };
// initiate the widgets // initiate the widgets
let mode_indicator = Paragraph::new(status.state().to_string())
.block(
Block::default()
.borders(Borders::ALL)
.style(Style::default().fg(Color::DarkGray)),
)
.style(Style::default().fg(Color::LightYellow));
let status_panel = status::init(status, &colors); let status_panel = status::init(status, &colors);
let rooms_panel = rooms::init(status, &colors); let rooms_panel = rooms::init(status, &colors);
let (messages_panel, mut messages_state) = messages::init(status.room(), &colors) let (messages_panel, mut messages_state) = messages::init(status.room(), &colors)
@ -155,8 +171,9 @@ impl UI<'_> {
frame.render_stateful_widget(rooms_panel, left_chunks[1], &mut self.rooms_state); frame.render_stateful_widget(rooms_panel, left_chunks[1], &mut self.rooms_state);
frame.render_stateful_widget(messages_panel, middle_chunks[0], &mut messages_state); frame.render_stateful_widget(messages_panel, middle_chunks[0], &mut messages_state);
frame.render_widget(self.message_compose.widget(), middle_chunks[1]); frame.render_widget(self.message_compose.widget(), middle_chunks[1]);
frame.render_widget(mode_indicator, bottom_chunks[0]);
match &self.cli { match &self.cli {
Some(cli) => frame.render_widget(cli.widget(), chunks[1]), Some(cli) => frame.render_widget(cli.widget(), bottom_chunks[1]),
None => (), None => (),
}; };
frame.render_widget(room_info_panel, right_chunks[0]); frame.render_widget(room_info_panel, right_chunks[0]);