Produce notifications for long-running commands in Elvish.
This file is written in literate programming style, to make it easy to explain. See long-running-notifications.elv for the generated file.
Install the elvish-modules
package using epm:
use epm
epm:install github.com/zzamboni/elvish-modules
In your rc.elv
, load this module:
use github.com/zzamboni/elvish-modules/long-running-notifications
Try it out! Run the following command:
sleep 11
The default notification threshold is 10 seconds, so when the command finishes, you will see a notification. The threshold can be changed by assigning a value in seconds to the long-running-notifications:threshold
variable. For example:
long-running-notifications:threshold = 20
You can specify a list commands for which you do not want notifications in the $long-running-notifications:never-notify
variable, and a list of commands that should always be notified (regardless of how long they took) in always-notify
. Their default values are:
never-notify = [ vi vim emacs nano less more bat ]
always-notify = [ ]
If you want to know how long the last command took (for example, for displaying in your prompt), you can use the $long-running-notifications:last-cmd-duration
variable. The value is in seconds.
Note: this module measures command execution time with a granularity of seconds, so anything that takes less than one second will be reported as zero.
By default, the module tries to determine the best notification method to use based on available commands. The method can be specified manually by assigning one of the following values directly to $long-running-notifications:notifier
:
- A string, which must be one of the predefined notification mechanisms:
macos
(GUI notifications on macOS, used automatically if terminal-notifier is available)libnotify
(GUI notifications using libnotify, used automatically ifnotify-send
is available)text
(prints to the same terminal where the command ran)
- A lambda, which must take three arguments and produce the corresponding notification. The arguments contain the last command (string), its duration (in seconds) and its start time (as seconds in Unix epoch format). For example:
long-running-notifications:notifier = [cmd duration start]{ echo "LONG COMMAND! Lasted "$duration }
If you write a new notification mechanism which you think might be useful to others, please submit a pull request!
Threshold in seconds for producing notifications (default 10).
var threshold = 10
Variables which can be used to extract information about the last command executed.
var last-cmd-start-time = 0
var last-cmd = ""
var last-cmd-duration = 0
The $notifier
variable determines which notification mechanism to use. By default it starts with the value "auto"
which chooses which one to use automatically, based on the value of $notifications-to-try
(see below). But you can also hand-choose the method by assigning one of the following:
- A string, which must be one of the predefined notification mechanisms (at the moment
text
,macos
orlibnotify
). - A lambda, which must take three arguments and produce the corresponding notification. The arguments contain the last command (string), its duration (in seconds) and its start time (as seconds in Unix epoch format).
var notifier = auto
The $notifications-to-try
variable contains the order in which notification mechanisms should be attempted. For each one, their check
function is executed, and the first one for which it returns $true
is used.
var notifications-to-try = [ macos libnotify text ]
Commands for which notifications should never or always be produced, regardless of how long they take
var never-notify = [ vi vim emacs nano less more bat ]
var always-notify = [ ]
Each notification mechanism is defined as a map with two elements: check
should be a lambda which returns $true
if that mechanism can be used in the current session, and notify
must be a lambda which receives three arguments: the command (string), its duration (in seconds) and its start time (as seconds in Unix epoch format).
All notification mechanisms are stored in the notification-fns
map, by their user-visible name.
var notification-fns = [
&text= [
&check= { put $true }
¬ify= {|cmd dur start|
echo (styled "Command lasted "$dur"s" magenta) > /dev/tty
}
]
&libnotify= [
&check= { put ?(which notify-send >/dev/null 2>&1) }
¬ify= {|cmd duration start|
notify-send "Finished: "$cmd "Running time: "$duration"s"
}
]
&macos= [
&check= { put ?(which terminal-notifier >/dev/null 2>&1) }
¬ify= {|cmd duration start|
terminal-notifier -title "Finished: "$cmd -message "Running time: "$duration"s"
}
]
]
The -choose-notification-fn
goes through the notification mechanisms in the order defined by $notifications-to-try
and chooses which one to use.
fn -choose-notification-fn {
each {|method-name|
var method = $notification-fns[$method-name]
if ($method[check]) {
put $method[notify]
return
}
} $notifications-to-try
fail "No valid notification mechanism was found"
}
The -produce-notification
function chooses (if needed) a notification function, and calls it with the correct arguments.
fn -produce-notification {
if (not-eq (kind-of $notifier) fn) {
if (eq $notifier auto) {
set notifier = (-choose-notification-fn)
} elif (has-key $notification-fns $notifier) {
set notifier = $notification-fns[$notifier][notify]
} else {
fail "Invalid value for $long-running-notifications:notifier: "$notifier", please double check"
}
}
$notifier $last-cmd $last-cmd-duration $last-cmd-start-time
}
These are the main functions which keep track of how long a command takes and call the notifier function if needed.
Return the current time in Unix epoch value.
fn now {
put (date +%s)
}
Check if the last command is in the given list, so that we can check the never-notify
and always-notify
lists.
fn -last-cmd-in-list {|list|
var cmd = (take 1 [(edit:wordify $last-cmd) ""])
has-value $list $cmd
}
Wrapper functions to check the never-notify
and always-notify
lists.
fn -always-notify { -last-cmd-in-list $always-notify }
fn -never-notify { -last-cmd-in-list $never-notify }
Check the duration of the last command and produce a notification if it exceeds the threshold.
fn before-readline-hook {
var -end-time = (now)
set last-cmd-duration = (- $-end-time $last-cmd-start-time)
if (or (-always-notify) (and (not (-never-notify)) (> $last-cmd-duration $threshold))) {
-produce-notification
}
}
Record the command and its start time.
fn after-readline-hook {|cmd|
set last-cmd = $cmd
set last-cmd-start-time = (now)
}
The init
function sets up the prompt hooks to compute times and produce notifications as needed.
fn init {
# Set up the hooks
use ./prompt-hooks
prompt-hooks:add-before-readline $before-readline-hook~
prompt-hooks:add-after-readline $after-readline-hook~
# Initialize to avoid spurious notification when the module is loaded
set last-cmd-start-time = (now)
}
We call init
automatically on module load.
init