Add paid pomodoro breaks
Some jobs pay for breaks, so a pomodoro break shouldn't take time off the workday. By default (new setting `paid_breaks`, in [reminders]), b on the Tracker, zb anywhere or :break starts a pomodoro break WITHOUT pausing the shift: the break stays in your worked time and the data file isn't touched. b (or s) brings you back to focus. With the setting off, b pauses the shift like p. p and e always stop the clock. For the rhythm, a paid break still counts as a break: reminders::PaidBreaks keeps them in memory and pomodoro() cuts them out of the focus stretches, the same way as a pause. Stopping the shift ends a paid break, and closing the work day forgets them. The Tracker shows "on a paid break" and the paid break's progress, and the nudges name the right key (b or p/s). Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
fe5e2d9a7f
commit
49e14b820a
8 files changed
+233
-21
No files matched your search
@@ -41,6 +41,7 @@ in the settings.
|
||||
| `<Space>c` | copy in another format, with a preview |
|
||||
| `gh` `gd` `gi` `gs` | Tracker, Days, Insights, Settings (also `gt`, `Tab`) |
|
||||
| `u` `Ctrl-r` | undo, redo: mistakes cost nothing |
|
||||
| `b` (`zb`) | take a pomodoro break, and come back from it |
|
||||
| `zz` | snooze the break suggestion |
|
||||
| `:` | command line: `:pause 17:30`, `:export csv lastmonth`, `:set time_format=12h` |
|
||||
| `?` | every key, for the screen you're on |
|
||||
@@ -60,6 +61,10 @@ settings, or with `<Space>w`.
|
||||
- Times you type (`22:30`, `2230`, `22h30`, `10:30pm`, `10p`) are read in
|
||||
both 12h and 24h styles. An end time before the start time means the next
|
||||
day.
|
||||
- Pomodoro breaks (`b` on the Tracker, `zb` anywhere) are **paid** by
|
||||
default: the shift keeps running, so the break stays in your worked
|
||||
time. Turn off `paid_breaks` in the settings to make `b` pause the shift
|
||||
instead. `p` and `e` always stop the clock (lunch, end of day).
|
||||
- To add work to a day you didn't track, press `o` and change the **Work
|
||||
day** field: `2026-09-14`, `09-14`, `yesterday`, `-3` (3 days ago) or a
|
||||
weekday like `mon` all work.
|
||||
|
||||
@@ -41,6 +41,9 @@ pub enum Action {
|
||||
since: Timestamp,
|
||||
stopped: Option<Timestamp>,
|
||||
},
|
||||
/// Start a pomodoro break, or come back from one. With paid breaks the
|
||||
/// shift keeps running; without, it's the same as Pause / Start.
|
||||
TakeBreak,
|
||||
/// Silence the break suggestion for a while.
|
||||
Snooze,
|
||||
|
||||
|
||||
+72
-7
@@ -26,7 +26,7 @@ use crate::action::Action;
|
||||
use crate::domain::{DomainError, Journal, ShiftState, StartOutcome, WorkDay};
|
||||
use crate::export::clipboard::Clipboard;
|
||||
use crate::export::{self, Report, timesheet};
|
||||
use crate::reminders::{self, Phase};
|
||||
use crate::reminders::{self, PaidBreaks, Phase};
|
||||
use crate::settings::{FIELDS, Settings};
|
||||
use crate::storage::journal_file::{JournalStore, OutsideChange};
|
||||
use crate::storage::paths::Paths;
|
||||
@@ -77,6 +77,8 @@ struct Model {
|
||||
paths: Paths,
|
||||
export_folder: PathBuf,
|
||||
nudges: Nudges,
|
||||
/// Pomodoro breaks taken without pausing the shift.
|
||||
paid_breaks: PaidBreaks,
|
||||
}
|
||||
|
||||
/// What the reminders already said, so each is said only once.
|
||||
@@ -139,6 +141,7 @@ impl App {
|
||||
store,
|
||||
paths,
|
||||
nudges: Nudges::default(),
|
||||
paid_breaks: PaidBreaks::default(),
|
||||
};
|
||||
let mut app = App {
|
||||
model,
|
||||
@@ -356,6 +359,7 @@ impl App {
|
||||
Command::PauseAt => Some(popup(TimePrompt::new(Purpose::Pause))),
|
||||
Command::EndDayAt => Some(popup(TimePrompt::new(Purpose::EndDay))),
|
||||
Command::Snooze => Some(Action::Snooze),
|
||||
Command::TakeBreak => Some(Action::TakeBreak),
|
||||
Command::Undo => Some(Action::Undo),
|
||||
Command::Redo => Some(Action::Redo),
|
||||
Command::Help => Some(Action::ShowHelp),
|
||||
@@ -397,6 +401,12 @@ impl App {
|
||||
}
|
||||
Action::Notify(text) => self.say(Tone::Info, text),
|
||||
|
||||
// During a paid break the shift is still running: Start means
|
||||
// "back to focus".
|
||||
Action::Start if self.model.on_paid_break() => {
|
||||
self.model.paid_breaks.finish(self.model.clock.now().timestamp());
|
||||
self.say(Tone::Good, "Back to focus. The break stayed in your worked time.");
|
||||
}
|
||||
Action::Start => {
|
||||
let now = self.model.clock.punch();
|
||||
let new_day_after = self.model.settings.new_day_after_break;
|
||||
@@ -468,6 +478,25 @@ impl App {
|
||||
};
|
||||
self.say(Tone::Good, text);
|
||||
}
|
||||
Action::TakeBreak => {
|
||||
let state = self.model.journal.state();
|
||||
// Without paid breaks, a pomodoro break is a plain pause.
|
||||
if !self.model.settings.paid_breaks || !state.is_working() {
|
||||
return match state {
|
||||
ShiftState::Working { .. } => self.apply(Action::Pause),
|
||||
ShiftState::OnBreak { .. } => self.apply(Action::Start),
|
||||
ShiftState::Idle => Err("You're not working, so there's no break to take".into()),
|
||||
};
|
||||
}
|
||||
let now = self.model.clock.now().timestamp();
|
||||
if self.model.on_paid_break() {
|
||||
self.model.paid_breaks.finish(now);
|
||||
self.say(Tone::Good, "Back to focus. The break stayed in your worked time.");
|
||||
} else {
|
||||
self.model.paid_breaks.start(now);
|
||||
self.say(Tone::Good, "Paid break: the shift keeps running. b when you're back.");
|
||||
}
|
||||
}
|
||||
Action::Snooze => {
|
||||
let until = self.model.clock.now().timestamp() + self.model.settings.snooze;
|
||||
self.model.nudges.snoozed_until = Some(until);
|
||||
@@ -607,7 +636,7 @@ impl App {
|
||||
self.model.nudges.break_suggested = None;
|
||||
}
|
||||
|
||||
let pomodoro = reminders::pomodoro(&self.model.journal, now, &self.model.settings.pomodoro());
|
||||
let pomodoro = self.model.pomodoro(now);
|
||||
let nudge = match pomodoro.phase {
|
||||
Phase::Focus { started, focused, .. }
|
||||
if pomodoro.break_due()
|
||||
@@ -615,13 +644,16 @@ impl App {
|
||||
&& self.model.nudges.break_suggested != Some(started) =>
|
||||
{
|
||||
self.model.nudges.break_suggested = Some(started);
|
||||
Some(format!("Break time: {} of focus. p pauses, zz snoozes.", fmt.duration(focused)))
|
||||
let take_it =
|
||||
if self.model.settings.paid_breaks { "b takes a paid break" } else { "p pauses" };
|
||||
Some(format!("Break time: {} of focus. {take_it}, zz snoozes.", fmt.duration(focused)))
|
||||
}
|
||||
Phase::Break { started, goal, .. }
|
||||
Phase::Break { started, goal, paid, .. }
|
||||
if pomodoro.break_over() && self.model.nudges.break_over != Some(started) =>
|
||||
{
|
||||
self.model.nudges.break_over = Some(started);
|
||||
Some(format!("Your {} break is over. s when you're ready.", fmt.duration(goal)))
|
||||
let back = if paid { "b" } else { "s" };
|
||||
Some(format!("Your {} break is over. {back} when you're ready.", fmt.duration(goal)))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
@@ -659,12 +691,22 @@ impl Model {
|
||||
now,
|
||||
fmt: self.settings.formatter(),
|
||||
theme: &self.theme,
|
||||
pomodoro: reminders::pomodoro(&self.journal, now.timestamp(), &self.settings.pomodoro()),
|
||||
pomodoro: self.pomodoro(now.timestamp()),
|
||||
snoozed_until: self.nudges.snoozed_until,
|
||||
export_folder: &self.export_folder,
|
||||
}
|
||||
}
|
||||
|
||||
/// Where you are in the pomodoro rhythm.
|
||||
fn pomodoro(&self, now: Timestamp) -> reminders::Pomodoro {
|
||||
reminders::pomodoro(&self.journal, &self.paid_breaks, now, &self.settings.pomodoro())
|
||||
}
|
||||
|
||||
/// On a paid break: the shift runs, but you're taking a pomodoro break.
|
||||
fn on_paid_break(&self) -> bool {
|
||||
self.journal.state().is_working() && self.paid_breaks.current().is_some()
|
||||
}
|
||||
|
||||
/// Changes the journal with `edit`, keeping the previous version for
|
||||
/// undo, then saves. `edit` works on a copy: if it fails, or saving
|
||||
/// fails, nothing changes at all.
|
||||
@@ -681,6 +723,12 @@ impl Model {
|
||||
self.undo.remove(0);
|
||||
}
|
||||
self.redo.clear();
|
||||
// Stopping the shift ends a paid break; closing the day forgets them.
|
||||
match self.journal.state() {
|
||||
ShiftState::Idle => self.paid_breaks.clear(),
|
||||
ShiftState::OnBreak { .. } => self.paid_breaks.finish(self.clock.now().timestamp()),
|
||||
ShiftState::Working { .. } => {}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
@@ -848,8 +896,9 @@ mod tests {
|
||||
}
|
||||
|
||||
/// Everything to look at: each screen, view, popup and hint.
|
||||
const TOUR: [&str; 16] = [
|
||||
const TOUR: [&str; 17] = [
|
||||
"",
|
||||
"b",
|
||||
"<Space>",
|
||||
"<Esc>gd",
|
||||
"l",
|
||||
@@ -911,6 +960,22 @@ mod tests {
|
||||
assert_eq!(dates_of_segments_at(&app, other_day, 18), vec![other_day]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paid_breaks_keep_the_shift_running() {
|
||||
let mut app = sample_app("paid-break");
|
||||
let journal_before = app.model.journal.clone();
|
||||
press(&mut app, "b");
|
||||
assert!(app.model.on_paid_break());
|
||||
assert_eq!(app.model.journal, journal_before, "the worked time isn't touched");
|
||||
press(&mut app, "s");
|
||||
assert!(!app.model.on_paid_break(), "s means back to focus");
|
||||
assert!(app.model.journal.state().is_working());
|
||||
|
||||
// With paid breaks off, a pomodoro break is a plain pause.
|
||||
press(&mut app, ":set nopaid_breaks<CR>b");
|
||||
assert!(!app.model.journal.state().is_working());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editing_through_the_keys() {
|
||||
let mut app = sample_app("editing");
|
||||
|
||||
+102
-8
@@ -8,11 +8,52 @@
|
||||
//! interruptions (shorter than the short break) don't reset it.
|
||||
//! - When a stretch reaches the focus length, a break is due: a short one,
|
||||
//! or a long one after every `long_break_every` completed stretches.
|
||||
//! - A break is either a pause (the shift stops) or a **paid break**
|
||||
//! ([`PaidBreaks`]): the shift keeps running, so the break stays in your
|
||||
//! worked time, but for the rhythm it counts as a break all the same.
|
||||
|
||||
use jiff::{SignedDuration, Timestamp};
|
||||
|
||||
use crate::domain::{Journal, ShiftState};
|
||||
|
||||
/// Pomodoro breaks taken without pausing the shift. They're only kept in
|
||||
/// memory: your worked time doesn't change, so the data file doesn't either.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct PaidBreaks {
|
||||
/// Breaks that are over, as (start, end).
|
||||
taken: Vec<(Timestamp, Timestamp)>,
|
||||
/// When the break in progress started.
|
||||
current: Option<Timestamp>,
|
||||
}
|
||||
|
||||
impl PaidBreaks {
|
||||
pub fn current(&self) -> Option<Timestamp> {
|
||||
self.current
|
||||
}
|
||||
|
||||
pub fn start(&mut self, at: Timestamp) {
|
||||
self.current.get_or_insert(at);
|
||||
}
|
||||
|
||||
/// Ends the break in progress, if there is one.
|
||||
pub fn finish(&mut self, at: Timestamp) {
|
||||
if let Some(start) = self.current.take()
|
||||
&& at > start
|
||||
{
|
||||
self.taken.push((start, at));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
*self = PaidBreaks::default();
|
||||
}
|
||||
|
||||
/// Every break, the one in progress lasting until `now`.
|
||||
fn spans(&self, now: Timestamp) -> impl Iterator<Item = (Timestamp, Timestamp)> + '_ {
|
||||
self.taken.iter().copied().chain(self.current.map(|start| (start, now.max(start))))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct PomodoroSettings {
|
||||
pub enabled: bool,
|
||||
@@ -30,7 +71,8 @@ pub enum Phase {
|
||||
/// Working. `started` is when the current focus stretch began.
|
||||
Focus { started: Timestamp, focused: SignedDuration, goal: SignedDuration },
|
||||
/// On a break that began at `started`. `goal` is the suggested length.
|
||||
Break { started: Timestamp, taken: SignedDuration, goal: SignedDuration, long: bool },
|
||||
/// `paid` means the shift is still running (a paid break).
|
||||
Break { started: Timestamp, taken: SignedDuration, goal: SignedDuration, long: bool, paid: bool },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -62,7 +104,12 @@ impl Pomodoro {
|
||||
}
|
||||
|
||||
/// The rhythm of the open work day, as of `now`.
|
||||
pub fn pomodoro(journal: &Journal, now: Timestamp, settings: &PomodoroSettings) -> Pomodoro {
|
||||
pub fn pomodoro(
|
||||
journal: &Journal,
|
||||
paid_breaks: &PaidBreaks,
|
||||
now: Timestamp,
|
||||
settings: &PomodoroSettings,
|
||||
) -> Pomodoro {
|
||||
let mut result =
|
||||
Pomodoro { phase: Phase::Off, completed: 0, long_break_every: settings.long_break_every };
|
||||
let Some(open_day) = journal.state().open_day() else { return result };
|
||||
@@ -70,14 +117,20 @@ pub fn pomodoro(journal: &Journal, now: Timestamp, settings: &PomodoroSettings)
|
||||
return result;
|
||||
}
|
||||
|
||||
// The open day's segments, with the running session up to now.
|
||||
// The open day's segments, with the running session up to now...
|
||||
let mut pieces: Vec<(Timestamp, Timestamp)> = journal
|
||||
.day(open_day)
|
||||
.map(|day| day.segments().iter().map(|segment| (segment.start(), segment.end())).collect())
|
||||
.unwrap_or_default();
|
||||
let working = journal.state().is_working();
|
||||
if let ShiftState::Working { since, .. } = journal.state() {
|
||||
pieces.push((since, now.max(since)));
|
||||
}
|
||||
// ...with paid breaks cut out: for the rhythm, they're breaks too.
|
||||
let on_paid_break = working && paid_breaks.current().is_some();
|
||||
for (break_start, break_end) in paid_breaks.spans(now) {
|
||||
pieces = pieces.into_iter().flat_map(|piece| cut(piece, break_start, break_end)).collect();
|
||||
}
|
||||
|
||||
// Join pieces into focus stretches: a gap shorter than a short break
|
||||
// is an interruption, not a break.
|
||||
@@ -94,7 +147,7 @@ pub fn pomodoro(journal: &Journal, now: Timestamp, settings: &PomodoroSettings)
|
||||
result.completed = stretches.iter().filter(|stretch| stretch.worked >= settings.focus).count() as u32;
|
||||
let Some(last) = stretches.last() else { return result };
|
||||
|
||||
result.phase = if journal.state().is_working() {
|
||||
result.phase = if working && !on_paid_break {
|
||||
Phase::Focus { started: last.start, focused: last.worked, goal: settings.focus }
|
||||
} else {
|
||||
// The break that follows a completed stretch is long every
|
||||
@@ -108,11 +161,24 @@ pub fn pomodoro(journal: &Journal, now: Timestamp, settings: &PomodoroSettings)
|
||||
taken: now.duration_since(last.end),
|
||||
goal: if long { settings.long_break } else { settings.short_break },
|
||||
long,
|
||||
paid: on_paid_break,
|
||||
}
|
||||
};
|
||||
result
|
||||
}
|
||||
|
||||
/// What's left of the span `piece` once `start..end` is taken out of it:
|
||||
/// nothing, the piece itself, or the parts before and after.
|
||||
fn cut(piece: (Timestamp, Timestamp), start: Timestamp, end: Timestamp) -> Vec<(Timestamp, Timestamp)> {
|
||||
let (piece_start, piece_end) = piece;
|
||||
if end <= piece_start || start >= piece_end {
|
||||
return vec![piece];
|
||||
}
|
||||
let before = (piece_start, start.min(piece_end));
|
||||
let after = (end.max(piece_start), piece_end);
|
||||
[before, after].into_iter().filter(|(from, to)| to > from).collect()
|
||||
}
|
||||
|
||||
struct Stretch {
|
||||
start: Timestamp,
|
||||
end: Timestamp,
|
||||
@@ -145,7 +211,7 @@ mod tests {
|
||||
journal.start(&at(9, 0), NEW_DAY_AFTER).unwrap();
|
||||
journal.pause(at(9, 20).timestamp()).unwrap();
|
||||
journal.start(&at(9, 22), NEW_DAY_AFTER).unwrap(); // a 2 minute interruption
|
||||
let rhythm = pomodoro(&journal, at(9, 27).timestamp(), &SETTINGS);
|
||||
let rhythm = pomodoro(&journal, &PaidBreaks::default(), at(9, 27).timestamp(), &SETTINGS);
|
||||
assert!(rhythm.break_due());
|
||||
assert_eq!(
|
||||
rhythm.phase,
|
||||
@@ -162,14 +228,42 @@ mod tests {
|
||||
let mut journal = Journal::default();
|
||||
journal.start(&at(9, 0), NEW_DAY_AFTER).unwrap();
|
||||
journal.pause(at(9, 25).timestamp()).unwrap();
|
||||
let first_break = pomodoro(&journal, at(9, 26).timestamp(), &SETTINGS);
|
||||
assert!(matches!(first_break.phase, Phase::Break { long: false, .. }));
|
||||
let none = PaidBreaks::default();
|
||||
let first_break = pomodoro(&journal, &none, at(9, 26).timestamp(), &SETTINGS);
|
||||
assert!(matches!(first_break.phase, Phase::Break { long: false, paid: false, .. }));
|
||||
|
||||
journal.start(&at(9, 30), NEW_DAY_AFTER).unwrap();
|
||||
journal.pause(at(9, 55).timestamp()).unwrap();
|
||||
let second_break = pomodoro(&journal, at(10, 15).timestamp(), &SETTINGS);
|
||||
let second_break = pomodoro(&journal, &none, at(10, 15).timestamp(), &SETTINGS);
|
||||
assert_eq!(second_break.completed, 2);
|
||||
assert!(matches!(second_break.phase, Phase::Break { long: true, .. }));
|
||||
assert!(second_break.break_over());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_paid_break_is_a_break_while_the_shift_keeps_running() {
|
||||
let mut journal = Journal::default();
|
||||
journal.start(&at(9, 0), NEW_DAY_AFTER).unwrap();
|
||||
let mut paid = PaidBreaks::default();
|
||||
paid.start(at(9, 25).timestamp());
|
||||
|
||||
let on_break = pomodoro(&journal, &paid, at(9, 28).timestamp(), &SETTINGS);
|
||||
assert_eq!(
|
||||
on_break.phase,
|
||||
Phase::Break {
|
||||
started: at(9, 25).timestamp(),
|
||||
taken: SignedDuration::from_mins(3),
|
||||
goal: SETTINGS.short_break,
|
||||
long: false,
|
||||
paid: true
|
||||
}
|
||||
);
|
||||
|
||||
paid.finish(at(9, 30).timestamp());
|
||||
let back = pomodoro(&journal, &paid, at(9, 40).timestamp(), &SETTINGS);
|
||||
assert_eq!(back.completed, 1);
|
||||
assert!(matches!(back.phase, Phase::Focus { started, .. } if started == at(9, 30).timestamp()));
|
||||
// The shift itself never stopped: 40 minutes worked.
|
||||
assert_eq!(journal.running_for(at(9, 40).timestamp()), SignedDuration::from_mins(40));
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,7 @@ pub struct Settings {
|
||||
pub export_folder: String,
|
||||
// Reminders
|
||||
pub pomodoro: bool,
|
||||
pub paid_breaks: bool,
|
||||
pub focus_length: SignedDuration,
|
||||
pub short_break: SignedDuration,
|
||||
pub long_break: SignedDuration,
|
||||
@@ -97,6 +98,7 @@ impl Default for Settings {
|
||||
export_format: ExportFormat::Csv,
|
||||
export_folder: String::new(),
|
||||
pomodoro: true,
|
||||
paid_breaks: true,
|
||||
focus_length: SignedDuration::from_mins(25),
|
||||
short_break: SignedDuration::from_mins(5),
|
||||
long_break: SignedDuration::from_mins(15),
|
||||
@@ -538,6 +540,19 @@ pub static FIELDS: &[Field] = &[
|
||||
Ok(())
|
||||
},
|
||||
},
|
||||
Field {
|
||||
section: "reminders",
|
||||
key: "paid_breaks",
|
||||
label: "Paid breaks",
|
||||
help: "on: b takes a pomodoro break without pausing the shift, so breaks stay in your worked time (for jobs that pay breaks). off: b pauses the shift like p. Either way, p and e always stop the clock.",
|
||||
expert: false,
|
||||
kind: Kind::Toggle,
|
||||
get: |s| s.paid_breaks.to_string(),
|
||||
set: |s, text| {
|
||||
s.paid_breaks = toggle(text)?;
|
||||
Ok(())
|
||||
},
|
||||
},
|
||||
Field {
|
||||
section: "reminders",
|
||||
key: "focus_length",
|
||||
|
||||
@@ -48,6 +48,7 @@ pub enum Command {
|
||||
EndDayAt,
|
||||
StartEarlier,
|
||||
StartLater,
|
||||
TakeBreak,
|
||||
Snooze,
|
||||
// Recorded work
|
||||
Edit,
|
||||
@@ -124,6 +125,7 @@ pub const GLOBAL: &[Binding] = &[
|
||||
bind("<Tab>", C::NextScreen, "next screen"),
|
||||
bind("<S-Tab>", C::PreviousScreen, "previous screen"),
|
||||
bind("zz", C::Snooze, "snooze the break reminder"),
|
||||
bind("zb", C::TakeBreak, "take a pomodoro break / back from it"),
|
||||
bind("<Space><Space>", C::Toggle, "start / pause"),
|
||||
bind("<Space>s", C::StartAt, "start at an earlier time..."),
|
||||
bind("<Space>p", C::PauseAt, "pause at an earlier time..."),
|
||||
@@ -152,6 +154,7 @@ pub const LIST: &[Binding] = &[
|
||||
];
|
||||
|
||||
pub const TRACKER: &[Binding] = &[
|
||||
bind("b", C::TakeBreak, "take a pomodoro break / back from it"),
|
||||
bind("<CR>", C::Edit, "fix the session in progress"),
|
||||
bind("i", C::Edit, "fix the session in progress"),
|
||||
bind("o", C::AddAfter, "add forgotten work to today"),
|
||||
|
||||
@@ -35,6 +35,7 @@ pub const COMMANDS: &[(&str, &str)] = &[
|
||||
(":set name :set noname", "turn an on/off setting on or off (name! flips it)"),
|
||||
(":undo :redo", "undo / redo"),
|
||||
(":snooze", "snooze the break reminder"),
|
||||
(":break", "take a pomodoro break / back from it"),
|
||||
(":tracker :days :insights :settings", "go to a screen"),
|
||||
(":help", "all keys"),
|
||||
(":q", "quit"),
|
||||
@@ -125,6 +126,7 @@ pub fn parse(text: &str, ctx: &Context) -> Result<Action, String> {
|
||||
"undo" | "u" => Ok(Action::Undo),
|
||||
"redo" => Ok(Action::Redo),
|
||||
"snooze" => Ok(Action::Snooze),
|
||||
"break" => Ok(Action::TakeBreak),
|
||||
"tracker" => Ok(Action::GoTo(Tab::Tracker)),
|
||||
"days" => Ok(Action::GoTo(Tab::Days)),
|
||||
"insights" => Ok(Action::GoTo(Tab::Insights)),
|
||||
|
||||
@@ -82,7 +82,16 @@ impl Tracker {
|
||||
let state = ctx.journal.state();
|
||||
let (symbol, text) = match state {
|
||||
ShiftState::Working { day, since } => {
|
||||
("●", format!("Working since {} · work day {}", ctx.time(since), ctx.fmt.date_short(day)))
|
||||
let paid_break = matches!(ctx.pomodoro.phase, Phase::Break { paid: true, .. });
|
||||
let status = if paid_break { " · on a paid break" } else { "" };
|
||||
(
|
||||
"●",
|
||||
format!(
|
||||
"Working since {}{status} · work day {}",
|
||||
ctx.time(since),
|
||||
ctx.fmt.date_short(day)
|
||||
),
|
||||
)
|
||||
}
|
||||
ShiftState::OnBreak { day } => {
|
||||
("◐", format!("On a break · work day {}", ctx.fmt.date_short(day)))
|
||||
@@ -181,21 +190,37 @@ impl Tracker {
|
||||
),
|
||||
Style::new().fg(ctx.theme.warning).add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(" p pause · zz snooze", ctx.theme.muted()),
|
||||
Span::styled(
|
||||
if settings.paid_breaks {
|
||||
" b break · zz snooze"
|
||||
} else {
|
||||
" p pause · zz snooze"
|
||||
},
|
||||
ctx.theme.muted(),
|
||||
),
|
||||
])
|
||||
}
|
||||
},
|
||||
Phase::Break { taken, goal, long, .. } if taken < goal => Line::from(vec![
|
||||
Span::styled(if long { "Long break " } else { "Break " }, ctx.theme.muted()),
|
||||
Phase::Break { taken, goal, long, paid, .. } if taken < goal => Line::from(vec![
|
||||
Span::styled(
|
||||
match (long, paid) {
|
||||
(true, true) => "Long paid break ",
|
||||
(true, false) => "Long break ",
|
||||
(false, true) => "Paid break ",
|
||||
(false, false) => "Break ",
|
||||
},
|
||||
ctx.theme.muted(),
|
||||
),
|
||||
Span::raw(format!("{} / {} ", fmt.duration(taken), fmt.duration(goal))),
|
||||
Span::styled(dots, ctx.theme.accent()),
|
||||
Span::styled(if paid { " b back" } else { "" }, ctx.theme.muted()),
|
||||
]),
|
||||
Phase::Break { goal, .. } => Line::from(vec![
|
||||
Phase::Break { goal, paid, .. } => Line::from(vec![
|
||||
Span::styled(
|
||||
format!("Break's over ({}). Back when you're ready.", fmt.duration(goal)),
|
||||
ctx.theme.title(),
|
||||
),
|
||||
Span::styled(" s start", ctx.theme.muted()),
|
||||
Span::styled(if paid { " b back" } else { " s start" }, ctx.theme.muted()),
|
||||
]),
|
||||
};
|
||||
frame.render_widget(Paragraph::new(line).centered(), area);
|
||||
|
||||
Reference in new issue
Block a user