1use std::{
41 cell::{Cell, RefCell},
42 collections::{HashMap, HashSet},
43 path::{Path, PathBuf},
44 rc::Rc,
45};
46
47use gtk4_layer_shell::{Edge, KeyboardMode, Layer, LayerShell};
48use relm4::gtk::{gdk, gio, glib, prelude::*};
49use relm4::prelude::*;
50
51use crate::cmd::{Command, WidthArg};
52use crate::config::{
53 Bookmark, BuiltinAction, Config, ContextAction, PANEL_MAX_WIDTH, PANEL_MIN_WIDTH, PanelConfig,
54 PanelLayer, PanelSide, SessionState, ShortcutTarget, StartupRoot, WorkspaceMove,
55 bookmark_file_path, load_stylesheet, save_bookmarks_to_path,
56};
57use crate::workspace::Hyprland;
58use crate::fs::SortKey;
59use crate::ipc;
60use crate::ui::bookmarks::{self, BookmarkEvent, MoveTarget};
61use crate::ui::toolbar::{
62 PaneShortcuts, Toolbar, ToolbarInit, ToolbarMsg, ToolbarOutput, parse_accelerator,
63};
64use crate::ui::tree::{Tree, TreeInit, TreeOutput, TreeMsg};
65
66#[derive(Debug, Clone, PartialEq, Eq)]
68enum ViewEntry {
69 Dir(PathBuf),
70 Bookmarks,
71}
72
73#[derive(Debug, Default, Clone, PartialEq, Eq)]
80struct NavHistory {
81 entries: Vec<ViewEntry>,
82 cursor: usize,
84 navigating: bool,
87}
88
89impl NavHistory {
90 fn record(&mut self, entry: ViewEntry) {
94 if self.navigating {
95 return;
96 }
97 if self.entries.get(self.cursor).is_some_and(|cur| cur == &entry) {
98 return;
99 }
100 if self.entries.is_empty() {
101 self.entries.push(entry);
102 self.cursor = 0;
103 return;
104 }
105 self.entries.truncate(self.cursor + 1);
106 self.entries.push(entry);
107 self.cursor = self.entries.len() - 1;
108 }
109
110 fn can_back(&self) -> bool {
111 self.cursor > 0 && self.cursor < self.entries.len()
112 }
113
114 fn can_forward(&self) -> bool {
115 !self.entries.is_empty() && self.cursor + 1 < self.entries.len()
116 }
117
118 fn back(&mut self) -> Option<ViewEntry> {
120 if !self.can_back() {
121 return None;
122 }
123 self.cursor -= 1;
124 self.navigating = true;
125 Some(self.entries[self.cursor].clone())
126 }
127
128 fn forward(&mut self) -> Option<ViewEntry> {
130 if !self.can_forward() {
131 return None;
132 }
133 self.cursor += 1;
134 self.navigating = true;
135 Some(self.entries[self.cursor].clone())
136 }
137
138 fn finish_navigation(&mut self) {
140 self.navigating = false;
141 }
142}
143
144pub struct Pane {
146 id: u64,
147 toolbar: Controller<Toolbar>,
148 tree: Controller<Tree>,
149 workspace: String,
152 root: Option<PathBuf>,
155 canonical_root: Option<PathBuf>,
158 history: NavHistory,
160 filter_bar: gtk::Box,
163 filter_entry: gtk::SearchEntry,
164 body: gtk::Stack,
168 bookmarks_list: gtk::Box,
171 bookmark_filter: String,
174 bookmark_drag: Rc<bookmarks::BookmarkDrag>,
176 bookmark_nav: Rc<bookmarks::BookmarkNav>,
178 widget: gtk::Overlay,
184 grip: gtk::Image,
188 nav_buttons: Option<NavButtons>,
191 shortcuts: Rc<PaneShortcuts>,
194}
195
196struct NavButtons {
198 up: gtk::Button,
199 back: gtk::Button,
200 forward: gtk::Button,
201}
202
203impl Pane {
204 fn set_root(&mut self, root: PathBuf) {
207 self.canonical_root = std::fs::canonicalize(&root).ok();
208 self.root = Some(root);
209 self.refresh_nav();
210 }
211
212 fn show_bookmarks(&self) {
214 self.tree.emit(TreeMsg::Suspend);
217 self.body.set_visible_child_name("bookmarks");
218 self.toolbar.emit(ToolbarMsg::SetBookmarks(true));
219 self.refresh_nav();
220 }
221
222 fn show_tree(&self) {
224 self.body.set_visible_child_name("tree");
225 self.toolbar.emit(ToolbarMsg::SetBookmarks(false));
226 self.refresh_nav();
227 }
228
229 fn on_bookmarks(&self) -> bool {
231 self.body.visible_child_name().as_deref() == Some("bookmarks")
232 }
233
234 fn refresh_nav(&self) {
239 let Some(nav) = &self.nav_buttons else {
240 return;
241 };
242 let has_parent = !self.on_bookmarks()
243 && self.root.as_deref().and_then(Path::parent).is_some();
244 nav.up.set_sensitive(has_parent);
245 nav.back.set_sensitive(self.history.can_back());
246 nav.forward.set_sensitive(self.history.can_forward());
247 }
248}
249
250struct Dock {
255 side: PanelSide,
256 monitor: Option<gdk::Monitor>,
259 window: gtk::Window,
262 container: gtk::Box,
265 panes: Vec<Pane>,
266 active_pane: Option<u64>,
268}
269
270struct PaneDrag {
272 id: u64,
274 base: (f64, f64),
277 highlighted: Option<usize>,
279}
280
281#[derive(Debug, Clone, Copy, PartialEq, Eq)]
285pub enum VisibilityIntent {
286 ShowAll,
288 HideAll,
290 ToggleAll,
292 ShowSide(PanelSide),
294 HideSide(PanelSide),
296 ToggleSide(PanelSide),
298}
299
300#[derive(Debug, Clone, PartialEq, Eq)]
306pub struct VisibilityPlan {
307 pub show: Vec<PanelSide>,
309 pub hide: Vec<PanelSide>,
311 pub create: Option<PanelSide>,
313 pub seed: bool,
316}
317
318pub fn resolve_visibility(
322 intent: VisibilityIntent,
323 existing: &[PanelSide],
324 shown: &[PanelSide],
325) -> VisibilityPlan {
326 match intent {
327 VisibilityIntent::ShowAll => VisibilityPlan {
328 show: existing.to_vec(),
329 hide: Vec::new(),
330 create: None,
331 seed: true,
332 },
333 VisibilityIntent::HideAll => VisibilityPlan {
334 show: Vec::new(),
335 hide: PanelSide::ALL.to_vec(),
336 create: None,
337 seed: false,
338 },
339 VisibilityIntent::ToggleAll => {
340 if shown.is_empty() {
341 VisibilityPlan {
342 show: existing.to_vec(),
343 hide: Vec::new(),
344 create: None,
345 seed: true,
346 }
347 } else {
348 VisibilityPlan {
349 show: Vec::new(),
350 hide: PanelSide::ALL.to_vec(),
351 create: None,
352 seed: false,
353 }
354 }
355 }
356 VisibilityIntent::ShowSide(side) => VisibilityPlan {
357 show: vec![side],
358 hide: Vec::new(),
359 create: (!existing.contains(&side)).then_some(side),
360 seed: true,
361 },
362 VisibilityIntent::HideSide(side) => VisibilityPlan {
363 show: Vec::new(),
364 hide: vec![side],
365 create: None,
366 seed: false,
367 },
368 VisibilityIntent::ToggleSide(side) => {
369 if !existing.contains(&side) {
370 VisibilityPlan {
371 show: vec![side],
372 hide: Vec::new(),
373 create: Some(side),
374 seed: true,
375 }
376 } else if shown.contains(&side) {
377 VisibilityPlan {
378 show: Vec::new(),
379 hide: vec![side],
380 create: None,
381 seed: false,
382 }
383 } else {
384 VisibilityPlan {
385 show: vec![side],
386 hide: Vec::new(),
387 create: None,
388 seed: true,
389 }
390 }
391 }
392 }
393}
394
395#[derive(Debug)]
397pub enum AppMsg {
398 PaneToolbar { id: u64, out: ToolbarOutput },
400 PaneTree { id: u64, out: TreeOutput },
402 PaneShortcut { id: u64, action: ContextAction },
405 OpenFolderPicked { id: u64, path: Option<PathBuf> },
407 LaunchRequest { command: Command },
409 ActiveWorkspace(String),
411 SplitFromPane { id: u64 },
413 OpenSplitFrom { id: u64, root: PathBuf },
416 OpenOppositeFrom { id: u64, root: PathBuf },
419 ClosePane { id: u64 },
422 FilterChanged { id: u64, filter: String },
424 FilterClosed { id: u64 },
426 Shutdown,
428 ResizeBy { side: PanelSide, delta: i32 },
430 ResizeCommit,
432 PaneDragBegin { id: u64, start: (f64, f64) },
435 PaneDragUpdate { id: u64, offset: (f64, f64) },
437 PaneDragEnd { id: u64, offset: (f64, f64) },
440 ResolveDockMonitor { di: usize },
442 FocusPane { id: u64 },
444 FocusVisible,
447 BookmarkEvent { id: u64, event: BookmarkEvent },
449 BookmarkEditSaved { index_path: Vec<usize>, name: String, path: Option<PathBuf> },
451 BookmarkAdded { name: String, path: PathBuf },
453 BookmarkFolderAdded { name: String },
455}
456
457pub struct AppInit {
460 pub command: Command,
461 pub listener: Option<std::os::unix::net::UnixListener>,
462}
463
464pub struct App {
466 config: Config,
467 status: String,
468 window: gtk::Window,
469 file_dialog: gtk::FileDialog,
470 docks: Vec<Dock>,
472 pane_container: gtk::Box,
474 primary_side: PanelSide,
476 hypr: Option<Hyprland>,
479 active_workspace: String,
482 next_id: u64,
484 hidden_workspaces: HashSet<String>,
488 widths: HashMap<PanelSide, u32>,
492 last_root: Option<PathBuf>,
494 bookmarks: Vec<Bookmark>,
497 pane_drag: Option<PaneDrag>,
499 save_generation: Rc<Cell<u64>>,
503 sender: ComponentSender<App>,
507}
508
509#[relm4::component(pub)]
510impl SimpleComponent for App {
511 type Init = AppInit;
512 type Input = AppMsg;
513 type Output = ();
514
515 view! {
516 gtk::Window {
517 set_default_size: (model.config.panel.width as i32, 520),
518
519 gtk::Box {
520 set_orientation: gtk::Orientation::Vertical,
521 set_spacing: 0,
522 add_css_class: "panel",
523
524 append: &model.pane_container,
525
526 append: status = >k::Label {
527 #[watch]
528 set_label: &model.status,
529 set_halign: gtk::Align::Start,
530 set_ellipsize: gtk::pango::EllipsizeMode::End,
531 add_css_class: "status-bar",
532 }
533 }
534 }
535 }
536
537 fn init(
538 init: Self::Init,
539 root: Self::Root,
540 sender: ComponentSender<Self>,
541 ) -> ComponentParts<Self> {
542 let loaded = Config::load();
543 let mut config = loaded.config;
544 let parent = root.clone();
545
546 let (bookmarks, bookmark_problem) = crate::config::load_bookmarks(&config.bookmarks);
549 if let Err(problem) = crate::config::ensure_bookmarks_file(&config.bookmarks, &bookmarks) {
550 eprintln!("tree-space: could not create bookmarks file: {problem:?}");
551 }
552
553 let session = SessionState::load();
554 let default_width = config.panel.width;
557 let widths: HashMap<PanelSide, u32> = [
558 (
559 PanelSide::Left,
560 session.left_width.unwrap_or(default_width).clamp(PANEL_MIN_WIDTH, PANEL_MAX_WIDTH),
561 ),
562 (
563 PanelSide::Right,
564 session.right_width.unwrap_or(default_width).clamp(PANEL_MIN_WIDTH, PANEL_MAX_WIDTH),
565 ),
566 ]
567 .into_iter()
568 .collect();
569
570 let primary_side = init.command.side.unwrap_or(config.panel.side);
571 config.panel.width = widths[&primary_side];
573 let primary_width = config.panel.width;
574 let hypr = Hyprland::connect();
578 let active_workspace =
579 hypr.as_ref().and_then(Hyprland::active_workspace).unwrap_or_default();
580 let roots = if !init.command.roots.is_empty() {
584 init.command.roots.clone()
585 } else if config.startup.is_bookmarks() {
586 Vec::new()
587 } else {
588 let last = session.last_root.clone().filter(|p| p.is_dir());
589 let fallback = config
590 .startup
591 .resolve(last)
592 .filter(|p| p.is_dir())
593 .or_else(home_dir);
594 if let Some(p) = fallback { vec![p] } else { Vec::new() }
595 };
596
597 let mut panes: Vec<Pane> = Vec::new();
598 let mut next_id = 0u64;
599 for root in roots.iter() {
600 let pane = make_pane(
601 &config,
602 parent.clone(),
603 next_id,
604 primary_side,
605 primary_width,
606 active_workspace.clone(),
607 sender.clone(),
608 );
609 pane.tree.emit(TreeMsg::OpenRoot(root.clone()));
610 panes.push(pane);
611 panes.last_mut().unwrap().set_root(root.clone());
612 next_id += 1;
613 }
614 if panes.is_empty() {
617 let mut pane = make_pane(
618 &config,
619 parent.clone(),
620 next_id,
621 primary_side,
622 primary_width,
623 active_workspace.clone(),
624 sender.clone(),
625 );
626 pane.show_bookmarks();
627 pane.history.record(ViewEntry::Bookmarks);
628 pane.refresh_nav();
629 panes.push(pane);
630 next_id += 1;
631 }
632
633 let pane_container = gtk::Box::new(gtk::Orientation::Vertical, 0);
634 pane_container.set_vexpand(true);
635 fill_pane_container(&pane_container, &panes, &active_workspace);
636
637 let mut status = String::new();
638 if let Some(problem) = loaded.problem {
639 status = format!("config: {problem:?}");
640 } else if let Some(problem) = bookmark_problem {
641 status = format!("bookmarks: {problem:?}");
642 }
643
644 let hidden_workspaces = if init.command.hidden {
646 HashSet::from([active_workspace.clone()])
647 } else {
648 HashSet::new()
649 };
650
651 let mut model = App {
652 config,
653 status,
654 window: root.clone(),
655 file_dialog: gtk::FileDialog::new(),
656 docks: vec![Dock {
657 side: primary_side,
658 monitor: None,
659 window: root.clone(),
660 container: pane_container.clone(),
661 panes,
662 active_pane: None,
663 }],
664 pane_container,
665 primary_side,
666 hypr: hypr.clone(),
667 active_workspace,
668 next_id,
669 hidden_workspaces,
670 widths,
671 last_root: session.last_root.clone(),
672 bookmarks,
673 pane_drag: None,
674 save_generation: Rc::new(Cell::new(0)),
675 sender: sender.clone(),
676 };
677
678 if let Some(hypr) = &hypr {
681 let workspace_sender = sender.clone();
682 hypr.spawn_watcher(move |name| {
683 let sender = workspace_sender.clone();
684 glib::MainContext::default().invoke(move || {
685 sender.input(AppMsg::ActiveWorkspace(name));
686 });
687 });
688 }
689
690 init_layer_window(&model.window, &model.config, primary_side, primary_width, None);
691 install_css(&model.config);
692 if layer_shell_available() {
695 attach_resize_controls(&model.window, primary_side, &sender);
696 }
697
698 if let Some(listener) = init.listener {
701 let ipc_sender = sender.input_sender().clone();
702 ipc::spawn_listener(listener, move |command| {
703 let sender = ipc_sender.clone();
704 glib::MainContext::default().invoke(move || {
705 let _ = sender.send(AppMsg::LaunchRequest { command });
706 });
707 });
708 }
709
710 let widgets = view_output!();
711
712 {
716 let sender = sender.clone();
717 model.window.connect_map(move |_| {
718 let sender = sender.clone();
719 sender.input(AppMsg::ResolveDockMonitor { di: 0 });
721 glib::idle_add_local_once(move || sender.input(AppMsg::FocusVisible));
722 });
723 }
724
725 model.refresh_all_bookmarks(&sender);
727
728 let shutting_down = Rc::new(Cell::new(false));
735 let flag = shutting_down.clone();
736 let shutdown_sender = sender.clone();
737 model.window.connect_close_request(move |_| {
738 if flag.get() {
739 return glib::Propagation::Proceed;
740 }
741 flag.set(true);
742 shutdown_sender.input(AppMsg::Shutdown);
743 glib::Propagation::Stop
744 });
745
746 ComponentParts { model, widgets }
747 }
748
749 fn update(&mut self, msg: Self::Input, sender: ComponentSender<Self>) {
750 match msg {
751 AppMsg::PaneToolbar { id, out } => match out {
752 ToolbarOutput::OpenFolder => {
753 self.set_active(id);
754 self.show_open_folder(id, &sender);
755 }
756 ToolbarOutput::NavigateTo(path) => {
757 self.open_root_in_pane(id, path);
758 }
759 ToolbarOutput::FilterRequested => {
760 self.set_active(id);
761 self.open_filter(id);
762 }
763 ToolbarOutput::SplitView => {
764 sender.input(AppMsg::SplitFromPane { id });
765 }
766 ToolbarOutput::Collapse => {
767 self.collapse_dock(id);
768 }
769 ToolbarOutput::ClosePane => {
770 sender.input(AppMsg::ClosePane { id });
771 }
772 ToolbarOutput::PaneItem(action) => self.run_pane_shortcut(id, action, &sender),
773 },
774
775 AppMsg::PaneTree { id, out } => match out {
776 TreeOutput::Status(message) => {
777 self.set_active(id);
778 self.status = message;
779 }
780 TreeOutput::OpenFolderRequested => {
781 self.set_active(id);
782 self.show_open_folder(id, &sender);
783 }
784 TreeOutput::OpenSplit(root) => {
785 self.set_active(id);
786 sender.input(AppMsg::OpenSplitFrom { id, root });
787 }
788 TreeOutput::OpenOpposite(root) => {
789 self.set_active(id);
790 sender.input(AppMsg::OpenOppositeFrom { id, root });
791 }
792 TreeOutput::PaneAction(action) => {
793 self.dispatch_pane_builtin(id, action, &sender);
794 }
795 TreeOutput::AddBookmark(path) => {
796 self.set_active(id);
797 self.add_bookmark(path);
798 self.refresh_all_bookmarks(&sender);
799 }
800 TreeOutput::RootChanged(root) => {
801 self.set_active(id);
802 if let Some((di, pi)) = self.dock_pane_of(id) {
803 let pane = &mut self.docks[di].panes[pi];
804 pane.history.record(ViewEntry::Dir(root.clone()));
808 pane.history.finish_navigation();
809 pane.show_tree();
810 pane.set_root(root.clone());
811 pane.toolbar.emit(ToolbarMsg::SetRoot(root.clone()));
812 }
813 self.status = String::new();
814 self.last_root = Some(root);
816 self.persist_session();
817 }
818 },
819
820 AppMsg::PaneShortcut { id, action } => {
821 self.run_pane_shortcut(id, action, &sender);
822 }
823
824 AppMsg::OpenFolderPicked { id, path: Some(path) } => {
825 self.open_root_in_pane(id, path);
826 }
827 AppMsg::OpenFolderPicked { path: None, .. } => {}
828
829 AppMsg::LaunchRequest { command } => {
830 self.handle_launch(command, &sender);
831 }
832
833 AppMsg::ActiveWorkspace(name) => self.on_active_workspace(name),
834
835 AppMsg::SplitFromPane { id } => {
836 let di = self.dock_pane_of(id).map(|(di, _)| di).unwrap_or(0);
839 self.add_pane(di, None, &sender);
840 }
841
842 AppMsg::OpenSplitFrom { id, root } => {
843 if let Some((di, _pi)) = self.dock_pane_of(id) {
844 self.add_pane(di, Some(root), &sender);
845 }
846 }
847
848 AppMsg::OpenOppositeFrom { id, root } => {
849 if let Some((di, _pi)) = self.dock_pane_of(id) {
850 let side = self.docks[di].side.opposite();
851 let monitor = self.docks[di].monitor.clone();
852 let target = self.ensure_dock(side, monitor, false, &sender);
853 self.add_pane(target, Some(root), &sender);
854 let visible = self.visible();
857 self.set_docks_visible(visible);
858 }
859 }
860
861 AppMsg::ClosePane { id } => {
862 let Some((di, pi)) = self.dock_pane_of(id) else {
863 return;
864 };
865 self.docks[di].panes[pi].tree.emit(TreeMsg::Suspend);
869 self.docks[di].panes.remove(pi);
870 if self.docks[di].active_pane == Some(id) {
871 self.docks[di].active_pane = self.last_active_pane_id(di);
872 }
873 let panes_left: usize = self.docks.iter().map(|d| d.panes.len()).sum();
875 if panes_left == 0 {
876 self.window.close();
877 return;
878 }
879 if self.docks[di].panes.is_empty() && di != 0 {
880 let dock = self.docks.remove(di);
882 dock.window.close();
883 } else {
884 fill_pane_container(
885 &self.docks[di].container.clone(),
886 &self.docks[di].panes,
887 &self.active_workspace,
888 );
889 let shown = self.visible() && self.dock_has_active_pane(di);
894 self.docks[di].window.set_visible(shown);
895 }
896 }
897
898 AppMsg::FilterChanged { id, filter } => {
899 if let Some((di, pi)) = self.dock_pane_of(id) {
900 if self.docks[di].panes[pi].on_bookmarks() {
901 self.docks[di].panes[pi].bookmark_filter = filter;
902 self.refresh_all_bookmarks(&sender);
903 } else {
904 self.docks[di].panes[pi].tree.emit(TreeMsg::SetFilter(filter));
905 }
906 }
907 }
908 AppMsg::FilterClosed { id } => {
909 if let Some((di, pi)) = self.dock_pane_of(id) {
910 let pane = &mut self.docks[di].panes[pi];
911 pane.filter_entry.set_text("");
912 pane.tree.emit(TreeMsg::SetFilter(String::new()));
913 pane.bookmark_filter.clear();
914 pane.filter_bar.set_visible(false);
915 }
916 self.refresh_all_bookmarks(&sender);
917 }
918
919 AppMsg::FocusPane { id } => {
920 self.focus_pane(id);
921 }
922
923 AppMsg::FocusVisible => {
924 let ids: Vec<u64> = (0..self.docks.len())
925 .filter(|&di| self.docks[di].window.is_visible())
926 .filter_map(|di| self.last_active_pane_id(di))
927 .collect();
928 for id in ids {
929 self.focus_pane(id);
930 }
931 }
932
933 AppMsg::Shutdown => {
934 for dock in &self.docks {
939 stop_video_widgets(dock.window.upcast_ref());
940 }
941 for dock in &self.docks {
943 for pane in &dock.panes {
944 pane.tree.emit(TreeMsg::Shutdown);
945 }
946 }
947 let window = self.window.clone();
950 glib::timeout_add_local_once(std::time::Duration::from_millis(SHUTDOWN_GRACE_MS), move || {
951 window.close();
952 });
953 }
954
955 AppMsg::ResizeBy { side, delta } => self.resize_by(side, delta),
956 AppMsg::ResizeCommit => self.persist_session(),
957
958 AppMsg::ResolveDockMonitor { di } => {
959 let Some(dock) = self.docks.get(di) else { return };
960 if dock.monitor.is_some() {
961 return;
962 }
963 let (Some(surface), Some(display)) =
964 (dock.window.surface(), gdk::Display::default())
965 else {
966 return;
967 };
968 if let Some(monitor) = display.monitor_at_surface(&surface) {
969 self.docks[di].monitor = Some(monitor);
970 }
971 }
972
973 AppMsg::PaneDragBegin { id, start } => self.begin_pane_drag(id, start),
974 AppMsg::PaneDragUpdate { id, offset } => self.update_pane_drag(id, offset),
975 AppMsg::PaneDragEnd { id, offset } => self.end_pane_drag(id, offset, &sender),
976
977 AppMsg::BookmarkEvent { id, event } => match event {
978 BookmarkEvent::Open(path) => self.open_bookmark(id, path, &sender),
979 BookmarkEvent::Toggle(index_path) => {
980 if let Some(entry) = Bookmark::get_mut(&mut self.bookmarks, &index_path) {
981 entry.expanded = !entry.expanded;
982 }
983 self.refresh_all_bookmarks(&sender);
984 }
985 BookmarkEvent::Edit(index_path) => self.edit_bookmark(id, index_path, &sender),
986 BookmarkEvent::NewBookmark => self.new_bookmark(id, &sender),
987 BookmarkEvent::NewBookmarkFolder => self.new_bookmark_folder(id, &sender),
988 BookmarkEvent::Action { path, target } => {
989 self.run_bookmark_action(id, path, target);
990 }
991 BookmarkEvent::Move { from, to } => self.move_bookmark(from, to, &sender),
992 BookmarkEvent::Delete(index_path) => {
993 if Bookmark::remove(&mut self.bookmarks, &index_path) {
994 self.save_bookmarks();
995 self.refresh_all_bookmarks(&sender);
996 }
997 }
998 },
999 AppMsg::BookmarkEditSaved { index_path, name, path } => {
1000 if let Some(entry) = Bookmark::get_mut(&mut self.bookmarks, &index_path) {
1001 entry.name = name;
1002 if path.is_some() {
1004 entry.path = path;
1005 }
1006 self.save_bookmarks();
1007 self.refresh_all_bookmarks(&sender);
1008 }
1009 }
1010 AppMsg::BookmarkAdded { name, path } => {
1011 if Bookmark::contains_path(&self.bookmarks, &path) {
1012 self.status = format!("{} is already bookmarked", path.display());
1013 return;
1014 }
1015 self.bookmarks.push(Bookmark::leaf(name, path));
1016 self.save_bookmarks();
1017 self.refresh_all_bookmarks(&sender);
1018 }
1019 AppMsg::BookmarkFolderAdded { name } => {
1020 self.bookmarks.push(Bookmark {
1021 name,
1022 path: None,
1023 items: Vec::new(),
1024 expanded: false,
1025 });
1026 self.save_bookmarks();
1027 self.refresh_all_bookmarks(&sender);
1028 }
1029 }
1030 }
1031}
1032
1033impl App {
1034 fn handle_launch(&mut self, command: Command, sender: &ComponentSender<Self>) {
1036 if let Some(accel) = command.key.clone() {
1040 match self.active_pane_id(command.side) {
1041 Some(id) => {
1042 self.run_accelerator(id, &accel, sender);
1043 return;
1044 }
1045 None if !self.docks.is_empty() => return,
1046 None => {}
1047 }
1048 }
1049
1050 let hidden = command.hidden;
1051
1052 if let Some(width) = command.width {
1055 let side = command.side.unwrap_or(self.primary_side);
1056 match width {
1057 WidthArg::To(px) => self.set_width(side, px),
1058 WidthArg::By(delta) => self.resize_by(side, delta),
1059 }
1060 self.persist_session();
1061 if command.roots.is_empty() && command.reveal.is_empty() {
1062 return;
1063 }
1064 }
1065
1066 if command.roots.is_empty() && command.reveal.is_empty() {
1067 let intent = match (command.side, hidden) {
1070 (Some(side), true) => VisibilityIntent::HideSide(side),
1071 (Some(side), false) => VisibilityIntent::ToggleSide(side),
1072 (None, true) => VisibilityIntent::HideAll,
1073 (None, false) => VisibilityIntent::ToggleAll,
1074 };
1075 self.apply_visibility(intent, sender);
1076 return;
1077 }
1078
1079 for root in &command.roots {
1080 if self.find_pane_with_dir(root).is_some() {
1081 continue;
1084 }
1085 let side = command.side.unwrap_or(self.primary_side);
1086 let monitor = self.default_monitor();
1087 let di = self.ensure_dock(side, monitor, false, sender);
1088 self.add_pane(di, Some(root.clone()), sender);
1089 }
1090
1091 for (root, select) in command.reveal_targets() {
1096 if !root.is_dir() {
1097 continue;
1098 }
1099 let side = command.side.unwrap_or(self.primary_side);
1100 let monitor = self.default_monitor();
1101 let di = self.ensure_dock(side, monitor, false, sender);
1102 let id = self.add_pane(di, Some(root), sender);
1103 if let Some(select) = select
1104 && let Some((di, pi)) = self.dock_pane_of(id)
1105 {
1106 self.docks[di].panes[pi].tree.emit(TreeMsg::SelectPath(select));
1107 }
1108 }
1109 self.set_docks_visible(!hidden);
1111 }
1112
1113 fn apply_visibility(&mut self, intent: VisibilityIntent, sender: &ComponentSender<Self>) {
1117 let mut existing: Vec<PanelSide> = Vec::new();
1120 let mut shown: Vec<PanelSide> = Vec::new();
1121 for dock in &self.docks {
1122 if !existing.contains(&dock.side) {
1123 existing.push(dock.side);
1124 }
1125 if dock.window.is_visible() && !shown.contains(&dock.side) {
1126 shown.push(dock.side);
1127 }
1128 }
1129 let plan = resolve_visibility(intent, &existing, &shown);
1130
1131 let whole_panel =
1136 matches!(intent, VisibilityIntent::ShowAll | VisibilityIntent::ToggleAll);
1137 let panes_exist = self
1140 .docks
1141 .iter()
1142 .flat_map(|dock| &dock.panes)
1143 .any(|pane| pane.workspace == self.active_workspace);
1144 let seed_empty = should_seed_empty(panes_exist, plan.seed, whole_panel);
1145
1146 if let Some(side) = plan.create {
1147 let monitor = self.default_monitor();
1148 self.ensure_dock(side, monitor, plan.seed, sender);
1149 }
1150 for side in &plan.show {
1151 if self.dock_of_side(*side).is_none() {
1154 let monitor = self.default_monitor();
1155 self.ensure_dock(*side, monitor, seed_empty || !whole_panel, sender);
1156 }
1157 let indices: Vec<usize> = self
1158 .docks
1159 .iter()
1160 .enumerate()
1161 .filter(|(_, dock)| dock.side == *side)
1162 .map(|(di, _)| di)
1163 .collect();
1164 for di in indices {
1165 if seed_empty && !self.dock_has_active_pane(di) {
1166 let startup = self.config.startup.clone();
1167 match default_root(&startup) {
1168 Some(root) => self.add_pane(di, Some(root), sender),
1169 None => self.add_pane(di, None, sender),
1170 };
1171 }
1172 let show = self.dock_has_active_pane(di);
1174 self.docks[di].window.set_visible(show);
1175 }
1176 }
1177 for side in &plan.hide {
1178 let indices: Vec<usize> = self
1179 .docks
1180 .iter()
1181 .enumerate()
1182 .filter(|(_, dock)| dock.side == *side)
1183 .map(|(di, _)| di)
1184 .collect();
1185 for di in indices {
1186 self.docks[di].window.set_visible(false);
1187 self.suspend_dock_panes(di);
1188 }
1189 }
1190 self.refresh_visible();
1192 let focus: Vec<u64> = plan
1195 .show
1196 .iter()
1197 .flat_map(|side| {
1198 self.docks
1199 .iter()
1200 .filter(move |dock| dock.side == *side)
1201 .filter_map(|dock| dock.panes.last())
1202 .map(|pane| pane.id)
1203 })
1204 .collect();
1205 for id in focus {
1206 self.focus_pane(id);
1207 }
1208 }
1209
1210 fn set_active(&mut self, id: u64) {
1212 if let Some((di, _pi)) = self.dock_pane_of(id) {
1213 self.docks[di].active_pane = Some(id);
1214 }
1215 }
1216
1217 fn focus_pane(&mut self, id: u64) {
1222 let Some((di, pi)) = self.dock_pane_of(id) else {
1223 return;
1224 };
1225 let pane = &self.docks[di].panes[pi];
1226 if pane.on_bookmarks() {
1227 pane.bookmark_nav.focus_start();
1228 } else {
1229 pane.tree.emit(TreeMsg::Focus);
1230 }
1231 }
1232
1233 fn focus_pane_later(&self, id: u64) {
1237 let sender = self.sender.clone();
1238 glib::idle_add_local_once(move || sender.input(AppMsg::FocusPane { id }));
1239 }
1240
1241 fn open_filter(&mut self, id: u64) {
1245 if let Some((di, pi)) = self.dock_pane_of(id) {
1246 let pane = &mut self.docks[di].panes[pi];
1247 pane.filter_bar.set_visible(true);
1248 let entry = pane.filter_entry.clone();
1249 glib::idle_add_local_once(move || {
1250 entry.grab_focus();
1251 });
1252 }
1253 }
1254
1255 fn add_bookmark(&mut self, path: PathBuf) {
1258 if Bookmark::contains_path(&self.bookmarks, &path) {
1259 self.status = format!("{} is already bookmarked", path.display());
1260 return;
1261 }
1262 self.bookmarks.push(Bookmark::leaf(Bookmark::default_name(&path), path.clone()));
1263 self.save_bookmarks();
1264 self.status = format!("Bookmarked {}", path.display());
1265 }
1266
1267 fn open_root_in_pane(&mut self, id: u64, path: PathBuf) {
1270 if let Some((di, pi)) = self.dock_pane_of(id) {
1271 self.docks[di].panes[pi].show_tree();
1272 self.docks[di].panes[pi].tree.emit(TreeMsg::OpenRoot(path));
1273 self.set_active(id);
1274 }
1275 self.focus_pane_later(id);
1276 }
1277
1278 fn open_bookmark(&mut self, id: u64, path: PathBuf, _sender: &ComponentSender<Self>) {
1281 let path = crate::config::expand_bookmark_path(&path);
1282 if !path.is_dir() {
1283 self.status = format!("{} is not a directory", path.display());
1284 return;
1285 }
1286 self.open_root_in_pane(id, path);
1287 }
1288
1289 fn run_bookmark_action(&mut self, id: u64, path: PathBuf, target: ShortcutTarget) {
1294 let path = crate::config::expand_bookmark_path(&path);
1295 let Some((di, pi)) = self.dock_pane_of(id) else {
1296 return;
1297 };
1298 match target {
1299 ShortcutTarget::Builtin(BuiltinAction::Open) => {
1300 if path.is_dir() {
1301 self.open_root_in_pane(id, path);
1302 } else {
1303 self.docks[di].panes[pi].tree.emit(TreeMsg::OpenWithDefault(path));
1304 }
1305 }
1306 ShortcutTarget::Builtin(action) => {
1307 if let Some(msg) = crate::ui::tree::path_action_message(action, &path) {
1308 self.docks[di].panes[pi].tree.emit(msg);
1309 }
1310 }
1311 ShortcutTarget::Command(cmd) => {
1312 self.docks[di].panes[pi]
1313 .tree
1314 .emit(TreeMsg::RunCommand { command: cmd.command, path });
1315 }
1316 }
1317 }
1318
1319 fn show_bookmarks_view(&mut self, id: u64) {
1321 if let Some((di, pi)) = self.dock_pane_of(id) {
1322 self.docks[di].panes[pi].show_bookmarks();
1323 self.docks[di].panes[pi].history.record(ViewEntry::Bookmarks);
1324 self.docks[di].panes[pi].refresh_nav();
1325 self.set_active(id);
1326 }
1327 self.focus_pane_later(id);
1328 }
1329
1330 fn edit_bookmark(&mut self, id: u64, index_path: Vec<usize>, sender: &ComponentSender<Self>) {
1333 let Some(bookmark) = Bookmark::get(&self.bookmarks, &index_path).cloned() else {
1334 return;
1335 };
1336 let Some((di, _pi)) = self.dock_pane_of(id) else { return };
1337 let parent = self.docks[di].window.clone();
1338 let sender = sender.clone();
1339 let path = bookmark.path.as_deref().map(crate::config::expand_bookmark_path);
1340 let title = if bookmark.is_folder() { "Edit Folder" } else { "Edit Bookmark" };
1341 bookmarks::show_bookmark_dialog(
1342 &parent,
1343 title,
1344 &bookmark.name,
1345 path.as_deref(),
1346 move |name, path| {
1347 sender.input(AppMsg::BookmarkEditSaved { index_path: index_path.clone(), name, path });
1348 },
1349 );
1350 }
1351
1352 fn new_bookmark(&mut self, id: u64, sender: &ComponentSender<Self>) {
1355 let Some((di, _pi)) = self.dock_pane_of(id) else { return };
1356 let parent = self.docks[di].window.clone();
1357 let sender = sender.clone();
1358 bookmarks::show_bookmark_dialog(
1359 &parent,
1360 "New Bookmark",
1361 "",
1362 Some(Path::new("")),
1363 move |name, path| {
1364 if let Some(path) = path {
1365 sender.input(AppMsg::BookmarkAdded { name, path });
1366 }
1367 },
1368 );
1369 }
1370
1371 fn new_bookmark_folder(&mut self, id: u64, sender: &ComponentSender<Self>) {
1373 let Some((di, _pi)) = self.dock_pane_of(id) else { return };
1374 let parent = self.docks[di].window.clone();
1375 let sender = sender.clone();
1376 bookmarks::show_bookmark_dialog(&parent, "New Folder", "", None, move |name, _path| {
1377 sender.input(AppMsg::BookmarkFolderAdded { name });
1378 });
1379 }
1380
1381 fn move_bookmark(&mut self, from: Vec<usize>, to: MoveTarget, sender: &ComponentSender<Self>) {
1384 if from.is_empty() {
1385 return;
1386 }
1387 let (parent, index) = match &to {
1390 MoveTarget::Root => (Vec::new(), usize::MAX),
1391 MoveTarget::Into(folder) => {
1392 if folder.starts_with(&from) {
1393 return;
1394 }
1395 (adjust_path_after_removal(&from, folder), usize::MAX)
1396 }
1397 MoveTarget::Before(leaf) => {
1398 if leaf.is_empty() || leaf.starts_with(&from) {
1399 return;
1400 }
1401 let parent_orig = leaf[..leaf.len() - 1].to_vec();
1402 let parent = adjust_path_after_removal(&from, &parent_orig);
1403 let mut index = leaf[leaf.len() - 1];
1404 if from.len() == leaf.len()
1406 && from[..from.len() - 1] == parent_orig[..]
1407 && from[from.len() - 1] < index
1408 {
1409 index -= 1;
1410 }
1411 (parent, index)
1412 }
1413 };
1414 let Some(entry) = Bookmark::take(&mut self.bookmarks, &from) else {
1415 return;
1416 };
1417 if !Bookmark::insert(&mut self.bookmarks, &parent, index, entry) {
1418 return;
1419 }
1420 self.save_bookmarks();
1421 self.refresh_all_bookmarks(sender);
1422 }
1423
1424 fn save_bookmarks(&mut self) {
1426 let path = bookmark_file_path(&self.config.bookmarks.file);
1427 if let Err(err) = save_bookmarks_to_path(&path, &self.bookmarks) {
1428 self.status = format!("Could not save bookmarks: {err:?}");
1429 }
1430 }
1431
1432 fn refresh_all_bookmarks(&mut self, sender: &ComponentSender<Self>) {
1436 let context = self.config.context_menu.clone();
1437 let extras = self.config.bookmarks.context.clone();
1438 let bookmarks = self.bookmarks.clone();
1439 for di in 0..self.docks.len() {
1440 for pi in 0..self.docks[di].panes.len() {
1441 let pane = &self.docks[di].panes[pi];
1442 let menu = bookmarks::BookmarkMenuConfig {
1443 context: &context,
1444 extras: &extras,
1445 side: self.docks[di].side,
1446 };
1447 let id = pane.id;
1448 let filter = pane.bookmark_filter.clone();
1449 let sender = sender.clone();
1450 let on_event: Rc<dyn Fn(BookmarkEvent)> =
1451 Rc::new(move |event| sender.input(AppMsg::BookmarkEvent { id, event }));
1452 bookmarks::fill_bookmarks(
1453 &pane.bookmarks_list,
1454 &bookmarks,
1455 &filter,
1456 &menu,
1457 &pane.bookmark_drag,
1458 &pane.bookmark_nav,
1459 on_event,
1460 );
1461 }
1462 }
1463 }
1464
1465 fn dispatch_pane_builtin(
1469 &mut self,
1470 id: u64,
1471 action: BuiltinAction,
1472 sender: &ComponentSender<Self>,
1473 ) {
1474 self.set_active(id);
1475 match action {
1476 BuiltinAction::OpenFolder => self.show_open_folder(id, sender),
1477 BuiltinAction::Filter => self.open_filter(id),
1478 BuiltinAction::SplitView => sender.input(AppMsg::SplitFromPane { id }),
1479 BuiltinAction::Up => self.go_up(id),
1480 BuiltinAction::Back => self.go_back(id),
1481 BuiltinAction::Forward => self.go_forward(id),
1482 BuiltinAction::Collapse => self.collapse_dock(id),
1483 BuiltinAction::ClosePane => sender.input(AppMsg::ClosePane { id }),
1484 BuiltinAction::ToggleBookmarks => self.show_bookmarks_view(id),
1485 BuiltinAction::NewBookmark => self.new_bookmark(id, sender),
1486 BuiltinAction::NewBookmarkFolder => self.new_bookmark_folder(id, sender),
1487 _ => {}
1488 }
1489 }
1490
1491 fn go_up(&mut self, id: u64) {
1494 let Some((di, pi)) = self.dock_pane_of(id) else {
1495 return;
1496 };
1497 let Some(parent) = self.docks[di].panes[pi]
1498 .root
1499 .as_deref()
1500 .and_then(Path::parent)
1501 .map(Path::to_path_buf)
1502 else {
1503 return;
1504 };
1505 self.docks[di].panes[pi].tree.emit(TreeMsg::OpenRoot(parent));
1506 self.focus_pane_later(id);
1507 }
1508
1509 fn go_back(&mut self, id: u64) {
1511 let Some((di, pi)) = self.dock_pane_of(id) else {
1512 return;
1513 };
1514 if let Some(target) = self.docks[di].panes[pi].history.back() {
1515 self.show_view_entry(id, target);
1516 }
1517 }
1518
1519 fn go_forward(&mut self, id: u64) {
1521 let Some((di, pi)) = self.dock_pane_of(id) else {
1522 return;
1523 };
1524 if let Some(target) = self.docks[di].panes[pi].history.forward() {
1525 self.show_view_entry(id, target);
1526 }
1527 }
1528
1529 fn show_view_entry(&mut self, id: u64, entry: ViewEntry) {
1533 let Some((di, pi)) = self.dock_pane_of(id) else {
1534 return;
1535 };
1536 match entry {
1537 ViewEntry::Dir(path) => {
1538 self.docks[di].panes[pi].show_tree();
1539 self.docks[di].panes[pi].tree.emit(TreeMsg::OpenRoot(path));
1540 }
1541 ViewEntry::Bookmarks => {
1542 self.docks[di].panes[pi].show_bookmarks();
1543 self.docks[di].panes[pi].history.finish_navigation();
1544 self.docks[di].panes[pi].refresh_nav();
1545 }
1546 }
1547 self.set_active(id);
1548 self.focus_pane_later(id);
1549 }
1550
1551 fn run_pane_shortcut(
1555 &mut self,
1556 id: u64,
1557 action: ContextAction,
1558 sender: &ComponentSender<Self>,
1559 ) {
1560 self.set_active(id);
1561 if let ContextAction::Entry(entry) = &action
1564 && entry.action == BuiltinAction::MoveToWorkspace
1565 && entry.workspace.is_none()
1566 {
1567 self.status = "Move to Workspace needs a workspace = \"...\" target".to_owned();
1568 return;
1569 }
1570 if let Some(target) = action.workspace_move() {
1571 self.move_active_pane_to_workspace(target);
1572 return;
1573 }
1574 let Some(target) = ShortcutTarget::from_action(&action) else {
1575 return;
1576 };
1577 if let ShortcutTarget::Builtin(builtin) = &target
1578 && builtin.is_pane_action()
1579 {
1580 self.dispatch_pane_builtin(id, *builtin, sender);
1581 return;
1582 }
1583 if let Some((di, pi)) = self.dock_pane_of(id)
1584 && let Some(msg) = pane_item_message(&target)
1585 {
1586 self.docks[di].panes[pi].tree.emit(msg);
1587 }
1588 }
1589
1590 fn active_pane_id(&self, side: Option<PanelSide>) -> Option<u64> {
1594 let di = match side {
1595 Some(side) => self.dock_of_side(side)?,
1596 None => self
1597 .dock_of_side(self.primary_side)
1598 .or_else(|| self.docks.iter().position(|dock| dock.window.is_visible()))
1599 .or_else(|| (!self.docks.is_empty()).then_some(0))?,
1600 };
1601 let dock = &self.docks[di];
1602 let active = &self.active_workspace;
1603 dock.active_pane
1604 .filter(|id| {
1605 dock.panes.iter().any(|pane| pane.id == *id && &pane.workspace == active)
1606 })
1607 .or_else(|| {
1608 dock.panes
1609 .iter()
1610 .rev()
1611 .find(|pane| &pane.workspace == active)
1612 .map(|pane| pane.id)
1613 })
1614 }
1615
1616 fn run_accelerator(&mut self, id: u64, accel: &str, sender: &ComponentSender<Self>) {
1621 let Some((key, mods)) = parse_accelerator(accel) else {
1622 return;
1623 };
1624 let Some((di, pi)) = self.dock_pane_of(id) else {
1625 return;
1626 };
1627 if let Some(action) = self.docks[di].panes[pi].shortcuts.action_for(key, mods) {
1628 self.run_pane_shortcut(id, action, sender);
1629 return;
1630 }
1631 if self.docks[di].panes[pi].on_bookmarks() {
1634 if bookmarks::BookmarkNav::is_nav_key(key) {
1635 if let Some(node) = self.docks[di].panes[pi].bookmark_nav.navigate(key) {
1636 sender.input(AppMsg::BookmarkEvent {
1637 id,
1638 event: BookmarkEvent::Toggle(node.index_path),
1639 });
1640 }
1641 return;
1642 }
1643 if matches!(key, gdk::Key::Return | gdk::Key::KP_Enter | gdk::Key::space) {
1644 if let Some(event) = self.docks[di].panes[pi].bookmark_nav.activate() {
1645 sender.input(AppMsg::BookmarkEvent { id, event });
1646 }
1647 return;
1648 }
1649 }
1650 self.docks[di].panes[pi]
1651 .tree
1652 .emit(TreeMsg::RunAccelerator(accel.to_owned()));
1653 }
1654
1655 fn dock_pane_of(&self, id: u64) -> Option<(usize, usize)> {
1657 self.docks.iter().enumerate().find_map(|(di, dock)| {
1658 dock.panes
1659 .iter()
1660 .position(|pane| pane.id == id)
1661 .map(|pi| (di, pi))
1662 })
1663 }
1664
1665 fn find_pane_with_dir(&self, path: &PathBuf) -> Option<(usize, usize)> {
1670 let canonical = std::fs::canonicalize(path).ok().unwrap_or_else(|| path.clone());
1671 self.docks.iter().enumerate().find_map(|(di, dock)| {
1672 dock.panes.iter().enumerate().find_map(|(pi, pane)| {
1673 if pane.canonical_root.as_deref() == Some(canonical.as_path()) {
1674 Some((di, pi))
1675 } else {
1676 None
1677 }
1678 })
1679 })
1680 }
1681
1682 fn dock_of_side(&self, side: PanelSide) -> Option<usize> {
1684 self.docks.iter().position(|dock| dock.side == side)
1685 }
1686
1687 fn dock_index_of(&self, side: PanelSide, monitor: Option<&gdk::Monitor>) -> Option<usize> {
1690 self.docks.iter().position(|dock| {
1691 dock.side == side
1692 && match (&dock.monitor, monitor) {
1693 (Some(a), Some(b)) => monitor_key(a) == monitor_key(b),
1694 (None, None) => true,
1695 _ => false,
1696 }
1697 })
1698 }
1699
1700 fn default_monitor(&self) -> Option<gdk::Monitor> {
1703 self.docks
1704 .first()
1705 .and_then(|dock| dock.monitor.clone())
1706 .or_else(|| {
1707 gdk::Display::default()?
1708 .monitors()
1709 .item(0)
1710 .and_downcast::<gdk::Monitor>()
1711 })
1712 }
1713
1714 fn collapse_dock(&mut self, id: u64) {
1717 if let Some((di, _)) = self.dock_pane_of(id) {
1718 self.set_dock_visible(di, false);
1719 }
1720 }
1721
1722 fn set_dock_visible(&mut self, di: usize, visible: bool) {
1725 let shown = visible && self.dock_has_active_pane(di);
1726 self.docks[di].window.set_visible(shown);
1727 if !shown {
1728 self.suspend_dock_panes(di);
1729 }
1730 self.refresh_visible();
1731 }
1732
1733 fn suspend_dock_panes(&self, di: usize) {
1736 for pane in &self.docks[di].panes {
1737 pane.tree.emit(TreeMsg::Suspend);
1738 }
1739 }
1740
1741 fn dock_has_active_pane(&self, di: usize) -> bool {
1743 let active = &self.active_workspace;
1744 self.docks[di].panes.iter().any(|pane| &pane.workspace == active)
1745 }
1746
1747 fn last_active_pane_id(&self, di: usize) -> Option<u64> {
1749 let active = &self.active_workspace;
1750 self.docks[di]
1751 .panes
1752 .iter()
1753 .rev()
1754 .find(|pane| &pane.workspace == active)
1755 .map(|pane| pane.id)
1756 }
1757
1758 fn on_active_workspace(&mut self, name: String) {
1761 if self.active_workspace == name {
1762 return;
1763 }
1764 self.active_workspace = name;
1765 self.refill_docks();
1766 self.sync_dock_visibility();
1767 let active = (0..self.docks.len())
1768 .find(|&di| self.docks[di].window.is_visible())
1769 .and_then(|di| self.last_active_pane_id(di));
1770 if let Some(id) = active {
1771 self.focus_pane(id);
1772 }
1773 }
1774
1775 fn refill_docks(&mut self) {
1777 for dock in &self.docks {
1778 fill_pane_container(&dock.container, &dock.panes, &self.active_workspace);
1779 }
1780 }
1781
1782 fn visible(&self) -> bool {
1786 !self.hidden_workspaces.contains(&self.active_workspace)
1787 }
1788
1789 fn set_visible(&mut self, visible: bool) {
1791 if visible {
1792 self.hidden_workspaces.remove(&self.active_workspace);
1793 } else {
1794 self.hidden_workspaces.insert(self.active_workspace.clone());
1795 }
1796 }
1797
1798 fn sync_dock_visibility(&mut self) {
1801 let show = self.visible();
1802 for di in 0..self.docks.len() {
1803 let shown = show && self.dock_has_active_pane(di);
1804 self.docks[di].window.set_visible(shown);
1805 if !shown {
1806 self.suspend_dock_panes(di);
1807 }
1808 }
1809 }
1810
1811 fn set_docks_visible(&mut self, visible: bool) {
1814 self.set_visible(visible);
1815 self.sync_dock_visibility();
1816 if visible {
1817 let focus: Vec<u64> = (0..self.docks.len())
1822 .filter(|&di| self.docks[di].window.is_visible())
1823 .filter_map(|di| self.last_active_pane_id(di))
1824 .collect();
1825 for id in focus {
1826 self.focus_pane(id);
1827 }
1828 }
1829 }
1830
1831 fn move_active_pane_to_workspace(&mut self, target: WorkspaceMove) {
1835 if self.hypr.is_none() {
1836 self.status = "Workspace moves need a Hyprland session".to_owned();
1837 return;
1838 }
1839 let Some(id) = self.active_pane_id(None) else { return };
1840 let Some((di, pi)) = self.dock_pane_of(id) else { return };
1841 let from = self.docks[di].panes[pi].workspace.clone();
1842 let name = match target {
1843 WorkspaceMove::Named(name) => name,
1844 WorkspaceMove::Previous => self.neighbor_workspace(&from, -1),
1845 WorkspaceMove::Next => self.neighbor_workspace(&from, 1),
1846 };
1847 if name.is_empty() || name == from {
1848 return;
1849 }
1850 self.docks[di].panes[pi].workspace = name.clone();
1851 let container = self.docks[di].container.clone();
1852 fill_pane_container(&container, &self.docks[di].panes, &self.active_workspace);
1853 self.sync_dock_visibility();
1854 self.status = format!("Pane moved to workspace {name}");
1855 }
1856
1857 fn neighbor_workspace(&self, from: &str, delta: i32) -> String {
1862 if let Ok(id) = from.parse::<i64>() {
1863 return (id + delta as i64).max(1).to_string();
1864 }
1865 let Some(hypr) = &self.hypr else { return String::new() };
1866 let names = hypr.workspace_names();
1867 if names.is_empty() {
1868 return from.to_owned();
1869 }
1870 match names.iter().position(|name| name == from) {
1871 Some(i) => {
1872 let j = (i as i32 + delta).rem_euclid(names.len() as i32) as usize;
1873 names[j].clone()
1874 }
1875 None => names[0].clone(),
1876 }
1877 }
1878
1879 fn refresh_visible(&mut self) {
1883 let any = self.docks.iter().any(|dock| dock.window.is_visible());
1884 self.set_visible(any);
1885 }
1886
1887 fn width_for(&self, side: PanelSide) -> u32 {
1889 self.widths.get(&side).copied().unwrap_or(self.config.panel.width)
1890 }
1891
1892 fn set_width(&mut self, side: PanelSide, width: u32) {
1895 let width = width.clamp(PANEL_MIN_WIDTH, PANEL_MAX_WIDTH);
1896 if width == self.width_for(side) {
1897 return;
1898 }
1899 self.widths.insert(side, width);
1900 let panel = PanelConfig { width, ..self.config.panel };
1901 for dock in self.docks.iter().filter(|dock| dock.side == side) {
1902 apply_window_width(&dock.window, width);
1903 for pane in &dock.panes {
1904 pane.tree.emit(TreeMsg::SetPanel(panel));
1905 }
1906 }
1907 if side == self.primary_side {
1909 self.config.panel.width = width;
1910 }
1911 self.schedule_save();
1912 }
1913
1914 fn resize_by(&mut self, side: PanelSide, delta: i32) {
1916 let target = (self.width_for(side) as i64 + delta as i64)
1917 .clamp(PANEL_MIN_WIDTH as i64, PANEL_MAX_WIDTH as i64) as u32;
1918 self.set_width(side, target);
1919 }
1920
1921 fn session_state(&self) -> SessionState {
1923 SessionState {
1924 last_root: self.last_root.clone(),
1925 left_width: Some(self.width_for(PanelSide::Left)),
1926 right_width: Some(self.width_for(PanelSide::Right)),
1927 }
1928 }
1929
1930 fn persist_session(&mut self) {
1932 self.save_generation.set(self.save_generation.get().wrapping_add(1));
1933 if let Err(err) = self.session_state().save() {
1934 self.status = format!("Could not save session: {err}");
1935 }
1936 }
1937
1938 fn schedule_save(&mut self) {
1942 let generation = self.save_generation.get().wrapping_add(1);
1943 self.save_generation.set(generation);
1944 let current = self.save_generation.clone();
1945 let state = self.session_state();
1946 glib::timeout_add_local_once(std::time::Duration::from_millis(SAVE_DEBOUNCE_MS), move || {
1947 if current.get() == generation {
1948 let _ = state.save();
1949 }
1950 });
1951 }
1952
1953 fn ensure_dock(
1957 &mut self,
1958 side: PanelSide,
1959 monitor: Option<gdk::Monitor>,
1960 seed: bool,
1961 sender: &ComponentSender<Self>,
1962 ) -> usize {
1963 if let Some(di) = self.dock_index_of(side, monitor.as_ref()) {
1964 if seed && self.docks[di].panes.is_empty() {
1965 let startup = self.config.startup.clone();
1967 if let Some(root) = default_root(&startup) {
1968 self.add_pane(di, Some(root), sender);
1969 }
1970 }
1971 return di;
1972 }
1973 let config = &self.config;
1974 let width = self.width_for(side);
1975 let window = gtk::Window::new();
1976 init_layer_window(&window, config, side, width, monitor.as_ref());
1977 window.set_default_size(width as i32, 520);
1978 if layer_shell_available() {
1979 attach_resize_controls(&window, side, sender);
1980 }
1981 let outer = gtk::Box::new(gtk::Orientation::Vertical, 0);
1983 outer.set_vexpand(true);
1984 let container = gtk::Box::new(gtk::Orientation::Vertical, 0);
1985 container.set_vexpand(true);
1986 outer.append(&container);
1987 window.set_child(Some(&outer));
1988
1989 let di = self.docks.len();
1990 self.docks.push(Dock {
1991 side,
1992 monitor,
1993 window,
1994 container,
1995 panes: Vec::new(),
1996 active_pane: None,
1997 });
1998 {
2001 let sender = sender.clone();
2002 let window = self.docks[di].window.clone();
2003 window.connect_map(move |_| {
2004 let sender = sender.clone();
2005 sender.input(AppMsg::ResolveDockMonitor { di });
2006 glib::idle_add_local_once(move || sender.input(AppMsg::FocusVisible));
2007 });
2008 }
2009 if seed {
2010 let startup = self.config.startup.clone();
2011 if let Some(root) = default_root(&startup) {
2012 self.add_pane(di, Some(root), sender);
2013 } else {
2014 self.add_pane(di, None, sender);
2016 }
2017 }
2018 di
2019 }
2020
2021 fn add_pane(&mut self, di: usize, root: Option<PathBuf>, sender: &ComponentSender<Self>) -> u64 {
2024 let id = self.next_id;
2025 self.next_id += 1;
2026 let parent = self.docks[di].window.clone();
2027 let side = self.docks[di].side;
2028 let width = self.width_for(side);
2029 let workspace = self.active_workspace.clone();
2030 let pane = make_pane(&self.config, parent, id, side, width, workspace, sender.clone());
2031 let mut pane = pane;
2032 match &root {
2033 Some(root) => pane.tree.emit(TreeMsg::OpenRoot(root.clone())),
2034 None => {
2035 pane.show_bookmarks();
2036 pane.history.record(ViewEntry::Bookmarks);
2037 pane.refresh_nav();
2038 }
2039 }
2040 if let Some(root) = &root {
2041 pane.set_root(root.clone());
2042 }
2043 let dock = &mut self.docks[di];
2044 dock.panes.push(pane);
2045 dock.active_pane = Some(id);
2046 let container = dock.container.clone();
2047 fill_pane_container(&container, &dock.panes, &self.active_workspace);
2048 let focus_sender = sender.clone();
2051 glib::idle_add_local_once(move || focus_sender.input(AppMsg::FocusPane { id }));
2052 self.refresh_all_bookmarks(sender);
2054 id
2055 }
2056
2057 fn show_open_folder(&self, id: u64, sender: &ComponentSender<Self>) {
2058 let sender = sender.clone();
2059 self.file_dialog.select_folder(
2060 None::<>k::Window>,
2061 None::<&gio::Cancellable>,
2062 move |result| {
2063 let path = result.ok().and_then(|file| file.path());
2064 sender.input(AppMsg::OpenFolderPicked { id, path });
2065 },
2066 );
2067 }
2068
2069 fn dock_origin(&self, di: usize) -> (f64, f64) {
2073 let dock = &self.docks[di];
2074 let margin = self.config.panel.margin as f64;
2075 let width = self.width_for(dock.side) as f64;
2076 let (mx, my, mw) = match dock.monitor.as_ref().map(gdk::Monitor::geometry) {
2077 Some(g) => (g.x() as f64, g.y() as f64, g.width() as f64),
2078 None => (0.0, 0.0, 0.0),
2079 };
2080 let x = match dock.side {
2081 PanelSide::Left => mx + margin,
2082 PanelSide::Right => mx + mw - width - margin,
2083 };
2084 (x, my + margin)
2085 }
2086
2087 fn point_target(&self, point: (f64, f64)) -> Option<(PanelSide, gdk::Monitor)> {
2091 let (x, y) = point;
2092 let monitors = gdk::Display::default()?.monitors();
2093 for i in 0..monitors.n_items() {
2094 let Some(monitor) = monitors.item(i).and_downcast::<gdk::Monitor>() else {
2095 continue;
2096 };
2097 let g = monitor.geometry();
2098 let (x0, y0) = (g.x() as f64, g.y() as f64);
2099 let (w, h) = (g.width() as f64, g.height() as f64);
2100 if x >= x0 && x < x0 + w && y >= y0 && y < y0 + h {
2101 let side = if x < x0 + w / 2.0 { PanelSide::Left } else { PanelSide::Right };
2102 return Some((side, monitor));
2103 }
2104 }
2105 None
2106 }
2107
2108 fn begin_pane_drag(&mut self, id: u64, start: (f64, f64)) {
2111 let Some((di, pi)) = self.dock_pane_of(id) else { return };
2112 let grip_origin = {
2116 let grip = &self.docks[di].panes[pi].grip;
2117 let window = &self.docks[di].window;
2118 grip.compute_point(window, &relm4::gtk::graphene::Point::new(0.0, 0.0))
2119 .map(|p| (p.x() as f64, p.y() as f64))
2120 .unwrap_or((0.0, 0.0))
2121 };
2122 let origin = self.dock_origin(di);
2123 let base = (origin.0 + grip_origin.0 + start.0, origin.1 + grip_origin.1 + start.1);
2124 self.docks[di].panes[pi].widget.add_css_class("pane-moving");
2125 self.pane_drag = Some(PaneDrag { id, base, highlighted: None });
2126 self.status = "Drag the pane to the edge of a screen to move it".to_owned();
2127 }
2128
2129 fn update_pane_drag(&mut self, id: u64, offset: (f64, f64)) {
2132 let Some(drag) = self.pane_drag.as_ref() else { return };
2133 if drag.id != id {
2134 return;
2135 }
2136 let point = (drag.base.0 + offset.0, drag.base.1 + offset.1);
2137 let target = self.point_target(point);
2138 let highlight = target
2139 .as_ref()
2140 .and_then(|(side, monitor)| self.dock_index_of(*side, Some(monitor)));
2141 if let Some(drag) = self.pane_drag.as_mut()
2142 && drag.highlighted != highlight
2143 {
2144 if let Some(di) = drag.highlighted
2145 && let Some(dock) = self.docks.get(di)
2146 {
2147 dock.window.remove_css_class("pane-drop-target");
2148 }
2149 if let Some(di) = highlight
2150 && let Some(dock) = self.docks.get(di)
2151 {
2152 dock.window.add_css_class("pane-drop-target");
2153 }
2154 drag.highlighted = highlight;
2155 }
2156 self.status = match target {
2157 Some((side, monitor)) => format!(
2158 "Drop to move the pane to the {} panel on {}",
2159 side.name(),
2160 monitor_label(&monitor)
2161 ),
2162 None => "Drag the pane to the edge of a screen to move it".to_owned(),
2163 };
2164 }
2165
2166 fn end_pane_drag(&mut self, id: u64, offset: (f64, f64), sender: &ComponentSender<Self>) {
2169 let Some(drag) = self.pane_drag.take() else { return };
2170 if drag.id != id {
2171 self.pane_drag = Some(drag);
2172 return;
2173 }
2174 if let Some((di, pi)) = self.dock_pane_of(id) {
2175 self.docks[di].panes[pi].widget.remove_css_class("pane-moving");
2176 }
2177 if let Some(di) = drag.highlighted
2178 && let Some(dock) = self.docks.get(di)
2179 {
2180 dock.window.remove_css_class("pane-drop-target");
2181 }
2182 let point = (drag.base.0 + offset.0, drag.base.1 + offset.1);
2183 match self.point_target(point) {
2184 Some((side, monitor)) => self.move_pane_to(id, side, monitor, sender),
2185 None => self.status.clear(),
2186 }
2187 }
2188
2189 fn move_pane_to(
2192 &mut self,
2193 id: u64,
2194 side: PanelSide,
2195 monitor: gdk::Monitor,
2196 sender: &ComponentSender<Self>,
2197 ) {
2198 let Some((sdi, spi)) = self.dock_pane_of(id) else { return };
2199 if self.dock_index_of(side, Some(&monitor)) == Some(sdi) {
2200 self.status.clear();
2201 return;
2202 }
2203 let pane = self.docks[sdi].panes.remove(spi);
2204 remove_from_parent(pane.widget.upcast_ref());
2205 if self.docks[sdi].active_pane == Some(id) {
2206 self.docks[sdi].active_pane = self.docks[sdi].panes.last().map(|p| p.id);
2207 }
2208 if self.docks[sdi].panes.is_empty() {
2209 if sdi == 0 {
2210 self.docks[sdi].window.set_visible(false);
2213 } else {
2214 let dock = self.docks.remove(sdi);
2215 dock.window.close();
2216 }
2217 } else {
2218 let container = self.docks[sdi].container.clone();
2219 fill_pane_container(&container, &self.docks[sdi].panes, &self.active_workspace);
2220 }
2221 let tdi = self.ensure_dock(side, Some(monitor), false, sender);
2222 pane.tree.emit(TreeMsg::SetSide(side));
2223 self.docks[tdi].panes.push(pane);
2224 self.docks[tdi].active_pane = Some(id);
2225 let container = self.docks[tdi].container.clone();
2226 fill_pane_container(&container, &self.docks[tdi].panes, &self.active_workspace);
2227 self.sync_dock_visibility();
2228 self.refresh_all_bookmarks(sender);
2229 self.focus_pane_later(id);
2230 self.status = format!("Moved pane to the {} panel", side.name());
2231 }
2232}
2233
2234fn pane_item_message(target: &ShortcutTarget) -> Option<TreeMsg> {
2242 match target {
2243 ShortcutTarget::Builtin(action) => match action {
2244 BuiltinAction::ToggleHidden => Some(TreeMsg::ToggleHidden),
2245 BuiltinAction::SortByName => Some(TreeMsg::SetSortKey(SortKey::Name)),
2246 BuiltinAction::SortBySize => Some(TreeMsg::SetSortKey(SortKey::Size)),
2247 BuiltinAction::SortByModified => Some(TreeMsg::SetSortKey(SortKey::Modified)),
2248 BuiltinAction::SortByType => Some(TreeMsg::SetSortKey(SortKey::Type)),
2249 BuiltinAction::ToggleSortAscending => Some(TreeMsg::ToggleSortDirection),
2250 other => Some(TreeMsg::RunShortcut {
2251 target: ShortcutTarget::Builtin(*other),
2252 }),
2253 },
2254 ShortcutTarget::Command(_) => Some(TreeMsg::RunShortcut { target: target.clone() }),
2255 }
2256}
2257
2258fn build_nav_bar(id: u64, sender: &ComponentSender<App>) -> (gtk::Box, NavButtons) {
2262 let bar = gtk::Box::new(gtk::Orientation::Horizontal, 2);
2263 bar.add_css_class("nav-toolbar");
2264
2265 let up = nav_button("pan-up-symbolic", "Up One Level", BuiltinAction::Up, id, sender);
2266 let back = nav_button("pan-start-symbolic", "Back", BuiltinAction::Back, id, sender);
2267 let forward = nav_button("pan-end-symbolic", "Forward", BuiltinAction::Forward, id, sender);
2268
2269 bar.append(&up);
2270 bar.append(&back);
2271 bar.append(&forward);
2272 (bar, NavButtons { up, back, forward })
2273}
2274
2275fn nav_button(
2278 icon: &str,
2279 tooltip: &str,
2280 action: BuiltinAction,
2281 id: u64,
2282 sender: &ComponentSender<App>,
2283) -> gtk::Button {
2284 let button = gtk::Button::from_icon_name(icon);
2285 button.add_css_class("nav-button");
2286 button.add_css_class("flat");
2287 button.set_tooltip_text(Some(tooltip));
2288 button.set_focusable(false);
2289 button.set_can_focus(false);
2290 button.set_valign(gtk::Align::Center);
2291 let sender = sender.clone();
2292 button.connect_clicked(move |_| {
2293 sender.input(AppMsg::PaneToolbar {
2294 id,
2295 out: ToolbarOutput::PaneItem(ContextAction::Builtin(action)),
2296 });
2297 });
2298 button
2299}
2300
2301fn make_pane(
2302 config: &Config,
2303 parent: gtk::Window,
2304 id: u64,
2305 side: PanelSide,
2306 width: u32,
2307 workspace: String,
2308 sender: ComponentSender<App>,
2309) -> Pane {
2310 let overlay = gtk::Overlay::new();
2313 let toolbar = Toolbar::builder()
2314 .launch(ToolbarInit {
2315 overlay: overlay.clone(),
2316 pane_menu: config.pane_menu.clone(),
2317 bookmarks_menu: config.bookmarks.menu.clone(),
2318 })
2319 .forward(sender.input_sender(), move |out| AppMsg::PaneToolbar { id, out });
2320 let tree = Tree::builder()
2321 .launch(TreeInit {
2322 config: config.tree.clone(),
2323 parent,
2324 menu: config.context_menu.clone(),
2325 side,
2326 panel: PanelConfig { width, ..config.panel },
2327 })
2328 .forward(sender.input_sender(), move |out| AppMsg::PaneTree { id, out });
2329
2330 let grip = toolbar.model().grip().clone();
2335 {
2336 let drag = gtk::GestureDrag::new();
2337 drag.set_button(gdk::BUTTON_PRIMARY);
2338 drag.set_propagation_phase(gtk::PropagationPhase::Capture);
2339 let s = sender.clone();
2340 drag.connect_drag_begin(move |_, start_x, start_y| {
2341 s.input(AppMsg::PaneDragBegin { id, start: (start_x, start_y) });
2342 });
2343 let s = sender.clone();
2344 drag.connect_drag_update(move |_, offset_x, offset_y| {
2345 s.input(AppMsg::PaneDragUpdate { id, offset: (offset_x, offset_y) });
2346 });
2347 let s = sender.clone();
2348 drag.connect_drag_end(move |_, offset_x, offset_y| {
2349 s.input(AppMsg::PaneDragEnd { id, offset: (offset_x, offset_y) });
2350 });
2351 grip.add_controller(drag);
2352 }
2353
2354 let widget = gtk::Box::new(gtk::Orientation::Vertical, 0);
2355 widget.append(toolbar.widget());
2356
2357 let nav_buttons = if config.panel.nav_toolbar {
2359 let (bar, buttons) = build_nav_bar(id, &sender);
2360 widget.append(&bar);
2361 Some(buttons)
2362 } else {
2363 None
2364 };
2365
2366 let filter_bar = gtk::Box::new(gtk::Orientation::Horizontal, 4);
2368 filter_bar.add_css_class("filter-bar");
2369 filter_bar.set_visible(false);
2370
2371 let filter_entry = gtk::SearchEntry::new();
2372 filter_entry.set_placeholder_text(Some("Filter..."));
2373 filter_entry.set_hexpand(true);
2374 filter_entry.set_valign(gtk::Align::Center);
2375 filter_entry.add_css_class("filter-entry");
2376 {
2377 let s = sender.clone();
2378 filter_entry.connect_search_changed(move |entry| {
2379 s.input(AppMsg::FilterChanged {
2380 id,
2381 filter: entry.text().to_string(),
2382 });
2383 });
2384 }
2385 filter_bar.append(&filter_entry);
2386
2387 let close_btn = gtk::Button::from_icon_name("window-close-symbolic");
2388 close_btn.set_tooltip_text(Some("Clear filter"));
2389 close_btn.set_valign(gtk::Align::Center);
2390 close_btn.add_css_class("flat");
2391 {
2392 let s = sender.clone();
2393 close_btn.connect_clicked(move |_| {
2394 s.input(AppMsg::FilterClosed { id });
2395 });
2396 }
2397 filter_bar.append(&close_btn);
2398
2399 widget.append(&filter_bar);
2400
2401 let tree_widget = tree.widget();
2403 tree_widget.set_vexpand(true);
2404
2405 let bookmarks_list = gtk::Box::new(gtk::Orientation::Vertical, 0);
2406 bookmarks_list.add_css_class("bookmarks-list");
2407 let bookmarks_scroll = gtk::ScrolledWindow::new();
2408 bookmarks_scroll.set_policy(gtk::PolicyType::Never, gtk::PolicyType::Automatic);
2409 bookmarks_scroll.set_vexpand(true);
2410 bookmarks_scroll.add_css_class("bookmarks-view");
2411 bookmarks_scroll.set_child(Some(&bookmarks_list));
2412 let bookmark_drag = bookmarks::BookmarkDrag::new();
2413 let bookmark_nav = bookmarks::BookmarkNav::new(&bookmarks_list);
2414 {
2417 let items = config.bookmarks.blank.clone();
2418 let sender = sender.clone();
2419 let on_event: Rc<dyn Fn(BookmarkEvent)> =
2420 Rc::new(move |event| sender.input(AppMsg::BookmarkEvent { id, event }));
2421 bookmarks::attach_bookmarks_scroller(&bookmarks_scroll, &items, side, &bookmark_drag, &bookmark_nav, on_event);
2422 }
2423
2424 let body = gtk::Stack::new();
2425 body.set_vexpand(true);
2426 body.add_named(tree_widget, Some("tree"));
2427 body.add_named(&bookmarks_scroll, Some("bookmarks"));
2428 body.set_visible_child_name("tree");
2429 widget.append(&body);
2430 widget.set_vexpand(true);
2431
2432 overlay.set_child(Some(&widget));
2433
2434 let shortcuts = {
2439 let mut items = config.pane_menu.items.clone();
2442 items.extend(config.bookmarks.menu.iter().cloned());
2443 let shortcuts = Rc::new(PaneShortcuts::compile(&items));
2444 let s = sender.clone();
2445 let bound = shortcuts.clone();
2446 let keys = gtk::EventControllerKey::new();
2447 keys.set_propagation_phase(gtk::PropagationPhase::Capture);
2448 keys.connect_key_pressed(move |_, key, _, state| {
2449 if let Some(action) = bound.action_for(key, state) {
2450 s.input(AppMsg::PaneShortcut { id, action });
2451 glib::Propagation::Stop
2452 } else {
2453 glib::Propagation::Proceed
2454 }
2455 });
2456 widget.add_controller(keys);
2457 shortcuts
2458 };
2459
2460 Pane {
2461 id,
2462 toolbar,
2463 tree,
2464 workspace,
2465 root: None,
2466 canonical_root: None,
2467 history: NavHistory::default(),
2468 filter_bar,
2469 filter_entry,
2470 body,
2471 bookmarks_list,
2472 bookmark_filter: String::new(),
2473 bookmark_drag,
2474 bookmark_nav,
2475 nav_buttons,
2476 widget: overlay,
2477 grip,
2478 shortcuts,
2479 }
2480}
2481
2482fn remove_from_parent(w: >k::Widget) {
2490 let Some(parent) = w.parent() else { return };
2491 if let Ok(paned) = parent.clone().downcast::<gtk::Paned>() {
2492 let none: Option<>k::Widget> = None;
2493 if paned.start_child().is_some_and(|c| c == *w) {
2494 paned.set_start_child(none);
2495 }
2496 let none: Option<>k::Widget> = None;
2497 if paned.end_child().is_some_and(|c| c == *w) {
2498 paned.set_end_child(none);
2499 }
2500 } else if let Ok(b) = parent.downcast::<gtk::Box>() {
2501 b.remove(w);
2502 } else {
2503 w.unparent();
2504 }
2505}
2506
2507fn pane_widget(pane: &Pane) -> gtk::Widget {
2508 pane.widget.clone().upcast()
2509}
2510
2511fn fill_pane_container(container: >k::Box, panes: &[Pane], active: &str) {
2524 for pane in panes {
2530 remove_from_parent(pane.widget.upcast_ref());
2531 }
2532 while let Some(child) = container.first_child() {
2534 container.remove(&child);
2535 }
2536
2537 let visible: Vec<&Pane> = panes.iter().filter(|pane| pane.workspace == active).collect();
2538 match visible.as_slice() {
2539 [] => {}
2540 [single] => {
2541 let w = pane_widget(single);
2542 w.set_vexpand(true);
2543 container.append(&w);
2544 }
2545 visible => {
2546 let n = visible.len();
2549 let last_w = pane_widget(visible[n - 1]);
2550 last_w.set_vexpand(true);
2551 let mut right: gtk::Widget = last_w.clone().upcast();
2552 for pane in visible[..n - 1].iter().rev() {
2553 let left_w = pane_widget(pane);
2554 left_w.set_vexpand(true);
2555 let split = gtk::Paned::new(gtk::Orientation::Vertical);
2556 split.set_vexpand(true);
2557 split.set_wide_handle(true);
2558 split.set_shrink_start_child(true);
2560 split.set_shrink_end_child(true);
2561 split.set_resize_start_child(true);
2562 split.set_resize_end_child(true);
2563 split.set_start_child(Some(&left_w));
2564 split.set_end_child(Some(&right));
2565 {
2568 let split2 = split.clone();
2569 relm4::gtk::glib::timeout_add_local_once(
2570 std::time::Duration::from_millis(PANED_CENTER_DELAY_MS),
2571 move || {
2572 let alloc = split2.height();
2575 if alloc > 0 {
2576 split2.set_position(alloc / 2);
2577 }
2578 },
2579 );
2580 }
2581 right = split.upcast();
2582 }
2583 container.append(&right);
2584 }
2585 }
2586}
2587
2588fn stop_video_widgets(widget: >k::Widget) {
2594 if let Some(video) = widget.downcast_ref::<gtk::Video>() {
2595 crate::ui::stop_video_stream(video);
2596 }
2597 let mut child = widget.first_child();
2598 while let Some(current) = child {
2599 stop_video_widgets(¤t);
2600 child = current.next_sibling();
2601 }
2602}
2603
2604const WIDTH_STEP: i32 = 24;
2606
2607const SAVE_DEBOUNCE_MS: u64 = 300;
2609
2610const PANED_CENTER_DELAY_MS: u64 = 100;
2613
2614const SHUTDOWN_GRACE_MS: u64 = 150;
2617
2618fn apply_window_width(window: >k::Window, width: u32) {
2620 let (_, height) = window.default_size();
2621 if window.is_layer_window() {
2624 window.set_exclusive_zone(width as i32);
2625 }
2626 window.set_size_request(width as i32, -1);
2627 window.set_default_size(width as i32, if height > 0 { height } else { -1 });
2628}
2629
2630fn attach_resize_controls(window: >k::Window, side: PanelSide, sender: &ComponentSender<App>) {
2633 attach_resize_drag(window, side, sender.input_sender().clone());
2634 attach_resize_keys(window, side, sender.input_sender().clone());
2635}
2636
2637fn attach_resize_drag(window: >k::Window, side: PanelSide, sender: relm4::Sender<AppMsg>) {
2638 let drag = gtk::GestureDrag::new();
2639 drag.set_button(gdk::BUTTON_SECONDARY);
2640 drag.set_propagation_phase(gtk::PropagationPhase::Capture);
2641 let state = Rc::new(RefCell::new((false, 0.0f64)));
2643
2644 let begin_state = state.clone();
2645 drag.connect_drag_begin(move |gesture, _, _| {
2646 let super_held = gesture
2647 .current_event_state()
2648 .contains(gdk::ModifierType::SUPER_MASK);
2649 *begin_state.borrow_mut() = (super_held, 0.0);
2650 });
2651
2652 let update_sender = sender.clone();
2653 let update_state = state.clone();
2654 drag.connect_drag_update(move |_, offset_x, _| {
2655 let mut state = update_state.borrow_mut();
2656 if !state.0 {
2657 return;
2658 }
2659 let step = offset_x - state.1;
2660 state.1 = offset_x;
2661 let widen = match side {
2663 PanelSide::Left => step,
2664 PanelSide::Right => -step,
2665 };
2666 let delta = widen.round() as i32;
2667 if delta != 0 {
2668 let _ = update_sender.send(AppMsg::ResizeBy { side, delta });
2669 }
2670 });
2671
2672 let end_sender = sender;
2673 let end_state = state.clone();
2674 drag.connect_drag_end(move |_, _, _| {
2675 if end_state.borrow().0 {
2676 let _ = end_sender.send(AppMsg::ResizeCommit);
2677 }
2678 });
2679
2680 window.add_controller(drag);
2681}
2682
2683fn attach_resize_keys(window: >k::Window, side: PanelSide, sender: relm4::Sender<AppMsg>) {
2684 let keys = gtk::EventControllerKey::new();
2685 keys.set_propagation_phase(gtk::PropagationPhase::Capture);
2686 keys.connect_key_pressed(move |_, key, _, state| {
2687 if !state.contains(gdk::ModifierType::SUPER_MASK) {
2688 return glib::Propagation::Proceed;
2689 }
2690 match key {
2691 gdk::Key::minus | gdk::Key::underscore | gdk::Key::KP_Subtract => {
2692 let _ = sender.send(AppMsg::ResizeBy { side, delta: -WIDTH_STEP });
2693 glib::Propagation::Stop
2694 }
2695 gdk::Key::equal | gdk::Key::plus | gdk::Key::KP_Add => {
2696 let _ = sender.send(AppMsg::ResizeBy { side, delta: WIDTH_STEP });
2697 glib::Propagation::Stop
2698 }
2699 _ => glib::Propagation::Proceed,
2700 }
2701 });
2702 window.add_controller(keys);
2703}
2704
2705fn layer_shell_available() -> bool {
2713 static SUPPORTED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2714 *SUPPORTED.get_or_init(gtk4_layer_shell::is_supported)
2715}
2716
2717fn init_layer_window(
2724 window: >k::Window,
2725 config: &Config,
2726 side: PanelSide,
2727 width: u32,
2728 monitor: Option<&gdk::Monitor>,
2729) {
2730 if !layer_shell_available() {
2731 configure_plain_window(window, width);
2732 return;
2733 }
2734 if window.is_layer_window() {
2735 return;
2736 }
2737 window.init_layer_shell();
2738 window.set_namespace(Some(crate::ui::LAYER_NAMESPACE));
2739 window.set_monitor(monitor);
2742 window.set_layer(layer_of(config.panel.layer));
2743 let edge = match side {
2744 PanelSide::Left => Edge::Left,
2745 PanelSide::Right => Edge::Right,
2746 };
2747 window.set_anchor(Edge::Top, true);
2748 window.set_anchor(Edge::Bottom, true);
2749 window.set_anchor(edge, true);
2750 window.set_exclusive_zone(width as i32);
2751 window.set_size_request(width as i32, -1);
2752 let margin = config.panel.margin as i32;
2753 window.set_margin(Edge::Top, margin);
2754 window.set_margin(Edge::Bottom, margin);
2755 window.set_margin(Edge::Left, margin);
2756 window.set_margin(Edge::Right, margin);
2757
2758 window.set_keyboard_mode(KeyboardMode::OnDemand);
2773 let motion = gtk::EventControllerMotion::new();
2774 {
2775 let kb_window = window.clone();
2776 motion.connect_enter(move |_, _, _| kb_window.set_keyboard_mode(KeyboardMode::OnDemand));
2777 }
2778 {
2779 let kb_window = window.clone();
2780 motion.connect_leave(move |_| {
2781 if has_visible_popover(kb_window.upcast_ref()) {
2786 return;
2787 }
2788 kb_window.set_keyboard_mode(KeyboardMode::None);
2789 });
2790 }
2791 window.add_controller(motion);
2792}
2793
2794fn configure_plain_window(window: >k::Window, width: u32) {
2800 static ANNOUNCED: std::sync::OnceLock<()> = std::sync::OnceLock::new();
2801 ANNOUNCED.get_or_init(|| {
2802 eprintln!(
2803 "tree-space: this compositor does not support wlr-layer-shell, so the panel \
2804 cannot dock; opening it as a normal window instead."
2805 );
2806 });
2807 window.set_title(Some("tree-space"));
2808 window.set_default_size(width as i32, 720);
2809 window.set_size_request(width as i32, -1);
2810}
2811
2812fn has_visible_popover(widget: >k::Widget) -> bool {
2815 if widget.is::<gtk::Popover>() && widget.is_visible() {
2816 return true;
2817 }
2818 let mut child = widget.first_child();
2819 while let Some(current) = child {
2820 if has_visible_popover(¤t) {
2821 return true;
2822 }
2823 child = current.next_sibling();
2824 }
2825 false
2826}
2827
2828fn monitor_key(monitor: &gdk::Monitor) -> String {
2832 if let Some(connector) = monitor.connector().filter(|c| !c.is_empty()) {
2833 return connector.to_string();
2834 }
2835 let g = monitor.geometry();
2836 let model = monitor.model().map(|m| m.to_string()).unwrap_or_default();
2837 format!("{model}:{}x{}+{}+{}", g.width(), g.height(), g.x(), g.y())
2838}
2839
2840fn should_seed_empty(panes_exist: bool, seed: bool, whole_panel: bool) -> bool {
2845 seed && (!whole_panel || !panes_exist)
2846}
2847
2848fn monitor_label(monitor: &gdk::Monitor) -> String {
2850 monitor
2851 .connector()
2852 .filter(|c| !c.is_empty())
2853 .or_else(|| monitor.model())
2854 .map(|name| name.to_string())
2855 .unwrap_or_else(|| "this monitor".to_owned())
2856}
2857
2858fn layer_of(layer: PanelLayer) -> Layer {
2859 use PanelLayer::*;
2860 match layer {
2861 Background => Layer::Background,
2862 Bottom => Layer::Bottom,
2863 Top => Layer::Top,
2864 Overlay => Layer::Overlay,
2865 }
2866}
2867
2868fn install_css(config: &Config) {
2872 let stylesheet = load_stylesheet(&config.theme);
2873 if let Some(problem) = &stylesheet.problem {
2874 eprintln!("tree-space: could not load stylesheet: {problem:?}");
2875 }
2876 crate::highlight::set_palette(crate::theme::syntax_palette(&config.theme));
2877 let css = format!(
2878 "{}\n.tree-row label, .tree-rename-entry, .tree-menu, .hamburger-menu {{ font-size: {}px; }}",
2879 stylesheet.css, config.tree.font_size
2880 );
2881 let provider = gtk::CssProvider::new();
2882 provider.load_from_string(&css);
2883 if let Some(display) = gdk::Display::default() {
2884 gtk::style_context_add_provider_for_display(
2885 &display,
2886 &provider,
2887 gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
2888 );
2889 }
2890}
2891
2892fn home_dir() -> Option<PathBuf> {
2893 std::env::var_os("HOME").map(PathBuf::from)
2894}
2895
2896fn default_root(startup: &StartupRoot) -> Option<PathBuf> {
2899 if startup.is_bookmarks() {
2901 return None;
2902 }
2903 let last = SessionState::load().last_root.filter(|p| p.is_dir());
2904 startup.resolve(last).filter(|p| p.is_dir()).or_else(home_dir)
2905}
2906
2907fn adjust_path_after_removal(from: &[usize], path: &[usize]) -> Vec<usize> {
2911 let mut out = path.to_vec();
2912 for level in 0..path.len().min(from.len()) {
2913 if from[level] < path[level] {
2914 out[level] -= 1;
2915 return out;
2916 } else if from[level] > path[level] {
2917 return out;
2918 }
2919 }
2920 out
2921}
2922
2923#[cfg(test)]
2924mod adjust_path_tests {
2925 use super::adjust_path_after_removal;
2926
2927 #[test]
2928 fn adjusts_sibling_indices_after_a_removal() {
2929 assert_eq!(adjust_path_after_removal(&[0], &[2]), vec![1]);
2931 assert_eq!(adjust_path_after_removal(&[2], &[0]), vec![0]);
2933 assert_eq!(adjust_path_after_removal(&[0], &[1, 3]), vec![0, 3]);
2935 assert_eq!(adjust_path_after_removal(&[1, 2], &[1]), vec![1]);
2937 assert_eq!(adjust_path_after_removal(&[1, 0], &[1, 2]), vec![1, 1]);
2939 }
2940}
2941
2942#[cfg(test)]
2943mod visibility_tests {
2944 use super::*;
2945
2946 const L: PanelSide = PanelSide::Left;
2947 const R: PanelSide = PanelSide::Right;
2948
2949 fn plan(intent: VisibilityIntent, existing: &[PanelSide], shown: &[PanelSide]) -> VisibilityPlan {
2950 resolve_visibility(intent, existing, shown)
2951 }
2952
2953 #[test]
2954 fn show_all_shows_both() {
2955 let p = plan(VisibilityIntent::ShowAll, &[L], &[]);
2956 assert_eq!(p.show, vec![L]);
2958 assert!(p.seed);
2959 let both = plan(VisibilityIntent::ShowAll, &[L, R], &[]);
2960 assert_eq!(both.show, vec![L, R]);
2961 }
2962
2963 #[test]
2964 fn hide_all_hides_both() {
2965 let p = plan(VisibilityIntent::HideAll, &[L, R], &[L, R]);
2966 assert_eq!(p.hide, vec![L, R]);
2967 assert!(p.show.is_empty());
2968 }
2969
2970 #[test]
2971 fn toggle_all_shows_when_nothing_visible_and_hides_otherwise() {
2972 let shown = plan(VisibilityIntent::ToggleAll, &[L], &[]);
2973 assert_eq!(shown.show, vec![L]);
2975 let hidden = plan(VisibilityIntent::ToggleAll, &[L, R], &[L]);
2976 assert_eq!(hidden.hide, vec![L, R]);
2977 }
2978
2979 #[test]
2980 fn show_side_creates_a_missing_dock() {
2981 let p = plan(VisibilityIntent::ShowSide(R), &[L], &[L]);
2982 assert_eq!(p.create, Some(R));
2983 assert_eq!(p.show, vec![R]);
2984 }
2985
2986 #[test]
2987 fn hide_side_never_creates() {
2988 let p = plan(VisibilityIntent::HideSide(R), &[L], &[L]);
2989 assert_eq!(p.create, None);
2990 assert_eq!(p.hide, vec![R]);
2991 }
2992
2993 #[test]
2994 fn toggle_side_is_granular() {
2995 let create = plan(VisibilityIntent::ToggleSide(R), &[L], &[L]);
2997 assert_eq!(create.create, Some(R));
2998 assert_eq!(create.show, vec![R]);
2999 let hide = plan(VisibilityIntent::ToggleSide(L), &[L, R], &[L, R]);
3001 assert_eq!(hide.hide, vec![L]);
3002 let show = plan(VisibilityIntent::ToggleSide(L), &[L, R], &[R]);
3004 assert_eq!(show.show, vec![L]);
3005 }
3006
3007 #[test]
3008 fn toggling_one_side_leaves_the_other_alone() {
3009 let p = plan(VisibilityIntent::ToggleSide(R), &[L, R], &[L]);
3011 assert_eq!(p.show, vec![R]);
3012 assert!(!p.hide.contains(&L));
3013 }
3014
3015 #[test]
3016 fn whole_panel_show_does_not_seed_while_a_pane_is_open() {
3017 assert!(!should_seed_empty(true, true, true));
3020 assert!(should_seed_empty(false, true, true));
3022 assert!(should_seed_empty(true, true, false));
3024 assert!(!should_seed_empty(false, false, false));
3026 }
3027}
3028
3029#[cfg(test)]
3030mod nav_history_tests {
3031 use super::{NavHistory, ViewEntry};
3032 use std::path::PathBuf;
3033
3034 fn dir(s: &str) -> ViewEntry {
3035 ViewEntry::Dir(PathBuf::from(s))
3036 }
3037
3038 #[test]
3039 fn records_visits_in_order_and_navigates_both_ways() {
3040 let mut h = NavHistory::default();
3041 h.record(dir("/a"));
3042 h.record(dir("/b"));
3043 h.record(dir("/c"));
3044 assert!(h.can_back());
3045 assert!(!h.can_forward());
3046
3047 assert_eq!(h.back(), Some(dir("/b")));
3048 h.finish_navigation();
3049 assert_eq!(h.back(), Some(dir("/a")));
3050 h.finish_navigation();
3051 assert!(!h.can_back());
3052 assert!(h.can_forward());
3053
3054 assert_eq!(h.forward(), Some(dir("/b")));
3055 h.finish_navigation();
3056 assert_eq!(h.forward(), Some(dir("/c")));
3057 h.finish_navigation();
3058 assert!(h.can_back());
3059 assert!(!h.can_forward());
3060 }
3061
3062 #[test]
3063 fn a_new_visit_after_going_back_truncates_the_forward_tail() {
3064 let mut h = NavHistory::default();
3065 h.record(dir("/a"));
3066 h.record(dir("/b"));
3067 h.record(dir("/c"));
3068 assert_eq!(h.back(), Some(dir("/b")));
3069 h.finish_navigation();
3070 h.record(dir("/d"));
3072 assert!(!h.can_forward());
3073 assert_eq!(h.back(), Some(dir("/b")));
3074 }
3075
3076 #[test]
3077 fn navigating_does_not_record_and_repeats_are_ignored() {
3078 let mut h = NavHistory::default();
3079 h.record(dir("/a"));
3080 h.record(dir("/b"));
3081 let target = h.back().unwrap();
3083 assert_eq!(target, dir("/a"));
3084 h.record(target.clone());
3085 h.finish_navigation();
3086 assert!(h.can_forward());
3088 assert_eq!(h.forward(), Some(dir("/b")));
3089 h.finish_navigation();
3090
3091 h.record(dir("/b"));
3093 assert_eq!(h.cursor, 1);
3094 assert_eq!(h.entries.len(), 2);
3095 }
3096
3097 #[test]
3098 fn bookmarks_view_is_a_history_entry() {
3099 let mut h = NavHistory::default();
3100 h.record(ViewEntry::Bookmarks);
3101 h.record(dir("/a"));
3102 assert_eq!(h.back(), Some(ViewEntry::Bookmarks));
3104 h.finish_navigation();
3105 h.record(ViewEntry::Bookmarks);
3107 assert_eq!(h.entries.len(), 2);
3108 assert_eq!(h.forward(), Some(dir("/a")));
3110 h.finish_navigation();
3111 }
3112
3113 #[test]
3114 fn cannot_navigate_an_empty_history() {
3115 let mut h = NavHistory::default();
3116 assert!(!h.can_back());
3117 assert!(!h.can_forward());
3118 assert_eq!(h.back(), None);
3119 assert_eq!(h.forward(), None);
3120 }
3121}