mirror of
https://github.com/youwen5/iamb.git
synced 2025-06-20 13:49:52 -07:00
Want a Matrix client that uses Vim keybindings (#1)
This commit is contained in:
parent
704f631d54
commit
262c96b62f
22 changed files with 9050 additions and 7 deletions
696
src/windows/mod.rs
Normal file
696
src/windows/mod.rs
Normal file
|
@ -0,0 +1,696 @@
|
|||
use std::cmp::{Ord, Ordering, PartialOrd};
|
||||
use std::collections::hash_map::Entry;
|
||||
|
||||
use matrix_sdk::{
|
||||
encryption::verification::{format_emojis, SasVerification},
|
||||
room::Room as MatrixRoom,
|
||||
ruma::RoomId,
|
||||
DisplayName,
|
||||
};
|
||||
|
||||
use modalkit::tui::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::{Modifier as StyleModifier, Style},
|
||||
text::{Span, Spans, Text},
|
||||
widgets::{Block, Borders, Widget},
|
||||
};
|
||||
|
||||
use modalkit::{
|
||||
editing::{
|
||||
action::{
|
||||
EditError,
|
||||
EditInfo,
|
||||
EditResult,
|
||||
Editable,
|
||||
EditorAction,
|
||||
Jumpable,
|
||||
PromptAction,
|
||||
Promptable,
|
||||
Scrollable,
|
||||
UIError,
|
||||
WindowAction,
|
||||
},
|
||||
base::{
|
||||
CloseFlags,
|
||||
MoveDir1D,
|
||||
OpenTarget,
|
||||
PositionList,
|
||||
ScrollStyle,
|
||||
ViewportContext,
|
||||
WordStyle,
|
||||
},
|
||||
},
|
||||
widgets::{
|
||||
list::{ListCursor, ListItem, ListState},
|
||||
TermOffset,
|
||||
TerminalCursor,
|
||||
Window,
|
||||
WindowOps,
|
||||
},
|
||||
};
|
||||
|
||||
use super::base::{
|
||||
ChatStore,
|
||||
IambBufferId,
|
||||
IambId,
|
||||
IambInfo,
|
||||
IambResult,
|
||||
ProgramAction,
|
||||
ProgramContext,
|
||||
ProgramStore,
|
||||
};
|
||||
|
||||
use self::{room::RoomState, welcome::WelcomeState};
|
||||
|
||||
pub mod room;
|
||||
pub mod welcome;
|
||||
|
||||
#[inline]
|
||||
fn selected_style(selected: bool) -> Style {
|
||||
if selected {
|
||||
Style::default().add_modifier(StyleModifier::REVERSED)
|
||||
} else {
|
||||
Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn selected_span(s: &str, selected: bool) -> Span {
|
||||
Span::styled(s, selected_style(selected))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn selected_text(s: &str, selected: bool) -> Text {
|
||||
Text::from(selected_span(s, selected))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn room_prompt(
|
||||
room_id: &RoomId,
|
||||
act: &PromptAction,
|
||||
ctx: &ProgramContext,
|
||||
) -> EditResult<Vec<(ProgramAction, ProgramContext)>, IambInfo> {
|
||||
match act {
|
||||
PromptAction::Submit => {
|
||||
let room = IambId::Room(room_id.to_owned());
|
||||
let open = WindowAction::Switch(OpenTarget::Application(room));
|
||||
let acts = vec![(open.into(), ctx.clone())];
|
||||
|
||||
Ok(acts)
|
||||
},
|
||||
PromptAction::Abort(_) => {
|
||||
let msg = "Cannot abort entry inside a list";
|
||||
let err = EditError::Failure(msg.into());
|
||||
|
||||
Err(err)
|
||||
},
|
||||
PromptAction::Recall(_, _) => {
|
||||
let msg = "Cannot recall history inside a list";
|
||||
let err = EditError::Failure(msg.into());
|
||||
|
||||
Err(err)
|
||||
},
|
||||
_ => Err(EditError::Unimplemented("unknown prompt action".to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! delegate {
|
||||
($s: expr, $id: ident => $e: expr) => {
|
||||
match $s {
|
||||
IambWindow::Room($id) => $e,
|
||||
IambWindow::DirectList($id) => $e,
|
||||
IambWindow::RoomList($id) => $e,
|
||||
IambWindow::SpaceList($id) => $e,
|
||||
IambWindow::VerifyList($id) => $e,
|
||||
IambWindow::Welcome($id) => $e,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub enum IambWindow {
|
||||
DirectList(DirectListState),
|
||||
Room(RoomState),
|
||||
VerifyList(VerifyListState),
|
||||
RoomList(RoomListState),
|
||||
SpaceList(SpaceListState),
|
||||
Welcome(WelcomeState),
|
||||
}
|
||||
|
||||
impl IambWindow {
|
||||
pub fn focus_toggle(&mut self) {
|
||||
if let IambWindow::Room(w) = self {
|
||||
w.focus_toggle()
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_title(&self, store: &mut ProgramStore) -> String {
|
||||
match self {
|
||||
IambWindow::Room(w) => w.get_title(store),
|
||||
IambWindow::DirectList(_) => "Direct Messages".to_string(),
|
||||
IambWindow::RoomList(_) => "Rooms".to_string(),
|
||||
IambWindow::SpaceList(_) => "Spaces".to_string(),
|
||||
IambWindow::VerifyList(_) => "Verifications".to_string(),
|
||||
IambWindow::Welcome(_) => "Welcome to iamb".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type DirectListState = ListState<DirectItem, IambInfo>;
|
||||
pub type RoomListState = ListState<RoomItem, IambInfo>;
|
||||
pub type SpaceListState = ListState<SpaceItem, IambInfo>;
|
||||
pub type VerifyListState = ListState<VerifyItem, IambInfo>;
|
||||
|
||||
impl From<RoomState> for IambWindow {
|
||||
fn from(room: RoomState) -> Self {
|
||||
IambWindow::Room(room)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<VerifyListState> for IambWindow {
|
||||
fn from(list: VerifyListState) -> Self {
|
||||
IambWindow::VerifyList(list)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DirectListState> for IambWindow {
|
||||
fn from(list: DirectListState) -> Self {
|
||||
IambWindow::DirectList(list)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RoomListState> for IambWindow {
|
||||
fn from(list: RoomListState) -> Self {
|
||||
IambWindow::RoomList(list)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SpaceListState> for IambWindow {
|
||||
fn from(list: SpaceListState) -> Self {
|
||||
IambWindow::SpaceList(list)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WelcomeState> for IambWindow {
|
||||
fn from(win: WelcomeState) -> Self {
|
||||
IambWindow::Welcome(win)
|
||||
}
|
||||
}
|
||||
|
||||
impl Editable<ProgramContext, ProgramStore, IambInfo> for IambWindow {
|
||||
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 IambWindow {
|
||||
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 IambWindow {
|
||||
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 IambWindow {
|
||||
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 IambWindow {
|
||||
fn get_term_cursor(&self) -> Option<TermOffset> {
|
||||
delegate!(self, w => w.get_term_cursor())
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowOps<IambInfo> for IambWindow {
|
||||
fn draw(&mut self, area: Rect, buf: &mut Buffer, focused: bool, store: &mut ProgramStore) {
|
||||
let title = self.get_title(store);
|
||||
let block = Block::default().title(title.as_str()).borders(Borders::ALL);
|
||||
let inner = block.inner(area);
|
||||
block.render(area, buf);
|
||||
|
||||
match self {
|
||||
IambWindow::Room(state) => state.draw(inner, buf, focused, store),
|
||||
IambWindow::DirectList(state) => {
|
||||
let dms = store.application.worker.direct_messages();
|
||||
let items = dms.into_iter().map(|(id, name)| DirectItem::new(id, name, store));
|
||||
state.set(items.collect());
|
||||
state.draw(inner, buf, focused, store);
|
||||
},
|
||||
IambWindow::RoomList(state) => {
|
||||
let joined = store.application.worker.joined_rooms();
|
||||
let items = joined.into_iter().map(|(id, name)| RoomItem::new(id, name, store));
|
||||
state.set(items.collect());
|
||||
state.draw(inner, buf, focused, store);
|
||||
},
|
||||
IambWindow::SpaceList(state) => {
|
||||
let spaces = store.application.worker.spaces();
|
||||
let items =
|
||||
spaces.into_iter().map(|(room, name)| SpaceItem::new(room, name, store));
|
||||
state.set(items.collect());
|
||||
state.draw(inner, buf, focused, store);
|
||||
},
|
||||
IambWindow::VerifyList(state) => {
|
||||
let verifications = &store.application.verifications;
|
||||
let mut items = verifications.iter().map(VerifyItem::from).collect::<Vec<_>>();
|
||||
|
||||
// Sort the active verifications towards the top.
|
||||
items.sort();
|
||||
|
||||
state.set(items);
|
||||
state.draw(inner, buf, focused, store);
|
||||
},
|
||||
IambWindow::Welcome(state) => state.draw(inner, buf, focused, store),
|
||||
}
|
||||
}
|
||||
|
||||
fn dup(&self, store: &mut ProgramStore) -> Self {
|
||||
delegate!(self, w => w.dup(store).into())
|
||||
}
|
||||
|
||||
fn close(&mut self, flags: CloseFlags, store: &mut ProgramStore) -> bool {
|
||||
delegate!(self, w => w.close(flags, store))
|
||||
}
|
||||
|
||||
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 Window<IambInfo> for IambWindow {
|
||||
fn id(&self) -> IambId {
|
||||
match self {
|
||||
IambWindow::Room(room) => IambId::Room(room.id().to_owned()),
|
||||
IambWindow::DirectList(_) => IambId::DirectList,
|
||||
IambWindow::RoomList(_) => IambId::RoomList,
|
||||
IambWindow::SpaceList(_) => IambId::SpaceList,
|
||||
IambWindow::VerifyList(_) => IambId::VerifyList,
|
||||
IambWindow::Welcome(_) => IambId::Welcome,
|
||||
}
|
||||
}
|
||||
|
||||
fn open(id: IambId, store: &mut ProgramStore) -> IambResult<Self> {
|
||||
match id {
|
||||
IambId::Room(room_id) => {
|
||||
let (room, name) = store.application.worker.get_room(room_id)?;
|
||||
let room = RoomState::new(room, name, store);
|
||||
|
||||
return Ok(room.into());
|
||||
},
|
||||
IambId::DirectList => {
|
||||
let list = DirectListState::new(IambBufferId::DirectList, vec![]);
|
||||
|
||||
return Ok(list.into());
|
||||
},
|
||||
IambId::RoomList => {
|
||||
let list = RoomListState::new(IambBufferId::RoomList, vec![]);
|
||||
|
||||
return Ok(list.into());
|
||||
},
|
||||
IambId::SpaceList => {
|
||||
let list = SpaceListState::new(IambBufferId::SpaceList, vec![]);
|
||||
|
||||
return Ok(list.into());
|
||||
},
|
||||
IambId::VerifyList => {
|
||||
let list = VerifyListState::new(IambBufferId::VerifyList, vec![]);
|
||||
|
||||
return Ok(list.into());
|
||||
},
|
||||
IambId::Welcome => {
|
||||
let win = WelcomeState::new(store);
|
||||
|
||||
return Ok(win.into());
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn find(name: String, store: &mut ProgramStore) -> IambResult<Self> {
|
||||
let ChatStore { names, worker, .. } = &mut store.application;
|
||||
|
||||
match names.entry(name) {
|
||||
Entry::Vacant(v) => {
|
||||
let room_id = worker.join_room(v.key().to_string())?;
|
||||
v.insert(room_id.clone());
|
||||
|
||||
let (room, name) = store.application.worker.get_room(room_id)?;
|
||||
let room = RoomState::new(room, name, store);
|
||||
|
||||
Ok(room.into())
|
||||
},
|
||||
Entry::Occupied(o) => {
|
||||
let id = IambId::Room(o.get().clone());
|
||||
|
||||
IambWindow::open(id, store)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn posn(index: usize, _: &mut ProgramStore) -> IambResult<Self> {
|
||||
let msg = format!("Cannot find indexed buffer (index = {})", index);
|
||||
let err = UIError::Unimplemented(msg);
|
||||
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RoomItem {
|
||||
room: MatrixRoom,
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl RoomItem {
|
||||
fn new(room: MatrixRoom, name: DisplayName, store: &mut ProgramStore) -> Self {
|
||||
let name = name.to_string();
|
||||
|
||||
store.application.set_room_name(room.room_id(), name.as_str());
|
||||
|
||||
RoomItem { room, name }
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for RoomItem {
|
||||
fn to_string(&self) -> String {
|
||||
return self.name.clone();
|
||||
}
|
||||
}
|
||||
|
||||
impl ListItem<IambInfo> for RoomItem {
|
||||
fn show(&self, selected: bool, _: &ViewportContext<ListCursor>, _: &mut ProgramStore) -> Text {
|
||||
selected_text(self.name.as_str(), selected)
|
||||
}
|
||||
}
|
||||
|
||||
impl Promptable<ProgramContext, ProgramStore, IambInfo> for RoomItem {
|
||||
fn prompt(
|
||||
&mut self,
|
||||
act: &PromptAction,
|
||||
ctx: &ProgramContext,
|
||||
_: &mut ProgramStore,
|
||||
) -> EditResult<Vec<(ProgramAction, ProgramContext)>, IambInfo> {
|
||||
room_prompt(self.room.room_id(), act, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DirectItem {
|
||||
room: MatrixRoom,
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl DirectItem {
|
||||
fn new(room: MatrixRoom, name: DisplayName, store: &mut ProgramStore) -> Self {
|
||||
let name = name.to_string();
|
||||
|
||||
store.application.set_room_name(room.room_id(), name.as_str());
|
||||
|
||||
DirectItem { room, name }
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for DirectItem {
|
||||
fn to_string(&self) -> String {
|
||||
return self.name.clone();
|
||||
}
|
||||
}
|
||||
|
||||
impl ListItem<IambInfo> for DirectItem {
|
||||
fn show(&self, selected: bool, _: &ViewportContext<ListCursor>, _: &mut ProgramStore) -> Text {
|
||||
selected_text(self.name.as_str(), selected)
|
||||
}
|
||||
}
|
||||
|
||||
impl Promptable<ProgramContext, ProgramStore, IambInfo> for DirectItem {
|
||||
fn prompt(
|
||||
&mut self,
|
||||
act: &PromptAction,
|
||||
ctx: &ProgramContext,
|
||||
_: &mut ProgramStore,
|
||||
) -> EditResult<Vec<(ProgramAction, ProgramContext)>, IambInfo> {
|
||||
room_prompt(self.room.room_id(), act, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SpaceItem {
|
||||
room: MatrixRoom,
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl SpaceItem {
|
||||
fn new(room: MatrixRoom, name: DisplayName, store: &mut ProgramStore) -> Self {
|
||||
let name = name.to_string();
|
||||
|
||||
store.application.set_room_name(room.room_id(), name.as_str());
|
||||
|
||||
SpaceItem { room, name }
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for SpaceItem {
|
||||
fn to_string(&self) -> String {
|
||||
return self.room.room_id().to_string();
|
||||
}
|
||||
}
|
||||
|
||||
impl ListItem<IambInfo> for SpaceItem {
|
||||
fn show(&self, selected: bool, _: &ViewportContext<ListCursor>, _: &mut ProgramStore) -> Text {
|
||||
selected_text(self.name.as_str(), selected)
|
||||
}
|
||||
}
|
||||
|
||||
impl Promptable<ProgramContext, ProgramStore, IambInfo> for SpaceItem {
|
||||
fn prompt(
|
||||
&mut self,
|
||||
act: &PromptAction,
|
||||
ctx: &ProgramContext,
|
||||
_: &mut ProgramStore,
|
||||
) -> EditResult<Vec<(ProgramAction, ProgramContext)>, IambInfo> {
|
||||
room_prompt(self.room.room_id(), act, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct VerifyItem {
|
||||
user_dev: String,
|
||||
sasv1: SasVerification,
|
||||
}
|
||||
|
||||
impl VerifyItem {
|
||||
fn new(user_dev: String, sasv1: SasVerification) -> Self {
|
||||
VerifyItem { user_dev, sasv1 }
|
||||
}
|
||||
|
||||
fn show_item(&self) -> String {
|
||||
let state = if self.sasv1.is_done() {
|
||||
"done"
|
||||
} else if self.sasv1.is_cancelled() {
|
||||
"cancelled"
|
||||
} else if self.sasv1.emoji().is_some() {
|
||||
"accepted"
|
||||
} else {
|
||||
"not accepted"
|
||||
};
|
||||
|
||||
if self.sasv1.is_self_verification() {
|
||||
let device = self.sasv1.other_device();
|
||||
|
||||
if let Some(display_name) = device.display_name() {
|
||||
format!("Device verification with {} ({})", display_name, state)
|
||||
} else {
|
||||
format!("Device verification with device {} ({})", device.device_id(), state)
|
||||
}
|
||||
} else {
|
||||
format!("User Verification with {} ({})", self.sasv1.other_user_id(), state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for VerifyItem {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.user_dev == other.user_dev
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for VerifyItem {}
|
||||
|
||||
impl Ord for VerifyItem {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
fn state_val(sas: &SasVerification) -> usize {
|
||||
if sas.is_done() {
|
||||
return 3;
|
||||
} else if sas.is_cancelled() {
|
||||
return 2;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn device_val(sas: &SasVerification) -> usize {
|
||||
if sas.is_self_verification() {
|
||||
return 1;
|
||||
} else {
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
let state1 = state_val(&self.sasv1);
|
||||
let state2 = state_val(&other.sasv1);
|
||||
|
||||
let dev1 = device_val(&self.sasv1);
|
||||
let dev2 = device_val(&other.sasv1);
|
||||
|
||||
let scmp = state1.cmp(&state2);
|
||||
let dcmp = dev1.cmp(&dev2);
|
||||
|
||||
scmp.then(dcmp).then_with(|| {
|
||||
let did1 = self.sasv1.other_device().device_id();
|
||||
let did2 = other.sasv1.other_device().device_id();
|
||||
|
||||
did1.cmp(did2)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for VerifyItem {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
self.cmp(other).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(&String, &SasVerification)> for VerifyItem {
|
||||
fn from((user_dev, sasv1): (&String, &SasVerification)) -> Self {
|
||||
VerifyItem::new(user_dev.clone(), sasv1.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for VerifyItem {
|
||||
fn to_string(&self) -> String {
|
||||
if self.sasv1.is_done() {
|
||||
String::new()
|
||||
} else if self.sasv1.is_cancelled() {
|
||||
format!(":verify request {}", self.sasv1.other_user_id())
|
||||
} else if self.sasv1.emoji().is_some() {
|
||||
format!(":verify confirm {}", self.user_dev)
|
||||
} else {
|
||||
format!(":verify accept {}", self.user_dev)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ListItem<IambInfo> for VerifyItem {
|
||||
fn show(&self, selected: bool, _: &ViewportContext<ListCursor>, _: &mut ProgramStore) -> Text {
|
||||
let mut lines = vec![];
|
||||
|
||||
let bold = Style::default().add_modifier(StyleModifier::BOLD);
|
||||
let item = Span::styled(self.show_item(), selected_style(selected));
|
||||
lines.push(Spans::from(item));
|
||||
|
||||
if self.sasv1.is_done() {
|
||||
// Print nothing.
|
||||
} else if self.sasv1.is_cancelled() {
|
||||
if let Some(info) = self.sasv1.cancel_info() {
|
||||
lines.push(Spans::from(format!(" Cancelled: {}", info.reason())));
|
||||
lines.push(Spans::from(""));
|
||||
}
|
||||
|
||||
lines.push(Spans::from(" You can start a new verification request with:"));
|
||||
} else if let Some(emoji) = self.sasv1.emoji() {
|
||||
lines.push(Spans::from(
|
||||
" Both devices should see the following Emoji sequence:".to_string(),
|
||||
));
|
||||
lines.push(Spans::from(""));
|
||||
|
||||
for line in format_emojis(emoji).lines() {
|
||||
lines.push(Spans::from(format!(" {}", line)));
|
||||
}
|
||||
|
||||
lines.push(Spans::from(""));
|
||||
lines.push(Spans::from(" If they don't match, run:"));
|
||||
lines.push(Spans::from(""));
|
||||
lines.push(Spans::from(Span::styled(
|
||||
format!(":verify mismatch {}", self.user_dev),
|
||||
bold,
|
||||
)));
|
||||
lines.push(Spans::from(""));
|
||||
lines.push(Spans::from(" If everything looks right, you can confirm with:"));
|
||||
} else {
|
||||
lines.push(Spans::from(" To accept this request, run:"));
|
||||
}
|
||||
|
||||
let cmd = self.to_string();
|
||||
|
||||
if !cmd.is_empty() {
|
||||
lines.push(Spans::from(""));
|
||||
lines.push(Spans(vec![Span::from(" "), Span::styled(cmd, bold)]));
|
||||
lines.push(Spans::from(""));
|
||||
lines.push(Spans(vec![
|
||||
Span::from("You can copy the above command with "),
|
||||
Span::styled("yy", bold),
|
||||
Span::from(" and then execute it with "),
|
||||
Span::styled("@\"", bold),
|
||||
]));
|
||||
}
|
||||
|
||||
Text { lines }
|
||||
}
|
||||
}
|
||||
|
||||
impl Promptable<ProgramContext, ProgramStore, IambInfo> for VerifyItem {
|
||||
fn prompt(
|
||||
&mut self,
|
||||
act: &PromptAction,
|
||||
_: &ProgramContext,
|
||||
_: &mut ProgramStore,
|
||||
) -> EditResult<Vec<(ProgramAction, ProgramContext)>, IambInfo> {
|
||||
match act {
|
||||
PromptAction::Submit => Ok(vec![]),
|
||||
PromptAction::Abort(_) => {
|
||||
let msg = "Cannot abort entry inside a list";
|
||||
let err = EditError::Failure(msg.into());
|
||||
|
||||
Err(err)
|
||||
},
|
||||
PromptAction::Recall(_, _) => {
|
||||
let msg = "Cannot recall history inside a list";
|
||||
let err = EditError::Failure(msg.into());
|
||||
|
||||
Err(err)
|
||||
},
|
||||
_ => Err(EditError::Unimplemented("unknown prompt action".to_string())),
|
||||
}
|
||||
}
|
||||
}
|
319
src/windows/room/chat.rs
Normal file
319
src/windows/room/chat.rs
Normal 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
182
src/windows/room/mod.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
1471
src/windows/room/scrollback.rs
Normal file
1471
src/windows/room/scrollback.rs
Normal file
File diff suppressed because it is too large
Load diff
105
src/windows/room/space.rs
Normal file
105
src/windows/room/space.rs
Normal 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)
|
||||
}
|
||||
}
|
44
src/windows/welcome.md
Normal file
44
src/windows/welcome.md
Normal file
|
@ -0,0 +1,44 @@
|
|||
# Welcome to iamb!
|
||||
|
||||
## Useful Keybindings
|
||||
|
||||
- `<Enter>` will send a typed message
|
||||
- `O`/`o` can be used to insert blank lines before and after the cursor line
|
||||
- `^O` can be used in Insert mode to enter a single Normal mode keybinding sequence
|
||||
- `^Wm` can be used to toggle whether the message bar or scrollback is selected
|
||||
- `^Wz` can be used to toggle whether the current window takes up the full screen
|
||||
|
||||
## Room Commands
|
||||
|
||||
- `:dms` will open a list of direct messages
|
||||
- `:rooms` will open a list of joined rooms
|
||||
- `:spaces` will open a list of joined spaces
|
||||
- `:join` can be used to switch to join a new room or start a direct message
|
||||
- `:split` and `:vsplit` can be used to open rooms in a new window
|
||||
|
||||
## Verification Commands
|
||||
|
||||
The `:verify` command has several different subcommands for working with
|
||||
verification requests. When used without any arguments, it will take you to a
|
||||
list of current verifications, where you can see and compare the Emoji.
|
||||
|
||||
The different subcommands are:
|
||||
|
||||
- `:verify request USERNAME` will send a verification request to a user
|
||||
- `:verify confirm USERNAME/DEVICE` will confirm a verification
|
||||
- `:verify mismatch USERNAME/DEVICE` will cancel a verification where the Emoji don't match
|
||||
- `:verify cancel USERNAME/DEVICE` will cancel a verification
|
||||
|
||||
## Other Useful Commands
|
||||
|
||||
- `:welcome` will take you back to this screen
|
||||
|
||||
## Additional Configuration
|
||||
|
||||
You can customize iamb in your `$CONFIG_DIR/iamb/config.json` file, where
|
||||
`$CONFIG_DIR` is your system's per-user configuration directory.
|
||||
|
||||
You can edit the following values in the file:
|
||||
|
||||
- `"default_profile"`, a profile name to use when starting iamb if one wasn't specified
|
||||
- `"cache"`, a directory for cached iamb
|
71
src/windows/welcome.rs
Normal file
71
src/windows/welcome.rs
Normal file
|
@ -0,0 +1,71 @@
|
|||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use modalkit::tui::{buffer::Buffer, layout::Rect};
|
||||
|
||||
use modalkit::{
|
||||
widgets::textbox::TextBoxState,
|
||||
widgets::WindowOps,
|
||||
widgets::{TermOffset, TerminalCursor},
|
||||
};
|
||||
|
||||
use modalkit::editing::base::{CloseFlags, WordStyle};
|
||||
|
||||
use crate::base::{IambBufferId, IambInfo, ProgramStore};
|
||||
|
||||
const WELCOME_TEXT: &str = include_str!("welcome.md");
|
||||
|
||||
pub struct WelcomeState {
|
||||
tbox: TextBoxState<IambInfo>,
|
||||
}
|
||||
|
||||
impl WelcomeState {
|
||||
pub fn new(store: &mut ProgramStore) -> Self {
|
||||
let buf = store.buffers.load_str(IambBufferId::Welcome, WELCOME_TEXT);
|
||||
|
||||
WelcomeState { tbox: TextBoxState::new(buf) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for WelcomeState {
|
||||
type Target = TextBoxState<IambInfo>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
return &self.tbox;
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for WelcomeState {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
return &mut self.tbox;
|
||||
}
|
||||
}
|
||||
|
||||
impl TerminalCursor for WelcomeState {
|
||||
fn get_term_cursor(&self) -> Option<TermOffset> {
|
||||
self.tbox.get_term_cursor()
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowOps<IambInfo> for WelcomeState {
|
||||
fn draw(&mut self, area: Rect, buf: &mut Buffer, focused: bool, store: &mut ProgramStore) {
|
||||
self.tbox.draw(area, buf, focused, store)
|
||||
}
|
||||
|
||||
fn dup(&self, store: &mut ProgramStore) -> Self {
|
||||
let tbox = self.tbox.dup(store);
|
||||
|
||||
WelcomeState { tbox }
|
||||
}
|
||||
|
||||
fn close(&mut self, flags: CloseFlags, store: &mut ProgramStore) -> bool {
|
||||
self.tbox.close(flags, store)
|
||||
}
|
||||
|
||||
fn get_cursor_word(&self, style: &WordStyle) -> Option<String> {
|
||||
self.tbox.get_cursor_word(style)
|
||||
}
|
||||
|
||||
fn get_selected_word(&self) -> Option<String> {
|
||||
self.tbox.get_selected_word()
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue