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
Hyprland Stack
sudo pacman -S \
hyprland xdg-desktop-portal-hyprland \
waybar wofi mako \
hyprlock hypridle hyprpaper \
grim slurp wl-clipboard cliphist swappy
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
# 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@
# 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>
# 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/.
| Key | Action |
|---|---|
|
Decrease 5%, notify |
|
Increase 5%, notify |
# 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%
# 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.
| Key | Action |
|---|---|
|
Region select β swappy editor |
|
Fullscreen β swappy editor |
|
Region select β save to |
|
Fullscreen β save to |
# 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
| Tool | Role |
|---|---|
|
Screen capture for Wayland compositors. |
|
Interactive region selector β click and drag. Returns |
|
Annotation editor β draw, arrow, text, blur. |
|
Wayland clipboard. Pipe image data to it for paste into any app. |
Audio Troubleshooting
| Symptom | Fix |
|---|---|
|
|
RTX 5090 HDMI audio detected but no speakers |
Same β |
PipeWire not running |
|
No sound after reboot |
Check |
Crackling/distortion |
|
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%+
| Action | pamixer | wpctl (PipeWire-native) |
|---|---|---|
Volume up 5% |
|
|
Volume down 5% |
|
|
Mute toggle |
|
|
Mic mute toggle |
|
|
Get volume (%) |
|
|
Check mute state |
|
|
Automated fix script:
#!/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"
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:
-
pairissued immediately afterremovegets swallowed βremoveis still processing. Wait for the prompt, then runpairas 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 |
|---|---|
|
|
Device not discovered during scan |
Put device in pairing mode (varies by device). Check |
Paired but no audio |
Check |
Audio stutters over Bluetooth |
Switch codec: |
rfkill blocks 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
# 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 ( |
[ ] |
DRM modesetting enabled ( |
[ ] |
NVIDIA persistence daemon running |
[ ] |
PipeWire running ( |
[ ] |
|
[ ] |
Audio output verified ( |
[ ] |
Bluetooth service active |
[ ] |
At least one BT device paired and connected |
[ ] |
Hyprland running with correct monitor scaling |
[ ] |
Wayland session confirmed ( |
[ ] |
Waybar and Mako running |
[ ] |
Notifications working ( |
[ ] |
Screenshots working ( |
[ ] |
Clipboard working ( |
[ ] |
pavucontrol accessible |
[ ] |
Theme applied (Catppuccin Mocha) |
[ ] |
TLP running ( |
[ ] |