Unattended updates for Devuan Ceres, 2021 edition

This is an update to Unattended software updates on Devuan (2018).

I use Devuan Ceres, which is the rolling release of Devuan (Debian without systemd): It’s the equivalent of Debian Sid. One of the by-products of it being a rolling release is that it on occasion changes release names (from Beowulf to Chimaera, for example). To handle updating the packages when that occurs, you have to run a dist-upgrade, which can get tricky.

My whole script for updating unattended (which is a misnomer) follows.

# File: update-devuan.sh
# Location: /mnt/public/Support/Platforms/devuan
# Author: bgstack15
# Startdate: 2019-07-01 18:48
# Title: The command to run for mostly-unattended updates on Devuan
# Purpose: Mostly-unattended apt-get dist-upgrade
# History:
#    2019-12-15 add the y/n if dist-upgrade will remove packages
#    2020-02-26 add --allow-downgrades for the libqt5core5a which was customized for freefilesync or similar
# Usage:
# Reference:
# Improve:
# Documentation:

export PATH=$PATH:/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games

test "${1}" = "preseed" && extrastring="--allow-downgrades"

mkdir -p ~/log
myupdate() {
   sudo apt-get update ;
   sudo DEBIAN_FRONTEND=noninteractive apt-get -q -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" -o Dpkg::Options::="--force-overwrite" upgrade ;
   ___remove_count="$( yes no | sudo apt-get dist-upgrade 2>&1 | grep -oiE '[0-9]+ to remove' | grep -oE '[0-9]*' )"
   if test "${___remove_count}" = "0" ;
      ___to_remove="$( yes no | sudo apt-get dist-upgrade 2>&1 | awk 'BEGIN{a=0} /^[ \s]/{ if(a==1)print;} /^[a-zA-Z]/ {if(/REMOVED/ || /NEW /){a=1;print} else {a=0}}' )"
      echo "${___to_remove}" 1>&2
      echo "WARNING: are you sure you want to do this [yN]? " 1>&2
      test -z "${extrastring}" && { read response ; } || ___do_run="yes"
      if test "$( echo "${response}" | cut -c1 2>/dev/null | tr '[A-Z]' '[a-z]' )" = "y" ; then ___do_run="yes" ; fi
   if test "${___do_run}" = "yes" ;
      sudo DEBIAN_FRONTEND=noninteractive apt-get -q ${extrastring} -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" -o Dpkg::Options::="--force-overwrite" dist-upgrade ;__f=$? ; 
   date ; return ${__f} ;
} ; myupdate 2>&1 | tee -a ~/log/apt-get.upgrade.$( date "+%F" ).log

I open up GNU screen (screen -S updates) and run it.


It handles the regular upgrade process just fine. It will then parse the output of a dry-run of apt-get dist-upgrade for any removals. If there are zero removed packages, it will plow right into dist-upgrading. If there are any removals, it will pause and wait for input. So it’s not entirely unattended, but it is at a good pause point if any manual approval is required before re-running the whole operation.

I suppose one could learn how to insert these Dpkg::Options into apt.conf or dpkg.conf or somewhere, but I’m not sure that I would want them around permanently, ergo the long invocations here.

How do you, my readers, do you update your systems (mostly) unattended?

Adding redirect from /git to /cgit for web browsers

As part of the Cgit solution for my network, I added some logic that will redirect web browsers from any /git URLs to the respective /cgit URLs. Maybe someday I will figure how to serve both cgit and git on just /git, but I can deal with this forwarding arrangement for now.

Change the httpd directive from ScriptAlias /git/ /usr/libexec/git-core/git-http-backend-custom/ to

ScriptAlias /git/ /usr/sbin/git-http-backend-custom/

And the trailing slash is important!

Write new file /usr/sbin/git-http-backend-custom which checks the user agent string and passes git to the real backend utility.

# File: /usr/sbin/git-http-backend-custom
# Startdate: 2021-04-25
# Part of cgit solution for internal network
unset DEBUG
if echo "${HTTP_USER_AGENT}" | grep -qiE '^git' ;
   #printf 'Content-type: text/html\n\n'
   #printf "need to run real git backend\n"
   #cat 1>/dev/null
   /usr/libexec/git-core/git-http-backend "${@}"
   printf 'Content-type: text/html\n\n'
   if test -n "${DEBUG}" ;
      printf "<pre>\n"
      echo "${0} ${@}"
      printf "</pre>\n"
      printf "<meta http-equiv=\"refresh\" content=\"0; URL=%s\" />\n" "/cgit${PATH_INFO}"
      cat 1>/dev/null

Populating my new cgit instance


Gitlab is the curent source of truth for all repos, except for the secure data stored locally at /mnt/public/packages.
To prepare my on-prem git repos, I need to be be able to synchronize my repositories from the sources of truth. This process sets up the bare repos on the main git and web server, and sets up the sync-location git repos on any VM.

Preparing list assets

We need the csv lists that show the old and new locations.

Generating gitlab token

Visit gitlab.com in web browser, sign in, and visit “Edit profile” in user icon menu. Visit “Access Tokens” in left-side menu, and create a personal access token that is read-only. Save the token, which will resemble string `MnrEnTVfA-7kujMarjsG`, to file /mnt/public/work/gitlab/gitlab.com.personal_access_token.

Generating list of all relevant projects

Use generate-list.sh to pull the list of all my personal projects from gitlab. It is not complete, because even Your projects on gitlab shows 70 projects total, not the 60 the API returns. So after running that file, manually add any additional entries that should be synced.

./generate-list.sh > list.csv
# manually add any additional repos to list.csv.

To add the destination links, run add-dest.sh with redirection in and out.

< list.csv ./add-dest.sh > repos.csv

Preparing main git destinations

With all the lists created, now prepare the final destinations of the repositories.

Preparing blank repositories on git server

You need to make the blank repositories ahead of time due to how git-over-http works: It only accepts pushes for extant projects. For this task, run make-blank.sh.

time < repos.csv sh -x ./make-blank.sh

On main web server, fix the permissions of these new git repos.

sudo chgrp apache -R /mnt/public/www/git ; sudo chmod g+rwX -R /mnt/public/www/git ;

Setting up sync locations and synchronizing

Now that the destinations are prepared, use a temporary (or at least alternate) location to pull and then push the repositories.

Initialize sync-location git repos

VM d2-03a is the main implementation of the sync-location. This will be the system that performs the main work of pulling all git repos and contents down, and pushing them up to the server.

time OUTDIR=~/dev/sync-git INFILE=/mnt/public/Support/Programs/cgit/populate/repos.csv /mnt/public/Support/Programs/cgit/populate/populate-git-remotes.sh

The above command will need APPLY=1 as a variable when ready for real execution.

Perform synchronization of all git repos

This is the main operation of this whole process. It could take some time to execute.

INDIR=~/dev/sync-git /mnt/public/Support/Programs/cgit/populate/sync-all.sh

Build initial permissions list

To restrict push access to all the new repos, run this command, and save its output inside /etc/git_access.conf.

find /var/www/git -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | awk '{print "Use Project "$0" \"user bgstack15\" \"all granted\""}' | sort

And of course an httpd -t and then reload.

Appendix A: File listings


# Startdate: 2021-04-16 20:16
# Goal: generate csv to stdout of directory,origin,dest
# STEP 1
# Notes:
#    the gitlab api doesn't show the "contributed" projects in the API at all, so the output from this is incomplete. The output file will need to be curated with additional entries for projects not from my userspace.
# Dependencies:
#    Gitlab token at /mnt/public/work/gitlab/gitlab.com.personal_access_token
#    jq
# Reference: 
#    https://stackoverflow.com/questions/57242240/jq-object-cannot-be-csv-formatted-only-array
#    https://stackoverflow.com/questions/32960857/how-to-convert-arbitrary-simple-json-to-csv-using-jq/32965227#32965227

test -z "${TOKEN_FILE}" && TOKEN_FILE="/mnt/public/work/gitlab/gitlab.com.personal_access_token"

test ! -r "${TOKEN_FILE}" && { echo "Fatal! Cannot find token file ${TOKEN_FILE}. Aborted." 1>&2 ; exit 1 ; }
TOKEN="$( cat "${TOKEN_FILE}" )"
echo "${TOKEN}" | grep -qE "token:" && { TOKEN="$( echo "${TOKEN}" | awk '/^token:/{print $NF}' )" ; }


# Functions
handle_pagination() {
   # call: handle_pagination "https://gitlab.com/api/v4/users/${GUSER}/projects"
   # return: stdout: a single json list of of all returned objects from all pages
   ___hp_next_link="dummy value"
   ___hp_MAX=30 # safety valve
   while test -n "${___hp_next_link}" && test ${x} -lt ${___hp_MAX} ;
      raw="$( curl --include --header "PRIVATE-TOKEN: ${TOKEN}" "${___hp_thisurl}" )"
      set +x ; links="$( echo "${raw}" | awk '/^link:/' )" ; set -x
      ___hp_next_link="$( echo "${links}" | tr ',' '\n' | sed -n -r -e '/rel="next"/{s/.*<//;s/>;.*$//;p;}' )" # will be blank if next_link is not valid; that is, if this is the last page.
      set +x ; ___hp_json="${___hp_json}$( echo "${raw}" | awk '/^\[/' )" ; set -x
   # combine all json lists into one
   # ref: https://stackoverflow.com/a/34477713/3569534
   echo "${___hp_json}" | jq --compact-output --null-input 'reduce inputs as $in (null; . + $in)'

json="$( handle_pagination "https://gitlab.com/api/v4/users/${GUSER}/projects" )"
echo "${json}" | jq '.[] | [{ web_url, path }]' | jq -r '(map(keys) | add | unique) as $cols | map(. as $row|$cols|map($row[.])) as $rows | $cols, $rows[] | @csv' | awk 'NR == 1 {print} NR >1 && !/"web_url"/{print}'


Just a snippet:



# Startdate: 2021-04-17
# STEP 2
# Goal: fix column names, and also parse to add dest column.
test -z "${GIT_URL_BASE}" && GIT_URL_BASE="https://www.example.com/git"
   # fix column name, and then add the dest link
   sed -r -e '1s/web_url/origin/;' | tr -d '"' | \
   awk -v "topurl=${GIT_URL_BASE}" -F',' 'BEGIN{OFS=","} NR==1 {print $0",dest"} NR>1 {$NF=$NF","topurl"/"$1;print}'


The final list asset. Just a snippet:



# Startdate: 2021-04-17 16:13
# Goal: given the repos.csv output from STEP 2 add-dest.sh script, make the blank git repos for each of those on the final destination server
# STEP 3

test -z "${GIT_URL_BASE}" && GIT_URL_BASE="https://www.example.com/git"
test -z "${GIT_TOP_DIR}" && GIT_TOP_DIR="/mnt/public/www/git"

cd "${GIT_TOP_DIR}"
# this awk will read stdin, and skip the first line which is the headers for the columns
for word in $( awk -F',' -v "topurl=${GIT_URL_BASE%%/}/" 'NR>1 {gsub(topurl,"",$3);print $3}' ) ;
   # if OVERWRITE and the dir already exists, then delete it
   test -d "${word}" && test -n "${OVERWRITE}" && rm -r "${word}"

   # If inside a namespace, then perform a few extra steps.
   echo "${word}" | grep -qE "\/" && {
      # make any subdirs between here and there
      mkdir -p "${word}"
   # DISABLED; can just use section-from-path=1 in main cgitrc
   ## if in a subdir, add a cgitrc file for this repo that indicates its section.
   #   section="$( echo "${word}" | awk -F'/' 'BEGIN{OFS="/"} {$NF="";print}' )"
   #   if ! grep -qE "section=.+" "${word}/cgitrc" 2>/dev/null ;
   #   then
   #      echo "section=${section%%/}" >> "${word}/cgitrc"
   #   fi

   # actually make the blank git repo
   git init --bare "${word}" &


# STEP 5 repeating
# Startdate: 2020-05-21
# Goal: download every single git repository in full from bitbucket cloud for migration to bitbucket on-prem.
# History:
#    2021-04-17 forked from gituser.tgz to Support/Programs/cgit/populate project
# Usage:
#    INDIR=~/dev/sync-git 
# References:
#    git-sync-all.ps1
# Dependencies:


if test -z "${INDIR}" || ! test -r "${INDIR}" ;
   echo "Fatal! Invalid INDIR ${INDIR} which is either absent or unreadable. Aborted" 1>&2
   exit 1

cd "${INDIR}"
for dir in $( find . -maxdepth 2 -mindepth 1 -type d -name '.git' -printf '%h\n' ) ;
   x=$(( x=x+1 ))
   lecho "Starting repo $x ${dir}"
   cd "${INDIR}/${dir}"


# STEP 5 or manual
# Startdate: 2020-05-21
# Goal: sync just this one directory git repo.
# History:
#    2021-04-17 forked from gituser.tgz to Supoprt/Programs/cgit/populate project
# References:
#    How to actually pull all branches from the remote https://stackoverflow.com/questions/67699/how-to-clone-all-remote-branches-in-git/16563327#16563327
# Dependencies:
#    $PWD is the git repo in question.
git pull --all
   git branch -a | sed -n "/\/HEAD /d; /remotes\/origin/p;" | xargs -L1 git checkout -t
} 2>&1 | grep -vE 'fatal:.* already exists'
git push dest --all

Cgit solution for my network


Experimentation using gitweb and cgit was carried out on d2-02a at some point before 2020-10. Cgit on Devuan (d2-02a) was examined after investigating a key Stackoverflow post and determined to meet my needs.


Git-scm itself provides native mechanisms to allow operations through http, and basic Apache httpd configuration examples abound. Adding a decent web frontend with cgit is a small additional step. This setup also uses Apache httpd basic auth to limit read and write access to the true git projects, but not the cgit simple http clone operations.


Apache httpd is installed.
TLS certificate exists and is already in use.

Installing entire cgit solution

Installing and configuring cgit

Install cgit from stackrpms repo for el8, which uses https://src.fedoraproject.org/fork/tmz/rpms/cgit or https://src.fedoraproject.org/forks/tmz/rpms/cgit.git to build the rpm. Tmz is the Fedora maintainer of this package, so his adaptation of it for el8 is trustworthy.

sudo dnf install cgit

Modify /etc/cgitrc with my own customizations. See internal file /mnt/public/Support/Programs/cgit/files/cgitrc for the original configuration.
Install a few dependencies. Python3-markdown is also from stackrpms for el8, because I had to build it on my copr, also from its source https://src.fedoraproject.org/rpms/python-markdown.

sudo dnf install python3-pygments python3-markdown

These dependencies support displaying markdown (.md) files as the cgit and repo “about” pages. I tend to use markdown for these types of files.
Copy in a logo file for myself to /usr/share/cgit/bgstack15_64.png.
The top-level readme file is configured in the example cgitrc to /var/www/git/readme.md

Configuring httpd

Set up file /etc/git_access.conf with snippets that will be included by the httpd config. This file will control access to the git repositories over git itself, not the cgit web presentation.

# File /etc/git_access.conf
# Part of cgit solution for Mersey network, 2021-04-15
# The last phrase can be "all granted" to allow anybody to read.
# Use httpd "Require" strings for param2, param3. Param2 grants read/write permission, Param3 is read-only.
#Use Project dirname "user alice bob charlie" "all granted"
#Use Project dirname "user charlie" "user bob alice"

Use Project certreq "user bgstack15" "all granted"

In the above example, the “certreq” is a project name (directory name under /var/www/git). The second and third parameters, the “user bgstack15” and “all granted” will be passed to Apache “Require” directives. They represent “read-write access” and “read-only access” respectively. Apache interprets the example, resolved “Required all granted” as anonymous read access is allowed.

One of the main operations described under the Operations heading below is adding new entries to this list of projects.

Set up file /etc/gitweb.conf which is traditionally for gitweb, a different implementation of serving git repos over http, but can also be used here.

$export_auth_hook = sub {
    my $repo = shift;
    my $user = $cgi->remote_user;
    if($repo =~ s/\/var\/www\/git\///) {
        open FILE, '/etc/git_access.conf'; 
        while(<FILE>) {
            if ($_ =~ m/Use Project $repo \"(.*)\" \"(.*)\"/)
                my $users = $1 . ' ' . $2;
                $users =~ s/all granted/$user/;
                $users =~ s/user//;
                if ( $users =~ m/$user/ ) {
                    return 1;
    return 0;

I do not understand this perl file: It came straight from reference 2.
Set up file /etc/git_access with htpasswd(1).

sudo htpasswd -c /etc/git_access bgstack15

Set up file /etc/httpd/conf.d/cgit.conf with modifications. This file is laid down by the cgit rpm, but might need some adjustments. The exact contents as of the time of this writing are the following.

Alias /cgit-data /usr/share/cgit
ScriptAlias /cgit /var/www/cgi-bin/cgit
RedirectMatch ^/cgit$ /git/
<Directory "/usr/share/cgit/">
    AllowOverride None
    Require all granted

Set up the apache virtualhost. For server storage1, this means modifying extant file /etc/httpd/conf.d/local_mirror.conf. Inside the main VirtualHost definition (the port 80 one), add contents:

# cgit + git. See /mnt/public/Support/Programs/cgit/cgit-README.md
SetEnv GIT_PROJECT_ROOT /var/www/git
SetEnv GITWEB_CONFIG /etc/gitweb.conf

ScriptAlias /git/ /usr/libexec/git-core/git-http-backend/
<Directory "/usr/libexec/git-core*">
    Options +ExecCGI +Indexes
    Order allow,deny
    Allow from all
    Require all granted

# a2enmod macro # for Devuan
<Macro Project $repository $rwstring $rostring>
    <LocationMatch "^/git/$repository.*$">
        AuthType Basic
        AuthName "Git Access"
        AuthUserFile /etc/git_access
        Require $rwstring
        Require $rostring
    <LocationMatch "^/git/$repository/git-receive-pack$">
        AuthType Basic
        AuthName "Git Access"
        AuthUserFile /etc/git_access
        Require $rwstring
# Devuan apache2 works with IncludeOptional but EL8 httpd doesn't work with it.
Include /etc/git_access.conf

# https://ic3man5.wordpress.com/2013/01/26/installing-cgit-on-debian/
# depends on conf-enabled/cgit.conf (available as cgit.conf.devuan)
<Directory "/usr/share/cgit/">
    SetEnv CGIT_CONFIG /etc/cgitrc
    SetEnv GIT_URL cgit
    AllowOverride all
    Options +ExecCGI +FollowSymLinks +Indexes
    DirectoryIndex cgit.cgi
    AddHandler cgi-script .cgi
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule (.*) /cgit/cgit.cgi/$1 [END,QSA]

After making changes to these files, test (sudo httpd -t), and then reload or restart httpd.

Configure SELinux

I used the standard mechanisms to troubleshoot SELinux.

sudo setenforce 0
semodule --disable_dontaudit --build
echo "" | sudo tee /var/log/audit/audit.log 1>/dev/null
# perform a large number of git and cgit operations
sudo tail -n15000 /var/log/audit/audit.log | audit2allow -M foo
# manually merge any new entries into cgitmersey.te
semodule --build
_func() {sudo checkmodule -M -m -o cgitmersey.mod cgitmersey.te && sudo semodule_package -o cgitmersey.pp -m cgitmersey.mod && sudo semodule -i cgitmersey.pp ; } ; time _func

Final asset is cgitmersey.te which can be loaded and installed with the last line of the above command block.

module cgitmersey 1.0;

require {
type git_script_t;
type httpd_t;
type httpd_cache_t;
type var_t;
class process { noatsecure rlimitinh siginh };
class file { getattr map open read };
class dir { getattr open read search };

#============= git_script_t ==============
allow git_script_t var_t:dir read;
allow git_script_t var_t:file { read getattr open };
allow git_script_t httpd_cache_t:dir { getattr open read search };
allow git_script_t httpd_cache_t:file { map getattr open read };

#============= httpd_t ==============
allow httpd_t git_script_t:process { noatsecure rlimitinh siginh };

Populating cgit with my content

See separate blog post Populating my New Cgit Instance for this whole task.

Summary of associated files

  • /etc/gitweb.conf
  • /usr/share/cgit/bgstack15_64.png
  • /etc/git_access.conf
  • /etc/git_access
  • /etc/cgitrc
    # Disable owner on index page
    # Allow http transport git clone
    # Show extra links for each repository on the index page
    # Enable blame page and create links to it from tree page
    # Enable ASCII art commit history graph on the log pages
    # Show number of affected files per commit on the log pages
    # Show number of added/removed lines per commit on the log pages
    # Sort branches by age or name
    # Use a custom logo
    # Enable statistics per week, month, quarter, or year
    # Set the title and heading of the repository index page
    root-title=Stackrpms git
    root-desc=Bgstack15 local repos
    # Allow download of tar.gz, tar.bz2 and zip-files
    snapshots=tar.gz tar.bz2 zip
    # Enable syntax highlighting (requires the highlight package)
    # Format markdown, restructuredtext, manpages, text files, and html files
    # through the right converters
  • /etc/httpd/conf.d/cgit.conf
  • /var/www/git/readme.md
  • /mnt/public/Support/Programs/cgit/cgitmersey.te


Some useful operations are described here.

Making a new project

To set up a new project that can receive pushes, you need to initialize a new git bare repo.

Make the “newname” project in /var/www/git, and grant access to the apache user.

cd /var/www/git ; git init --bare newname ; sudo chgrp apache -R newname

Set up permissions for the new project in /etc/git_access.conf. For example:

Use Project "newname" "user adminuser1 adminuser2 adminuser3" "user1 user2 user3"

After making changes, ensure httpd accepts the new config (because it loads /etc/git_access.conf as an included file), and then reload httpd.

sudo httpd -t
sudo systemctl reload httpd

Changing cgit-related attributes of a project

Cgit can be configured to read a file named cgitrc within a git repository. I tend to use just the top directory of these bare git repos, so for project certreq, use /var/www/git/certreq/cgitrc.
See cgitrc(5) for more info, but a brief list of useful attributes (var=value) include: defbranch, desc, hide, homepage, ignore, logo, name, owner, readme, section, url.
For the description of a repo that appears in the main index, use regular file in a git bare repo: description.

Adding a new user

Use regular htpasswd mechanism to add users or change passwords. See htpasswd(1).

htpasswd /etc/git_access bgstack15

Removing a project

Just delete the project directory.

Changing cgitrc values

Changing cgitrc values take effect immediately (when caching is set to 0); no httpd reload is necessary.


  1. https://stackoverflow.com/questions/26734933/how-to-set-up-git-over-http git_access.conf
  2. https://stackoverflow.com/a/50317063/3569534 /etc/gitweb.conf and httpd.conf macro and other snippets
  3. https://stackoverflow.com/questions/40924641/setting-up-git-http-backend-with-apache-2-4
  4. https://stackoverflow.com/a/2200662/3569534 had to convert my sample project to a bare git one

Auxiliary reading

  1. https://ic3man5.wordpress.com/2013/01/26/installing-cgit-on-debian/

Replicate some functionality of SenseiDE on GNU/Linux installation of AoE2DE

I run Age of Empires 2 Definitive Edition on my Fedora system. A fellow clan member showed me Greg Stein’s SenseiDE which makes a great control panel for enabling/disabling some extraneous features of the game.

Due to the open source nature of this neat tool, I have been able to replicate some of the tasks for my installation, in shell! Go check out aoe2de-seteffects on my gitlab.
SenseiDE with GNU logo superimposed

Chicago95 icon theme for LibreOffice

I have started a project for converting the LibreOffice Writer and Calc experiences to as close to the Microsoft Office 95 experience as I could. You can contribute to the project on gitlab: https://gitlab.com/bgstack15/libreoffice-chicago95-iconset.

Some instructions from the README:

To assemble the `images_chicago95.zip` file and also the .oxt file, manually inspect and run build.sh.


To place the iconset in the public location for a 64-bit Linux distro:

    sudo cp -p Chicago95-theme/iconsets/images_chicago95.zip /usr/lib64/libreoffice/share/config/

The OOTB (out-of-the-box) experience is now complete, but there are way more buttons to finish. Some of these buttons are hand-drawn, because a modern office suite does a lot more than Office 95 ever did.

LibreOffice Calc with the Chicago95 iconset LibreOffice Writer with the Chicago95 iconset
Contributions are welcome!

Save webfonts for specific sites

If you disable webfonts (aka remote fonts) in your web browser, you might discover that many sites use fonts for all sorts of glyphs that you wouldn’t have thought were stored as fonts. While that’s a neat technical trick, it can make navigation difficult when you disable those remote fonts. I won’t ramble about why one might choose to disable webfonts.

What I will do is show how I allow fonts for a specific site (or page), so that my system caches the fonts locally so I can view things correctly! I wrote a tool, save-webfonts (Gitlab), that parses through the given URL(s), finds the css files, finds the listed fonts, and saves those down.

The project has a few gaps, such as it just downloads all ttf, woff, woff2, and eot files listed, even though they might be duplicates.

However, it does have a few nice features. If you are well-versed in fontconfig, you’ll realize that of course ttf is the native format for fonts for displaying locally. Woff2 and the older woff format are decent choices for webfonts, but not useful locally. These non-ttf formats can be converted down, however, to ttf, which this script handles for you when you add the –convert flag.

So take back control of your web browsing by choosing whose fonts you use! I started with the Open Build Service (SuSE OBS) and COmmunity PRojects (Fedora copr).

Convert woff to ttf in python

Apparently it’s so hard on the Internet to search for the answer to query “convert woff to ttf python”.

But an important nugget of information is available on a random github issue, where anthrotype said:

Or via python code, you can

from fontTools import ttLib

font = ttLib.TTFont("MyFont.woff")
font.flavor = None  # was "woff"; `None` restores the default value, for non-compressed OpenType

Use system package python3-fonttools. This is for a woff version 1 (not woff2) file specifically, but this library supports multiple filetypes so it will probably do woff2 just fine.

Drawing a globe in pixel art

I have been working on a side project which someday I’ll publish. I wanted to draw a hyperlink icon. I decided the best thing to do is draw a globe, and a little chain link in front of it, all within a 24×24 canvas.
I have drawn some circles by hand, but larger diameters get tricky. I eventually just searched it, and found a great little webapp: https://pcg.cytodev.io/.
I plugged in my radius of 12, and it generated just slightly too big a curve to fit correctly in the 24×24 space. So a radius of 11 worked great!
Screenshot showing the input and output for the Pixel Circle Generator web app, of radius 11
Using gimp, it is easy to copy this, flip it as needed, and make a complete circle.
Whole circle, with transparency, in GIMP
And then to make it look like a globe, I added some lines to resemble latitude and longitude, this time against a background instead of the transparency.
Whole circle with lines that help make it look like a globe
And the final asset has a little chain in front of the globe:
Final hyperlink image, 24x24

Send desktop notifications through ssh X forwarding

Thanks directly to a user at StackExchange!

If you have desktop notifications on your ssh server that you wish to display on your local X server, you can accomplish that with a few steps.

In the session you intend to forward desktop notifications, you need to make sure the DBUS_* environment variables are set. From Unix.SE:

START="dbus-launch --exit-with-session"
# set dbus for remote SSH connections
if [ -n "$SSH_CLIENT" -a -n "$DISPLAY" ]; then
   machine_id=$(LANGUAGE=C hostnamectl|grep 'Machine ID:'| sed 's/^.*: //')
   x_display=$(echo $DISPLAY|sed 's/^.*:\([0-9]\+\)\(\.[0-9]\+\)*$/\1/')
   if [ -r "$dbus_session_file" ]; then
      export $(grep '^DBUS.*=' "$dbus_session_file")
      # check if PID still running, if not launch dbus
      ps $DBUS_SESSION_BUS_PID | tail -1 | grep dbus-daemon 1> /dev/null 2>&1
      [ "$?" != "0" ] && export $(${START}) 1> /dev/null 2>&1
      export $(${START}) 1> /dev/null 2>&1

I set this ~/bin/forward-notifications.sh, but the author places it directly in his ~/.bashrc. If this snippet is in a separate script the way I use it, you need to dot-source it.

. forward-notifications.sh

But you still also need at least one .service file that handles org.freedesktop.Notifications. I use Xfce on my X server system, so I just installed its notification library.

$ apt-file search .service | grep -i notif
xfce4-notifyd: /usr/lib/systemd/user/xfce4-notifyd.service
$ sudo apt-get install xfce4-notifyd

And then your notify-send or zenity –notification commands will work!