Knowledge Base

Preserving for the future: Shell scripts, AoC, and more

Sample colorized icon in desktop notification from browser

I am considering adding desktop notifications to my fork of InfCloud, and to do that I had to learn how to send desktop notifications. MDN did not disappoint!

You'll see that I do some very basic find-button logic to add the click function. Of course this is boilerplate and won't look exactly the same whenever I get around to adding it to InfCloud/CalDAVZap.

files/2023/11/listings/notifications.html (Source)

<html>
<header>
<title>Example page</title>
<script src="notifications.js">
</script>
</header>
<body>
<h1>example</h1>
Lorem ipsum, etc.
<button>Notify me!</button>
</body>
<footer>
</footer>
</html>
files/2023/11/listings/notifications.js (Source)
// vim: set et ts=3 sw=3 sts=3:
// Startdate: 2023-11-09 18:36
// Purpose: Reference implementation for sending a notification with a colorized image from this server
// Reference:
//    https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API/Using_the_Notifications_API
//    https://www.w3docs.com/snippets/html/how-to-display-base64-images-in-html.html
//    https://stackoverflow.com/questions/28450471/convert-inline-svg-to-base64-string
const img = "/calendar/images/banner_calendar.svg";
const notification_attempts = 10;
const notification_ms = 200;
// Due to how browsers/css work with colors, you can pass #000000 or web safe color names.
var colors = Array("red","orange","yellow","green","blue","purple","black","white","brown");
function colorizeSvg(inSvg, oldColor, newColor) {
   // In case we have to do more than `s//g` in the future.
   return inSvg.replaceAll(oldColor, newColor);
}
function SendColorizedNotification(_title, _body, _icon, _tag, _color) {
   // given _icon as path on server, get contents and adjust the main color in it to desired color.
   // The 585858 is the specific color of the calendar.svg we intend to replace.
   const xhr = new XMLHttpRequest();
   xhr.open("GET",_icon);
   xhr.onload = () => {
      newSvg = colorizeSvg(xhr.responseText,"#585858",_color);
      encodedData = window.btoa(newSvg); // turn it into the base64 stream
      encodedData = "data:image/svg+xml;base64," + encodedData; // prepend the type of stream
      SendNotification(_title, _body, encodedData, _tag, _color);
   }
   xhr.send();
}
function SendNotification(_title, _body, _icon, _tag) {
   // From: https://developer.mozilla.org/en-US/docs/Web/API/notification
   if (!("Notification" in window)) {
      // Check if the browser supports notifications
      alert("This browser does not support desktop notification");
   } else if (Notification.permission === "granted") {
      // Check whether notification permissions have already been granted;
      // if so, create a notification
      const notification = new Notification(String(_title), { tag: _tag, body: _body, icon: _icon });
      // …
   } else if (Notification.permission !== "denied") {
      // We need to ask the user for permission
      Notification.requestPermission().then((permission) => {
         // If the user accepts, let's create a notification
         if (permission === "granted") {
            const notification = new Notification(String(_title), { tag: _tag, body: _body, icon: _icon });
            // …
         }
      });
   }
   // At last, if the user has denied notifications, and you
   // want to be respectful there is no need to bother them anymore.
}
function SendNotificationManyTimes(_title, _body, _icon, _tag) {
   // From: https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API/Using_the_Notifications_API
   if (Notification?.permission === "granted") {
      // If the user agreed to get notified
      // Let's try to send ten notifications
      let i = 0;
      // Using an interval cause some browsers (including Firefox) are blocking notifications if there are too much in a certain time.
      const interval = setInterval(() => {
         // Thanks to the tag, we should only see the "Hi! 9" notification
         const n = new Notification(String(_title), { tag: _tag, body: _body, icon: _icon });
         //const n = new Notification(`Hi! breakfast ${i}`, { tag: "soManyNotification", body: "this is body?", icon: img });
         if (i === (notification_attempts-1)) {
            clearInterval(interval);
         }
         i++;
      }, notification_ms);
   } else if (Notification && Notification.permission !== "denied") {
      // If the user hasn't told if they want to be notified or not
      // Note: because of Chrome, we are not sure the permission property
      // is set, therefore it's unsafe to check for the "default" value.
      Notification.requestPermission().then((status) => {
         // If the user said okay
         if (status === "granted") {
            let i = 0;
            // Using an interval cause some browsers (including Firefox) are blocking notifications if there are too much in a certain time.
            const interval = setInterval(() => {
               // Thanks to the tag, we should only see the "Hi! 9" notification
               const n = new Notification(String(_title), { tag: _tag, body: _body, icon: _icon });
               if (i === (notification_attempts-1)) {
                  clearInterval(interval);
               }
               i++;
            }, notification_ms);
         } else {
            // Otherwise, we can fallback to a regular modal alert
            alert(String(_title)+": "+String(_body));
         }
      });
   } else {
      // If the user refuses to get notified, we can fallback to a regular modal alert
      alert(String(_title)+": "+String(_body));
   }
}
function getRandomColor() {
   return colors[Math.floor(Math.random()*colors.length)];
}
window.addEventListener("load", () => {
   const button = document.querySelector("button");
   button.addEventListener("click", () => {
      SendColorizedNotification("sample calendar appointment","in 10 minutes",img,"",getRandomColor());
   });
});

apt/preferences.d: A Useful Example

I used to use apt-mark to hold packages, but I never dug far enough to learn where it stores this state. I found a much easier way to hold a package: pin it in preferences!

Make a file, e.g. /etc/apt/preferences.d/krb5-auth-dialog with contents:

Package: krb5-auth-dialog
Version: 3.26.1-4
Pin: release
Pin-Priority: 1000

So now apt-cache policy shows the following:

$ apt-cache policy krb5-auth-dialog
krb5-auth-dialog:
  Installed: 3.26.1-4
  Candidate: 3.26.1-4
  Version table:
     43.0-1+b1 500
        500 http://www.example.com/mirror/devuan ceres/main amd64 Packages
 *** 3.26.1-4 1000
        500 http://www.example.com/internal/repo/devuan-deb  Packages
        100 /var/lib/dpkg/status

For the exact reason of this example package, see krb5-auth-dialog and the news.

Current process for AoE2DE on Linux

Of all the non-open-source software I'm aware of, Valve is a great publisher. I use Steam weekly for playing my one modern computer game, Age of Empires 2 Definitive Edition.

I upgraded my main workstation to only a 4-year-old computer (which now requires UEFI boot, bleh, for local devices; oh, but legacy boot works for removable devices which makes no sense to me), instead of my 10-year-old computer. I reinstalled Devuan GNU+Linux and got the Nvidia driver loaded.

With some of the recent Proton fixes, I need only vanilla Proton experimental as my compatibility layer.

I strained for a long time to get DXVK working. When I ran the game with PROTON_USE_WINED3D=1 in the launch command, the graphics would work only 2% of the time. But finally I found the magic instructions in a github issues discussion: DXVK_FILTER_DEVICE_NAME="NVIDIA".

So here is my full steam launch command for Age of Empires 2: Definitive Edition.

DXVK_FILTER_DEVICE_NAME="NVIDIA" PROTON_USE_DXVK=1 PROTON_LOG=1 DXVK_HUD=all %command% NOSTARTUP SKIPINTRO

Fixed the offending icons for audio volume notifications!

In a recent post I had a complicated workaround just to have my notification daemon use a different icon theme. But now in my Numix icon theme I've fixed the offending gray-on-gray icon. Check out this insanity:

sudo sed -i.bup -r -e "s/fill:#ececec;/fill:#000000;/g;" $( readlink -f $( find /usr/share/icons \( -iname 'audio-volume-medium.*' -o -iname 'audio-volume-low.*' -o -iname 'audio-volume-high.*' -o -iname 'audio-volume-*mute*' \) ! -iname '*block*' -ipath '*Numix/*' ) | sort -u )

The Unix master Foo would be pleased.

For myself whenever I find this again:

I find the exact icons I care about by fuzzy name (find), and then get target files (readlink -f) so I don't waste time on symlinks or trying to modify twice, and then remove duplicates (sort -u) and then I change the evil middling gray (#ececec;) to a visible black (#000000;). Ah, glorious visible icons. It's a shame the Numix only has up to size 24x24 icons for this one and my daemon doesn't try to scale the scaleable icon to 64x64 or something. That's a different problem for a different day.

pacpl in Devuan and gnudb

I have decided to use headless tools to rip my audio CDs, because that's easier to automate. I looked into options other than the nice and simple but graphical tool asunder, and found the Debian list for ripping software.

I tested out pacpl, which works well enough. However, as tends to happen in Debian, the tool points to cddb.org which is long gone. The open source folks use gnudb.org nowadays, but this is not reflected in the Devuan unstable package of gnudb. So you just have to adjust /etc/pacpl/pacpl.conf with this oneliner if you use my bgscripts package:

sudo python3 /usr/libexec/bgscripts/py/modconf.py -a -l '=' /etc/pacpl/pacpl.conf set CDDB_HOST gnudb.gnudb.org

If you don't use that, you need to set variable CDDB_HOST to gnudb.gnudb.org. Hopefully that was obvious.

Dosbox-x now in upstream Debian

I wrote recently about my Devuan dosbox-x package.

But now, I have learned that Dosbox-X is now in Debian proper. It only entered unstable last month, but it was in experimental right around the time I built my package for Devuan! So now my package should not be needed anymore, and it sounds like this package maintainer knows way more about building packages than I do.

Happy vintage Windows 98 gaming, everybody!

Zoneminder and php.ini

If you research Zoneminder and how to set the event log to show you event timestamps in your timezone, you will be told that php.ini used to be the way to configure your timezone but is not necessary anymore.

I don't know about how the timestamps are stored in the database. Any changes I made to the database config did not matter. What actually mattered was modifying my timezone in /etc/php.ini and restarting php-fpm.service. I am using Zoneminder rpms on Rocky Linux 9.

Change GTK icon theme for one program

Problem statement

I want one application to use a different icon theme than the other programs in my X11 desktop environment that uses GTK.

Solution

You can make a custom .config directory for this program, e.g., notification-daemon.

mkdir -p ~/.config/custom-notification-daemon/gtk-3.0/
cat <<EOF >~/.config/custom-notification-daemon/gtk-3.0/settings.ini
[Settings]
gtk-icon-theme-name = HighContrast
EOF

And then run your program:

XDG_CONFIG_HOME=~/.config/custom-notification-daemon notification-daemon &

I include the background command & merely because this example program is a daemon that should not occupy the current shell/thread.

References

  1. Failed Internet search for gtk3 set theme with environment variable which led to:
  2. Ripped directly from gnome - GTK - Enable/set dark theme on a per-application basis - Unix & Linux Stack Exchange

Aside

Does using anything labeled HighContrast make me officially old?

Helper script: pulsemixer-notification

Story

Due to a minor hardware problem where my sound card keeps jumping from aplay -l position 0 to position 1, my wine malfunctioned with audio. I set the specific card to use in the winecfg tab for Audio. After a reboot (and the alsa card position switched again), winecfg would crash on the Audio tab and I was unable to figure how to wipe the settings back to all defaults. This wine audio problem meant everything in wine that uses audio (every game) crashes. It was frustrating.

So I bit the bullet and installed pulseaudio on my Devuan GNU+Linux desktop. It was a matter of adding start-pulseaudio-x11 & to ~/.fluxbox/startup. I learned you need to keep alsa-utils and alsamixergui installed. Pulseaudio sits on top of alsa instead of replacing it.

There is a great program named pulsemixer that is a very powerful TUI and CLI tool for controlling your inputs and outputs. But I still needed alsamixer to disable the "auto-mute" when I plug in headphones, because I accidentally discovered a few years ago that if sound comes out of both speakers and headphones, it makes my life easier. I'm probably weird for that reason (and dozens of others), but I like it and I'm going to keep it that way, and it takes alsamixer to control that. (Take that, ... atheists? pulseaudio peeps anyways.)

So, pulsemixer does provide a great, simple way to toggle mute, and change volume up and down. I was only missing notifications about changing the volume.

Configuring other apps

volumeicon

I still use volumeicon but now I have configured middle-click to "open mixer," with mixer defined as pulsemixer --togle-mute. I found that while the volumeicon can still use its built-in "Mute Volume" setting, it mutes it on the alsa level, which mutes the master volume in pulseaudio, but it cannot unmute that pulseaudio master volume. So it is now always muted until I use pulsemixer.

I also had to disable volumeicon from handling the XF86AudioRaiseVolume keypresses. For that, I modified my fluxbox config.

fluxbox

For bare pulsemixer control you can just run:

122 :Exec pulsemixer --change-volume -5
123 :Exec pulsemixer --change-volume +5
121 :Exec pulsemixer --toggle-mute

Which worked well but it doesn't have a cute little popup telling me what the current volume level is. So I had to write the following script, and configure fluxbox to use it.

121 :Exec pulsemixer-notification toggle
122 :Exec pulsemixer-notification down
123 :Exec pulsemixer-notification up

The script

Explanation

This script displays a relative volume level (0/33/66/100) and muted symbol. It will always unmute if you change volume, which is the behavior I expect because that's how alsa handled volume. A neat little feature is that it uses a notification ID, so it can replace it if you're scrolling up on the volume; it will just replace the current notification instead of slowly (1.25s per step) trickling through them.

Listing

files/2023/10/listings/pulsemixer-notification (Source)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#!/bin/sh
# File: pulsemixer-notification
# Location: /usr/bin
# Author: bgstack15
# Startdate: 2023-10-04-4 09:06
# SPDX-License-Identifier: GPL-3.0
# Title: pulsemixer wrapper script that includes notifications
# Package: bgscripts
# Purpose: wrap around pulsemixer hotkey invocations to include status notifications
# History:
# Usage:
#    In ~/.fluxbox/keys:
#       121 :Exec pulsemixer-notification toggle
#       122 :Exec pulsemixer-notification down
#       123 :Exec pulsemixer-notification up
# Reference:
# Improve:
# Dependencies:
#    devuan-dep: pulsemixer, pulseaudio, notify-send
# Documentation:

# Load configuration
test -z "${devtty}" && devtty=/dev/null
test -z "${PN_TEMPFILE}" && PN_TEMPFILE=~/.cache/audio.temp
test -z "${PN_STEP}" && PN_STEP=3
test -z "${PN_EXPIRE_MS}" && PN_EXPIRE_MS=1250
test -f "${HOME}/.config/pulsemixer-notification" && . "${HOME}/.config/pulsemixer-notification"
test -f "/etc/pulsemixer-notification.conf" && . "/etc/pulsemixer-notification.conf"

# runtime
exec 1>>"${devtty}"
action="${1}"
unset icon vol relative_level message replacestring

case "${action}" in
   toggle)
      pulsemixer --toggle-mute
      case "$( pulsemixer --get-mute )" in
         0)
            action="unmute"
            ;;
         1)
            icon="audio-volume-muted"
            message="muted"
            ;;
      esac
      ;;
   up) pulsemixer --unmute ; pulsemixer --change-volume +"${PN_STEP}" ;;
   down) pulsemixer --unmute ; pulsemixer --change-volume -"${PN_STEP}" ;;
   *)
      echo "unknown: ${action}"
      ;;
esac

get_vol_level() {
   vol="$( pulsemixer --get-volume | awk '{print $1}' )"
   is_low="$( printf '%s\n' "${vol}<=33" | bc )"
   is_med="$( printf '%s\n' "(${vol}<=66)*(${vol}>33)" | bc )"
   is_hih="$( printf '%s\n' "${vol}>66" | bc )"
   is_off="$( printf '%s\n' "${vol}==0" | bc )"
   test "${is_low}" = "1" && printf '%s' "low" && return 0
   test "${is_med}" = "1" && printf '%s' "medium" && return 0
   test "${is_hih}" = "1" && printf '%s' "high" && return 0
   test "${is_off}" = "1" && printf '%s' "muted" && return 0
}

# awk: cheat and just use left-side volume of stereo volume.
vol="$( pulsemixer --get-volume | awk '{print $1}' )"
relative_level="$( get_vol_level )"

case "${action}" in
   up|down)
      icon="audio-volume-${relative_level}"
      message="${vol}"
      ;;
   unmute)
      icon="audio-volume-${relative_level}"
      message="unmuted"
      ;;
esac

test -f "${PN_TEMPFILE}" && replacestring="--replace-id=$( cat "${PN_TEMPFILE}" 2>/dev/null )"
# leave replacestring unquoted in case it is empty
notify-send ${replacestring} --icon "${icon}" --transient "${message}" --urgency low --expire-time "${PN_EXPIRE_MS}" --print-id > "${PN_TEMPFILE}"