tree_space/entry.rs
1//! Process entry point, shared by the `ts` and `tree-space` binaries.
2//!
3//! Usage:
4//! ts — start the panel, or toggle it if a
5//! panel is already running
6//! ts --side left|right — dock to that screen edge
7//! (default: the configured side)
8//! ts /path/a [/path/b ...] — open each directory as a pane
9//! ts --side right /path — open a pane in the right dock
10//! ts /path/to/file — reveal a file: open its folder and
11//! select it
12//! ts --select /path/to/anything — reveal an explicit file or folder
13//! ts --hidden — launch (or keep) the panel hidden
14//! ts --width 420 — set the panel width (absolute px)
15//! ts --width +40 — widen the panel by 40px (`-40` narrows)
16//!
17//! Directories may also be passed via the TREE_SPACE_DIRS environment variable
18//! as a colon-separated list:
19//! TREE_SPACE_DIRS=/a:/b ts
20//!
21//! Single instance
22//! ───────────────
23//! Only one panel runs at a time. When an instance is already alive, a new
24//! invocation forwards its intent over the instance socket (see `ipc.rs`)
25//! and exits without starting a second panel:
26//! * no path, no side → toggle every dock together
27//! * `--side X` → toggle just that side (create and show it if absent)
28//! * `--side X -H` → hide just that side
29//! * with path(s) → add a pane for each directory (never a duplicate of
30//! an already-open root), then show the panel
31//! * `--select P` → open a pane for P's folder and select P
32//! * `--hidden` → hide every dock; never shows
33//!
34//! Launch arguments are consumed here (via `Command`) and the process argv
35//! handed to GApplication is reduced to the bare program name, so neither the
36//! `--side` flag nor directory paths reach GLib's option parser.
37//!
38//! Desktop / XDG activation
39//! ────────────────────────
40//! The `GApplication` is created with `HANDLES_OPEN` and an `open` handler, so
41//! a `.desktop` entry using `Exec=tree-space %u` (or `xdg-open`, or a portal
42//! request) forwards the file/folder to the running panel instead of being
43//! discarded. The handler funnels every opened file back through the same
44//! instance socket, so it behaves exactly like `ts --select <path>`.
45
46use std::path::PathBuf;
47
48use relm4::gtk::gio;
49use relm4::gtk::prelude::{ApplicationExtManual, FileExt};
50use relm4::gtk;
51use relm4::prelude::*;
52
53use crate::cmd::Command;
54use crate::freedesktop;
55use crate::ipc;
56use crate::ui::app::{App, AppInit};
57
58/// Run the application: parse arguments, hand off to a running instance or
59/// start the panel. Shared by the `ts` and `tree-space` binaries.
60pub fn run() -> Result<(), Box<dyn std::error::Error>> {
61 if std::env::args().any(|a| a == "--help" || a == "-h") {
62 print_usage();
63 return Ok(());
64 }
65 if std::env::args().any(|a| a == "--version" || a == "-V") {
66 println!("tree-space {}", env!("CARGO_PKG_VERSION"));
67 return Ok(());
68 }
69
70 let mut command = Command::parse(std::env::args_os().skip(1));
71 merge_env_dirs(&mut command);
72
73 // Hand off to a running instance, if there is one.
74 if ipc::deliver(&command) {
75 return Ok(());
76 }
77 let listener = match ipc::bind() {
78 Ok(listener) => Some(listener),
79 Err(_) => {
80 // Most likely lost a race with another starting instance: try to
81 // hand off to it. Without a listener we can still run server-side,
82 // just without the ability to receive further requests.
83 if ipc::deliver(&command) {
84 return Ok(());
85 }
86 None
87 }
88 };
89
90 // The primary instance also serves the freedesktop `FileManager1` D-Bus
91 // interface. Integrations that bypass the MIME default — the portal's
92 // "Show in folder" and Electron's `showItemInFolder`, used by VS Code's
93 // "Open Containing Folder" — call it directly, so without this they land in
94 // whichever file manager ships that service (usually Nautilus).
95 if listener.is_some() {
96 freedesktop::serve_file_manager();
97 }
98
99 // Build the GApplication by hand so it carries `HANDLES_OPEN` and an `open`
100 // handler: that is what lets desktop/X!DG activation (a `.desktop` with
101 // `Exec=tree-space %u`, `xdg-open`, a portal request) hand us a file or
102 // folder instead of silently discarding it. Every such request is turned
103 // back into a [`Command`] and pushed through the same instance socket the
104 // CLI uses, so it lands in the running panel as an ordinary reveal.
105 let app = gtk::Application::new(
106 Some("org.tree_space.panel"),
107 gio::ApplicationFlags::HANDLES_OPEN,
108 );
109 app.connect_open(|_app, files, _hint| {
110 let mut command = Command::default();
111 for path in files.iter().filter_map(|f| f.path()) {
112 command.reveal.push(path);
113 }
114 if !command.reveal.is_empty() {
115 let _ = ipc::deliver(&command);
116 }
117 });
118
119 let app = RelmApp::from_app(app)
120 .visible_on_activate(!command.hidden)
121 .with_args(vec!["ts".to_owned()]);
122 app.run::<App>(AppInit { command, listener });
123
124 Ok(())
125}
126
127/// Fold TREE_SPACE_DIRS=path1:path2 into the command's roots.
128fn merge_env_dirs(command: &mut Command) {
129 if let Ok(dirs) = std::env::var("TREE_SPACE_DIRS") {
130 for part in dirs.split(':') {
131 let trimmed = part.trim();
132 let path = std::path::absolute(trimmed).unwrap_or_else(|_| PathBuf::from(trimmed));
133 if path.is_dir() {
134 command.roots.push(path);
135 }
136 }
137 }
138}
139
140fn print_usage() {
141 println!(
142 "\
143tree-space — a dockable, keyboard-first file manager panel.
144
145USAGE:
146 ts [OPTIONS] [PATH ...]
147
148ARGS:
149 PATH... Paths to open. A directory opens as a pane (an already-open
150 directory is never duplicated — the panel is just shown). A file
151 opens its containing folder as a pane and selects the file.
152
153OPTIONS:
154 -s, --side <left|right> Which screen edge to dock to. Defaults to the
155 configured side when omitted.
156 --select <PATH> Reveal PATH: open its containing folder and
157 select it. Works for a file or a folder, and is
158 the mechanism `.desktop`/`xdg-open` activation
159 uses (`Exec=tree-space %u`).
160 -H, --hidden Launch hidden (or, for a running panel, hide it
161 and keep it hidden). Never shows the panel.
162 -w, --width <W> Resize the panel. `420` sets an absolute width;
163 `+40` grows it by 40px and `-40` shrinks it.
164 Does not change whether the panel is shown.
165 -k, --key <ACCEL> Run a configured shortcut against the active pane
166 as if it were pressed (e.g. `Ctrl+c`, `F2`,
167 `Alt+Left`). Needs no keyboard focus; used by
168 external button decks. Does not change visibility.
169 -V, --version Print the version and exit.
170 -h, --help Print this help.
171
172The panel is single-instance: an invocation while one is already running is
173forwarded to it and exits. With no directory arguments it toggles the panel
174(hide when visible, show when hidden); with `--side` it ensures a dock on that
175side and shows it; with `--hidden` it ensures the panel is hidden instead."
176 );
177}