Packages

fnord

0.8.40

AI code archaeology

Current section

Files

Jump to
fnord lib notifier.ex
Raw

lib/notifier.ex

defmodule Notifier do
@moduledoc """
A simple notification module that works on MacOS and Linux.
On macOS it uses AppleScript (`osascript`); on Linux it uses `notify-send` or
`dunstify`.
If neither is available, it falls back to printing a bell to STDERR.
"""
@type platform :: :mac | :linux | :other
@spec notify(String.t(), String.t(), keyword) :: :ok | {:error, term}
def notify(title, body, opts \\ []) do
case platform(opts) do
:mac -> mac_notify(title, body, opts)
:linux -> linux_notify(title, body, opts)
:other -> fallback_beep(title, body, opts)
end
end
@doc """
Attempts to dismiss/clear notifications for the given group.
Only works on systems that support notification dismissal.
"""
@spec dismiss(String.t(), keyword) :: :ok | {:error, term}
def dismiss(group \\ "fnord", opts \\ []) do
case platform(opts) do
:mac -> mac_dismiss(group)
:linux -> linux_dismiss(group)
:other -> :ok
end
end
# Decide platform (injectable for tests)
@spec platform(keyword) :: platform
defp platform(opts) do
case Keyword.get(opts, :platform) do
:mac ->
:mac
:linux ->
:linux
:other ->
:other
nil ->
case :os.type() do
{:unix, :darwin} -> :mac
{:unix, :linux} -> :linux
_ -> :other
end
end
end
# ----------------------------------------------------------------------------
# MacOS (always AppleScript)
# ----------------------------------------------------------------------------
defp mac_notify(title, body, opts) do
subtitle = Keyword.get(opts, :subtitle)
title = applestr(title)
body = applestr(body)
subtitle_script =
if subtitle do
"subtitle " <> applestr(subtitle)
else
""
end
run("osascript", [
"-e",
"display notification #{body} with title #{title} #{subtitle_script}"
])
end
# AppleScript notifications can't be programmatically dismissed.
defp mac_dismiss(_group), do: :ok
defp applestr(s), do: ~s(") <> String.replace(s, ~s("), ~s(\\")) <> ~s(")
# ----------------------------------------------------------------------------
# Linux
# ----------------------------------------------------------------------------
defp linux_notify(title, body, opts) do
if gui_env?() do
urgency = Keyword.get(opts, :urgency, "critical")
timeout_ms = Keyword.get(opts, :timeout_ms, 10000)
cond do
exe?("notify-send") ->
run("notify-send", ["--urgency=#{urgency}", "--expire-time=#{timeout_ms}", title, body])
exe?("dunstify") ->
run("dunstify", ["-u", urgency, "-t", Integer.to_string(timeout_ms), title, body])
true ->
fallback_beep(title, body, opts)
end
else
fallback_beep(title, body, opts)
end
end
defp gui_env?() do
(System.get_env("WAYLAND_DISPLAY") || "") != "" ||
(System.get_env("DISPLAY") || "") != ""
end
defp linux_dismiss(_group), do: :ok
# ----------------------------------------------------------------------------
# Fallback
# ----------------------------------------------------------------------------
defp fallback_beep(_title, _body, _opts) do
IO.puts(:stderr, "\a")
:ok
end
# ----------------------------------------------------------------------------
# Util
# ----------------------------------------------------------------------------
defp exe?(name), do: System.find_executable(name) != nil
defp run(cmd, args) do
case System.cmd(cmd, args, stderr_to_stdout: true) do
{_out, 0} -> :ok
{out, code} -> {:error, {cmd, code, out}}
end
end
end