Skip to content

Commit f58618f

Browse files
AtmanActiveclaude
andcommitted
Add window state persistence and start_minimized option
- Window position, size, and maximized state saved to <exe>.window.json and restored on next launch - New config option start_minimized: "on" or "off" - Updated README with new features documentation - Added *.window.json to .gitignore Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 1086de8 commit f58618f

File tree

5 files changed

+150
-5
lines changed

5 files changed

+150
-5
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,8 @@ Thumbs.db
1414
# Claude Code
1515
.claude/
1616

17+
# Window state (auto-generated, user-specific)
18+
*.window.json
19+
1720
# Windows reserved filename artifact
1821
nul

README.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ A lightweight [Tauri v2](https://v2.tauri.app/) desktop app that wraps any websi
99
- **Custom title** — Optionally set a fixed window title via config
1010
- **Custom icon** — Set your own window icon (ICO or PNG)
1111
- **Dark mode control** — Request dark/light theme from sites, or force-dark all sites (Windows)
12+
- **Remember window position** — Window size, position, and maximized state are saved and restored across sessions
13+
- **Start minimized** — Optionally launch the app minimized to the taskbar
1214
- **Rename-to-configure** — Rename the executable and it auto-detects its config file (`MyApp.exe``MyApp.json`)
1315
- **Cross-platform** — Builds for Windows x64, Linux x64, and macOS ARM64
1416

@@ -44,6 +46,7 @@ The config file is a simple JSON file placed next to the executable. The filenam
4446
| `icon` | No | `""` | Path to a custom window icon (`.ico` or `.png`). Absolute path, or relative to the executable |
4547
| `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 |
4648
| `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 |
49+
| `start_minimized` | No | `"off"` | Start minimized to taskbar: `"on"` or `"off"` |
4750

4851
### Example — minimal
4952

@@ -61,7 +64,8 @@ The config file is a simple JSON file placed next to the executable. The filenam
6164
"title": "YouTube Music",
6265
"icon": "music.png",
6366
"prefer_dark_mode": "dark",
64-
"force_dark_mode": "off"
67+
"force_dark_mode": "off",
68+
"start_minimized": "off"
6569
}
6670
```
6771

@@ -73,6 +77,15 @@ The config file is a simple JSON file placed next to the executable. The filenam
7377

7478
The two options can be combined: `prefer_dark_mode` handles CSS-aware sites gracefully, while `force_dark_mode` catches everything else.
7579

80+
### Window state persistence
81+
82+
The app automatically remembers your window position, size, and maximized state between sessions. This works out of the box — no configuration needed.
83+
84+
- The state is saved to `<exe_name>.window.json` beside the executable (e.g. `app.window.json`)
85+
- Updated every time you move, resize, or maximize/restore the window
86+
- On next launch, the window opens exactly where you left it
87+
- To reset to defaults, simply delete the `.window.json` file
88+
7689
## Platform Notes
7790

7891
| Platform | Runtime Requirement |
@@ -115,7 +128,7 @@ The binary will be at `src-tauri/target/release/app` (or `app.exe` on Windows).
115128
├── tauri.conf.json # Tauri build config
116129
└── src/
117130
├── main.rs # Entry point
118-
├── lib.rs # App setup, navigation, title sync, dark mode
131+
├── lib.rs # App setup, navigation, title sync, dark mode, window state
119132
└── config.rs # Config struct + loader
120133
```
121134

app.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,8 @@
1212
"prefer_dark_mode": "default",
1313

1414
"_comment_force_dark_mode": "Optional. Values: 'on' or 'off'. When 'on', forces all sites into dark mode even if they don't natively support it (like Chrome's force-dark flag). Windows only.",
15-
"force_dark_mode": "off"
15+
"force_dark_mode": "off",
16+
17+
"_comment_start_minimized": "Optional. Values: 'on' or 'off'. When 'on', the app starts minimized to the taskbar.",
18+
"start_minimized": "off"
1619
}

src-tauri/src/config.rs

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use serde::Deserialize;
1+
use serde::{Deserialize, Serialize};
22
use std::path::PathBuf;
33

44
#[derive(Deserialize)]
@@ -12,6 +12,18 @@ pub struct AppConfig {
1212
pub prefer_dark_mode: String,
1313
#[serde(default)]
1414
pub force_dark_mode: String,
15+
#[serde(default)]
16+
pub start_minimized: String,
17+
}
18+
19+
/// Persisted window geometry — saved beside the config as `<name>.window.json`
20+
#[derive(Serialize, Deserialize, Default)]
21+
pub struct WindowState {
22+
pub x: i32,
23+
pub y: i32,
24+
pub width: u32,
25+
pub height: u32,
26+
pub maximized: bool,
1527
}
1628

1729
impl AppConfig {
@@ -62,6 +74,29 @@ impl AppConfig {
6274
Err(format!("{} not found", config_name).into())
6375
}
6476

77+
/// Path for the window state file: `<exe_name>.window.json` beside the config
78+
pub fn window_state_path() -> Option<PathBuf> {
79+
let exe_name = std::env::current_exe()
80+
.ok()
81+
.and_then(|p| p.file_stem().map(|s| s.to_string_lossy().into_owned()))?;
82+
let filename = format!("{}.window.json", exe_name);
83+
84+
// In debug mode, check project root first
85+
#[cfg(debug_assertions)]
86+
{
87+
if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") {
88+
if let Some(project_root) = PathBuf::from(manifest_dir).parent() {
89+
return Some(project_root.join(&filename));
90+
}
91+
}
92+
}
93+
94+
// Beside the executable
95+
std::env::current_exe()
96+
.ok()
97+
.and_then(|p| p.parent().map(|d| d.join(&filename)))
98+
}
99+
65100
pub fn resolve_icon_path(&self) -> Option<PathBuf> {
66101
if self.icon.is_empty() {
67102
return None;
@@ -98,3 +133,19 @@ impl AppConfig {
98133
None
99134
}
100135
}
136+
137+
impl WindowState {
138+
pub fn load() -> Option<Self> {
139+
let path = AppConfig::window_state_path()?;
140+
let contents = std::fs::read_to_string(&path).ok()?;
141+
serde_json::from_str(&contents).ok()
142+
}
143+
144+
pub fn save(&self) {
145+
if let Some(path) = AppConfig::window_state_path() {
146+
if let Ok(json) = serde_json::to_string_pretty(self) {
147+
let _ = std::fs::write(path, json);
148+
}
149+
}
150+
}
151+
}

src-tauri/src/lib.rs

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
mod config;
22

3-
use config::AppConfig;
3+
use config::{AppConfig, WindowState};
44
use tauri::Manager;
55

66
#[cfg_attr(mobile, tauri::mobile_entry_point)]
@@ -25,6 +25,9 @@ pub fn run() {
2525
.get_webview_window("main")
2626
.expect("Failed to get main window");
2727

28+
// Restore saved window position/size (if available)
29+
restore_window_state(&window);
30+
2831
// Set initial title from config (if provided)
2932
if !config.title.is_empty() {
3033
window
@@ -47,16 +50,88 @@ pub fn run() {
4750
let color_scheme = config.prefer_dark_mode.clone();
4851
setup_webview_handlers(&window, title_window, has_static_title, &color_scheme);
4952

53+
// Register window event handler to persist position/size
54+
let save_window = window.clone();
55+
window.on_window_event(move |event| {
56+
use tauri::WindowEvent;
57+
match event {
58+
WindowEvent::Moved(_) | WindowEvent::Resized(_) => {
59+
save_window_state(&save_window);
60+
}
61+
_ => {}
62+
}
63+
});
64+
5065
// Navigate to the configured URL
5166
let url: tauri::Url = config.url.parse().expect("Invalid URL in config.json");
5267
let _ = window.navigate(url);
5368

69+
// Start minimized (if configured)
70+
if config.start_minimized.eq_ignore_ascii_case("on") {
71+
let _ = window.minimize();
72+
}
73+
5474
Ok(())
5575
})
5676
.run(tauri::generate_context!())
5777
.expect("error while running tauri application");
5878
}
5979

80+
/// Restore window position, size, and maximized state from the saved state file
81+
fn restore_window_state(window: &tauri::WebviewWindow) {
82+
if let Some(state) = WindowState::load() {
83+
// Validate that the saved size is reasonable (at least 200x200)
84+
if state.width >= 200 && state.height >= 200 {
85+
let _ = window.set_size(tauri::PhysicalSize::new(state.width, state.height));
86+
}
87+
// Restore position
88+
let _ = window.set_position(tauri::PhysicalPosition::new(state.x, state.y));
89+
// Restore maximized state
90+
if state.maximized {
91+
let _ = window.maximize();
92+
}
93+
}
94+
}
95+
96+
/// Save current window position, size, and maximized state to disk
97+
fn save_window_state(window: &tauri::WebviewWindow) {
98+
let maximized = window.is_maximized().unwrap_or(false);
99+
100+
// When maximized, don't overwrite the saved normal position/size —
101+
// we want to restore the non-maximized geometry next time.
102+
// Only save the maximized flag.
103+
if maximized {
104+
if let Some(mut state) = WindowState::load() {
105+
state.maximized = true;
106+
state.save();
107+
} else {
108+
// No previous state — save current dimensions with maximized flag
109+
let pos = window.outer_position().unwrap_or_default();
110+
let size = window.outer_size().unwrap_or_default();
111+
let state = WindowState {
112+
x: pos.x,
113+
y: pos.y,
114+
width: size.width,
115+
height: size.height,
116+
maximized: true,
117+
};
118+
state.save();
119+
}
120+
return;
121+
}
122+
123+
let pos = window.outer_position().unwrap_or_default();
124+
let size = window.outer_size().unwrap_or_default();
125+
let state = WindowState {
126+
x: pos.x,
127+
y: pos.y,
128+
width: size.width,
129+
height: size.height,
130+
maximized: false,
131+
};
132+
state.save();
133+
}
134+
60135
#[cfg(target_os = "windows")]
61136
fn setup_webview_handlers(
62137
webview_window: &tauri::WebviewWindow,

0 commit comments

Comments
 (0)