Skip to content

Commit f24b490

Browse files
committed
issue 11: add startup pop up for os autostart
1 parent c0c8d66 commit f24b490

4 files changed

Lines changed: 152 additions & 6 deletions

File tree

docs/toml-config-reference.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,12 @@ Videos = ["mp4", "mkv", "mov", "avi", "webm", "flv", "wmv", "m4v"]
113113
After `filo init` writes config, automatically start watcher mode.
114114
- Runtime behavior:
115115
Used only by `filo init`.
116+
- Notes:
117+
This is a one-time action for the current session, not startup behavior.
118+
It does not survive a reboot. Starting filo on every login is a separate
119+
choice, offered as its own prompt in `filo init` and available any time as
120+
`filo autostart enable`. That setting lives in the OS service manager, not
121+
in this file.
116122

117123
#### `watch.debounce_ms`
118124

@@ -294,6 +300,7 @@ These are the built-in defaults used when no custom rules are set:
294300
- `start` watcher is currently non-recursive.
295301
- Keyword rules match on filename keywords only. Matching a keyword *and* an extension in one rule is not expressible in TOML today; use `filo arrange -k <keyword> -e <ext> -d <path>` for that.
296302
- Path destinations are not watched implicitly. If you want files that land in a `to_type = "path"` destination to be organized further, add that path to `watch.folders` yourself.
303+
- OS autostart (start filo on login) has no TOML key. It is a launchd plist, systemd user unit, or Windows registry Run key, installed by `filo autostart enable` or by answering yes to the autostart prompt in `filo init`. Query it with `filo autostart status`. Do not confuse it with `watch.auto_start`.
297304

298305
## Extending this document for future TOML options
299306

readme.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,12 @@ filo init
155155
or `move` (relocate it to a `Duplicates` subfolder next to the category).
156156
4. **Start watching now?** If yes, `filo` jumps straight into `start` mode
157157
when setup ends.
158+
5. **Start filo on system startup/login?** Default: off. If yes, the wizard
159+
installs the OS login service for you (launchd on macOS, systemd on Linux,
160+
a registry Run key on Windows) and prints where it was installed, so you
161+
never have to discover `filo autostart enable` on your own. If the service
162+
manager refuses, setup still finishes and prints the exact command to
163+
retry. Say no and nothing changes; you can enable it any time later.
158164

159165
The wizard writes `config.toml` to the platform config directory
160166
(see [Config file](#config-file)). You can re-run `filo init` any time to
@@ -224,6 +230,11 @@ filo autostart disable
224230
filo autostart status
225231
```
226232

233+
`filo init` offers this as a prompt, so you usually never need to run
234+
`enable` by hand. These commands remain the way to change your mind later.
235+
On macOS and Linux, enabling also starts the watcher right away; on Windows
236+
it takes effect at your next login.
237+
227238
### Examples
228239

229240
Preview what would happen if you cleaned up your Downloads folder right now:
@@ -299,6 +310,8 @@ folders = [
299310
"/home/you/Desktop",
300311
]
301312
# If true, `filo init` launches `filo start` automatically after setup.
313+
# This is a one-off for that session, not startup behavior. To run filo on
314+
# every login, use `filo autostart enable` (or say yes when init asks).
302315
auto_start = false
303316
# How long (milliseconds) a file must sit idle before filo acts on it.
304317
# Protects against operating on files that are still downloading.

src/commands/autostart.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,16 @@ pub fn disable() -> Result<()> {
9999
Ok(())
100100
}
101101

102+
/// Whether the login service is currently installed.
103+
///
104+
/// Lets callers (notably `filo init`) skip a redundant enable, which on
105+
/// macOS would fail outright because `launchctl load` rejects an
106+
/// already-loaded service.
107+
#[cfg(target_os = "macos")]
108+
pub fn is_enabled() -> bool {
109+
plist_path().map(|p| p.exists()).unwrap_or(false)
110+
}
111+
102112
#[cfg(target_os = "macos")]
103113
pub fn status() -> Result<()> {
104114
let path = plist_path()?;
@@ -192,6 +202,13 @@ pub fn disable() -> Result<()> {
192202
Ok(())
193203
}
194204

205+
/// Whether the login service is currently installed. See the macOS
206+
/// implementation for why callers need this.
207+
#[cfg(target_os = "linux")]
208+
pub fn is_enabled() -> bool {
209+
unit_path().map(|p| p.exists()).unwrap_or(false)
210+
}
211+
195212
#[cfg(target_os = "linux")]
196213
pub fn status() -> Result<()> {
197214
let path = unit_path()?;
@@ -231,12 +248,24 @@ pub fn enable() -> Result<()> {
231248

232249
if status.success() {
233250
println!("Autostart enabled. filo will start on login.");
251+
println!(" registry: {}\\filo", REG_KEY);
234252
} else {
235253
anyhow::bail!("reg add failed (exit {})", status);
236254
}
237255
Ok(())
238256
}
239257

258+
/// Whether the login service is currently installed. See the macOS
259+
/// implementation for why callers need this.
260+
#[cfg(target_os = "windows")]
261+
pub fn is_enabled() -> bool {
262+
std::process::Command::new("reg")
263+
.args(["query", REG_KEY, "/v", "filo"])
264+
.output()
265+
.map(|o| o.status.success())
266+
.unwrap_or(false)
267+
}
268+
240269
#[cfg(target_os = "windows")]
241270
pub fn disable() -> Result<()> {
242271
let status = std::process::Command::new("reg")

src/commands/init.rs

Lines changed: 103 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66
77
use std::io::{self, Write};
88
use std::path::PathBuf;
9+
use std::time::{Duration, Instant};
910

1011
use anyhow::{Context, Result};
1112
use dialoguer::{theme::ColorfulTheme, Confirm, MultiSelect, Select};
1213

13-
use crate::commands::scan;
14+
use crate::commands::{autostart, scan};
1415
use crate::config::{Config, DuplicateAction};
1516
use crate::daemon;
1617

@@ -69,12 +70,25 @@ pub fn run() -> Result<()> {
6970
.default(true)
7071
.interact()?;
7172

73+
// Asked alongside the other questions so the user decides everything up
74+
// front, but acted on further down: installing the login service starts a
75+
// watcher that reads the config, so the config has to be on disk first.
76+
let want_autostart = Confirm::with_theme(&theme)
77+
.with_prompt("Start filo automatically on system startup/login?")
78+
.default(false)
79+
.interact()?;
80+
7281
let path = config.save().context("saving config")?;
7382
println!();
7483
println!("Config saved to: {}", path.display());
7584
println!("Run `filo preview` to see planned moves, `filo scan` to organize existing files,");
7685
println!("or `filo start` to begin watching.");
86+
if !want_autostart {
87+
println!("To start filo on every login later, run `filo autostart enable`.");
88+
}
7789

90+
// The initial scan runs before anything starts watching, so a one-shot
91+
// scan and a freshly launched watcher never race over the same files.
7892
if config.watch.auto_start {
7993
println!();
8094
let scan_first = Confirm::with_theme(&theme)
@@ -83,18 +97,101 @@ pub fn run() -> Result<()> {
8397
.interact()?;
8498
if scan_first {
8599
scan::run(&config, false).context("running initial scan before watcher start")?;
86-
println!();
87100
}
88-
daemon::spawn_daemon().context("starting daemon")?;
89-
match daemon::running_pid() {
101+
}
102+
103+
// launchd and systemd start the watcher the moment the service is
104+
// installed, so find out whether that happened before deciding to start
105+
// one here.
106+
let mut watcher_pid = None;
107+
if want_autostart {
108+
println!();
109+
if install_autostart() {
110+
watcher_pid = wait_for_watcher(Duration::from_secs(2));
111+
if let Some(pid) = watcher_pid {
112+
println!(" It also started the watcher now (PID {}).", pid);
113+
}
114+
}
115+
}
116+
117+
if config.watch.auto_start {
118+
println!();
119+
match watcher_pid {
90120
Some(pid) => println!(
91-
"Watcher started in background (PID {}). Use `filo stop` to stop it.",
121+
"Watcher is already running (PID {}). Use `filo stop` to stop it.",
92122
pid
93123
),
94-
None => println!("Watcher started in background. Use `filo stop` to stop it."),
124+
None => start_watcher()?,
125+
}
126+
}
127+
128+
Ok(())
129+
}
130+
131+
/// Install the OS login service, reporting rather than propagating failure.
132+
/// Returns whether the service was installed by this call.
133+
///
134+
/// By this point the config is already written and setup has essentially
135+
/// succeeded, so a service manager that refuses to cooperate must not abort
136+
/// the wizard. The user gets the error and the exact command to retry.
137+
fn install_autostart() -> bool {
138+
if autostart::is_enabled() {
139+
println!("Autostart is already enabled; leaving it as it is.");
140+
return false;
141+
}
142+
// `autostart::enable` prints its own success line, including where the
143+
// service was installed.
144+
match autostart::enable() {
145+
Ok(()) => true,
146+
Err(e) => {
147+
println!("Could not enable autostart: {:#}", e);
148+
println!(" Setup is otherwise complete. To retry, run: filo autostart enable");
149+
false
150+
}
151+
}
152+
}
153+
154+
/// Wait briefly for a just-installed login service to bring the watcher up.
155+
///
156+
/// The PID file only appears once the watcher process is running, so checking
157+
/// immediately after `launchctl load` or `systemctl enable --now` usually
158+
/// loses the race. Returns `None` on Windows, where the registry Run key
159+
/// takes effect at the next login rather than now.
160+
fn wait_for_watcher(timeout: Duration) -> Option<u32> {
161+
let deadline = Instant::now() + timeout;
162+
loop {
163+
if let Some(pid) = daemon::running_pid() {
164+
return Some(pid);
95165
}
166+
if Instant::now() >= deadline {
167+
return None;
168+
}
169+
std::thread::sleep(Duration::from_millis(100));
170+
}
171+
}
172+
173+
/// Start the background watcher, unless something already started one.
174+
///
175+
/// On macOS and Linux, installing the login service starts the watcher
176+
/// immediately, so spawning another here would be a second watcher on the
177+
/// same folders (and `spawn_daemon` would fail outright).
178+
fn start_watcher() -> Result<()> {
179+
if let Some(pid) = daemon::running_pid() {
180+
println!(
181+
"Watcher is already running (PID {}). Use `filo stop` to stop it.",
182+
pid
183+
);
184+
return Ok(());
96185
}
97186

187+
daemon::spawn_daemon().context("starting daemon")?;
188+
match daemon::running_pid() {
189+
Some(pid) => println!(
190+
"Watcher started in background (PID {}). Use `filo stop` to stop it.",
191+
pid
192+
),
193+
None => println!("Watcher started in background. Use `filo stop` to stop it."),
194+
}
98195
Ok(())
99196
}
100197

0 commit comments

Comments
 (0)