pub struct Scheduler {Show 25 fields
pub settings: Arc<Mutex<Settings>>,
pub pause_state: Arc<Mutex<PauseState>>,
pub camera_active: Arc<AtomicBool>,
pub video_active: Arc<AtomicBool>,
pub plugin_suppress: Arc<AtomicBool>,
pub auto_suppress_reason: Arc<AtomicU8>,
pub config_path: PathBuf,
pub pause_path: PathBuf,
pub events_path: PathBuf,
pub screen_time_path: PathBuf,
pub chores_path: PathBuf,
pub plugins_path: PathBuf,
pub plugins: Arc<Mutex<PluginRegistry>>,
pub plugin_dialog_busy: Arc<AtomicBool>,
pub timers: Arc<Mutex<BreakTimers>>,
pub stats: Arc<Mutex<BreakStats>>,
pub screen_time: Arc<Mutex<ScreenTimeState>>,
pub chores: Arc<Mutex<ChoresState>>,
pub current_break: Arc<Mutex<Option<BreakEvent>>>,
pub logger: Logger,
pub profiles: Arc<Mutex<Vec<Profile>>>,
pub active_profile_name: Arc<Mutex<String>>,
pub hook_dialog_busy: Arc<AtomicBool>,
pub onboarding_completed: Arc<AtomicBool>,
pub import_in_progress: Arc<AtomicBool>,
}Expand description
Live, mutable state for the break scheduler.
Constructed once in lib::run and shared across the app via
tauri::State and Arc-cloning. Every mutable field sits behind a
tokio::Mutex (or a std::sync::Mutex for the renderer-bound
current_break slot, which only needs short critical sections).
Clone is cheap — it bumps the inner Arcs.
The persisted paths (config_path, pause_path, etc.) are captured
at construction so the scheduler can write them back without
re-resolving Tauri’s app_data_dir each tick.
§Locking convention: no nested async mutexes across .await
Every call site in this module releases a tokio::Mutex guard
before acquiring the next one across an .await point. The pattern
is “snapshot then act”:
let s = sched.settings.lock().await.clone(); // release before next lock
let name = sched.active_profile_name.lock().await.clone();
let mut profiles = sched.profiles.lock().await; // safe — others releasedFollowing this rule, deadlock becomes structurally impossible — the
classic “thread A holds X waiting for Y, thread B holds Y waiting
for X” cycle cannot form if guards never overlap on .await.
What this rules out:
let s = sched.settings.lock().await; let p = sched.profiles.lock().await;(holdingsettingsacross theprofilesacquisition)let g = sched.timers.lock().await; some_async_fn(&sched).await;(holding any guard across a call that may itself lock the same scheduler)
What it allows:
- Re-acquiring the same lock back-to-back to mutate after an awaited side-effect (write to disk, emit event). Each scope drops first.
- The std
current_breakmutex, which is only ever taken inside short non-async blocks (seeoverlay::fire_break). - Short synchronous emits (
app.emit("evt", &single_field)) that borrow a guard expression in the argument list and drop it at the end of the statement — the emit itself does not.awaitand yields no scheduler lock. - Reading two unrelated single-field snapshots back-to-back inside
one command (see
get_postpone_state): clone the first, drop, then acquire the second. Brief observational skew is fine for renderer queries that never make causal decisions across the pair.
If a new code path genuinely needs nested holds — say, an atomic read-modify-write across two pieces of state — consolidate them into one struct under one mutex instead of introducing the nesting.
Fields§
§settings: Arc<Mutex<Settings>>§pause_state: Arc<Mutex<PauseState>>§camera_active: Arc<AtomicBool>§video_active: Arc<AtomicBool>§plugin_suppress: Arc<AtomicBool>Whether any installed detector plugin currently votes to suppress
breaks. Written by the off-tick detector-eval task, read by the 1Hz
loop’s suppression chain — like camera_active, an atomic so the
per-tick read is free.
auto_suppress_reason: Arc<AtomicU8>0 = not auto-suppressed; otherwise SuppressReason::from_u8
decodes which guard fired. The tray reads this each tick to
pick between the Inactive icon + reason tooltip vs the Normal
icon. Atomic instead of a mutex so the per-tick read is free.
config_path: PathBuf§pause_path: PathBuf§events_path: PathBuf§screen_time_path: PathBuf§chores_path: PathBufThe day’s chore “post-it” (chores.json beside screen_time.json):
a daily-reset list the user enters and the overlay surfaces as a
long-break nudge. Global, not per-profile — a chore list is about
the day, not the active settings profile.
plugins_path: PathBufInstalled content plugins + the merge-and-track record of what each
added, persisted to plugins.json beside settings.json. See
docs/developer/plugin-api-design.md.
plugins: Arc<Mutex<PluginRegistry>>§plugin_dialog_busy: Arc<AtomicBool>Single-flight guard for the plugin-install confirmation dialog,
mirroring Scheduler::hook_dialog_busy.
timers: Arc<Mutex<BreakTimers>>§stats: Arc<Mutex<BreakStats>>§screen_time: Arc<Mutex<ScreenTimeState>>§chores: Arc<Mutex<ChoresState>>§current_break: Arc<Mutex<Option<BreakEvent>>>§logger: Logger§profiles: Arc<Mutex<Vec<Profile>>>§active_profile_name: Arc<Mutex<String>>§hook_dialog_busy: Arc<AtomicBool>§onboarding_completed: Arc<AtomicBool>Whether first-run onboarding has been completed. Mirrors
ProfilesFile::onboarding_completed; persisted back to disk via
snapshot_profiles_file. Atomic so the IPC commands can read and
flip it without taking the profiles lock.
import_in_progress: Arc<AtomicBool>Set by the backup-import flow while it’s mid-restore. The run loop short-circuits each tick while this is true so it can’t fire a break with mid-write state (e.g. new events.jsonl on disk but old settings still in memory).
Implementations§
Source§impl Scheduler
impl Scheduler
Sourcepub async fn tray_countdown_snapshot(&self) -> (TrayCountdownSnapshot, bool)
pub async fn tray_countdown_snapshot(&self) -> (TrayCountdownSnapshot, bool)
Snapshot the per-tick state the tray ticker needs. Polled once
per second on macOS/Linux. See TrayCountdownSnapshot for the
precedence rules.
Returns (snapshot, text_enabled). text_enabled mirrors the
user’s tray_countdown_enabled setting — the ticker uses it to
gate the always-visible title text (icon + tooltip aren’t
gated, since the icon is the visual signal and the tooltip is
hover-only opt-in).
Source§impl Scheduler
impl Scheduler
Sourcepub fn new(
config_path: PathBuf,
pause_path: PathBuf,
events_path: PathBuf,
screen_time_path: PathBuf,
chores_path: PathBuf,
) -> Self
pub fn new( config_path: PathBuf, pause_path: PathBuf, events_path: PathBuf, screen_time_path: PathBuf, chores_path: PathBuf, ) -> Self
Load persisted state from disk and spawn the camera / video
monitor threads. Does not start the main scheduler loop —
call spawn for that, after app.manage-ing the result.
Sourcepub(crate) async fn resolve_chore_prompt(
&self,
kind: BreakKind,
) -> Option<String>
pub(crate) async fn resolve_chore_prompt( &self, kind: BreakKind, ) -> Option<String>
Resolve the day’s chore nudge for a firing break. Long breaks only — micro is too short and bedtime is for winding down. Rolls the list over at local midnight, advances the rotation cursor so consecutive long breaks suggest different tasks, and persists whenever either changed. Shared by the scheduled-fire and manual-trigger paths so the rollover/rotation logic lives in exactly one place.
Sourcepub fn spawn(&self, app: AppHandle)
pub fn spawn(&self, app: AppHandle)
Launch the 1Hz scheduler loop on the Tauri async runtime. Safe
to call exactly once per Scheduler instance.
Sourcefn spawn_detector_eval(&self)
fn spawn_detector_eval(&self)
Run the installed detector plugins off the 1Hz tick, on a throttled
interval, and publish their aggregate verdict to plugin_suppress for
the run loop to read. The wasm work happens on a blocking thread so it
never stalls the scheduler tick; building/running a detector is
fail-closed (a broken detector never suppresses). No detectors → the
flag is cleared and the loop short-circuits.
Sourcepub async fn snapshot_profiles_file(&self) -> ProfilesFile
pub async fn snapshot_profiles_file(&self) -> ProfilesFile
Build the on-disk shape ({ profiles, active }) by snapshotting
the in-memory profile list. Used by persist_profiles.
Trait Implementations§
Auto Trait Implementations§
impl !RefUnwindSafe for Scheduler
impl !UnwindSafe for Scheduler
impl Freeze for Scheduler
impl Send for Scheduler
impl Sync for Scheduler
impl Unpin for Scheduler
impl UnsafeUnpin for Scheduler
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
impl<S, T> Duplex<S> for Twhere
T: FromSample<S> + ToSample<S>,
§impl<S> FromSample<S> for S
impl<S> FromSample<S> for S
fn from_sample_(s: S) -> S
§impl<T> GetSetFdFlags for T
impl<T> GetSetFdFlags for T
§fn get_fd_flags(&self) -> Result<FdFlags, Error>where
T: AsFilelike,
fn get_fd_flags(&self) -> Result<FdFlags, Error>where
T: AsFilelike,
self file descriptor.§fn new_set_fd_flags(&self, fd_flags: FdFlags) -> Result<SetFdFlags<T>, Error>where
T: AsFilelike,
fn new_set_fd_flags(&self, fd_flags: FdFlags) -> Result<SetFdFlags<T>, Error>where
T: AsFilelike,
§impl<T> Instrument for T
impl<T> Instrument for T
§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more