AvengeMedia / AvengeMedia/DankMaterialShell
night mode: location automation never starts, because the coordinates are only sent to the daemon after night mode is enabled
- Dominant language
- QML
- Stars
- 8.1k
- Forks
- 515
- Avg merge
- 1d 8h
- Merged PRs (30d)
- 102
Description
# Night mode: `location` automation never computes a schedule — sunrise/sunset stay at the zero time
**Package:** `dms-shell 1.5.3-1` (Arch/CachyOS), quickshell 0.3.1
**Compositor:** niri, Wayland
**Severity:** night mode never engages when `nightModeAutoMode = location`. Silent — no log line, no error.
## What happens
With coordinates configured and location automation enabled, the whole solar schedule
comes back as the Go zero value:
```
$ dms ipc call night getSchedule
Mode: location
Period: day
Dawn: 0001-01-01T00:00:00Z
Sunrise: 0001-01-01T00:00:00Z
Sunset: 0001-01-01T00:00:00Z
Night: 0001-01-01T00:00:00Z
Next transition: 2026-08-27T00:00:00+02:00
Sun position: 1.00°
```
`Period` is therefore `day` forever and `Next transition` falls back to local midnight,
so the screen never warms at sunset. That was the user-visible symptom that led here:
"night mode did not come on at sunset".
## The settings are present
`~/.local/state/DankMaterialShell/session.json`:
```json
"nightModeAutoEnabled": true,
"nightModeAutoMode": "location",
"latitude": 45.6946319,
"longitude": 13.7837527,
"nightModeUseIPLocation": false,
"nightModeLocationProvider": ""
```
## The sun position is not a wrong computation — it is no computation
Measured at 14:04 UTC on 2026-08-26, with a standard low-precision solar position
algorithm (Meeus/NOAA approximation):
| coordinates used | solar elevation |
|---|---|
| the configured 45.6946 N, 13.7838 E | **+38.21°** |
| (0, 0), i.e. lat/lon lost | +57.85° |
| what DMS reports | **+1.00°** |
+1.00° matches neither the real location nor a zeroed one, so this is not "the
coordinates did not reach the solver" — the solver's output is a placeholder that
was never overwritten. The four zero-valued timestamps say the same thing.
## The coordinates are sufficient: another consumer on the same machine gets it right
A local tool on this same desktop, reading the same latitude/longitude, computes the day
with ordinary low-precision solar formulas:
posizione 45.69, 13.78
alba 06:17:54 tramonto 19:55:12
crepuscoli 05:46:34 / 20:26:25
periodo giorno
Cross-checked against a second, independent implementation (Meeus/NOAA approximation,
bisection on elevation = -0.833 deg):
| event | independent | the local tool | difference |
|---|---|---|---|
| sunrise | 06:18:24 | 06:17:54 | 30 s |
| sunset | 19:54:06 | 19:55:12 | 66 s |
Two implementations agree to within a minute — the residual is the expected precision of
these formulas. So this is not a bad-coordinates problem and not an unsolvable-input
problem: the same numbers, on the same machine, at the same moment, produce a correct
schedule for anything that actually runs the solver.
## Possibly related: the key is not reachable over IPC
```
$ dms ipc call settings set nightModeAutoMode time
SETTINGS_INVALID_KEY
```
The key exists in `session.json` and is honoured by the QML side, but the settings
IPC does not know it, which suggests the Go side and the QML side are reading two
different stores. `settings.json` (the config file) carries `nightModeEnabled` and
`useAutoLocation` but no coordinates at all.
## Root cause: the location is only sent to the daemon *after* night mode is enabled
Every path in `quickshell/Services/DisplayService.qml` that reaches
`wayland.gamma.setLocation` goes through `startAutomation()`, and all four call sites are
guarded by `nightModeEnabled`:
line 476 inside enableNightMode(), after gamma control is turned on
line 746 restartTimer, armed from the same places
line 1163 only `if (root.nightModeEnabled)`
line 1190 only `if (root.nightModeEnabled && SessionData.nightModeAutoEnabled)`
So with `nightModeEnabled: false` and `nightModeAutoEnabled: true`, the coordinates never
reach the Go manager, `m.getLocation()` returns nil, the schedule is wiped, and the state
becomes permanent day. **Nothing will ever enable night mode, because the schedule that
would trigger it is only computed once night mode is already enabled.** The automation
depends on the schedule, the schedule depends on the enable, and the enable is what the
automation was supposed to do.
### Confirmed by effect
$ dms ipc call night getSchedule
Dawn: 0001-01-01T00:00:00Z ... Next transition: 2026-08-27T00:00:00+02:00
$ dms ipc call night enable
Night mode enabled
$ dms ipc call night getSchedule
Dawn: 2026-08-26T05:42:38+02:00
Sunrise: 2026-08-26T06:34:59+02:00
Sunset: 2026-08-26T19:39:28+02:00
Night: 2026-08-26T20:31:49+02:00
Next transition: 2026-08-26T19:39:28+02:00
One `enable`, same coordinates, same instant: the whole schedule appears and the next
transition becomes tonight's sunset instead of midnight. **Workaround for anyone hitting
this: enable night mode once by hand; the automation works from then on.**
Note the times are ~17 min inside the astronomical ones (real sunrise here is 06:17:54,
sunset 19:55:12) because `CalculateSunTimes` is called with `elevDaylight = 3.0`, i.e. the
sun 3 degrees above the horizon. That is a reasonable choice for a gamma ramp, but the IPC
labels it `Sunrise`/`Sunset`, which reads as the astronomical event.
`Sun position` stays `1.00` with a valid schedule too — it is the 0..1 day factor from
`getSunPosition`, rendered with a degree sign by `DisplayService.qml`.
## The two silent fallbacks that hid it
`core/internal/server/wayland/manager.go`, in the schedule computation:
```go
lat, lon := m.getLocation()
if lat == nil || lon == nil {
m.gammaState = StateStatic
// stale times from a previous config must not drive applies
m.schedule = sunSchedule{} // <- zero SunTimes
return // <- no log, no error
}
```
and then, in `updateStateFromSchedule`:
```go
if times.Sunrise.IsZero() {
pos = 1.0 // <- this is the reported "Sun position: 1.00"
temp = config.HighTemp
isDay = true // <- this is "Period: day", permanently
deadline = m.tomorrow(now) // <- this is "Next transition: tomorrow 00:00"
}
```
So every field I observed is accounted for, and `1.00` is not an elevation at all: it is
the hardcoded day-position fallback, which `DisplayService.qml` then renders with a degree
sign (`parts.push("Sun position: " + root.gammaSunPosition.toFixed(2) + "°")`).
Two things are worth separating here:
1. **`m.getLocation()` returns nil** although the QML side holds valid coordinates. That is
the bug to fix.
2. **"I have no location" degrades to "it is daytime forever"**, silently, and is then
presented through an IPC surface that looks like a computed schedule. That is what kept
(1) invisible: there is no state in which the user or a script can tell "no schedule"
apart from "day". Even a single `log.Warn("no location, gamma scheduling disabled")`
would have made this a five-minute diagnosis instead of a day.
A distinguishable state would help more than a log line: leaving `SunPosition` at NaN or
adding an explicit `scheduleValid bool` to `State` lets `getSchedule` say "no schedule"
instead of inventing one.
## A separate, smaller defect in the same area: operator precedence in `sunHourAngle`
`core/internal/server/wayland/suncalc.go`:
```go
func sunHourAngle(latRad, declination, targetSunRad float64) float64 {
return math.Acos(math.Cos(targetSunRad)/
math.Cos(latRad)*math.Cos(declination) -
math.Tan(latRad)*math.Tan(declination))
}
```
The standard sunrise equation is
cos(H) = cos(z) / (cos(lat) * cos(dec)) - tan(lat) * tan(dec)
but in Go `a / b * c` is `(a / b) * c`, so this multiplies by `cos(dec)` where it should
divide. Evaluated for 2026-08-26 (declination +10.69 deg):
| latitude | as written | with the parentheses |
|---|---|---|
| 45.7 (daylight, elev 3 deg) | 98.060 deg | 97.950 deg |
| 60.0 | 104.635 deg | 104.478 deg |
| 66.5 | 109.936 deg | 109.734 deg |
| 70.0 | 114.206 deg | 113.963 deg |
Small — tens of seconds of clock time at mid latitudes — but it also shifts where `Acos`
leaves its domain, which is the boundary that decides midnight-sun / polar-night. It is an
independent one-character fix and does not explain the zero times above.
## It fails quietly, which is the part worth fixing regardless
Of 5477 `dms.service` journal lines today, 24 mention gamma/night/temperature and
none of them is a schedule computation, an error, or a "could not determine sunrise".
A subsystem that produces the zero time should say so; as it stands the only symptom
is a screen that stays at 6500 K all night.
## What would confirm a fix
`dms ipc call night getSchedule` returning a sunset within a few minutes of the true
one for the configured coordinates, and `Sun position` tracking the real elevation
over the day.
Contributor guide
Research direction
Start with quickshell/Services/DisplayService.qml and core/internal/server/wayland/manager.go, tracing how location reaches the daemon before night mode is enabled. Run dms ipc call night getSchedule with location automation enabled, then verify that it reports computed sunrise and sunset without manual enabling. Also inspect core/internal/server/wayland/suncalc.go and confirm the corrected calculation preserves expected behavior near polar boundaries.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- desktop
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100