diff options
| author | AlexanderCurl <alexc@alexc.hu> | 2026-04-18 17:46:06 +0100 |
|---|---|---|
| committer | AlexanderCurl <alexc@alexc.hu> | 2026-04-18 17:46:06 +0100 |
| commit | 70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69 (patch) | |
| tree | ab1123d4067c1b086dd6faa7ee4ea643236b565a /raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/thememode | |
| parent | 5d94c0a7d44a2255b81815a52a7056a94a39842d (diff) | |
| download | RaveOS-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')
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"` +} |