Skip to main content

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}