This is a re-write of a Ruby implementation into Tcl. The original idea is from: http://richardmavis.info/a-complete-program
#!/usr/bin/env tclsh
# Barry Arthur, 2019-2-13
# Original: https://github.com/rmavis/timer/blob/master/timer (http://richardmavis.info/a-complete-program)
# Returns the necessary key:varname pairs that [dict update] expects,
# and which are painful to write out by hand.
proc using_keys {keys} {
concat {*}[lmap x $keys {list $x $x}]
}
# Returns the singular or plural form of unit.
proc inflect {quantity unit} {
return ${unit}[expr {$quantity == 1 ? "" : "s"}]
}
namespace eval timer {
variable default_config {
delay 5
title Timer
time {}
message {}
command {}
}
variable time_units {
d {desc day mult {60 * 60 * 24}}
h {desc hour mult {60 * 60}}
m {desc minute mult 60}
s {desc second mult 1}
}
}
proc timer::help_message {} {
variable default_config
# following line allows here-doc style of text block.
subst [regsub -all -lineanchor "^\\s*> ?" {
> timer: sleep a while, then send a notification via `notify-send`.
>
> To specify the delay time, provide one or more arguments in the form:
>
> <DURATION>\[<UNITS>\]
>
> UNITS:
> d = days
> h = hours
> m = minutes
> s = seconds (default)
>
> If you provide multiple time-delay arguments, their values will accrue.
> So you can delay 90 seconds either with `90s` or `1m 30s`.
>
> You can also provide a message and a title to use for the notification.
> The first argument given that does not look like a time-delay argument
> will be used for the message. The second will be used for the title.
> Both the message and title are optional. If neither are given, "[dict get $default_config title]"
> will be used as the title, and the message will state the delay duration.
>
> The notification will be sent via `notify-send`. If you'd like to run a
> custom command instead, you can specify that with the `-c` flag, e.g.,
> `-c "path/to/command"`. Information about the timer will be passed to
> the command via standard input.
>
> Examples:
> timer 5m Tea
> timer 1h 10m Laundry
> timer 45 "Fresh is best" Pasta
> timer 30d "Up 30 days" -c "~/bin/post_uptime_notice"
} {}]
}
proc timer::default_command {title {message {}}} {
return "notify-send -u critical \"$title\" \"$message\""
}
proc timer::custom_command {conf} {
return "echo \"$conf\" | [dict get $conf command] &"
}
proc timer::time_format {delay} {
variable time_units
# days:hours:minutes:seconds
set durations [split [clock format $delay -timezone UTC -format "%d:%T"] :]
# days starts at 1 so reset to zero-based
lset durations 0 [expr {[lindex $durations 0] - 1}]
# remove leading-zero on numbers for better presentation
# and to prevent octal parsing.
set durations [lmap x $durations {regsub -all {\m0} $x {}}]
set inflected [lmap d $durations u [dict keys $time_units] {
if {$d eq ""} continue
list $d [inflect $d [dict get $time_units $u desc]]
}]
if {[llength $inflected] > 1} {
concat [join [lrange $inflected 0 end-1] {, }] and [lindex $inflected end]
} else {
join $inflected
}
}
proc timer::normalize_conf {conf} {
variable default_config
dict update conf {*}[using_keys [dict keys $default_config]] {
if {$title eq ""} {
set title [dict get $default_config title]
}
if {$delay == 0} {
set delay [dict get $default_config delay]
}
set time [time_format $delay]
if {$message eq ""} {
set message $time
}
if {$command eq ""} {
set command [default_command $title $message]
} else {
set command [custom_command $conf]
}
}
return $conf
}
proc timer::from_stdin {} {
variable default_config
set conf [concat {*}[split [read stdin] \n]]
# remove unset elements so they don't shadow default in merge
set conf [dict remove $conf [dict filter $conf value {}]]
set conf [normalize_conf [dict merge $default_config $conf]]
return $conf
}
proc timer::parse_cmdline_args {cmdline_args} {
variable default_config
variable time_units
set delay 0
set title {}
set message {}
set command {}
set time_units_charclass [join [dict keys $time_units] {}]
set time_regex [subst -nocommands {^([0-9]+)([$time_units_charclass])?\$}]
for {set i 0} {$i < [llength $cmdline_args]} {incr i} {
set arg [lindex $cmdline_args $i]
if {[string tolower $arg] == "-h" || [string tolower $arg] == "--help"} {
puts [help_message]
exit
} elseif {$arg eq "-"} {
set conf [from_stdin]
# update local variables from conf dictionary
dict update conf {*}[using_keys [dict keys $default_config]] {}
} elseif {[string tolower $arg] == "-c" || [string tolower $arg] == "--command"} {
set command [lindex $cmdline_args [incr i]]
} elseif {[regexp $time_regex $arg -> duration units]} {
if {$units eq "" || [string tolower $units] == "s"} {
incr delay [expr {$duration * [dict get $time_units s mult]}]
} else {
incr delay [expr {$duration * [dict get $time_units [string tolower $units] mult]}]
}
} elseif {$message eq ""} {
set message $arg
} else {
set title $arg
}
}
return [normalize_conf [dict create \
delay $delay \
title $title \
message $message \
command $command \
]]
}
proc timer::init {cmdline_args} {
set conf [parse_cmdline_args $cmdline_args]
set pid [exec sh -c "(sleep [dict get $conf delay] ; [dict get $conf command])" &]
puts "\[$pid\] Timer set for [dict get $conf time]."
}
timer::init $argvaplsimple - 2019-02-14 05:37:16
> <DURATION>\[<UNITS>\]
no?
Bah! You're right. I failed to test that after updating the synopsis. Thanks.