From 0041e69a5fcdea1bbf7d29c235bf9a6ee372d257 Mon Sep 17 00:00:00 2001 From: vaw Date: Tue, 25 Mar 2025 18:31:27 +0100 Subject: [PATCH] Add space tree on `:spaces!` --- src/base.rs | 9 ++ src/commands.rs | 8 +- src/windows/mod.rs | 21 +++- src/windows/spacetree.rs | 215 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 251 insertions(+), 2 deletions(-) create mode 100644 src/windows/spacetree.rs diff --git a/src/base.rs b/src/base.rs index 362a7c02..1626ec2e 100644 --- a/src/base.rs +++ b/src/base.rs @@ -1574,6 +1574,9 @@ pub enum IambId { /// The `:spaces` window. SpaceList, + /// The `:spaces!` window. + SpaceTree, + /// The `:verify` window. VerifyList, @@ -1602,6 +1605,7 @@ impl Display for IambId { IambId::DirectList => f.write_str("iamb://dms"), IambId::RoomList => f.write_str("iamb://rooms"), IambId::SpaceList => f.write_str("iamb://spaces"), + IambId::SpaceTree => f.write_str("iamb://spacetree"), IambId::VerifyList => f.write_str("iamb://verify"), IambId::Welcome => f.write_str("iamb://welcome"), IambId::ChatList => f.write_str("iamb://chats"), @@ -1803,6 +1807,9 @@ pub enum IambBufferId { /// The `:spaces` window. SpaceList, + /// The `:spaces!` window. + SpaceTree, + /// The `:verify` window. VerifyList, @@ -1826,6 +1833,7 @@ impl IambBufferId { IambBufferId::MemberList(room) => IambId::MemberList(room.clone()), IambBufferId::RoomList => IambId::RoomList, IambBufferId::SpaceList => IambId::SpaceList, + IambBufferId::SpaceTree => IambId::SpaceTree, IambBufferId::VerifyList => IambId::VerifyList, IambBufferId::Welcome => IambId::Welcome, IambBufferId::ChatList => IambId::ChatList, @@ -1861,6 +1869,7 @@ impl ApplicationInfo for IambInfo { IambBufferId::MemberList(_) => vec![], IambBufferId::RoomList => vec![], IambBufferId::SpaceList => vec![], + IambBufferId::SpaceTree => vec![], IambBufferId::VerifyList => vec![], IambBufferId::Welcome => vec![], IambBufferId::ChatList => vec![], diff --git a/src/commands.rs b/src/commands.rs index 2577bf4f..5e6ceb4c 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -337,7 +337,13 @@ fn iamb_spaces(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult { return Result::Err(CommandError::InvalidArgument); } - let open = ctx.switch(OpenTarget::Application(IambId::SpaceList)); + let target = if desc.bang { + IambId::SpaceTree + } else { + IambId::SpaceList + }; + + let open = ctx.switch(OpenTarget::Application(target)); let step = CommandStep::Continue(open, ctx.context.clone()); return Ok(step); diff --git a/src/windows/mod.rs b/src/windows/mod.rs index 12287817..639392b0 100644 --- a/src/windows/mod.rs +++ b/src/windows/mod.rs @@ -80,11 +80,12 @@ use crate::base::{ UnreadInfo, }; -use self::{room::RoomState, welcome::WelcomeState}; +use self::{room::RoomState, spacetree::SpaceTreeState, welcome::WelcomeState}; use crate::message::MessageTimeStamp; use feruca::Collator; pub mod room; +pub mod spacetree; pub mod welcome; type MatrixRoomInfo = Arc<(MatrixRoom, Option)>; @@ -326,6 +327,7 @@ macro_rules! delegate { IambWindow::MemberList($id, _, _) => $e, IambWindow::RoomList($id) => $e, IambWindow::SpaceList($id) => $e, + IambWindow::SpaceTree($id) => $e, IambWindow::VerifyList($id) => $e, IambWindow::Welcome($id) => $e, IambWindow::ChatList($id) => $e, @@ -341,6 +343,7 @@ pub enum IambWindow { VerifyList(VerifyListState), RoomList(RoomListState), SpaceList(SpaceListState), + SpaceTree(SpaceTreeState), Welcome(WelcomeState), ChatList(ChatListState), UnreadList(UnreadListState), @@ -452,6 +455,12 @@ impl From for IambWindow { } } +impl From for IambWindow { + fn from(tree: SpaceTreeState) -> Self { + IambWindow::SpaceTree(tree) + } +} + impl From for IambWindow { fn from(win: WelcomeState) -> Self { IambWindow::Welcome(win) @@ -513,6 +522,7 @@ impl WindowOps for IambWindow { fn draw(&mut self, area: Rect, buf: &mut Buffer, focused: bool, store: &mut ProgramStore) { match self { IambWindow::Room(state) => state.draw(area, buf, focused, store), + IambWindow::SpaceTree(state) => state.draw(area, buf, focused, store), IambWindow::DirectList(state) => { let mut items = store .application @@ -695,6 +705,7 @@ impl WindowOps for IambWindow { }, IambWindow::RoomList(w) => w.dup(store).into(), IambWindow::SpaceList(w) => w.dup(store).into(), + IambWindow::SpaceTree(w) => w.dup(store).into(), IambWindow::VerifyList(w) => w.dup(store).into(), IambWindow::Welcome(w) => w.dup(store).into(), IambWindow::ChatList(w) => w.dup(store).into(), @@ -736,6 +747,7 @@ impl Window for IambWindow { IambWindow::MemberList(_, room_id, _) => IambId::MemberList(room_id.clone()), IambWindow::RoomList(_) => IambId::RoomList, IambWindow::SpaceList(_) => IambId::SpaceList, + IambWindow::SpaceTree(_) => IambId::SpaceTree, IambWindow::VerifyList(_) => IambId::VerifyList, IambWindow::Welcome(_) => IambId::Welcome, IambWindow::ChatList(_) => IambId::ChatList, @@ -748,6 +760,7 @@ impl Window for IambWindow { IambWindow::DirectList(_) => bold_spans("Direct Messages"), IambWindow::RoomList(_) => bold_spans("Rooms"), IambWindow::SpaceList(_) => bold_spans("Spaces"), + IambWindow::SpaceTree(_) => bold_spans("Space Tree"), IambWindow::VerifyList(_) => bold_spans("Verifications"), IambWindow::Welcome(_) => bold_spans("Welcome to iamb"), IambWindow::ChatList(_) => bold_spans("DMs & Rooms"), @@ -776,6 +789,7 @@ impl Window for IambWindow { IambWindow::DirectList(_) => bold_spans("Direct Messages"), IambWindow::RoomList(_) => bold_spans("Rooms"), IambWindow::SpaceList(_) => bold_spans("Spaces"), + IambWindow::SpaceTree(_) => bold_spans("Space Tree"), IambWindow::VerifyList(_) => bold_spans("Verifications"), IambWindow::Welcome(_) => bold_spans("Welcome to iamb"), IambWindow::ChatList(_) => bold_spans("DMs & Rooms"), @@ -826,6 +840,11 @@ impl Window for IambWindow { return Ok(list.into()); }, + IambId::SpaceTree => { + let tree = SpaceTreeState::new(); + + return Ok(tree.into()); + }, IambId::VerifyList => { let list = VerifyListState::new(IambBufferId::VerifyList, vec![]); diff --git a/src/windows/spacetree.rs b/src/windows/spacetree.rs new file mode 100644 index 00000000..127f32da --- /dev/null +++ b/src/windows/spacetree.rs @@ -0,0 +1,215 @@ +use std::time::{Duration, Instant}; + +use modalkit::{ + actions::{Editable, EditorAction, Jumpable, PromptAction, Promptable, Scrollable}, + editing::completion::CompletionList, + errors::EditResult, + prelude::*, +}; +use modalkit_ratatui::{ + list::{List, ListState}, + TermOffset, + TerminalCursor, + WindowOps, +}; +use ratatui::{ + buffer::Buffer, + layout::Rect, + style::{Color, Style}, + text::{Line, Span, Text}, + widgets::StatefulWidget, +}; + +use crate::base::{ + IambBufferId, + IambInfo, + IambResult, + ProgramAction, + ProgramContext, + ProgramStore, +}; + +use crate::windows::RoomItem; + +use super::room_fields_cmp; + +const SPACE_HIERARCHY_DEBOUNCE: Duration = Duration::from_secs(15); + +/// [StatefulWidget] for Matrix space tree. +pub struct SpaceTree<'a> { + focused: bool, + store: &'a mut ProgramStore, +} + +impl<'a> SpaceTree<'a> { + pub fn new(store: &'a mut ProgramStore) -> Self { + SpaceTree { focused: false, store } + } + + pub fn focus(mut self, focused: bool) -> Self { + self.focused = focused; + self + } +} + +impl StatefulWidget for SpaceTree<'_> { + type State = SpaceTreeState; + + fn render(self, area: Rect, buffer: &mut Buffer, state: &mut Self::State) { + let mut empty_message = None; + let need_fetch = match state.last_fetch { + Some(i) => i.elapsed() >= SPACE_HIERARCHY_DEBOUNCE, + None => true, + }; + + if need_fetch { + let mut children = vec![]; + let res = self.store.application.sync_info.spaces.iter().try_for_each(|room| { + let id = room.0.room_id(); + let res = self.store.application.worker.space_members(id.to_owned()); + + res.map(|members| children.extend(members.into_iter().filter(|child| child != id))) + }); + + if let Err(e) = res { + let lines = vec![ + Line::from("Unable to fetch space room hierarchy:"), + Span::styled(e.to_string(), Style::default().fg(Color::Red)).into(), + ]; + + empty_message = Text::from(lines).into(); + } else { + let mut items = self + .store + .application + .sync_info + .spaces + .clone() + .into_iter() + .filter(|space| !children.contains(&space.0.room_id().to_owned())) + .map(|room| RoomItem::new(room, self.store)) + .collect::>(); + let fields = &self.store.application.settings.tunables.sort.spaces; + let collator = &mut self.store.application.collator; + items.sort_by(|a, b| room_fields_cmp(a, b, fields, collator)); + + state.list.set(items); + state.last_fetch = Some(Instant::now()); + } + } + + let mut list = List::new(self.store).focus(self.focused); + + if let Some(text) = empty_message { + list = list.empty_message(text); + } else { + list = list.empty_message(Text::from("You haven't joined any spaces yet")); + } + + list.render(area, buffer, &mut state.list) + } +} + +/// State for the list of toplevel spaces +pub struct SpaceTreeState { + list: ListState, + last_fetch: Option, +} + +impl SpaceTreeState { + pub fn new() -> Self { + let content = IambBufferId::SpaceTree; + let list = ListState::new(content, vec![]); + + SpaceTreeState { list, last_fetch: None } + } +} + +impl Editable for SpaceTreeState { + fn editor_command( + &mut self, + act: &EditorAction, + ctx: &ProgramContext, + store: &mut ProgramStore, + ) -> EditResult { + self.list.editor_command(act, ctx, store) + } +} + +impl Jumpable for SpaceTreeState { + fn jump( + &mut self, + list: PositionList, + dir: MoveDir1D, + count: usize, + ctx: &ProgramContext, + ) -> IambResult { + self.list.jump(list, dir, count, ctx) + } +} + +impl Scrollable for SpaceTreeState { + fn scroll( + &mut self, + style: &ScrollStyle, + ctx: &ProgramContext, + store: &mut ProgramStore, + ) -> EditResult { + self.list.scroll(style, ctx, store) + } +} + +impl Promptable for SpaceTreeState { + fn prompt( + &mut self, + act: &PromptAction, + ctx: &ProgramContext, + store: &mut ProgramStore, + ) -> EditResult, IambInfo> { + self.list.prompt(act, ctx, store) + } +} + +impl TerminalCursor for SpaceTreeState { + fn get_term_cursor(&self) -> Option { + self.list.get_term_cursor() + } +} + +impl WindowOps for SpaceTreeState { + fn draw(&mut self, area: Rect, buf: &mut Buffer, focused: bool, store: &mut ProgramStore) { + SpaceTree::new(store).focus(focused).render(area, buf, self); + } + + fn dup(&self, store: &mut ProgramStore) -> Self { + SpaceTreeState { + list: self.list.dup(store), + last_fetch: self.last_fetch, + } + } + + fn close(&mut self, flags: CloseFlags, store: &mut ProgramStore) -> bool { + self.list.close(flags, store) + } + + fn get_completions(&self) -> Option { + self.list.get_completions() + } + + fn get_cursor_word(&self, style: &WordStyle) -> Option { + self.list.get_cursor_word(style) + } + + fn get_selected_word(&self) -> Option { + self.list.get_selected_word() + } + + fn write( + &mut self, + path: Option<&str>, + flags: WriteFlags, + store: &mut ProgramStore, + ) -> IambResult { + self.list.write(path, flags, store) + } +}