Phase 6: Desktop Environment

Phase 6: Desktop Environment

NVIDIA GPU

RTX 5090 requires the open kernel module β€” the proprietary nvidia package doesn’t exist for this GPU generation.

# nvidia-open for main kernel, nvidia-open-lts for LTS fallback kernel
sudo pacman -S nvidia-open nvidia-open-lts nvidia-utils nvidia-settings lib32-nvidia-utils

Sync Kernels to ESP

The nvidia install regenerated initramfs images on /boot (ext4). systemd-boot reads from the ESP β€” sync now, then set up a pacman hook so it’s automatic.

sudo cp /boot/vmlinuz-linux /boot/efi/
sudo cp /boot/vmlinuz-linux-lts /boot/efi/
sudo cp /boot/initramfs-linux.img /boot/efi/
sudo cp /boot/initramfs-linux-lts.img /boot/efi/
sudo cp /boot/intel-ucode.img /boot/efi/
# Verify
ls -lh /boot/efi/vmlinuz-* /boot/efi/initramfs-* /boot/efi/intel-ucode.img
df -h /boot/efi

Pacman Hook: Auto-Sync Kernels to ESP

Without this, every kernel or nvidia update requires manual copy. This hook runs automatically after pacman upgrades.

sudo mkdir -p /etc/pacman.d/hooks
sudo tee /etc/pacman.d/hooks/99-esp-kernel-sync.hook << 'HOOKEOF'
[Trigger]
Type = Path
Operation = Install
Operation = Upgrade
Target = usr/lib/modules/*/vmlinuz
Target = usr/lib/initcpio/*
Target = boot/*

[Action]
Description = Syncing kernels and initramfs to ESP...
When = PostTransaction
Exec = /bin/sh -c 'cp /boot/vmlinuz-* /boot/efi/ && cp /boot/initramfs-linux.img /boot/initramfs-linux-lts.img /boot/intel-ucode.img /boot/efi/'
HOOKEOF
cat /etc/pacman.d/hooks/99-esp-kernel-sync.hook

Reboot for NVIDIA Module

sudo reboot

SSH back in, then verify:

nvidia-smi

Hyprland Stack

sudo pacman -S \
    hyprland xdg-desktop-portal-hyprland \
    waybar wofi mako \
    hyprlock hypridle hyprpaper \
    grim slurp wl-clipboard cliphist swappy

Terminals

sudo pacman -S kitty
yay -S ghostty

Fonts

sudo pacman -S \
    ttf-jetbrains-mono-nerd ttf-nerd-fonts-symbols ttf-font-awesome \
    noto-fonts noto-fonts-emoji noto-fonts-cjk

Audio

sudo pacman -S pipewire pipewire-alsa pipewire-pulse pipewire-jack wireplumber pavucontrol
systemctl --user enable --now pipewire pipewire-pulse wireplumber

Audio Verification

The P16g has onboard Dolby Atmos speakers that require Intel Sound Open Firmware (SOF).

# Install SOF firmware (REQUIRED for onboard speakers)
sudo pacman -S sof-firmware
Reboot required after installing sof-firmware β€” firmware loads at kernel boot.
# After reboot: verify PipeWire sees real audio devices (not "Dummy Output")
wpctl status
# List audio sinks (should show onboard speakers, not just HDMI)
pactl list sinks short
# Set default sink (if multiple sinks detected)
wpctl set-default <SINK_ID>
# Test audio output (pw-play is native PipeWire β€” no alsa-utils dependency)
pw-play /usr/share/sounds/freedesktop/stereo/bell.oga
# Target a specific sink by ID (find IDs with: wpctl status)
pw-play --target <SINK_ID> /usr/share/sounds/freedesktop/stereo/bell.oga
# Volume control (CLI)
wpctl set-volume @DEFAULT_AUDIO_SINK@ 50%
# GUI volume control
pavucontrol &

Volume Control β€” Terminal Reference

Quick commands
# Get current volume (decimal β€” 0.50 = 50%)
wpctl get-volume @DEFAULT_AUDIO_SINK@

# Set absolute volume
wpctl set-volume @DEFAULT_AUDIO_SINK@ 50%

# Raise / lower by 5%
wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+
wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-

# Cap volume at 100% (prevents amplification distortion)
wpctl set-volume -l 1.0 @DEFAULT_AUDIO_SINK@ 5%+

# Mute / unmute toggle
wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle

# Mute microphone toggle
wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle

# Check mute state (output contains "MUTED" if muted)
wpctl get-volume @DEFAULT_AUDIO_SINK@
Switch output device
# List all sinks with IDs
wpctl status | grep -A20 'Sinks:'

# Set default sink by ID (get ID from wpctl status β€” the number before the name)
wpctl set-default <SINK_ID>
Test audio
# Play system bell through default sink
pw-play /usr/share/sounds/freedesktop/stereo/bell.oga

# Play through a specific sink
pw-play --target <SINK_ID> /usr/share/sounds/freedesktop/stereo/bell.oga

Teaching point: @DEFAULT_AUDIO_SINK@ and @DEFAULT_AUDIO_SOURCE@ are PipeWire aliases β€” wpctl resolves them to whichever device is marked * in wpctl status. Never hardcode sink IDs; they change across reboots.

Screen Brightness β€” Terminal Reference

The P16g has a 3.2K Tandem OLED β€” brightness is controlled via brightnessctl which writes to /sys/class/backlight/.

Table 1. Hyprland keybinds (from hyprland.conf)
Key Action

Fn + F5 (XF86MonBrightnessDown)

Decrease 5%, notify

Fn + F6 (XF86MonBrightnessUp)

Increase 5%, notify

CLI commands
# Current brightness (absolute value + percentage)
brightnessctl get
brightnessctl info

# Machine-readable (CSV: device,type,current,percentage,max)
brightnessctl -m

# Extract just the percentage
brightnessctl -m | awk -F, '{print $4}'

# Set absolute percentage
brightnessctl set 50%

# Raise / lower by 5%
brightnessctl set +5%
brightnessctl set 5%-

# Set to specific value (out of max β€” check with brightnessctl max)
brightnessctl set 500

# Minimum usable brightness (1% β€” OLED can go very dim)
brightnessctl set 1%

# Maximum
brightnessctl set 100%
List all controllable devices
# Shows backlight device name, type, and current value
brightnessctl -l

Teaching point: brightnessctl writes to sysfs (/sys/class/backlight/<device>/brightness). The video group grants non-root access β€” your user is already in this group from the Arch install. No sudo needed. The +5% and 5%- syntax is relative β€” it’s brightnessctl doing the math, not the shell.

External keyboard / lid-closed scenario: The Fn keys (XF86MonBrightnessUp/Down) only exist on the laptop keyboard. When using a Kinesis or any external keyboard with the lid closed, control brightness exclusively from the terminal. Same applies to volume β€” wpctl commands work from any terminal session regardless of keyboard.

Screenshot Pipeline β€” Terminal Reference

The screenshot stack is grim (capture) + slurp (region select) + swappy (annotate/edit). All Wayland-native.

Table 2. Hyprland keybinds (from hyprland.conf)
Key Action

Print

Region select β†’ swappy editor

Shift + Print

Fullscreen β†’ swappy editor

Super + Print

Region select β†’ save to ~/Pictures/screenshot-YYYYMMDD-HHMMSS.png

Super + Shift + Print

Fullscreen β†’ save to ~/Pictures/screenshot-YYYYMMDD-HHMMSS.png

CLI commands
# Region select β†’ clipboard (no file saved)
grim -g "$(slurp)" - | wl-copy

# Region select β†’ file
grim -g "$(slurp)" ~/Pictures/screenshot-$(date +%Y%m%d-%H%M%S).png

# Fullscreen β†’ file
grim ~/Pictures/screenshot-$(date +%Y%m%d-%H%M%S).png

# Region select β†’ swappy editor (annotate, crop, then save/copy)
grim -g "$(slurp)" - | swappy -f -

# Capture specific window (by Hyprland address)
grim -g "$(hyprctl activewindow -j | jq -r '"\(.at[0]),\(.at[1]) \(.size[0])x\(.size[1])"')" ~/Pictures/window.png

# Delayed capture (3 seconds β€” time to arrange windows)
sleep 3 && grim ~/Pictures/screenshot-delayed.png
Table 3. Pipeline anatomy
Tool Role

grim

Screen capture for Wayland compositors. -g takes geometry from slurp. - outputs to stdout.

slurp

Interactive region selector β€” click and drag. Returns X,Y WxH geometry string.

swappy

Annotation editor β€” draw, arrow, text, blur. -f - reads from stdin. Save overwrites or copies to clipboard.

wl-copy

Wayland clipboard. Pipe image data to it for paste into any app.

Audio Troubleshooting

Symptom Fix

Dummy Output only, no real sinks

sof-firmware not installed. Install + reboot.

RTX 5090 HDMI audio detected but no speakers

Same β€” sof-firmware missing. HDMI audio is from GPU, onboard needs SOF.

PipeWire not running

systemctl --user enable --now pipewire pipewire-pulse wireplumber

No sound after reboot

Check wpctl status, verify correct sink is default, check pactl list sinks

Crackling/distortion

pw-top to check for xruns. Try PIPEWIRE_LATENCY=256/48000 in environment.

Hyprland Volume Keys β€” pamixer β†’ wpctl Migration

The default Hyprland keybinds for XF86AudioRaiseVolume, XF86AudioLowerVolume, XF86AudioMute, and XF86AudioMicMute used pamixer β€” a separate package that was never installed on the P16g. The volume buttons silently failed because the exec command returned non-zero and Hyprland swallowed the error.

Root cause: pamixer is a PulseAudio-compatible CLI tool. PipeWire ships wpctl natively β€” no extra dependency needed.

Diagnosis flow:

# 1. Confirm PipeWire sees speakers and volume is zero
wpctl status | grep -A5 Sinks
# β†’ Speaker [vol: 0.00] β€” hardware works, volume zeroed

# 2. Manually set volume and test
wpctl set-volume @DEFAULT_AUDIO_SINK@ 50%
pw-play /usr/share/sounds/freedesktop/stereo/bell.oga
# β†’ Sound plays β€” driver and sink are fine

# 3. Check what the keybind calls
grep 'XF86Audio' ~/.config/hypr/hyprland.conf
# β†’ pamixer β€” not installed

# 4. Verify pamixer is missing
command -v pamixer
# β†’ (nothing) β€” silent failure confirmed

Fix: Replace pamixer with wpctl in hyprland.conf:

# Before (pamixer β€” fails silently if not installed)
bindle = , XF86AudioRaiseVolume, exec, pamixer -i 5

# After (wpctl β€” ships with PipeWire, always available)
bindle = , XF86AudioRaiseVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+
Table 4. Command translation table
Action pamixer wpctl (PipeWire-native)

Volume up 5%

pamixer -i 5

wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+

Volume down 5%

pamixer -d 5

wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-

Mute toggle

pamixer -t

wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle

Mic mute toggle

pamixer --default-source -t

wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle

Get volume (%)

pamixer --get-volume

wpctl get-volume @DEFAULT_AUDIO_SINK@ | awk '{printf "%.0f", $2*100}'

Check mute state

pamixer --get-mute

wpctl get-volume @DEFAULT_AUDIO_SINK@ | grep -q MUTED

Automated fix script:

fix-hyprland-volume-keys.sh
#!/usr/bin/env bash
# fix-hyprland-volume-keys.sh β€” Replace pamixer binds with wpctl (PipeWire-native)
#
# WHY: Hyprland volume/mute keys silently fail when pamixer is not installed.
#      wpctl ships with PipeWire β€” no extra package needed.
#
# WHAT THIS DOES:
#   1. Backs up hyprland.conf
#   2. Replaces 4 pamixer keybinds with wpctl equivalents (via awk)
#   3. Shows before/after diff
#
# USAGE: bash fix-hyprland-volume-keys.sh
#        Then reload Hyprland: hyprctl reload

set -euo pipefail

CONF="${HOME}/.config/hypr/hyprland.conf"
BACKUP="${CONF}.bak.$(date +%F-%H%M%S)"

# ── Verify before ──────────────────────────────────────────────────────────

if [[ ! -f "$CONF" ]]; then
    echo "ERROR: $CONF not found" >&2
    exit 1
fi

if ! grep -q 'pamixer' "$CONF"; then
    echo "No pamixer binds found in $CONF β€” nothing to fix."
    exit 0
fi

echo "=== Before ==="
grep -n 'XF86Audio.*pamixer' "$CONF"

# ── Backup ─────────────────────────────────────────────────────────────────

cp "$CONF" "$BACKUP"
echo ""
echo "Backup: $BACKUP"

# ── Replace with awk ───────────────────────────────────────────────────────
# awk avoids sed quoting hell β€” each line matched by pattern, replaced wholesale

awk '
/XF86AudioRaiseVolume.*pamixer/ {
    print "bindle = , XF86AudioRaiseVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+ && notify-send -t 1000 \"Volume: $(wpctl get-volume @DEFAULT_AUDIO_SINK@ | awk '"'"'{printf \"%.0f%%\", $2*100}'"'"')\""
    next
}
/XF86AudioLowerVolume.*pamixer/ {
    print "bindle = , XF86AudioLowerVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%- && notify-send -t 1000 \"Volume: $(wpctl get-volume @DEFAULT_AUDIO_SINK@ | awk '"'"'{printf \"%.0f%%\", $2*100}'"'"')\""
    next
}
/XF86AudioMute.*pamixer/ && !/MicMute/ {
    print "bindl = , XF86AudioMute, exec, wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle && notify-send -t 1000 \"$(wpctl get-volume @DEFAULT_AUDIO_SINK@ | grep -q MUTED && echo Muted || echo Unmuted)\""
    next
}
/XF86AudioMicMute.*pamixer/ {
    print "bindl = , XF86AudioMicMute, exec, wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle && notify-send -t 1000 \"Mic $(wpctl get-volume @DEFAULT_AUDIO_SOURCE@ | grep -q MUTED && echo Muted || echo Unmuted)\""
    next
}
{ print }
' "$BACKUP" > "$CONF"

# ── Verify after ───────────────────────────────────────────────────────────

echo ""
echo "=== After ==="
grep -n 'XF86Audio' "$CONF" | head -4

echo ""
echo "=== Diff ==="
diff "$BACKUP" "$CONF" || true

echo ""
echo "Done. Reload Hyprland:"
echo "  hyprctl reload"
Run it
bash docs/modules/ROOT/examples/codex/linux/fix-hyprland-volume-keys.sh
hyprctl reload

Teaching point: @DEFAULT_AUDIO_SINK@ is a PipeWire alias β€” wpctl resolves it to whatever sink is marked * in wpctl status. No hardcoded sink IDs. Same pattern for @DEFAULT_AUDIO_SOURCE@ (microphone).

Resolved: 2026-06-15. Volume keys, mute, and mic mute all functional via wpctl.

Bluetooth

sudo pacman -S bluez bluez-utils blueman
sudo systemctl enable --now bluetooth

Bluetooth Verification & Pairing

Start Bluetooth Service

# Enable and start (if not already)
sudo systemctl enable --now bluetooth.service
# Verify running
systemctl is-active bluetooth.service

Pair a Device (Interactive)

bluetoothctl

Inside bluetoothctl:

# Power on the adapter
power on

# Enable scanning
scan on

# Wait for your device to appear β€” watch the [NEW] Device lines
# Look for a recognizable name, not just a MAC address

# Pair with the device
pair XX:XX:XX:XX:XX:XX

# Trust (auto-connect on future boots β€” without this you re-pair every boot)
trust XX:XX:XX:XX:XX:XX

# Connect
connect XX:XX:XX:XX:XX:XX

# Stop scanning and exit
scan off
quit

Kinesis Advantage 360 Pro β€” Paired 2026-06-15

MAC: FF:E9:28:2C:ED:FD
Name: Adv360 Pro
BT Channel: 1 (the keyboard supports 3 BT profiles β€” use the physical Bluetooth button to cycle channels)

Procedure that worked:

# 1. Enter bluetoothctl
bluetoothctl

# 2. Power on and scan
power on
scan on

# 3. Wait for "Adv360 Pro" to appear in scan output
#    Look for: [NEW] Device FF:E9:28:2C:ED:FD Adv360 Pro

# 4. If previously attempted, remove stale pairing first
remove FF:E9:28:2C:ED:FD

# 5. Put keyboard in pairing mode (hold BT button until LED blinks fast)
#    Then pair β€” wait for "Pairing successful"
pair FF:E9:28:2C:ED:FD

# 6. Trust for auto-reconnect on boot
trust FF:E9:28:2C:ED:FD

# 7. Verify β€” prompt changes to [Adv360 Pro]> and keyboard types

Gotchas encountered:

  • pair issued immediately after remove gets swallowed β€” remove is still processing. Wait for the prompt, then run pair as a separate command.

  • The keyboard shows HID Report Map characteristics during pairing β€” that’s the GATT service enumeration, not an error. Wait for Pairing successful.

  • If scan doesn’t show Adv360 Pro, the keyboard’s pairing mode timed out β€” hold the BT button again to re-enter pairing mode.

  • The MAC may change if you switch BT channels on the keyboard β€” each channel has its own address.

Verify Bluetooth Audio

After pairing Bluetooth headphones/earbuds:

# Should show Bluetooth audio sink
wpctl status
# Switch audio output to Bluetooth device
wpctl set-default <BT_SINK_ID>
# Test audio through Bluetooth
pw-play /usr/share/sounds/freedesktop/stereo/bell.oga

Bluetooth Troubleshooting

Symptom Fix

bluetoothctl shows no controller

sudo systemctl restart bluetooth.service β€” adapter may need reset

Device not discovered during scan

Put device in pairing mode (varies by device). Check rfkill list for soft block.

Paired but no audio

Check wpctl status β€” Bluetooth sink may not be default. Run wpctl set-default <ID>.

Audio stutters over Bluetooth

Switch codec: pactl list cards then pactl set-card-profile <card> a2dp-sink-sbc_xq

rfkill blocks Bluetooth

rfkill unblock bluetooth

File Manager & Tools

sudo pacman -S \
    thunar thunar-volman gvfs tumbler ffmpegthumbnailer \
    zathura zathura-pdf-mupdf imv polkit-gnome wf-recorder

Theme & Wayland

sudo pacman -S qt5-wayland qt6-wayland gtk3 gtk4 nwg-look brightnessctl
yay -S catppuccin-gtk-theme-mocha catppuccin-cursors-mocha

GPU Verification

NVIDIA RTX 5090 Status

# GPU info, driver version, VRAM, temperature
nvidia-smi
# Verify DRM modesetting (required for Wayland/Hyprland)
cat /sys/module/nvidia_drm/parameters/modeset
# Must show: Y
# Verify NVIDIA kernel modules loaded
lsmod | grep nvidia
# Check for GPU errors in dmesg
dmesg | grep -iE 'nvidia|drm' | tail -20

NVIDIA Persistence Daemon

Keeps the GPU initialized between workloads (prevents cold-start delays):

sudo systemctl enable --now nvidia-persistenced.service
# Verify
systemctl is-active nvidia-persistenced.service

Display: 3.2K Tandem OLED

The P16g has a 16" 3200x2000 Tandem OLED display. At native resolution, text is tiny without scaling.

Hyprland Monitor Configuration

In ~/.config/hypr/hyprland.conf (or the stowed equivalent):

# Check current monitor config
hyprctl monitors
Hyprland monitor config for 3.2K OLED
# Scale 1.5x for readable text at 3200x2000
monitor = eDP-1, 3200x2000@60, 0x0, 1.5

# For external displays (adjust as needed)
# monitor = DP-1, 2560x1440@144, 3200x0, 1

HiDPI Cursor and Font Scaling

# Environment variables for HiDPI (in env-gpu.conf or hyprland.conf)
env = XCURSOR_SIZE,32
env = GDK_SCALE,1.5
env = QT_AUTO_SCREEN_SCALE_FACTOR,1

Verify Display

# Resolution and refresh rate
hyprctl monitors | grep -E 'Monitor|resolution|scale'
# Verify Wayland session
echo $XDG_SESSION_TYPE
# Must show: wayland

Hyprland Validation

# Verify Hyprland is running
pgrep -a Hyprland
# List workspaces
hyprctl workspaces
# Verify key components
pgrep -a waybar
pgrep -a mako
# Test notifications
notify-send "P16g Test" "Desktop environment operational"
# Test screenshot pipeline
grim -g "$(slurp)" /tmp/test-screenshot.png && ls -lh /tmp/test-screenshot.png
# Test clipboard
echo "clipboard test" | wl-copy && wl-paste

Power Management (ThinkPad)

sudo pacman -S tlp tlp-rdw
sudo systemctl enable --now tlp

Phase 6 Checklist

Check Status

NVIDIA driver loaded (nvidia-smi)

[ ]

DRM modesetting enabled (cat /sys/module/nvidia_drm/parameters/modeset)

[ ]

NVIDIA persistence daemon running

[ ]

PipeWire running (wpctl status)

[ ]

sof-firmware installed (onboard speakers work)

[ ]

Audio output verified (pw-play)

[ ]

Bluetooth service active

[ ]

At least one BT device paired and connected

[ ]

Hyprland running with correct monitor scaling

[ ]

Wayland session confirmed (echo $XDG_SESSION_TYPE)

[ ]

Waybar and Mako running

[ ]

Notifications working (notify-send)

[ ]

Screenshots working (grim + slurp)

[ ]

Clipboard working (wl-copy + wl-paste)

[ ]

pavucontrol accessible

[ ]

Theme applied (Catppuccin Mocha)

[ ]

TLP running (tlp-stat -b)

[ ]