Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 0 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
"@tauri-apps/plugin-fs": "^2.4.5",
"@tauri-apps/plugin-opener": "^2",
"@tauri-apps/plugin-store": "^2.4.2",
"@tauri-apps/plugin-window-state": "^2.4.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
Expand Down
16 changes: 0 additions & 16 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,3 @@ chrono = { version = "0.4", features = ["serde"] }
reqwest = { version = "0.12", features = ["rustls-tls"], default-features = false }
scraper = "0.23"
tauri-plugin-fs = "2"
tauri-plugin-window-state = "2"
1 change: 0 additions & 1 deletion src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
"core:window:allow-set-maximizable",
"store:default",
"dialog:default",
"window-state:default",
"fs:default",
{
"identifier": "fs:allow-write-file",
Expand Down
28 changes: 15 additions & 13 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ mod db;
mod file_io;
mod groups;
mod link_preview;
mod window;

use tauri::Manager;
use tauri_plugin_window_state::StateFlags;

/// Initializes and runs the Tauri application.
///
Expand All @@ -14,14 +14,16 @@ use tauri_plugin_window_state::StateFlags;
/// - **store** — Persistent key-value config storage.
/// - **dialog** — Native file/message dialogs.
/// - **fs** — Scoped filesystem access.
/// - **window-state** — Saves and optionally restores window position and
/// size across launches (initial restore is skipped for the `"main"`
/// window so the frontend can decide via a user setting).
///
/// During setup the SQLite database is initialized and a
/// During setup the SQLite database is initialized, a
/// [`LinkPreviewCache`](link_preview::LinkPreviewCache) is registered as
/// managed state. All note CRUD and utility commands are then registered
/// via the invoke handler.
/// managed state, and the main window is created via
/// [`window::create_main_window`]. Window position and size are restored
/// from `config.json` when the user has enabled the setting; otherwise
/// the window opens at the default 1200×800 dimensions.
///
/// On window close the current geometry is persisted to `config.json`
/// so it can be restored on the next launch.
///
/// # Panics
///
Expand All @@ -37,19 +39,19 @@ pub fn run() {
.plugin(tauri_plugin_store::Builder::default().build())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
.plugin(
tauri_plugin_window_state::Builder::new()
.with_state_flags(StateFlags::POSITION | StateFlags::SIZE)
.skip_initial_state("main")
.build(),
)
.setup(|app| {
db::init_db(app.handle())?;
app.manage(link_preview::LinkPreviewCache(std::sync::Mutex::new(
std::collections::HashMap::new(),
)));
window::create_main_window(app.handle())?;
Ok(())
})
.on_window_event(|window, event| {
if let tauri::WindowEvent::CloseRequested { .. } = event {
window::save_window_geometry(window.app_handle());
}
})
.invoke_handler(tauri::generate_handler![
db::get_note,
db::list_notes,
Expand Down
168 changes: 168 additions & 0 deletions src-tauri/src/window.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
//! Window creation and geometry persistence.
//!
//! This module is responsible for creating the main application window
//! and saving/restoring its position and size across launches. The
//! geometry is stored in `config.json` via the Tauri store plugin and
//! only restored when the user has enabled the
//! `windowStateRestoreEnabled` setting.

use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindowBuilder};
use tauri_plugin_store::StoreExt;

/// Default window width in logical pixels when no saved geometry is restored.
const DEFAULT_WIDTH: f64 = 1200.0;

/// Default window height in logical pixels when no saved geometry is restored.
const DEFAULT_HEIGHT: f64 = 800.0;

/// Title displayed in the window title bar at creation time.
const WINDOW_TITLE: &str = "Scripta";

/// Store key under which the serialized [`WindowGeometry`] is persisted in `config.json`.
const GEOMETRY_STORE_KEY: &str = "windowGeometry";

/// Store key for the boolean flag that controls whether saved geometry
/// is applied on startup. Mirrors the frontend constant in
/// `windowStateConfig.ts`.
const RESTORE_ENABLED_KEY: &str = "windowStateRestoreEnabled";

/// Saved window geometry in logical (DPI-independent) coordinates.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct WindowGeometry {
x: f64,
y: f64,
width: f64,
height: f64,
}

/// Creates the main application window.
///
/// Reads `config.json` to determine whether to restore the previous
/// window position and size. When restoration is enabled and valid
/// saved geometry exists within the visible monitor area, the window
/// opens at the saved coordinates. Otherwise it opens at the default
/// 1200×800 size, centred by the OS.
///
/// The window is created with `resizable(false)` and
/// `maximizable(false)` so the splash screen cannot be resized.
/// The frontend unlocks these constraints once the splash has faded
/// out.
pub fn create_main_window(app: &AppHandle) -> Result<(), Box<dyn std::error::Error>> {
let geometry = read_saved_geometry(app);

let mut builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default())
.title(WINDOW_TITLE)
.resizable(false)
.maximizable(false);

match geometry {
Some(geo) => {
builder = builder
.inner_size(geo.width, geo.height)
.position(geo.x, geo.y);
}
None => {
builder = builder.inner_size(DEFAULT_WIDTH, DEFAULT_HEIGHT);
// Position is omitted so the OS centres the window.
}
}

builder.build()?;
Ok(())
}

/// Saves the current window geometry (logical coordinates) to `config.json`.
///
/// Called from the `CloseRequested` window event handler so the
/// position and size are persisted for the next launch.
pub fn save_window_geometry(app: &AppHandle) {
let Some(window) = app.get_webview_window("main") else {
return;
};

let scale = match window.scale_factor() {
Ok(s) => s,
Err(_) => return,
};
let Ok(pos) = window.outer_position() else {
return;
};
let Ok(size) = window.inner_size() else {
return;
};

let geo = WindowGeometry {
x: pos.x as f64 / scale,
y: pos.y as f64 / scale,
width: size.width as f64 / scale,
height: size.height as f64 / scale,
};

if let Ok(store) = app.store("config.json") {
store.set(
GEOMETRY_STORE_KEY,
serde_json::to_value(&geo).unwrap_or_default(),
);
}
}

/// Reads the saved geometry from `config.json` if restoration is
/// enabled and the saved position is within a visible monitor.
///
/// Returns `None` when:
/// - The `windowStateRestoreEnabled` setting is `false`.
/// - No saved geometry exists.
/// - The saved position is outside all connected monitors (e.g. an
/// external monitor was disconnected).
fn read_saved_geometry(app: &AppHandle) -> Option<WindowGeometry> {
let store = app.store("config.json").ok()?;

let restore_enabled = store
.get(RESTORE_ENABLED_KEY)
.and_then(|v| v.as_bool())
.unwrap_or(true);

if !restore_enabled {
return None;
}

let raw = store.get(GEOMETRY_STORE_KEY)?;
let geo: WindowGeometry = serde_json::from_value(raw).ok()?;

if geo.width <= 0.0 || geo.height <= 0.0 {
return None;
}

if is_position_on_screen(app, geo.x, geo.y) {
Some(geo)
} else {
None
}
}

/// Returns `true` when the given logical position falls within at
/// least one connected monitor.
fn is_position_on_screen(app: &AppHandle, x: f64, y: f64) -> bool {
let monitors = match app.available_monitors() {
Ok(m) => m,
Err(_) => return true, // assume on-screen when detection fails
};

if monitors.is_empty() {
return true;
}

monitors.iter().any(|m| {
let pos = m.position();
let size = m.size();
let scale = m.scale_factor();
let mx = pos.x as f64 / scale;
let my = pos.y as f64 / scale;
let mw = size.width as f64 / scale;
let mh = size.height as f64 / scale;

x >= mx && x < mx + mw && y >= my && y < my + mh
})
}
11 changes: 1 addition & 10 deletions src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,7 @@
"frontendDist": "../dist"
},
"app": {
"windows": [
{
"title": "Scripta",
"width": 1200,
"height": 800,
"resizable": false,
"maximizable": false,
"dragDropEnabled": false
}
],
"windows": [],
"security": {
"csp": "default-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; script-src 'self'; img-src 'self' data: asset: https://asset.localhost",
"assetProtocol": {
Expand Down
11 changes: 0 additions & 11 deletions src/app/providers/store-provider.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import { LazyStore } from '@tauri-apps/plugin-store'
import {
restoreStateCurrent,
StateFlags,
} from '@tauri-apps/plugin-window-state'
import { createContext, type ReactNode, use, useContext } from 'react'
import type { Theme } from '@/app/providers/theme-provider'
import {
Expand Down Expand Up @@ -90,13 +86,6 @@ export const storeInitPromise = Promise.all([
if (storedTitlePrefix != null) {
configDefaults.windowTitlePrefixEnabled = storedTitlePrefix
}
if (restoreEnabled) {
try {
await restoreStateCurrent(StateFlags.POSITION | StateFlags.SIZE)
} catch (err) {
console.error('Failed to restore window state:', err)
}
}
})

/**
Expand Down
2 changes: 1 addition & 1 deletion src/features/editor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export { useCursorCentering } from './hooks/useCursorCentering'
export { useEditorFontSize } from './hooks/useEditorFontSize'
export type { UseSearchReplaceReturn } from './hooks/useSearchReplace'
export { useSearchReplace } from './hooks/useSearchReplace'
export { checklistSplitFixExtension } from './lib/checklistSplitFix'
export { DEFAULT_BLOCKS, DEFAULT_CONTENT, extractTitle } from './lib/constants'
export { cursorCenteringExtension } from './lib/cursorCentering'
export { cursorVimKeysExtension } from './lib/cursorVimKeys'
Expand All @@ -45,7 +46,6 @@ export {
exportToMarkdown,
fixBlockNoteTableExport,
} from './lib/markdown-export'
export { checklistSplitFixExtension } from './lib/checklistSplitFix'
export { rangeCheckToggleExtension } from './lib/rangeCheckToggle'
export { searchExtension } from './lib/searchExtension'
export { slashMenuEmacsKeysExtension } from './lib/slashMenuEmacsKeys'
Expand Down
12 changes: 6 additions & 6 deletions src/features/settings/lib/windowStateConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ export const DEFAULT_WINDOW_STATE_RESTORE = true
/**
* Store key name for persistence via `configStore`.
*
* When `true`, the app restores the last-saved window position and size
* on startup. When `false`, the window opens at the default 1200×800
* dimensions (as specified in `tauri.conf.json`).
* When `true`, the Rust backend restores the last-saved window position
* and size before creating the main window frame. When `false`, the
* window opens at the default 1200×800 dimensions.
*
* @remarks
* The window-state plugin always saves position/size on close regardless
* of this setting. This key only controls whether restore is performed
* at startup.
* Window geometry is always saved to `config.json` on close regardless
* of this setting. This key only controls whether the saved geometry is
* applied when the window is created at startup.
*/
export const WINDOW_STATE_STORE_KEY = 'windowStateRestoreEnabled' as const
Loading