#!/usr/bin/env bash ############################################################################### # dtime — World clock via dmenu + dunst # # Maintainer: Kris Yotam # License: MIT # Created: 2026-02-15 # # Select a city/region from presets or type any city name. # Displays the current time there via dunst notification. ############################################################################### # Preset cities → IANA timezone declare -A PRESETS=( ["New York"]="America/New_York" ["Los Angeles"]="America/Los_Angeles" ["Chicago"]="America/Chicago" ["London"]="Europe/London" ["Paris"]="Europe/Paris" ["Berlin"]="Europe/Berlin" ["Tokyo"]="Asia/Tokyo" ["Seoul"]="Asia/Seoul" ["Shanghai"]="Asia/Shanghai" ["Hong Kong"]="Asia/Hong_Kong" ["Dubai"]="Asia/Dubai" ["Mumbai"]="Asia/Kolkata" ["Sydney"]="Australia/Sydney" ["Auckland"]="Pacific/Auckland" ["Moscow"]="Europe/Moscow" ["Istanbul"]="Europe/Istanbul" ["Cairo"]="Africa/Cairo" ["São Paulo"]="America/Sao_Paulo" ["Mexico City"]="America/Mexico_City" ["Toronto"]="America/Toronto" ["Singapore"]="Asia/Singapore" ["Bangkok"]="Asia/Bangkok" ["Jakarta"]="Asia/Jakarta" ["Nairobi"]="Africa/Nairobi" ["Lagos"]="Africa/Lagos" ["Reykjavik"]="Atlantic/Reykjavik" ["Honolulu"]="Pacific/Honolulu" ["Anchorage"]="America/Anchorage" ["Denver"]="America/Denver" ["UTC"]="UTC" ) # Display order ORDERED=( "New York" "Los Angeles" "Chicago" "Toronto" "Mexico City" "São Paulo" "London" "Paris" "Berlin" "Moscow" "Istanbul" "Cairo" "Nairobi" "Lagos" "Dubai" "Mumbai" "Bangkok" "Jakarta" "Singapore" "Shanghai" "Hong Kong" "Seoul" "Tokyo" "Sydney" "Auckland" "Reykjavik" "Honolulu" "Anchorage" "Denver" "UTC" ) ############################################################################### # resolve a free-form city name to a timezone ############################################################################### resolve_tz() { local query="$1" # Check presets first (case-insensitive) for city in "${!PRESETS[@]}"; do if [[ "${city,,}" == "${query,,}" ]]; then echo "${PRESETS[$city]}" return 0 fi done # Search IANA timezone database for a match local match match=$(timedatectl list-timezones | grep -i "${query// /_}" | head -1) if [ -n "$match" ]; then echo "$match" return 0 fi # Try partial match on city component (after /) match=$(timedatectl list-timezones | grep -i "/${query// /_}" | head -1) if [ -n "$match" ]; then echo "$match" return 0 fi # Try fuzzy: match anywhere in the tz string match=$(timedatectl list-timezones | grep -i "${query// /.*}" | head -1) if [ -n "$match" ]; then echo "$match" return 0 fi return 1 } ############################################################################### # format and notify ############################################################################### show_time() { local city="$1" local tz="$2" local time_str offset_str time_str=$(TZ="$tz" date '+%I:%M %p') offset_str=$(TZ="$tz" date '+%Z (UTC%:z)') local day_str day_str=$(TZ="$tz" date '+%A, %B %d') local local_time local_time=$(date '+%I:%M %p') notify-send -t 8000 "󰥔 $city" "$time_str\n$day_str\n$offset_str\n\nLocal: $local_time" } ############################################################################### # main ############################################################################### # Build menu MENU="" for city in "${ORDERED[@]}"; do tz="${PRESETS[$city]}" t=$(TZ="$tz" date '+%H:%M') MENU+="󰥔 $city ($t)"$'\n' done CHOICE=$(printf '%s' "$MENU" | sed '/^$/d' | dmenu -i -l 20 -p "󰥔 World Clock") [ -z "$CHOICE" ] && exit 0 # Extract city name (strip icon and time) CITY=$(echo "$CHOICE" | sed 's/^[^ ]* *//; s/ *([0-9:]*) *$//') # Try preset first TZ_FOUND="${PRESETS[$CITY]}" if [ -z "$TZ_FOUND" ]; then # User typed a custom city — resolve it TZ_FOUND=$(resolve_tz "$CITY") if [ -z "$TZ_FOUND" ]; then notify-send -t 5000 "󰥔 dtime" "Could not find timezone for: $CITY" exit 1 fi fi show_time "$CITY" "$TZ_FOUND"