diff --git a/README.md b/README.md index 7a228ad..5f42a42 100644 --- a/README.md +++ b/README.md @@ -89,8 +89,10 @@ and start the app again. Grab `usage-monitor.exe` from the [Releases page](https://github.com/tiennm99/usage-monitor/releases/latest). -Put it anywhere on disk (e.g. `%LOCALAPPDATA%\UsageMonitor\`) and -run it. The app self-updates from the same Releases feed. +Put it in a folder of its own that you can write to (e.g. +`%LOCALAPPDATA%\UsageMonitor\`) and run it. The app is portable: it keeps +`settings.json` next to the exe, so moving the folder keeps your settings. It +self-updates from the same Releases feed. **First-run note:** the binary is unsigned, so SmartScreen will show "Windows protected your PC". Click **More info** → **Run anyway**. Code @@ -103,7 +105,9 @@ From the same pick one: - `usage-monitor_X.Y.Z_amd64.AppImage`: make it executable - (`chmod +x`) and run it. The AppImage self-updates in place. + (`chmod +x`) and run it. Like the Windows exe it is portable and keeps + `settings.json` next to the AppImage file, so give it a folder you can + write to. The AppImage self-updates in place. - `usage-monitor_X.Y.Z_amd64.deb`: install it with `sudo apt install ./usage-monitor_X.Y.Z_amd64.deb`. The app still checks for updates, but installing one is left to you: download the @@ -209,8 +213,9 @@ Settings are saved to: | OS | Settings file | |----|---------------| -| Windows | `%APPDATA%\UsageMonitor\settings.json` | -| Linux | `$XDG_CONFIG_HOME/UsageMonitor/settings.json`, usually `~/.config/UsageMonitor/settings.json` | +| Windows | `settings.json` next to `usage-monitor.exe` | +| Linux AppImage | `settings.json` next to the `.AppImage` file | +| Linux `.deb` | `$XDG_CONFIG_HOME/UsageMonitor/settings.json`, usually `~/.config/UsageMonitor/settings.json` | Other files the app writes: diff --git a/docs/release-process.md b/docs/release-process.md index 0226417..00465d6 100644 --- a/docs/release-process.md +++ b/docs/release-process.md @@ -119,8 +119,9 @@ updater code at v0.4.0, v0.4.1 and v0.5.0, they need: `updates/updater-helper-*.exe` files they leave behind. Since the rename, an upgraded install starts with default settings and -keeps any autostart entry the legacy app registered: the settings folder -and autostart entry names changed, and nothing migrates them. See +keeps any autostart entry the legacy app registered: settings now live in +`settings.json` next to the exe, the autostart entry name changed, and +nothing migrates either. See [the rename plan](../plans/261021-1226-rename-to-usage-monitor/plan.md). `scripts/make-latest-json.js` enforces the asset rules before diff --git a/plans/261021-1226-rename-to-usage-monitor/plan.md b/plans/261021-1226-rename-to-usage-monitor/plan.md index 3cdff3a..ba690ab 100644 --- a/plans/261021-1226-rename-to-usage-monitor/plan.md +++ b/plans/261021-1226-rename-to-usage-monitor/plan.md @@ -41,8 +41,9 @@ merges into `main`. ## Accepted costs -- Installs that upgrade start with default settings, because the settings - folder name changed and nothing migrates it. +- Installs that upgrade start with default settings: the portable Windows + exe and the AppImage now keep `settings.json` next to the file, and nothing + migrates the old one. - On Windows, an install that had autostart on keeps its old Run entry, which points at the same exe file. Turning autostart on in the new app adds a second entry. diff --git a/src/core/settings.rs b/src/core/settings.rs index 309cf22..b037393 100644 --- a/src/core/settings.rs +++ b/src/core/settings.rs @@ -1,6 +1,7 @@ -// Persisted settings. The path, schema, key names and physical-px positions -// stay byte-compatible with the Win32 builds, so no migration step exists. +// Persisted settings. The schema, key names and physical-px positions stay +// byte-compatible with the Win32 builds. +use std::ffi::OsString; use std::path::{Path, PathBuf}; use serde::{Deserialize, Serialize}; @@ -8,6 +9,7 @@ use serde::{Deserialize, Serialize}; use crate::core::geom::{ position_on_any_monitor, Rect, DEFAULT_BUBBLE_SIZE, MAX_BUBBLE_SIZE, MIN_BUBBLE_SIZE, }; +use crate::update::channel::Channel; use crate::usage::ProviderId; pub const APP_DIR_NAME: &str = "UsageMonitor"; @@ -187,8 +189,33 @@ impl Settings { } } +/// The folder holding `settings.json`. The Windows exe and the AppImage are +/// portable, so their settings sit next to the file and move with it. A +/// `.deb` install lives in a read-only system folder and keeps them in the +/// user's config folder. pub fn settings_dir() -> Option { - dirs::config_dir().map(|d| d.join(APP_DIR_NAME)) + portable_dir( + crate::update::channel::current(), + std::env::var_os("APPIMAGE"), + std::env::current_exe().ok(), + ) + .or_else(|| dirs::config_dir().map(|d| d.join(APP_DIR_NAME))) +} + +/// The folder of the portable file for `channel`: the exe on Windows, the +/// AppImage file (not its temporary mount) on Linux. `None` for installs +/// that are not portable. +fn portable_dir( + channel: Channel, + appimage: Option, + exe: Option, +) -> Option { + let file = match channel { + Channel::Portable => exe?, + Channel::AppImage => PathBuf::from(appimage?), + Channel::PackageManager | Channel::Unsupported => return None, + }; + file.parent().map(Path::to_path_buf) } pub fn settings_path() -> PathBuf { @@ -275,6 +302,37 @@ pub fn save_to(path: &Path, settings: &Settings) -> std::io::Result<()> { mod tests { use super::*; + #[test] + fn portable_installs_keep_settings_next_to_the_file() { + let folder = Path::new("Tools").join("Usage"); + assert_eq!( + portable_dir( + Channel::Portable, + None, + Some(folder.join("usage-monitor.exe")) + ), + Some(folder) + ); + // The AppImage file's folder, not the mount the exe runs from. + assert_eq!( + portable_dir( + Channel::AppImage, + Some(OsString::from("/home/u/Apps/usage-monitor.AppImage")), + Some(PathBuf::from("/tmp/.mount_usage/usr/bin/usage-monitor")), + ), + Some(PathBuf::from("/home/u/Apps")) + ); + assert_eq!( + portable_dir( + Channel::PackageManager, + None, + Some(PathBuf::from("/usr/bin/usage-monitor")) + ), + None + ); + assert_eq!(portable_dir(Channel::Unsupported, None, None), None); + } + #[test] fn old_two_provider_settings_json_still_loads() { let json = r#"{