1use std::{cell::{Cell, RefCell}, collections::HashMap, path::{Path, PathBuf}, rc::Rc};
41
42use gtk4_layer_shell::{Edge, KeyboardMode, Layer, LayerShell};
43use relm4::gtk::{gdk, gio, glib, prelude::*};
44use relm4::prelude::*;
45
46use crate::cmd::{Command, WidthArg};
47use crate::config::{
48 Bookmark, BookmarkPosition, BuiltinAction, Config, ContextAction, PANEL_MAX_WIDTH,
49 PANEL_MIN_WIDTH, PanelConfig, PanelLayer, PanelSide, PaneMenu, SessionState, ShortcutTarget,
50 StartupRoot, bookmark_file_path, load_stylesheet, save_bookmarks_to_path,
51};
52use crate::fs::SortKey;
53use crate::ipc;
54use crate::ui::bookmarks::{self, BookmarkEvent};
55use crate::ui::toolbar::{PaneShortcuts, Toolbar, ToolbarInit, ToolbarMsg, ToolbarOutput};
56use crate::ui::tree::{Tree, TreeInit, TreeOutput, TreeMsg};
57
58#[derive(Debug, Default, Clone, PartialEq, Eq)]
65struct NavHistory {
66 entries: Vec<PathBuf>,
67 cursor: usize,
69 navigating: bool,
72}
73
74impl NavHistory {
75 fn record(&mut self, path: &Path) {
79 if self.navigating {
80 return;
81 }
82 if self.entries.get(self.cursor).is_some_and(|cur| cur == path) {
83 return;
84 }
85 if self.entries.is_empty() {
86 self.entries.push(path.to_path_buf());
87 self.cursor = 0;
88 return;
89 }
90 self.entries.truncate(self.cursor + 1);
91 self.entries.push(path.to_path_buf());
92 self.cursor = self.entries.len() - 1;
93 }
94
95 fn can_back(&self) -> bool {
96 self.cursor > 0 && self.cursor < self.entries.len()
97 }
98
99 fn can_forward(&self) -> bool {
100 !self.entries.is_empty() && self.cursor + 1 < self.entries.len()
101 }
102
103 fn back(&mut self) -> Option<PathBuf> {
105 if !self.can_back() {
106 return None;
107 }
108 self.cursor -= 1;
109 self.navigating = true;
110 Some(self.entries[self.cursor].clone())
111 }
112
113 fn forward(&mut self) -> Option<PathBuf> {
115 if !self.can_forward() {
116 return None;
117 }
118 self.cursor += 1;
119 self.navigating = true;
120 Some(self.entries[self.cursor].clone())
121 }
122
123 fn finish_navigation(&mut self) {
125 self.navigating = false;
126 }
127}
128
129pub struct Pane {
131 id: u64,
132 toolbar: Controller<Toolbar>,
133 tree: Controller<Tree>,
134 root: Option<PathBuf>,
137 canonical_root: Option<PathBuf>,
140 history: NavHistory,
142 filter_bar: gtk::Box,
145 filter_entry: gtk::SearchEntry,
146 widget: gtk::Overlay,
152}
153
154impl Pane {
155 fn set_root(&mut self, root: PathBuf) {
158 self.canonical_root = std::fs::canonicalize(&root).ok();
159 self.root = Some(root);
160 }
161}
162
163struct Dock {
165 side: PanelSide,
166 window: gtk::Window,
169 container: gtk::Box,
172 bookmarks: Option<gtk::Widget>,
176 panes: Vec<Pane>,
177 active_pane: Option<u64>,
179}
180
181#[derive(Debug, Clone, Copy, PartialEq, Eq)]
185pub enum VisibilityIntent {
186 ShowAll,
188 HideAll,
190 ToggleAll,
192 ShowSide(PanelSide),
194 HideSide(PanelSide),
196 ToggleSide(PanelSide),
198}
199
200#[derive(Debug, Clone, PartialEq, Eq)]
206pub struct VisibilityPlan {
207 pub show: Vec<PanelSide>,
209 pub hide: Vec<PanelSide>,
211 pub create: Option<PanelSide>,
213 pub seed: bool,
216}
217
218pub fn resolve_visibility(
222 intent: VisibilityIntent,
223 existing: &[PanelSide],
224 shown: &[PanelSide],
225) -> VisibilityPlan {
226 match intent {
227 VisibilityIntent::ShowAll => VisibilityPlan {
228 show: existing.to_vec(),
229 hide: Vec::new(),
230 create: None,
231 seed: true,
232 },
233 VisibilityIntent::HideAll => VisibilityPlan {
234 show: Vec::new(),
235 hide: PanelSide::ALL.to_vec(),
236 create: None,
237 seed: false,
238 },
239 VisibilityIntent::ToggleAll => {
240 if shown.is_empty() {
241 VisibilityPlan {
242 show: existing.to_vec(),
243 hide: Vec::new(),
244 create: None,
245 seed: true,
246 }
247 } else {
248 VisibilityPlan {
249 show: Vec::new(),
250 hide: PanelSide::ALL.to_vec(),
251 create: None,
252 seed: false,
253 }
254 }
255 }
256 VisibilityIntent::ShowSide(side) => VisibilityPlan {
257 show: vec![side],
258 hide: Vec::new(),
259 create: (!existing.contains(&side)).then_some(side),
260 seed: true,
261 },
262 VisibilityIntent::HideSide(side) => VisibilityPlan {
263 show: Vec::new(),
264 hide: vec![side],
265 create: None,
266 seed: false,
267 },
268 VisibilityIntent::ToggleSide(side) => {
269 if !existing.contains(&side) {
270 VisibilityPlan {
271 show: vec![side],
272 hide: Vec::new(),
273 create: Some(side),
274 seed: true,
275 }
276 } else if shown.contains(&side) {
277 VisibilityPlan {
278 show: Vec::new(),
279 hide: vec![side],
280 create: None,
281 seed: false,
282 }
283 } else {
284 VisibilityPlan {
285 show: vec![side],
286 hide: Vec::new(),
287 create: None,
288 seed: true,
289 }
290 }
291 }
292 }
293}
294
295#[derive(Debug)]
297pub enum AppMsg {
298 PaneToolbar { id: u64, out: ToolbarOutput },
300 PaneTree { id: u64, out: TreeOutput },
302 PaneShortcut { id: u64, action: ContextAction },
305 OpenFolderPicked { id: u64, path: Option<PathBuf> },
307 LaunchRequest { command: Command },
309 SplitFromPane { id: u64 },
311 OpenSplitFrom { id: u64, root: PathBuf },
314 OpenOppositeFrom { id: u64, root: PathBuf },
317 ClosePane { id: u64 },
320 FilterChanged { id: u64, filter: String },
322 FilterClosed { id: u64 },
324 Shutdown,
326 ResizeBy { side: PanelSide, delta: i32 },
328 ResizeCommit,
330 FocusPane { id: u64 },
332 BookmarkEvent { side: PanelSide, event: BookmarkEvent },
334 BookmarkEditSaved { index: usize, name: String, path: PathBuf },
336 ToggleBookmarks,
338}
339
340pub struct AppInit {
343 pub command: Command,
344 pub listener: Option<std::os::unix::net::UnixListener>,
345}
346
347pub struct App {
349 config: Config,
350 status: String,
351 window: gtk::Window,
352 file_dialog: gtk::FileDialog,
353 docks: Vec<Dock>,
355 pane_container: gtk::Box,
357 primary_side: PanelSide,
359 next_id: u64,
361 visible: bool,
363 widths: HashMap<PanelSide, u32>,
367 last_root: Option<PathBuf>,
369 show_bookmarks: bool,
373 bookmarks: Vec<Bookmark>,
376 save_generation: Rc<Cell<u64>>,
380}
381
382#[relm4::component(pub)]
383impl SimpleComponent for App {
384 type Init = AppInit;
385 type Input = AppMsg;
386 type Output = ();
387
388 view! {
389 gtk::Window {
390 set_default_size: (model.config.panel.width as i32, 520),
391
392 gtk::Box {
393 set_orientation: gtk::Orientation::Vertical,
394 set_spacing: 0,
395 add_css_class: "panel",
396
397 append: &model.pane_container,
398
399 append: status = >k::Label {
400 #[watch]
401 set_label: &model.status,
402 set_halign: gtk::Align::Start,
403 set_ellipsize: gtk::pango::EllipsizeMode::End,
404 add_css_class: "status-bar",
405 }
406 }
407 }
408 }
409
410 fn init(
411 init: Self::Init,
412 root: Self::Root,
413 sender: ComponentSender<Self>,
414 ) -> ComponentParts<Self> {
415 let loaded = Config::load();
416 let mut config = loaded.config;
417 let parent = root.clone();
418
419 let (bookmarks, bookmark_problem) = crate::config::load_bookmarks(&config.bookmarks);
422 if let Err(problem) = crate::config::ensure_bookmarks_file(&config.bookmarks, &bookmarks) {
423 eprintln!("tree-space: could not create bookmarks file: {problem:?}");
424 }
425
426 let session = SessionState::load();
427 let default_width = config.panel.width;
430 let widths: HashMap<PanelSide, u32> = [
431 (
432 PanelSide::Left,
433 session.left_width.unwrap_or(default_width).clamp(PANEL_MIN_WIDTH, PANEL_MAX_WIDTH),
434 ),
435 (
436 PanelSide::Right,
437 session.right_width.unwrap_or(default_width).clamp(PANEL_MIN_WIDTH, PANEL_MAX_WIDTH),
438 ),
439 ]
440 .into_iter()
441 .collect();
442
443 let primary_side = init.command.side.unwrap_or(config.panel.side);
444 config.panel.width = widths[&primary_side];
446 let primary_width = config.panel.width;
447 let bookmarks_startup = config.startup.is_bookmarks();
450 let show_bookmarks = config.bookmarks.show || bookmarks_startup;
451 let roots = if !init.command.roots.is_empty() {
455 init.command.roots.clone()
456 } else if bookmarks_startup {
457 Vec::new()
458 } else {
459 let last = session.last_root.clone().filter(|p| p.is_dir());
460 let fallback = config
461 .startup
462 .resolve(last)
463 .filter(|p| p.is_dir())
464 .or_else(home_dir);
465 if let Some(p) = fallback { vec![p] } else { Vec::new() }
466 };
467
468 let mut panes: Vec<Pane> = Vec::new();
469 let mut next_id = 0u64;
470 for root in roots.iter() {
471 let pane =
472 make_pane(&config, parent.clone(), next_id, primary_side, primary_width, sender.clone());
473 pane.tree.emit(TreeMsg::OpenRoot(root.clone()));
474 panes.push(pane);
475 panes.last_mut().unwrap().set_root(root.clone());
476 next_id += 1;
477 }
478 if panes.is_empty() && !bookmarks_startup {
481 panes.push(make_pane(
482 &config,
483 parent.clone(),
484 next_id,
485 primary_side,
486 primary_width,
487 sender.clone(),
488 ));
489 next_id += 1;
490 }
491
492 let pane_container = gtk::Box::new(gtk::Orientation::Vertical, 0);
493 pane_container.set_vexpand(true);
494 fill_pane_container(&pane_container, &panes);
495
496 let mut status = String::new();
497 if let Some(problem) = loaded.problem {
498 status = format!("config: {problem:?}");
499 } else if let Some(problem) = bookmark_problem {
500 status = format!("bookmarks: {problem:?}");
501 }
502
503 let mut model = App {
504 config,
505 status,
506 window: root.clone(),
507 file_dialog: gtk::FileDialog::new(),
508 docks: vec![Dock {
509 side: primary_side,
510 window: root.clone(),
511 container: pane_container.clone(),
512 bookmarks: None,
513 panes,
514 active_pane: None,
515 }],
516 pane_container,
517 primary_side,
518 next_id,
519 visible: !init.command.hidden,
520 widths,
521 last_root: session.last_root.clone(),
522 show_bookmarks,
523 bookmarks,
524 save_generation: Rc::new(Cell::new(0)),
525 };
526
527 init_layer_window(&model.window, &model.config, primary_side, primary_width);
528 install_css(&model.config);
529 attach_bookmarks_shortcut(&model.window, &model.config.pane_menu, &sender);
530 if layer_shell_available() {
533 attach_resize_controls(&model.window, primary_side, &sender);
534 }
535
536 if let Some(listener) = init.listener {
539 let ipc_sender = sender.input_sender().clone();
540 ipc::spawn_listener(listener, move |command| {
541 let sender = ipc_sender.clone();
542 glib::MainContext::default().invoke(move || {
543 let _ = sender.send(AppMsg::LaunchRequest { command });
544 });
545 });
546 }
547
548 let widgets = view_output!();
549
550 model.refresh_bookmarks(0, &sender);
552
553 let shutting_down = Rc::new(Cell::new(false));
560 let flag = shutting_down.clone();
561 let shutdown_sender = sender.clone();
562 model.window.connect_close_request(move |_| {
563 if flag.get() {
564 return glib::Propagation::Proceed;
565 }
566 flag.set(true);
567 shutdown_sender.input(AppMsg::Shutdown);
568 glib::Propagation::Stop
569 });
570
571 ComponentParts { model, widgets }
572 }
573
574 fn update(&mut self, msg: Self::Input, sender: ComponentSender<Self>) {
575 match msg {
576 AppMsg::PaneToolbar { id, out } => match out {
577 ToolbarOutput::OpenFolder => {
578 self.set_active(id);
579 self.show_open_folder(id, &sender);
580 }
581 ToolbarOutput::NavigateTo(path) => {
582 self.set_active(id);
583 if let Some((di, pi)) = self.dock_pane_of(id) {
584 self.docks[di].panes[pi].tree.emit(TreeMsg::OpenRoot(path));
585 }
586 }
587 ToolbarOutput::FilterRequested => {
588 self.set_active(id);
589 self.open_filter(id);
590 }
591 ToolbarOutput::SplitView => {
592 sender.input(AppMsg::SplitFromPane { id });
593 }
594 ToolbarOutput::Collapse => {
595 self.collapse_dock(id);
596 }
597 ToolbarOutput::ClosePane => {
598 sender.input(AppMsg::ClosePane { id });
599 }
600 ToolbarOutput::PaneItem(target) => {
601 self.set_active(id);
602 if let ShortcutTarget::Builtin(action) = &target
605 && action.is_pane_action()
606 {
607 self.dispatch_pane_builtin(id, *action, &sender);
608 } else if let Some((di, pi)) = self.dock_pane_of(id)
609 && let Some(msg) = pane_item_message(&target)
610 {
611 self.docks[di].panes[pi].tree.emit(msg);
612 }
613 }
614 },
615
616 AppMsg::PaneTree { id, out } => match out {
617 TreeOutput::Status(message) => {
618 self.set_active(id);
619 self.status = message;
620 }
621 TreeOutput::OpenFolderRequested => {
622 self.set_active(id);
623 self.show_open_folder(id, &sender);
624 }
625 TreeOutput::OpenSplit(root) => {
626 self.set_active(id);
627 sender.input(AppMsg::OpenSplitFrom { id, root });
628 }
629 TreeOutput::OpenOpposite(root) => {
630 self.set_active(id);
631 sender.input(AppMsg::OpenOppositeFrom { id, root });
632 }
633 TreeOutput::PaneAction(action) => {
634 self.dispatch_pane_builtin(id, action, &sender);
635 }
636 TreeOutput::AddBookmark(path) => {
637 self.set_active(id);
638 self.add_bookmark(path);
639 self.refresh_all_bookmarks(&sender);
640 }
641 TreeOutput::RootChanged(root) => {
642 self.set_active(id);
643 if let Some((di, pi)) = self.dock_pane_of(id) {
644 let pane = &mut self.docks[di].panes[pi];
645 pane.history.record(&root);
649 pane.history.finish_navigation();
650 pane.set_root(root.clone());
651 pane.toolbar.emit(ToolbarMsg::SetRoot(root.clone()));
652 }
653 self.status = String::new();
654 self.last_root = Some(root);
656 self.persist_session();
657 }
658 },
659
660 AppMsg::PaneShortcut { id, action } => {
661 self.run_pane_shortcut(id, action, &sender);
662 }
663
664 AppMsg::OpenFolderPicked { id, path: Some(path) } => {
665 if let Some((di, pi)) = self.dock_pane_of(id) {
666 self.docks[di].panes[pi].tree.emit(TreeMsg::OpenRoot(path));
667 }
668 }
669 AppMsg::OpenFolderPicked { path: None, .. } => {}
670
671 AppMsg::LaunchRequest { command } => {
672 self.handle_launch(command, &sender);
673 }
674
675 AppMsg::SplitFromPane { id } => {
676 let (di, pi) = self.dock_pane_of(id).unwrap_or((0, 0));
677 let seed = self.docks[di].panes.get(pi).and_then(|p| p.root.clone()).or_else(home_dir);
678 self.add_pane(di, seed, &sender);
679 }
680
681 AppMsg::OpenSplitFrom { id, root } => {
682 if let Some((di, _pi)) = self.dock_pane_of(id) {
683 self.add_pane(di, Some(root), &sender);
684 }
685 }
686
687 AppMsg::OpenOppositeFrom { id, root } => {
688 if let Some((di, _pi)) = self.dock_pane_of(id) {
689 let side = self.docks[di].side.opposite();
690 let target = self.ensure_dock(side, false, &sender);
691 self.add_pane(target, Some(root), &sender);
692 self.set_docks_visible(self.visible);
695 }
696 }
697
698 AppMsg::ClosePane { id } => {
699 let Some((di, pi)) = self.dock_pane_of(id) else {
700 return;
701 };
702 self.docks[di].panes.remove(pi);
703 if self.docks[di].active_pane == Some(id) {
704 self.docks[di].active_pane = self.docks[di].panes.last().map(|p| p.id);
705 }
706 let panes_left: usize = self.docks.iter().map(|d| d.panes.len()).sum();
710 if panes_left == 0 && !self.show_bookmarks {
711 self.window.close();
712 return;
713 }
714 if self.docks[di].panes.is_empty() {
715 if self.show_bookmarks {
716 fill_pane_container(&self.docks[di].container.clone(), &[]);
719 } else if di == 0 {
720 self.visible = false;
724 self.docks[di].window.set_visible(false);
725 } else {
726 let dock = self.docks.remove(di);
727 dock.window.close();
728 }
729 } else {
730 fill_pane_container(&self.docks[di].container.clone(), &self.docks[di].panes);
731 }
732 }
733
734 AppMsg::FilterChanged { id, filter } => {
735 if let Some((di, pi)) = self.dock_pane_of(id) {
736 self.docks[di].panes[pi].tree.emit(TreeMsg::SetFilter(filter));
737 }
738 }
739 AppMsg::FilterClosed { id } => {
740 if let Some((di, pi)) = self.dock_pane_of(id) {
741 let pane = &mut self.docks[di].panes[pi];
742 pane.filter_entry.set_text("");
743 pane.tree.emit(TreeMsg::SetFilter(String::new()));
744 pane.filter_bar.set_visible(false);
745 }
746 }
747
748 AppMsg::FocusPane { id } => {
749 if let Some((di, pi)) = self.dock_pane_of(id) {
750 self.docks[di].panes[pi].tree.emit(TreeMsg::Focus);
751 }
752 }
753
754 AppMsg::Shutdown => {
755 for dock in &self.docks {
760 stop_video_widgets(dock.window.upcast_ref());
761 }
762 for dock in &self.docks {
764 for pane in &dock.panes {
765 pane.tree.emit(TreeMsg::Shutdown);
766 }
767 }
768 let window = self.window.clone();
771 glib::timeout_add_local_once(std::time::Duration::from_millis(SHUTDOWN_GRACE_MS), move || {
772 window.close();
773 });
774 }
775
776 AppMsg::ResizeBy { side, delta } => self.resize_by(side, delta),
777 AppMsg::ResizeCommit => self.persist_session(),
778
779 AppMsg::BookmarkEvent { side, event } => match event {
780 BookmarkEvent::Open(path) => self.open_bookmark(side, path, &sender),
781 BookmarkEvent::Edit(index) => self.edit_bookmark(side, index, &sender),
782 BookmarkEvent::Delete(index) => {
783 if index < self.bookmarks.len() {
784 self.bookmarks.remove(index);
785 self.save_bookmarks();
786 self.refresh_all_bookmarks(&sender);
787 }
788 }
789 BookmarkEvent::Hide => self.set_bookmarks_visible(false, &sender),
790 },
791 AppMsg::BookmarkEditSaved { index, name, path } => {
792 if let Some(bookmark) = self.bookmarks.get_mut(index) {
793 bookmark.name = name;
794 bookmark.path = path;
795 self.save_bookmarks();
796 self.refresh_all_bookmarks(&sender);
797 }
798 }
799 AppMsg::ToggleBookmarks => self.set_bookmarks_visible(!self.show_bookmarks, &sender),
800 }
801 }
802}
803
804impl App {
805 fn handle_launch(&mut self, command: Command, sender: &ComponentSender<Self>) {
807 let hidden = command.hidden;
808
809 if let Some(width) = command.width {
812 let side = command.side.unwrap_or(self.primary_side);
813 match width {
814 WidthArg::To(px) => self.set_width(side, px),
815 WidthArg::By(delta) => self.resize_by(side, delta),
816 }
817 self.persist_session();
818 if command.roots.is_empty() && command.reveal.is_empty() {
819 return;
820 }
821 }
822
823 if command.roots.is_empty() && command.reveal.is_empty() {
824 let intent = match (command.side, hidden) {
827 (Some(side), true) => VisibilityIntent::HideSide(side),
828 (Some(side), false) => VisibilityIntent::ToggleSide(side),
829 (None, true) => VisibilityIntent::HideAll,
830 (None, false) => VisibilityIntent::ToggleAll,
831 };
832 self.apply_visibility(intent, sender);
833 return;
834 }
835
836 for root in &command.roots {
837 if self.find_pane_with_dir(root).is_some() {
838 continue;
841 }
842 let side = command.side.unwrap_or(self.primary_side);
843 let di = self.ensure_dock(side, false, sender);
844 self.add_pane(di, Some(root.clone()), sender);
845 }
846
847 for (root, select) in command.reveal_targets() {
852 if !root.is_dir() {
853 continue;
854 }
855 let side = command.side.unwrap_or(self.primary_side);
856 let di = self.ensure_dock(side, false, sender);
857 let id = self.add_pane(di, Some(root), sender);
858 if let Some(select) = select
859 && let Some((di, pi)) = self.dock_pane_of(id)
860 {
861 self.docks[di].panes[pi].tree.emit(TreeMsg::SelectPath(select));
862 }
863 }
864 self.set_docks_visible(!hidden);
866 }
867
868 fn apply_visibility(&mut self, intent: VisibilityIntent, sender: &ComponentSender<Self>) {
872 let existing: Vec<PanelSide> = self.docks.iter().map(|d| d.side).collect();
873 let shown: Vec<PanelSide> = self
874 .docks
875 .iter()
876 .filter(|d| d.window.is_visible())
877 .map(|d| d.side)
878 .collect();
879 let plan = resolve_visibility(intent, &existing, &shown);
880
881 if let Some(side) = plan.create {
882 self.ensure_dock(side, plan.seed, sender);
883 }
884 for side in &plan.show {
885 let di = self.ensure_dock(*side, plan.seed, sender);
888 self.docks[di].window.set_visible(true);
889 }
890 for side in &plan.hide {
891 if let Some(di) = self.dock_of_side(*side) {
892 self.docks[di].window.set_visible(false);
893 }
894 }
895 self.refresh_visible();
897 let focus = plan
900 .show
901 .iter()
902 .filter_map(|side| self.dock_of_side(*side))
903 .filter_map(|di| self.docks[di].panes.last())
904 .map(|pane| pane.tree.emit(TreeMsg::Focus))
905 .next();
906 let _ = focus;
907 }
908
909 fn set_active(&mut self, id: u64) {
911 if let Some((di, _pi)) = self.dock_pane_of(id) {
912 self.docks[di].active_pane = Some(id);
913 }
914 }
915
916 fn open_filter(&mut self, id: u64) {
920 if let Some((di, pi)) = self.dock_pane_of(id) {
921 let pane = &mut self.docks[di].panes[pi];
922 pane.filter_bar.set_visible(true);
923 let entry = pane.filter_entry.clone();
924 glib::idle_add_local_once(move || {
925 entry.grab_focus();
926 });
927 }
928 }
929
930 fn add_bookmark(&mut self, path: PathBuf) {
932 if self.bookmarks.iter().any(|b| b.path == path) {
933 self.status = format!("{} is already bookmarked", path.display());
934 return;
935 }
936 self.bookmarks.push(Bookmark {
937 name: Bookmark::default_name(&path),
938 path: path.clone(),
939 });
940 self.save_bookmarks();
941 self.status = format!("Bookmarked {}", path.display());
942 }
943
944 fn open_bookmark(&mut self, side: PanelSide, path: PathBuf, sender: &ComponentSender<Self>) {
946 let path = crate::config::expand_bookmark_path(&path);
947 if !path.is_dir() {
948 self.status = format!("{} is not a directory", path.display());
949 return;
950 }
951 if let Some((di, pi)) = self.find_pane_with_dir(&path) {
953 let id = self.docks[di].panes[pi].id;
954 self.set_active(id);
955 self.docks[di].panes[pi].tree.emit(TreeMsg::Focus);
956 self.set_docks_visible(true);
957 return;
958 }
959 let di = self.dock_of_side(side).unwrap_or(0);
960 self.add_pane(di, Some(path), sender);
961 }
962
963 fn edit_bookmark(&mut self, side: PanelSide, index: usize, sender: &ComponentSender<Self>) {
965 let Some(bookmark) = self.bookmarks.get(index).cloned() else {
966 return;
967 };
968 let Some(di) = self.dock_of_side(side) else { return };
969 let parent = self.docks[di].window.clone();
970 let sender = sender.clone();
971 let path = crate::config::expand_bookmark_path(&bookmark.path);
972 bookmarks::show_bookmark_editor(&parent, &bookmark.name, &path, move |name, path| {
973 sender.input(AppMsg::BookmarkEditSaved { index, name, path });
974 });
975 }
976
977 fn save_bookmarks(&mut self) {
979 let path = bookmark_file_path(&self.config.bookmarks.file);
980 if let Err(err) = save_bookmarks_to_path(&path, &self.bookmarks) {
981 self.status = format!("Could not save bookmarks: {err:?}");
982 }
983 }
984
985 fn refresh_all_bookmarks(&mut self, sender: &ComponentSender<Self>) {
988 for di in 0..self.docks.len() {
989 self.refresh_bookmarks(di, sender);
990 }
991 }
992
993 fn set_bookmarks_visible(&mut self, show: bool, sender: &ComponentSender<Self>) {
996 self.show_bookmarks = show;
997 self.refresh_all_bookmarks(sender);
998 if !show {
999 self.prune_empty_docks();
1000 }
1001 }
1002
1003 fn prune_empty_docks(&mut self) {
1006 let mut di = self.docks.len();
1007 while di > 1 {
1008 di -= 1;
1009 if self.docks[di].panes.is_empty() {
1010 let dock = self.docks.remove(di);
1011 dock.window.close();
1012 }
1013 }
1014 if self.docks.iter().all(|dock| dock.panes.is_empty()) {
1017 self.window.close();
1018 }
1019 }
1020
1021 fn refresh_bookmarks(&mut self, di: usize, sender: &ComponentSender<Self>) {
1025 let widget = if self.show_bookmarks {
1026 let list = self.bookmarks.clone();
1027 let side = self.docks[di].side;
1028 let sender = sender.clone();
1029 let on_event: Rc<dyn Fn(BookmarkEvent)> =
1030 Rc::new(move |event| sender.input(AppMsg::BookmarkEvent { side, event }));
1031 Some(bookmarks::bookmarks_section(&list, on_event))
1032 } else {
1033 None
1034 };
1035 self.place_bookmark_widget(di, widget);
1036 }
1037
1038 fn place_bookmark_widget(&mut self, di: usize, widget: Option<gtk::Widget>) {
1041 if let Some(old) = self.docks[di].bookmarks.take() {
1042 remove_from_parent(&old);
1043 }
1044 let Some(widget) = widget else { return };
1045 let stack = self.docks[di].container.clone();
1046 let outer = stack
1047 .parent()
1048 .and_downcast::<gtk::Box>()
1049 .unwrap_or_else(|| stack.clone());
1050 match self.config.bookmarks.position {
1051 BookmarkPosition::Top => outer.prepend(&widget),
1052 BookmarkPosition::Bottom => outer.insert_child_after(&widget, Some(&stack)),
1053 }
1054 self.docks[di].bookmarks = Some(widget);
1055 }
1056
1057 fn dispatch_pane_builtin(
1061 &mut self,
1062 id: u64,
1063 action: BuiltinAction,
1064 sender: &ComponentSender<Self>,
1065 ) {
1066 self.set_active(id);
1067 match action {
1068 BuiltinAction::OpenFolder => self.show_open_folder(id, sender),
1069 BuiltinAction::Filter => self.open_filter(id),
1070 BuiltinAction::SplitView => sender.input(AppMsg::SplitFromPane { id }),
1071 BuiltinAction::Up => self.go_up(id),
1072 BuiltinAction::Back => self.go_back(id),
1073 BuiltinAction::Forward => self.go_forward(id),
1074 BuiltinAction::Collapse => self.collapse_dock(id),
1075 BuiltinAction::ClosePane => sender.input(AppMsg::ClosePane { id }),
1076 BuiltinAction::ToggleBookmarks => sender.input(AppMsg::ToggleBookmarks),
1077 _ => {}
1078 }
1079 }
1080
1081 fn go_up(&mut self, id: u64) {
1084 let Some((di, pi)) = self.dock_pane_of(id) else {
1085 return;
1086 };
1087 let Some(parent) = self.docks[di].panes[pi]
1088 .root
1089 .as_deref()
1090 .and_then(Path::parent)
1091 .map(Path::to_path_buf)
1092 else {
1093 return;
1094 };
1095 self.docks[di].panes[pi].tree.emit(TreeMsg::OpenRoot(parent));
1096 }
1097
1098 fn go_back(&mut self, id: u64) {
1100 let Some((di, pi)) = self.dock_pane_of(id) else {
1101 return;
1102 };
1103 if let Some(target) = self.docks[di].panes[pi].history.back() {
1104 self.docks[di].panes[pi].tree.emit(TreeMsg::OpenRoot(target));
1105 }
1106 }
1107
1108 fn go_forward(&mut self, id: u64) {
1110 let Some((di, pi)) = self.dock_pane_of(id) else {
1111 return;
1112 };
1113 if let Some(target) = self.docks[di].panes[pi].history.forward() {
1114 self.docks[di].panes[pi].tree.emit(TreeMsg::OpenRoot(target));
1115 }
1116 }
1117
1118 fn run_pane_shortcut(
1122 &mut self,
1123 id: u64,
1124 action: ContextAction,
1125 sender: &ComponentSender<Self>,
1126 ) {
1127 self.set_active(id);
1128 let Some(target) = ShortcutTarget::from_action(&action) else {
1129 return;
1130 };
1131 if let ShortcutTarget::Builtin(builtin) = &target
1132 && builtin.is_pane_action()
1133 {
1134 self.dispatch_pane_builtin(id, *builtin, sender);
1135 return;
1136 }
1137 if let Some((di, pi)) = self.dock_pane_of(id)
1138 && let Some(msg) = pane_item_message(&target)
1139 {
1140 self.docks[di].panes[pi].tree.emit(msg);
1141 }
1142 }
1143
1144 fn dock_pane_of(&self, id: u64) -> Option<(usize, usize)> {
1146 self.docks.iter().enumerate().find_map(|(di, dock)| {
1147 dock.panes
1148 .iter()
1149 .position(|pane| pane.id == id)
1150 .map(|pi| (di, pi))
1151 })
1152 }
1153
1154 fn find_pane_with_dir(&self, path: &PathBuf) -> Option<(usize, usize)> {
1159 let canonical = std::fs::canonicalize(path).ok().unwrap_or_else(|| path.clone());
1160 self.docks.iter().enumerate().find_map(|(di, dock)| {
1161 dock.panes.iter().enumerate().find_map(|(pi, pane)| {
1162 if pane.canonical_root.as_deref() == Some(canonical.as_path()) {
1163 Some((di, pi))
1164 } else {
1165 None
1166 }
1167 })
1168 })
1169 }
1170
1171 fn dock_of_side(&self, side: PanelSide) -> Option<usize> {
1173 self.docks.iter().position(|dock| dock.side == side)
1174 }
1175
1176 fn collapse_dock(&mut self, id: u64) {
1179 if let Some((di, _)) = self.dock_pane_of(id) {
1180 self.set_dock_visible(di, false);
1181 }
1182 }
1183
1184 fn set_dock_visible(&mut self, di: usize, visible: bool) {
1186 self.docks[di].window.set_visible(visible);
1187 self.refresh_visible();
1188 }
1189
1190 fn set_docks_visible(&mut self, visible: bool) {
1192 for dock in &self.docks {
1193 dock.window.set_visible(visible);
1194 if visible
1196 && let Some(pane) = dock.panes.last()
1197 {
1198 pane.tree.emit(TreeMsg::Focus);
1199 }
1200 }
1201 self.visible = visible;
1202 }
1203
1204 fn refresh_visible(&mut self) {
1207 self.visible = self.docks.iter().any(|dock| dock.window.is_visible());
1208 }
1209
1210 fn width_for(&self, side: PanelSide) -> u32 {
1212 self.widths.get(&side).copied().unwrap_or(self.config.panel.width)
1213 }
1214
1215 fn set_width(&mut self, side: PanelSide, width: u32) {
1218 let width = width.clamp(PANEL_MIN_WIDTH, PANEL_MAX_WIDTH);
1219 if width == self.width_for(side) {
1220 return;
1221 }
1222 self.widths.insert(side, width);
1223 let panel = PanelConfig { width, ..self.config.panel };
1224 for dock in self.docks.iter().filter(|dock| dock.side == side) {
1225 apply_window_width(&dock.window, width);
1226 for pane in &dock.panes {
1227 pane.tree.emit(TreeMsg::SetPanel(panel));
1228 }
1229 }
1230 if side == self.primary_side {
1232 self.config.panel.width = width;
1233 }
1234 self.schedule_save();
1235 }
1236
1237 fn resize_by(&mut self, side: PanelSide, delta: i32) {
1239 let target = (self.width_for(side) as i64 + delta as i64)
1240 .clamp(PANEL_MIN_WIDTH as i64, PANEL_MAX_WIDTH as i64) as u32;
1241 self.set_width(side, target);
1242 }
1243
1244 fn session_state(&self) -> SessionState {
1246 SessionState {
1247 last_root: self.last_root.clone(),
1248 left_width: Some(self.width_for(PanelSide::Left)),
1249 right_width: Some(self.width_for(PanelSide::Right)),
1250 }
1251 }
1252
1253 fn persist_session(&mut self) {
1255 self.save_generation.set(self.save_generation.get().wrapping_add(1));
1256 if let Err(err) = self.session_state().save() {
1257 self.status = format!("Could not save session: {err}");
1258 }
1259 }
1260
1261 fn schedule_save(&mut self) {
1265 let generation = self.save_generation.get().wrapping_add(1);
1266 self.save_generation.set(generation);
1267 let current = self.save_generation.clone();
1268 let state = self.session_state();
1269 glib::timeout_add_local_once(std::time::Duration::from_millis(SAVE_DEBOUNCE_MS), move || {
1270 if current.get() == generation {
1271 let _ = state.save();
1272 }
1273 });
1274 }
1275
1276 fn ensure_dock(&mut self, side: PanelSide, seed: bool, sender: &ComponentSender<Self>) -> usize {
1280 if let Some((di, _)) = self.docks.iter().enumerate().find(|(_, d)| d.side == side) {
1281 if seed && self.docks[di].panes.is_empty() {
1282 let startup = self.config.startup.clone();
1284 if let Some(root) = default_root(&startup) {
1285 self.add_pane(di, Some(root), sender);
1286 }
1287 }
1288 return di;
1289 }
1290 let config = &self.config;
1291 let width = self.width_for(side);
1292 let window = gtk::Window::new();
1293 init_layer_window(&window, config, side, width);
1294 window.set_default_size(width as i32, 520);
1295 if layer_shell_available() {
1296 attach_resize_controls(&window, side, sender);
1297 }
1298 attach_bookmarks_shortcut(&window, &config.pane_menu, sender);
1299 let outer = gtk::Box::new(gtk::Orientation::Vertical, 0);
1303 outer.set_vexpand(true);
1304 let container = gtk::Box::new(gtk::Orientation::Vertical, 0);
1305 container.set_vexpand(true);
1306 outer.append(&container);
1307 window.set_child(Some(&outer));
1308
1309 let di = self.docks.len();
1310 self.docks.push(Dock {
1311 side,
1312 window,
1313 container,
1314 bookmarks: None,
1315 panes: Vec::new(),
1316 active_pane: None,
1317 });
1318 if seed {
1319 let startup = self.config.startup.clone();
1320 if let Some(root) = default_root(&startup) {
1321 self.add_pane(di, Some(root), sender);
1322 }
1323 }
1324 self.refresh_bookmarks(di, sender);
1325 di
1326 }
1327
1328 fn add_pane(&mut self, di: usize, root: Option<PathBuf>, sender: &ComponentSender<Self>) -> u64 {
1330 let id = self.next_id;
1331 self.next_id += 1;
1332 let parent = self.docks[di].window.clone();
1333 let side = self.docks[di].side;
1334 let width = self.width_for(side);
1335 let pane = make_pane(&self.config, parent, id, side, width, sender.clone());
1336 let mut pane = pane;
1337 if let Some(root) = &root {
1338 pane.tree.emit(TreeMsg::OpenRoot(root.clone()));
1339 pane.set_root(root.clone());
1340 }
1341 let dock = &mut self.docks[di];
1342 dock.panes.push(pane);
1343 dock.active_pane = Some(id);
1344 let container = dock.container.clone();
1345 fill_pane_container(&container, &dock.panes);
1346 let focus_sender = sender.clone();
1349 glib::idle_add_local_once(move || focus_sender.input(AppMsg::FocusPane { id }));
1350 id
1351 }
1352
1353 fn show_open_folder(&self, id: u64, sender: &ComponentSender<Self>) {
1354 let sender = sender.clone();
1355 self.file_dialog.select_folder(
1356 None::<>k::Window>,
1357 None::<&gio::Cancellable>,
1358 move |result| {
1359 let path = result.ok().and_then(|file| file.path());
1360 sender.input(AppMsg::OpenFolderPicked { id, path });
1361 },
1362 );
1363 }
1364}
1365
1366fn pane_item_message(target: &ShortcutTarget) -> Option<TreeMsg> {
1374 match target {
1375 ShortcutTarget::Builtin(action) => match action {
1376 BuiltinAction::ToggleHidden => Some(TreeMsg::ToggleHidden),
1377 BuiltinAction::SortByName => Some(TreeMsg::SetSortKey(SortKey::Name)),
1378 BuiltinAction::SortBySize => Some(TreeMsg::SetSortKey(SortKey::Size)),
1379 BuiltinAction::SortByModified => Some(TreeMsg::SetSortKey(SortKey::Modified)),
1380 BuiltinAction::SortByType => Some(TreeMsg::SetSortKey(SortKey::Type)),
1381 BuiltinAction::ToggleSortAscending => Some(TreeMsg::ToggleSortDirection),
1382 other => Some(TreeMsg::RunShortcut {
1383 target: ShortcutTarget::Builtin(*other),
1384 }),
1385 },
1386 ShortcutTarget::Command(_) => Some(TreeMsg::RunShortcut { target: target.clone() }),
1387 }
1388}
1389
1390fn make_pane(
1391 config: &Config,
1392 parent: gtk::Window,
1393 id: u64,
1394 side: PanelSide,
1395 width: u32,
1396 sender: ComponentSender<App>,
1397) -> Pane {
1398 let overlay = gtk::Overlay::new();
1401 let toolbar = Toolbar::builder()
1402 .launch(ToolbarInit {
1403 overlay: overlay.clone(),
1404 pane_menu: config.pane_menu.clone(),
1405 })
1406 .forward(sender.input_sender(), move |out| AppMsg::PaneToolbar { id, out });
1407 let tree = Tree::builder()
1408 .launch(TreeInit {
1409 config: config.tree.clone(),
1410 parent,
1411 menu: config.context_menu.clone(),
1412 side,
1413 panel: PanelConfig { width, ..config.panel },
1414 })
1415 .forward(sender.input_sender(), move |out| AppMsg::PaneTree { id, out });
1416
1417 let widget = gtk::Box::new(gtk::Orientation::Vertical, 0);
1418 widget.append(toolbar.widget());
1419
1420 let filter_bar = gtk::Box::new(gtk::Orientation::Horizontal, 4);
1422 filter_bar.add_css_class("filter-bar");
1423 filter_bar.set_visible(false);
1424
1425 let filter_entry = gtk::SearchEntry::new();
1426 filter_entry.set_placeholder_text(Some("Filter..."));
1427 filter_entry.set_hexpand(true);
1428 filter_entry.set_valign(gtk::Align::Center);
1429 filter_entry.add_css_class("filter-entry");
1430 {
1431 let s = sender.clone();
1432 filter_entry.connect_search_changed(move |entry| {
1433 s.input(AppMsg::FilterChanged {
1434 id,
1435 filter: entry.text().to_string(),
1436 });
1437 });
1438 }
1439 filter_bar.append(&filter_entry);
1440
1441 let close_btn = gtk::Button::from_icon_name("window-close-symbolic");
1442 close_btn.set_tooltip_text(Some("Clear filter"));
1443 close_btn.set_valign(gtk::Align::Center);
1444 close_btn.add_css_class("flat");
1445 {
1446 let s = sender.clone();
1447 close_btn.connect_clicked(move |_| {
1448 s.input(AppMsg::FilterClosed { id });
1449 });
1450 }
1451 filter_bar.append(&close_btn);
1452
1453 widget.append(&filter_bar);
1454
1455 let tree_widget = tree.widget();
1456 tree_widget.set_vexpand(true);
1457 widget.append(tree_widget);
1458 widget.set_vexpand(true);
1459
1460 overlay.set_child(Some(&widget));
1461
1462 {
1466 let shortcuts = Rc::new(PaneShortcuts::compile(&config.pane_menu));
1467 let s = sender.clone();
1468 let keys = gtk::EventControllerKey::new();
1469 keys.set_propagation_phase(gtk::PropagationPhase::Capture);
1470 keys.connect_key_pressed(move |_, key, _, state| {
1471 if let Some(action) = shortcuts.action_for(key, state) {
1472 s.input(AppMsg::PaneShortcut { id, action });
1473 glib::Propagation::Stop
1474 } else {
1475 glib::Propagation::Proceed
1476 }
1477 });
1478 widget.add_controller(keys);
1479 }
1480
1481 Pane {
1482 id,
1483 toolbar,
1484 tree,
1485 root: None,
1486 canonical_root: None,
1487 history: NavHistory::default(),
1488 filter_bar,
1489 filter_entry,
1490 widget: overlay,
1491 }
1492}
1493
1494fn remove_from_parent(w: >k::Widget) {
1502 let Some(parent) = w.parent() else { return };
1503 if let Ok(paned) = parent.clone().downcast::<gtk::Paned>() {
1504 let none: Option<>k::Widget> = None;
1505 if paned.start_child().is_some_and(|c| c == *w) {
1506 paned.set_start_child(none);
1507 }
1508 let none: Option<>k::Widget> = None;
1509 if paned.end_child().is_some_and(|c| c == *w) {
1510 paned.set_end_child(none);
1511 }
1512 } else if let Ok(b) = parent.downcast::<gtk::Box>() {
1513 b.remove(w);
1514 } else {
1515 w.unparent();
1516 }
1517}
1518
1519fn pane_widget(pane: &Pane) -> gtk::Widget {
1520 pane.widget.clone().upcast()
1521}
1522
1523fn fill_pane_container(container: >k::Box, panes: &[Pane]) {
1534 for pane in panes {
1539 remove_from_parent(pane.widget.upcast_ref());
1540 }
1541 while let Some(child) = container.first_child() {
1543 container.remove(&child);
1544 }
1545
1546 match panes {
1547 [] => {}
1548 [single] => {
1549 let w = pane_widget(single);
1550 w.set_vexpand(true);
1551 container.append(&w);
1552 }
1553 panes => {
1554 let n = panes.len();
1557 let last_w = pane_widget(&panes[n - 1]);
1558 last_w.set_vexpand(true);
1559 let mut right: gtk::Widget = last_w.clone().upcast();
1560 for pane in panes[..n - 1].iter().rev() {
1561 let left_w = pane_widget(pane);
1562 left_w.set_vexpand(true);
1563 let split = gtk::Paned::new(gtk::Orientation::Vertical);
1564 split.set_vexpand(true);
1565 split.set_wide_handle(true);
1566 split.set_shrink_start_child(true);
1568 split.set_shrink_end_child(true);
1569 split.set_resize_start_child(true);
1570 split.set_resize_end_child(true);
1571 split.set_start_child(Some(&left_w));
1572 split.set_end_child(Some(&right));
1573 {
1576 let split2 = split.clone();
1577 relm4::gtk::glib::timeout_add_local_once(
1578 std::time::Duration::from_millis(PANED_CENTER_DELAY_MS),
1579 move || {
1580 let alloc = split2.height();
1583 if alloc > 0 {
1584 split2.set_position(alloc / 2);
1585 }
1586 },
1587 );
1588 }
1589 right = split.upcast();
1590 }
1591 container.append(&right);
1592 }
1593 }
1594}
1595
1596fn stop_video_widgets(widget: >k::Widget) {
1602 if let Some(video) = widget.downcast_ref::<gtk::Video>() {
1603 video.set_media_stream(None::<>k::MediaStream>);
1604 }
1605 let mut child = widget.first_child();
1606 while let Some(current) = child {
1607 stop_video_widgets(¤t);
1608 child = current.next_sibling();
1609 }
1610}
1611
1612const WIDTH_STEP: i32 = 24;
1614
1615const SAVE_DEBOUNCE_MS: u64 = 300;
1617
1618const PANED_CENTER_DELAY_MS: u64 = 100;
1621
1622const SHUTDOWN_GRACE_MS: u64 = 150;
1625
1626fn apply_window_width(window: >k::Window, width: u32) {
1628 let (_, height) = window.default_size();
1629 if window.is_layer_window() {
1632 window.set_exclusive_zone(width as i32);
1633 }
1634 window.set_size_request(width as i32, -1);
1635 window.set_default_size(width as i32, if height > 0 { height } else { -1 });
1636}
1637
1638fn attach_bookmarks_shortcut(window: >k::Window, menu: &PaneMenu, sender: &ComponentSender<App>) {
1642 let shortcuts = Rc::new(PaneShortcuts::compile(menu));
1643 let s = sender.clone();
1644 let keys = gtk::EventControllerKey::new();
1645 keys.set_propagation_phase(gtk::PropagationPhase::Capture);
1646 keys.connect_key_pressed(move |_, key, _, state| {
1647 if matches!(
1648 shortcuts.action_for(key, state),
1649 Some(ContextAction::Builtin(BuiltinAction::ToggleBookmarks))
1650 ) {
1651 s.input(AppMsg::ToggleBookmarks);
1652 glib::Propagation::Stop
1653 } else {
1654 glib::Propagation::Proceed
1655 }
1656 });
1657 window.add_controller(keys);
1658}
1659
1660fn attach_resize_controls(window: >k::Window, side: PanelSide, sender: &ComponentSender<App>) {
1663 attach_resize_drag(window, side, sender.input_sender().clone());
1664 attach_resize_keys(window, side, sender.input_sender().clone());
1665}
1666
1667fn attach_resize_drag(window: >k::Window, side: PanelSide, sender: relm4::Sender<AppMsg>) {
1668 let drag = gtk::GestureDrag::new();
1669 drag.set_button(gdk::BUTTON_SECONDARY);
1670 drag.set_propagation_phase(gtk::PropagationPhase::Capture);
1671 let state = Rc::new(RefCell::new((false, 0.0f64)));
1673
1674 let begin_state = state.clone();
1675 drag.connect_drag_begin(move |gesture, _, _| {
1676 let super_held = gesture
1677 .current_event_state()
1678 .contains(gdk::ModifierType::SUPER_MASK);
1679 *begin_state.borrow_mut() = (super_held, 0.0);
1680 });
1681
1682 let update_sender = sender.clone();
1683 let update_state = state.clone();
1684 drag.connect_drag_update(move |_, offset_x, _| {
1685 let mut state = update_state.borrow_mut();
1686 if !state.0 {
1687 return;
1688 }
1689 let step = offset_x - state.1;
1690 state.1 = offset_x;
1691 let widen = match side {
1693 PanelSide::Left => step,
1694 PanelSide::Right => -step,
1695 };
1696 let delta = widen.round() as i32;
1697 if delta != 0 {
1698 let _ = update_sender.send(AppMsg::ResizeBy { side, delta });
1699 }
1700 });
1701
1702 let end_sender = sender;
1703 let end_state = state.clone();
1704 drag.connect_drag_end(move |_, _, _| {
1705 if end_state.borrow().0 {
1706 let _ = end_sender.send(AppMsg::ResizeCommit);
1707 }
1708 });
1709
1710 window.add_controller(drag);
1711}
1712
1713fn attach_resize_keys(window: >k::Window, side: PanelSide, sender: relm4::Sender<AppMsg>) {
1714 let keys = gtk::EventControllerKey::new();
1715 keys.set_propagation_phase(gtk::PropagationPhase::Capture);
1716 keys.connect_key_pressed(move |_, key, _, state| {
1717 if !state.contains(gdk::ModifierType::SUPER_MASK) {
1718 return glib::Propagation::Proceed;
1719 }
1720 match key {
1721 gdk::Key::minus | gdk::Key::underscore | gdk::Key::KP_Subtract => {
1722 let _ = sender.send(AppMsg::ResizeBy { side, delta: -WIDTH_STEP });
1723 glib::Propagation::Stop
1724 }
1725 gdk::Key::equal | gdk::Key::plus | gdk::Key::KP_Add => {
1726 let _ = sender.send(AppMsg::ResizeBy { side, delta: WIDTH_STEP });
1727 glib::Propagation::Stop
1728 }
1729 _ => glib::Propagation::Proceed,
1730 }
1731 });
1732 window.add_controller(keys);
1733}
1734
1735fn layer_shell_available() -> bool {
1743 static SUPPORTED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1744 *SUPPORTED.get_or_init(gtk4_layer_shell::is_supported)
1745}
1746
1747fn init_layer_window(window: >k::Window, config: &Config, side: PanelSide, width: u32) {
1754 if !layer_shell_available() {
1755 configure_plain_window(window, width);
1756 return;
1757 }
1758 if window.is_layer_window() {
1759 return;
1760 }
1761 window.init_layer_shell();
1762 window.set_layer(layer_of(config.panel.layer));
1763 let edge = match side {
1764 PanelSide::Left => Edge::Left,
1765 PanelSide::Right => Edge::Right,
1766 };
1767 window.set_anchor(Edge::Top, true);
1768 window.set_anchor(Edge::Bottom, true);
1769 window.set_anchor(edge, true);
1770 window.set_exclusive_zone(width as i32);
1771 window.set_size_request(width as i32, -1);
1772 let margin = config.panel.margin as i32;
1773 window.set_margin(Edge::Top, margin);
1774 window.set_margin(Edge::Bottom, margin);
1775 window.set_margin(Edge::Left, margin);
1776 window.set_margin(Edge::Right, margin);
1777
1778 window.set_keyboard_mode(KeyboardMode::OnDemand);
1793 let motion = gtk::EventControllerMotion::new();
1794 {
1795 let kb_window = window.clone();
1796 motion.connect_enter(move |_, _, _| kb_window.set_keyboard_mode(KeyboardMode::OnDemand));
1797 }
1798 {
1799 let kb_window = window.clone();
1800 motion.connect_leave(move |_| {
1801 if has_visible_popover(kb_window.upcast_ref()) {
1806 return;
1807 }
1808 kb_window.set_keyboard_mode(KeyboardMode::None);
1809 });
1810 }
1811 window.add_controller(motion);
1812}
1813
1814fn configure_plain_window(window: >k::Window, width: u32) {
1820 static ANNOUNCED: std::sync::OnceLock<()> = std::sync::OnceLock::new();
1821 ANNOUNCED.get_or_init(|| {
1822 eprintln!(
1823 "tree-space: this compositor does not support wlr-layer-shell, so the panel \
1824 cannot dock; opening it as a normal window instead."
1825 );
1826 });
1827 window.set_title(Some("tree-space"));
1828 window.set_default_size(width as i32, 720);
1829 window.set_size_request(width as i32, -1);
1830}
1831
1832fn has_visible_popover(widget: >k::Widget) -> bool {
1835 if widget.is::<gtk::Popover>() && widget.is_visible() {
1836 return true;
1837 }
1838 let mut child = widget.first_child();
1839 while let Some(current) = child {
1840 if has_visible_popover(¤t) {
1841 return true;
1842 }
1843 child = current.next_sibling();
1844 }
1845 false
1846}
1847
1848fn layer_of(layer: PanelLayer) -> Layer {
1849 use PanelLayer::*;
1850 match layer {
1851 Background => Layer::Background,
1852 Bottom => Layer::Bottom,
1853 Top => Layer::Top,
1854 Overlay => Layer::Overlay,
1855 }
1856}
1857
1858fn install_css(config: &Config) {
1862 let stylesheet = load_stylesheet();
1863 if let Some(problem) = &stylesheet.problem {
1864 eprintln!("tree-space: could not read stylesheet: {problem:?}");
1865 }
1866 let css = format!(
1867 "{}\n.tree-row label, .tree-rename-entry, .tree-menu, .hamburger-menu {{ font-size: {}px; }}",
1868 stylesheet.css, config.tree.font_size
1869 );
1870 let provider = gtk::CssProvider::new();
1871 provider.load_from_string(&css);
1872 if let Some(display) = gdk::Display::default() {
1873 gtk::style_context_add_provider_for_display(
1874 &display,
1875 &provider,
1876 gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
1877 );
1878 }
1879}
1880
1881fn home_dir() -> Option<PathBuf> {
1882 std::env::var_os("HOME").map(PathBuf::from)
1883}
1884
1885fn default_root(startup: &StartupRoot) -> Option<PathBuf> {
1888 if startup.is_bookmarks() {
1890 return None;
1891 }
1892 let last = SessionState::load().last_root.filter(|p| p.is_dir());
1893 startup.resolve(last).filter(|p| p.is_dir()).or_else(home_dir)
1894}
1895#[cfg(test)]
1896mod visibility_tests {
1897 use super::*;
1898
1899 const L: PanelSide = PanelSide::Left;
1900 const R: PanelSide = PanelSide::Right;
1901
1902 fn plan(intent: VisibilityIntent, existing: &[PanelSide], shown: &[PanelSide]) -> VisibilityPlan {
1903 resolve_visibility(intent, existing, shown)
1904 }
1905
1906 #[test]
1907 fn show_all_shows_both() {
1908 let p = plan(VisibilityIntent::ShowAll, &[L], &[]);
1909 assert_eq!(p.show, vec![L]);
1911 assert!(p.seed);
1912 let both = plan(VisibilityIntent::ShowAll, &[L, R], &[]);
1913 assert_eq!(both.show, vec![L, R]);
1914 }
1915
1916 #[test]
1917 fn hide_all_hides_both() {
1918 let p = plan(VisibilityIntent::HideAll, &[L, R], &[L, R]);
1919 assert_eq!(p.hide, vec![L, R]);
1920 assert!(p.show.is_empty());
1921 }
1922
1923 #[test]
1924 fn toggle_all_shows_when_nothing_visible_and_hides_otherwise() {
1925 let shown = plan(VisibilityIntent::ToggleAll, &[L], &[]);
1926 assert_eq!(shown.show, vec![L]);
1928 let hidden = plan(VisibilityIntent::ToggleAll, &[L, R], &[L]);
1929 assert_eq!(hidden.hide, vec![L, R]);
1930 }
1931
1932 #[test]
1933 fn show_side_creates_a_missing_dock() {
1934 let p = plan(VisibilityIntent::ShowSide(R), &[L], &[L]);
1935 assert_eq!(p.create, Some(R));
1936 assert_eq!(p.show, vec![R]);
1937 }
1938
1939 #[test]
1940 fn hide_side_never_creates() {
1941 let p = plan(VisibilityIntent::HideSide(R), &[L], &[L]);
1942 assert_eq!(p.create, None);
1943 assert_eq!(p.hide, vec![R]);
1944 }
1945
1946 #[test]
1947 fn toggle_side_is_granular() {
1948 let create = plan(VisibilityIntent::ToggleSide(R), &[L], &[L]);
1950 assert_eq!(create.create, Some(R));
1951 assert_eq!(create.show, vec![R]);
1952 let hide = plan(VisibilityIntent::ToggleSide(L), &[L, R], &[L, R]);
1954 assert_eq!(hide.hide, vec![L]);
1955 let show = plan(VisibilityIntent::ToggleSide(L), &[L, R], &[R]);
1957 assert_eq!(show.show, vec![L]);
1958 }
1959
1960 #[test]
1961 fn toggling_one_side_leaves_the_other_alone() {
1962 let p = plan(VisibilityIntent::ToggleSide(R), &[L, R], &[L]);
1964 assert_eq!(p.show, vec![R]);
1965 assert!(!p.hide.contains(&L));
1966 }
1967}
1968
1969#[cfg(test)]
1970mod nav_history_tests {
1971 use super::NavHistory;
1972 use std::path::{Path, PathBuf};
1973
1974 fn p(s: &str) -> PathBuf {
1975 PathBuf::from(s)
1976 }
1977
1978 #[test]
1979 fn records_visits_in_order_and_navigates_both_ways() {
1980 let mut h = NavHistory::default();
1981 h.record(Path::new("/a"));
1982 h.record(Path::new("/b"));
1983 h.record(Path::new("/c"));
1984 assert!(h.can_back());
1985 assert!(!h.can_forward());
1986
1987 assert_eq!(h.back(), Some(p("/b")));
1988 h.finish_navigation();
1989 assert_eq!(h.back(), Some(p("/a")));
1990 h.finish_navigation();
1991 assert!(!h.can_back());
1992 assert!(h.can_forward());
1993
1994 assert_eq!(h.forward(), Some(p("/b")));
1995 h.finish_navigation();
1996 assert_eq!(h.forward(), Some(p("/c")));
1997 h.finish_navigation();
1998 assert!(h.can_back());
1999 assert!(!h.can_forward());
2000 }
2001
2002 #[test]
2003 fn a_new_visit_after_going_back_truncates_the_forward_tail() {
2004 let mut h = NavHistory::default();
2005 h.record(Path::new("/a"));
2006 h.record(Path::new("/b"));
2007 h.record(Path::new("/c"));
2008 assert_eq!(h.back(), Some(p("/b")));
2009 h.finish_navigation();
2010 h.record(Path::new("/d"));
2012 assert!(!h.can_forward());
2013 assert_eq!(h.back(), Some(p("/b")));
2014 }
2015
2016 #[test]
2017 fn navigating_does_not_record_and_repeats_are_ignored() {
2018 let mut h = NavHistory::default();
2019 h.record(Path::new("/a"));
2020 h.record(Path::new("/b"));
2021 let target = h.back().unwrap();
2023 assert_eq!(target, p("/a"));
2024 h.record(&target);
2025 h.finish_navigation();
2026 assert!(h.can_forward());
2028 assert_eq!(h.forward(), Some(p("/b")));
2029 h.finish_navigation();
2030
2031 h.record(Path::new("/b"));
2033 assert_eq!(h.cursor, 1);
2034 assert_eq!(h.entries.len(), 2);
2035 }
2036
2037 #[test]
2038 fn cannot_navigate_an_empty_history() {
2039 let mut h = NavHistory::default();
2040 assert!(!h.can_back());
2041 assert!(!h.can_forward());
2042 assert_eq!(h.back(), None);
2043 assert_eq!(h.forward(), None);
2044 }
2045}