use crate::cli::Command;
use crate::clock::Clock;
use crate::config::{Config, WakePolicy};
use crate::os::dnd::worker::DndWorker;
use crate::os::dnd::DndOutcome;
use crate::session;
use crate::timer::{Cmd, Effect, Phase, State, Timer};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Event {
FocusStart,
FocusEnd,
BreakStart,
BreakEnd,
}
#[derive(Clone, Debug, PartialEq)]
pub enum UiEvent {
Notify { title: String, body: String },
Sound(Event),
Refresh,
Warn(String),
}
pub struct Orchestrator {
pub config: Config,
pub timer: Timer,
clock: Clock,
dnd: DndWorker,
dnd_note: Option<String>,
dnd_engaged: bool,
dnd_active: bool,
dnd_poll_in: u32,
}
const DND_POLL_TICKS: u32 = 4;
impl Orchestrator {
pub fn new(config: Config, timer: Timer, dnd: DndWorker) -> Self {
Orchestrator {
config,
timer,
clock: Clock::new(),
dnd,
dnd_note: None,
dnd_engaged: false,
dnd_active: false,
dnd_poll_in: 0,
}
}
pub fn dnd_status(&self) -> &'static str {
if !self.config.dnd.enabled {
return "muting is switched off";
}
if self.dnd_engaged {
return "notifications muted";
}
match self.timer.state().phase() {
Some(p) if p.is_break() => "notifications on during breaks",
Some(_) => "notifications not muted",
None => "notifications on - nothing running",
}
}
pub fn dnd_note(&self) -> Option<&str> {
self.dnd_note.as_deref()
}
pub fn dnd_active(&self) -> bool {
self.dnd_active
}
fn poll_dnd(&mut self) {
self.dnd_active = if self.config.dnd.wants_indicator() {
crate::os::dnd::wnf::query().map_or(self.dnd_engaged, |state| state.is_on())
} else {
self.dnd_engaged
};
}
pub fn dispatch(&mut self, command: &Command) -> Vec<UiEvent> {
let cmd = match command {
Command::StartFocus => Cmd::Start(Phase::Focus),
Command::StartBreak => Cmd::Start(Phase::ShortBreak),
Command::Stop => Cmd::Stop,
Command::Pause => Cmd::Pause,
Command::Resume => Cmd::Resume,
Command::Toggle => Cmd::Toggle,
Command::Skip => Cmd::Skip,
Command::Preset(name) => {
let mut out = Vec::new();
if self.config.select_preset(name) {
let _ = self.config.save();
out.push(UiEvent::Refresh);
} else {
out.push(UiEvent::Warn(format!("No preset named \"{name}\"")));
}
return out;
}
Command::Run | Command::Quit | Command::Help | Command::Version | Command::Explain => {
return Vec::new()
}
};
self.apply(cmd)
}
pub fn tick(&mut self) -> Vec<UiEvent> {
let elapsed = self.clock.tick(self.config.behavior.wake_policy);
let mut out = Vec::new();
if self.dnd_poll_in == 0 {
self.poll_dnd();
self.dnd_poll_in = DND_POLL_TICKS;
} else {
self.dnd_poll_in -= 1;
}
if let Some(slept) = elapsed.slept {
if self.config.behavior.wake_policy == WakePolicy::Pause
&& self.timer.state().is_running()
{
out.extend(self.apply(Cmd::Pause));
out.push(UiEvent::Warn(format!(
"Paused: the machine was asleep for {}.",
human_duration(slept)
)));
return out;
}
}
if !elapsed.delta.is_zero() {
out.extend(self.apply(Cmd::Tick(elapsed.delta)));
}
out
}
pub fn note_time_change(&mut self) {
self.clock.note_time_change();
}
pub fn collect_dnd_reports(&mut self) -> Vec<UiEvent> {
let mut out = Vec::new();
for r in self.dnd.reports().collect::<Vec<_>>() {
crate::audit::log_dnd(r.engaging, &format!("{:?}", r.outcome));
match &r.outcome {
DndOutcome::Applied(_) => {
self.dnd_engaged = r.engaging;
self.dnd_note = None;
}
DndOutcome::AlreadyCorrect => {
if !r.engaging {
self.dnd_engaged = false;
}
}
DndOutcome::Unverified => {
self.dnd_engaged = r.engaging;
self.dnd_note = Some("Could not confirm the change with Windows".into());
}
DndOutcome::Failed(why) => {
self.dnd_note = Some(format!("Do Not Disturb failed: {why}"));
out.push(UiEvent::Warn(format!(
"Could not {} Do Not Disturb: {why}",
if r.engaging { "turn on" } else { "turn off" }
)));
}
}
out.push(UiEvent::Refresh);
}
if !out.is_empty() {
self.poll_dnd();
}
out
}
fn apply(&mut self, cmd: Cmd) -> Vec<UiEvent> {
let plan = self.config.plan();
let effects = self.timer.update(cmd, &plan);
if effects.is_empty() {
return Vec::new();
}
let mut out = Vec::new();
for e in &effects {
match *e {
Effect::Started(Phase::Focus) => {
self.set_dnd(true);
self.emit(
&mut out,
Event::FocusStart,
"Focus started",
&self.focus_body(),
);
}
Effect::Started(phase) => {
self.set_dnd(false);
self.emit(
&mut out,
Event::BreakStart,
&format!("{} started", phase.label()),
&format!("{} to relax.", self.remaining_text()),
);
}
Effect::Ended { phase, completed } => {
if completed {
if phase == Phase::Focus {
self.emit(
&mut out,
Event::FocusEnd,
"Focus session complete",
"Nice work - time for a break.",
);
} else {
self.emit(
&mut out,
Event::BreakEnd,
"Break over",
"Ready for the next focus session?",
);
}
}
}
Effect::WentIdle => self.set_dnd(false),
Effect::Paused(_) | Effect::Resumed(_) => {}
}
}
session::save(&self.timer);
out.push(UiEvent::Refresh);
out
}
fn focus_body(&self) -> String {
if self.config.dnd.enabled {
format!(
"{} of focus. Notifications are muted.",
self.remaining_text()
)
} else {
format!("{} of focus.", self.remaining_text())
}
}
fn remaining_text(&self) -> String {
self.timer
.state()
.remaining()
.map(human_duration)
.unwrap_or_default()
}
fn set_dnd(&mut self, on: bool) {
if !self.config.dnd.enabled {
return;
}
if on {
self.dnd.engage();
} else {
self.dnd.release();
}
}
fn emit(&self, out: &mut Vec<UiEvent>, event: Event, title: &str, body: &str) {
let n = &self.config.notifications;
let s = &self.config.sounds;
let enabled = |t: &crate::config::EventToggles| match event {
Event::FocusStart => t.focus_start,
Event::FocusEnd => t.focus_end,
Event::BreakStart => t.break_start,
Event::BreakEnd => t.break_end,
};
if n.enabled && enabled(&n.events) {
out.push(UiEvent::Notify {
title: title.to_string(),
body: body.to_string(),
});
}
if !s.muted && enabled(&s.events) {
out.push(UiEvent::Sound(event));
}
}
pub fn tooltip(&self) -> String {
match self.timer.state() {
State::Idle => "taskbar-focus - idle".into(),
State::Running {
phase, remaining, ..
} => format!("{} - {} left", phase.label(), human_duration(*remaining)),
State::Paused {
phase, remaining, ..
} => format!(
"{} - paused, {} left",
phase.label(),
human_duration(*remaining)
),
}
}
}
pub fn mmss(d: std::time::Duration) -> String {
let total = d.as_secs();
let (h, m, s) = (total / 3600, (total % 3600) / 60, total % 60);
if h > 0 {
format!("{h}:{m:02}:{s:02}")
} else {
format!("{m:02}:{s:02}")
}
}
pub fn human_duration(d: std::time::Duration) -> String {
let total = d.as_secs();
match (total / 3600, (total % 3600) / 60, total % 60) {
(0, 0, s) => format!("{s} second{}", plural(s)),
(0, m, _) => format!("{m} minute{}", plural(m)),
(h, 0, _) => format!("{h} hour{}", plural(h)),
(h, m, _) => format!("{h}h {m}m"),
}
}
fn plural(n: u64) -> &'static str {
if n == 1 {
""
} else {
"s"
}
}