#!/usr/bin/env bash
###############################################################################
# dtime — World clock via dmenu + dunst
#
# Maintainer: Kris Yotam <krisyotam@pm.me>
# 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"