use super::*;
pub(crate) fn draw_dashboard(f: &mut Frame, app: &App, area: Rect) {
let theme = &app.theme;
let icons = app.icons;
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Percentage(58), Constraint::Percentage(42)])
.split(area);
draw_compact_timer_block(f, app, chunks[0]);
let today = crate::storage::today_focus_minutes(&app.data);
let goal = app.data.daily_goal_minutes.max(1);
let progress_ratio = (today as f64 / goal as f64).min(1.0);
let task_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(chunks[1]);
let task_block = dense_panel(
theme,
Line::from(Span::styled(
format!(" {} Tasks ", icons.tasks),
Style::default().fg(theme.accent),
)),
);
let pending_tasks = app.dashboard_tasks();
if pending_tasks.is_empty() {
let empty_msg = if app.queue_empty() && !app.data.tasks.is_empty() {
"All tasks done — free focus or [a] add more"
} else {
"No tasks yet — press [a] to add one"
};
let empty_list = List::new(vec![ListItem::new(Span::styled(
empty_msg,
Style::default().fg(if app.queue_empty() && !app.data.tasks.is_empty() {
theme.success
} else {
theme.dim
}),
))])
.block(task_block);
f.render_widget(empty_list, task_chunks[0]);
} else {
let visible = chunks[1].height.saturating_sub(4) as usize;
let pending: Vec<ListItem> = pending_tasks
.into_iter()
.take(visible.max(4))
.enumerate()
.map(|(idx, t)| {
let selected = idx == app.dashboard_task_selected;
let marker = match t.priority {
crate::model::Priority::High => icons.alert,
crate::model::Priority::Medium => icons.dot,
crate::model::Priority::Low => " ",
};
let active = if app.active_task == Some(t.id) {
format!("{} ", icons.task_active)
} else if selected {
format!("{} ", icons.chevron)
} else {
" ".into()
};
let status_color = task_status_color(theme, t.status);
let row_style = if selected {
Style::default()
.bg(theme.select_bg)
.fg(theme.select_fg)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(theme.text)
};
ListItem::new(Line::from(vec![
Span::styled(active, Style::default().fg(theme.accent)),
Span::styled(
format!("{} ", task_status_icon(icons, t.status)),
Style::default().fg(status_color),
),
Span::styled(format!("[{}] ", marker), Style::default().fg(theme.warning)),
Span::styled(t.title.clone(), row_style),
Span::styled(
format!(" {}m", t.estimated_minutes),
Style::default().fg(theme.dim),
),
]))
.style(row_style)
})
.collect();
let list = List::new(pending).block(task_block);
f.render_widget(list, task_chunks[0]);
}
let goal_met = app.daily_goal_met();
let remaining = goal.saturating_sub(today);
let goal_reached = progress_ratio >= 1.0;
let gauge_color = if goal_reached {
theme.accent
} else {
theme.success
};
let goal_inner = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(2), Constraint::Min(2)])
.split(task_chunks[1]);
f.render_widget(
Gauge::default()
.gauge_style(Style::default().fg(gauge_color).bg(theme.task_track))
.ratio(progress_ratio)
.label(format!(
"{} {}/{} ({}%)",
icons.target,
format_minutes(today),
format_minutes(goal),
(progress_ratio * 100.0) as u32
))
.block(dense_panel(
theme,
Line::from(Span::styled(
format!(" {} Daily goal ", icons.target),
Style::default().fg(theme.accent),
)),
)),
goal_inner[0],
);
let goal_lines = vec![
Line::from(Span::styled(
if goal_met {
format!("{} Goal complete!", icons.check)
} else {
format!(
"{} {} remaining today",
icons.dot,
format_minutes(remaining)
)
},
Style::default().fg(if goal_met { theme.success } else { theme.text }),
)),
Line::from(vec![
Span::styled(format!("{} ", icons.fire), Style::default().fg(theme.info)),
Span::styled(
format!(
"{}d · {}d goal · {} open",
app.data.streak_days,
app.data.goal_streak_days,
crate::storage::pending_tasks(&app.data).count()
),
Style::default().fg(theme.dim),
),
]),
Line::from(vec![
Span::styled(format!("{} ", icons.chart), Style::default().fg(theme.dim)),
Span::styled(
format!(
"{} all-time focus",
format_minutes(app.data.total_focus_minutes)
),
Style::default().fg(theme.dim),
),
]),
];
f.render_widget(
Paragraph::new(goal_lines).block(dense_panel(
theme,
Line::from(Span::styled(
format!(" {} Today ", icons.stats),
Style::default().fg(theme.accent),
)),
)),
goal_inner[1],
);
}
pub(crate) fn draw_compact_timer_block(f: &mut Frame, app: &App, area: Rect) {
let theme = &app.theme;
let t = &app.timer;
let mc = mode_color(theme, t.mode);
let is_finished = t.state == crate::model::TimerState::Finished;
let border_color = if is_finished {
theme.success
} else {
theme.panel_border
};
let title_suffix = if is_finished {
format!(" {} DONE ", app.icons.check)
} else {
String::new()
};
let outer = timer_panel(
theme,
Line::from(Span::styled(
format!(" {} {}", t.mode.label(), title_suffix),
Style::default()
.fg(if is_finished { theme.success } else { mc })
.add_modifier(Modifier::BOLD),
)),
border_color,
);
f.render_widget(outer, area);
let inner = Rect {
x: area.x + 1,
y: area.y + 1,
width: area.width.saturating_sub(2),
height: area.height.saturating_sub(2),
};
let on_break = is_break_mode(t.mode);
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(if on_break {
[
Constraint::Min(5),
Constraint::Length(2),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(2),
]
} else {
[
Constraint::Min(5),
Constraint::Length(2),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
]
})
.split(inner);
let cycle = t.config.long_break_every.max(1);
let style = theme.scene_style(mc);
let options = DashboardSceneOptions {
task_progress: app.active_task_progress(),
pending_tasks: app.pending_task_count(),
active_task_index: app.active_task_pending_index(),
sessions_done: t.completed_focus_sessions % cycle,
sessions_total: cycle,
layout: crate::canvas_timer::SceneLayout::Dashboard,
};
draw_dashboard_canvas(f, layout[0], t, &style, &options);
let (main_time, tenths, _) = format_time_stack(t);
let today_logged = crate::storage::today_focus_minutes(&app.data);
f.render_widget(
Paragraph::new(vec![
Line::from(vec![
Span::styled(
main_time,
Style::default().fg(theme.text).add_modifier(Modifier::BOLD),
),
Span::styled(tenths, Style::default().fg(theme.dim)),
]),
Line::from(Span::styled(
format!(
"{} logged today",
super::widgets::format_minutes(today_logged)
),
Style::default().fg(theme.dim),
)),
])
.alignment(Alignment::Center),
layout[1],
);
draw_timer_footer(f, app, &layout[2..], mc);
}
pub(crate) fn draw_timer_footer(f: &mut Frame, app: &App, areas: &[Rect], mc: Color) {
let theme = &app.theme;
let t = &app.timer;
let cycle = t.config.long_break_every.max(1);
let done_in_cycle = t.completed_focus_sessions % cycle;
let in_focus = t.mode == TimerMode::Focus
&& matches!(
t.state,
crate::model::TimerState::Running | crate::model::TimerState::Paused
);
let dots = session_dots(done_in_cycle, cycle, in_focus);
f.render_widget(
Paragraph::new(Line::from(vec![
Span::styled(dots, Style::default().fg(mc)),
Span::styled(
format!(" {} ", t.cycle_label()),
Style::default().fg(theme.dim),
),
Span::styled(
format!("{}% left", ((1.0 - t.progress()) * 100.0) as u32),
Style::default().fg(theme.dim),
),
]))
.alignment(Alignment::Center),
areas[0],
);
if let Some(mut spans) = active_task_spans(app, theme) {
if let Some(id) = app.active_task {
if let Some(task) = app.data.tasks.iter().find(|t| t.id == id) {
let left = crate::storage::sessions_remaining_hint(task, app.data.focus_minutes);
if left > 0 {
spans.push(Span::styled(
format!(" ~{} left", left),
Style::default().fg(theme.dim),
));
}
f.render_widget(
Paragraph::new(Line::from(spans)).alignment(Alignment::Center),
areas[1],
);
}
}
} else if areas.len() > 1 {
let msg = if app.queue_empty() {
"All tasks done — free focus (general sessions)"
} else {
"No active task — Tasks tab, Space to set one"
};
f.render_widget(
Paragraph::new(Span::styled(msg, Style::default().fg(theme.dim)))
.alignment(Alignment::Center),
areas[1],
);
}
if areas.len() > 2 {
if is_break_mode(t.mode) {
draw_break_tip(f, areas[2], t, mc, theme.text, theme.dim, app.icons.heart);
} else {
let state_label = match t.state {
crate::model::TimerState::Idle => "ready",
crate::model::TimerState::Running => "focusing",
crate::model::TimerState::Paused => "paused",
crate::model::TimerState::Finished => "complete",
};
f.render_widget(
Paragraph::new(Span::styled(state_label, Style::default().fg(mc)))
.alignment(Alignment::Center),
areas[2],
);
}
}
}
pub(crate) fn mode_color(theme: &crate::app::Theme, mode: TimerMode) -> Color {
match mode {
TimerMode::Focus => theme.accent,
TimerMode::ShortBreak => theme.success,
TimerMode::LongBreak => theme.warning,
TimerMode::Custom => theme.info,
}
}