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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1211,8 +1211,8 @@ See [Session manager examples](examples/session-manager.md) for complete CLI, ov

- When a fresh launch restores ink onto the transparent overlay board, a toast such as "Restored 7 annotations from last session" offers **Clear** (undoable, like Clear Canvas), because that ink now sits over whatever is on screen. Daemon toggles, empty restores, and solid boards stay quiet.
- Config values seed startup defaults. When `restore_tool_state` is enabled (default), the last-used tool settings saved in the session (including arrow head placement and the starting Spotlight magnification) override those config defaults on startup. Run `wayscriber --clear-tool-state` to remove only that saved tool layer so config defaults apply next startup while saved boards/history remain. In a running overlay, use Command Palette → Reset Tool Defaults to clear the saved layer and immediately apply config defaults to the active tools.
- `--session-file` uses exactly the selected file, implies persistence for that overlay run, rejects directories/symlinks/special files, and does not create missing parent directories. A running daemon can launch a hidden overlay with a named target; if the overlay is already visible, hide it before switching to a different named session.
- The overlay Session controls live in the top toolbar's Session popover (overflow menu → Session...). They can open an existing named session, save the current overlay as another named session, show session info, clear the active session, reopen recent named sessions, and jump to the configurator. The Open/Save As dialogs use `zenity` or `kdialog`; Save As appends `.wayscriber-session` when no extension is supplied and asks before replacing existing session artifacts.
- `--session-file` uses exactly the selected file, implies persistence for that overlay run, rejects directories/symlinks/special files, and does not create missing parent directories. A running daemon can launch a hidden overlay with a named target; if the overlay is already visible, hide it before switching to a different named session. The daemon's overlay keeps the session it last opened or saved as across hide and show, while the daemon runs.
- The overlay Session controls live in the top toolbar's Session popover (overflow menu → Session...). They can open an existing named session, save the current overlay as another named session, return to the home session (**Back to** the startup session file, or **Default session**), show session info, clear the active session, reopen recent named sessions, and jump to the configurator. The Open/Save As dialogs use `zenity` or `kdialog`; Save As appends `.wayscriber-session` when no extension is supplied and asks before replacing existing session artifacts.
- The configurator Session tab manages recent named sessions recorded when named-session targets are opened or saved from the CLI, daemon, or overlay. It can rename catalog labels, reveal files, and forget metadata without touching files. Clear Tool State removes only the saved tool layer; Clear Saved Data removes session files. Duplicate, Move, Clear Tool State, and Clear are disabled while an overlay, manually started daemon, or background service is active.

</details>
Expand Down
7 changes: 5 additions & 2 deletions docs/CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2111,10 +2111,12 @@ Use the CLI helpers for quick maintenance:
- `wayscriber --clear-tool-state` removes only the saved tool defaults from the session snapshot, preserving saved boards and history.
- `wayscriber --active --session-file ~/Documents/lecture-04.wayscriber-session` opens and saves a named session file directly.
- `wayscriber --freeze --session-file ~/Documents/lecture-04.wayscriber-session` starts frozen mode with that same named session target.
- `wayscriber --daemon --session-file ~/Documents/lecture-04.wayscriber-session` starts a daemon whose overlay activations use that named session target.
- `wayscriber --daemon-toggle --session-file ~/Documents/meeting.wayscriber-session` asks the running daemon to launch a hidden overlay with that named session target. If the overlay is already visible with a different target, hide it before switching.
- `wayscriber --daemon --session-file ~/Documents/lecture-04.wayscriber-session` starts a daemon whose overlay activations use that named session target, the daemon's home session.
- `wayscriber --daemon-toggle --session-file ~/Documents/meeting.wayscriber-session` asks the running daemon to launch a hidden overlay with that named session target. It applies to that activation only. If the overlay is already visible with a different target, hide it before switching.
- `wayscriber --session-info --session-file <path>`, `wayscriber --clear-session --session-file <path>`, and `wayscriber --clear-tool-state --session-file <path>` target only that named file.

The daemon's overlay keeps its session across hide and show. After **Open** or **Save As** switches the overlay to another session, the next activation continues in that session, until the overlay returns to the daemon's home session: the file given with `--daemon --session-file`, or the configured default session without one. The Session popover's `Back to <session>` or `Default session` button returns home. A `--daemon-toggle --session-file` request takes precedence for its activation. If the remembered file was moved, deleted, or replaced by something other than a regular file, the overlay opens the home session instead and says so, even when a backup or recovery copy is left beside the old path; unlike a `--session-file` given at startup, a remembered session is never restored from those copies. The daemon keeps this only while it runs; a restarted daemon starts at home. A daemon started with `--no-resume-session` still continues a named session the overlay switched to, since a named session file always persists; its default home stays unsaved.

Config values seed startup defaults. When `restore_tool_state = true`, the saved session tool state is applied after those defaults, so edits such as `[arrow] head_at_end = true` can appear ignored if the session snapshot still stores an older arrow setting. Run `wayscriber --clear-tool-state` (or add `--session-file <path>` for a named session) to make config defaults apply on the next startup without deleting saved boards. In a running overlay, Command Palette -> Reset Tool Defaults clears the saved layer for the active session and immediately applies config defaults to the current tools so the next autosave keeps those defaults.

The configurator Session tab exposes the same distinction for recent named sessions: Clear Tool State preserves saved boards/history while removing only persisted tool settings; Clear Saved Data removes saved session files. Offline catalog actions are disabled while an overlay, manually started daemon, or background service is active. Use the command palette for the active overlay session.
Expand All @@ -2125,6 +2127,7 @@ The overlay Session panel lives in the top strip's overflow **"Session..."** pop
- `Save As` writes the current overlay to another named session and switches the active target. It appends `.wayscriber-session` when no extension is supplied and asks before replacing existing session artifacts.
- `Info` reports the active session file size, board shape counts, and history status.
- `Clear` writes a durable empty session boundary for the active target.
- `Back to <session>` returns to the home session: the file the overlay or daemon started with. Without one it reads `Default session` and returns to the configured default session. Like `Open`, it saves dirty current data first and switches only once home has loaded. It is disabled while home is already active, and absent while no persisted session is active, since the overlay cannot leave home then. Hide it with the `side.session.home` toolbar item.
- Recent session rows reopen other named sessions. If a recent target is missing, Wayscriber removes that stale catalog entry after the failed open.
- `Manager` opens the configurator. Overlay Open/Save As dialogs use `zenity` or `kdialog`.

Expand Down
15 changes: 9 additions & 6 deletions docs/codebase-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -539,18 +539,21 @@ capture suppression operates on the paired resources without runtime pairing che
**Modules:**
- `src/session/`: target options, primary-file validation, snapshot load/save, sidecars, clear/recovery markers, saved tool-state reset, locks, catalog metadata, and inactive file operations.
- `src/backend/wayland/session/`: runtime Open, Save As, Clear, and saved tool-state reset transactions for the active overlay.
- `src/backend/wayland/state/toolbar/events/session.rs`: overlay Session popover routing for Open, Save As, Info, Clear, recent sessions, and configurator launch.
- `src/daemon/`: accepts daemon-toggle requests that carry an optional named session target.
- `src/backend/wayland/state/toolbar/events/session.rs`: overlay Session popover routing for Open, Save As, return home, Info, Clear, recent sessions, and configurator launch.
- `src/backend/wayland/session/home.rs`: the overlay's home session, the remembered session a daemon launch carries, and loading a remembered session or home in its place. `src/backend/wayland/state/core/session_home.rs` reports the overlay's session to the daemon.
- `src/daemon/protocol_v2/session_target.rs`: writes and reads the per-generation session reports in `daemon-commands/overlay-targets/`.
- `src/daemon/`: accepts daemon-toggle requests that carry an optional named session target, and remembers the session its overlay last reported across hide and show.

**Flow:**
1. CLI `--session-file` creates a named target instead of using configured storage. Named targets force persistence for that run, reject `--no-resume-session`, require an existing parent directory for foreground/open flows, and reject directories, symlinks, and special files.
2. Backend startup builds `SessionOptions` from config plus any named target, then session loading restores boards/history/tool state before rendering begins.
3. Runtime Open first saves dirty current data when needed, loads the candidate named session without mutating it, replaces board state only after a valid load, and records the open in the named-session catalog.
4. Runtime Save As validates the target, prompts before replacing existing artifacts, writes the snapshot, switches the active target, and records the save in the catalog.
5. Runtime Clear writes a durable empty-session boundary so older backup or recovery artifacts do not restore stale drawings.
6. Runtime saved tool-state reset clears the persisted tool layer for the active session and applies config-derived tool defaults in memory so autosave does not restore stale values.
7. Offline CLI maintenance can inspect sessions, clear all saved data, or clear only persisted tool state so config defaults seed the next startup without deleting boards.
8. The configurator reads the same catalog for inactive-session management: rename/reveal/forget metadata, duplicate primary files, move non-lock sidecars, clear saved tool state, and clear saved data when daemon/overlay locks are absent.
5. Returning home saves dirty current data the same way, then loads the home session as a launch would. A daemon overlay reports each committed target change, so the daemon starts the next overlay in that session; a remembered session that no longer exists falls back to home.
6. Runtime Clear writes a durable empty-session boundary so older backup or recovery artifacts do not restore stale drawings.
7. Runtime saved tool-state reset clears the persisted tool layer for the active session and applies config-derived tool defaults in memory so autosave does not restore stale values.
8. Offline CLI maintenance can inspect sessions, clear all saved data, or clear only persisted tool state so config defaults seed the next startup without deleting boards.
9. The configurator reads the same catalog for inactive-session management: rename/reveal/forget metadata, duplicate primary files, move non-lock sidecars, clear saved tool state, and clear saved data when daemon/overlay locks are absent.

---

Expand Down
23 changes: 23 additions & 0 deletions docs/daemon-protocol-v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,36 @@ About clipboard integration, and named test fixtures. The same check audits the
stub: before `execve` it may reach only the fixed `fcntl`, `dup3`, `setpgid`, `close_range`,
`execve`, and `exit_group` syscall set over prebuilt buffers.

## Overlay session reports

A daemon launch passes three optional environment variables, so an older overlay ignores them:
`WAYSCRIBER_OVERLAY_SESSION_REPORTS=1` says the daemon reads session reports,
`WAYSCRIBER_OVERLAY_HOME_SESSION` names its startup session file, and
`WAYSCRIBER_OVERLAY_PREFERRED_SESSION` names the session it remembers, omitted when the request
carried its own `--session-file`. The command line is unchanged. The broker strips these variables
from helpers that do not relaunch wayscriber.

An overlay with a published child identity reports its session in
`daemon-commands/overlay-targets/<generation>.target`, a private sibling of `v2/`, never inside
it. The canonical JSON record carries a schema version, the generation, PID and process-start
identity, and a target: an absolute session file, or null for home. It is replaced on each change.
When the child is retired, on exit, stop or forced reap, the daemon reads the report before it
releases the child's identity and removes it, along with any temporary its writer left. While the
child runs, the visible-target guard reads the current report without removing it. Either read
accepts a report only from a private regular file in a real private directory, with exactly the
identity captured at readiness; anything else is ignored. If the directory itself is not private,
nothing in it is read or removed. The remembered session lives only in daemon memory; startup
removes reports an earlier daemon left without restoring them.

## Compatibility and rollback

- A v2 client against a v1 daemon uses the strict legacy parser and v1 request path.
- A frozen v1 client against a v2 daemon rejects typed requests because no v1 token exists. Its
explicitly empty visibility signal remains supported.
- V1 cleanup removes only exact v1 request/response artifacts and never recursively removes the v2
root.
- Session reports sit outside the strict v2 tree, and the legacy request scan reads only plain
files in `daemon-commands/`, so reports left behind never reach an older daemon's parsers.
- Restart recovery rejects prior-generation open commands with a durable no-effect response and
records authorized commands without terminal proof as indeterminate. Foreign-generation journal
entries are abandoned rather than replayed.
Expand Down
3 changes: 3 additions & 0 deletions examples/session-manager.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ Open Wayscriber with any persisted session target, then use the top strip overfl
- `Info` reports the active session file size, board shape counts, and history
status.
- `Clear` writes a durable empty session boundary for the active target.
- `Back to <session>`, or `Default session` without a startup session file,
saves the current session and returns to the session the overlay or daemon
started with. It is disabled while that session is already active.
- Recent session rows reopen other named sessions.
- `Manager` opens the configurator.

Expand Down
30 changes: 15 additions & 15 deletions src/backend/wayland/backend/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -284,10 +284,17 @@ pub(super) fn dispatch_with_timeout(
dispatch_runtime_cycle(&mut ops, timeout)
}

/// The resume override this run follows: its own, such as the one a named
/// session file forces on, else the policy the launch environment passed.
pub(super) fn resume_override_from_env() -> Option<bool> {
if let Some(runtime) = runtime_session_override() {
return Some(runtime);
}
runtime_session_override().or_else(launched_resume_policy)
}

/// The resume policy the launch environment passed in
/// `WAYSCRIBER_RESUME_SESSION`. Unlike [`resume_override_from_env`], it leaves
/// out the override this run applies for itself, such as the one a named
/// session file forces on.
pub(super) fn launched_resume_policy() -> Option<bool> {
match env::var(RESUME_SESSION_ENV) {
Ok(raw) => {
let normalized = raw.trim().to_ascii_lowercase();
Expand All @@ -309,21 +316,14 @@ pub(super) fn resume_override_from_env() -> Option<bool> {

#[cfg(test)]
mod tests {
use super::*;
use crate::capture::CaptureError;
use crate::set_runtime_session_override;
use std::collections::VecDeque;
use std::io::Write;
use std::os::unix::net::UnixStream;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Mutex, OnceLock};

use super::*;
use crate::capture::CaptureError;
use crate::set_runtime_session_override;

fn env_mutex() -> &'static Mutex<()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
}

#[test]
fn timeout_to_poll_ms_supports_none_and_caps_large_values() {
Expand Down Expand Up @@ -807,7 +807,7 @@ mod tests {

#[test]
fn resume_override_from_env_prefers_runtime_override() {
let _guard = env_mutex().lock().unwrap();
let _guard = crate::test_env::lock();

// SAFETY: test serialized by env mutex.
unsafe {
Expand All @@ -826,7 +826,7 @@ mod tests {

#[test]
fn resume_override_from_env_parses_expected_values() {
let _guard = env_mutex().lock().unwrap();
let _guard = crate::test_env::lock();
set_runtime_session_override(None);

// SAFETY: test serialized by env mutex.
Expand Down
25 changes: 23 additions & 2 deletions src/backend/wayland/backend/state_init/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use super::WaylandBackend;
use super::runtime_wake::RuntimeWakeSource;
use super::setup::WaylandSetup;
use crate::backend::wayland::portal_capture::portal_freeze_fallback;
use crate::backend::wayland::session::{SessionHome, SessionLaunch, session_target};
use crate::env_vars::{DESKTOP_SESSION_ENV, XDG_CURRENT_DESKTOP_ENV, XDG_SESSION_DESKTOP_ENV};
use crate::{
capture::CaptureManager,
Expand Down Expand Up @@ -46,8 +47,24 @@ pub(super) fn init_state(backend: &WaylandBackend, setup: WaylandSetup) -> Resul
let session_config_failed = load_failure
.as_ref()
.is_some_and(|failure| failure.section_failed("session"));
let session_options =
session::build_session_options(&config, &config_dir, backend.named_session_file.clone());
let launch = SessionLaunch::from_environment(backend.named_session_file.as_deref());
let home_options = session::home_session_options(&config, &config_dir, &launch);
if let Some(preferred) = &launch.preferred {
info!("Continuing remembered session {}", preferred.display());
}
let session_options = session::build_session_options(
&config,
&config_dir,
launch
.preferred
.clone()
.or_else(|| backend.named_session_file.clone()),
);
let session_home = SessionHome::new(
launch,
home_options,
session_target(session_options.as_ref()),
);
let runtime_wake = RuntimeWakeSource::new()
.map_err(|err| anyhow::anyhow!("failed to create runtime wake descriptor: {err}"))?;
let persistence =
Expand Down Expand Up @@ -194,6 +211,7 @@ pub(super) fn init_state(backend: &WaylandBackend, setup: WaylandSetup) -> Resul
palette_recents,
capture_manager,
session_options,
session_home,
session_config_failed,
persistence,
runtime_ui,
Expand All @@ -214,6 +232,9 @@ pub(super) fn init_state(backend: &WaylandBackend, setup: WaylandSetup) -> Resul
tablet_manager,
});

// A continued remembered session is reported before the daemon sees this
// overlay ready, so it never mistakes the overlay for being at home.
state.report_session_to_daemon();
// Decide the toolbar frontend before the first visibility sync so the
// built-in surfaces are never created just to be torn down when the
// GTK bars take over.
Expand Down
Loading
Loading