diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index db137bc..cc51e57 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,3 +1,7 @@ +//! Backend bridge for the Tauclock application. This module binds the shared +//! state container to the Tauri command system, configures plugins, and exposes +//! convenience helpers for logging and persistence. + mod state; use log::LevelFilter; @@ -5,18 +9,24 @@ use state::{AppState, FrontendState}; use std::{path::PathBuf, str::FromStr}; use tauri_plugin_log::{Target, TargetKind}; +/// Returns the current frontend snapshot without mutating any state. Used for +/// the initial UI bootstrap. #[tauri::command] fn get_state(app_state: tauri::State) -> FrontendState { log::debug!("get_state invoked"); app_state.snapshot() } +/// Lightweight tick endpoint that allows the frontend to poll for changes such +/// as timer countdowns and stopwatch progress without issuing full commands. #[tauri::command] fn tick(app_state: tauri::State) -> FrontendState { log::trace!("tick invoked"); app_state.tick() } +/// Creates a new alarm entry for the provided time/label combination and +/// returns the refreshed snapshot so the UI can re-render immediately. #[tauri::command] fn create_alarm( time: String, @@ -35,18 +45,22 @@ fn create_alarm( app_state.create_alarm(time, label) } +/// Enables or disables the specified alarm without deleting it. #[tauri::command] fn toggle_alarm(id: String, app_state: tauri::State) -> FrontendState { log::info!("toggle_alarm invoked (id={id})", id = id); app_state.toggle_alarm(id) } +/// Permanently removes the referenced alarm and emits a new snapshot. #[tauri::command] fn delete_alarm(id: String, app_state: tauri::State) -> FrontendState { log::info!("delete_alarm invoked (id={id})", id = id); app_state.delete_alarm(id) } +/// Starts, pauses, or reconfigures the countdown timer depending on the +/// requested input, optionally forcing the duration string to be reparsed. #[tauri::command] fn toggle_timer( input: Option, @@ -65,30 +79,36 @@ fn toggle_timer( app_state.toggle_timer(input, force_parse) } +/// Resets the timer to its initial duration and stops any active countdown. #[tauri::command] fn reset_timer(app_state: tauri::State) -> FrontendState { log::debug!("reset_timer invoked"); app_state.reset_timer() } +/// Starts or pauses the stopwatch while retaining any accumulated elapsed time. #[tauri::command] fn toggle_stopwatch(app_state: tauri::State) -> FrontendState { log::info!("toggle_stopwatch invoked"); app_state.toggle_stopwatch() } +/// Records a lap entry at the stopwatch's current elapsed time. #[tauri::command] fn add_lap(app_state: tauri::State) -> FrontendState { log::debug!("add_lap invoked"); app_state.add_lap() } +/// Clears the stopwatch, deleting all laps and resetting the elapsed counter. #[tauri::command] fn reset_stopwatch(app_state: tauri::State) -> FrontendState { log::debug!("reset_stopwatch invoked"); app_state.reset_stopwatch() } +/// Initializes the Tauri runtime, configures plugins, and wires shared +/// application state into every window. #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { let context = tauri::generate_context!(); @@ -130,6 +150,8 @@ pub fn run() { .expect("error while running tauri application"); } +/// Determines the log level from environment variables, falling back to a +/// debug build aware default. fn resolve_log_level() -> LevelFilter { let env_value = std::env::var("TAUCLOCK_LOG_LEVEL") .or_else(|_| std::env::var("TAUCLOCK_LOG")) @@ -151,6 +173,8 @@ fn resolve_log_level() -> LevelFilter { } } +/// Resolves the per-user storage file, ensuring persisted alarms live under an +/// identifier-scoped directory. fn resolve_storage_path(config: &tauri::Config) -> PathBuf { let mut dir = dirs::data_dir() .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))); diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index 72be650..95fda6b 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -11,16 +11,23 @@ use std::{ }; use uuid::Uuid; +/// Duration (ms) an informational status message remains visible. const STATUS_DURATION_MS: i64 = 4000; +/// Amount of time (ms) an alarm is considered to be "ringing" once triggered. const RINGING_DURATION_MS: i64 = 6000; +/// Minimum timer duration accepted from the UI to avoid accidental zero timers. const TIMER_MIN_MS: u64 = 1000; +/// Shared state container held by Tauri. It wraps a mutex-protected +/// [`StateInner`] and is responsible for persistence plus providing immutable +/// snapshots to the frontend. pub struct AppState { inner: Mutex, storage_path: PathBuf, } impl AppState { + /// Constructs a new state container and eagerly loads persisted alarms. pub fn new(storage_path: PathBuf) -> Self { let mut inner = StateInner::default(); if let Some(list) = load_alarms(&storage_path) { @@ -44,56 +51,69 @@ impl AppState { } } + /// Returns a `FrontendState` without mutating anything. Primarily used when + /// the UI first boots. pub fn snapshot(&self) -> FrontendState { trace!("Building snapshot for frontend"); self.apply(|_, _, _| {}) } + /// Executes the housekeeping refresh logic and returns an updated snapshot. pub fn tick(&self) -> FrontendState { trace!("Processing tick request"); self.apply(|_, _, _| {}) } + /// Creates a new alarm entry using the provided time and label. pub fn create_alarm(&self, time: String, label: Option) -> FrontendState { self.apply(|inner, now_ms, now_local| { inner.create_alarm(&time, label.as_deref(), now_ms, now_local); }) } + /// Toggles an existing alarm's active state. pub fn toggle_alarm(&self, id: String) -> FrontendState { self.apply(|inner, _now_ms, now_local| { inner.toggle_alarm(&id, now_local); }) } + /// Deletes an alarm permanently. pub fn delete_alarm(&self, id: String) -> FrontendState { self.apply(|inner, now_ms, _| { inner.delete_alarm(&id, now_ms); }) } + /// Starts or pauses the timer depending on the current state. pub fn toggle_timer(&self, input: Option, force_parse: bool) -> FrontendState { self.apply(|inner, now_ms, _| { inner.toggle_timer(input.as_deref(), force_parse, now_ms); }) } + /// Resets the timer to its initial duration. pub fn reset_timer(&self) -> FrontendState { self.apply(|inner, _, _| inner.reset_timer()) } + /// Starts or pauses the stopwatch. pub fn toggle_stopwatch(&self) -> FrontendState { self.apply(|inner, now_ms, _| inner.toggle_stopwatch(now_ms)) } + /// Records a lap for the stopwatch if running. pub fn add_lap(&self) -> FrontendState { self.apply(|inner, now_ms, _| inner.add_lap(now_ms)) } + /// Resets the stopwatch to zero elapsed time and clears laps. pub fn reset_stopwatch(&self) -> FrontendState { self.apply(|inner, _, _| inner.reset_stopwatch()) } + /// Executes a state mutation closure while handling locking, refresh logic, + /// snapshot generation, and persistence side effects. fn apply(&self, mutator: F) -> FrontendState where F: FnOnce(&mut StateInner, i64, &DateTime), @@ -125,6 +145,7 @@ impl AppState { snapshot } + /// Persists the alarm list to disk, creating directories as needed. fn save_alarms(&self, alarms: &[PersistedAlarm]) { if let Some(parent) = self.storage_path.parent() { if let Err(error) = fs::create_dir_all(parent) { @@ -161,6 +182,7 @@ impl AppState { } #[derive(Default)] +/// Mutable inner state containing alarms, timers, and transient UI messaging. struct StateInner { alarms: Vec, timer: TimerState, @@ -171,12 +193,14 @@ struct StateInner { } impl StateInner { + /// Executes periodic tasks that should run on every mutation or tick. fn refresh(&mut self, now_ms: i64, now_local: &DateTime) { self.process_alarms(now_ms, now_local); self.update_timer(now_ms); self.expire_status(now_ms); } + /// Builds a full [`FrontendState`] representation of the current data. fn to_frontend(&mut self, now_local: &DateTime) -> FrontendState { let now_ms = now_local.timestamp_millis(); let now_utc = now_local.with_timezone(&Utc); @@ -201,6 +225,7 @@ impl StateInner { } } + /// Adds a new alarm and schedules its next trigger time. fn create_alarm( &mut self, raw_time: &str, @@ -235,6 +260,7 @@ impl StateInner { self.push_status(&format!("アラーム追加: {}", label_text), now_ms); } + /// Toggles an alarm's active state and recalculates its next trigger. fn toggle_alarm(&mut self, id: &str, now_local: &DateTime) { if let Some(alarm) = self.alarms.iter_mut().find(|alarm| alarm.id == id) { alarm.active = !alarm.active; @@ -251,6 +277,7 @@ impl StateInner { } } + /// Removes an alarm completely. fn delete_alarm(&mut self, id: &str, now_ms: i64) { if let Some(index) = self.alarms.iter().position(|alarm| alarm.id == id) { let removed = self.alarms.remove(index); @@ -262,6 +289,7 @@ impl StateInner { } } + /// Handles timer start/pause logic, parsing inputs when necessary. fn toggle_timer(&mut self, input: Option<&str>, force_parse: bool, now_ms: i64) { if self.timer.running { if let Some(target) = self.timer.target_epoch_ms { @@ -317,6 +345,7 @@ impl StateInner { ); } + /// Restores the timer to its initial duration without starting it. fn reset_timer(&mut self) { self.timer.running = false; self.timer.target_epoch_ms = None; @@ -324,6 +353,7 @@ impl StateInner { info!("Timer reset to {}ms", self.timer.initial_ms); } + /// Starts or pauses the stopwatch while accounting for elapsed time. fn toggle_stopwatch(&mut self, now_ms: i64) { if self.stopwatch.running { if let Some(start) = self.stopwatch.start_epoch_ms { @@ -340,6 +370,8 @@ impl StateInner { } } + /// Records a lap entry capturing both the total elapsed time and the delta + /// since the previous lap. fn add_lap(&mut self, now_ms: i64) { let elapsed = self.stopwatch_elapsed(now_ms); if elapsed == 0 { @@ -369,6 +401,7 @@ impl StateInner { ); } + /// Stops the stopwatch and clears any lap history. fn reset_stopwatch(&mut self) { self.stopwatch.running = false; self.stopwatch.start_epoch_ms = None; @@ -377,6 +410,8 @@ impl StateInner { info!("Stopwatch reset"); } + /// Iterates configured alarms, triggering those scheduled for the current + /// tick and scheduling their next activation. fn process_alarms(&mut self, now_ms: i64, now_local: &DateTime) { let mut triggered_labels = Vec::new(); for alarm in &mut self.alarms { @@ -408,6 +443,8 @@ impl StateInner { } } + /// Updates timer countdown bookkeeping and fires completion status when the + /// clock hits zero. fn update_timer(&mut self, now_ms: i64) { if self.timer.running { if let Some(target) = self.timer.target_epoch_ms { @@ -425,6 +462,7 @@ impl StateInner { } } + /// Expires the active status message once its TTL elapses. fn expire_status(&mut self, now_ms: i64) { if let Some(status) = &self.status { if status.expires_at <= now_ms { @@ -433,6 +471,7 @@ impl StateInner { } } + /// Builds the alarm section view model that the frontend expects. fn build_alarm_view(&self, now_ms: i64) -> AlarmSection { let mut sorted = self.alarms.iter().collect::>(); sorted.sort_by_key(|alarm| alarm.next_trigger); @@ -471,6 +510,7 @@ impl StateInner { } } + /// Builds the timer view model by formatting durations and button labels. fn build_timer_view(&self) -> TimerView { let display_ms = if self.timer.running { self.timer.remaining_ms @@ -502,6 +542,7 @@ impl StateInner { } } + /// Builds the stopwatch view model including lap breakdowns. fn build_stopwatch_view(&self, now_ms: i64) -> StopwatchView { let elapsed = self.stopwatch_elapsed(now_ms); let toggle_label = if self.stopwatch.running { @@ -517,6 +558,8 @@ impl StateInner { } } + /// Returns the total stopwatch elapsed milliseconds, optionally including + /// the in-flight span if the stopwatch is currently running. fn stopwatch_elapsed(&self, now_ms: i64) -> u64 { self.stopwatch.elapsed_before_ms + if self.stopwatch.running { @@ -526,6 +569,7 @@ impl StateInner { } } + /// Stores a transient status message that the UI can render for users. fn push_status(&mut self, message: &str, now_ms: i64) { self.status = Some(StatusMessage { text: message.to_string(), @@ -575,6 +619,7 @@ struct StatusMessage { } #[derive(Serialize)] +/// Comprehensive state payload consumed by the frontend. pub struct FrontendState { pub clock: ClockState, pub alarms: AlarmSection, @@ -585,6 +630,7 @@ pub struct FrontendState { } #[derive(Serialize)] +/// Clock label strings rendered in the UI. pub struct ClockState { pub local_time: String, pub local_date: String, @@ -593,12 +639,14 @@ pub struct ClockState { } #[derive(Serialize)] +/// Aggregated alarm listing plus small summary for the next trigger. pub struct AlarmSection { pub items: Vec, pub next_summary: String, } #[derive(Serialize)] +/// Individual alarm entry displayed in the frontend list. pub struct AlarmView { pub id: String, pub label: String, @@ -609,6 +657,7 @@ pub struct AlarmView { } #[derive(Serialize)] +/// Timer labels consumed by the UI controls. pub struct TimerView { pub display: String, pub toggle_label: String, @@ -616,6 +665,7 @@ pub struct TimerView { } #[derive(Serialize)] +/// Stopwatch value for the UI including lap breakdowns. pub struct StopwatchView { pub display: String, pub toggle_label: String, @@ -623,6 +673,7 @@ pub struct StopwatchView { } #[derive(Serialize)] +/// Single lap renderable information. pub struct LapView { pub id: String, pub label: String, @@ -631,6 +682,7 @@ pub struct LapView { } #[derive(Serialize, Deserialize, Clone)] +/// Minimal alarm representation stored on disk. struct PersistedAlarm { id: String, hour: u8, @@ -640,6 +692,7 @@ struct PersistedAlarm { } #[derive(Serialize, Deserialize, Default)] +/// Wrapper used when serializing alarm lists to JSON. struct AlarmStore { alarms: Vec, } @@ -656,6 +709,7 @@ impl From<&Alarm> for PersistedAlarm { } } +/// Attempts to load the alarm store JSON file from disk. fn load_alarms(path: &Path) -> Option> { match fs::read(path) { Ok(bytes) => match serde_json::from_slice::(&bytes) { @@ -678,18 +732,23 @@ fn load_alarms(path: &Path) -> Option> { } } +/// Formats a timestamp as HH:MM:SS. fn format_time(hour: u32, minute: u32, second: u32) -> String { format!("{:02}:{:02}:{:02}", hour, minute, second) } +/// Formats a date as YYYY-MM-DD. fn format_date(year: i32, month: u32, day: u32) -> String { format!("{year:04}-{month:02}-{day:02}") } +/// Formats an alarm display label. fn format_time_label(hour: u8, minute: u8) -> String { format!("{:02}:{:02}", hour, minute) } +/// Produces a human-friendly relative time string describing when an alarm +/// will next trigger. fn format_relative(target_ms: i64, now_ms: i64) -> String { let diff = target_ms - now_ms; if diff <= 0 { @@ -707,6 +766,7 @@ fn format_relative(target_ms: i64, now_ms: i64) -> String { format!("{}時間{}分後", hours, mins) } +/// Formats a duration for timer display, switching to HH:MM:SS when needed. fn format_duration(ms: u64) -> String { let total_seconds = (ms / 1000) as u64; let minutes = total_seconds / 60; @@ -721,6 +781,7 @@ fn format_duration(ms: u64) -> String { } } +/// Formats stopwatch durations with millisecond precision. fn format_stopwatch(ms: u64) -> String { let total_seconds = ms / 1000; let minutes = total_seconds / 60; @@ -729,6 +790,7 @@ fn format_stopwatch(ms: u64) -> String { format!("{:02}:{:02}.{:03}", minutes, seconds, millis) } +/// Converts raw lap entries into renderable view data. fn build_lap_views(entries: &[LapEntry]) -> Vec { if entries.is_empty() { return Vec::new(); @@ -747,6 +809,7 @@ fn build_lap_views(entries: &[LapEntry]) -> Vec { .collect() } +/// Parses HH:MM strings emitted by the UI. fn parse_alarm_time(value: &str) -> Option<(u8, u8)> { let trimmed = value.trim(); let mut parts = trimmed.split(':'); @@ -758,6 +821,7 @@ fn parse_alarm_time(value: &str) -> Option<(u8, u8)> { Some((hour, minute)) } +/// Parses timer duration input supporting seconds, MM:SS, or `1h 30m` formats. fn parse_duration(value: &str) -> Option { let trimmed = value.trim(); if trimmed.is_empty() { @@ -778,6 +842,7 @@ fn parse_duration(value: &str) -> Option { parse_unit_duration(trimmed) } +/// Helper that parses unit-suffixed duration tokens like `10m 5s`. fn parse_unit_duration(value: &str) -> Option { let mut total_ms = 0u64; for chunk in value.split_whitespace() { @@ -805,6 +870,7 @@ fn parse_unit_duration(value: &str) -> Option { } } +/// Calculates the next trigger timestamp in milliseconds for an alarm. fn resolve_next_trigger(hour: u8, minute: u8, now_local: &DateTime) -> i64 { let naive_time = NaiveTime::from_hms_opt(hour as u32, minute as u32, 0) .unwrap_or_else(|| NaiveTime::from_hms_opt(0, 0, 0).unwrap()); @@ -816,6 +882,8 @@ fn resolve_next_trigger(hour: u8, minute: u8, now_local: &DateTime) -> i6 candidate.timestamp_millis() } +/// Safely combines a date and time within the Local timezone, falling back to +/// UTC if the local conversion is ambiguous or invalid. fn combine_local(date: NaiveDate, time: NaiveTime) -> DateTime { let naive = NaiveDateTime::new(date, time); match Local.from_local_datetime(&naive) { diff --git a/src/main.js b/src/main.js index 9d336eb..b25d534 100644 --- a/src/main.js +++ b/src/main.js @@ -1,7 +1,23 @@ +/** + * Static prefix string used in every log entry to make browser console + * filtering easier when multiple apps share the same window. + */ const LOG_PREFIX = "[tauclock]"; +/** + * Permitted log levels ordered by severity so we can determine whether a + * message should be printed by comparing their indexes. + */ const LOG_LEVELS = ["error", "warn", "info", "debug"]; +/** + * Effective log level obtained either from localStorage, a query parameter or + * the default, which then gates every log call in the frontend. + */ const LOG_LEVEL = resolveLogLevel(); +/** + * JSDoc log wrapper that normalizes console usage and ensures we respect the + * active log level before emitting potentially noisy diagnostic data. + */ function logFrontend(level, message, details) { if (!shouldLog(level)) return; const output = `${LOG_PREFIX} ${message}`; @@ -18,6 +34,10 @@ const logWarn = (message, details) => logFrontend("warn", message, details); const logDebug = (message, details) => logFrontend("debug", message, details); const logError = (message, details) => logFrontend("error", message, details); +/** + * Returns true if the requested log level is enabled by comparing the + * requested level's index with the configured LOG_LEVEL bucket. + */ function shouldLog(level) { const requested = LOG_LEVELS.indexOf(level); const active = LOG_LEVELS.indexOf(LOG_LEVEL); @@ -25,6 +45,10 @@ function shouldLog(level) { return requested <= active; } +/** + * Determines the preferred log level by first checking localStorage, then the + * `logLevel` URL parameter, and finally falling back to `info`. + */ function resolveLogLevel() { const stored = window.localStorage?.getItem("tauclock:log-level"); const normalizedStored = stored?.toLowerCase(); @@ -39,6 +63,11 @@ function resolveLogLevel() { const invoke = resolveInvoke(); +/** + * Finds the best available Tauri invoke bridge and emits structured debug + * information for troubleshooting. If the bridge is missing, a stub is + * returned so the UI can fail loudly rather than silently. + */ function resolveInvoke() { const tauriGlobal = window.__TAURI__ || {}; const candidates = [ @@ -59,10 +88,12 @@ function resolveInvoke() { }; } +/** Cache of DOM nodes keyed by logical role identifiers. */ const elements = {}; let tickInFlight = false; let latestState = null; +/** Keyboard shortcut handlers keyed by `KeyboardEvent.code`. */ const SHORTCUTS = { KeyA: () => focusAndSelect(elements.alarmTime), KeyN: () => elements.alarmForm.requestSubmit(), @@ -82,6 +113,10 @@ window.addEventListener("DOMContentLoaded", () => { bootstrap(); }); +/** + * Finds and stores all the DOM nodes we need to reference frequently so we + * avoid repeated lookups every render. + */ function cacheElements() { elements.localTime = document.getElementById("local-time"); elements.localDate = document.getElementById("local-date"); @@ -116,6 +151,10 @@ function cacheElements() { } } +/** + * Hooks up all DOM events, including keyboard shortcuts, button clicks, and + * form submissions, translating them into backend commands where needed. + */ function bindEvents() { elements.alarmForm.addEventListener("submit", async (event) => { event.preventDefault(); @@ -164,6 +203,10 @@ function bindEvents() { logInfo("Core UI events bound"); } +/** + * Performs the initial state fetch and schedules the recurring `tick` loop so + * that the UI stays synchronized with the backend clock. + */ async function bootstrap() { logInfo("Bootstrapping state"); await requestState("get_state"); @@ -171,6 +214,10 @@ async function bootstrap() { logInfo("Tick loop scheduled", { intervalMs: 250 }); } +/** + * Requests a timer toggle from the backend, optionally forcing a reparse of + * the duration input if the user explicitly pressed Enter inside the field. + */ function triggerTimerToggle(forceParse) { logDebug("Timer toggle requested", { forceParse, input: elements.timerInput.value }); requestState("toggle_timer", { @@ -179,6 +226,10 @@ function triggerTimerToggle(forceParse) { }); } +/** + * Generic wrapper around the Tauri invoke API that logs both the request and + * response, applies the new state snapshot, and gracefully handles failures. + */ async function requestState(command, payload = {}) { try { logInfo(`Invoking backend command: ${command}`, payload); @@ -195,6 +246,11 @@ async function requestState(command, payload = {}) { } } +/** + * Polling task that asks the backend for a light-weight state update so that + * timers, alarms, and stopwatches continue to advance even without user + * interaction. Re-entrancy is avoided via `tickInFlight`. + */ async function runTick() { if (tickInFlight) { logDebug("Tick skipped because a previous request is still pending"); @@ -213,6 +269,10 @@ async function runTick() { } } +/** + * Applies a backend-provided snapshot to every UI subsystem (clock, alarms, + * timer, stopwatch, and status messaging). Also plays the chime if requested. + */ function applyState(state) { if (!state) return; latestState = state; @@ -231,6 +291,7 @@ function applyState(state) { } } +/** Re-renders the clock labels for both local time and UTC. */ function updateClock(clock) { if (!clock) return; elements.localTime.textContent = clock.local_time; @@ -239,6 +300,10 @@ function updateClock(clock) { elements.utcDate.textContent = clock.utc_date; } +/** + * Renders the alarm list including badges, actions, and empty states, while + * also surfacing the next upcoming alarm summary. + */ function renderAlarms(section) { const list = elements.alarmList; list.innerHTML = ""; @@ -280,6 +345,7 @@ function renderAlarms(section) { elements.nextAlarm.textContent = section ? section.next_summary : ""; } +/** Reflects the backend timer information in the UI controls and labels. */ function renderTimer(view) { if (!view) return; elements.timerDisplay.textContent = view.display; @@ -287,6 +353,7 @@ function renderTimer(view) { elements.timerStatus.textContent = view.status_text || ""; } +/** Displays stopwatch totals, the toggle label, and lap entries. */ function renderStopwatch(view) { if (!view) return; elements.stopwatchDisplay.textContent = view.display; @@ -313,17 +380,23 @@ function renderStopwatch(view) { }); } +/** Shows transient status messages returned by the backend. */ function renderStatus(message) { if (!elements.statusMessage) return; elements.statusMessage.textContent = message || ""; } +/** Focuses a control and selects its text when possible for easy overwriting. */ function focusAndSelect(element) { if (!element) return; element.focus(); if (typeof element.select === "function") element.select(); } +/** + * Toggles the keyboard shortcut cheat sheet overlay, optionally forcing the + * visibility state when invoked with a boolean. + */ function toggleShortcutOverlay(force) { const overlay = elements.shortcutOverlay; if (!overlay) return; @@ -335,6 +408,10 @@ function toggleShortcutOverlay(force) { } } +/** + * Global keyboard listener that translates single-key shortcuts into higher + * level actions while respecting modifier keys, focus states, and overlays. + */ function handleShortcuts(event) { if (event.defaultPrevented) return; const overlayVisible = elements.shortcutOverlay && !elements.shortcutOverlay.hidden; @@ -370,6 +447,7 @@ function handleShortcuts(event) { } } +/** Returns true when the event target is an editable/typing-friendly element. */ function isTypingTarget(target) { if (!target) return false; const element = target instanceof Element ? target : target.parentElement; @@ -378,6 +456,10 @@ function isTypingTarget(target) { return Boolean(element.closest("input, textarea, select, [contenteditable='true']")); } +/** + * Plays a very small synthesized chime using the Web Audio API in order to + * alert the user even when no native notifications are available. + */ function playChime(patternName = "alarm") { const AudioCtx = window.AudioContext || window.webkitAudioContext; if (!AudioCtx) return;