mirror of
https://github.com/youwen5/iamb.git
synced 2025-06-20 13:49:52 -07:00
320 lines
8.7 KiB
Rust
320 lines
8.7 KiB
Rust
![]() |
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);
|
||
|
}
|
||
|
}
|