Skip to content

Commit 6f5f7f0

Browse files
authored
Merge pull request #113 from sullyj3/timer-messages
feat: Timer messages
2 parents 3964cb8 + c0b2352 commit 6f5f7f0

File tree

9 files changed

+120
-25
lines changed

9 files changed

+120
-25
lines changed

src/client.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,8 @@ pub fn main(cli_cmd: cli::ClientCommand) -> io::Result<()> {
9797

9898
// TODO: support passing multiple IDs in protocol
9999
let result: ClientResult<()> = match cli_cmd {
100-
cli::ClientCommand::Start(StartArgs { durations }) => {
101-
start(&mut conn, durations).inspect_err(|err| eprintln!("{err}"))
100+
cli::ClientCommand::Start(StartArgs { durations, message }) => {
101+
start(&mut conn, durations, message).inspect_err(|err| eprintln!("{err}"))
102102
}
103103
cli::ClientCommand::NextDue => next_due(&mut conn).inspect_err(|err| eprintln!("{err}")),
104104
cli::ClientCommand::Ls => ls(&mut conn),
@@ -119,9 +119,13 @@ pub fn main(cli_cmd: cli::ClientCommand) -> io::Result<()> {
119119
// Command handler functions
120120
/////////////////////////////////////////////////////////////////////////////////////////
121121

122-
fn start(conn: &mut DaemonConnection, durations: Vec<Duration>) -> ClientResult<()> {
122+
fn start(
123+
conn: &mut DaemonConnection,
124+
durations: Vec<Duration>,
125+
message: Option<String>,
126+
) -> ClientResult<()> {
123127
let dur: Duration = durations.iter().sum();
124-
let StartTimerResponse::Ok { id } = conn.add_timer(dur)?;
128+
let StartTimerResponse::Ok { id } = conn.add_timer(dur, message)?;
125129

126130
let dur_string = dur.format_colon_separated();
127131
println!("Timer {id} created for {dur_string}.");

src/client/daemon_connection.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,12 @@ impl DaemonConnection {
2121
Ok(Self { read, write })
2222
}
2323

24-
pub fn add_timer(&mut self, duration: Duration) -> io::Result<StartTimerResponse> {
25-
self.send(Command::StartTimer { duration })?;
24+
pub fn add_timer(
25+
&mut self,
26+
duration: Duration,
27+
message: Option<String>,
28+
) -> io::Result<StartTimerResponse> {
29+
self.send(Command::StartTimer { duration, message })?;
2630
self.recv::<StartTimerResponse>()
2731
}
2832

src/client/ui.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ struct TableConfig<'a> {
1212
status_column_width: usize,
1313
id_column_width: usize,
1414
remaining_column_width: usize,
15+
message_column_width: usize,
1516
gap: &'a str,
1617
}
1718

19+
// TODO this needs a complete rework
1820
pub fn ls(mut timers: Vec<TimerInfo>) -> impl Display {
1921
if timers.len() == 0 {
2022
return "There are currently no timers.\n".to_owned();
@@ -61,25 +63,38 @@ pub fn ls(mut timers: Vec<TimerInfo>) -> impl Display {
6163
widest_remaining_duration.max(remaining_header.len())
6264
};
6365

66+
let message_header = "Message";
67+
let message_column_width = {
68+
let widest_message = timers
69+
.iter()
70+
.map(|ti| ti.message.as_ref().map(|s| s.len()).unwrap_or(0))
71+
.max()
72+
.expect("timers.len() != 0");
73+
widest_message.max(message_header.len())
74+
};
75+
6476
let gap = " ";
6577
let table_config = TableConfig {
6678
status_column_width,
6779
id_column_width,
6880
remaining_column_width,
81+
message_column_width,
6982
gap,
7083
};
7184

7285
// Stylize doesn't seem to support formatting with padding,
7386
// so we have to pre-pad
7487
let id_header_padded = format!("{:<id_column_width$}", id_header);
7588
let remaining_header_padded = format!("{:<remaining_column_width$}", remaining_header);
89+
let message_header_padded = format!("{:<message_column_width$}", message_header);
7690

7791
// Header
7892
write!(
7993
output,
80-
"{status_header}{gap}{}{gap}{}\n",
94+
"{status_header}{gap}{}{gap}{}{gap}{}\n",
8195
id_header_padded.underlined(),
8296
remaining_header_padded.underlined(),
97+
message_header_padded.underlined(),
8398
)
8499
.unwrap();
85100

@@ -116,10 +131,12 @@ fn timers_table_row(output: &mut impl Write, timer_info: &TimerInfo, table_confi
116131
status_column_width,
117132
id_column_width,
118133
remaining_column_width,
134+
message_column_width,
119135
gap,
120136
} = table_config;
137+
let message: &str = timer_info.message.as_deref().unwrap_or("");
121138
write!(output,
122-
"{play_pause:>status_column_width$}{gap}{:>id_column_width$}{gap}{remaining:>remaining_column_width$}\n",
139+
"{play_pause:>status_column_width$}{gap}{:>id_column_width$}{gap}{remaining:>remaining_column_width$}{gap}{message:<message_column_width$}\n",
123140
// need the string conversion first for the padding to work
124141
id.to_string(),
125142
).unwrap();

src/daemon/ctx.rs

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -176,15 +176,21 @@ impl DaemonCtx {
176176
}
177177

178178
pub async fn do_notification(&self, timer_id: TimerId) {
179-
let initial_duration = match self.timers.entry(timer_id) {
179+
let (initial_duration, message) = match self.timers.entry(timer_id) {
180180
dashmap::Entry::Vacant(_) => {
181181
log::error!("Bug: do_notification called for nonexistent timer {timer_id}");
182182
return;
183183
}
184-
dashmap::Entry::Occupied(occupied_entry) => occupied_entry.get().initial_duration,
184+
dashmap::Entry::Occupied(occupied_entry) => {
185+
let timer = occupied_entry.get();
186+
(timer.initial_duration, timer.message.clone())
187+
}
185188
};
189+
190+
let summary: &str = message.as_deref().unwrap_or("Time's up!");
191+
186192
let notification = Notification::new()
187-
.summary("Time's up!")
193+
.summary(summary)
188194
.body(&format!(
189195
"Timer {timer_id} set for {} has elapsed",
190196
initial_duration.format_colon_separated()
@@ -227,8 +233,13 @@ impl DaemonCtx {
227233
});
228234
}
229235

230-
pub async fn start_timer(&self, now: Instant, duration: Duration) -> TimerId {
231-
let id = self._start_timer(now, duration);
236+
pub async fn start_timer(
237+
&self,
238+
now: Instant,
239+
duration: Duration,
240+
message: Option<String>,
241+
) -> TimerId {
242+
let id = self._start_timer(now, duration, message);
232243
log::info!(
233244
"Started timer {} for {}",
234245
id,
@@ -242,10 +253,10 @@ impl DaemonCtx {
242253
}
243254

244255
/// Helper for start_timer() and again()
245-
fn _start_timer(&self, now: Instant, duration: Duration) -> TimerId {
256+
fn _start_timer(&self, now: Instant, duration: Duration, message: Option<String>) -> TimerId {
246257
let vacant = self.timers.first_vacant_entry();
247258
let id = *vacant.key();
248-
vacant.insert(Timer::new_running(now, duration));
259+
vacant.insert(Timer::new_running(now, duration, message));
249260
self.refresh_next_due.notify_one();
250261
id
251262
}
@@ -356,7 +367,13 @@ impl DaemonCtx {
356367
let last_started = { *self.last_started.read().await };
357368
match last_started {
358369
Some(duration) => {
359-
let id = self._start_timer(now, duration);
370+
let id = self._start_timer(
371+
now, duration,
372+
// TODO: we need to store all details about the most recently
373+
// started timer, (perhaps as another field in `Timers`) so
374+
// that `again` can re-use the same message
375+
None,
376+
);
360377
log::info!(
361378
"Restarted most recent timer duration {} with new id {}",
362379
duration.format_colon_separated(),

src/daemon/handle_client.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ async fn handle_command(cmd: Command, ctx: &DaemonCtx) -> Response {
1818
let now = Instant::now();
1919
match cmd {
2020
Command::List => ListResponse::ok(ctx.get_timerinfo_for_client(now)).into(),
21-
Command::StartTimer { duration } => {
22-
StartTimerResponse::ok(ctx.start_timer(now, duration).await).into()
21+
Command::StartTimer { duration, message } => {
22+
StartTimerResponse::ok(ctx.start_timer(now, duration, message).await).into()
2323
}
2424
Command::PauseTimer(id) => ctx.pause_timer(id, now).into(),
2525
Command::ResumeTimer(id) => ctx.resume_timer(id, now).into(),

src/sand/cli.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ impl Cli {
4646

4747
#[derive(Args, Clone)]
4848
pub struct StartArgs {
49+
/// Message to display when the timer is done
50+
#[arg(short, long)]
51+
pub message: Option<String>,
52+
4953
#[clap(name = "DURATION", value_parser = sand::duration::parse_duration_component, num_args = 1..)]
5054
pub durations: Vec<Duration>,
5155
}

src/sand/message.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ use crate::sand::timer::{self, PausedTimer, RunningTimer, Timer, TimerId};
1616
#[serde(rename_all = "lowercase")]
1717
pub enum Command {
1818
List,
19-
StartTimer { duration: Duration },
19+
StartTimer {
20+
duration: Duration,
21+
message: Option<String>,
22+
},
2023
PauseTimer(TimerId),
2124
ResumeTimer(TimerId),
2225
CancelTimer(TimerId),
@@ -112,6 +115,7 @@ pub struct TimerInfo {
112115
pub id: TimerId,
113116
pub state: TimerState,
114117
pub remaining: Duration,
118+
pub message: Option<String>,
115119
}
116120

117121
impl TimerInfo {
@@ -128,6 +132,7 @@ impl TimerInfo {
128132
id,
129133
state,
130134
remaining,
135+
message: timer.message.clone(),
131136
}
132137
}
133138

src/sand/timer.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,15 @@ pub struct RunningTimer {
3939
pub struct Timer {
4040
/// The initial duration of the timer. Should not be modified after creation.
4141
pub initial_duration: Duration,
42+
pub message: Option<String>,
4243
pub state: TimerState,
4344
}
4445

4546
impl Timer {
46-
pub fn new_running(now: Instant, initial_duration: Duration) -> Self {
47+
pub fn new_running(now: Instant, initial_duration: Duration, message: Option<String>) -> Self {
4748
Timer {
4849
initial_duration,
50+
message,
4951
state: TimerState::Running(RunningTimer {
5052
due: now + initial_duration,
5153
}),

test.py

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,36 @@ def test_add(self, daemon):
155155
diff = DeepDiff(expected, response, ignore_order=True)
156156
assert not diff, f"Response shape mismatch:\n{pformat(diff)}"
157157

158+
def test_add_with_message(self, daemon):
159+
msg_and_response(
160+
{
161+
"starttimer": {
162+
"duration": {"secs": 10 * 60, "nanos": 0},
163+
"message": "Hello, world!",
164+
}
165+
}
166+
)
167+
response = msg_and_response("list")
168+
expected_shape = {
169+
"ok": {
170+
"timers": [
171+
{
172+
"id": 1,
173+
"message": "Hello, world!",
174+
"state": "Running",
175+
"remaining": None,
176+
},
177+
]
178+
}
179+
}
180+
diff = DeepDiff(
181+
expected_shape,
182+
response,
183+
exclude_regex_paths=IGNORE_REMAINING,
184+
ignore_order=True,
185+
)
186+
assert not diff, f"Response shape mismatch:\n{pformat(diff)}"
187+
158188
def test_list(self, daemon):
159189
msg_and_response({"starttimer": {"duration": {"secs": 10 * 60, "nanos": 0}}})
160190
msg_and_response({"starttimer": {"duration": {"secs": 20 * 60, "nanos": 0}}})
@@ -164,8 +194,8 @@ def test_list(self, daemon):
164194
expected_shape = {
165195
"ok": {
166196
"timers": [
167-
{"id": 2, "state": "Running", "remaining": None},
168-
{"id": 1, "state": "Running", "remaining": None},
197+
{"id": 2, "message": None, "state": "Running", "remaining": None},
198+
{"id": 1, "message": None, "state": "Running", "remaining": None},
169199
]
170200
}
171201
}
@@ -184,7 +214,11 @@ def test_pause_resume(self, daemon):
184214

185215
response = msg_and_response("list")
186216
expected_shape = {
187-
"ok": {"timers": [{"id": 1, "state": "Paused", "remaining": None}]}
217+
"ok": {
218+
"timers": [
219+
{"id": 1, "message": None, "state": "Paused", "remaining": None}
220+
]
221+
}
188222
}
189223
diff = DeepDiff(
190224
expected_shape,
@@ -198,7 +232,11 @@ def test_pause_resume(self, daemon):
198232

199233
response = msg_and_response("list")
200234
expected_shape = {
201-
"ok": {"timers": [{"id": 1, "state": "Running", "remaining": None}]}
235+
"ok": {
236+
"timers": [
237+
{"id": 1, "message": None, "state": "Running", "remaining": None}
238+
]
239+
}
202240
}
203241
diff = DeepDiff(
204242
expected_shape,
@@ -223,7 +261,11 @@ def test_cancel_paused(self, daemon):
223261

224262
response = msg_and_response("list")
225263
expected_shape = {
226-
"ok": {"timers": [{"id": 1, "state": "Paused", "remaining": None}]}
264+
"ok": {
265+
"timers": [
266+
{"id": 1, "message": None, "state": "Paused", "remaining": None}
267+
]
268+
}
227269
}
228270
diff = DeepDiff(
229271
expected_shape,

0 commit comments

Comments
 (0)