summaryrefslogtreecommitdiff
path: root/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/thememode
diff options
context:
space:
mode:
authorAlexanderCurl <alexc@alexc.hu>2026-04-18 17:46:06 +0100
committerAlexanderCurl <alexc@alexc.hu>2026-04-18 17:46:06 +0100
commit70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69 (patch)
treeab1123d4067c1b086dd6faa7ee4ea643236b565a /raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/thememode
parent5d94c0a7d44a2255b81815a52a7056a94a39842d (diff)
downloadRaveOS-PKGBUILD-70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69.tar.gz
RaveOS-PKGBUILD-70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69.zip
Replaced file structures for themes in the correct format
Diffstat (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/thememode')
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/thememode/handlers.go154
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/thememode/manager.go462
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/thememode/types.go23
3 files changed, 639 insertions, 0 deletions
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/thememode/handlers.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/thememode/handlers.go
new file mode 100644
index 0000000..6891875
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/thememode/handlers.go
@@ -0,0 +1,154 @@
+package thememode
+
+import (
+ "encoding/json"
+ "fmt"
+ "net"
+
+ "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
+ "github.com/AvengeMedia/DankMaterialShell/core/internal/server/params"
+)
+
+func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
+ if manager == nil {
+ models.RespondError(conn, req.ID, "theme mode manager not initialized")
+ return
+ }
+
+ switch req.Method {
+ case "theme.auto.getState":
+ handleGetState(conn, req, manager)
+ case "theme.auto.setEnabled":
+ handleSetEnabled(conn, req, manager)
+ case "theme.auto.setMode":
+ handleSetMode(conn, req, manager)
+ case "theme.auto.setSchedule":
+ handleSetSchedule(conn, req, manager)
+ case "theme.auto.setLocation":
+ handleSetLocation(conn, req, manager)
+ case "theme.auto.setUseIPLocation":
+ handleSetUseIPLocation(conn, req, manager)
+ case "theme.auto.trigger":
+ handleTrigger(conn, req, manager)
+ case "theme.auto.subscribe":
+ handleSubscribe(conn, req, manager)
+ default:
+ models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
+ }
+}
+
+func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
+ models.Respond(conn, req.ID, manager.GetState())
+}
+
+func handleSetEnabled(conn net.Conn, req models.Request, manager *Manager) {
+ enabled, err := params.Bool(req.Params, "enabled")
+ if err != nil {
+ models.RespondError(conn, req.ID, err.Error())
+ return
+ }
+
+ manager.SetEnabled(enabled)
+ models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "theme auto enabled set"})
+}
+
+func handleSetMode(conn net.Conn, req models.Request, manager *Manager) {
+ mode, err := params.String(req.Params, "mode")
+ if err != nil {
+ models.RespondError(conn, req.ID, err.Error())
+ return
+ }
+
+ if mode != "time" && mode != "location" {
+ models.RespondError(conn, req.ID, "invalid mode")
+ return
+ }
+
+ manager.SetMode(mode)
+ models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "theme auto mode set"})
+}
+
+func handleSetSchedule(conn net.Conn, req models.Request, manager *Manager) {
+ startHour, err := params.Int(req.Params, "startHour")
+ if err != nil {
+ models.RespondError(conn, req.ID, err.Error())
+ return
+ }
+ startMinute, err := params.Int(req.Params, "startMinute")
+ if err != nil {
+ models.RespondError(conn, req.ID, err.Error())
+ return
+ }
+ endHour, err := params.Int(req.Params, "endHour")
+ if err != nil {
+ models.RespondError(conn, req.ID, err.Error())
+ return
+ }
+ endMinute, err := params.Int(req.Params, "endMinute")
+ if err != nil {
+ models.RespondError(conn, req.ID, err.Error())
+ return
+ }
+
+ if err := manager.ValidateSchedule(startHour, startMinute, endHour, endMinute); err != nil {
+ models.RespondError(conn, req.ID, err.Error())
+ return
+ }
+
+ manager.SetSchedule(startHour, startMinute, endHour, endMinute)
+ models.Respond(conn, req.ID, manager.GetState())
+}
+
+func handleSetLocation(conn net.Conn, req models.Request, manager *Manager) {
+ lat, err := params.Float(req.Params, "latitude")
+ if err != nil {
+ models.RespondError(conn, req.ID, err.Error())
+ return
+ }
+ lon, err := params.Float(req.Params, "longitude")
+ if err != nil {
+ models.RespondError(conn, req.ID, err.Error())
+ return
+ }
+
+ manager.SetLocation(lat, lon)
+ models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "theme auto location set"})
+}
+
+func handleSetUseIPLocation(conn net.Conn, req models.Request, manager *Manager) {
+ use, err := params.Bool(req.Params, "use")
+ if err != nil {
+ models.RespondError(conn, req.ID, err.Error())
+ return
+ }
+
+ manager.SetUseIPLocation(use)
+ models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "theme auto IP location set"})
+}
+
+func handleTrigger(conn net.Conn, req models.Request, manager *Manager) {
+ manager.TriggerUpdate()
+ models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "theme auto update triggered"})
+}
+
+func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
+ clientID := fmt.Sprintf("client-%p", conn)
+ stateChan := manager.Subscribe(clientID)
+ defer manager.Unsubscribe(clientID)
+
+ initialState := manager.GetState()
+ if err := json.NewEncoder(conn).Encode(models.Response[State]{
+ ID: req.ID,
+ Result: &initialState,
+ }); err != nil {
+ return
+ }
+
+ for state := range stateChan {
+ if err := json.NewEncoder(conn).Encode(models.Response[State]{
+ Result: &state,
+ }); err != nil {
+ return
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/thememode/manager.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/thememode/manager.go
new file mode 100644
index 0000000..f7644ad
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/thememode/manager.go
@@ -0,0 +1,462 @@
+package thememode
+
+import (
+ "errors"
+ "sync"
+ "time"
+
+ "github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
+ "github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
+ "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
+ "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
+)
+
+const (
+ defaultStartHour = 18
+ defaultStartMinute = 0
+ defaultEndHour = 6
+ defaultEndMinute = 0
+ defaultElevationTwilight = -6.0
+ defaultElevationDaylight = 3.0
+)
+
+type Manager struct {
+ config Config
+ configMutex sync.RWMutex
+
+ state *State
+ stateMutex sync.RWMutex
+
+ subscribers syncmap.Map[string, chan State]
+
+ locationMutex sync.RWMutex
+ cachedIPLat *float64
+ cachedIPLon *float64
+
+ geoClient geolocation.Client
+
+ stopChan chan struct{}
+ updateTrigger chan struct{}
+ wg sync.WaitGroup
+}
+
+func NewManager() *Manager {
+ m := &Manager{
+ config: Config{
+ Enabled: false,
+ Mode: "time",
+ StartHour: defaultStartHour,
+ StartMinute: defaultStartMinute,
+ EndHour: defaultEndHour,
+ EndMinute: defaultEndMinute,
+ ElevationTwilight: defaultElevationTwilight,
+ ElevationDaylight: defaultElevationDaylight,
+ },
+ stopChan: make(chan struct{}),
+ updateTrigger: make(chan struct{}, 1),
+ }
+
+ m.updateState(time.Now())
+
+ m.wg.Add(1)
+ go m.schedulerLoop()
+
+ return m
+}
+
+func (m *Manager) GetState() State {
+ m.stateMutex.RLock()
+ defer m.stateMutex.RUnlock()
+ if m.state == nil {
+ return State{Config: m.getConfig()}
+ }
+ stateCopy := *m.state
+ return stateCopy
+}
+
+func (m *Manager) Subscribe(id string) chan State {
+ ch := make(chan State, 64)
+ m.subscribers.Store(id, ch)
+ return ch
+}
+
+func (m *Manager) Unsubscribe(id string) {
+ if val, ok := m.subscribers.LoadAndDelete(id); ok {
+ close(val)
+ }
+}
+
+func (m *Manager) SetEnabled(enabled bool) {
+ m.configMutex.Lock()
+ if m.config.Enabled == enabled {
+ m.configMutex.Unlock()
+ return
+ }
+ m.config.Enabled = enabled
+ m.configMutex.Unlock()
+ m.TriggerUpdate()
+}
+
+func (m *Manager) SetMode(mode string) {
+ m.configMutex.Lock()
+ if m.config.Mode == mode {
+ m.configMutex.Unlock()
+ return
+ }
+ m.config.Mode = mode
+ m.configMutex.Unlock()
+ m.TriggerUpdate()
+}
+
+func (m *Manager) SetSchedule(startHour, startMinute, endHour, endMinute int) {
+ m.configMutex.Lock()
+ changed := m.config.StartHour != startHour ||
+ m.config.StartMinute != startMinute ||
+ m.config.EndHour != endHour ||
+ m.config.EndMinute != endMinute
+ if !changed {
+ m.configMutex.Unlock()
+ return
+ }
+ m.config.StartHour = startHour
+ m.config.StartMinute = startMinute
+ m.config.EndHour = endHour
+ m.config.EndMinute = endMinute
+ m.configMutex.Unlock()
+ m.TriggerUpdate()
+}
+
+func (m *Manager) SetLocation(lat, lon float64) {
+ m.configMutex.Lock()
+ if m.config.Latitude != nil && m.config.Longitude != nil &&
+ *m.config.Latitude == lat && *m.config.Longitude == lon && !m.config.UseIPLocation {
+ m.configMutex.Unlock()
+ return
+ }
+ m.config.Latitude = &lat
+ m.config.Longitude = &lon
+ m.config.UseIPLocation = false
+ m.configMutex.Unlock()
+
+ m.locationMutex.Lock()
+ m.cachedIPLat = nil
+ m.cachedIPLon = nil
+ m.locationMutex.Unlock()
+
+ m.TriggerUpdate()
+}
+
+func (m *Manager) SetUseIPLocation(use bool) {
+ m.configMutex.Lock()
+ if m.config.UseIPLocation == use {
+ m.configMutex.Unlock()
+ return
+ }
+ m.config.UseIPLocation = use
+ if use {
+ m.config.Latitude = nil
+ m.config.Longitude = nil
+ }
+ m.configMutex.Unlock()
+
+ if use {
+ m.locationMutex.Lock()
+ m.cachedIPLat = nil
+ m.cachedIPLon = nil
+ m.locationMutex.Unlock()
+ }
+
+ m.TriggerUpdate()
+}
+
+func (m *Manager) TriggerUpdate() {
+ select {
+ case m.updateTrigger <- struct{}{}:
+ default:
+ }
+}
+
+func (m *Manager) Close() {
+ select {
+ case <-m.stopChan:
+ return
+ default:
+ close(m.stopChan)
+ }
+ m.wg.Wait()
+ m.subscribers.Range(func(key string, ch chan State) bool {
+ close(ch)
+ m.subscribers.Delete(key)
+ return true
+ })
+}
+
+func (m *Manager) WatchLoginctl(lm *loginctl.Manager) {
+ ch := lm.Subscribe("thememode")
+ m.wg.Add(1)
+ go func() {
+ defer m.wg.Done()
+ defer lm.Unsubscribe("thememode")
+ for {
+ select {
+ case <-m.stopChan:
+ return
+ case state, ok := <-ch:
+ if !ok {
+ return
+ }
+ if state.PreparingForSleep {
+ continue
+ }
+ m.TriggerUpdate()
+ }
+ }
+ }()
+}
+
+func (m *Manager) schedulerLoop() {
+ defer m.wg.Done()
+
+ var timer *time.Timer
+ for {
+ config := m.getConfig()
+ now := time.Now()
+ var isLight bool
+ var next time.Time
+ if config.Enabled {
+ isLight, next = m.computeSchedule(now, config)
+ } else {
+ m.stateMutex.RLock()
+ if m.state != nil {
+ isLight = m.state.IsLight
+ }
+ m.stateMutex.RUnlock()
+ next = now.Add(24 * time.Hour)
+ }
+
+ m.updateStateWithValues(config, isLight, next)
+
+ waitDur := time.Until(next)
+ if !config.Enabled {
+ waitDur = 24 * time.Hour
+ }
+ if waitDur < time.Second {
+ waitDur = time.Second
+ }
+
+ if timer != nil {
+ timer.Stop()
+ }
+ timer = time.NewTimer(waitDur)
+
+ select {
+ case <-m.stopChan:
+ timer.Stop()
+ return
+ case <-m.updateTrigger:
+ timer.Stop()
+ continue
+ case <-timer.C:
+ continue
+ }
+ }
+}
+
+func (m *Manager) updateState(now time.Time) {
+ config := m.getConfig()
+ var isLight bool
+ var next time.Time
+ if config.Enabled {
+ isLight, next = m.computeSchedule(now, config)
+ } else {
+ m.stateMutex.RLock()
+ if m.state != nil {
+ isLight = m.state.IsLight
+ }
+ m.stateMutex.RUnlock()
+ next = now.Add(24 * time.Hour)
+ }
+ m.updateStateWithValues(config, isLight, next)
+}
+
+func (m *Manager) updateStateWithValues(config Config, isLight bool, next time.Time) {
+ newState := State{
+ Config: config,
+ IsLight: isLight,
+ NextTransition: next,
+ }
+
+ m.stateMutex.Lock()
+ if m.state != nil && statesEqual(m.state, &newState) {
+ m.stateMutex.Unlock()
+ return
+ }
+ m.state = &newState
+ m.stateMutex.Unlock()
+
+ m.notifySubscribers()
+}
+
+func (m *Manager) notifySubscribers() {
+ state := m.GetState()
+ m.subscribers.Range(func(key string, ch chan State) bool {
+ select {
+ case ch <- state:
+ default:
+ }
+ return true
+ })
+}
+
+func (m *Manager) getConfig() Config {
+ m.configMutex.RLock()
+ defer m.configMutex.RUnlock()
+ return m.config
+}
+
+func (m *Manager) SetGeoClient(client geolocation.Client) {
+ m.geoClient = client
+}
+
+func (m *Manager) getLocation(config Config) (*float64, *float64) {
+ if config.Latitude != nil && config.Longitude != nil {
+ return config.Latitude, config.Longitude
+ }
+ if !config.UseIPLocation {
+ return nil, nil
+ }
+ if m.geoClient == nil {
+ return nil, nil
+ }
+
+ m.locationMutex.RLock()
+ if m.cachedIPLat != nil && m.cachedIPLon != nil {
+ lat, lon := m.cachedIPLat, m.cachedIPLon
+ m.locationMutex.RUnlock()
+ return lat, lon
+ }
+ m.locationMutex.RUnlock()
+
+ location, err := m.geoClient.GetLocation()
+ if err != nil {
+ return nil, nil
+ }
+
+ m.locationMutex.Lock()
+ m.cachedIPLat = &location.Latitude
+ m.cachedIPLon = &location.Longitude
+ m.locationMutex.Unlock()
+
+ return m.cachedIPLat, m.cachedIPLon
+}
+
+func statesEqual(a, b *State) bool {
+ if a == nil || b == nil {
+ return a == b
+ }
+ if a.IsLight != b.IsLight || !a.NextTransition.Equal(b.NextTransition) {
+ return false
+ }
+ return a.Config == b.Config
+}
+
+func (m *Manager) computeSchedule(now time.Time, config Config) (bool, time.Time) {
+ switch config.Mode {
+ case "location":
+ return m.computeLocationSchedule(now, config)
+ default:
+ return computeTimeSchedule(now, config)
+ }
+}
+
+func computeTimeSchedule(now time.Time, config Config) (bool, time.Time) {
+ startMinutes := config.StartHour*60 + config.StartMinute
+ endMinutes := config.EndHour*60 + config.EndMinute
+ currentMinutes := now.Hour()*60 + now.Minute()
+
+ startTime := time.Date(now.Year(), now.Month(), now.Day(), config.StartHour, config.StartMinute, 0, 0, now.Location())
+ endTime := time.Date(now.Year(), now.Month(), now.Day(), config.EndHour, config.EndMinute, 0, 0, now.Location())
+
+ if startMinutes == endMinutes {
+ next := startTime
+ if !next.After(now) {
+ next = next.Add(24 * time.Hour)
+ }
+ return true, next
+ }
+
+ if startMinutes < endMinutes {
+ if currentMinutes < startMinutes {
+ return true, startTime
+ }
+ if currentMinutes >= endMinutes {
+ return true, startTime.Add(24 * time.Hour)
+ }
+ return false, endTime
+ }
+
+ if currentMinutes >= startMinutes {
+ return false, endTime.Add(24 * time.Hour)
+ }
+ if currentMinutes < endMinutes {
+ return false, endTime
+ }
+ return true, startTime
+}
+
+func (m *Manager) computeLocationSchedule(now time.Time, config Config) (bool, time.Time) {
+ lat, lon := m.getLocation(config)
+ if lat == nil || lon == nil {
+ currentIsLight := false
+ m.stateMutex.RLock()
+ if m.state != nil {
+ currentIsLight = m.state.IsLight
+ }
+ m.stateMutex.RUnlock()
+ return currentIsLight, now.Add(10 * time.Minute)
+ }
+
+ times, cond := wayland.CalculateSunTimesWithTwilight(*lat, *lon, now, config.ElevationTwilight, config.ElevationDaylight)
+ switch cond {
+ case wayland.SunMidnightSun:
+ return true, startOfNextDay(now)
+ case wayland.SunPolarNight:
+ return false, startOfNextDay(now)
+ }
+
+ if now.Before(times.Sunrise) {
+ return false, times.Sunrise
+ }
+ if now.Before(times.Sunset) {
+ return true, times.Sunset
+ }
+
+ nextDay := startOfNextDay(now)
+ nextTimes, nextCond := wayland.CalculateSunTimesWithTwilight(*lat, *lon, nextDay, config.ElevationTwilight, config.ElevationDaylight)
+ switch nextCond {
+ case wayland.SunMidnightSun:
+ return true, startOfNextDay(nextDay)
+ case wayland.SunPolarNight:
+ return false, startOfNextDay(nextDay)
+ }
+
+ return false, nextTimes.Sunrise
+}
+
+func startOfNextDay(t time.Time) time.Time {
+ next := t.Add(24 * time.Hour)
+ return time.Date(next.Year(), next.Month(), next.Day(), 0, 0, 0, 0, next.Location())
+}
+
+func validateHourMinute(hour, minute int) bool {
+ return hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59
+}
+
+func (m *Manager) ValidateSchedule(startHour, startMinute, endHour, endMinute int) error {
+ if !validateHourMinute(startHour, startMinute) || !validateHourMinute(endHour, endMinute) {
+ return errInvalidTime
+ }
+ return nil
+}
+
+var errInvalidTime = errors.New("invalid schedule time")
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/thememode/types.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/thememode/types.go
new file mode 100644
index 0000000..72e6468
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/thememode/types.go
@@ -0,0 +1,23 @@
+package thememode
+
+import "time"
+
+type Config struct {
+ Enabled bool `json:"enabled"`
+ Mode string `json:"mode"`
+ StartHour int `json:"startHour"`
+ StartMinute int `json:"startMinute"`
+ EndHour int `json:"endHour"`
+ EndMinute int `json:"endMinute"`
+ Latitude *float64 `json:"latitude,omitempty"`
+ Longitude *float64 `json:"longitude,omitempty"`
+ UseIPLocation bool `json:"useIPLocation"`
+ ElevationTwilight float64 `json:"elevationTwilight"`
+ ElevationDaylight float64 `json:"elevationDaylight"`
+}
+
+type State struct {
+ Config Config `json:"config"`
+ IsLight bool `json:"isLight"`
+ NextTransition time.Time `json:"nextTransition"`
+}