Skip to content

Latest commit

 

History

History
249 lines (189 loc) · 9.06 KB

long-running-notifications.org

File metadata and controls

249 lines (189 loc) · 9.06 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

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.

Configuration of 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).

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 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).
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 = [ ]

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.

var 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|
    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
}

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 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)
}

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

We call init automatically on module load.

init