Header 1

ioan

ioan biticu's website

Arch: CachyOS + Niri + Noctalia + Steam + OBS + Code

I've used Nobara for almost 6 months, and now I've transitioned to Niri. So far I can say that I don't miss it. Below is my guide on how to set it up including the apps I've used on Nobara.

Desktop 1 Desktop 2

First, install cachyos using the settings here.

Install yay and flatpak:

# Install required dependencies
sudo pacman -S --needed base-devel git

# Clone yay repository
cd ~
git clone https://aur.archlinux.org/yay.git

# Build and install yay
cd yay
makepkg -si

# Clean up
cd ..
rm -rf yay

sudo pacman -S flatpak

Install requirements:

  • sudo pacman -S bluez bluez-utils && sudo systemctl start bluetooth.service && sudo systemctl enable bluetooth.service
  • sudo pacman -S xdg-desktop-portal-gnome && gsettings set org.gnome.desktop.interface color-scheme 'prefer-dark' && gsettings set org.gnome.desktop.interface gtk-theme 'Adwaita-dark'
  • sudo pacman -S steam
  • sudo pacman -S discord
  • yay -S obsidian google-chrome
  • sudo pacman -S obs-studio
  • sudo pacman -S vlc
  • yay -S fsearch
  • yay -S visual-studio-code-bin
  • sudo pacman -S podman podman-compose
  • sudo pacman -S gnome-keyring ( & set it up in vscode: Configure Runtime Arguments and add "password-store":"gnome-libsecret" )
  • sudo pacman -S dbeaver
  • flatpak install flathub io.missioncenter.MissionCenter
  • sudo pacman -S filelight
  • yay -S github-desktop-bin
  • sudo pacman -S grim slurp
  • sudo pacman -S wl-clipboard
  • sudo pacman -S swappy
  • curl -fsSL https://tailscale.com/install.sh | sh

Noctalia changes:

Begin by reading the FAQ For Noctalia: https://docs.noctalia.dev/getting-started/faq/

  1. Add your wallpaper and profile picture.
  2. Add dock
  3. Set location
  4. Set opacity
  5. Change the size of the dock (in accordance with the value you put for struts in niri)
  6. Fix the icons by adding QT_QPA_PLATFORMTHEME=gtk3 to /etc/environment (as root)

Niri changes

Note: before you quit niri and log back in, run niri validate to make sure the config is still working, otherwise niri is going to start with a blank config.

Change your keyboard layout:

input {
    keyboard {
        xkb {
            layout "gb"
        }
        numlock
    }
}

Add your displays:

output "DP-1" {
    mode "3440x1440@165.001"
    position x=0 y=0
}

output "HDMI-A-1" {
    mode "1920x1080"
    position x=720 y=1440
}

You can find their info with the command niri msg outputs

Add key bindings


    // Core Noctalia binds
    Mod+Space { spawn "qs" "-c" "noctalia-shell" "ipc" "call" "launcher" "toggle"; }
    Mod+S { spawn "qs" "-c" "noctalia-shell" "ipc" "call" "controlCenter" "toggle"; }
    Mod+Comma { spawn "qs" "-c" "noctalia-shell" "ipc" "call" "settings" "toggle"; }
    
    // Audio controls
    XF86AudioRaiseVolume { spawn "qs" "-c" "noctalia-shell" "ipc" "call" "volume" "increase"; }
    XF86AudioLowerVolume { spawn "qs" "-c" "noctalia-shell" "ipc" "call" "volume" "decrease"; }
    XF86AudioMute { spawn "qs" "-c" "noctalia-shell" "ipc" "call" "volume" "muteOutput"; }
    
    // Brightness controls
    XF86MonBrightnessUp { spawn "qs" "-c" "noctalia-shell" "ipc" "call" "brightness" "increase"; }
    XF86MonBrightnessDown { spawn "qs" "-c" "noctalia-shell" "ipc" "call" "brightness" "decrease"; }

    // Screenshot region to clipboard
    Mod+Shift+S { spawn "sh" "-c" "grim -g \"$(slurp)\" - | wl-copy"; }
    
    // Screenshot region with editing (Swappy)
    Mod+Print { spawn "sh" "-c" "grim -g \"$(slurp)\" - | swappy -f -"; }

Make sure you remove previosu entries for Mod+Space, XF86AudioRaiseVolume, XF86AudioLowerVolume, XF86AudioMute, Mod+Print

First, make room for the dock at the bottom of the screen

layout {
    gaps 7
    
    struts {
        left 0
        right 0
        top 0
        bottom 40
    }
}

Startup script with windows in the right place

Note: to find chrome app, use: grep -il bitwarden ~/.local/share/applications/chrome-*.desktop

In my case I want the following:

First screen (large): Workspace 1:

  • Todoist (50% width) /home/ioan/.local/share/applications/chrome-knaiokfnmjjldlfhlioejgcompgenfhb-Default.desktop
  • Chrome ( 75% width)
  • Claude ( 75% width) /home/ioan/.local/share/applications/chrome-fmpnliohjhemenmnlpbfagaolkdacoja-Default.desktop

Workspace 2:

  • Vscode main projects (75% width) opens /home/ioan/Documents/repos/ps2mono with code)
  • Terminal allacrity (50% default)

Workspace 3:

  • Vscode secondary projects (75% width) (has code open /home/ioan/Documents/repos/mysql-downloader/)

Workspace 4:

  • SSH sessions running in vscode (flatcar project) (opens /home/ioan/Documents/repos/flatcar with code) (75% width)

Workspace 5:

  • Gaming + my app in another browser session chrome http://ps2immersion.com/ (game off by default) (35% width)

Secondary screen (small): Workspace 1:

  • Bitwarden (chrome app) (35% width) /home/ioan/.local/share/applications/chrome-fflifmfnonladkgkdehllhbcghakccgh-Default.desktop
  • Chrome (75% width)
  • Terminal allacrity (default) (50% width)

Workspace 2:

  • Discord (60% width)
  • Obsidian (60% width) /home/ioan/Documents/Obsidian/SaMearga

Find the script below.

Discord servers:

The script for setting up the workspaces:

#!/bin/bash

# Workspace Setup Script for Niri - v2.2
# Automates opening applications across multiple workspaces and monitors

echo "===========================================
  Niri Workspace Layout Setup Script v2.2
==========================================="
echo ""

# ==================== CONFIGURATION ====================

# Debug mode (set to true to see what commands are being run)
DEBUG=true

# Monitor/Output names (from niri msg outputs)
PRIMARY_MONITOR="DP-1"
SECONDARY_MONITOR="HDMI-A-1"

# Paths to Chrome Apps
TODOIST_APP="/home/ioan/.local/share/applications/chrome-knaiokfnmjjldlfhlioejgcompgenfhb-Default.desktop"
CLAUDE_APP="/home/ioan/.local/share/applications/chrome-fmpnliohjhemenmnlpbfagaolkdacoja-Default.desktop"
BITWARDEN_APP="/home/ioan/.local/share/applications/chrome-fflifmfnonladkgkdehllhbcghakccgh-Default.desktop"

# Project Paths
PS2MONO_PATH="/home/ioan/Documents/repos/ps2mono"
MYSQL_DOWNLOADER_PATH="/home/ioan/Documents/repos/mysql-downloader"
FLATCAR_PATH="/home/ioan/Documents/repos/flatcar"
OBSIDIAN_VAULT="/home/ioan/Documents/Obsidian/SaMearga"

# URLs
PS2_IMMERSION_URL="http://ps2immersion.com/"

# Application commands
TERMINAL_CMD="alacritty"
CHROME_CMD="google-chrome-stable"
DISCORD_CMD="discord"  # or "Discord" depending on your installation
OBSIDIAN_CMD="obsidian"

# Wait times (in seconds)
APP_LAUNCH_WAIT=2.0
VSCODE_LAUNCH_WAIT=3.0
WORKSPACE_SWITCH_WAIT=2.5
WINDOW_OPERATION_WAIT=0.3

# ======================================================

# Color output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color

# Logging functions
log_info() {
    echo -e "${GREEN}[INFO]${NC} $1"
}

log_warn() {
    echo -e "${YELLOW}[WARN]${NC} $1"
}

log_error() {
    echo -e "${RED}[ERROR]${NC} $1"
}

log_debug() {
    if [ "$DEBUG" = true ]; then
        echo -e "${BLUE}[DEBUG]${NC} $1"
    fi
}

# Execute niri command with optional debug output
niri_cmd() {
    log_debug "Executing: niri msg $*"
    niri msg "$@"
}

# Check required tools
check_dependencies() {
    local missing_deps=()
    
    # Check for niri
    if ! command -v niri &> /dev/null; then
        log_error "Niri window manager not found"
        missing_deps+=("niri")
    fi
    
    # Check for niri msg (IPC command)
    if ! niri msg version &> /dev/null; then
        log_error "Cannot communicate with Niri (is it running?)"
        exit 1
    fi
    
    for cmd in alacritty; do
        if ! command -v "$cmd" &> /dev/null; then
            missing_deps+=("$cmd")
        fi
    done
    
    if [ ${#missing_deps[@]} -gt 0 ]; then
        log_error "Missing required dependencies: ${missing_deps[*]}"
        exit 1
    fi
    
    log_info "Niri detected and running ✓"
}

# Get monitor/output information from Niri
get_monitor_info() {
    log_info "Detecting outputs from Niri..."
    
    # Get output information from niri
    local outputs=$(niri_cmd outputs 2>&1)
    
    # Verify the configured outputs exist
    if ! echo "$outputs" | grep -q "$PRIMARY_MONITOR"; then
        log_warn "Primary monitor '$PRIMARY_MONITOR' not found in niri outputs"
        log_info "Available outputs:"
        echo "$outputs" | head -20
    else
        log_info "Primary Output: $PRIMARY_MONITOR ✓"
    fi
    
    if [ -n "$SECONDARY_MONITOR" ]; then
        if ! echo "$outputs" | grep -q "$SECONDARY_MONITOR"; then
            log_warn "Secondary monitor '$SECONDARY_MONITOR' not found in niri outputs"
            log_info "Disabling secondary monitor setup"
            SECONDARY_MONITOR=""
        else
            log_info "Secondary Output: $SECONDARY_MONITOR ✓"
        fi
    else
        log_info "No secondary monitor configured"
    fi
}

# Function to launch application
launch_app() {
    local app_command="$1"
    local app_name="$2"
    
    log_info "Launching $app_name..."
    log_debug "Command: $app_command"
    eval "$app_command" &>/dev/null &
}

# Function to wait for a new window to appear
wait_for_window() {
    local app_name="$1"
    local max_wait="${2:-5}"
    local wait_time=0
    local initial_count=$(niri msg windows 2>/dev/null | wc -l)
    
    log_debug "Waiting for $app_name window to appear (max ${max_wait}s)..."
    
    while [ $wait_time -lt $((max_wait * 2)) ]; do
        sleep 0.5
        wait_time=$((wait_time + 1))
        local current_count=$(niri msg windows 2>/dev/null | wc -l)
        
        if [ $current_count -gt $initial_count ]; then
            log_debug "Window appeared after ${wait_time}x0.5s"
            # Add extra delay to ensure window is fully rendered and placed
            log_debug "Waiting 0.5s for window to fully render..."
            sleep 0.5
            return 0
        fi
    done
    
    log_debug "Window for $app_name didn't appear within ${max_wait}s, but continuing (app may reuse existing window)..."
    # Always return success - some apps reuse windows
    return 0
}

# Function to switch to workspace using Niri
switch_to_workspace() {
    local workspace="$1"
    local output="${2:-$PRIMARY_MONITOR}"
    
    log_info "Switching to workspace $((workspace + 1)) on $output"
    niri msg action focus-workspace "$((workspace + 1))"
    sleep "$WINDOW_OPERATION_WAIT"
}

# Function to set window width using Niri
set_window_width() {
    local width_percent="$1"
    
    log_info "Setting focused window width to ${width_percent}%"
    niri msg action set-column-width "${width_percent}%"
    sleep "$WINDOW_OPERATION_WAIT"
}

# Function to move focused window to output
move_window_to_output() {
    local output="$1"
    
    if [ -n "$output" ]; then
        log_info "Moving focused window to output: $output"
        niri msg action move-column-to-output "$output"
        sleep "$WINDOW_OPERATION_WAIT"
    fi
}

# Function to move focused window to workspace
move_window_to_workspace() {
    local workspace="$1"
    
    log_info "Moving focused window to workspace $((workspace + 1))"
    niri msg action move-column-to-workspace "$((workspace + 1))"
    sleep "$WINDOW_OPERATION_WAIT"
}

# Main setup function for Niri
setup_workspaces() {
    log_info "Starting workspace setup for Niri..."
    
    # Get monitor information
    get_monitor_info
    
    # Focus primary monitor and go to workspace 1
    log_info "Setting up on primary monitor..."
    niri_cmd action focus-monitor "$PRIMARY_MONITOR"
    log_debug "Sleeping 1.0s..."
    sleep 1.0
    
    # Assume user is already on workspace 1 or will manually switch there
    log_info "Starting from current workspace (should be workspace 1)"
    
    # === PRIMARY OUTPUT - WORKSPACE 1 ===
    log_info "=== Workspace 1: Launching apps ==="
    
    launch_app "gtk-launch $(basename $TODOIST_APP)" "Todoist"
    wait_for_window "Todoist" 5
    niri_cmd action set-column-width "50%"
    sleep 1.0
    
    launch_app "$CHROME_CMD" "Chrome"
    wait_for_window "Chrome" 5
    niri_cmd action set-column-width "75%"
    sleep 1.0
    
    launch_app "gtk-launch $(basename $CLAUDE_APP)" "Claude"
    wait_for_window "Claude" 5
    niri_cmd action set-column-width "75%"
    sleep 1.0
    
    # === PRIMARY OUTPUT - WORKSPACE 2 ===
    log_info "=== Workspace 2: Switching and launching apps ==="
    niri_cmd action focus-workspace 2
    log_debug "Sleeping ${WORKSPACE_SWITCH_WAIT}s for workspace switch..."
    sleep "$WORKSPACE_SWITCH_WAIT"
    
    launch_app "code $PS2MONO_PATH" "VSCode ps2mono"
    wait_for_window "VSCode" 8
    niri_cmd action set-column-width "75%"
    sleep 1.0
    
    launch_app "$TERMINAL_CMD" "Alacritty"
    wait_for_window "Alacritty" 5
    niri_cmd action set-column-width "50%"
    sleep 1.0
    
    # === PRIMARY OUTPUT - WORKSPACE 3 ===
    log_info "=== Workspace 3: Switching and launching apps ==="
    niri_cmd action focus-workspace 3
    log_debug "Sleeping ${WORKSPACE_SWITCH_WAIT}s for workspace switch..."
    sleep "$WORKSPACE_SWITCH_WAIT"
    
    launch_app "code $MYSQL_DOWNLOADER_PATH" "VSCode mysql-downloader"
    wait_for_window "VSCode" 8
    niri_cmd action set-column-width "75%"
    sleep 1.0
    
    # === PRIMARY OUTPUT - WORKSPACE 4 ===
    log_info "=== Workspace 4: Switching and launching apps ==="
    niri_cmd action focus-workspace 4
    log_debug "Sleeping ${WORKSPACE_SWITCH_WAIT}s for workspace switch..."
    sleep "$WORKSPACE_SWITCH_WAIT"
    
    launch_app "code $FLATCAR_PATH" "VSCode flatcar"
    wait_for_window "VSCode" 8
    niri_cmd action set-column-width "75%"
    sleep 1.0
    
    # === PRIMARY OUTPUT - WORKSPACE 5 ===
    log_info "=== Workspace 5: Switching and launching apps ==="
    niri_cmd action focus-workspace 5
    log_debug "Sleeping ${WORKSPACE_SWITCH_WAIT}s for workspace switch..."
    sleep "$WORKSPACE_SWITCH_WAIT"
    
    launch_app "$CHROME_CMD --new-window http://ps2immersion.com/" "PS2 Immersion"
    wait_for_window "Chrome" 5
    niri_cmd action set-column-width "35%"
    sleep 1.0
    
    # === SECONDARY OUTPUT - WORKSPACE 1 ===
    if [ -n "$SECONDARY_MONITOR" ]; then
        log_info "=== Secondary Monitor: Workspace 1 ==="
        
        niri_cmd action focus-monitor "$SECONDARY_MONITOR"
        log_debug "Sleeping 1.0s..."
        sleep 1.0
        
        log_info "Starting from current workspace on secondary monitor (should be workspace 1)"
        
        launch_app "gtk-launch $(basename $BITWARDEN_APP)" "Bitwarden"
        wait_for_window "Bitwarden" 5
        niri_cmd action set-column-width "35%"
        sleep 1.0
        
        launch_app "$CHROME_CMD --new-window" "Chrome"
        wait_for_window "Chrome" 5
        niri_cmd action set-column-width "75%"
        sleep 1.0
        
        launch_app "$TERMINAL_CMD" "Alacritty"
        wait_for_window "Alacritty" 5
        niri_cmd action set-column-width "50%"
        sleep 1.0
        
        # === SECONDARY OUTPUT - WORKSPACE 2 ===
        log_info "=== Secondary Monitor: Workspace 2 ==="
        niri_cmd action focus-workspace 2
        log_debug "Sleeping ${WORKSPACE_SWITCH_WAIT}s for workspace switch..."
        sleep "$WORKSPACE_SWITCH_WAIT"
        
        launch_app "$DISCORD_CMD" "Discord"
        wait_for_window "Discord" 5
        niri_cmd action set-column-width "60%"
        sleep 1.0
        
        launch_app "$OBSIDIAN_CMD $OBSIDIAN_VAULT" "Obsidian"
        wait_for_window "Obsidian" 5
        niri_cmd action set-column-width "60%"
        sleep 1.0
    fi
    
    # Return to workspace 1 on primary output
    log_info "Returning to primary monitor..."
    niri_cmd action focus-monitor "$PRIMARY_MONITOR"
    log_debug "Sleeping 1.0s..."
    sleep 1.0
    
    log_info "Workspace setup complete!"
    log_info "Note: If apps are on wrong workspaces, try increasing wait times in the config"
}

# Main execution
main() {
    check_dependencies
    setup_workspaces
    
    echo ""
    log_info "All done! Your Niri workspaces are ready."
}

main "$@"