Add session-owned Pomodoro cycles and controls

Add configurable work and break intervals, pause/resume, CLI and activity-popup controls, and silent completion alerts. Use CLOCK_BOOTTIME deadlines so running timers include suspend without consuming paused time.

Refs #31
This commit is contained in:
Samuel Onoja committed 2026-10-20 15:19:55 +01:00
1 parent 53ba599918
commit 2c9bb85f2c
18 files changed
+1209 -16

No files matched your search

+66 -4
View File
@@ -145,7 +145,7 @@ fn label(activity: &Activity) -> String {
match &activity.data.value {
Value::Text(text) => text.clone(),
Value::Progress(progress) => format!("{}%", (progress * 100.).round() as u32),
Value::Countdown { .. } => {
Value::Countdown { .. } | Value::PausedCountdown { .. } => {
let seconds = activity.data.value.remaining_ms(now_ms()).unwrap_or(0).div_ceil(1_000);
format!("{}:{:02}", seconds / 60, seconds % 60)
}
@@ -449,7 +449,9 @@ impl FereseShell {
};
let ring = ring::view(
progress,
animate && activity.state == State::Running,
animate
&& activity.state == State::Running
&& !matches!(activity.data.value, Value::PausedCountdown { .. }),
reduced,
ring_color,
);
@@ -947,6 +949,60 @@ impl FereseShell {
.width(if intrinsic { Length::Fixed(budget) } else { Length::Fill })
.into()
}
fn pomodoro_controls(&self, opacity: f32) -> Element<'_, cosmic::Action<Message>> {
use ferese_activities::pomodoro::Status;
use ferese_ipc::activities::PomodoroAction as Action;
let current = self.activities.snapshot.pomodoro.as_ref();
let session = current.map(|current| current.id.clone());
let control = |label: &'static str, action| {
button::text(label).on_press(cosmic::Action::App(Message::Pomodoro(action, session.clone())))
};
let mut controls = row::with_capacity(4).spacing(6).align_y(Alignment::Center);
let description = if let Some(current) = current {
let timer = &current.timer;
if timer.status == Status::Finished {
controls = controls
.push(control("Start again", Action::Start))
.push(control("Dismiss", Action::Stop));
format!("Pomodoro finished · {} work intervals", timer.completed_work)
} else {
controls = controls.push(match timer.status {
Status::Running => control("Pause", Action::Pause),
Status::Paused => control("Resume", Action::Resume),
_ => control("Start phase", Action::Resume),
});
if timer.status != Status::Ready {
controls = controls.push(control("Skip", Action::Next));
}
controls = controls
.push(control("Reset", Action::Reset))
.push(control("Stop", Action::Stop));
format!(
"Pomodoro · {}{} · {}/{}",
timer.phase.label(),
match timer.status {
Status::Paused => " · Paused",
Status::Ready => " · Ready",
_ => "",
},
timer.completed_work,
timer.settings.intervals
)
}
} else {
controls = controls.push(control("Start Pomodoro", Action::Start));
"Pomodoro".into()
};
column![
self.render.text(description).size(12).class(theme::Text::Color(
self.config.theme.palette().text.scale_alpha(opacity)
)),
controls
]
.spacing(6)
.width(Length::Fill)
.into()
}
pub fn activity_list(&self, id: window::Id, opacity: f32) -> Element<'_, cosmic::Action<Message>> {
let palette = self.config.theme.palette();
let name = self.output_for_bar(id).map(|output| output.name.as_str()).unwrap_or("");
@@ -965,7 +1021,7 @@ impl FereseShell {
});
let mut rows = Vec::with_capacity(entries.len());
if entries.is_empty() {
return self.render.text("No active tasks").size(13).into();
rows.push(self.render.text("No active tasks").size(13).into());
}
for activity in entries {
let mut text = column![
@@ -1073,6 +1129,12 @@ impl FereseShell {
// Keep unnamed row roots: the toolkit's column does not move directly
// named child trees when their order changes. Inner button IDs restore
// focus through the normal named-state reconciliation on each rebuild.
column::with_children(rows).spacing(4).width(Length::Fill).into()
column![
self.pomodoro_controls(opacity),
column::with_children(rows).spacing(4).width(Length::Fill)
]
.spacing(8)
.width(Length::Fill)
.into()
}
}