Dominic Böttger

← Back to blog

Published on January 14, 2026 by Dominic Böttger · 8 min read

I’ve always wanted my computer to automatically switch between light and dark themes based on actual daylight - not arbitrary fixed times like “8 AM to 6 PM”. Living in Heidelberg, the sun sets at 4:30 PM in winter but stays up until 9 PM in summer. A fixed schedule just doesn’t work.

In this guide, I’ll show you how to set up truly intelligent auto dark mode on Linux that:

The End Result

Once configured, your system will automatically:

Time of DayThemeScreen Temperature
After sunriseLight theme (Catppuccin Latte)6000K (neutral daylight)
After sunsetDark theme (Tokyo Night)4000K (warm, easy on eyes)

The transition happens at civil twilight - about 30 minutes before sunrise and after sunset - giving you a smooth, natural transition that matches how your eyes perceive the changing light.

Prerequisites

This guide is written for Omarchy, an opinionated Arch Linux distribution with Hyprland. You’ll need:

Step 1: Install sunwait

sunwait is a small utility that calculates sunrise and sunset times based on GPS coordinates. It supports different twilight types (civil, nautical, astronomical) and can either report times or wait until an event occurs.

yay -S sunwait

Test it works

# Check if it's currently day or night (using civil twilight)
sunwait poll civil 48.14N 11.58E
# Returns: DAY or NIGHT

# Get today's sunrise and sunset times
sunwait list civil 48.14N 11.58E
# Returns something like: 07:25, 17:19

Step 2: Create the Auto-Theme Script

Create a script at ~/.local/bin/omarchy-auto-theme (or any location in your PATH):

#!/bin/bash
#
# Auto-switches theme and screen temperature based on real sunrise/sunset
# Uses IP geolocation and sunwait for accurate sun position calculation
#

CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/omarchy"
LOCATION_CACHE="$CACHE_DIR/location"
STATE_FILE="$CACHE_DIR/auto-theme-state"

# Theme configuration - customize these!
LIGHT_THEME="Catppuccin Latte"
DARK_THEME="Tokyo Night"

# Screen temperature (night light)
DAY_TEMP=6000
NIGHT_TEMP=4000

mkdir -p "$CACHE_DIR"

# Get location (cached for 24 hours)
get_location() {
    local cache_age=86400  # 24 hours

    if [[ -f "$LOCATION_CACHE" ]]; then
        local file_age=$(($(date +%s) - $(stat -c %Y "$LOCATION_CACHE")))
        if [[ $file_age -lt $cache_age ]]; then
            cat "$LOCATION_CACHE"
            return 0
        fi
    fi

    # Fetch from ipinfo.io
    local loc
    loc=$(curl -s --connect-timeout 5 ipinfo.io/loc 2>/dev/null)

    if [[ -n "$loc" && "$loc" =~ ^-?[0-9]+\.[0-9]+,-?[0-9]+\.[0-9]+$ ]]; then
        echo "$loc" > "$LOCATION_CACHE"
        echo "$loc"
        return 0
    fi

    # Fallback: use cached even if old
    if [[ -f "$LOCATION_CACHE" ]]; then
        cat "$LOCATION_CACHE"
        return 0
    fi

    return 1
}

# Convert location to sunwait format
format_location() {
    local loc="$1"
    local lat lon lat_dir lon_dir

    lat=$(echo "$loc" | cut -d',' -f1)
    lon=$(echo "$loc" | cut -d',' -f2)

    # Determine direction
    if (( $(echo "$lat < 0" | bc -l) )); then
        lat_dir="S"
        lat=$(echo "$lat" | tr -d '-')
    else
        lat_dir="N"
    fi

    if (( $(echo "$lon < 0" | bc -l) )); then
        lon_dir="W"
        lon=$(echo "$lon" | tr -d '-')
    else
        lon_dir="E"
    fi

    echo "${lat}${lat_dir} ${lon}${lon_dir}"
}

# Check if it's day or night using sunwait
is_daytime() {
    local location="$1"
    local result

    # sunwait poll returns: DAY (exit 1) or NIGHT (exit 3)
    # Using civil twilight (default) for smooth transitions
    result=$(sunwait poll civil $location 2>/dev/null)

    [[ "$result" == "DAY" ]]
}

# Set screen temperature via hyprsunset
set_temperature() {
    local temp="$1"

    # Ensure hyprsunset is running
    if ! pgrep -x hyprsunset > /dev/null; then
        setsid uwsm-app -- hyprsunset &
        sleep 1
    fi

    hyprctl hyprsunset temperature "$temp" 2>/dev/null
}

# Main logic
main() {
    local location loc_formatted current_state new_state

    location=$(get_location)
    if [[ -z "$location" ]]; then
        echo "Failed to get location" >&2
        exit 1
    fi

    loc_formatted=$(format_location "$location")

    # Determine day/night state
    if is_daytime "$loc_formatted"; then
        new_state="day"
    else
        new_state="night"
    fi

    # Read previous state
    current_state=""
    [[ -f "$STATE_FILE" ]] && current_state=$(cat "$STATE_FILE")

    # Only switch if state changed (or forced)
    if [[ "$1" == "--force" ]] || [[ "$current_state" != "$new_state" ]]; then
        if [[ "$new_state" == "day" ]]; then
            echo "Switching to day mode (light theme)"
            # Replace with your theme switching command
            omarchy-theme-set "$LIGHT_THEME"
            set_temperature "$DAY_TEMP"
        else
            echo "Switching to night mode (dark theme)"
            omarchy-theme-set "$DARK_THEME"
            set_temperature "$NIGHT_TEMP"
        fi

        echo "$new_state" > "$STATE_FILE"
    else
        echo "Already in $new_state mode, no change needed"
    fi
}

# Handle arguments
case "$1" in
    --status)
        location=$(get_location)
        loc_formatted=$(format_location "$location")
        echo "Location: $location"
        echo "Formatted: $loc_formatted"
        echo "Sun state: $(sunwait poll civil $loc_formatted 2>/dev/null)"
        echo "Current theme state: $(cat "$STATE_FILE" 2>/dev/null || echo 'unknown')"
        ;;
    --force|"")
        main "$1"
        ;;
    --help)
        echo "Usage: $(basename $0) [--status|--force|--help]"
        echo "  --status  Show current state and location info"
        echo "  --force   Force theme switch even if state unchanged"
        echo "  --help    Show this help"
        ;;
    *)
        echo "Unknown option: $1"
        exit 1
        ;;
esac

Make it executable:

chmod +x ~/.local/bin/omarchy-auto-theme

Step 3: Set Up the Systemd Timer

We’ll use a systemd user timer to run the script every 5 minutes. This approach is lightweight - the script exits immediately if no change is needed.

Create the service file at ~/.config/systemd/user/omarchy-auto-theme.service:

[Unit]
Description=Auto switch theme based on sunrise/sunset
After=graphical-session.target

[Service]
Type=oneshot
ExecStart=%h/.local/bin/omarchy-auto-theme
Environment=DISPLAY=:0
Environment=WAYLAND_DISPLAY=wayland-1

[Install]
WantedBy=default.target

Create the timer file at ~/.config/systemd/user/omarchy-auto-theme.timer:

[Unit]
Description=Check and switch theme every 5 minutes based on sunrise/sunset

[Timer]
OnStartupSec=10sec
OnUnitActiveSec=5min
Persistent=true

[Install]
WantedBy=timers.target

Note: We use OnStartupSec instead of OnBootSec because this is a user timer - it triggers relative to when your user session starts, not system boot.

Enable and start the timer:

systemctl --user daemon-reload
systemctl --user enable --now omarchy-auto-theme.timer

Verify it’s running:

systemctl --user status omarchy-auto-theme.timer

Handling Resume from Suspend

The systemd timer works great, but there’s a catch: if your laptop sleeps overnight and wakes up after sunrise, the timer won’t trigger immediately. User-level timers don’t reliably fire right after resume.

The fix is to hook into hypridle’s after_sleep_cmd. Edit ~/.config/hypr/hypridle.conf and update the after_sleep_cmd line:

general {
    # ... other settings ...
    after_sleep_cmd = hyprctl dispatch dpms on && omarchy-auto-theme
}

Now the theme check runs immediately when your system wakes from suspend.

Step 4: Test Everything

Check the current status:

omarchy-auto-theme --status

You should see output like:

Location: 48.1374,11.5755
Formatted: 48.1374N 11.5755E
Sun state: NIGHT
Current theme state: night

Force a theme switch to verify it works:

omarchy-auto-theme --force

Understanding Twilight Types

The script uses “civil twilight” by default, but you can adjust this for different behavior:

Twilight TypeSun PositionEffect
daylightAt horizonExact sunrise/sunset moment
civil6° belowEnough light to work outdoors without artificial light
nautical12° belowHorizon still visible at sea
astronomical18° belowSky completely dark

Civil twilight is recommended because it matches when humans naturally perceive the transition between day and night. The switch happens about 30 minutes before actual sunrise and after actual sunset.

To change it, modify the is_daytime() function:

result=$(sunwait poll nautical $location 2>/dev/null)

Customization Options

Fixed Location (No IP Geolocation)

If you prefer not to use IP geolocation, create a permanent location cache:

echo "48.1374,11.5755" > ~/.cache/omarchy/location
chmod 444 ~/.cache/omarchy/location  # Make read-only

Different Check Interval

Edit the timer file and change OnUnitActiveSec:

OnUnitActiveSec=10min  # Check every 10 minutes instead

Then reload: systemctl --user daemon-reload

Screen Temperature Only (No Theme Switch)

If you only want the night light effect without theme switching, comment out the theme-set lines in the script and keep only the set_temperature calls.

Troubleshooting

Timer not running:

systemctl --user status omarchy-auto-theme.timer
journalctl --user -u omarchy-auto-theme.service -n 20

Wrong location:

curl -s ipinfo.io/loc  # Check detected location
# Override if wrong:
echo "YOUR_LAT,YOUR_LON" > ~/.cache/omarchy/location

Screen temperature not changing:

pgrep hyprsunset  # Check if running
hyprctl hyprsunset temperature 4000  # Manual test

Why This Approach?

  1. True location awareness: The setup auto-detects your location via IP and works when you travel - no manual coordinate updates needed.

  2. Civil twilight: The transition happens when it feels like day/night is changing, not at the exact astronomical moment.

  3. Unified control: Both the Omarchy theme AND screen temperature change together, creating a cohesive experience.

  4. Lightweight: The 5-minute polling approach uses negligible resources - the script exits in milliseconds when no change is needed.

Conclusion

With this setup, your Linux desktop now intelligently adapts to the natural light cycle. No more manually switching themes or dealing with dark mode at 4 PM in winter while it’s still configured for summer schedules.

The system is completely automatic - it detects your location, calculates the real sunrise and sunset times, and smoothly transitions your entire desktop experience. Your eyes will thank you.

Written by Dominic Böttger

← Back to blog
  • Using Figma with Local Fonts on Omarchy Linux

    Using Figma with Local Fonts on Omarchy Linux

    How to set up Figma on Omarchy Linux with full local font support using figma-agent-linux and a user-agent workaround. Complete guide with desktop integration and systemd socket activation.

  • Syncing SharePoint and OneDrive on Arch Linux with One Click

    Syncing SharePoint and OneDrive on Arch Linux with One Click

    Set up Microsoft SharePoint and OneDrive sync on Arch Linux using abraunegg/onedrive. Includes a protocol handler script that makes the "Sync" button in SharePoint work natively, with automatic drive detection and systemd background sync.

  • From Sequential to Parallel: Adding Agent Teams to Spec Kit

    From Sequential to Parallel: Adding Agent Teams to Spec Kit

    How we extended Spec Kit with a new /speckit.team-implement command that auto-detects parallel work streams from your task list and spawns specialized AI agent teams to implement features simultaneously -- complete source code, algorithm walkthrough, and usage guide.

  • Building a Private Claude Code Plugin Marketplace for Your Team

    Building a Private Claude Code Plugin Marketplace for Your Team

    Learn how to structure a private Claude Code plugin marketplace for your team, including repository layout, naming conventions, and how to register local or remote marketplaces.

  • Spec Kit + Ralph Loop: Solving AI Context Exhaustion in Large Features

    Spec Kit + Ralph Loop: Solving AI Context Exhaustion in Large Features

    How we combined Spec Kit's structured planning with Ralph Wiggum's fresh context methodology to build an AI-powered development loop that can implement features of any size without context pollution.

  • Mistral Releases Vibe CLI and Devstral 2: Open-Source AI Coding Goes Next Level

    Mistral Releases Vibe CLI and Devstral 2: Open-Source AI Coding Goes Next Level

    Mistral AI launches Vibe CLI and Devstral 2, bringing powerful open-source AI coding assistance to your terminal. Learn how to install and get started with these game-changing tools.