From a5c25f248741927b352463df7d2e9d08354edaa3 Mon Sep 17 00:00:00 2001 From: Ulyssa Date: Fri, 28 Apr 2023 16:52:33 -0700 Subject: [PATCH] Support leaving rooms (#45) --- Cargo.lock | 79 +++++++++++++++++++++++++++++++--------- Cargo.toml | 2 +- src/base.rs | 5 ++- src/commands.rs | 29 +++++++++++++-- src/main.rs | 51 ++++++++++++++++++++++---- src/windows/mod.rs | 6 +-- src/windows/room/chat.rs | 20 ++++++++-- src/windows/room/mod.rs | 20 ++++++++++ src/worker.rs | 2 +- 9 files changed, 177 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d7ef7e4..8534450 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -625,6 +625,22 @@ dependencies = [ "winapi", ] +[[package]] +name = "crossterm" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84cda67535339806297f1b331d6dd6320470d2a0fe65381e79ee9e156dd3d13" +dependencies = [ + "bitflags 1.3.2", + "crossterm_winapi", + "libc", + "mio", + "parking_lot 0.12.1", + "signal-hook", + "signal-hook-mio", + "winapi", +] + [[package]] name = "crossterm_winapi" version = "0.9.0" @@ -2006,24 +2022,26 @@ dependencies = [ [[package]] name = "modalkit" -version = "0.0.14" +version = "0.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c48c7d7e6d764a09435b43a7e4d342ba2d2e026626ca773b16a5ba34b90b933" +checksum = "b44af1b5a7737da948719b907c870b4c852f1d98300d873bd12568f4028d908a" dependencies = [ "anymap2", "arboard", "bitflags 1.3.2", - "crossterm", + "crossterm 0.25.0", "derive_more", "intervaltree", "libc", "nom", "radix_trie", + "ratatui", "regex", "ropey", + "textwrap", "thiserror", - "tui", "unicode-segmentation", + "unicode-width", ] [[package]] @@ -2699,6 +2717,19 @@ dependencies = [ "rand_core 0.3.1", ] +[[package]] +name = "ratatui" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcc0d032bccba900ee32151ec0265667535c230169f5a011154cdcd984e16829" +dependencies = [ + "bitflags 1.3.2", + "cassowary", + "crossterm 0.26.1", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "rdrand" version = "0.4.0" @@ -3209,6 +3240,12 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +[[package]] +name = "smawk" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" + [[package]] name = "socket2" version = "0.4.9" @@ -3383,6 +3420,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "textwrap" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width", +] + [[package]] name = "thiserror" version = "1.0.40" @@ -3638,19 +3686,6 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" -[[package]] -name = "tui" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccdd26cbd674007e649a272da4475fb666d3aa0ad0531da7136db6fab0e5bad1" -dependencies = [ - "bitflags 1.3.2", - "cassowary", - "crossterm", - "unicode-segmentation", - "unicode-width", -] - [[package]] name = "typed-arena" version = "2.0.2" @@ -3684,6 +3719,16 @@ version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +[[package]] +name = "unicode-linebreak" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137" +dependencies = [ + "hashbrown", + "regex", +] + [[package]] name = "unicode-normalization" version = "0.1.22" diff --git a/Cargo.toml b/Cargo.toml index e0cfa18..34d4edd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,7 +40,7 @@ unicode-width = "0.1.10" url = {version = "^2.2.2", features = ["serde"]} [dependencies.modalkit] -version = "0.0.14" +version = "0.0.15" [dependencies.matrix-sdk] version = "0.6" diff --git a/src/base.rs b/src/base.rs index 7fe1282..ab7cb49 100644 --- a/src/base.rs +++ b/src/base.rs @@ -118,7 +118,9 @@ pub enum MessageAction { React(String), /// Redact a message, with an optional reason. - Redact(Option), + /// + /// The [bool] argument indicates whether to skip confirmation. + Redact(Option, bool), /// Reply to a message. Reply, @@ -178,6 +180,7 @@ pub enum RoomAction { InviteAccept, InviteReject, InviteSend(OwnedUserId), + Leave(bool), Members(Box>), Set(RoomField, String), Unset(RoomField), diff --git a/src/commands.rs b/src/commands.rs index ce06e5a..63ac904 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -161,6 +161,17 @@ fn iamb_members(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult { return Ok(step); } +fn iamb_leave(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult { + if !desc.arg.text.is_empty() { + return Result::Err(CommandError::InvalidArgument); + } + + let leave = IambAction::Room(RoomAction::Leave(desc.bang)); + let step = CommandStep::Continue(leave.into(), ctx.context.take()); + + return Ok(step); +} + fn iamb_cancel(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult { if !desc.arg.text.is_empty() { return Result::Err(CommandError::InvalidArgument); @@ -237,7 +248,8 @@ fn iamb_redact(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult { return Result::Err(CommandError::InvalidArgument); } - let ract = IambAction::from(MessageAction::Redact(args.into_iter().next())); + let reason = args.into_iter().next(); + let ract = IambAction::from(MessageAction::Redact(reason, desc.bang)); let step = CommandStep::Continue(ract.into(), ctx.context.take()); return Ok(step); @@ -469,6 +481,11 @@ fn add_iamb_commands(cmds: &mut ProgramCommands) { f: iamb_invite, }); cmds.add_command(ProgramCommand { name: "join".into(), aliases: vec![], f: iamb_join }); + cmds.add_command(ProgramCommand { + name: "leave".into(), + aliases: vec![], + f: iamb_leave, + }); cmds.add_command(ProgramCommand { name: "members".into(), aliases: vec![], @@ -870,15 +887,19 @@ mod tests { let ctx = ProgramContext::default(); let res = cmds.input_cmd("redact", ctx.clone()).unwrap(); - let act = IambAction::Message(MessageAction::Redact(None)); + let act = IambAction::Message(MessageAction::Redact(None, false)); + assert_eq!(res, vec![(act.into(), ctx.clone())]); + + let res = cmds.input_cmd("redact!", ctx.clone()).unwrap(); + let act = IambAction::Message(MessageAction::Redact(None, true)); assert_eq!(res, vec![(act.into(), ctx.clone())]); let res = cmds.input_cmd("redact Removed", ctx.clone()).unwrap(); - let act = IambAction::Message(MessageAction::Redact(Some("Removed".into()))); + let act = IambAction::Message(MessageAction::Redact(Some("Removed".into()), false)); assert_eq!(res, vec![(act.into(), ctx.clone())]); let res = cmds.input_cmd("redact \"Removed\"", ctx.clone()).unwrap(); - let act = IambAction::Message(MessageAction::Redact(Some("Removed".into()))); + let act = IambAction::Message(MessageAction::Redact(Some("Removed".into()), false)); assert_eq!(res, vec![(act.into(), ctx.clone())]); let res = cmds.input_cmd("redact Removed Removed", ctx.clone()); diff --git a/src/main.rs b/src/main.rs index fbc394c..cf3afec 100644 --- a/src/main.rs +++ b/src/main.rs @@ -76,12 +76,14 @@ use modalkit::{ EditInfo, Editable, EditorAction, + InfoMessage, InsertTextAction, Jumpable, Promptable, Scrollable, TabContainer, TabCount, + UIError, WindowAction, WindowContainer, }, @@ -90,7 +92,7 @@ use modalkit::{ key::KeyManager, store::Store, }, - input::{bindings::BindingMachine, key::TerminalKey}, + input::{bindings::BindingMachine, dialog::Pager, key::TerminalKey}, widgets::{ cmdbar::CommandBarState, screen::{Screen, ScreenState}, @@ -156,8 +158,7 @@ impl Application { } fn redraw(&mut self, full: bool, store: &mut ProgramStore) -> Result<(), std::io::Error> { - let modestr = self.bindings.showmode(); - let cursor = self.bindings.get_cursor_indicator(); + let bindings = &mut self.bindings; let sstate = &mut self.screen; let term = &mut self.terminal; @@ -168,9 +169,20 @@ impl Application { term.draw(|f| { let area = f.size(); - let screen = Screen::new(store).showmode(modestr).borders(true); + let modestr = bindings.show_mode(); + let cursor = bindings.get_cursor_indicator(); + let dialogstr = bindings.show_dialog(area.height as usize, area.width as usize); + + // Don't show terminal cursor when we show a dialog. + let hide_cursor = !dialogstr.is_empty(); + + let screen = Screen::new(store).show_dialog(dialogstr).show_mode(modestr).borders(true); f.render_stateful_widget(screen, area, sstate); + if hide_cursor { + return; + } + if let Some((cx, cy)) = sstate.get_term_cursor() { if let Some(c) = cursor { let style = Style::default().fg(Color::Green); @@ -215,7 +227,8 @@ impl Application { match self.screen.editor_command(&act, &ctx, store.deref_mut()) { Ok(None) => {}, Ok(Some(info)) => { - self.screen.push_info(info); + drop(store); + self.handle_info(info); }, Err(e) => { self.screen.push_error(e); @@ -279,7 +292,7 @@ impl Application { Action::CommandBar(act) => self.screen.command_bar(&act, &ctx)?, Action::Macro(act) => self.bindings.macro_command(&act, &ctx, store)?, Action::Scroll(style) => self.screen.scroll(&style, &ctx, store)?, - Action::Suspend => self.terminal.program_suspend()?, + Action::ShowInfoMessage(info) => Some(info), Action::Tab(cmd) => self.screen.tab_command(&cmd, &ctx, store)?, Action::Window(cmd) => self.screen.window_command(&cmd, &ctx, store)?, @@ -289,6 +302,11 @@ impl Application { None }, + Action::Suspend => { + self.terminal.program_suspend()?; + + None + }, // UI actions. Action::RedrawScreen => { @@ -402,6 +420,18 @@ impl Application { } } + fn handle_info(&mut self, info: InfoMessage) { + match info { + InfoMessage::Message(info) => { + self.screen.push_info(info); + }, + InfoMessage::Pager(text) => { + let pager = Box::new(Pager::new(text, vec![])); + self.bindings.run_dialog(pager); + }, + } + } + pub async fn run(&mut self) -> Result<(), std::io::Error> { self.terminal.clear()?; @@ -422,11 +452,18 @@ impl Application { continue; }, Ok(Some(info)) => { - self.screen.push_info(info); + self.handle_info(info); // Continue processing; we'll redraw later. continue; }, + Err( + UIError::NeedConfirm(dialog) | + UIError::EditingFailure(EditError::NeedConfirm(dialog)), + ) => { + self.bindings.run_dialog(dialog); + continue; + }, Err(e) => { self.screen.push_error(e); diff --git a/src/windows/mod.rs b/src/windows/mod.rs index 493bb17..4dec1d3 100644 --- a/src/windows/mod.rs +++ b/src/windows/mod.rs @@ -199,7 +199,7 @@ fn room_prompt( Err(err) }, - PromptAction::Recall(_, _) => { + PromptAction::Recall(..) => { let msg = "Cannot recall history inside a list"; let err = EditError::Failure(msg.into()); @@ -1043,7 +1043,7 @@ impl Promptable for VerifyItem { Err(err) }, - PromptAction::Recall(_, _) => { + PromptAction::Recall(..) => { let msg = "Cannot recall history inside a list"; let err = EditError::Failure(msg.into()); @@ -1120,7 +1120,7 @@ impl Promptable for MemberItem { Err(err) }, - PromptAction::Recall(_, _) => { + PromptAction::Recall(..) => { let msg = "Cannot recall history inside a list"; let err = EditError::Failure(msg.into()); diff --git a/src/windows/room/chat.rs b/src/windows/room/chat.rs index d6307a3..30d5f50 100644 --- a/src/windows/room/chat.rs +++ b/src/windows/room/chat.rs @@ -27,6 +27,7 @@ use matrix_sdk::{ }; use modalkit::{ + input::dialog::PromptYesNo, tui::{ buffer::Buffer, layout::Rect, @@ -40,6 +41,7 @@ use modalkit::{ use modalkit::editing::{ action::{ + Action, EditError, EditInfo, EditResult, @@ -310,7 +312,16 @@ impl ChatState { Ok(None) }, - MessageAction::Redact(reason) => { + MessageAction::Redact(reason, skip_confirm) => { + if !skip_confirm { + let msg = "Are you sure you want to redact this message?"; + let act = IambAction::Message(MessageAction::Redact(reason, true)); + let prompt = PromptYesNo::new(msg, vec![Action::from(act)]); + let prompt = Box::new(prompt); + + return Err(UIError::NeedConfirm(prompt)); + } + let room = self.get_joined(&store.application.worker)?; let event_id = match &msg.event { MessageEvent::EncryptedOriginal(ev) => ev.event_id.clone(), @@ -672,13 +683,14 @@ impl PromptActions for ChatState { &mut self, dir: &MoveDir1D, count: &Count, + prefixed: bool, ctx: &ProgramContext, _: &mut ProgramStore, ) -> EditResult, IambInfo> { let count = ctx.resolve(count); let rope = self.tbox.get(); - let text = self.sent.recall(&rope, &mut self.sent_scrollback, *dir, count); + let text = self.sent.recall(&rope, &mut self.sent_scrollback, *dir, prefixed, count); if let Some(text) = text { self.tbox.set_text(text); @@ -702,7 +714,9 @@ impl Promptable for ChatState { 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), + PromptAction::Recall(dir, count, prefixed) => { + self.recall(dir, count, *prefixed, ctx, store) + }, _ => Err(EditError::Unimplemented("unknown prompt action".to_string())), } } diff --git a/src/windows/room/mod.rs b/src/windows/room/mod.rs index eac9027..f82a2f6 100644 --- a/src/windows/room/mod.rs +++ b/src/windows/room/mod.rs @@ -43,11 +43,13 @@ use modalkit::{ WriteFlags, }, editing::completion::CompletionList, + input::dialog::PromptYesNo, input::InputContext, widgets::{TermOffset, TerminalCursor, WindowOps}, }; use crate::base::{ + IambAction, IambError, IambId, IambInfo, @@ -218,6 +220,24 @@ impl RoomState { Err(IambError::NotJoined.into()) } }, + RoomAction::Leave(skip_confirm) => { + if let Some(room) = store.application.worker.client.get_joined_room(self.id()) { + if skip_confirm { + room.leave().await.map_err(IambError::from)?; + + Ok(vec![]) + } else { + let msg = "Do you really want to leave this room?"; + let leave = IambAction::Room(RoomAction::Leave(true)); + let prompt = PromptYesNo::new(msg, vec![Action::from(leave)]); + let prompt = Box::new(prompt); + + Err(UIError::NeedConfirm(prompt)) + } + } else { + Err(IambError::NotJoined.into()) + } + }, RoomAction::Members(mut cmd) => { let width = Count::Exact(30); let act = diff --git a/src/worker.rs b/src/worker.rs index d4852a1..785593a 100644 --- a/src/worker.rs +++ b/src/worker.rs @@ -1215,7 +1215,7 @@ impl ClientWorker { let _req = request.await.map_err(IambError::from)?; let info = format!("Sent verification request to {user_id}"); - Ok(InfoMessage::from(info).into()) + Ok(Some(InfoMessage::from(info))) }, None => { let msg = format!("Could not find identity information for {user_id}");