use eframe::egui;
use crate::client::Client;
use crate::protocol::{Request, TimerInfo, TimerStatus};
use crate::task::Task;
pub struct HyperfocusApp {
tasks: Vec<Task>,
timer_info: TimerInfo,
selected_task: Option<usize>,
new_task_name: String,
new_task_duration: String,
}
impl HyperfocusApp {
pub fn new(_cc: &eframe::CreationContext<'_>) -> Self {
let mut app = Self {
tasks: Vec::new(),
timer_info: TimerInfo::default(),
selected_task: None,
new_task_name: String::new(),
new_task_duration: String::from("25"),
};
app.refresh();
app
}
/// Fetch latest state from the service.
fn refresh(&mut self) {
if let Ok(resp) = Client::send(&Request::Status) {
if let Some(tasks) = resp.tasks {
self.tasks = tasks;
}
if let Some(timer) = resp.timer {
self.timer_info = timer;
}
}
}
/// Send a command to the service and update local caches from the response.
fn send(&mut self, req: &Request) {
if let Ok(resp) = Client::send(req) {
if let Some(tasks) = resp.tasks {
self.tasks = tasks;
}
if let Some(timer) = resp.timer {
self.timer_info = timer;
}
}
}
fn format_remaining(&self) -> String {
let total = self.timer_info.remaining_secs;
let mins = total / 60;
let secs = total % 60;
format!("{mins:02}:{secs:02}")
}
#[allow(clippy::cast_precision_loss)]
fn progress(&self) -> f32 {
if self.timer_info.total_secs == 0 {
return 0.0;
}
1.0 - (self.timer_info.remaining_secs as f32 / self.timer_info.total_secs as f32)
}
}
impl eframe::App for HyperfocusApp {
#[allow(clippy::too_many_lines)]
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
// Poll service while timer is running
if self.timer_info.state == TimerStatus::Running {
self.refresh();
ctx.request_repaint_after(std::time::Duration::from_millis(250));
}
// Sync selected_task with service's active task when timer is active
if matches!(
self.timer_info.state,
TimerStatus::Running | TimerStatus::Paused
) {
self.selected_task = self.timer_info.task_index;
}
egui::TopBottomPanel::top("header").show(ctx, |ui| {
ui.add_space(4.0);
ui.horizontal(|ui| {
ui.heading("Hyperfocus");
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let remaining = self.tasks.iter().filter(|t| !t.completed).count();
ui.label(format!("{remaining} task(s) remaining"));
});
});
ui.add_space(4.0);
});
egui::SidePanel::left("tasks_panel")
.resizable(true)
.default_width(240.0)
.min_width(180.0)
.show(ctx, |ui| {
ui.add_space(8.0);
ui.heading("Tasks");
ui.separator();
// Add task form
ui.horizontal(|ui| {
let name_response = ui.add(
egui::TextEdit::singleline(&mut self.new_task_name)
.hint_text("Task name")
.desired_width(100.0),
);
ui.add(
egui::TextEdit::singleline(&mut self.new_task_duration)
.hint_text("min")
.desired_width(32.0),
);
let add_clicked = ui.button("Add").clicked();
let enter_pressed =
name_response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
if (add_clicked || enter_pressed) && !self.new_task_name.trim().is_empty() {
let duration: u32 = self.new_task_duration.trim().parse().unwrap_or(25);
self.send(&Request::AddTask {
name: self.new_task_name.trim().to_string(),
duration_minutes: duration,
});
self.new_task_name.clear();
self.new_task_duration = String::from("25");
}
});
ui.add_space(4.0);
ui.separator();
ui.add_space(4.0);
// Task list
let mut to_remove: Option<usize> = None;
let mut to_toggle: Option<usize> = None;
egui::ScrollArea::vertical().show(ui, |ui| {
for (i, task) in self.tasks.iter().enumerate() {
let is_selected = self.selected_task == Some(i);
let is_active = self.timer_info.task_index == Some(i)
&& matches!(
self.timer_info.state,
TimerStatus::Running | TimerStatus::Paused
);
ui.horizontal(|ui| {
let mut completed = task.completed;
if ui.checkbox(&mut completed, "").changed() {
to_toggle = Some(i);
}
let label = if is_active {
format!("\u{25b6} {} ({}m)", task.name, task.duration_minutes)
} else {
format!("{} ({}m)", task.name, task.duration_minutes)
};
let response = ui.selectable_label(is_selected, &label);
if response.clicked()
&& (self.timer_info.state == TimerStatus::Idle
|| self.timer_info.state == TimerStatus::Finished)
{
self.selected_task = Some(i);
}
ui.with_layout(
egui::Layout::right_to_left(egui::Align::Center),
|ui| {
if ui.small_button("\u{2715}").clicked() {
to_remove = Some(i);
}
},
);
});
}
});
if let Some(i) = to_toggle {
self.send(&Request::ToggleDone { index: i });
}
if let Some(i) = to_remove {
if self.selected_task == Some(i) {
self.selected_task = None;
} else if let Some(sel) = self.selected_task
&& sel > i
{
self.selected_task = Some(sel - 1);
}
self.send(&Request::RemoveTask { index: i });
}
});
egui::CentralPanel::default().show(ctx, |ui| {
ui.add_space(16.0);
match self.selected_task {
Some(idx) if idx < self.tasks.len() => {
let task_name = self.tasks[idx].name.clone();
let task_duration = self.tasks[idx].duration_minutes;
ui.vertical_centered(|ui| {
ui.heading(&task_name);
ui.add_space(4.0);
ui.label(format!("Duration: {task_duration} minutes"));
ui.add_space(24.0);
// Timer display
ui.label(
egui::RichText::new(self.format_remaining())
.size(64.0)
.monospace(),
);
ui.add_space(8.0);
// Progress bar
let bar = egui::ProgressBar::new(self.progress()).show_percentage();
ui.add_sized([280.0, 20.0], bar);
ui.add_space(16.0);
// Controls
ui.horizontal(|ui| {
ui.add_space((ui.available_width() - 200.0).max(0.0) / 2.0);
match self.timer_info.state {
TimerStatus::Idle | TimerStatus::Finished => {
if ui
.add_sized(
[90.0, 36.0],
egui::Button::new("\u{25b6} Start"),
)
.clicked()
{
self.send(&Request::StartTimer { index: idx });
}
}
TimerStatus::Running => {
if ui
.add_sized(
[90.0, 36.0],
egui::Button::new("\u{23f8} Pause"),
)
.clicked()
{
self.send(&Request::PauseTimer);
}
if ui
.add_sized(
[90.0, 36.0],
egui::Button::new("\u{23f9} Stop"),
)
.clicked()
{
self.send(&Request::StopTimer);
}
}
TimerStatus::Paused => {
if ui
.add_sized(
[90.0, 36.0],
egui::Button::new("\u{25b6} Resume"),
)
.clicked()
{
self.send(&Request::ResumeTimer);
}
if ui
.add_sized(
[90.0, 36.0],
egui::Button::new("\u{23f9} Stop"),
)
.clicked()
{
self.send(&Request::StopTimer);
}
}
}
});
if self.timer_info.state == TimerStatus::Finished {
ui.add_space(16.0);
ui.label(
egui::RichText::new("\u{2705} Timer finished!")
.size(18.0)
.color(egui::Color32::from_rgb(80, 200, 80)),
);
let next = self
.tasks
.iter()
.enumerate()
.skip(idx + 1)
.find(|(_, t)| !t.completed)
.map(|(i, _)| i);
if let Some(next_idx) = next {
let next_name = self.tasks[next_idx].name.clone();
ui.add_space(8.0);
if ui.button(format!("Start next: {next_name}")).clicked() {
self.selected_task = Some(next_idx);
self.send(&Request::StartTimer { index: next_idx });
}
}
}
});
}
_ => {
ui.vertical_centered(|ui| {
ui.add_space(80.0);
ui.label(
egui::RichText::new("Select a task to start focusing")
.size(20.0)
.weak(),
);
ui.add_space(8.0);
ui.label("Add tasks on the left, then click one to begin.");
});
}
}
});
}
}