dotfiles

Notes on NixOS, Home Manager, and software in general.

Quickstart:

git clone https://github.com/stephen-huan/nixos-config ~/.config/home-manager
sudo nixos-rebuild switch --flake ~/.config/home-manager

Nix

"Nix" is an ambiguous term that can refer to the Nix programming language, the package manager, the collection of packages Nixpkgs, and the operating system NixOS (at the very least).

These parts are documented in the Nix Reference Manual, the Nixpkgs Manual, the NixOS Manual, a short-form tutorial series called the Nix Pills, and the official documentation nix.dev.

nix (language)

The first layer in the stack is the Nix DSL (or the Nix expression language). The official documentation provides a quick tutorial and the official manual provides a comprehensive reference.

The editor tooling I use is

Despite the relative simplicity of the language ("JSON with functions"), there can still be unusual behavior. Within a few days of playing around with the expression language, I found what I thought was a interpreter bug. I posted it on the Discourse (and filed an issue), which caught the attention of a long-time Nix contributor, which led to increased attention on a few old outstanding issues and finally led to a fix which landed in 2.17. So even very simple languages can have nasty parser bugs!

nix (package manager)

Important paths

  • /nix/store: Nix store (built derivations, where everything lives)
  • /nix/var/nix/gcroots: Roots of the garbage collector
  • /nix/var/nix/profiles: System profiles
  • /nix/var/nix/profiles/system: Current system
  • /etc/profiles/per-user/<user>: User packages
  • ~/.local/state/nix/profiles: User profiles (if using xdg)
  • ~/.local/state/nix/profiles/home-manager: Current Home Manager generation
  • ~/.nix-profile (if not using xdg)

nix (cli)

Here are some quick recipes for common tasks.

  • enter a (bash) shell with a given package

    nix-shell -I nixpkgs=$(nixpkgs) -p python3
    

    or, to use the current shell,

    nix shell $(nixpkgs)#python3
    

    where the $(nixpkgs) syntax is explained in "removing channels and flake registries".

  • query list of dependencies of (current) system

    nix-store --query --requisites /nix/var/nix/profiles/system
    
  • in tree format

    nix-store --query --tree /nix/var/nix/profiles/system
    
  • list of things referring to a store path

    nix-store --query --referrers <store-path>
    
  • optimize nix store (dedup)

    nix-store --optimise
    

    or

    nix store optimise
    
  • garbage collection

    nix-env --delete-generations old # all non-current generations
    nix-env --delete-generations 14d # generations older than 14 days
    nix-store --gc
    

    or use nix-collect-garbage -d which essentially wraps the above

    nix-collect-garbage --delete-old
    nix-collect-garbage --delete-older-than 14d
    

    There is a difference between running with sudo (system) and no sudo (user); try --dry-run.

  • verify nix store paths are valid (hashes match and it is trusted)

    nix store verify --all
    
  • why does one package depend on another?

    nix why-depends $(nixpkgs)#zotero $(nixpkgs)#nss
    
    /nix/store/ihp6sm6xn1q19pblxb968q3cm8x9aimq-zotero-6.0.27
    └───/nix/store/mbyn9dp2pf3vfsp82g0a289ldck3xibw-nss-3.90
    
  • why does my (current) system depend on a package?

    nix why-depends /nix/var/nix/profiles/system $(nixpkgs)#nss
    
    /nix/store/7hjlhfzzf4ricswgm1wzvpaac34pwvbm-nixos-system-sora-23.11.20231009.f99e5f0
    └───/nix/store/bvsjja2xsx2z68h52wxwcriw9vjjzazb-etc
        └───/nix/store/6xk1k4kl42qqkds2vrprm0mbp1k2mn0l-user-environment
            └───/nix/store/baxyh4bqi0amw2pi6gv5c28b6lr75jzb-home-manager-path
                └───/nix/store/ihp6sm6xn1q19pblxb968q3cm8x9aimq-zotero-6.0.27
                    └───/nix/store/mbyn9dp2pf3vfsp82g0a289ldck3xibw-nss-3.90
    
  • pass --precise to see more information on each edge

    /nix/store/7hjlhfzzf4ricswgm1wzvpaac34pwvbm-nixos-system-sora-23.11.20231009.f99e5f0
        → /nix/store/bvsjja2xsx2z68h52wxwcriw9vjjzazb-etc
            → /nix/store/6xk1k4kl42qqkds2vrprm0mbp1k2mn0l-user-environment
                → /nix/store/baxyh4bqi0amw2pi6gv5c28b6lr75jzb-home-manager-path
                    → /nix/store/ihp6sm6xn1q19pblxb968q3cm8x9aimq-zotero-6.0.27
                        → /nix/store/mbyn9dp2pf3vfsp82g0a289ldck3xibw-nss-3.90
    └───activate: …fsx38qi-setup-etc.pl /nix/store/bvsjja2xsx2z68h52wxwcriw9vjjzazb-etc/etc...if (( _localstatus > …
        └───etc/profiles/per-user/ikue -> /nix/store/6xk1k4kl42qqkds2vrprm0mbp1k2mn0l-user-environment
            └───bin/accessdb -> /nix/store/baxyh4bqi0amw2pi6gv5c28b6lr75jzb-home-manager-path/bin/accessdb
                └───bin/.zotero-wrapped -> /nix/store/ihp6sm6xn1q19pblxb968q3cm8x9aimq-zotero-6.0.27/bin/.zotero-wrapped
                    └───usr/lib/zotero-bin-6.0.27/gmp-clearkey/0.1/libclearkey.so: …sm01mc-nspr-4.35/lib:/nix/store/mbyn9dp2pf3vfsp82g0a289ldck3xibw-nss-3.90/lib:/nix/store/73whsps…
    
  • and --all for all paths, not just the shortest one (or both flags)

    /nix/store/7hjlhfzzf4ricswgm1wzvpaac34pwvbm-nixos-system-sora-23.11.20231009.f99e5f0
    └───/nix/store/bvsjja2xsx2z68h52wxwcriw9vjjzazb-etc
        ├───/nix/store/6xk1k4kl42qqkds2vrprm0mbp1k2mn0l-user-environment
        │   └───/nix/store/baxyh4bqi0amw2pi6gv5c28b6lr75jzb-home-manager-path
        │       ├───/nix/store/ihp6sm6xn1q19pblxb968q3cm8x9aimq-zotero-6.0.27
        │       │   └───/nix/store/mbyn9dp2pf3vfsp82g0a289ldck3xibw-nss-3.90
        │       └───/nix/store/9qmg4vg8hrs6pbbd4cxjrq4jb8fcyxk7-chromium-117.0.5938.149
        │           └───/nix/store/ypas0qsb3ikz6k84bk8q89qjlyr9snk5-chromium-unwrapped-117.0.5938.149
        │               └───/nix/store/mbyn9dp2pf3vfsp82g0a289ldck3xibw-nss-3.90
        └───/nix/store/hrm25v2z602j1qywsia9x638wv1l41f5-system-units
            └───/nix/store/3dxhmg5jabmihc14j8m0b1rlaq6p3inq-unit-home-manager-ikue.service
                └───/nix/store/j6xq61kffmfzqcnhgd32ia13z8yl3hk0-home-manager-generation
                    ├───/nix/store/baxyh4bqi0amw2pi6gv5c28b6lr75jzb-home-manager-path
                    └───/nix/store/fncqlph162dpxh4x4879m2y6zy33fkyf-home-manager-files
                        └───/nix/store/3haw6qx1gmyka60xnrs8mi7d4c81pv6l-hm_fontconfigconf.d10hmfonts.conf
                            └───/nix/store/baxyh4bqi0amw2pi6gv5c28b6lr75jzb-home-manager-path
    

Nixpkgs

Nixpkgs is the package repository. There are search engines for packages and library functions.

NUR

Like Arch, there is a Nix User Repository with an associated package repository and search engine.

nix-index

nix-index is a nixpkgs search tool. There are also pre-built databases and an auto-run wrapper.

Generate the index with

nix-index --nixpkgs $(nixpkgs)

and search for something with

nix-locate bin/mdbook
mdbook-open-on-gh.out                         3,824,576 x /nix/store/7f33s7i9q9qr3l4ahpbpgrqp2icgml0n-mdbook-open-on-gh-2.4.1/bin/mdbook-open-on-gh
mdbook-i18n-helpers.out                       4,095,192 x /nix/store/v06r1p7zpy9x6jlfrrssrf6nbqqm64pk-mdbook-i18n-helpers-0.2.4/bin/mdbook-gettext
mdbook-i18n-helpers.out                       3,807,080 x /nix/store/v06r1p7zpy9x6jlfrrssrf6nbqqm64pk-mdbook-i18n-helpers-0.2.4/bin/mdbook-i18n-normalize
mdbook-i18n-helpers.out                       4,140,880 x /nix/store/v06r1p7zpy9x6jlfrrssrf6nbqqm64pk-mdbook-i18n-helpers-0.2.4/bin/mdbook-xgettext
mdbook-kroki-preprocessor.out                 8,566,376 x /nix/store/f5gxdaa8fw43ar0wcrsz5bc52yzsjkr2-mdbook-kroki-preprocessor-0.2.0/bin/mdbook-kroki-preprocessor
mdbook-linkcheck.out                         13,164,440 x /nix/store/4kicypw0dc07i1syzv6abnyfzfns6n1m-mdbook-linkcheck-0.7.7/bin/mdbook-linkcheck
mdbook-man.out                                3,166,272 x /nix/store/s1rmgghpfxam6f0bf5kb9q8ag2305ld7-mdbook-man-unstable-2022-11-05/bin/mdbook-man
mdbook-mermaid.out                            5,643,400 x /nix/store/1z7b8af7j3plzkk6i5p4176i5w839b4j-mdbook-mermaid-0.12.6/bin/mdbook-mermaid
mdbook.out                                   16,064,552 x /nix/store/44ygcgxpw8q7d7gldihkpbjlxd0181gn-mdbook-0.4.34/bin/mdbook
mdbook-admonish.out                           5,479,400 x /nix/store/rnpfm3pk7rhj4f7l0lcy0d91630qwa45-mdbook-admonish-1.12.1/bin/mdbook-admonish
mdbook-d2.out                                 2,453,072 x /nix/store/jyziwz01h3ykich7wcanyw05rbjk8swc-mdbook-d2-unstable-2023-03-30/bin/mdbook-d2
mdbook-epub.out                               9,321,376 x /nix/store/v2q8j9gbbwis37x41w53r4p2a07m6pjz-mdbook-epub-unstable-2022-12-25/bin/mdbook-epub
mdbook-pdf.out                               10,520,968 x /nix/store/mr5hz1vbpx9jgc27f29890w4k25p3c7c-mdbook-pdf-0.1.7/bin/mdbook-pdf
mdbook-plantuml.out                           6,485,488 x /nix/store/09dvwh1ifpr32bj940xsg5p0w1zshvad-mdbook-plantuml-0.8.0/bin/mdbook-plantuml
mdbook-graphviz.out                           4,792,168 x /nix/store/lynyplv8i5yq9kigpl0ibslp6hq6l3sl-mdbook-graphviz-0.1.6/bin/mdbook-graphviz
mdbook-cmdrun.out                             3,254,448 x /nix/store/mk8d2xv5kpyznpd4yk8b73q6s0qak1cb-mdbook-cmdrun-unstable-2023-01-10/bin/mdbook-cmdrun
mdbook-pagetoc.out                            3,700,888 x /nix/store/c46jrg5bj2yg0cqixraxj0jwhjxipdqv-mdbook-pagetoc-0.1.7/bin/mdbook-pagetoc
mdbook-toc.out                                2,347,464 x /nix/store/1jzsc6yvhd0daqnwrk1k14y31xwl0lqm-mdbook-toc-0.14.1/bin/mdbook-toc
mdbook-katex.out                              2,630,344 x /nix/store/x961ynwvdnjlxmdvp0d5lzvj2bmqlf32-mdbook-katex-0.5.8/bin/mdbook-katex
mdbook-emojicodes.out                         3,890,048 x /nix/store/kic0qqckp1y8jda1g724nldfyn0xyrh8-mdbook-emojicodes-0.3.0/bin/mdbook-emojicodes

store permissions

As recommended by the NixOS Manual, and the Discourse, the permissions on the store should be

sudo chown -R 0:0 /nix
sudo chmod +t /nix

writing derivations

Ultimately almost everything in Nix is a derivation --- a specification for producing outputs given fixed inputs. Indeed, the entire NixOS system can be seen as a single large derivation with many inputs. Writing derivations is a skill developed through trial and error. Here I attempt to document implicit conventions, lessons, common gotcha's, and other tips and tricks for writing derivations.

seeing shared libraries

All provided by binutils

  • readelf -Ws,
  • objdump -TC,
  • nm -gD.

Show undefined symbols only:

  • nm -ugD.

hacking on nixpkgs

Build the NixOS configuration from a nixpkgs fork with

sudo nixos-rebuild test --override-input nixpkgs . --fast

pull requests

For reference, here is a list of pull requests I've submitted to nixpkgs, ordered by submission time.

NixOS

The canonical source of truth is the Nix code.

There is a search engine for options.

testing configuration

I couldn't get nix-instantiate --eval to play well with flakes, so I use the following setup.

Although I don't usually use a REPL for most languages (python, julia, e.g.), preferring file-based development, unfortunately I couldn't find a more ergonomic setup than using the REPL.

I wrap nix repl with a shell script like

#!/run/current-system/sw/bin/sh

nix repl \
  --file "$(dirname "$0")/nixos-repl.nix" \
  --argstr username "$(whoami)" \
  --argstr hostname "$(hostname)" \
  --argstr path "/keep$HOME/.config/home-manager"

that loads nixos-repl.nix placed in the same directory.

{ username, hostname, path }:

let
  self = builtins.getFlake path;
  nixos = self.nixosConfigurations.${hostname};
in
{
  inherit self;
  ${hostname} = nixos;
  ${username} = nixos.config.home-manager.users.${username};
  inherit (nixos) config pkgs options;
  inherit (nixos.pkgs) lib;
}

This can be then used like

nix-repl> <hostname>.config.

and press <tab> to see completions. :p can be used to force evaluation.

If I have something I want to inspect over and over again, I use a wrapper of nix eval

nix eval ".#nixosConfigurations.$(hostname)" --apply "$(hostname): $1"

like so.

nixos-eval "<hostname>.config.system.activationScripts.usrbinenv"

updating

Simple script to make sure /boot is mounted before updating.

#!/run/current-system/sw/bin/sh

if [ ! "$(findmnt /boot)" ]; then
    sudo mkdir --parents /boot
    sudo mount --onlyonce /dev/nvme0n1p1 /boot
fi
sudo nixos-rebuild switch

nixos-rebuild has a few possible commands.

  • switch: activate and make boot default
  • boot: make boot default but don't activate
  • test: activate but don't make boot default
  • build: neither activate nor make boot default
    • the result is a symlink placed in ./result

removing channels and flake registries

Channels and flake registries are unnecessary and a source of impurity as they are unpinned.

In NixOS the following configuration disables channels.

{
  nix.channel.enable = false;
}

In Home Manager the following configuration disables flake registries.

{
  nix.settings = {
    experimental-features = [ "nix-command" "flakes" "repl-flake" ];
    flake-registry = "";
    use-registries = false;
  };
}

Note flake-registry controls the global registry while use-registries controls user registries.

It is convenient to replace the nixpkgs reference with the shell script nixpkgs.

#!/run/current-system/sw/bin/sh

flake="/keep$HOME/.config/home-manager"
# `--impure` as the flake may be dirty and considered unlocked
# error: cannot call 'getFlake' on unlocked flake reference
nix eval --impure --raw \
  --expr "(builtins.getFlake \"$flake\").inputs.nixpkgs.outPath"

Then commands like

nix-shell -I nixpkgs=flake:nixpkgs -p python3
nix shell nixpkgs#python3

can be replaced by

nix-shell -I nixpkgs=$(nixpkgs) -p python3
nix shell $(nixpkgs)#python3

which has the advantage of not requiring internet as it uses the NixOS configuration's nixpkgs.

In addition, a dependency lookup like

nix why-depends /nix/var/nix/profiles/system $(nixpkgs)#nss

is more accurate as it uses the same version of the package as the system.

miscellaneous

installing from arch

One can switch from Arch Linux completely "in-place", i.e. without re-partitioning any drives. This can be done by prototyping with kexec to get a working configuration and then using NIXOS_LUSTRATE through the lustrate mechanism (which will move the old root partition to /old-root).

After this, it's still possible to get into arch with chroot, e.g.

sudo chroot /old-root /bin/bash
/bin/pacman -Q

Note that commands need to be fully qualified as $PATH is still from NixOS.

live boot

Live boot can be done from an Arch live boot (see NixOS Wiki - Change root).

cryptsetup open /dev/nvme0n1p3 cryptlvm
mount /dev/VolumeGroup/root /mnt
mount -o bind /dev /mnt/dev
mount -o bind /proc /mnt/proc
mount -o bind /sys /mnt/sys
chroot /mnt /nix/var/nix/profiles/system/activate
chroot /mnt /run/current-system/sw/bin/bash

/bin/sh

Having /bin/sh is technically an impurity as applications can reference it without knowing the exact version. However, it's required to do system() calls in libc, so it can't be easily disabled entirely. This can cause issues with reproducibility but progress in fixing this seems to have stalled.

(see also: NixOS Discourse - Add /bin/bash to avoid unnecessary pain, NixOS Wiki - Command Shell)

I think the cleanest thing to do is to use the default sandbox shell provided in the default stdenv, currently a statically linked ash shell from busybox. The reasoning being that if /bin/sh matches the one used at build-time, there's less chance of a runtime error due to possible incompatibility.

environment.binsh = "${pkgs.busybox-sandbox-shell}/bin/busybox";

It does warn about changing from bash, but considering it's over 10 years old, it's probably fine now.

/usr/bin/env

Like /bin/sh, /usr/bin/env is an impurity that does not exist at build time.

Unlike sh, it can be disabled relatively easily.

environment.usrbinenv = null;

This can cause issues for unpatched software that rely on env, e.g. prettier installed with npm.

There are (currently) also a few minor spurious errors that should be fixed, see this issue.

system.activationScripts.usrbinenv =
  lib.mkIf (config.environment.usrbinenv == null) (
    lib.mkForce ''
      rm -f /usr/bin/env
      mkdir -p /usr/bin
      rmdir --ignore-fail-on-non-empty /usr/bin /usr
    ''
  );
systemd.services.systemd-update-done.serviceConfig.ExecStart = [
  "" # clear
  (
    pkgs.writeShellScript "systemd-update-done-wrapper" ''
      mkdir -p /usr
      ${pkgs.systemd}/lib/systemd/systemd-update-done
      rmdir --ignore-fail-on-non-empty /usr
    ''
  )
];

vulnix

vulnix scans the dependencies of the entire system for CVEs.

vulnix --system

Home manager

Home Manager brings NixOS-like modules to per-user configuration (files in ~).

If the NixOS module is used, then the home-manager configuration is built along with NixOS.

There is also a search engine for options, not to be confused with NixOS modules/options.

not updating

If one deletes a folder managed by Home Manager, ~/.config/nix/nix.conf, say, it won't necessarily be regenerated by sudo nixos-rebuild switch if the configuration hasn't changed.

A rebuild can be forced with

sudo systemctl restart home-manager-<user>.service

impermanence

impermanence registers persistent storage for when root gets wiped on reboot (e.g. tmpfs on /).

See

configuration

Example configurations

Configuration is relatively simple, change something like

  fileSystems."/" = {
    device = "/dev/VolumeGroup/root";
    fsType = "ext4";
  };

to

  fileSystems."/" = {
    fsType = "tmpfs";
    options = [ "defaults" "size=2G" "mode=755" ];
  };
  fileSystems."/keep" = {
    device = "/dev/VolumeGroup/root";
    fsType = "ext4";
    neededForBoot = true;
  };
  # https://nixos.wiki/wiki/Filesystems
  fileSystems."/nix" = {
    device = "/keep/nix";
    options = [ "bind" ];
  };

Here the name /keep is arbitrary.

important state

  • /etc/machine-id: if not stored, new id (re-)generated on every boot
    • used by systemd/journalctl in/var/log/journal/<machine-id>

persisting passwords

Can use

  • users.users.<name>.password: (plaintext) password
  • users.users.<name>.hashedPassword: hashed password from mkpasswd
  • users.users.<name>.hashedPasswordFile: path to hashed password

hashedPasswordFile is a file whose only line is a hashed password as generated by mkpasswd. Unfortunately hashedPassword and password overwrite hashedPasswordFile, so if the file is deleted, one can get locked out of their account. The configuration will warn on rebuild, however.

warning: password file ‘’ does not exist

Generate password with yescrypt hash function, now default on archlinux (and for mkpasswd).

mkpasswd --method=yescrypt "$(pass encryption/tuxedo/password)" > root.yescrypt

See also reddit, impermanence issue #120.

memory used

Can use df to measure tmpfs memory usage.

df -h
Filesystem             Size  Used Avail Use% Mounted on
devtmpfs               1.6G     0  1.6G   0% /dev
tmpfs                   16G  8.0K   16G   1% /dev/shm
tmpfs                  7.7G  6.3M  7.7G   1% /run
tmpfs                   16G  1.2M   16G   1% /run/wrappers
tmpfs                  2.0G  1.6M  2.0G   1% /
/dev/VolumeGroup/root  883G  447G  392G  54% /keep
tmpfs                  3.1G   32K  3.1G   1% /run/user/1000

The relevant line is

tmpfs                  2.0G  1.6M  2.0G   1% /

Can check what's about to be cleared with

ncdu -x /

(-x means to not cross filesystem boundaries)

sudo ncdu -x /

will show the contents of /root, which may not be accessible normally.

running out of memory

Nix builds works in /tmp (see boot.tmp.useTmpfs) which can cause memory issues for large builds.

One can make a shell script called mktmp

#!/run/current-system/sw/bin/sh
# make a persistent /tmp

sudo mkdir -p /keep/tmp
sudo chmod 1777 /keep/tmp
sudo mount --onlyonce --bind /keep/tmp/ /tmp || true

and rmtmp

#!/run/current-system/sw/bin/sh
# remove a persistent /tmp

sudo umount /tmp
sudo chmod --silent -t /keep/tmp
sudo rm -rf /keep/tmp

to make and remove a temporary persisted /tmp, respectively.

These shell scripts are designed to be idempotent and inverses of each other.

modules

The list of arguments to a module are

  • config: option definitions (setting options)
  • lib: Nixpkg's library functions
  • pkgs: reference to Nixpkgs
  • options: option declarations (defining options)
  • modulesPath: path to Nixpkg's NixOS modules

These are documented in NixOS Manual - Writing NixOS Modules.

Using lib instead of pkgs.lib can sometime prevent infinite recursion errors.

Flakes

Flakes are a (currently) experimental feature replacing the channel mechanism along with a few other Nix interfaces (default.nix, shell.nix). Reproducibility is achieved by declaring inputs in flake.nix whose resolved versions are pinned in flake.lock, like the package managers for many programming languages, enabling easy updates of dependencies. In addition, the project's interface is also declared in flake.nix, creating a unified experience for interacting with any Flake project.

packages

Next is advice for specific packages (following the structure of nixpkgs).

cmus

cmus is a terminal based music player. It's lightweight and fast; what more could you ask for? My only complaint is that if you close the window, the music stops playing. One solution is to put it in a tmux server (this has the added benefit of being able to open multiple cmus windows at the same time, because each is a view into the same running process). These days I'm too lazy to start tmux, so I just put a cmus window in a high-number workspace and switch to that workspace on demand.

My friend swears by mpd because of its client-server architecture: that gives the ability to use any client, from curses-based terminal interfaces to integration in emacs. It also makes it easy to do cool things like fancy frequency visualizations and sharing what music you're listening to on a webserver. I don't have these use cases, so I stick to cmus. And when I tried to setup mpd a long time ago, I found it hard to do and never got it working.

The highest praise I can give is that I have not touched the configuration at all, but I use cmus daily.

pipewire

Make sure the pulseaudio backend is used with :set output_plugin=pulse.

plugins

The default build supports many different audio formats (cmus --plugins).

Input Plugins: /nix/store/ndlabjql98bw7yzrdm8cg0yncp0qfg12-cmus-2.10.0/lib/cmus/ip
  mad:
    Priority: 55
    File Types: mp3 mp2
    MIME Types: audio/mpeg audio/x-mp3 audio/x-mpeg
  cue:
    Priority: 50
    File Types:
    MIME Types: application/x-cue
  cdio:
    Priority: 50
    File Types:
    MIME Types: x-content/audio-cdda
  wav:
    Priority: 50
    File Types: wav
    MIME Types:
  opus:
    Priority: 50
    File Types: opus
    MIME Types:
  flac:
    Priority: 50
    File Types: flac fla
    MIME Types:
  vorbis:
    Priority: 50
    File Types: ogg oga ogx
    MIME Types: application/ogg audio/x-ogg
  mpc:
    Priority: 50
    File Types: mpc mpp mp+
    MIME Types: audio/x-musepack
  wavpack:
    Priority: 50
    File Types: wv
    MIME Types: audio/x-wavpack
  modplug:
    Priority: 50
    File Types: mod s3m xm it 669 amf ams dbm dmf dsm far mdl med mtm okt ptm stm ult umx mt2 psm
    MIME Types:
  mikmod:
    Priority: 40
    File Types: mod s3m xm it 669 amf dsm far med mtm stm ult
    MIME Types:
  ffmpeg:
    Priority: 30
    File Types: aa aac ac3 aif aifc aiff ape au fla flac m4a m4b mka mkv mp+ mp2 mp3 mp4 mpc mpp ogg opus shn tak tta wav webm wma wv
    MIME Types:

Output Plugins: /nix/store/ndlabjql98bw7yzrdm8cg0yncp0qfg12-cmus-2.10.0/lib/cmus/op
  pulse
  alsa

hangs after close

If playing music is paused and then cmus is closed, cmus will hang. Quickly attempting to open another cmus window results in the error message

cmus: Error: an error occured while initializing MPRIS: File exists. MPRIS will be disabled.
cmus: Press <enter> to continue.

MPRIS is the freedesktop specification for music player control, see MPRIS. It's probably a pipewire issue, see Delayed exit from paused music in cmus as well as Quitting while playback is paused takes several seconds which has made a recent recurrence (Cmus hangs when paused for a long time).

usage notes

Playlists

The man pages cmus and cmus-tutorial are pretty good, the only complaint I have is the playlist creation is not well-explained. Press 3 to go to playlist view, by default this will have only a single playlist called "default". Create a new playlist with :pl-create playlist-name (by default this is not bound to any key so you have to use the command) and delete a playlist with D. Playlists are sorted alphabetically but this can probably be configured somewhere. The asterisk "*" indicates which playlist new songs will be added to. To change what the active playlist is, press <space> (this was the confusing part which I could not find documentation on). Finally, to add songs press y on a song (you can also press y on albums and artists, which is very useful). To remove the song from the playlist, go to the playlist view and press D on the song. To change the order, press p to move a song down and P to move a song up. Finally, to play the playlist press <enter> and the same controls as usual can be used (x to play, v to stop, c for play/pause toggle, z previous song and b next song).

Note that playing from a playlist switches the mode to "playlist mode" (this means that there can be two highlights at the same time, one in playlist view and the other in library view). One can press shift-M to switch between modes without interrupting the current song.

Queue

The queue is well-explained, I just never knew what it was for. My use case for the queue is if I'm listening to something but I want to hear a particular song, so I can tell cmus to play that song without interrupting the flow of my current listening session. The best way to think of the queue is terms of two concepts: playlists and the implicit queue inherent to cmus.

The "implicit queue" is how I like to think of the "all from library", "artist from library", and "album from library" modes (toggleable with m). If you select "artist from library" and you play an artist, cmus essentially adds all of that artist's songs to the queue (the "real" queue which is accessible through pressing 4 is empty, hence the name implicit queue). Thus, the modes control which song play next, which is what the queue does. The toggles C, r, and s also control the implicit queue --- C toggles "continue", which determines whether the next song is automatically played or not (if not, then you will have to start the next song manually). r toggles "repeat", which determines whether the queue infinitely cycles, and s toggles "shuffle", which determines whether the queue is permuted. Thus, it is helpful to think of the modes and toggles and manipulating an implicit queue (the modes determine which songs go into the queue and the toggles determine how that queue behaves).

To go back to the "real" queue, it behaves similarity to a playlist. One adds songs with e instead of y, and the same playlist manipulation can be done (D to remove, p/P to move around). When cmus determines which song to play after the current song finishes, it first tries to dequeue a song from the "real" queue. If the real queue is empty, it dequeues a song from the implicit queue described above. Thus, if you're listening to an artist but want to play another song one-off, then you can add that song to the queue with e. When the song ends, you'll be back to listening to that artist in the same position in the implicit queue you were in.

cmus-remote

cmus-remote can be used to control cmus from the command-line, e.g. to generate a status text based on the current song (which I do for i3status) or to integrate into ranger, e.g. when selecting an audio file, add the file to the queue and play it. See the man page cmus-remote for more details.

sddm

sudo pacman -S sddm
  • Enable sddm
sudo systemctl enable sddm.service
  • Default configuration: /usr/lib/sddm/sddm.conf.d/default.conf or
sddm --example-config
  • Configuration directory: /etc/sddm.conf.d/, can place any files in the directory e.g. sddm.conf, name/extension doesn't matter
  • Testing
sddm-greeter --test-mode --theme /usr/share/sddm/themes/simplicity

Theming

  • Install theme
paru -S simplicity-sddm-theme-git
  • or my patch that fixes an issue where username was empty if real name was not set
    • currently merged
  • Themes go in /usr/share/sddm/themes/ by default
  • Can edit theme settings by copying default theme file /usr/share/sddm/themes/simplicity/theme.conf to /usr/share/sddm/themes/simplicity/theme.conf.user and making custom changes

Profile Icon

  • Add a PNG file username.face.icon to /usr/share/sddm/faces/
  • Or create ~/.face.icon and let SDDM find it:
setfacl -m u:sddm:x ~/
setfacl -m u:sddm:r ~/.face.icon

neovim

neovim is a fork of vim focused on extensibility and usability. It's an almost drop-in replacement for vim, although there are a few minor differences. Neovim has a few major advantages over vim. In terms of performance, neovim is apparently truly asynchronous, uses the scripting language Lua instead of vimscript with LuaJIT for extra speed, and has a faster startup time. Neovim also has support for syntax-aware syntax highlighting and text formatting (through tree-sitter) as well as built-in support for LSP. In general, neovim is a more modern and extensible replacement for vim and I've found it basically acts as a drop-in replacement without much configuration hassle.

See also Daniel's config for inspiration.

Quickstart

There's at least three ways to configure neovim through Nix.

  • NixOS options under programs.neovim.*,
  • Home Manager options also under programs.neovim.*,
  • and NixVim.

The NixOS module only provides basic support for a configuration file and not much else. Note that the executable nvim is a wrapped shell script which can be viewed with nvim $(which nvim). It's therefore possible to have both programs.enable.neovim = true for both NixOS and Home Manager as they live in different places, namely,

  • /run/current-system/sw/bin/nvim and
  • ~/.local/state/nix/profile/bin/nvim

respectively. This means my (hardened) neovim configuration as root (which uses the NixOS system configuration) is different from my personal neovim configuration (which uses the Home Manager user configuration). One advantage is that plugins aren't loaded when running neovim as root.

The Home Manger module provides a few extra conveniences, most notably, plugin support (programs.neovim.plugins) which uses neovim's built-in plugin loading mechanism. This can be slower than modern aggressively optimized plugin managers which compile configuration and recommend manually managing lazy loading to delay loading plugins until they're truly necessary. My incredibly bloated configuration with 34 plugins starts in about ~250ms (at the time of writing), which is perfectly fine. I'd like to get to 100-150ms which feels "snappier" to me but objectively speaking, it makes no practical difference.

Finally, NixVim is a module system for configuring neovim. I haven't used it personally since I'm happy with Home Manager and writing Lua, but it's the most "Nix-like" system (clean modules where someone else does the heavy lifting of actually translating Nix declarations into final configuration).

(it was only recently that Nix overtook Lua by lines of code in my configuration!)

Miscellaneous tips

No additional plugin/package managers

Since plugins are automatically installed with neovim, the configuration is more portable. In addition, packages which should installed with neovim (language server protocol implementations, formatters, linters, etc.) can be installed with neovim through programs.neovim.extraPackages.

Neovim as a man pager

Neovim can be used to read man pages more easily.

export MANPAGER="nvim +Man!"
export MANWIDTH=80

Security-conscious editing

While editing passwords, important emails, or other sensitive information, it's best to have a different configuration so that vim is sandboxed. By default, vim generates swap files, backup files, etc. and will load modelines which have had and continue to have security vulnerabilities.

See my hardened init.lua

-- pass will automatically do some of this, even with no configuration
-- https://git.zx2c4.com/password-store/tree/contrib/vim/redact_pass.vim

vim.opt.shada = ""
vim.opt.history = 0
vim.opt.swapfile = false
vim.opt.backup = false
vim.opt.writebackup = false
vim.opt.undofile = false
vim.opt.secure = true
vim.opt.modeline = false
vim.opt.shelltemp = false

-- save file for all modes
vim.keymap.set({ "", "!" }, "<c-s>", "<cmd>w<cr>")
-- exit file for all modes
vim.keymap.set({ "", "!" }, "<c-q>", "<cmd>q!<cr>")

which can be used with

/run/current-system/sw/bin/nvim --clean --noplugin -n -u init.lua "$@"

Alias this to nvim-private, which can then be used as an value for EDITOR. The commands in init.lua were based on this Stack Exchange. For posterity, here is a vim-compatible version.

" pass will automatically do some of this, even with no configuration
" https://git.zx2c4.com/password-store/tree/contrib/vim/redact_pass.vim

set viminfo=
set history=0
set noswapfile
set nobackup
set nowritebackup
set noundofile
set secure
set nomodeline
set noshelltemp

set nocompatible               " turn off vi compatibility mode
set backspace=indent,eol,start " make backspace always work
set showcmd                    " show an incomplete command
set showmode                   " show mode

" save file for all modes
noremap  <c-s> <cmd>w<cr>
noremap! <c-s> <cmd>w<cr>
" exit file for all modes
noremap  <c-q> <cmd>q!<cr>
noremap! <c-q> <cmd>q!<cr>

Reflowing text paragraphs

When editing text in the terminal, it can be helpful for readability to wrap text to a certain line width (traditionally, less than 80 characters long). However, greedily wrapping (neovim's default behavior) can make the edges of paragraphs jagged, which look worse than more rectangular paragraphs.

See my blog post on a simple dynamic programming algorithm that reflows paragraphs "optimally".

In neovim, gq is the operator and the option 'formatprg' sets the program invoked on gq. I frequently run gqip (mnemonic: gq in a paragraph) while writing. I recommend using my program far or the original program it was based on, par.

wine

locale-gen
[multilib]
Include = /etc/pacman.d/mirrorlist
  • install wine
pacman -S wine
  • install optional dependencies
  • install winetricks (basically a package manger inside wine)
pacman -S winetricks
  • install zenity (GUI for winetricks, you could just use the CLI)
pacman -S zenity
  • wine-mono for .NET
pacamn -S wine-mono
  • if getting an error about ntlm_auth when installing things with winetricks, install samba
pacman -S samba
  • most visual novels are 32-bit, so set WINEARCH to win32
set -gx WINEARCH win32
set -gx DOTNET_CLI_TELEMETRY_OPTOUT 1
  • set windows version to windows XP, sometimes helpful
winecfg
  • "Applications" -> "Windows Version:" change to "Windows XP"

extra packages and fonts

  • if game doesn't work, try installing more libraries
pacman -S giflib lib32-giflib libpng lib32-libpng libldap lib32-libldap gnutls lib32-gnutls mpg123 lib32-mpg123 openal lib32-openal v4l-utils lib32-v4l-utils libpulse lib32-libpulse libgpg-error lib32-libgpg-error alsa-plugins lib32-alsa-plugins alsa-lib lib32-alsa-lib libjpeg-turbo lib32-libjpeg-turbo sqlite lib32-sqlite libxcomposite lib32-libxcomposite libxinerama lib32-libgcrypt libgcrypt lib32-libxinerama ncurses lib32-ncurses libxslt lib32-libxslt libva lib32-libva gtk3 lib32-gtk3 gst-plugins-base-libs lib32-gst-plugins-base-libs lib32-gst-plugins-good vulkan-icd-loader lib32-vulkan-icd-loader
  • if game doesn't work, try installing more libraries with winetricks
winetricks d3dx9 dirac dotnet35 dotnet40 dxvk lavfilters vcrun2003 vcrun2005 vcrun2008
  • disable wine DLL overrides
winetricks alldlls=default
  • start winetricks
winetricks
  • "Select the default wineprefix" -> "Install a font" -> check cjkfonts and corefonts

running a game

  • mount .iso as usual, unpack
sudo mount game.iso /mnt
sudo cp -r /mnt/* game/
  • run game
LC_ALL="ja_JP.UTF-8" TZ="Asia/Tokyo" wine game.exe
  • kill game
wineserver --kill

CDemu

  • if error along the lines of "disk not plugged in"
  • try restarting
  • if that doesn't work, use cdemu
sudo pacman -S cdemu-client cdemu-daemon
  • start daemon
cdemu-daemon
  • mount iso
cdemu load 0 game.iso
  • check mount point
lsblk
  • for me, either /dev/sr0 or /dev/sr1, check by file size / run lsblk before loading and after
  • mount
mount /dev/sr0 /mnt
  • add mount point to wine
winecfg
  • "Drives" -> "Add...", pick arbitrary letter ("E:" is typical), change "Path:" or click on "Browse..." to /mnt
  • run game as usual, hopefully just works

ranger

ranger is a terminal file manager. Moving around and manipulating files with ranger tends to be faster than the equivalent cd/ls/mv/cp/rm/etc. shell commands. For exploring a file system, ranger is much faster than cd and ls since one just presses h and l to go up and down the file hierarchy and always can see the current files. The operations on files mv/cp/rm/etc. require typing the paths of both the source and destination, while in ranger one simply acts on the file currently under the cursor. In general, operations in ranger are faster since the path does not need to be explicitly specified unlike shell commands in the terminal.

Previews

  • Image previews

Edit rc.conf:

set preview_images true
set preview_images_method ueberzug

preview_images_method can also be set to w3m for general terminals.

For the rest of the previews, edit scope.sh:

  • Videos
pacman -S ffmpegthumbnailer
  • PDF
pacman -S pdftoppm
  • Syntax highlighting (without bat)

Use the package highlight. To pick a theme, copy the scope via ranger --copy-config=scope and edit the variable HIGHLIGHT_STYLE near the top. To use a base16 theme, replace the highlight command near the bottom.

  • Syntax highlighting (with bat)
pacman -S bat

mupdf

  • has a pdf viewing tool

simulating greyscale/colorblindness

  • siam prints in black/white, preview how the document will look
  • greyscale: use mupdf, package mupdf-tools
mutool draw -c gray -o "output%d.png" input.pdf
  • colorblind: use gimp, package gimp

redshift

pacman -S redshift
  • add location to configuration file ~/.config/redshift/redshift.conf
[redshift]
location-provider=manual

[manual]
lat=33.78
lon=-84.39
  • can get coordinates with geonames.org
  • note: to convert xxo yy' zz" to decimal find xx + yy/60 + zz/3600 (hours minutes seconds)
  • note: north and east are positive, south and west are negative
  • can also use google maps, right-click location
  • start system service
systemctl --user status redshift.service

sioyek

  • sioyek
  • relatively new viewer explicitly designed for scientific use (research papers, technical books)
paru -S sioyek
  • pretty good, out-of-the-box integration with vimtex
let g:vimtex_view_method = 'sioyek'

zathura

pacman -S zathura
  • install specific formats (muPDF backend for PDF), comic book (.cbz), etc.
pacman -S zathura-pdf-mupdf
pacman -S zathura-cb

neomutt

neomutt is a mail user agent (MUA). More specifically, it lets you read email from the terminal. It is possible to download and send mail from mutt natively, but I prefer external programs for those functions.

A quick overview:

  • downloading mail: offlineimap
  • sending mail: msmtp
  • reading mail: neomutt + neovim (occasional full-screen reading) + w3m (for html emails)
  • editing mail: neovim
  • indexing mail: notmuch
  • encrypting mail: gpg
  • adding attachments: ranger
    • determining what program to use to open attachments: rifle
  • general selector: fzf

neomutt

neomutt is essentially a superset of regular mutt aiming to fix bugs, collect patches, and in general incite development of mutt. It therefore makes sense to use neomutt rather than mutt.

General notes

My primary email is gmail, which has its quirks. In particular, all emails go into "all mail" (including emails sent by oneself!) and the different "folders" are more like tags --- attributes of the emails in all mail. This mirrors notmuch nicely but IMAP not so much so there will be a few oddities.

I use PGP to sign and encrypt email.

Lastly, I use Google's Advanced Protection program. Surprisingly enough, one can use command-line tools to access mail even with advanced protection on (if you authenticate with OAuth2).

With that in mind, the first step is to get mail.

offlineimap

offlineimap is used to download (and sync!) mail. Note that this is a two-way operation: it will update the local repository of mail if there are changes in the remote and it will also update the remote if there are local changes! There is a risk of deleting email permanently if you delete locally and have offlineimap sync. Run with the --dry-run option to see what offlineimap will do while testing.

offlineimap is configured with Python, unfortunately Python 2. There's a Python 3 fork of offlineimap, and the same author also wrote imapfw, a Python 3 replacement, but the project appears to be dead.

In order to authenticate, we must use OAuth2 as mentioned before. The specific steps depends on the email provider, but in general we need a client id, secret, and refresh token. We can redeem the refresh token for an access token, which is what we actually use to authenticate. I store these credentials in the lightweight PGP-based password manager pass. To generate an access token, we could use offlineimap's built-in oauth2_refresh_token_eval option but for integration with msmtp and caching we might as well use our own program: offlineimap.py.

Google and Microsoft cover all my email accounts, including those which that are not necessarily @gmail.com or @hotmail.com. For example, my Georgia Tech email ending in @gatech.edu is actually provided by Outlook, so I can use Microsoft OAuth to authenticate, without needing to go through Georgia Tech's single sign-on authentication portal.

Google OAuth

We'll be using the gmail-oauth2-tools repository as the client library.

Follow the instructions here. The Google Cloud Console is pretty poorly designed, so it may take some effort to figure out how to create a new project.

If it initially works but after a week there's the error KeyError: 'access_token' it might be that the refresh token is invalid. This is because Google's OAuth policy restricts the lifespan of a refresh token to 7 days if the app is configured for external users and the publishing setting is "Testing", a common situation one would be in for personal use. The solution is to press the "PUBLISH APP" button on the OAuth consent screen. Although it will warn you that "Because you're using one or more sensitive scopes, your app registration requires verification by Google. Please prepare your app to submit for verification", you don't actually need to verify the app, that just removes the warning screen asking the user whether they trust the developer while getting a refresh token.

Microsoft OAuth

We'll be using the msal client library.

Follow the instructions to create a new application and add IMAP and SMTP permissions. These instructions are a bit verbose, so I'll condense them here:

  1. Navigate to the Azure portal
  2. Go to "Azure Active Directory", either by searching or clicking on the icon
  3. Find "App registrations" in the side bar under "Manage" and press "New registration"
  4. Under "Manage", select "Authentication". Use the "Web" platform with a redirect URI of http://localhost.
  5. Select "Certificates & secrets" and press "New client secret". Record the client id and secret.
  6. Select "API Permissions". Press "Add a permission" and use "Microsoft Graph" with "Delegated permissions". The permissions we need are offline_access (under OpenId permissions), User.Read (under User), IMAP.AccessAsUser.all (under IMAP) and SMTP.Send (under SMTP).
  7. Depending on the situation, we might need a tenant. For Georgia Tech, this is gtvault.onmicrosoft.com. This value can be found by going to the "Azure Active Directory" page and looking at the value of "Primary domain". Otherwise, this can be set to common.

Something strange Microsoft does is their refresh tokens: they give a new refresh token back after every access token request, and refresh tokens expire after 90 days. If you were authenticating through offlineimap, you might be passing oauth2_refresh_token so offlineimap can automatically request access tokens. So if you suddenly become unable to request access tokens, it might be because of the refresh token expiration. offlineimap.py will automatically save the refreshed refresh token, but you still need to update the client secrets at least every two years (since 24 months is the longest possible expiration date for client secrets).

msmtp

With offlineimap configured, msmtp works similarly. Set the authentication protocol to oauthbearer and passwordeval to run the above offlineimap.py script, passing in the email. That way both offlineimap and msmtp use the same cached access token.

notmuch

notmuch is an email indexer, tagger, and searcher. Add a postsync hook to offlineimap so tagging happens on new mail. We can also use notmuch as an address book by searching the addresses of previously received emails.

alacritty

I used to use kitty and there really wasn't anything wrong with it. The reason I switched was mainly philosophical, kitty just did too much like the whole integrated tab / window splitting and so on. Terminal emulators should be relatively simple and leave the complexity to other programs (your window manager, tmux, etc.). Alacritty doesn't too much, but has all the features I've come to expect from a terminal emulator. Also, the vim mode is sometimes useful if you really don't want to use the mouse.

See features.md for an overview of supported features.

Bugs

Keybindings Like "$" in Vim Mode Don't Work

The default keybindings that involve shift and non-letter keys don't work. This is an X-specific issue caused by a bug in the upstream library winit's handling of virtual keycodes. See

To fix, use the scancodes instead of the key names. Edit ~/.config/alacritty/alacritty.yml:

key_bindings:
  # specify scancode to get around invalid virtual keycode provided by winit
  - { key: 5, mods: Shift, mode: Vi|~Search, action: Last }
  - { key: 7, mods: Shift, mode: Vi|~Search, action: FirstOccupied }
  - { key: 6, mods: Shift, mode: Vi|~Search, action: Bracket }
  - { key: 53, mods: Shift, mode: Vi|~Search, action: SearchBackward }

The scancodes can be found with

sudo showkey --keycodes

There is theoretically a difference between interpreted keycodes and raw scancodes. See arch wiki - keyboard input for the details. However, the keycodes shown by showkey --keycodes seem to be the same as the ones shown by showkey --scancodes, both of which are different than the keycodes or keysyms shown by xev. For example, if I press the letter "a" on my keyboard:

  • showkey --scancodes: 0x1e (30)
  • showkey --scancodes: 0x9e (158) is also shown
  • showkey --keycodes: 30
  • xev keycode field: 38
  • xev keysym field: 0x61 (97), ASCII value for "a"

For Alacritty, you should be using the (decimal) keycode in common between showkey --scancodes and showkey --keycodes. It's possible to get these from showkey --scancodes but you have to convert the hex to decimal, and when I press a key it seems to alternate between two different values. showkey --keycodes is easier to use and works.

Cursor Spins on Empty Background

This is especially applicable to i3wm users. The problem is that if the mouse cursor is on the desktop background (not hovering over an active window), then it's stuck in the "spinning" or "waiting" state. For why this happens, see:

The summary is that the freedesktop startup-notification-spec provides a mechanism by which applications upon launching can signal through X that they have began started up, and finished starting up. This allows your cursor to appear "busy" when the application is starting up, and turn back to normal once the application opens. This also allows i3 to guarantee that the application's window is put where it was originally launched from. However, if i3 launches an application with exec and expects startup notifications when the application does not send them, then i3 assumes the application is taking a long time to startup, timing out after 60 seconds, causing the cursor to appear busy for 60 seconds. This can be fixed by starting the offending application with exec --no-startup-id.

Alacritty does not support startup notification events, causing the busy cursor. The default i3 configuration launches a terminal with the following line:

bindsym Mod1+Return exec i3-sensible-terminal

i3-sensible-terminal, as the name implies, looks for a sensible terminal in the user's path and since the application is launched with exec and not exec --no-startup-id, the cursor will be busy for 60 seconds after launching an alacritty window. See:

The first issue is a nearly 5 year old issue, and the second issue was re-filed by me because new evidence came out that it was in fact Alacritty's noncompliance to startup-notification-spec that causes the issue. Alacritty maintainers refuse to fix this since the issue is (mostly) cosmetic.

Note that the cursor can be fixed immediately by restarting i3.

i3 restart

Improper Spacing on Certain Characters

The characters "★" (Unicode codepoint 0x2605) and "☆" (0x2606) are displayed improperly for "most" monospace fonts. This is because the Unicode specification considers them single-width characters but they but are rendered as double-width, causing them to clip into their neighbors. See the issue I filed, alacritty/alacritty/#6144. I've tested:

  • Noto Sans Mono (noto-fonts)
  • IPAGothic (otf-ipafont)
  • Source Code Pro (adobe-source-code-pro-fonts)

The fonts which I've found to work has been:

  • DejaVu Sans Mono (ttf-dejavu)

It's a bit misleading to call this a "bug" since it's pretty much impossible to determine the display width of Unicode characters (it's font specific). That being said, "GUI" programs like Firefox, Signal, and Emacs (but not gvim) seem to have figured it out, so there's no real reason a terminal emulator couldn't.

git

I use git for all of my projects that need version control.

Git credential caching

By default, git will prompt you on every operation that needs authentication to provide a username/password. It is possible to use the built-in storage methods to either cache credentials in memory for 15 minutes, or to store credentials permanently on disk as plaintext. If one wants permanent encrypted credential storage, it requires some additional setup.

See

The simplest thing to do is to install the git-credential-manager which is developed by GitHub.

paru -S git-credential-manager-core

It's possible to configure git-credential-manger to use the GPG-based password manger pass.

git-credential-manager configure
git config --global credential.credentialStore gpg

or just edit ~/.config/git/config directly

[credential]
	helper = /usr/bin/git-credential-manager
	credentialStore = gpg

Next time a credential is requested, a pop-up appears and one can authenticate in a variety of ways (through browser login, through a personal access token, etc.). It then stores the data at

~/.password-store/git/https/github.com/stephen-huan.gpg

for GitHub, for example.

Another program that uses pass to perform git credential caching is pass-git-helper, but it seems a bit more complicated. One advantage of git-credential-manager is that it is able to use different methods to store its secrets.

git-credential-manager

paru -S git-credential-manager-core
  • run configuration and use gpg/pass files
git-credential-manager-core configure
git config --global credential.credentialStore gpg
  • or edit ~/.config/git/config directly
[credential]
	helper = /usr/share/git-credential-manager-core/git-credential-manager-core
	credentialStore = gpg
  • running a git-credential-manager-core command seems to break arrow keys?
  • next time credential is requested, pop-up appears, can authenticate in a variety of ways (browser, token, etc.)
  • stores at ~/.password-store/git/https/github.com/stephen-huan.gpg, e.g.

i3

  • use xev to get keysym names (i.e. [ is bracketleft)

cursor spins on empty background

  • see arch wiki - i3
  • see i3wm user guide
  • see freedesktop.org - startup-notification-spec
  • run offending command in i3 config with exec --no-startup-id
  • i3 has startup notifications, i.e. when an application finishes starting, it will signal to i3 that it's finished, i3 will change the cursor from waiting to normal.
  • but if the application doesn't support startup notifications, it'll take 1 minute for the cursor to reset to normal

issue in alacritty

i3 restart

archive

picom

pacman -S picom
  • edit config at ~/.config/picom/picom.conf
  • use opengl
# use OpenGL as the rendering backend
backend = "glx";
  • screen tears without fading, default fading animation is too slow
# without fading, some screen tears
fading = true;
# speed up default fade speed
fade-delta = 3;
  • transparency for aesthetic
# make inactive windows slightly transparent
inactive-opacity = 0.9;
  • exclude i3lock from transparency to prevent desktop leaking and exclude floating windows
opacity-rule = [
    # exclude screensaver (i3lock) window
    "100:class_g = 'i3lock'",
    # exclude floating windows
    "100:I3_FLOATING_WINDOW@:c",
];

ipafont

pacman -S otf-ipafont

fontconfig

fc-list
  • query settings
fc-match --verbose sans
  • edit ~/.config/fontconfig/fonts.conf
<?xml version="1.0" ?>
<!DOCTYPE fontconfig SYSTEM "urn:fontconfig:fonts.dtd">
<fontconfig>
  <alias>
    <family>serif</family>
    <prefer>
      <family>Noto Serif</family>
      <family>IPAMincho</family>
    </prefer>
  </alias>

  <alias>
    <family>sans-serif</family>
    <prefer>
      <family>Noto Sans</family>
      <family>IPAGothic</family>
    </prefer>
  </alias>

  <alias>
    <family>monospace</family>
    <prefer>
      <family>Noto Sans Mono</family>
      <family>IPAGothic</family>
    </prefer>
  </alias>
</fontconfig>

japanese

firefox japanese font is wrong

  • differences in kanji for chinese and japanese, e.g. "語" in 日本語, "直" in 直す
  • chinese ver., japanese ver.
  • firefox displays chinese version despite setting japanese fonts
  • enter about:config
  • change font.cjk_pref_fallback_order from zh-cn,zh-hk,zh-tw,ja,ko to ja,zh-cn,zh-hk,zh-tw,ko
  • also, default western font is serif instead of sans-serif for some reason, makes japanese also serif, e.g. in myanimelist
  • change font.default.x-western from serif to sans-serif
  • or just change in normal settings page about:preferences, "General" -> "Fonts" -> "Fonts for" select Latin, "Proportional" select "Sans Serif"

how to tell Noto Sans CJK JP and IPAGothic apart

  • katakana "ta": タ
    • ipa horizontal line precisely connects the two parallel lines
    • noto looks kind of like a ヌ, doesn't touch left and juts past right

pipewire

pacman -S pipewire
  • make sure to select "WirePlumber" session manager
  • replace ALSA with pipewire
pacman -S pipewire-alsa
  • replace pulseaudio with pipewire
pacman -S pipewire-pulse
  • also enables bluetooth management
  • enable services
systemctl --user enable pipewire-pulse.service
systemctl --user start  pipewire-pulse.service
  • check working
pactl info
  • speaker test works!
speaker-test

screensaver (xss-lock/i3lock)

pacman -S xss-lock
  • set to use i3lock as a locker
xss-lock --transfer-sleep-lock -- i3lock --nofork --image=$HOME/Pictures/config/screensaver &
  • for i3lock, --nofork prevents multiple instances and set background image
  • manually trigger lock
loginctl lock-session

picom transparency leaks screen after lock

  • picom set to make inactive windows slightly transparent (inactive-opacity = 0.9;)
  • this causes i3lock to also become transparent, leaking your desktop screen
  • see reddit - Picom make i3lock opaque
  • edit ~/.config/picom/picom.conf, add
opacity-rule = [ "100:class_g = 'i3lock'" ];

using picom-trans?

  • man picom
       --opacity-rule OPACITY:'CONDITION'
           Specify a list of opacity rules, in the format PERCENT:PATTERN, like
           50:name *= "Firefox". picom-trans is recommended over this.
  • okay, man picom-trans, should be ran like
picom-trans -n "i3lock" 100
  • however, this has to happen after the window exists
  • hard to do, since i3lock is should be ran with --nofork

archive

  • below simply doesn't work, works when running i3lock from a terminal but not when actually triggered (by sleep or with loginctl lock-session)
  • follow the instructions for slock in the picom article: arch wiki - picom
  • install xwininfo
sudo pacman -S xorg-xwininfo
  • run the command and click to get the window id
xwininfo & i3lock --nofork --image=$HOME/Pictures/config/screensaver
  • use the discovered id to have picom exclude the window
picom --daemon --focus-exclude 'id = 0x4600007'

alsa-project

pacman -S alsa-utils
  • for better resampling:
pacman -S alsa-plugins
  • unmute channels:
alsamixer
  • speaker test:
speaker-test -c2
  • hard to use, can't get working
  • just install userspace component, you'll have to anyways!

bluez

pacman -S bluez
pacman -S bluez-utils
  • enable service
systemctl enable bluetooth.service
systemctl start  bluetooth.service
  • start command line prompt
bluetoothctl
  • turn power on, turn agent, start scanning for devices
[bluetooth]# power on
[bluetooth]# agent on
[bluetooth]# default-agent
[bluetooth]# scan on
  • find MAC address of device
  • note: might be spammed by other devices, exit to prevent
  • pair, connect, and trust for future auto-connect
[bluetooth]# pair MAC_ADDRESS
[bluetooth]# connect MAC_ADDRESS
[bluetooth]# trust MAC_ADDRESS
  • enable auto power-on of bluetooth module in /etc/bluetooth/main.conf
[Policy]
AutoEnable=true

bluetooth randomly stuck

  • bluetooth stuck after waking up from sleep
  • sudo systemctl restart bluetooth.service and the like hangs
  • kill daemon directly
sudo pkill -9 bluetoothd

device not showing up

  • arch wiki - bluetooth
  • certain bluetooth low energy (BLE) devices don't show up in scan
  • set transport le
[bluetooth]# menu scan
[bluetooth]# transport le
[bluetooth]# back
[bluetooth]# scan on
[bluetooth]# devices

iwd

pacman -S iwd
  • edit configuration in /etc/iwd/main.conf to enable DHCP management
[General]
EnableNetworkConfiguration=true
  • add DNS with openresolv (see DNS)
[Network]
NameResolvingService=resolvconf
  • start systemd service
systemctl enable iwd
systemctl start  iwd
  • enter prompt
iwctl
  • helpful commands in prompt
[iwd]# help
[iwd]# station list
[iwd]# station wlan0 connect WIFI_NAME
[iwd]# station list
  • still need DNS, can only use systemd-resolved / resolvconf
  • it seems iwd can do its own DNS (or DHCP does DNS?)
  • DNS is provided by (g)libc, see arch wiki - domain name resolution

eduroam

  • generate password hash
iconv -t utf16le | openssl md4 -provider legacy
  • EOF to end (don't press enter, sends '\n'): press ctrl-D twice
  • edit /var/lib/iwd/essid.8021x, for eduroam /var/lib/iwd/eduroam.8021x:
[Security]
EAP-Method=PEAP
EAP-Identity=anonymous@gatech.edu
# EAP-PEAP-CACert=/path/to/root.crt
# EAP-PEAP-ServerDomainMask=lawn.gatech.edu
EAP-PEAP-Phase2-Method=MSCHAPV2
EAP-PEAP-Phase2-Identity=username@gatech.edu
EAP-PEAP-Phase2-Password-Hash=passwordhash

[Settings]
AutoConnect=true
  • can't put EAP-PEAP-CACert in home directory

ead

  • ethernet authentication daemon
systemctl start ead.service

plymouth

arch wiki - plymouth

  • install plymouth (recommended to use development version, but unstable)
paru -S plymouth
  • add plymouth hook to /etc/mkinitcpio.conf

    HOOKS=(base udev autodetect keyboard keymap consolefont modconf block encrypt lvm2 filesystems fsck)

    HOOKS=(base udev plymouth autodetect keyboard keymap consolefont modconf block plymouth-encrypt lvm2 filesystems fsck)

  • make sure to replace encrypt with plymouth-encrypt!
    • (now: plymouth-encrypt no longer necessary as of version 22.02.122-7)
  • add amdgpu to MODULES
MODULES=(amdgpu ...)
quiet loglevel=3 udev.log_level=3 splash vt.global_cursor_default=0 fbcon=nodefer
  • splash necessary, fbcon=nodefer: don't try to defer vendor logo
  • switch display manager service for smoother transition
sudo systemctl disable sddm.service
sudo systemctl enable sddm-plymouth.service
  • can't quite get totally smooth transition (goes to black then sddm) but good enough for me :p
    • (now: plymouth-encrypt no longer necessary as of version 22.02.122-7)

theming

  • list themes (can install additional from AUR)
plymouth-set-default-theme -l
  • use -R to rebuild initramfs
plymouth-set-default-theme -R theme
  • or edit /etc/plymouth/plymouthd.conf
[Daemon]
Theme=simple
  • and regenerate initramfs with sudo mkinitcpio -P
  • for themes using ModuleName two-step, e.g. spinner (check /usr/share/plymouth/themes/ folder, .plymouth file for module)
  • add background to /usr/share/plymouth/themes/theme/background-tile.png
  • can only tile! (seems hardcoded)

script module

  • problem: many modules compiled into .so, hard to modify
  • solution: use script module, write code in domain-specific language
  • language documented on Plymouth page but out of date
  • easiest to read C source directly
  • and examples:
  • language is sort of weird
    • everything is an object...
    • ...except functions, they seem not to be first-class objects
    • no runtime errors, NULL propagation
    • global easily pollutes namespace
  • feels like what I would imagine JavaScript is
    • but to be honest I have written more .script than .js...

testing

  • switch to virtual console with ctrl+alt+F6
  • log in as root, run
plymouthd
plymouth show-splash
plymouth quit
  • problem: once plymouth starts showing splash, cannot issue commands!
  • solution: make shell file, say test.fish
plymouthd --debug --debug-file=/usr/share/plymouth/themes/simple/testing/log.txt
plymouth show-splash
sleep 5
plymouth quit
  • run with test.fish, after 5 seconds automatically kills
  • testing messages:
plymouthd
plymouth show-splash
set message "test message"
sleep 1
plymouth display-message --text=$message
sleep 2
# has to be the same message or callback isn't called
plymouth hide-message --text=$message
sleep 2
  • testing passwords: need to inject key presses into /dev/tty1 (by default)
  • easiest way (not necessarily best) with TIOCSTI
  • see stackoverflow - inject.py:
import fcntl
import sys
import termios

with open(sys.argv[1], "w") as fd:
    chars = eval(f"'{sys.argv[2]}'")
    for c in chars:
        fcntl.ioctl(fd, termios.TIOCSTI, c)
  • client script:
plymouthd
plymouth show-splash
sleep 1
plymouth ask-for-password --prompt="test" &
sleep 1
python inject.py /dev/tty1 "these keypresses are sent to /dev/tty1"
sleep 1
# backspaces
python inject.py /dev/tty1 "\x7f\x7f\x7f\x7f\x7f\x7f\x7f"
sleep 1
python inject.py /dev/tty1 "additional text"
sleep 1
# enter, send password
python inject.py /dev/tty1 "\n"
sleep 2
plymouth quit
  • useful: tmux to have multiple consoles, vim settings:
set autoindent
set expandtab
set shiftwidth=4
set tabstop=4
set colorcolumn=80

multi-head

just works!

careful when scripting:

Window.GetX()
Window.GetY()

are not accurate (leads to black bars on multi-head setups with different resolutions). Basically assume the Window's top left corner is (0, 0) and everything will be ok.

tuxedo-keyboard

paru -S tuxedo-keyboard
  • install power / CPU / fan control
paru -S tuxedo-control-center-bin
  • set screen brightness (0-255)
echo 32 | sudo tee /sys/class/backlight/amdgpu_bl0/brightness
  • set keyboard backlight (0-2)
echo 0 | sudo tee /sys/devices/platform/tuxedo_keyboard/leds/white:kbd_backlight/brightness

xorg

xkb

  • arch wiki - xmodmap
  • outdated, use xkb instead
  • still useful for viewing what modifiers are set to, run with xmodmap
pacman -S xorg-xmodmap
  • xev, keyboard event viewer, also useful tool, run with xev
pacman -S xorg-xev
pacman -S xorg-xkbcomp
  • complicated as hell, can't say too much about this
  • if you screw up, can make keyboard unusable, first store default config
xkbcomp $DISPLAY output.xkb
  • can reset with
setxkbmap -layout us
  • once you make custom keymap at ~/.Xkeymap, put in ~/.xprofile at startup:
test -f ~/.Xkeymap && xkbcomp ~/.Xkeymap $DISPLAY

Super_R as mod3

  • use case: qmk "hyper" (not in the linux modifier sense) is ctrl_L + shift_L + alt_L + super_L
  • use this as i3 mod, need secondary modifier that can't be ctrl/shift/alt/super
  • use super_R as unused mod3 to distinguish
  • comment out (not necessary, no clue what this does)
    interpret Super_R+AnyOf(all) {
        virtualModifier= Super;
        action= SetMods(modifiers=modMapMods,clearLocks);
    };
  • change Super to Mod3 here
    interpret Super_R+AnyOfOrNone(all) {
        action= SetMods(modifiers=Super,clearLocks);
    };
  • change Mod4 to Mod3 here
    modifier_map Mod4 { <RWIN> };
  • n.b.: I no longer use this, it's simpler to make hyper shift_L + alt_L + super_L (still unlikely to conflict with other keybindings) and use ctrl as a secondary modifier

pulseaudio

pacman -S pulsemixer

fish

Fish is a non-POSIX compliant shell that can do a lot of interesting things.

See my asciinema for inspiration.

Package manager is fisher although it has its quirks (can only update all packages).

Theme is "Tomorrow", run fish_config to select it.

playerctl

pacman -S playerctl
  • start daemon to track most player with most recent activity, e.g. put in ~/.xprofile
playerctld daemon
  • XF86Audio audio control keys are already bound in default i3 config
# Use pactl to adjust volume in PulseAudio.
set $refresh_i3status killall -SIGUSR1 i3status
bindsym XF86AudioRaiseVolume exec --no-startup-id pactl set-sink-volume @DEFAULT_SINK@     +10% && $refresh_i3status
bindsym XF86AudioLowerVolume exec --no-startup-id pactl set-sink-volume @DEFAULT_SINK@     -10% && $refresh_i3status
bindsym XF86AudioMute        exec --no-startup-id pactl set-sink-mute   @DEFAULT_SINK@   toggle && $refresh_i3status
bindsym XF86AudioMicMute     exec --no-startup-id pactl set-source-mute @DEFAULT_SOURCE@ toggle && $refresh_i3status
  • bind XF86Audio player control keys to corresponding playerctl commands
# my headphones alternate between play/pause while my keyboard just has play
# so to keep it consistent, force both to toggle
bindsym XF86AudioPlay    exec playerctl play-pause
bindsym XF86AudioPause   exec playerctl play-pause
bindsym XF86AudioStop    exec playerctl stop
bindsym XF86AudioPrev    exec playerctl previous
bindsym XF86AudioNext    exec playerctl next
bindsym XF86AudioForward exec playerctl position 1+
bindsym XF86AudioRewind  exec playerctl position 1-

ibus

pacman -S ibus
  • run at startup, set environmental variables, put in ~/.xprofile
ibus-daemon --daemonize --replace --xim

export GTK_IM_MODULE=ibus
export QT_IM_MODULE=ibus
export XMODIFIERS=@im=ibus
  • no clue what --xim does, but if left out doesn't work in alacritty, e.g.
  • no clue what the environmental variables do, but if left out doesn't work in alacritty, e.g.
  • can put variables in /etc/environment but why would you want to? need to edit with sudo, etc.
  • edit config, add input methods, etc.
ibus-setup
  • can also right-click icon if it's running
  • change activation shortcut to whatever you like, etc.

mozc (japanese ime)

ibus overwrites xkb

  • using xmodmap/xkb to make changes
  • ibus randomly clears these when switching back and forth
  • "IBus Preferences" -> "Advanced" -> "Keyboard Layout" -> check "Use system keyboard layout"

ibus-mozc

paru -S mozc
  • install communication module with ibus
paru -S ibus-mozc
  • add to ibus
ibus-setup
  • "IBus Preferences" -> "Input method" -> "Add" -> "Japanese" -> "Mozc"
  • startup in hiragana mode, useful if no hardware key for Eisu etc.
  • edit ~/.config/mozc/ibus_config.textproto, change active_on_launch from False to True
...
}
active_on_launch: True

android-tools

pacman -S android-tools
  • install udev rules
pacman -S android-udev
  • enable usb debugging on phone

    • e.g., about phone, tap build number 7 times
    • system -> developer options -> usb debugging, enable
  • shows up in

adb devices
  • copy file from phone
adb pull src dest
  • copy file to phone
adb push src dest

clipster

Clipster is a Python clipboard manager that can be installed from the AUR:

paru -S clipster

In order to start it, add the command to your ~/.xprofile or however you want to run a command on startup.

clipster --daemon &

Clipster supports persistence out of the box and can be configured to store no history.

Edit the configuration file at ~/.config/clipster/clipster.ini.

[clipster]

# Number of items to save in the history file for each selection.
# 0 - don't save history.
history_size = 0

Note that you can still check your clipboard history with

clipster --select

This will show the entries you've copied, but this data is stored in memory instead of on disk. If you kill the daemon (pkill clipster) and re-run it, you'll see that the history is cleared.

GRUB

install

sudo pacman -S grub efibootmgr
  • Assume EFI system partition already mounted to /boot
  • Install GRUB
grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=GRUB
  • Configuration file at /etc/default/grub
  • Need to run grub-mkconfig!
  • Use grub-mkconfig to generate /boot/grub/grub.cfg
grub-mkconfig -o /boot/grub/grub.cfg
  • Or can edit /boot/grub/grub.cfg directly without /etc/default/grub

microcode

pacman -S amd-ucode
  • update GRUB, automatically adds microcode to startup
grub-mkconfig -o /boot/grub/grub.cfg
  • check amd-ucode.img before initramfs-linux.img in /boot/grub/grub.cfg

    initrd /boot/amd-ucode.img /boot/initramfs-linux.img

  • check kernel messages for early loading of microcode
journalctl -k --grep=microcode

image background, styling, theming

  • ubuntu - grub
  • option 1: set GRUB_BACKGROUND in /etc/default/grub
    • problem: filesystem encrypted, image not accessible by grub!
  • option 2: copy an image file to /boot/grub
    • solution: /boot isn't encrypted
  • colors: set "black" for transparent, full list here
  • set GRUB_COLOR_NORMAL (default) and GRUB_COLOR_HIGHLIGHT (selected)

fix rescue shell before menu

  • fix initially entering shell instead of menu
    • but still works with exit command
  • re-order UEFI entries
    • UEFI NVME Drive BBS Priorities set first to GRUB instead of ubuntu

plocate

plocate is a locate implementation.

Generate the index

sudo updatedb

Look for a path

locate <path>

qmk

pacman -S qmk
  • qmk setup
qmk setup
  • or use personal fork
qmk setup stephen-huan/qmk_firmware
  • set default keyboard and keymap
qmk config user.keyboard=dm9records/plaid
qmk config user.keymap=stephen-huan
  • compile and flash
qmk compile
qmk flash

tmux

tmux is a "terminal multiplexer", meaning its main functionality is in splitting terminal windows and controlling multiple terminals from the same window. In practice, I use it to detach a terminal window from the window itself (e.g. putting cmus in a tmux server and then detaching means the music still plays, even if the window is closed. The music player can then be brought up again with tmux attach, if I want to switch songs or adjust the volume). These days I don't make much use of tmux anymore, instead preferring to use features from my window manager, terminal emulator, or text editor.

tmux is probably most helpful for remote connections with ssh, so your commands keep running even after you close the connection.

yubikey-manager

pacman -S yubikey-manager
  • enable service
systemctl enable pcscd.service
systemctl start  pcscd.service
  • for U2F
pacman -S libfido2

dhcp (dhclient)

  • install dhclient (n.b. deprecated)
pacman -S dhclient
  • start system service
systemctl enable dhclient@eno1.service
systemctl start  dhclient@eno1.service
  • dhclient overwrites /etc/resolv.conf which is a problem if using vpn, etc.
  • see arch forum - dhclient overwrites resolv.conf even when resolvconf is installed
  • I can't get this to work, resolvconf processes it in the wrong order
  • this could be fixed by hardcoding or just ignoring DNS altogether (I get my DNS server from mullvad or 1.1.1.1 anyways)
  • patch from arch forum in /etc/dhclient-enter-hooks
	# if [ -f /etc/resolv.conf ]; then
	#     chown --reference=/etc/resolv.conf $new_resolv_conf
	#     chmod --reference=/etc/resolv.conf $new_resolv_conf
	# fi
	# mv -f $new_resolv_conf /etc/resolv.conf

        # use resolvconf
        cat $new_resolv_conf | /usr/bin/resolvconf -a $interface
        rm $new_resolv_conf
  • use unbound to prevent DNS servers being in the wrong order
operation not permitted
  • error journalctl -u dhclient@eno1.service
Jun 11 16:22:03 neko dhclient[687]: send_packet: Operation not permitted
Jun 11 16:22:03 neko dhclient[687]: dhclient.c:2996: Failed to send 300 byte long packet over fallback interface.
sudo tcpdump > out.txt
  • note that bootps is usually port 67 and bootpc is usually port 68
02:58:43.767759 IP neko.bootpc > 255.255.255.255.bootps: BOOTP/DHCP, Request from b0:25:aa:44:bd:c2 (oui Unknown), length 300
02:58:43.770219 ARP, Request who-has res388d-128-61-95-198.res.gatech.edu (Broadcast) tell _gateway, length 46
02:58:43.773852 IP _gateway.bootps > neko.bootpc: BOOTP/DHCP, Reply, length 300
02:58:43.774738 IP _gateway.bootps > neko.bootpc: BOOTP/DHCP, Reply, length 300
iptables -A OUTPUT -p udp --sport 1024:65535 --dport 67 -j ACCEPT
iptables -A OUTPUT -p udp --sport 68 --dport 67 -j ACCEPT

iptables -A INPUT -p udp --sport 1024:65535 --dport 68 -j ACCEPT
iptables -A INPUT -p udp --sport 67 --dport 68 -j ACCEPT
  • list
ebtables-nft --list
  • to undo run
iptables -F
ebtables-nft -F
# exclude dhclient from vpn, also from firewall
mullvad split-tunnel pid add "$(pgrep --oldest dhclient)"

dhcpcd

  • arch wiki - dhcpcd
  • need separate DHCP client for ethernet --- iwd has its own, but only for itself (wireless)
  • install dhcpcd
pacman -S dhcpcd
  • could enable for all interfaces, but only need for ethernet:
systemctl enable dhcpcd@eno1.service
systemctl start  dhcpcd@eno1.service
  • hangs on boot up, waiting for IP address
  • create /etc/systemd/system/dhcpcd@eno1.service.d/no-wait.conf
  • could use systemctl edit
[Service]
ExecStart=
ExecStart=/usr/bin/dhcpcd -b -q %I
  • already automatically enables/disables ethernet based on cable plug in/out
  • add wifi disable/enable based on ethernet state by adding hook to /etc/dhcpcd.exit-hook (or in /etc/dhcpcd.enter-hook or in /usr/lib/dhcpcd/dhcpcd-hooks)
  • based on this comment
  • see man dhcpcd-run-hooks for the values of $interface, $reason, etc.
# disable wifi if ethernet connected and enable wifi if ethernet disconnected

wired=eno1
wireless=wlan0

if [ "${interface}" = $wired ]; then
    case "${reason}" in NOCARRIER|BOUND)
    if $if_up; then     # ethernet up means wifi down
        iwctl station $wireless disconnect
    elif $if_down; then # ethernet down means wifi up
        # parse `iwctl known-networks list` and connect to most recent network
        last="$(/home/stephenhuan/bin/iwd-last-network)"
        iwctl station $wireless connect $last
    fi
    ;;
    esac
fi

losing connection

  • connection randomly drops for a few seconds, happens relatively frequently
  • journalctl -u dhcpcd@eno1.service
May 29 15:17:33 neko dhcpcd[806]: eno1: 00:56:2b:56:19:38(00:00:00:ff:eb:0d) claims 128.61.88.130
May 29 15:17:33 neko dhcpcd[806]: eno1: 00:aa:6e:d4:c0:38(00:00:00:ff:f2:b4) claims 128.61.88.130
May 29 15:17:33 neko dhcpcd[806]: eno1: 10 second defence failed for 128.61.88.130
May 29 15:17:33 neko dhcpcd[806]: eno1: deleting route to 128.61.80.0/20
May 29 15:17:33 neko dhcpcd[806]: eno1: deleting default route via 128.61.80.1
May 29 15:17:34 neko dhcpcd[806]: eno1: rebinding lease of 128.61.88.130
May 29 15:17:34 neko dhcpcd[806]: eno1: probing address 128.61.88.130/20
May 29 15:17:39 neko dhcpcd[806]: eno1: leased 128.61.88.130 for 7200 seconds
May 29 15:17:39 neko dhcpcd[806]: eno1: adding route to 128.61.80.0/20
May 29 15:17:39 neko dhcpcd[806]: eno1: adding default route via 128.61.80.1
  • same problem
    • advice was to fix the ip conflict, hard to do
      • already using dynamic ip instead of static
      • can't change the configuration of other devices
  • problem described in rfc2131
    1. The client receives the DHCPACK message with configuration parameters. The client SHOULD perform a final check on the parameters (e.g., ARP for allocated network address), and notes the duration of the lease specified in the DHCPACK message. At this point, the client is configured. If the client detects that the address is already in use (e.g., through the use of ARP), the client MUST send a DHCPDECLINE message to the server and restarts the configuration process. The client SHOULD wait a minimum of ten seconds before restarting the configuration process to avoid excessive network traffic in case of looping.
  • fits with journalctl log:
    • found ip conflict with ARP, someone else is claiming the address
    • eno1: 00:56:2b:56:19:38(00:00:00:ff:eb:0d) claims 128.61.88.130
    • wait for ten seconds before restarting
    • 10 second defence failed for 128.61.88.130
    • re-negotiate lease
    • eno1: leased 128.61.88.130 for 7200 seconds
  • check ARP with arp-scan:
pacman -S arp-scan
  • solution proposed in issue dhcpcd loses static IP
  • certain devices send faulty ARP probes, tell dhcpcd to ignore ARP
  • /etc/dhcpcd.conf
noarp
May 31 23:20:37 neko dhcpcd[743]: ps_root_recvmsg: Operation not permitted

globalprotect-openconnect

  • if using network manager, install package networkmanager-openconnect, unfortunately I am using iwd
  • can use GlobalProtect-openconnect or gp-saml-gui to do web login
  • surprisingly enough, GlobalProtect-openconnect is on official repositories
pacman -S globalprotect-openconnect
  • run
gpclient
  • enter vpn.gatech.edu for portal address
  • doesn't work with proprietary 2fa
Gateway authentication failed

Unknown response for gateway
prelogin interface.

ifplugd

pacman -S ifplugd
  • edit config to change default eth0 device to eno1 at /etc/ifplugd/ifplugd.conf
INTERFACES="eno1"
  • enable service
systemctl enable ifplugd@eno1.service
systemctl start  ifplugd@eno1.service
  • can use to disable wifi when ethernet connected and enable wifi when ethernet disconnected
  • runs /etc/ifplugd/ifplugd.action on up/down with two arguments: name of ethernet interface and whether it went up or down. Shell script inspired by this link:
#!/bin/sh
# disable wifi if ethernet connected and enable wifi if ethernet disconnected

case $2 in
    up)   # ethernet up means wifi down
        iwctl station wlan0 disconnect
        ;;
    down) # ethernet down means wifi up
        # parse `iwctl known-networks list` and connect to most recent network
        iwctl station wlan0 connect "$(/home/stephenhuan/bin/iwd-last-network)"
        ;;
esac
  • remember to mark as executable!
chmod +x /etc/ifplugd/ifplugd.action
  • parsing script iwd-last-network simply wraps iwctl known-networks list:
#!/usr/bin/env python3
"""
Script to parse `iwctl known-networks list`
and return the most recently connected network.

iwd version 1.30-1.
"""
import datetime
import subprocess


def __known_networks() -> str:
    """Wraps `iwctl known-networks list`."""
    out = subprocess.run(
        ["iwctl", "known-networks", "list"],
        capture_output=True,
        text=True,
    )
    return out.stdout


def get_date(date: str) -> datetime.datetime:
    """Parse iwctl date format into a datetime object."""
    return datetime.datetime.strptime(date, "%b %d, %H:%M %p")


def get_known_networks() -> list[str]:
    """Parses the output of iwctl."""
    lines = __known_networks().strip().splitlines()
    header = lines[2].lower()
    fields = header.split()[1:]
    start = header.find(" ")
    starts = {field: header.find(field) - start for field in fields}
    offset = {
        field: (starts[field], starts.get(next_field, len(header)))
        for field, next_field in zip(fields, fields[1:] + [None])
    }
    get_field = lambda line, field: line[
        offset[field][0] + line.find(" ") : offset[field][1] + line.find(" ")
    ].strip()
    return [
        (
            get_field(row, "name"),
            get_field(row, "security"),
            get_field(row, "hidden"),
            get_date(" ".join(row.split()[-4:])),
        )
        for row in lines[4:]
    ]


if __name__ == "__main__":
    lines = get_known_networks()
    recent = sorted(lines, key=lambda row: row[-1], reverse=True)
    print(recent[0][0])
  • hang on shutdown: [ *** ] A stop job is running for ...
  • forum post
  • bug tracker
  • use dhcpcd hook instead?

mullvad

  • arch wiki - mullvad
  • installing GUI also comes with CLI, nice to have around
  • choice of which to install after running below command
paru -S mullvad-vpn
  • enable service
systemctl enable mullvad-daemon.service
systemctl start  mullvad-daemon.service
  • set auto-connect
mullvad auto-connect set on
  • running mullvad-vpn opens GUI while mullvad is CLI
  • opening GUI creates lock icon in bar, but can't seem to close window
    • use i3, mod+alt+q
  • quitting app kills vpn connection
  • might be easiest to just use CLI:
  • set account number
mullvad account set 1234123412341234
  • set protocol to WireGuard
mullvad relay set tunnel-protocol wireguard
  • list servers
mullvad relay list
  • set server (format 2 character country code, 3 character city code, server-name), from mullvad relay list
mullvad relay set location us atl us-atl-001
  • can give any prefix, e.g. just set location us
  • connect
mullvad connect
  • disconnect
mullvad disconnect
  • check status
mullvad status
  • external check: am I mullvad?
  • launch <program> and exclude it from vpn
mullvad-exclude <program>
  • or, for currently running process with <pid>
mullvad split-tunnel pid add <pid>

OpenConnect

pacman -S openconnect
  • using global protect, proprietary vpn used on many university campuses
  • problem: authentication isn't with SAML, but with some proprietary 2fa
  • surprisingly enough, just works, run openconnect
sudo openconnect --protocol=gp vpn.gatech.edu
  • have to paste in password with ctrl-shift-V
  • make sure mullvad is disconnected before
  • automate login with fish script
function vpn --description "connect to gatech's proprietary global protect vpn"
    # disconnect mullvad before continuing
    set -g mullvad_status (mullvad status)
    mullvad disconnect

    set password "$(pass school/gatech/gatech.edu | head --lines=1)"
    # 1st line is password, 2nd line is 2fa prompt, and 3rd line is gateway
    echo -e "$password\\npush1\\ndc-ext-gw.vpn.gatech.edu" |
        sudo openconnect \
            --protocol=gp \
            vpn.gatech.edu \
            --user=shuan7 \
            --passwd-on-stdin
end

function vpn-cleanup --on-signal SIGINT --description "post hook"
    # if connected before disconnecting, reconnect to mullvad
    if ! string match --entire --ignore-case -q -- disconnected $mullvad_status
        mullvad connect
    end
end

openresolv

pacman -S openresolv
  • configuration file in /etc/resolvconf.conf
  • it seems openresolv works by itself by just specifying a nameserver:
# Configuration for resolvconf(8)
# See resolvconf.conf(5) for details

resolv_conf=/etc/resolv.conf
# If you run a local name server, you should uncomment the below line and
# configure your subscribers configuration files below.
#name_servers=127.0.0.1
name_servers=1.1.1.1
  • sudo resolvconf -u to generate /etc/resolv.conf

unbound

pacman -S unbound
  • install expat for DNSSEC verification
pacman -S expat
name_servers="::1 127.0.0.1"
resolv_conf_options="trust-ad"

private_interfaces="*"
unbound_conf=/etc/unbound/resolvconf.conf
  • edit unbound config /etc/unbound/unbound.conf
# include: "/etc/unbound/resolvconf.conf"

server:
    prefetch: yes
    hide-identity: yes
    hide-version: yes
    tls-system-cert: yes

    forward-zone:
        name: "."
        forward-addr: 194.242.2.2@853#doh.mullvad.net
        forward-addr: 193.19.108.2@853#doh.mullvad.net
        # forward-addr: 1.1.1.1@853#cloudflare-dns.com
        # forward-addr: 1.0.0.1@853#cloudflare-dns.com
        forward-tls-upstream: yes
  • if using vpn, resolvconf generated include should probably not be used, literally the definition of a DNS leak
  • also seems to be broken, can't resolve servers because of mullvad firewall
  • if using mullvad, should use local gateway, can't use TLS because domain name isn't known (10.64.0.1 corresponds to currently connected mullvad server, different hostname depending on which server you're currently connected to). This is annoying because then then the fallbacks can't use TLS. Could hypothetically fix by specifying a particular host. This is doubly annoying because the mullvad doh.mullvad.net DNS servers only use TLS, so they can't be used as fallbacks.
    forward-zone:
        name: "."
        # https://mullvad.net/en/help/socks5-proxy/
        forward-addr: 10.64.0.1
        forward-addr: 1.1.1.1
        forward-addr: 1.0.0.1

detailed notes

To enable DNSSEC for unbound, follow the instructions here.

Basically, to generate the root.key file at /usr/local/etc/unbound just run

sudo unbound-anchor

and to generate the root.hints file (which is not strictly necessary, as unbound comes with a default file, but if your package manager doesn't update as often, you can update it yourself) run

curl --output /usr/local/etc/unbound/root.hints https://www.internic.net/domain/named.cache

conda

paru -S miniconda3
  • lightweight installer for conda that doesn't install as much as anaconda (over 250 packages by default)
  • or micromamba, extremely minimal and fast re-implementation of conda
  • what they want you to do (and what the post-install message will say)
source /opt/miniconda3/etc/fish/conf.d/conda.fish
  • or if on bash/POSIX shell
source /opt/miniconda3/etc/profile.d/conda.sh
  • will add conda to PATH, to make changes permanent
conda init fish
  • or
conda init
  • but I don't necessarily want to have every shell startup be in (base)
  • instead, comment out the addition to the configuration file and run the command manually when using conda
eval /opt/miniconda3/bin/conda "shell.fish" "hook" $argv | source

terminals database is inaccessible

export TERMINFO=/usr/share/terminfo

micromamba

  • re-implementation of conda in C++
  • uses libsolv, library used by Red Hat's dnf and others (see internals)
  • pretty much drop-in replacement, just much faster

nix

n.b.: this is a guide for using nix/entering nixos on archlinux

pacman -S nix
  • start daemon (allows operations on nix store without sudo/root)
sudo systemctl enable nix-daemon.service
sudo systemctl start  nix-daemon.service
  • add self to nix-users
sudo gpasswd -a username nix-users
  • don't use channels because not reproducible, so don't do the below
nix-channel --add https://nixos.org/channels/nixpkgs-unstable
nix-channel --update
  • enable flakes and new CLI interface by editing ~/.config/nix/nix.conf
experimental-features = nix-command flakes
nix-collect-garbage

home-manager

nix run home-manager/master -- init --switch
  • problem with gc:
home-manager switch
nix-store --gc
home-manager switch
  • keeps clearing/re-downloading on every switch

  • looking at the stores: nixpkgs and home-manager, precisely the inputs to the flake

  • solution: use nix-direnv to register flake inputs as gc root

  • problem: failed to set locale

  • solution: set LOCALE_ARCHIVE to

/nix/store/{hash}-glibc-locales-{version}/lib/locale/locale-archive

installing nixos with kexec

nixos-generate-config
  • (optional) use flake template for configuration.nix
nix flake new /etc/nixos -t github:nix-community/home-manager#nixos

configuration tips

  • need to import netboot-minimal.nix
  imports =
    [ # Include the results of the hardware scan.
      ./hardware-configuration.nix
      (modulesPath + "/installer/netboot/netboot-minimal.nix")
    ];
  # overwrite /installer/netboot/netboot-minimal.nix
  hardware.enableRedistributableFirmware = lib.mkForce true;
  • on luks, probably need to manually specify root partition
  boot.initrd.luks.devices.cryptlvm.device =
    "/dev/disk/by-uuid/5d57809c-d0e9-49e9-939e-f5d68392faf4";
  # manually specify because `nixos-generate-config` doesn't pick it up
  fileSystems."/" = {
    device = "/dev/VolumeGroup/root";
    fsType = "ext4";
  };
  boot.kernelParams = [ "boot.shell_on_fail" ];

entering the build

  • not risking normal build (overwrites bootloader), try kexec to prototype
  • arch wiki - kexec
pacman -S kexec-tools
  • generate kernel image
nix-build '<nixpkgs/nixos>' \
    -I /nix/store/{hash}-nixpkgs \
    --arg configuration ./configuration.nix \
    --attr config.system.build.kexecTree
  • if using flakes, path to nixpkgs can also be -I nixpkgs=flake:nixpkgs

  • turn off kernel mode setting

  • /etc/default/grub

GRUB_CMDLINE_LINUX_DEFAULT="... quiet loglevel=3 ... nomodeset"
  • remove amdgpu kernel module

  • /etc/mkinitcpio.conf

MODULES=()
  • reload grub/initramfs, reboot, go into vt, run sudo ./result/kexec-boot

pacman

pacman -S pacman-contrib

basic operations

  • update
pacman -Syu
paru
pacman -S pkgname
  • if in AUR
paru -S pkgname
  • remove package and its dependencies and configuration files
pacman -Rns pkgname

searching

  • package list
pacman -Q
  • search for package
pacman -Qs query
  • package information
pacman -Qi pkgname
  • list of files installed by package
pacman -Ql pkgname
  • list aur packages
pacman -Qm
  • list explicitly installed packages
pacman -Qe
  • list all explicitly installed native packages (not aur) that are not direct or optional dependencies
pacman -Qent

orphans

  • list and remove all orphan packages
pacman -Qtdq | pacman -Rns -
  • include optional requirements as well
pacman -Qttdq
  • even more aggressive (account for cycles etc.)
pacman -Qqd | pacman -Rsu --print -

cache

  • clear cache (/var/cache/pacman/pkg/) except for last three versions (from pacman-contrib)
paccache -r
  • clear cache except for currently installed packages
pacman -Sc
  • clear cache completely
pacman -Scc

rollbacks

pacman -U https://archive.archlinux.org/packages/path/packagename.pkg.tar.zst
  • if downgrade, pin version so pacman doesn't update it
  • edit /etc/pacman.conf and add package to IgnorePkg
# Pacman won't upgrade packages listed in IgnorePkg and members of IgnoreGroup
IgnorePkg   = pkgname

reflector

pacman -S reflector
  • automatically update mirrorlist to select fastest mirrors
  • edit /etc/xdg/reflector/reflector.conf
--save /etc/pacman.d/mirrorlist # set the output path
--protocol https # force https
--country us # set country (get list with `reflector --list-countries`)
--latest 50 # use only the 50 most recently synchronized mirrors (--latest)
--sort rate # sort the mirrors by download speed
sudo systemctl start reflector.service
  • or
reflector --protocol https --country us --latest 50 --sort rate --save /etc/pacman.d/mirrorlist
  • I like https://iad.mirrors.misaka.one/archlinux/$repo/os/$arch :)

AUR helper

  • yay
  • paru
    • written in rust so it must be better
    • also forces you to check PKGBUILDs (which we do read carefully right?)
    • if migrating from other AUR helper
    paru --gendb
    

pacgraph

  • pacgraph makes a graph visualization of all installed packages
paru -S pacgraph
  • generate graph (svg)
pacgraph -f packages
  • convert to png with imagemagick
convert packages.svg packages.png
  • or ffmpeg
ffmpeg -i packages.svg packages.png
  • mine below

graph of my packages (18495 MB)

browserpass

pacman -S browserpass
pacman -S browserpass-firefox
paru -S browserpass-chrome
  • remember to quit and re-open chrome!

browserpass

pass can be used to autofill username and password forms in the browser through the browserpass extension. It is installed in two parts, first, a native messaging host, and second, an extension for the browser.

The native app can be installed with

pacman -S browserpass

Install the Firefox extension with

pacman -S browserpass-firefox

Or the Chrome extension (AUR) with

paru -S browserpass-chrome

Remember to quit and re-open the browser after installing the extension.

In order to use browserpass, the directory structure and file structure needs to be organized in a particular way. This was a bit of pain to migrate from whatever ad-hoc format I was using previously, but it didn't take too long. I recommend putting the password on the first line with no prefix so things like pass -c file still work, and specifying the username with username: ....

gnupg

pacman -S gnupg
  • set pinentry program (should use something graphical for background)
pacman -Ql pinentry | grep /usr/bin/
  • also add ssh support
  • edit ~/.gnupg/gpg-agent.conf
# set SSH_AUTH_SOCK to use gpg-agent instead of ssh-agent
enable-ssh-support

# use alternative pinentry
pinentry-program /usr/bin/pinentry-qt
  • copy over old data (private keys, revocation certificates)

mullvad

  • can't import keys from keyserver with mullvad!

yubikey

pass

pacman -S pass
  • if setup PGP and yubikey, should just work
  • comes with dmenu selection
passmenu

securing

  • set PASSWORD_STORE_SIGNING_KEY
set -gx PASSWORD_STORE_SIGNING_KEY "EA6E27948C7DBF5D0DF085A10FBC2E3BA99DD60E"
  • this requires a signature on .gpg-id and non-system extensions
  • if, for example, using remote git to track and pull update to .gpg-id or malicious extension, won't be used because signature breaks
  • generate signature
gpg --detach-sign .gpg-id
  • do the same for any non-system extensions (not recommended)
  • enable non-system extensions (if extension isn't packaged, e.g.)
set -gx PASSWORD_STORE_ENABLE_EXTENSIONS "true"

pass-otp

browserpass

git-credential-manager

detailed notes

pass is a simple GPG-based command-line password manger. To install, run

pacman -S pass

Note that the archlinux package comes with dmenu integration, with the binary

passmenu

Setting a signing key

In order to set a signing key, use the environmental variable PASSWORD_STORE_SIGNING_KEY

set -gx PASSWORD_STORE_SIGNING_KEY "EA6E27948C7DBF5D0DF085A10FBC2E3BA99DD60E"

Setting this is in order to require a signature on .gpg-id and non-system extensions. For example, if you are using a remote git server to track your password store; if you pull an update to .gpg-id that contains a different key from the one you usually use, you won't encrypt new passwords to the malicious key because the signature will break. New local extensions or modifications to existing extensions won't happen for the same reason.

In order to generate a signature, run

gpg --detach-sign .gpg-id

Do the same for any non-system extensions. However, it's probably more secure to install extensions with your system's package manager, since these packages will be automatically updated and also signed by the package maintainer. If an extension isn't packaged, you can enable non-system extensions with

set -gx PASSWORD_STORE_ENABLE_EXTENSIONS "true"

pass-otp

pacman -S pass-otp
  • automatically updates and checks gpg signature
  • probably more secure than copying to .extensions folder and signing

TOTP 2fa (pass-otp)

With the pass-otp extension, pass can generate time-based one-time passwords (TOTP). These are commonly used in two-factor authentication (the 6 digit codes that change every set period of time, usually every 30 seconds). Usually these are loaded into an app like Google Authenticator by scanning a QR code. However, one can usually show the secret directly, which can be stored in pass. Note that storing two-factor secrets in pass along with your passwords kind of defeats the point of two-factor authentication; stronger two-factor authentication can be achieved with hardware tokens like yubikeys.

pass-otp can be installed with

pacman -S pass-otp

A TOTP URI is then added to the password file in the following format:

otpauth://totp/SERVICE:USERNAME?secret=AAAAAAAAAAAAAAAA&issuer=SERVICE

texlive

pacman -S texlive-core
  • install a lot of TeX live packages
pacman -S texlive-most
pacman -S texlive-lang
  • install biber (biblatex backend)
pacman -S biber
  • tlmgr (tex live package manager) is broken, can install alternative
paru -S tllocalmgr-git
  • but let's be real, how often do you install something with tlmgr?

tectonic (LaTeX compiler)

PACE

  • project directory
~/p-fschaefer7-0
  • home directory
/storage/home/hcoda1/6/shuan7
  • scratch (temp)
~/scratch

logging in

  • need to run GT VPN (GlobalProtect)
  • logging in:
  • (kitty sets $TERM wrong)
TERM=xterm-color ssh shuan7@login-phoenix.pace.gatech.edu
  • (GT password)
  • to see headnodes
pace-whoami

transferring files

submitting jobs

gts-fschaefer7
  • see accounts
pace-quota
  • see queue status
pace-check-queue -c inferno
#!/bin/bash
#SBATCH -Jcknn-cg                 # job name
#SBATCH --account=gts-fschaefer7  # charge account
#SBATCH --nodes=1                 # number of nodes and cores per node required
#SBATCH --ntasks-per-node=1
#SBATCH --cpus-per-task=8
#SBATCH --mem-per-cpu=22gb        # memory per core
#SBATCH -t48:00:00                # duration of the job (hh:mm:ss)
#SBATCH -qinferno                 # QOS name
#SBATCH -ojobs/cg_%j.out          # combined output and error messages file
cd $SLURM_SUBMIT_DIR              # change to working directory

# load modules
module load anaconda3

# enter conda virtual environment
conda activate ./venv

# run commands
lscpu
lsmem

time srun python -m experiments.cg
  • submit job to scheduler (two queues: inferno and embers)
sbatch job.sbatch
  • job inherits current directory, have to run sbatch from proper directory!

  • submitted job status

squeue -u shuan7

interactive session

salloc -A gts-fschaefer7 -q inferno -N 1 --ntasks-per-node=4 -t 1:00:00
  • for gpu
salloc -A gts-fschaefer7 -q inferno -N 1 --gres=gpu:A100:1 --mem-per-gpu=12G -t 0:15:00

software

modules

conda

A (f)ast re-write of p(ar) - far

This is an old plaintext copy of the post on my blog.

par is a formatting tool that inserts line breaks to make the length of each line less than a set number of characters, usually 79 (terminals historically have 80 width). Unfortunately, par is incredibly complicated and introduces random whitespace. So I made my own.

For far to make the paragraphs look good, it minimizes the variance of each line. However, it's constrained to use the fewest number of lines possible, so it doesn't generate really short lines. Finally, it ignores the last line when minimizing variance and tries to make the last line shorter than average, because a typical paragraph usually looks better if the last line is shorter. To summarize,

  1. Minimize the variance of the lengths of each line ...
  2. ... subject to the constraint that the number of lines is smallest
  3. Ignore the last line, while making sure it's shorter than average

far uses dynamic programming to minimize variance. It tokenizes the paragraph by splitting on whitespace, and each subproblem in the dynamic program is a suffix of this token list.

Var[X] = E[X]^2 - E[X]^2 = sum(x^2 for x in X)/len(X) - (sum(X)/len(X))^2

The length len(X) is constant because of the smallest number of lines constraint, and so is the sum because the sum of the line lengths is determined by two things: the characters in the tokens and the number of spaces introduced by merging two tokens (combining the words "hello" and "world" onto the same line gives "hello world", with an additional space). The characters stay the same, and the number of spaces is fixed if the number of lines is fixed. Each token starts off as its own line, and each merge reduces the number of lines by 1, so if two solutions have the same number of lines, they must have done the same number of merges.

Thus, minimizing Var[X] is equivalent to minimizing the sum of squares sum(x^2 for x in X) if the number of lines is fixed. Recall that we are trying to minimize variance over the entire paragraph. The overall paragraph has some mean value u. Each line will contribute (x - u)^2 to the overall paragraph's variance. So we want to minimize:

(x1 - u)^2 + (x2 - u)^2 + ... + (xn - u)^2

where xi is the length of a line and we know that x1 + x2 + ... + xn is constant because of the above logic (sum(X) is constant). Expanding,

[x1^2 - 2u x1 + u^2] + [x2^2 - 2u x2 + u^2] + ... + [xn^2 - 2u xn + u^2]
u^2 is a constant, so we can discard those terms and reorganize into
[x1^2 + x2^2 + ... + xn^2] - 2u[x1 + x2 + ... + xn].

The last term is a constant, so minimizing the variance of the overall paragraph is equivalent to minimizing the variance for a suffix of the paragraph (both are minimizing the sum of squares). This is just the variance of the subproblem, so the dynamic programming is valid since optimal substructure holds. In practice, I skip calculating variance entirely and simply minimize the sum of squares. I also do dynamic programming on the variance of each prefix, so that I can easily ignore the last line.

That's it! The algorithm runs in O(NK) where N is the number of characters in the input text and K is the desired width. Since K is usually fixed to some small constant (79, 72, etc.), this is essentially linear in N and I suspect most of the running time is bottlenecked by just I/O (reading the input text and printing out the formatted text). Running with a width of 79 on a 1MB file with over 20,000 lines takes under 200 milliseconds. For 100MB, fmt takes around 11.9 seconds, par takes 15.7, and far takes 16.6. So far is slightly slower than the others, but certainly not enough to be noticeable for "reasonable" inputs, especially if output is redirected into a file rather than displayed to terminal.

Examples

original paragraph:

xxxxx xxx xxx xxxx xxxxxxxxx xx x xxxxxxxxx x xxxx xxxx xxxxxxx xxxxxxxx xxx
xxxxxxxxx xxxxxxxx xx xx xxxxx xxxxx xxxx xx x xxxx xx xxxxxxxx xxxxxxxx xxxx
xxx xxxx xxxx xxx xxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxx xxx xxxxx xx xxxx x xxxx
xxxxxxxx xxxx xxxx xx xxxxx xxxx xxxxx xxxx xxxxxxxxx xxx xxxxxxxxxxx xxxxxx
xxx xxxxxxxxx xxxx xxxx xx x xx xxxx xxx xxxx xx xxx xxx xxxxxxxxxxx xxxx xxxxx
x xxxxx xxxxxxx xxxxxxx xx xx xxxxxx xx xxxxx

fmt -w 72 (greedy algorithm):

xxxxx xxx xxx xxxx xxxxxxxxx xx x xxxxxxxxx x xxxx xxxx xxxxxxx xxxxxxxx
xxx xxxxxxxxx xxxxxxxx xx xx xxxxx xxxxx xxxx xx x xxxx xx xxxxxxxx
xxxxxxxx xxxx xxx xxxx xxxx xxx xxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxx xxx
xxxxx xx xxxx x xxxx xxxxxxxx xxxx xxxx xx xxxxx xxxx xxxxx xxxx
xxxxxxxxx xxx xxxxxxxxxxx xxxxxx xxx xxxxxxxxx xxxx xxxx xx x xx xxxx
xxx xxxx xx xxx xxx xxxxxxxxxxx xxxx xxxxx x xxxxx xxxxxxx xxxxxxx xx xx
xxxxxx xx xxxxx

par 72 (with PARINIT set to rTbgqR B=.,?'_A_a_@ Q=_s>|):

xxxxx xxx xxx xxxx xxxxxxxxx xx x xxxxxxxxx x xxxx xxxx xxxxxxx xxxxxxxx
xxx xxxxxxxxx xxxxxxxx xx xx xxxxx xxxxx xxxx xx x xxxx xx xxxxxxxx
xxxxxxxx xxxx xxx xxxx xxxx xxx xxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxx
xxx xxxxx xx xxxx x xxxx xxxxxxxx xxxx xxxx xx xxxxx xxxx xxxxx xxxx
xxxxxxxxx xxx xxxxxxxxxxx xxxxxx xxx xxxxxxxxx xxxx xxxx xx x xx xxxx
xxx xxxx xx xxx xxx xxxxxxxxxxx xxxx xxxxx x xxxxx xxxxxxx xxxxxxx xx xx
xxxxxx xx xxxxx

far 72:

xxxxx xxx xxx xxxx xxxxxxxxx xx x xxxxxxxxx x xxxx xxxx xxxxxxx
xxxxxxxx xxx xxxxxxxxx xxxxxxxx xx xx xxxxx xxxxx xxxx xx x xxxx
xx xxxxxxxx xxxxxxxx xxxx xxx xxxx xxxx xxx xxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxx xxx xxxxx xx xxxx x xxxx xxxxxxxx xxxx xxxx xx xxxxx
xxxx xxxxx xxxx xxxxxxxxx xxx xxxxxxxxxxx xxxxxx xxx xxxxxxxxx
xxxx xxxx xx x xx xxxx xxx xxxx xx xxx xxx xxxxxxxxxxx xxxx xxxxx
x xxxxx xxxxxxx xxxxxxx xx xx xxxxxx xx xxxxx

Looking at the output of the greedy algorithm, because it always forms a line if it's possible, it creates highly variable line lengths. For example, there are many "valleys" where a line is shorter than the lines adjacent to it, like lines 2 and lines 4, giving the overall paragraph a jagged appearance.

par improves on fmt, but still creates a single large valley. Finally, I would argue that far creates the most aesthetically pleasing paragraph because it minimizes the variance, creating the smoothest paragraph edge.

It's probably possible to modify PARINIT for par to work properly on this example, and in general par works quite well, but it's hard to work through the documentation to find precisely what to do and the recommended PARINIT in the man page should work well. far works well "out of the box" and for better or for worse, only has a single configuration parameter --- width.

Uses

This program is pretty useful whenever writing plaintext in a monospace text editor, e.g. when editing LaTeX, markdown files, college essays, and emails. It's especially useful in vim, which lets you set the option 'formatprg' so the operator gq formats using the external program.

archlinux on a tuxedo pulse

  • CPU: AMD Ryzen 7 4800H with Radeon graphics
  • wikipedia - radeon
  • Renoir (2020) table, Ryzen 7 4800H -> GCN 5th gen architecture
  • GPU: Radeon RX Vega 7 (AMD ATI 04:00.0 Renoir)
  • Driver AMDGPU
  • fn + space to change keyboard backlight
  • LVM on LUKS (LVM in encrypted drive)
    • what tuxedo does by default
  • filesystem: ext4
  • bootloader: grub

tuxedo specific configuration

uefi

  • enter by holding F2 on boot
  • or by selecting corresponding entry in GRUB
  • settings:
    • put USB first in boot order
    • put GRUB second in boot order
    • turn off secure boot (enable later)
    • disable webcam

live boot

  • need to set proper UEFI order (enter UEFI with USB plugged in)
  • to get started
cryptsetup open /dev/nvme0n1p3 cryptlvm
mount /dev/VolumeGroup/root /mnt
arch-chroot /mnt

boot process

  1. Power on --- Tuxedo logo (avoidable?)
  2. bootloarder: grub
  3. splash screen: plymouth
  4. display manager: sddm
  5. window manager: i3

miscellaneous

usb

sudo mount /dev/sda2 /mnt
  • unmount
sudo umount /mnt

pdf viewers

mkinitcpio

  • sudo mkinitcpio -P
==> WARNING: Possibly missing firmware for module: xhci_pci
-> Running build hook: [keymap]
-> Running build hook: [consolefont]
-> Running build hook: [modconf]
-> Running build hook: [block]
==> WARNING: Possibly missing firmware for module: wd719x
==> WARNING: Possibly missing firmware for module: qla2xxx
==> WARNING: Possibly missing firmware for module: qed
==> WARNING: Possibly missing firmware for module: qla1280
==> WARNING: Possibly missing firmware for module: aic94xx
==> WARNING: Possibly missing firmware for module: bfa
-> Running build hook: [plymouth-encrypt]
==> WARNING: Possibly missing firmware for module: qat_4xxx
paru -S mkinitcpio-firmware
  • however, warnings are harmless if nothing is broken
  • one of those cases where the "solution" may be more dangerous than the problem

virtual terminal

set consolefont

==> WARNING: consolefont: no font found in configuration
  • list of fonts /usr/share/kbd/consolefonts/, installed by the kbd package
  • switch to virtual terminal for the following
  • display font table
showconsolefont
  • set font
setfont
  • edit configuration file /etc/vconsole.conf, see man vconsole.conf
FONT=Lat2-Terminus16
  • default font cp437, appears not to be in folder and instead built-in to kernel
  • webpage of fonts
  • .psfu indicates Unicode translation map built-in
  • package for popular terminus font, includes more sizes than kbd
pacman -S terminus-font

proper naming

  • arch wiki - arch terminology

    Arch Linux

    Arch should be referred to as:

    • Arch Linux
    • Arch (Linux implied)
    • archlinux (UNIX name)

    Archlinux, ArchLinux, archLinux, aRcHlInUx, etc. are all weird, and weirder mutations.

    Officially, the 'Arch' in "Arch Linux" is pronounced /ˈɑrtʃ/ as in an "archer"/bowman, or "arch-nemesis", and not as in "ark" or "archangel".

  • can never remember what the "official" names are

install

making live USB

gpg --keyserver-options auto-key-retrieve --verify archlinux-version-x86_64.iso.sig
cp path/to/archlinux-version-x86_64.iso /dev/sdx
  • boot from USB (make sure to set correct UEFI order)

in live environment

  • Connect to WiFi (iwd)
iwctl
[iwd]# help
[iwd]# station list
[iwd]# station wlan0 connect WIFI_NAME
[iwd]# station list
  • Check connection
ping archlinux.org
  • Set system clock
timedatectl set-ntp true
  • Check system time
timedatectl status

partition disk

  • Partition disks: only need UEFI (EFI system partition) and root (/)
  • Swap does not need to be a partition, can be a file for flexibility/ease
lsblk
fdisk -l

If Tuxedo: by default, partitions look like (1 TB drive)

Devic          ...  Size Type
/dev/nvme0n1p1        1G EFI System
/dev/nvme0n1p2      512M Microsoft basic data
/dev/nvme0n1p3      930G Linux filesystem
  • Normally could use existing EFI partition
  • But file format is wrong (we want FAT32 for GRUB, format is ext3)
  • Will fix later

prepare drive for encryption

cryptsetup open --type plain -d /dev/urandom /dev/nvme0n1p3 to_be_wiped
  • Verify that it exists
lsblk
  • Wipe container with zeros
dd if=/dev/zero of=/dev/mapper/to_be_wiped status=progress

WARNING: 1 TB disk capacity / (80 MB/s write speed) = ~3.5 hours

  • Close temporary container
cryptsetup close to_be_wiped

encrypt entire drive

cryptsetup luksFormat /dev/nvme0n1p3
  • Open container (decrypted container now at /dev/mapper/cryptlvm)
cryptsetup open /dev/nvme0n1p3 cryptlvm
  • Create physical volume
pvcreate /dev/mapper/cryptlvm
  • Create volume group (name VolumeGroup, arbitrary)
vgcreate VolumeGroup /dev/mapper/cryptlvm
  • Create logical volumes
  • I said we could use a swap file
  • If using LVM, easy to re-size partitions, might as well use swap partition
  • Make swap partition same size as RAM for easy suspend to disk (hibernate)
  • Don't use entire volume group capacity for easy resizing in the future
lvcreate -L 32G      VolumeGroup -n swap
lvcreate -l 100%FREE VolumeGroup -n root
  • Format filesystems
mkswap    /dev/VolumeGroup/swap
mkfs.ext4 /dev/VolumeGroup/root
  • Mount filesystems
swapon /dev/VolumeGroup/swap
mount /dev/VolumeGroup/root /mnt
  • (n.b. the above steps also work for external storage, e.g. a backup drive)

  • Prepare boot partition

mkfs.fat -F 32 /dev/nvme0n1p1
mkdir /mnt/boot
mount /dev/nvme0n1p1 /mnt/boot
pacstrap /mnt base linux linux-firmware
  • Generate fstab
genfstab -U /mnt >> /mnt/etc/fstab

switch into new system

  • Change root into new system
arch-chroot /mnt
  • Install necessary packages
pacman -S lvm2 grub efibootmgr iwd
  • Install useful packages
pacman -S man-db man-pages neovim fish
  • Set timezone
ln -sf /usr/share/zoneinfo/US/Eastern /etc/localtime
  • Run hwclock
hwclock --systohc
  • Edit /etc/locale.gen and uncomment en_US.UTF-8 UTF-8, run locale-gen
locale-gen
  • Create /etc/locale.conf with the LANG variable
LANG=en_US.UTF-8
  • Create hostname in /etc/hostname
myhostname
  • Set root password
passwd

edit initramfs

  • Add the following to /etc/mkinitcpio.conf

    HOOKS=(base udev autodetect keyboard keymap consolefont modconf block encrypt lvm2 filesystems fsck)

  • Recreate initramfs image
mkinitcpio -P

install GRUB

grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=GRUB
  • Edit /etc/default/grub where device-UUID is the UUID of /dev/nvme0n1p1
  • This can be found with lsblk -f
GRUB_CMDLINE_LINUX_DEFAULT="cryptdevice=UUID=device-UUID:cryptlvm root=/dev/VolumeGroup/root resume=/dev/VolumeGroup/swap"
  • Use grub-mkconfig to generate /boot/grub/grub.cfg
grub-mkconfig -o /boot/grub/grub.cfg
  • Reboot
reboot
  • Hopefully the following:
    • "Arch Linux" appears in GRUB menu
    • Prompt for encryption key
    • Prompt for username
    • Prompt for password
    • Login successful!

post-install

useradd -m -s /usr/bin/fish username
  • Set password
passwd username
  • Give sudo permission
EDITOR=nvim visudo
  • Go to "User privilege specification" and add
USER_NAME   ALL=(ALL) ALL
  • Logout of root and log in to user account
exit
  • or switch user
su stephenhuan
sudo pacman -S xdg-user-dirs
  • Create user directories
xdg-user-dirs-update
  • Install AUR helper paru
sudo pacman -S --needed git base-devel
git clone https://aur.archlinux.org/paru.git
cd paru
sudo makepkg -si
  • Get dotfiles with yadm
cd ~
pacman -S yadm
yadm clone https://github.com/stephen-huan/dotfiles
sudo pacman -S sddm
  • Enable display manager
sudo systemctl enable sddm.service
  • Install window manager (i3)
sudo pacman -S i3
sudo pacman -S alacritty
  • Enter graphical
sudo systemctl start sddm.service
  • If no terminal emulator, can get suck in i3!
  • Use ctrl+alt+F[1-6] to switch to virtual console
  • From virtual console back to graphical
sudo systemctl restart sddm.service

audio

Music applications

Apple Music

  • use web client: music.apple.com
  • pros: no installation/configuration
  • cons: many
    • need apple device for 2fa sign in
      • this is not true
    • feels slow/clunky
    • randomly breaks
    • doesn't store place

iTunes

Sync music

sync to iphone

pacman -S ifuse
  • if freezes on write (see #63, probably fixed in newest versions) try aur
paru -S ifuse-git
  • use vlc on phone since simple copying directly to folder, see provided fish script
function musicsync --description "sync music to phone"
  # mount vlc media folder to ~/mnt
  ifuse --documents org.videolan.vlc-ios ~/mnt
  # see https://superuser.com/questions/1192448/rsync-mkstemp-filename-failed-function-not-implemented-38
  rsync -av --progress --no-perms --no-owner --no-group --exclude "*.m3u" \
    ~/Music/personal ~/mnt/
  # copy playlists from cmus
  set temp (pwd)
  cd ~/Music/personal/playlists
  python cmus_copy.py
  # generate playlists from artists
  python artist_playlist.py
  cd "$temp"
  rsync -av --progress --no-perms --no-owner --no-group \
    ~/Music/personal/playlists ~/mnt/personal
  # done
  umount ~/mnt
end

sync to android

function musicsync --description 'sync music to phone'
    # copy playlists from cmus
    set temp (pwd)
    cd ~/Music/personal/playlists
    python cmus_copy.py
    # generate playlists from artists
    python artist_playlist.py
    cd "$temp"
    # copy over to phone
    adb push --sync ~/Music/ /storage/self/primary/
end

clipboard

For an overview of the X window system's approach to the clipboard, see Arch wiki - clipboard. The summary is that the clipboard is managed by X, and has three distinct selections.

  • PRIMARY: selected text, i.e. highlighted by mouse,
  • CLIPBOARD: text that is explicitly copied (e.g. by ctrl-c),
  • SECONDARY: no agreed upon purpose.

In order to manage these selections, install some command-line tool like xclip.

pacman -S xclip

For example, to copy a screenshot (taken with the package maim) to the clipboard,

maim --select --nodrag | xclip -selection clipboard -target image/png

Packages might also use xsel, so it's probably best to have both installed.

pacman -S xsel

losing history

By default, the clipboard contents are lost if the application the is data from is closed. For example, if one opens a terminal window, types some text, then copies the text, it can be pasted somewhere else. But once the terminal window is closed, one can no longer paste the text --- it is lost.

This is because X only stores references to the data, not copies. See the Ubuntu wiki for more information as well as this Reddit post. The Ubuntu article recommends Parcellite while the Reddit post recommends clipmenu. The Arch wiki also has a list of clipboard managers.

Most clipboard managers don't directly solve the persistence issue. Instead, they maintain a history of everything that is copied, and if the selection is lost, one can use a command-line interface or open a GUI to select a previous entry and re-copy it to the clipboard.

But I don't want to have to manually re-copy the last thing I copied, I just want to be able to keep the entry in my clipboard if I close the application. I couldn't get clipmenu or clipcat to work like this.

xclipboard, the official X clipboard manager works, but it always launches a GUI window that can't be easily suppressed. Parcellite works but it's old and relies on GTK2. A modern replacement is ClipIt, but when I used it reminded me that there was a security concern: I sometimes copy passwords and other sensitive information to the clipboard, and all of these clipboard managers store data on disk as plaintext in a temporary directory. I wanted to find a clipboard manager that supported clipboard persistence without manual intervention while storing data only in memory, never touching disk.

For these reasons, I settled on clipster.

copy/paste

One can use the open-source tmk/qmk firmware to bind physical keys to copy/paste. The relevant keycodes are KC_CUT, KC_COPY, and KC_PASTE. The X keyboard event viewer xev (pacman -S xorg-xev) shows that these are mapped to XF86Cut (145) XF86Copy (141), and XF86Paste (143), respectively. Support for these keys seems to be built-in to X as well as most GUI applications.

gpu

sudo pacman -S mesa
  • test mesa support
sudo pacman -S mesa-utils
glxinfo
  • Install DDX driver for 2D acceleration
sudo pacman -S xf86-video-amdgpu
  • Enable vulkan support
sudo pacman -S amdvlk
  • test vulkan support
sudo pacman -S vulkan-tools
vulkaninfo
  • Accelerated video decoding
pacman -S libva-mesa-driver mesa-vdpau

internet/networking

ethernet

ip link show
  • something like this:
1: lo: ...
    link/loopback ...
2: eno1: ...
    link/ether ...
    altname enp2s0
4: wlan0: ...
    link/ether ...
  • turn on:
ip link set eno1 up
  • turn off:
ip link set eno1 down

systemd-networkd

georgia tech

  • need to remember to register new device at portal.lawn.gatech.edu!
  • otherwise, garbage data when trying to make DNS query
  • make sure ethernet is connected and DHCP is working
  • check MAC address with ip link
2: eno1: ...
    link/ether MAC_ADDRESS brd ff:ff:ff:ff:ff:ff
    altname enp2s0

DNS

systemd-resolved

systemctl start systemd-resolved
  • probably just works

nsdo

input

Screen capture

pacman -S maim
  • take full-screenshot
maim file.png
  • make selection with mouse (--nodrag for click twice instead of click-dragging)
maim --select --nodrag file.png
pacman -S obs-studio

copying images to clipboard with maim/xclip

security