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
12 changes: 12 additions & 0 deletions docs/CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2128,6 +2128,18 @@ The overlay Session panel lives in the top strip's overflow **"Session..."** pop
- 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`.

Recent-session catalog updates after Open are best effort. If recording a
successfully opened session fails, the opened canvas stays active and an
overlay toast reports the catalog error. Save As and ordinary saves also keep
catalog bookkeeping best effort, but catalog errors on those paths are logged
rather than shown in a toast; a successful session-file write remains successful.

Open, Save As, and Clear run their disk phases on the persistence worker while
event dispatch continues. One command runs at a time, after any pending autosave
finishes. If you edit while a captured phase is pending, the edits stay live and
the target switch is refused; retry the session operation with the updated
canvas.

The configurator Session tab also shows recent named sessions from the catalog, recorded when named-session targets are opened or saved from the CLI, daemon, or overlay. It can rename catalog display labels, reveal file locations, and forget catalog metadata without touching files. Duplicate, Move, and Clear are disabled while an overlay, manually started daemon, or background service is active.

Session overrides and recovery:
Expand Down
123 changes: 51 additions & 72 deletions src/backend/wayland/backend/event_loop/session_save.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use super::super::super::state::WaylandState;
use crate::{
backend::wayland::session::{
self as runtime_session, PersistenceCompletion, PersistenceOperation, PersistenceOutcome,
SaveCompletion, SaveStrategy, SessionState, SubmitFailure,
SaveStrategy, SessionState, SubmitFailure,
},
session,
session::SaveSnapshotReport,
Expand All @@ -15,13 +15,17 @@ mod notifications;

pub(super) use notifications::notify_session_failure;
#[cfg(test)]
pub(in crate::backend::wayland) use notifications::record_autosave_failure;
#[cfg(not(test))]
use notifications::record_autosave_failure;
#[cfg(test)]
use notifications::record_autosave_success;
#[cfg(test)]
use notifications::{
SessionSaveNotification, pending_save_notifications, session_save_notification_text,
};
use notifications::{
notify_persistence_worker_failure, notify_session_save_report, record_autosave_failure,
notify_persistence_worker_failure, notify_session_save_report,
show_persistence_worker_failure_toast, show_session_failure_toast,
};

Expand All @@ -37,6 +41,13 @@ pub(super) fn persist_session(state: &mut WaylandState) -> Result<(), anyhow::Er
state.session.target_epoch()
);
}
runtime_session::driver::persist_after_pending_commands(
state,
persist_final_session_and_shutdown,
)
}

fn persist_final_session_and_shutdown(state: &mut WaylandState) -> Result<(), anyhow::Error> {
let save_result = persist_final_session(state);
let worker_failed = !state.persistence.is_healthy();
let shutdown_result = state.persistence.shutdown(state.session.target_epoch());
Expand Down Expand Up @@ -187,6 +198,11 @@ fn persist_final_session(state: &mut WaylandState) -> Result<(), anyhow::Error>
}

pub(super) fn autosave_timeout(state: &WaylandState, now: Instant) -> Option<Duration> {
if state.session_transaction.is_some() {
// A queued command may outlive a failed autosave completion. Admit it
// on the next tick even when no worker wake remains outstanding.
return (!state.persistence.is_active()).then_some(Duration::ZERO);
}
let autosave = scheduled_autosave_timeout(
&state.session,
state.session_options(),
Expand Down Expand Up @@ -214,8 +230,13 @@ fn scheduled_autosave_timeout(
}

pub(super) fn autosave_if_due(state: &mut WaylandState, now: Instant) -> Result<(), anyhow::Error> {
drain_persistence_completion(state)?;
let completion_result = drain_persistence_completion(state);
observe_input_dirty(state, now);
state.poll_pending_session_command();
completion_result?;
if state.session_transaction.is_some() {
return Ok(());
}

if !state.persistence.is_healthy() {
return Ok(());
Expand Down Expand Up @@ -337,35 +358,9 @@ fn snapshot_or_empty(
})
}

pub(in crate::backend::wayland) fn observe_input_dirty(state: &mut WaylandState, now: Instant) {
let input_dirty = state.input_state.take_session_dirty();
state.session.record_input_dirty(now, input_dirty);
}

pub(in crate::backend::wayland) fn persistence_barrier(
state: &mut WaylandState,
) -> Result<(), anyhow::Error> {
observe_input_dirty(state, Instant::now());
if state.persistence.is_active() {
let completion = match state.persistence.wait_for_completion() {
Ok(Some(completion)) => completion,
Ok(None) => {
return Err(anyhow::anyhow!(
"active persistence request had no completion"
));
}
Err(err) => {
handle_persistence_transport_failure(state, Instant::now(), &err);
return Err(err);
}
};
apply_persistence_completion(state, completion)?;
}
if !state.persistence.is_healthy() {
return Err(anyhow::anyhow!("session persistence worker is unhealthy"));
}
Ok(())
}
pub(in crate::backend::wayland) use runtime_session::driver::{
observe_input_dirty, persistence_barrier,
};

pub(in crate::backend::wayland) fn run_persistence_operation(
state: &mut WaylandState,
Expand Down Expand Up @@ -412,14 +407,20 @@ impl PersistenceCompletionRuntime for WaylandState {
fn try_receive_persistence_completion(
&mut self,
) -> Result<Option<PersistenceCompletion>, anyhow::Error> {
self.persistence.try_receive()
let result = self.persistence.try_receive();
if let Err(error) = &result
&& let Some(transaction) = self.session_transaction.take()
{
self.fail_session_command(transaction.command(), error);
}
result
}

fn apply_persistence_completion(
&mut self,
completion: PersistenceCompletion,
) -> Result<(), anyhow::Error> {
apply_persistence_completion(self, completion)
runtime_session::driver::apply_session_completion(self, completion)
}

fn persistence_session_options(&self) -> Option<session::SessionOptions> {
Expand Down Expand Up @@ -455,46 +456,24 @@ pub(in crate::backend::wayland) fn drain_persistence_completion_for_runtime(
Ok(())
}

fn apply_persistence_completion(
pub(in crate::backend::wayland) fn report_autosave_success(
state: &mut WaylandState,
completion: PersistenceCompletion,
) -> Result<(), anyhow::Error> {
observe_input_dirty(state, Instant::now());
let id = completion.id;
let save_result: Result<SaveCompletion, anyhow::Error> = match completion.result {
Ok(PersistenceOutcome::Save(save)) => Ok(save),
Ok(other) => Err(anyhow::anyhow!(
"unexpected asynchronous persistence outcome: {other:?}"
)),
Err(err) => Err(err),
};
let completed_at = Instant::now();
let committed = state
.session
.complete_autosave(id, completed_at, &save_result)?;
match save_result {
Ok(save) if committed => {
log_session_save_result(
SessionSaveReason::Autosave,
save.report.as_ref(),
completion.execution_time,
);
notify_session_save_report(state, save.report.as_ref());
}
Ok(_) => {
let err = anyhow::anyhow!("autosave worker completed without writing session data");
handle_autosave_failure(state, completed_at, &err);
return Err(err);
}
Err(err) => {
handle_autosave_failure(state, completed_at, &err);
return Err(err);
}
}
Ok(())
save: runtime_session::SaveCompletion,
execution_time: Duration,
) {
log_session_save_result(
SessionSaveReason::Autosave,
save.report.as_ref(),
execution_time,
);
notify_session_save_report(state, save.report.as_ref());
}

fn handle_autosave_failure(state: &mut WaylandState, now: Instant, err: &anyhow::Error) {
pub(in crate::backend::wayland) fn handle_autosave_failure(
state: &mut WaylandState,
now: Instant,
err: &anyhow::Error,
) {
let Some(options) = state.session_options().cloned() else {
return;
};
Expand All @@ -504,7 +483,7 @@ fn handle_autosave_failure(state: &mut WaylandState, now: Instant, err: &anyhow:
}
}

fn handle_persistence_transport_failure(
pub(in crate::backend::wayland) fn handle_persistence_transport_failure(
state: &mut WaylandState,
now: Instant,
err: &anyhow::Error,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ pub(super) fn record_autosave_success(
}
}

pub(super) fn record_autosave_failure(
pub(in crate::backend::wayland) fn record_autosave_failure(
session_state: &mut SessionState,
now: Instant,
options: &session::SessionOptions,
Expand Down
14 changes: 14 additions & 0 deletions src/backend/wayland/runtime_ui_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,15 @@ mod lifecycle;
mod live_state;
mod positions;
mod rollback;
mod seed_refresh;
mod seeds;
mod wayland;

#[cfg(test)]
pub(in crate::backend::wayland) use seed_refresh::{
RuntimeUiSeedRefresh, SeedRefreshContext, refresh_runtime_ui_config_seeds,
};

use live_state::{
apply_live_board_state, apply_live_toolbar_positions, apply_live_toolbar_state,
apply_persisted_top_display_mode, runtime_preview_authority, top_display_mode_values,
Expand All @@ -49,6 +55,14 @@ pub(in crate::backend::wayland) struct ToolbarPositionSnapshot {
pub top: (f64, f64),
}

impl ToolbarPositionSnapshot {
fn from_chrome(chrome: &crate::backend::wayland::state::ToolbarChrome) -> Self {
Self {
top: chrome.top_offset(),
}
}
}

#[derive(Debug)]
pub(in crate::backend::wayland) enum ToolbarRuntimeFinish {
KeepPreview,
Expand Down
70 changes: 70 additions & 0 deletions src/backend/wayland/runtime_ui_state/seed_refresh.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//! Shared seed reconciliation and publication; only protocol drag teardown is adapter-owned.
use super::{ToolbarPositionSnapshot, ToolbarRuntimeState};
use crate::backend::wayland::{
state::{ToolbarChrome, ToolbarDrag},
toolbar::ToolbarSurfaceManager,
};
use crate::{config::Config, draw::TextMeasurer, input::InputState, ui_text::UiTextEngine};

pub(in crate::backend::wayland) struct SeedRefreshContext<'a> {
pub config: &'a Config,
pub input: &'a mut InputState,
pub engine: &'a UiTextEngine,
pub measurer: &'a TextMeasurer,
pub runtime: Option<&'a mut ToolbarRuntimeState>,
pub drag: &'a mut ToolbarDrag,
pub chrome: &'a mut ToolbarChrome,
pub toolbar: &'a mut ToolbarSurfaceManager,
}

pub(in crate::backend::wayland) trait RuntimeUiSeedRefresh {
fn seed_refresh_context(&mut self) -> SeedRefreshContext<'_>;
fn cancel_position_drags(&mut self);
}

pub(in crate::backend::wayland) fn refresh_runtime_ui_config_seeds(
owner: &mut impl RuntimeUiSeedRefresh,
) {
let (positions, refresh) = {
let context = owner.seed_refresh_context();

context
.input
.boards
.sync_pin_seeds_from_config(&context.config.resolved_boards());

let Some(runtime) = context.runtime else {
return;
};

let mut positions = ToolbarPositionSnapshot::from_chrome(context.chrome);
let refresh = runtime.refresh_config_seeds(
context.engine,
context.measurer,
context.config,
context.input,
&mut positions,
);
if !refresh.applied {
return;
}

if refresh.item_drag_aborted {
context.input.clear_toolbar_item_drag();
context.drag.set_item_dragging(false);
}

(positions, refresh)
};

if refresh.position_drag_aborted {
owner.cancel_position_drags();
}

// Seed reconciliation owns preview rollback; protocol drag teardown remains adapter-owned.
let context = owner.seed_refresh_context();
context.chrome.set_top_offset(positions.top);
context.toolbar.mark_dirty();
context.input.dirty_tracker.mark_full();
context.input.needs_redraw = true;
}
Loading
Loading