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 $argv
aplsimple - 2019-02-14 05:37:16
> <DURATION>\[<UNITS>\]
no?
Bah! You're right. I failed to test that after updating the synopsis. Thanks.