mirror of
https://github.com/youwen5/iamb.git
synced 2025-06-19 21:29: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
328
src/base.rs
Normal file
328
src/base.rs
Normal file
|
@ -0,0 +1,328 @@
|
|||
use std::collections::{HashMap, HashSet};
|
||||
use std::hash::Hash;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use tokio::sync::Mutex as AsyncMutex;
|
||||
use tracing::warn;
|
||||
|
||||
use matrix_sdk::{
|
||||
encryption::verification::SasVerification,
|
||||
ruma::{OwnedRoomId, RoomId},
|
||||
};
|
||||
|
||||
use modalkit::{
|
||||
editing::{
|
||||
action::{Action, UIError, UIResult},
|
||||
application::{
|
||||
ApplicationAction,
|
||||
ApplicationContentId,
|
||||
ApplicationError,
|
||||
ApplicationInfo,
|
||||
ApplicationStore,
|
||||
ApplicationWindowId,
|
||||
},
|
||||
context::EditContext,
|
||||
store::Store,
|
||||
},
|
||||
env::vim::{
|
||||
command::{VimCommand, VimCommandMachine},
|
||||
keybindings::VimMachine,
|
||||
VimContext,
|
||||
},
|
||||
input::bindings::SequenceStatus,
|
||||
input::key::TerminalKey,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
message::{Message, Messages},
|
||||
worker::Requester,
|
||||
ApplicationSettings,
|
||||
};
|
||||
|
||||
const ROOM_FETCH_DEBOUNCE: Duration = Duration::from_secs(3);
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum IambInfo {}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum VerifyAction {
|
||||
Accept,
|
||||
Cancel,
|
||||
Confirm,
|
||||
Mismatch,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum IambAction {
|
||||
Verify(VerifyAction, String),
|
||||
VerifyRequest(String),
|
||||
SendMessage(OwnedRoomId, String),
|
||||
ToggleScrollbackFocus,
|
||||
}
|
||||
|
||||
impl ApplicationAction for IambAction {
|
||||
fn is_edit_sequence<C: EditContext>(&self, _: &C) -> SequenceStatus {
|
||||
match self {
|
||||
IambAction::SendMessage(..) => SequenceStatus::Break,
|
||||
IambAction::ToggleScrollbackFocus => SequenceStatus::Break,
|
||||
IambAction::Verify(..) => SequenceStatus::Break,
|
||||
IambAction::VerifyRequest(..) => SequenceStatus::Break,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_last_action<C: EditContext>(&self, _: &C) -> SequenceStatus {
|
||||
match self {
|
||||
IambAction::SendMessage(..) => SequenceStatus::Atom,
|
||||
IambAction::ToggleScrollbackFocus => SequenceStatus::Atom,
|
||||
IambAction::Verify(..) => SequenceStatus::Atom,
|
||||
IambAction::VerifyRequest(..) => SequenceStatus::Atom,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_last_selection<C: EditContext>(&self, _: &C) -> SequenceStatus {
|
||||
match self {
|
||||
IambAction::SendMessage(..) => SequenceStatus::Ignore,
|
||||
IambAction::ToggleScrollbackFocus => SequenceStatus::Ignore,
|
||||
IambAction::Verify(..) => SequenceStatus::Ignore,
|
||||
IambAction::VerifyRequest(..) => SequenceStatus::Ignore,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_switchable<C: EditContext>(&self, _: &C) -> bool {
|
||||
match self {
|
||||
IambAction::SendMessage(..) => false,
|
||||
IambAction::ToggleScrollbackFocus => false,
|
||||
IambAction::Verify(..) => false,
|
||||
IambAction::VerifyRequest(..) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IambAction> for ProgramAction {
|
||||
fn from(act: IambAction) -> Self {
|
||||
Action::Application(act)
|
||||
}
|
||||
}
|
||||
|
||||
pub type ProgramAction = Action<IambInfo>;
|
||||
pub type ProgramContext = VimContext<IambInfo>;
|
||||
pub type Keybindings = VimMachine<TerminalKey, IambInfo>;
|
||||
pub type ProgramCommand = VimCommand<ProgramContext, IambInfo>;
|
||||
pub type ProgramCommands = VimCommandMachine<ProgramContext, IambInfo>;
|
||||
pub type ProgramStore = Store<IambInfo>;
|
||||
pub type AsyncProgramStore = Arc<AsyncMutex<ProgramStore>>;
|
||||
|
||||
pub type IambResult<T> = UIResult<T, IambInfo>;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum IambError {
|
||||
#[error("Unknown room identifier: {0}")]
|
||||
InvalidUserId(String),
|
||||
|
||||
#[error("Invalid verification user/device pair: {0}")]
|
||||
InvalidVerificationId(String),
|
||||
|
||||
#[error("Cryptographic storage error: {0}")]
|
||||
CryptoStore(#[from] matrix_sdk::encryption::CryptoStoreError),
|
||||
|
||||
#[error("HTTP client error: {0}")]
|
||||
Http(#[from] matrix_sdk::HttpError),
|
||||
|
||||
#[error("Matrix client error: {0}")]
|
||||
Matrix(#[from] matrix_sdk::Error),
|
||||
|
||||
#[error("Matrix client storage error: {0}")]
|
||||
Store(#[from] matrix_sdk::StoreError),
|
||||
|
||||
#[error("Serialization/deserialization error: {0}")]
|
||||
Serde(#[from] serde_json::Error),
|
||||
|
||||
#[error("Unknown room identifier: {0}")]
|
||||
UnknownRoom(OwnedRoomId),
|
||||
|
||||
#[error("Verification request error: {0}")]
|
||||
VerificationRequestError(#[from] matrix_sdk::encryption::identities::RequestVerificationError),
|
||||
}
|
||||
|
||||
impl From<IambError> for UIError<IambInfo> {
|
||||
fn from(err: IambError) -> Self {
|
||||
UIError::Application(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl ApplicationError for IambError {}
|
||||
|
||||
#[derive(Default)]
|
||||
pub enum RoomFetchStatus {
|
||||
Done,
|
||||
HaveMore(String),
|
||||
#[default]
|
||||
NotStarted,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct RoomInfo {
|
||||
pub name: Option<String>,
|
||||
pub messages: Messages,
|
||||
pub fetch_id: RoomFetchStatus,
|
||||
pub fetch_last: Option<Instant>,
|
||||
}
|
||||
|
||||
impl RoomInfo {
|
||||
fn recently_fetched(&self) -> bool {
|
||||
self.fetch_last.map_or(false, |i| i.elapsed() < ROOM_FETCH_DEBOUNCE)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ChatStore {
|
||||
pub worker: Requester,
|
||||
pub rooms: HashMap<OwnedRoomId, RoomInfo>,
|
||||
pub names: HashMap<String, OwnedRoomId>,
|
||||
pub verifications: HashMap<String, SasVerification>,
|
||||
pub settings: ApplicationSettings,
|
||||
pub need_load: HashSet<OwnedRoomId>,
|
||||
}
|
||||
|
||||
impl ChatStore {
|
||||
pub fn new(worker: Requester, settings: ApplicationSettings) -> Self {
|
||||
ChatStore {
|
||||
worker,
|
||||
settings,
|
||||
|
||||
names: Default::default(),
|
||||
rooms: Default::default(),
|
||||
verifications: Default::default(),
|
||||
need_load: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mark_for_load(&mut self, room_id: OwnedRoomId) {
|
||||
self.need_load.insert(room_id);
|
||||
}
|
||||
|
||||
pub fn load_older(&mut self, limit: u32) {
|
||||
let ChatStore { need_load, rooms, worker, .. } = self;
|
||||
|
||||
for room_id in std::mem::take(need_load).into_iter() {
|
||||
let info = rooms.entry(room_id.clone()).or_default();
|
||||
|
||||
if info.recently_fetched() {
|
||||
need_load.insert(room_id);
|
||||
continue;
|
||||
} else {
|
||||
info.fetch_last = Instant::now().into();
|
||||
}
|
||||
|
||||
let fetch_id = match &info.fetch_id {
|
||||
RoomFetchStatus::Done => continue,
|
||||
RoomFetchStatus::HaveMore(fetch_id) => Some(fetch_id.clone()),
|
||||
RoomFetchStatus::NotStarted => None,
|
||||
};
|
||||
|
||||
let res = worker.load_older(room_id.clone(), fetch_id, limit);
|
||||
|
||||
match res {
|
||||
Ok((fetch_id, msgs)) => {
|
||||
for msg in msgs.into_iter() {
|
||||
let key = (msg.origin_server_ts().into(), msg.event_id().to_owned());
|
||||
|
||||
info.messages.insert(key, Message::from(msg));
|
||||
}
|
||||
|
||||
info.fetch_id =
|
||||
fetch_id.map_or(RoomFetchStatus::Done, RoomFetchStatus::HaveMore);
|
||||
},
|
||||
Err(e) => {
|
||||
warn!(
|
||||
room_id = room_id.as_str(),
|
||||
err = e.to_string(),
|
||||
"Failed to load older messages"
|
||||
);
|
||||
|
||||
// Wait and try again.
|
||||
need_load.insert(room_id);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_room_info(&mut self, room_id: OwnedRoomId) -> &mut RoomInfo {
|
||||
self.rooms.entry(room_id).or_default()
|
||||
}
|
||||
|
||||
pub fn set_room_name(&mut self, room_id: &RoomId, name: &str) {
|
||||
self.rooms.entry(room_id.to_owned()).or_default().name = name.to_string().into();
|
||||
}
|
||||
|
||||
pub fn insert_sas(&mut self, sas: SasVerification) {
|
||||
let key = format!("{}/{}", sas.other_user_id(), sas.other_device().device_id());
|
||||
|
||||
self.verifications.insert(key, sas);
|
||||
}
|
||||
}
|
||||
|
||||
impl ApplicationStore for ChatStore {}
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum IambId {
|
||||
Room(OwnedRoomId),
|
||||
DirectList,
|
||||
RoomList,
|
||||
SpaceList,
|
||||
VerifyList,
|
||||
Welcome,
|
||||
}
|
||||
|
||||
impl ApplicationWindowId for IambId {}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum RoomFocus {
|
||||
Scrollback,
|
||||
MessageBar,
|
||||
}
|
||||
|
||||
impl RoomFocus {
|
||||
pub fn is_scrollback(&self) -> bool {
|
||||
matches!(self, RoomFocus::Scrollback)
|
||||
}
|
||||
|
||||
pub fn is_msgbar(&self) -> bool {
|
||||
matches!(self, RoomFocus::MessageBar)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum IambBufferId {
|
||||
Command,
|
||||
Room(OwnedRoomId, RoomFocus),
|
||||
DirectList,
|
||||
RoomList,
|
||||
SpaceList,
|
||||
VerifyList,
|
||||
Welcome,
|
||||
}
|
||||
|
||||
impl IambBufferId {
|
||||
pub fn to_window(&self) -> Option<IambId> {
|
||||
match self {
|
||||
IambBufferId::Command => None,
|
||||
IambBufferId::Room(room, _) => Some(IambId::Room(room.clone())),
|
||||
IambBufferId::DirectList => Some(IambId::DirectList),
|
||||
IambBufferId::RoomList => Some(IambId::RoomList),
|
||||
IambBufferId::SpaceList => Some(IambId::SpaceList),
|
||||
IambBufferId::VerifyList => Some(IambId::VerifyList),
|
||||
IambBufferId::Welcome => Some(IambId::Welcome),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ApplicationContentId for IambBufferId {}
|
||||
|
||||
impl ApplicationInfo for IambInfo {
|
||||
type Error = IambError;
|
||||
type Store = ChatStore;
|
||||
type Action = IambAction;
|
||||
type WindowId = IambId;
|
||||
type ContentId = IambBufferId;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue