Permalink
Switch branches/tags
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
216 lines (165 sloc) 7.56 KB

Notifications for long-running commands in Elvish

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.

Table of Contents

Usage

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

Notification mechanisms

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 if notify-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!

Implementation

Configuration and user-accessible variables

Threshold in seconds for producing notifications (default 10).

threshold = 10

Variables which can be used to extract information about the last command executed.

last-cmd-start-time = 0
last-cmd = ""
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 or libnotify).
  • 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).
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.

notifications-to-try = [ macos libnotify text ]

Notification mechanisms

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.

notification-fns = [
  &text= [
    &check= { put $true }
    &notify= [cmd dur start]{
      echo (styled "Command lasted "$dur"s" magenta) > /dev/tty
    }
  ]
  &libnotify= [
    &check= { put ?(which notify-send >/dev/null 2>&1) }
    &notify= [cmd duration start]{
      notify-send "Finished: "$cmd "Running time: "$duration"s"
    }
  ]
  &macos= [
    &check= { put ?(which terminal-notifier >/dev/null 2>&1) }
    &notify= [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]{
    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) {
      notifier = (-choose-notification-fn)
    } elif (has-key $notification-fns $notifier) {
      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
}

Time tracking functions

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 the duration of the last command and produce a notification if it exceeds the threshold.

fn before-readline-hook {
  -end-time = (now)
  last-cmd-duration = (- $-end-time $last-cmd-start-time)
  if (> $last-cmd-duration $threshold) {
    -produce-notification
  }
}

Record the command and its start time.

fn after-readline-hook [cmd]{
  last-cmd = $cmd
  last-cmd-start-time = (now)
}

Initialization

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
  last-cmd-start-time = (now)
}

We call init automatically on module load.

init