Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,8 @@ pub fn main(cli_cmd: cli::ClientCommand) -> io::Result<()> {

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

fn start(conn: &mut DaemonConnection, durations: Vec<Duration>) -> ClientResult<()> {
fn start(
conn: &mut DaemonConnection,
durations: Vec<Duration>,
message: Option<String>,
) -> ClientResult<()> {
let dur: Duration = durations.iter().sum();
let StartTimerResponse::Ok { id } = conn.add_timer(dur)?;
let StartTimerResponse::Ok { id } = conn.add_timer(dur, message)?;

let dur_string = dur.format_colon_separated();
println!("Timer {id} created for {dur_string}.");
Expand Down
8 changes: 6 additions & 2 deletions src/client/daemon_connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,12 @@ impl DaemonConnection {
Ok(Self { read, write })
}

pub fn add_timer(&mut self, duration: Duration) -> io::Result<StartTimerResponse> {
self.send(Command::StartTimer { duration })?;
pub fn add_timer(
&mut self,
duration: Duration,
message: Option<String>,
) -> io::Result<StartTimerResponse> {
self.send(Command::StartTimer { duration, message })?;
self.recv::<StartTimerResponse>()
}

Expand Down
21 changes: 19 additions & 2 deletions src/client/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ struct TableConfig<'a> {
status_column_width: usize,
id_column_width: usize,
remaining_column_width: usize,
message_column_width: usize,
gap: &'a str,
}

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

let message_header = "Message";
let message_column_width = {
let widest_message = timers
.iter()
.map(|ti| ti.message.as_ref().map(|s| s.len()).unwrap_or(0))
.max()
.expect("timers.len() != 0");
widest_message.max(message_header.len())
};

let gap = " ";
let table_config = TableConfig {
status_column_width,
id_column_width,
remaining_column_width,
message_column_width,
gap,
};

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

// Header
write!(
output,
"{status_header}{gap}{}{gap}{}\n",
"{status_header}{gap}{}{gap}{}{gap}{}\n",
id_header_padded.underlined(),
remaining_header_padded.underlined(),
message_header_padded.underlined(),
)
.unwrap();

Expand Down Expand Up @@ -116,10 +131,12 @@ fn timers_table_row(output: &mut impl Write, timer_info: &TimerInfo, table_confi
status_column_width,
id_column_width,
remaining_column_width,
message_column_width,
gap,
} = table_config;
let message: &str = timer_info.message.as_deref().unwrap_or("");
write!(output,
"{play_pause:>status_column_width$}{gap}{:>id_column_width$}{gap}{remaining:>remaining_column_width$}\n",
"{play_pause:>status_column_width$}{gap}{:>id_column_width$}{gap}{remaining:>remaining_column_width$}{gap}{message:<message_column_width$}\n",
// need the string conversion first for the padding to work
id.to_string(),
).unwrap();
Expand Down
33 changes: 25 additions & 8 deletions src/daemon/ctx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,15 +176,21 @@ impl DaemonCtx {
}

pub async fn do_notification(&self, timer_id: TimerId) {
let initial_duration = match self.timers.entry(timer_id) {
let (initial_duration, message) = match self.timers.entry(timer_id) {
dashmap::Entry::Vacant(_) => {
log::error!("Bug: do_notification called for nonexistent timer {timer_id}");
return;
}
dashmap::Entry::Occupied(occupied_entry) => occupied_entry.get().initial_duration,
dashmap::Entry::Occupied(occupied_entry) => {
let timer = occupied_entry.get();
(timer.initial_duration, timer.message.clone())
}
};

let summary: &str = message.as_deref().unwrap_or("Time's up!");

let notification = Notification::new()
.summary("Time's up!")
.summary(summary)
.body(&format!(
"Timer {timer_id} set for {} has elapsed",
initial_duration.format_colon_separated()
Expand Down Expand Up @@ -227,8 +233,13 @@ impl DaemonCtx {
});
}

pub async fn start_timer(&self, now: Instant, duration: Duration) -> TimerId {
let id = self._start_timer(now, duration);
pub async fn start_timer(
&self,
now: Instant,
duration: Duration,
message: Option<String>,
) -> TimerId {
let id = self._start_timer(now, duration, message);
log::info!(
"Started timer {} for {}",
id,
Expand All @@ -242,10 +253,10 @@ impl DaemonCtx {
}

/// Helper for start_timer() and again()
fn _start_timer(&self, now: Instant, duration: Duration) -> TimerId {
fn _start_timer(&self, now: Instant, duration: Duration, message: Option<String>) -> TimerId {
let vacant = self.timers.first_vacant_entry();
let id = *vacant.key();
vacant.insert(Timer::new_running(now, duration));
vacant.insert(Timer::new_running(now, duration, message));
self.refresh_next_due.notify_one();
id
}
Expand Down Expand Up @@ -356,7 +367,13 @@ impl DaemonCtx {
let last_started = { *self.last_started.read().await };
match last_started {
Some(duration) => {
let id = self._start_timer(now, duration);
let id = self._start_timer(
now, duration,
// TODO: we need to store all details about the most recently
// started timer, (perhaps as another field in `Timers`) so
// that `again` can re-use the same message
None,
);
log::info!(
"Restarted most recent timer duration {} with new id {}",
duration.format_colon_separated(),
Expand Down
4 changes: 2 additions & 2 deletions src/daemon/handle_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ async fn handle_command(cmd: Command, ctx: &DaemonCtx) -> Response {
let now = Instant::now();
match cmd {
Command::List => ListResponse::ok(ctx.get_timerinfo_for_client(now)).into(),
Command::StartTimer { duration } => {
StartTimerResponse::ok(ctx.start_timer(now, duration).await).into()
Command::StartTimer { duration, message } => {
StartTimerResponse::ok(ctx.start_timer(now, duration, message).await).into()
}
Command::PauseTimer(id) => ctx.pause_timer(id, now).into(),
Command::ResumeTimer(id) => ctx.resume_timer(id, now).into(),
Expand Down
4 changes: 4 additions & 0 deletions src/sand/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ impl Cli {

#[derive(Args, Clone)]
pub struct StartArgs {
/// Message to display when the timer is done
#[arg(short, long)]
pub message: Option<String>,

#[clap(name = "DURATION", value_parser = sand::duration::parse_duration_component, num_args = 1..)]
pub durations: Vec<Duration>,
}
Expand Down
7 changes: 6 additions & 1 deletion src/sand/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ use crate::sand::timer::{self, PausedTimer, RunningTimer, Timer, TimerId};
#[serde(rename_all = "lowercase")]
pub enum Command {
List,
StartTimer { duration: Duration },
StartTimer {
duration: Duration,
message: Option<String>,
},
PauseTimer(TimerId),
ResumeTimer(TimerId),
CancelTimer(TimerId),
Expand Down Expand Up @@ -112,6 +115,7 @@ pub struct TimerInfo {
pub id: TimerId,
pub state: TimerState,
pub remaining: Duration,
pub message: Option<String>,
}

impl TimerInfo {
Expand All @@ -128,6 +132,7 @@ impl TimerInfo {
id,
state,
remaining,
message: timer.message.clone(),
}
}

Expand Down
4 changes: 3 additions & 1 deletion src/sand/timer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,15 @@ pub struct RunningTimer {
pub struct Timer {
/// The initial duration of the timer. Should not be modified after creation.
pub initial_duration: Duration,
pub message: Option<String>,
pub state: TimerState,
}

impl Timer {
pub fn new_running(now: Instant, initial_duration: Duration) -> Self {
pub fn new_running(now: Instant, initial_duration: Duration, message: Option<String>) -> Self {
Timer {
initial_duration,
message,
state: TimerState::Running(RunningTimer {
due: now + initial_duration,
}),
Expand Down
52 changes: 47 additions & 5 deletions test.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,36 @@ def test_add(self, daemon):
diff = DeepDiff(expected, response, ignore_order=True)
assert not diff, f"Response shape mismatch:\n{pformat(diff)}"

def test_add_with_message(self, daemon):
msg_and_response(
{
"starttimer": {
"duration": {"secs": 10 * 60, "nanos": 0},
"message": "Hello, world!",
}
}
)
response = msg_and_response("list")
expected_shape = {
"ok": {
"timers": [
{
"id": 1,
"message": "Hello, world!",
"state": "Running",
"remaining": None,
},
]
}
}
diff = DeepDiff(
expected_shape,
response,
exclude_regex_paths=IGNORE_REMAINING,
ignore_order=True,
)
assert not diff, f"Response shape mismatch:\n{pformat(diff)}"

def test_list(self, daemon):
msg_and_response({"starttimer": {"duration": {"secs": 10 * 60, "nanos": 0}}})
msg_and_response({"starttimer": {"duration": {"secs": 20 * 60, "nanos": 0}}})
Expand All @@ -164,8 +194,8 @@ def test_list(self, daemon):
expected_shape = {
"ok": {
"timers": [
{"id": 2, "state": "Running", "remaining": None},
{"id": 1, "state": "Running", "remaining": None},
{"id": 2, "message": None, "state": "Running", "remaining": None},
{"id": 1, "message": None, "state": "Running", "remaining": None},
]
}
}
Expand All @@ -184,7 +214,11 @@ def test_pause_resume(self, daemon):

response = msg_and_response("list")
expected_shape = {
"ok": {"timers": [{"id": 1, "state": "Paused", "remaining": None}]}
"ok": {
"timers": [
{"id": 1, "message": None, "state": "Paused", "remaining": None}
]
}
}
diff = DeepDiff(
expected_shape,
Expand All @@ -198,7 +232,11 @@ def test_pause_resume(self, daemon):

response = msg_and_response("list")
expected_shape = {
"ok": {"timers": [{"id": 1, "state": "Running", "remaining": None}]}
"ok": {
"timers": [
{"id": 1, "message": None, "state": "Running", "remaining": None}
]
}
}
diff = DeepDiff(
expected_shape,
Expand All @@ -223,7 +261,11 @@ def test_cancel_paused(self, daemon):

response = msg_and_response("list")
expected_shape = {
"ok": {"timers": [{"id": 1, "state": "Paused", "remaining": None}]}
"ok": {
"timers": [
{"id": 1, "message": None, "state": "Paused", "remaining": None}
]
}
}
diff = DeepDiff(
expected_shape,
Expand Down