Navi is an application for defining desktop notifications in terms of a running notification system. That is, Navi does not implement desktop notifications from the ground up. Rather, given a running compatible notifications system, Navi provides a simple interface for defining notifications that hook into the running system.
There are built-in services for sending specific notifications, along with functionality to send custom notifications.
Navi is useful for when we have a running notification server and want to define custom notification events. For example, we may want a notification for cpu temperature. We can define a "service" that includes:
- The command to run.
- The command output that should trigger a notification.
- The notification to send.
# requires lm-sensors
[[single]]
command = """
temp_res=$(sensors | head -n 3 | tail -n 1)
regex="temp1:\\s*\\+([0-9]+)\\.[0-9]{0,2}°[C|F]"
if [[ $temp_res =~ $regex ]]; then
temp="${BASH_REMATCH[1]}"
# not actually that hot...
if [[ $temp -gt 20 ]]; then
echo "true"
else
echo "false"
fi
else
echo "couldn't parse: ${temp_res}"
exit 1
fi
"""
trigger = "true"
[single.note]
summary = "Temperature"
body = "We're hot!"
urgency = "critical"
timeout = 10
This allows us to define arbitrary notification services based on the current system. In other words, as long as we can query for a particular bit of information (e.g. bash code), then navi will take care of the rest: running this query every N seconds, sending notifications, caching previous values to avoid repeats, and error handling.
Navi currently supports:
-
DBus
If there is a DBus-compatible notification server running, navi can hook in directly. This has been tested with:
-
Libnotify
Navi can also use the
notify-send
tool directly. This is largely redundant sincenotify-send
itself requires a running DBus notification server, but this option is provided as an alternative.
Navi has the following usage:
Navi: A program for monitoring system status via desktop notifications.
Usage: navi [-c|--config-file PATH] [-v|--version]
Navi allows one to easily define custom notification 'services' that hook into
a running notification server. For example, one can provide a bash script
that, say, queries the connection status of a given network device. Navi will
periodically run this query and send a desktop notification if the status has
changed. See github.com/tbidne/navi#README for full documentation.
Available options:
-c,--config-file PATH Path to config file. Defaults to
<xdg-config>/navi/config.toml.
-h,--help Show this help text
Version: 0.1
This argument overrides where Navi searches for the configuration file.
The default path to the config file is based on the XDG config directory. Given xdg-config
, by default, Navi will look for <xdg-config>/navi/config.toml
e.g. ~/.config/navi/config.toml
.
Navi is configured via a toml file, by default located at <xdg-config>/navi/config.toml
. Full examples can be found in examples.
note-system
: Optional. One of["dbus"|"notify-send"]
. Defaults to"dbus"
.logging.severity
: Optional. One of["debug"|"info"|"error"]
. Controls the logging level. Defaults toerror
.logging.location
: Optional. Either"default"
,"stdout"
or"<filename>"
. No option ordefault
uses<xdg-state>/navi/<timestamp>.log
e.g.~/.local/state/navi/<timestamp>.log
.logging.size-mode
: Optional. Sets a size threshold for the file log directory, upon which we either print a warning or delete all prior logs, if the threshold is exceeded. TheSIZE
should include the value and units e.g.warn 10 mb
,warn 5 gigabytes
,delete 20.5B
. Defaults todelete 50 mb
. This only affects the default log path e.g.~/.local/state/navi
.
note-system = "dbus"
[logging]
severity = "debug"
location = "stdout"
size-mode = "warn 10 mb"
The full list of notification options are:
summary
: Text summary.body
: (Optional). Text body.urgency
: (Optional). One of["low"|"normal"|"critical"]
.timeout
: (Optional). One of["never"|<seconds>]
. Determines how long notifications persist. Defaults to 10 seconds.
Individual services have their own options, but there are a few that are common to most.
poll-interval
: Optional. One of[NATURAL | STRING]
. The provided interval be either a raw natural (interpreted as seconds), or a "time string" e.g.1d2m3h4s
,3h20s
. Determines how often a service is polled.repeat-events
: One of[true|false]
. Determines if we send off the same notification twice in a row. Defaults tofalse
(i.e. no repeats) unless stated otherwise.error-events
: One of["none"|"repeats"|"no-repeats"]
. Determines if we send off notifications for errors, and how we handle repeats. Defaults to"no-repeats"
unless stated otherwise i.e. we send error notifications but no repeats.
These are services that are built-in, in the sense that no custom script is required.
This service sends notifications based on the current battery status. Options include:
battery-status.app
: One of["sysfs" | "acpi" | "upower"]
.sysfs
reads/sys
or/sysfs
directly.acpi
requires theacpi
utility.upower
requires theupower
utility.
battery-status.poll-interval
: Defaults to 30 seconds.battery-status.repeat-events
battery-status.error-events
battery-status.timeout
[battery-status]
app = "sysfs"
repeat-events = false
error-events = "repeats"
This service sends notifications based on the current battery percentage when it is discharging. Options include:
battery-percentage.alert.percent
: integer in[0, 100]
. Sends a notification once the battery level drops to this level.battery-percentage.app
: One of["sysfs" | "acpi" | "upower"]
.sysfs
reads/sys
or/sysfs
directly.acpi
requires theacpi
utility.upower
requires theupower
utility.
battery-percentage.poll-interval
: Defaults to 30 seconds.battery-percentage.repeat-events
battery-percentage.error-events
battery-percentage.alert.urgency
battery-percentage.alert.timeout
[battery-percentage]
app = "upower"
repeat-events = false
error-events = "repeats"
[[battery-percentage.alert]]
percent = 80
[[battery-percentage.alert]]
percent = 20
urgency = "critical"
This service sends notifications based on the network connectivity for given devices.
net-interface.device
: The name of the network device to monitor (e.g.wlp0s20f3
).net-interface.app
: One of["nmcli" | "ip"]
.nmcli
requires thenmcli
(NetworkManager cli
) utility.ip
requires theip
utility.
net-interface.poll-interval
: Defaults to 30 seconds.net-interface.repeat-events
net-interface.error-events
net-interface.alert.urgency
net-interface.alert.timeout
[[net-interface]]
app = "nmcli"
device = "wlp0s20f3"
[[net-interface]]
app = "ip"
device = "enp0s31f6"
This service sends a single notification based on an arbitrary command.
single.command
: Command literal or path to a script.single.name
: Optional name to be used in logging.single.trigger
: Result that triggers the notification.
single.poll-interval
: Defaults to 30 seconds.single.repeat-events
single.error-events
single.note.summary
single.note.body
single.note.urgency
single.note.timeout
# Send alert when the current minute is even
[[single]]
poll-interval = 10
command = """
min=`date +%M`;
if [[ \"$min % 2\" -eq 0 ]]; then
echo -n "true"
else
echo -n "false"
fi
"""
trigger = "true"
[single.note]
summary = "Even/Odd"
body = "We're even, yay!"
timeout = 10
This service sends multiple notifications based on an arbitrary command.
multiple.command
: Command literal or path to a script.multiple.name
: Optional name to be used in logging.multiple.trigger-note.trigger
: Result that triggers the notification.
multiple.poll-interval
: Defaults to 30 seconds.multiple.repeat-events
multiple.error-events
multiple.trigger-note.summary
multiple.trigger-note.body
multiple.trigger-note.urgency
multiple.trigger-note.timeout
# Manual battery level alerts
[[multiple]]
name = "battery-manual"
command = """
regex="([0-9]{1,3})%"
power=$(upower -i `upower -e | grep 'BAT'` | grep percentage | awk '{print $2}')
if [[ $power =~ $regex ]]; then
power_num="${BASH_REMATCH[1]}"
if [[ $power_num -lt 5 ]]; then
echo 5
elif [[ $power_num -lt 40 ]]; then
echo 40
elif [[ $power_num -lt 80 ]]; then
echo 80
else
echo 100
fi
else
echo "Error reading battery: $power"
fi
"""
[[multiple.trigger-note]]
trigger = "100"
[multiple.trigger-note.note]
summary = "Battery Percentage"
body = "Full"
timeout = 10
[[multiple.trigger-note]]
trigger = 80
[multiple.trigger-note.note]
summary = "Battery Percentage"
body = "< 80"
timeout = 10
[[multiple.trigger-note]]
trigger = "40"
[multiple.trigger-note.note]
summary = "Battery Percentage"
body = "< 40"
urgency = "critical"
timeout = 10
If you have never built a haskell program before, Cabal is probably the best choice.
Using ghcup
, install cabal 2.4+
and one of:
ghc 9.6
ghc 9.8
ghc 9.10
Once you have cabal
and ghc
, navi
can be built with cabal build
or installed globally (i.e. ~/.cabal/bin/
) with cabal install
.
Building with nix
uses flakes. navi
can be built with nix build
, which will compile and run the tests.
Because Navi is a flake, it can be built as part of a nix expression. For instance, if you want to add Navi to NixOS
, your flake.nix
might look something like:
# flake.nix
{
inputs.navi.url = "github:tbidne/navi/main";
}
Then include this in the systemPackages
:
# wherever your global packages are defined
{
environment.systemPackages = [
navi.packages."${system}".default
];
}