feat: keep settings.json next to the portable exe and AppImage

The Windows exe and the AppImage now read and write settings.json in
their own folder, so a copied or moved folder keeps its settings. A .deb
install still uses the user's config folder, because it lives in a
read-only system folder.
This commit is contained in:
tiennm99 committed 2026-10-22 17:05:04 +07:00
1 parent 6afd603d5e
commit 7e446b6bda
4 files changed
+77 -12

No files matched your search

+10 -5
View File
@@ -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:
+3 -2
View File
@@ -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
@@ -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.
+61 -3
View File
@@ -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<PathBuf> {
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<OsString>,
exe: Option<PathBuf>,
) -> Option<PathBuf> {
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#"{