Skip to content

Commit b1886b7

Browse files
AtmanActiveclaude
andcommitted
Add single-instance control and multi-instance window cascading
- New `allow_only_one_instance` config option: "off" (multiple allowed), "on"/"first" (bring existing to foreground and exit), "last" (kill existing) - Multi-instance cascade: each new instance offsets +32px from siblings - "first" mode activates existing window (restores from minimized if needed) - Skip saving window state when minimized (prevents -32000 coordinates) - Update README with single-instance documentation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f58618f commit b1886b7

5 files changed

Lines changed: 293 additions & 11 deletions

File tree

README.md

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ A lightweight [Tauri v2](https://v2.tauri.app/) desktop app that wraps any websi
1111
- **Dark mode control** — Request dark/light theme from sites, or force-dark all sites (Windows)
1212
- **Remember window position** — Window size, position, and maximized state are saved and restored across sessions
1313
- **Start minimized** — Optionally launch the app minimized to the taskbar
14+
- **Single-instance control** — Prevent multiple instances, or let the latest instance take over (Windows)
1415
- **Rename-to-configure** — Rename the executable and it auto-detects its config file (`MyApp.exe``MyApp.json`)
1516
- **Cross-platform** — Builds for Windows x64, Linux x64, and macOS ARM64
1617

@@ -47,6 +48,7 @@ The config file is a simple JSON file placed next to the executable. The filenam
4748
| `prefer_dark_mode` | No | `"default"` | Color scheme preference: `"default"` (let OS decide), `"dark"` (request dark theme), `"light"` (request light theme). Only affects sites that support `prefers-color-scheme` CSS. Windows only |
4849
| `force_dark_mode` | No | `"off"` | Force-dark rendering: `"on"` or `"off"`. When `"on"`, forces all sites into dark mode even if they don't natively support it — same as Chrome's force-dark flag. Windows only |
4950
| `start_minimized` | No | `"off"` | Start minimized to taskbar: `"on"` or `"off"` |
51+
| `allow_only_one_instance` | No | `"off"` | Single-instance mode: `"off"` (allow multiple), `"on"` or `"first"` (exit if already running), `"last"` (kill existing and take over). Windows only |
5052

5153
### Example — minimal
5254

@@ -65,7 +67,8 @@ The config file is a simple JSON file placed next to the executable. The filenam
6567
"icon": "music.png",
6668
"prefer_dark_mode": "dark",
6769
"force_dark_mode": "off",
68-
"start_minimized": "off"
70+
"start_minimized": "off",
71+
"allow_only_one_instance": "off"
6972
}
7073
```
7174

@@ -85,6 +88,19 @@ The app automatically remembers your window position, size, and maximized state
8588
- Updated every time you move, resize, or maximize/restore the window
8689
- On next launch, the window opens exactly where you left it
8790
- To reset to defaults, simply delete the `.window.json` file
91+
- When multiple instances are allowed, each new instance opens with a +32px offset so windows don't stack exactly on top of each other
92+
93+
### Single-instance mode
94+
95+
**`allow_only_one_instance`** controls how the app handles multiple instances:
96+
97+
| Value | Behavior |
98+
|-------|----------|
99+
| `"off"` (default) | Multiple instances allowed. New windows cascade with a +32px offset |
100+
| `"on"` or `"first"` | If an instance is already running, it is brought to the foreground (restored from minimized if needed) and the new one exits |
101+
| `"last"` | If an instance is already running, it is terminated and the new one takes over |
102+
103+
This is useful for apps where only one window should exist at a time, like a dedicated music player or chat client.
88104

89105
## Platform Notes
90106

@@ -94,7 +110,7 @@ The app automatically remembers your window position, size, and maximized state
94110
| **Linux** | WebKit2GTK 4.1 (`libwebkit2gtk-4.1`) |
95111
| **macOS** | None (uses WKWebView) |
96112

97-
- **Dynamic title sync**, **prefer_dark_mode**, and **force_dark_mode** use the WebView2 API and are only available on Windows. On macOS and Linux, the window title stays at the default unless a static `title` is set in the config.
113+
- **Dynamic title sync**, **prefer_dark_mode**, **force_dark_mode**, and **allow_only_one_instance** use Windows APIs and are only available on Windows. On macOS and Linux, the window title stays at the default unless a static `title` is set in the config.
98114

99115
## Building from Source
100116

@@ -128,7 +144,7 @@ The binary will be at `src-tauri/target/release/app` (or `app.exe` on Windows).
128144
├── tauri.conf.json # Tauri build config
129145
└── src/
130146
├── main.rs # Entry point
131-
├── lib.rs # App setup, navigation, title sync, dark mode, window state
147+
├── lib.rs # App setup, navigation, title sync, dark mode, window state, single-instance
132148
└── config.rs # Config struct + loader
133149
```
134150

app.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,8 @@
1515
"force_dark_mode": "off",
1616

1717
"_comment_start_minimized": "Optional. Values: 'on' or 'off'. When 'on', the app starts minimized to the taskbar.",
18-
"start_minimized": "off"
18+
"start_minimized": "off",
19+
20+
"_comment_allow_only_one_instance": "Optional. Values: 'off' (allow multiple instances), 'on' or 'first' (exit if already running), 'last' (kill existing instance and take over). Windows only.",
21+
"allow_only_one_instance": "off"
1922
}

src-tauri/Cargo.toml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,11 @@ tauri = { version = "2", features = ["image-ico", "image-png"] }
2222

2323
[target.'cfg(windows)'.dependencies]
2424
webview2-com = "0.38"
25-
windows = { version = "0.61", features = ["Win32_System_WinRT", "Win32_System_Com"] }
25+
windows = { version = "0.61", features = [
26+
"Win32_System_WinRT",
27+
"Win32_System_Com",
28+
"Win32_System_Diagnostics_ToolHelp",
29+
"Win32_System_Threading",
30+
"Win32_Foundation",
31+
"Win32_UI_WindowsAndMessaging",
32+
] }

src-tauri/src/config.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ pub struct AppConfig {
1414
pub force_dark_mode: String,
1515
#[serde(default)]
1616
pub start_minimized: String,
17+
#[serde(default)]
18+
pub allow_only_one_instance: String,
1719
}
1820

1921
/// Persisted window geometry — saved beside the config as `<name>.window.json`
@@ -97,6 +99,16 @@ impl AppConfig {
9799
.and_then(|p| p.parent().map(|d| d.join(&filename)))
98100
}
99101

102+
/// Parse the single-instance mode from config.
103+
/// Returns: None (off/absent), Some("first"), or Some("last")
104+
pub fn instance_mode(&self) -> Option<&str> {
105+
match self.allow_only_one_instance.to_lowercase().as_str() {
106+
"on" | "first" => Some("first"),
107+
"last" => Some("last"),
108+
_ => None,
109+
}
110+
}
111+
100112
pub fn resolve_icon_path(&self) -> Option<PathBuf> {
101113
if self.icon.is_empty() {
102114
return None;

0 commit comments

Comments
 (0)