Set Elsewhen's here by tapping a city on the globe

A city on this machine's zone becomes homeCity, the zone's own city clears
it, and a city on another zone leaves it alone. Ported from omacom/elsewhen#5.

Co-Authored-By: Shawn Yeager <shawn@shawnyeager.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
David Heinemeier Hansson
2026-09-27 12:23:15 -07:00
co-authored by Shawn Yeager Claude Opus 5.5
parent e7218e36f2
commit aa515a73c4
5 changed files with 38 additions and 3 deletions
+2 -2
View File
@@ -302,7 +302,7 @@ The opening spin **lands on home** - the animation runs from `homeLon - 1080` to
The header reads "It's 10:28 AM here in Los Angeles." rather than a bare "here", which names your own city without spending a row on it. The zone comes from the same `date` probe the rows use - one extra `LOCAL|<zone>` line, from `timedatectl` - so it costs no additional process and follows a time-zone change on the next refresh.
The city name is the zone's last segment, and the tz database names zones after a *representative* city: someone in Boca Raton would read "here in New York". Set `homeCity` to override it.
The city name is the zone's last segment, and the tz database names zones after a *representative* city: someone in Boca Raton would read "here in New York". Tap your city on the globe to fix that: a city on this machine's zone becomes `homeCity`, and the header and home pin follow it. Tapping the zone's own city clears the choice, and a city on another zone leaves it alone, since the time beside the name is still this machine's.
## Two offsets, one line
@@ -369,7 +369,7 @@ Inline on the widget's `shell.json` entry:
| `offsetMode` | `home` for the offset from you (default), `utc` for the absolute one |
| `units` | `F` or `C`; blank (the default) follows the system's measurement units. Click any temperature to flip it |
| `globeEnabled` | `false` to remove the globe entry point (default on) |
| `homeCity` | your city for the header (blank = from the system zone) |
| `homeCity` | your city for the header (blank = from the system zone); set by tapping a city on this zone on the globe |
| `smoothMotion` | drop labels and detail while the globe moves (default true) |
## IPC
+5 -1
View File
@@ -56,6 +56,7 @@ Item {
signal offsetModeToggleRequested()
// Only real selections: -1 means "no city" here but "home" in the list.
signal citySelected(string label, string zone)
signal cityTapped(string label, string zone)
// ---- data -------------------------------------------------------------
property var land: [] // coastline rings, flat [lon,lat,...]
@@ -96,7 +97,10 @@ Item {
function pickAt(cx, cy) {
var hit = hitAt(cx, cy)
selectAt(hit)
if (hit >= 0) flyTo(allCities[hit][2], allCities[hit][3])
if (hit >= 0) {
flyTo(allCities[hit][2], allCities[hit][3])
cityTapped(allCities[hit][0], allCities[hit][1])
}
return hit
}
+13
View File
@@ -272,6 +272,18 @@ function labelForZoneId(id) {
return String(id || "").split("/").pop().replace(/_/g, " ")
}
// Tapping a city on this machine's clock makes it "here", and the zone's own
// city clears the choice. The new homeCity setting, or null to leave it.
function homeCityAfterTap(label, zoneId, homeOverride, localZone) {
var name = String(label || "").trim()
var local = String(localZone || "")
if (name === "" || local === "" || String(zoneId || "") !== local) return null
var override = String(homeOverride || "").trim()
var zoneCity = labelForZoneId(local)
if (name === (override === "" ? zoneCity : override)) return null
return name === zoneCity ? "" : name
}
function serializeZones(zones) {
var parts = []
for (var i = 0; i < zones.length; i++) {
@@ -880,6 +892,7 @@ if (typeof module !== "undefined") {
indexOfZone: indexOfZone,
indexOfZoneKey: indexOfZoneKey,
labelForZoneId: labelForZoneId,
homeCityAfterTap: homeCityAfterTap,
serializeZones: serializeZones,
addZone: addZone,
removeZoneAt: removeZoneAt,
+8
View File
@@ -118,6 +118,13 @@ Panel {
else g.showHome()
}
function claimHome(label, zone) {
var stored = Model.homeCityAfterTap(label, zone, setting("homeCity", ""), localZone)
if (stored === null) return
persistSettings({ homeCity: stored })
refreshFacts()
}
function focusFromGlobe(label, zone) {
var i = Model.indexOfZone(zones, label, zone)
if (i >= 0) focusOn(i)
@@ -876,6 +883,7 @@ Panel {
item.exitRequested.connect(function() { root.setGlobeMode(false, false) })
item.jumpDismissed.connect(function() { Qt.callLater(function() { keyCatcher.forceActiveFocus() }) })
item.citySelected.connect(function(label, zone) { root.focusFromGlobe(label, zone) })
item.cityTapped.connect(function(label, zone) { root.claimHome(label, zone) })
// A cold start can finish loading after the mode switched on.
if (root.globeMode) root.showFocusOnGlobe()
}
+10
View File
@@ -306,6 +306,16 @@ assertDeepEqual(seedProblems, [], 'elsewhen seeds four well-spread cities around
assertEqual(M.seedZones({ label: 'Nowhere', id: 'Not/AZone' }, offsets, 4).length, 1, 'elsewhen seeds only home when its offset is unknown')
assertEqual(M.pickSeedZones({ label: 'X', id: 'UTC' }, {}, 4).length, 0, 'elsewhen seeds nothing without offsets')
// ---- claiming home from the globe
const CHI = 'America/Chicago'
assertEqual(M.homeCityAfterTap('Nashville', CHI, '', CHI), 'Nashville', 'elsewhen makes a tapped city on this zone home')
assertEqual(M.homeCityAfterTap('Nashville', CHI, 'Nashville', CHI), null, 'elsewhen leaves home alone on a second tap')
assertEqual(M.homeCityAfterTap('Chicago', CHI, 'Nashville', CHI), '', 'elsewhen clears home when the zone\'s own city is tapped')
assertEqual(M.homeCityAfterTap('Chicago', CHI, '', CHI), null, 'elsewhen stores nothing for the zone\'s own city when none is set')
assertEqual(M.homeCityAfterTap('London', 'Europe/London', '', CHI), null, 'elsewhen never makes a city on another zone home')
assertEqual(M.homeCityAfterTap('Nashville', CHI, '', ''), null, 'elsewhen stores nothing before the machine zone is known')
assertEqual(M.homeCityAfterTap(' Nashville ', CHI, '', CHI), 'Nashville', 'elsewhen trims a tapped name')
// ---- coordinates and weather
const tab = '#comment\nUS\t+404251-0740023\tAmerica/New_York\nIN\t+2232+08822\tAsia/Kolkata\n'
assertDeepEqual(M.zoneTabCoords(tab, 'America/New_York'), { lat: 40.7142, lon: -74.0064 }, 'elsewhen reads seconds-precision zone1970 coordinates')