Want a Matrix client that uses Vim keybindings (#1)

This commit is contained in:
Ulyssa 2022-12-29 18:00:59 -08:00
parent 704f631d54
commit 262c96b62f
No known key found for this signature in database
GPG key ID: 1B3965A3D18B9B64
22 changed files with 9050 additions and 7 deletions

319
src/windows/room/chat.rs Normal file
View file

@ -0,0 +1,319 @@
use matrix_sdk::{
room::Room as MatrixRoom,
ruma::{OwnedRoomId, RoomId},
};
use modalkit::tui::{buffer::Buffer, layout::Rect, widgets::StatefulWidget};
use modalkit::{
widgets::textbox::{TextBox, TextBoxState},
widgets::TerminalCursor,
widgets::{PromptActions, WindowOps},
};
use modalkit::editing::{
action::{
EditError,
EditInfo,
EditResult,
Editable,
EditorAction,
Jumpable,
PromptAction,
Promptable,
Scrollable,
},
base::{CloseFlags, Count, MoveDir1D, PositionList, ScrollStyle, WordStyle},
context::Resolve,
history::{self, HistoryList},
rope::EditRope,
};
use crate::base::{
IambAction,
IambBufferId,
IambInfo,
IambResult,
ProgramAction,
ProgramContext,
ProgramStore,
RoomFocus,
};
use super::scrollback::{Scrollback, ScrollbackState};
pub struct ChatState {
room_id: OwnedRoomId,
tbox: TextBoxState<IambInfo>,
sent: HistoryList<EditRope>,
sent_scrollback: history::ScrollbackState,
scrollback: ScrollbackState,
focus: RoomFocus,
}
impl ChatState {
pub fn new(room: MatrixRoom, store: &mut ProgramStore) -> Self {
let room_id = room.room_id().to_owned();
let scrollback = ScrollbackState::new(room_id.clone());
let id = IambBufferId::Room(room_id.clone(), RoomFocus::MessageBar);
let ebuf = store.load_buffer(id);
let tbox = TextBoxState::new(ebuf);
ChatState {
room_id,
tbox,
sent: HistoryList::new(EditRope::from(""), 100),
sent_scrollback: history::ScrollbackState::Pending,
scrollback,
focus: RoomFocus::MessageBar,
}
}
pub fn focus_toggle(&mut self) {
self.focus = match self.focus {
RoomFocus::Scrollback => RoomFocus::MessageBar,
RoomFocus::MessageBar => RoomFocus::Scrollback,
};
}
pub fn id(&self) -> &RoomId {
&self.room_id
}
}
macro_rules! delegate {
($s: expr, $id: ident => $e: expr) => {
match $s.focus {
RoomFocus::Scrollback => {
match $s {
ChatState { scrollback: $id, .. } => $e,
}
},
RoomFocus::MessageBar => {
match $s {
ChatState { tbox: $id, .. } => $e,
}
},
}
};
}
impl WindowOps<IambInfo> for ChatState {
fn draw(&mut self, area: Rect, buf: &mut Buffer, focused: bool, store: &mut ProgramStore) {
Chat::new(store).focus(focused).render(area, buf, self)
}
fn dup(&self, store: &mut ProgramStore) -> Self {
// XXX: I want each WindowSlot to have its own shared buffer, instead of each Room; need to
// find a good way to pass that info here so that it can be part of the content id.
let id = IambBufferId::Room(self.room_id.clone(), RoomFocus::MessageBar);
let ebuf = store.load_buffer(id);
let tbox = TextBoxState::new(ebuf);
ChatState {
room_id: self.room_id.clone(),
tbox,
sent: self.sent.clone(),
sent_scrollback: history::ScrollbackState::Pending,
scrollback: self.scrollback.dup(store),
focus: self.focus,
}
}
fn close(&mut self, _: CloseFlags, _: &mut ProgramStore) -> bool {
// XXX: what's the right closing behaviour for a room?
// Should write send a message?
true
}
fn get_cursor_word(&self, style: &WordStyle) -> Option<String> {
delegate!(self, w => w.get_cursor_word(style))
}
fn get_selected_word(&self) -> Option<String> {
delegate!(self, w => w.get_selected_word())
}
}
impl Editable<ProgramContext, ProgramStore, IambInfo> for ChatState {
fn editor_command(
&mut self,
act: &EditorAction,
ctx: &ProgramContext,
store: &mut ProgramStore,
) -> EditResult<EditInfo, IambInfo> {
match delegate!(self, w => w.editor_command(act, ctx, store)) {
res @ Ok(_) => res,
Err(EditError::WrongBuffer(IambBufferId::Room(room_id, focus)))
if room_id == self.room_id && act.is_switchable(ctx) =>
{
// Switch focus.
self.focus = focus;
// Run command again.
delegate!(self, w => w.editor_command(act, ctx, store))
},
res @ Err(_) => res,
}
}
}
impl TerminalCursor for ChatState {
fn get_term_cursor(&self) -> Option<(u16, u16)> {
delegate!(self, w => w.get_term_cursor())
}
}
impl Jumpable<ProgramContext, IambInfo> for ChatState {
fn jump(
&mut self,
list: PositionList,
dir: MoveDir1D,
count: usize,
ctx: &ProgramContext,
) -> IambResult<usize> {
delegate!(self, w => w.jump(list, dir, count, ctx))
}
}
impl Scrollable<ProgramContext, ProgramStore, IambInfo> for ChatState {
fn scroll(
&mut self,
style: &ScrollStyle,
ctx: &ProgramContext,
store: &mut ProgramStore,
) -> EditResult<EditInfo, IambInfo> {
// Send all scroll commands to the scrollback.
//
// If there's enough message text for scrolling to be necessary,
// navigating with movement keys should be enough to do the job.
self.scrollback.scroll(style, ctx, store)
}
}
impl PromptActions<ProgramContext, ProgramStore, IambInfo> for ChatState {
fn submit(
&mut self,
ctx: &ProgramContext,
_: &mut ProgramStore,
) -> EditResult<Vec<(ProgramAction, ProgramContext)>, IambInfo> {
let txt = self.tbox.reset_text();
let act = if txt.is_empty() {
vec![]
} else {
let act = IambAction::SendMessage(self.room_id.clone(), txt).into();
vec![(act, ctx.clone())]
};
Ok(act)
}
fn abort(
&mut self,
empty: bool,
_: &ProgramContext,
_: &mut ProgramStore,
) -> EditResult<Vec<(ProgramAction, ProgramContext)>, IambInfo> {
let text = self.tbox.get();
if empty && text.is_blank() {
return Ok(vec![]);
}
let text = self.tbox.reset().trim();
if text.is_empty() {
let _ = self.sent.end();
} else {
self.sent.select(text);
}
return Ok(vec![]);
}
fn recall(
&mut self,
dir: &MoveDir1D,
count: &Count,
ctx: &ProgramContext,
_: &mut ProgramStore,
) -> EditResult<Vec<(ProgramAction, ProgramContext)>, IambInfo> {
let count = ctx.resolve(count);
let rope = self.tbox.get();
let text = self.sent.recall(&rope, &mut self.sent_scrollback, *dir, count);
if let Some(text) = text {
self.tbox.set_text(text);
}
Ok(vec![])
}
}
impl Promptable<ProgramContext, ProgramStore, IambInfo> for ChatState {
fn prompt(
&mut self,
act: &PromptAction,
ctx: &ProgramContext,
store: &mut ProgramStore,
) -> EditResult<Vec<(ProgramAction, ProgramContext)>, IambInfo> {
if let RoomFocus::Scrollback = self.focus {
return Ok(vec![]);
}
match act {
PromptAction::Submit => self.submit(ctx, store),
PromptAction::Abort(empty) => self.abort(*empty, ctx, store),
PromptAction::Recall(dir, count) => self.recall(dir, count, ctx, store),
_ => Err(EditError::Unimplemented("unknown prompt action".to_string())),
}
}
}
pub struct Chat<'a> {
store: &'a mut ProgramStore,
focused: bool,
}
impl<'a> Chat<'a> {
pub fn new(store: &'a mut ProgramStore) -> Chat<'a> {
Chat { store, focused: false }
}
pub fn focus(mut self, focused: bool) -> Self {
self.focused = focused;
self
}
}
impl<'a> StatefulWidget for Chat<'a> {
type State = ChatState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let lines = state.tbox.has_lines(5).max(1) as u16;
let drawh = area.height;
let texth = lines.min(drawh).clamp(1, 5);
let scrollh = drawh.saturating_sub(texth);
let scrollarea = Rect::new(area.x, area.y, area.width, scrollh);
let textarea = Rect::new(scrollarea.x, scrollarea.y + scrollh, scrollarea.width, texth);
let scrollback_focused = state.focus.is_scrollback() && self.focused;
let scrollback = Scrollback::new(self.store).focus(scrollback_focused);
scrollback.render(scrollarea, buf, &mut state.scrollback);
let prompt = if self.focused { "> " } else { " " };
let tbox = TextBox::new().prompt(prompt);
tbox.render(textarea, buf, &mut state.tbox);
}
}

182
src/windows/room/mod.rs Normal file
View file

@ -0,0 +1,182 @@
use matrix_sdk::room::Room as MatrixRoom;
use matrix_sdk::ruma::RoomId;
use matrix_sdk::DisplayName;
use modalkit::tui::{buffer::Buffer, layout::Rect, widgets::StatefulWidget};
use modalkit::{
editing::action::{
EditInfo,
EditResult,
Editable,
EditorAction,
Jumpable,
PromptAction,
Promptable,
Scrollable,
},
editing::base::{CloseFlags, MoveDir1D, PositionList, ScrollStyle, WordStyle},
widgets::{TermOffset, TerminalCursor, WindowOps},
};
use crate::base::{IambInfo, IambResult, ProgramAction, ProgramContext, ProgramStore};
use self::chat::ChatState;
use self::space::{Space, SpaceState};
mod chat;
mod scrollback;
mod space;
macro_rules! delegate {
($s: expr, $id: ident => $e: expr) => {
match $s {
RoomState::Chat($id) => $e,
RoomState::Space($id) => $e,
}
};
}
pub enum RoomState {
Chat(ChatState),
Space(SpaceState),
}
impl From<ChatState> for RoomState {
fn from(chat: ChatState) -> Self {
RoomState::Chat(chat)
}
}
impl From<SpaceState> for RoomState {
fn from(space: SpaceState) -> Self {
RoomState::Space(space)
}
}
impl RoomState {
pub fn new(room: MatrixRoom, name: DisplayName, store: &mut ProgramStore) -> Self {
let room_id = room.room_id().to_owned();
let info = store.application.get_room_info(room_id);
info.name = name.to_string().into();
if room.is_space() {
SpaceState::new(room).into()
} else {
ChatState::new(room, store).into()
}
}
pub fn get_title(&self, store: &mut ProgramStore) -> String {
store
.application
.rooms
.get(self.id())
.and_then(|i| i.name.as_ref())
.map(String::from)
.unwrap_or_else(|| "Untitled Matrix Room".to_string())
}
pub fn focus_toggle(&mut self) {
match self {
RoomState::Chat(chat) => chat.focus_toggle(),
RoomState::Space(_) => return,
}
}
pub fn id(&self) -> &RoomId {
match self {
RoomState::Chat(chat) => chat.id(),
RoomState::Space(space) => space.id(),
}
}
}
impl Editable<ProgramContext, ProgramStore, IambInfo> for RoomState {
fn editor_command(
&mut self,
act: &EditorAction,
ctx: &ProgramContext,
store: &mut ProgramStore,
) -> EditResult<EditInfo, IambInfo> {
delegate!(self, w => w.editor_command(act, ctx, store))
}
}
impl Jumpable<ProgramContext, IambInfo> for RoomState {
fn jump(
&mut self,
list: PositionList,
dir: MoveDir1D,
count: usize,
ctx: &ProgramContext,
) -> IambResult<usize> {
delegate!(self, w => w.jump(list, dir, count, ctx))
}
}
impl Scrollable<ProgramContext, ProgramStore, IambInfo> for RoomState {
fn scroll(
&mut self,
style: &ScrollStyle,
ctx: &ProgramContext,
store: &mut ProgramStore,
) -> EditResult<EditInfo, IambInfo> {
delegate!(self, w => w.scroll(style, ctx, store))
}
}
impl Promptable<ProgramContext, ProgramStore, IambInfo> for RoomState {
fn prompt(
&mut self,
act: &PromptAction,
ctx: &ProgramContext,
store: &mut ProgramStore,
) -> EditResult<Vec<(ProgramAction, ProgramContext)>, IambInfo> {
delegate!(self, w => w.prompt(act, ctx, store))
}
}
impl TerminalCursor for RoomState {
fn get_term_cursor(&self) -> Option<TermOffset> {
delegate!(self, w => w.get_term_cursor())
}
}
impl WindowOps<IambInfo> for RoomState {
fn draw(&mut self, area: Rect, buf: &mut Buffer, focused: bool, store: &mut ProgramStore) {
match self {
RoomState::Chat(chat) => chat.draw(area, buf, focused, store),
RoomState::Space(space) => {
Space::new(store).focus(focused).render(area, buf, space);
},
}
}
fn dup(&self, store: &mut ProgramStore) -> Self {
match self {
RoomState::Chat(chat) => RoomState::Chat(chat.dup(store)),
RoomState::Space(space) => RoomState::Space(space.dup(store)),
}
}
fn close(&mut self, _: CloseFlags, _: &mut ProgramStore) -> bool {
// XXX: what's the right closing behaviour for a room?
// Should write send a message?
true
}
fn get_cursor_word(&self, style: &WordStyle) -> Option<String> {
match self {
RoomState::Chat(chat) => chat.get_cursor_word(style),
RoomState::Space(space) => space.get_cursor_word(style),
}
}
fn get_selected_word(&self) -> Option<String> {
match self {
RoomState::Chat(chat) => chat.get_selected_word(),
RoomState::Space(space) => space.get_selected_word(),
}
}
}

File diff suppressed because it is too large Load diff

105
src/windows/room/space.rs Normal file
View file

@ -0,0 +1,105 @@
use std::ops::{Deref, DerefMut};
use matrix_sdk::{
room::Room as MatrixRoom,
ruma::{OwnedRoomId, RoomId},
};
use modalkit::tui::{buffer::Buffer, layout::Rect, widgets::StatefulWidget};
use modalkit::{
widgets::list::{List, ListState},
widgets::{TermOffset, TerminalCursor, WindowOps},
};
use crate::base::{IambBufferId, IambInfo, ProgramStore, RoomFocus};
use crate::windows::RoomItem;
pub struct SpaceState {
room_id: OwnedRoomId,
list: ListState<RoomItem, IambInfo>,
}
impl SpaceState {
pub fn new(room: MatrixRoom) -> Self {
let room_id = room.room_id().to_owned();
let content = IambBufferId::Room(room_id.clone(), RoomFocus::Scrollback);
let list = ListState::new(content, vec![]);
SpaceState { room_id, list }
}
pub fn id(&self) -> &RoomId {
&self.room_id
}
pub fn dup(&self, store: &mut ProgramStore) -> Self {
SpaceState {
room_id: self.room_id.clone(),
list: self.list.dup(store),
}
}
}
impl TerminalCursor for SpaceState {
fn get_term_cursor(&self) -> Option<TermOffset> {
self.list.get_term_cursor()
}
}
impl Deref for SpaceState {
type Target = ListState<RoomItem, IambInfo>;
fn deref(&self) -> &Self::Target {
&self.list
}
}
impl DerefMut for SpaceState {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.list
}
}
pub struct Space<'a> {
focused: bool,
store: &'a mut ProgramStore,
}
impl<'a> Space<'a> {
pub fn new(store: &'a mut ProgramStore) -> Self {
Space { focused: false, store }
}
pub fn focus(mut self, focused: bool) -> Self {
self.focused = focused;
self
}
}
impl<'a> StatefulWidget for Space<'a> {
type State = SpaceState;
fn render(self, area: Rect, buffer: &mut Buffer, state: &mut Self::State) {
let members = self.store.application.worker.space_members(state.room_id.clone()).unwrap();
let items = members
.into_iter()
.filter_map(|id| {
let (room, name) = self.store.application.worker.get_room(id.clone()).ok()?;
if id != state.room_id {
Some(RoomItem::new(room, name, self.store))
} else {
None
}
})
.collect();
state.list.set(items);
List::new(self.store)
.focus(self.focused)
.render(area, buffer, &mut state.list)
}
}