1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-04-04 04:42:05 -04:00

Add GeoClue2 integration as alternative to IP location (#1856)

* feat: switch auto location in weather widget to use GeoClue2 instead of simple IP check

* nix: enable GeoClue2 service by default

* lint: fix line endings

* fix: fall back to IP location if GeoClue is not available
This commit is contained in:
Sunner
2026-02-28 04:29:08 +01:00
committed by GitHub
parent ab211266a6
commit 7bea6b4a62
19 changed files with 974 additions and 71 deletions

View File

@@ -28,6 +28,12 @@ packages:
outpkg: mocks_brightness
interfaces:
DBusConn:
github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation:
config:
dir: "internal/mocks/geolocation"
outpkg: mocks_geolocation
interfaces:
Client:
github.com/AvengeMedia/DankMaterialShell/core/internal/server/network:
config:
dir: "internal/mocks/network"

View File

@@ -0,0 +1,14 @@
package geolocation
import "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
func NewClient() Client {
if geoclueClient, err := newGeoClueClient(); err != nil {
log.Warnf("Failed to initialize GeoClue2 client: %v", err)
} else {
return geoclueClient
}
log.Info("Falling back to IP location")
return newIpClient()
}

View File

@@ -0,0 +1,236 @@
package geolocation
import (
"fmt"
"sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/godbus/dbus/v5"
)
const (
dbusGeoClueService = "org.freedesktop.GeoClue2"
dbusGeoCluePath = "/org/freedesktop/GeoClue2"
dbusGeoClueInterface = dbusGeoClueService
dbusGeoClueManagerPath = dbusGeoCluePath + "/Manager"
dbusGeoClueManagerInterface = dbusGeoClueInterface + ".Manager"
dbusGeoClueManagerGetClient = dbusGeoClueManagerInterface + ".GetClient"
dbusGeoClueClientInterface = dbusGeoClueInterface + ".Client"
dbusGeoClueClientDesktopId = dbusGeoClueClientInterface + ".DesktopId"
dbusGeoClueClientTimeThreshold = dbusGeoClueClientInterface + ".TimeThreshold"
dbusGeoClueClientTimeStart = dbusGeoClueClientInterface + ".Start"
dbusGeoClueClientTimeStop = dbusGeoClueClientInterface + ".Stop"
dbusGeoClueClientLocationUpdated = dbusGeoClueClientInterface + ".LocationUpdated"
dbusGeoClueLocationInterface = dbusGeoClueInterface + ".Location"
dbusGeoClueLocationLatitude = dbusGeoClueLocationInterface + ".Latitude"
dbusGeoClueLocationLongitude = dbusGeoClueLocationInterface + ".Longitude"
)
type GeoClueClient struct {
currLocation *Location
locationMutex sync.RWMutex
dbusConn *dbus.Conn
clientPath dbus.ObjectPath
signals chan *dbus.Signal
stopChan chan struct{}
sigWG sync.WaitGroup
subscribers syncmap.Map[string, chan Location]
}
func newGeoClueClient() (*GeoClueClient, error) {
dbusConn, err := dbus.ConnectSystemBus()
if err != nil {
return nil, fmt.Errorf("system bus connection failed: %w", err)
}
c := &GeoClueClient{
dbusConn: dbusConn,
stopChan: make(chan struct{}),
signals: make(chan *dbus.Signal, 256),
currLocation: &Location{
Latitude: 0.0,
Longitude: 0.0,
},
}
if err := c.setupClient(); err != nil {
dbusConn.Close()
return nil, err
}
if err := c.startSignalPump(); err != nil {
return nil, err
}
return c, nil
}
func (c *GeoClueClient) Close() {
close(c.stopChan)
c.sigWG.Wait()
if c.signals != nil {
c.dbusConn.RemoveSignal(c.signals)
close(c.signals)
}
c.subscribers.Range(func(key string, ch chan Location) bool {
close(ch)
c.subscribers.Delete(key)
return true
})
if c.dbusConn != nil {
c.dbusConn.Close()
}
}
func (c *GeoClueClient) Subscribe(id string) chan Location {
ch := make(chan Location, 64)
c.subscribers.Store(id, ch)
return ch
}
func (c *GeoClueClient) Unsubscribe(id string) {
if ch, ok := c.subscribers.LoadAndDelete(id); ok {
close(ch)
}
}
func (c *GeoClueClient) setupClient() error {
managerObj := c.dbusConn.Object(dbusGeoClueService, dbusGeoClueManagerPath)
if err := managerObj.Call(dbusGeoClueManagerGetClient, 0).Store(&c.clientPath); err != nil {
return fmt.Errorf("failed to create GeoClue2 client: %w", err)
}
clientObj := c.dbusConn.Object(dbusGeoClueService, c.clientPath)
if err := clientObj.SetProperty(dbusGeoClueClientDesktopId, "dms"); err != nil {
return fmt.Errorf("failed to set desktop ID: %w", err)
}
if err := clientObj.SetProperty(dbusGeoClueClientTimeThreshold, uint(10)); err != nil {
return fmt.Errorf("failed to set time threshold: %w", err)
}
return nil
}
func (c *GeoClueClient) startSignalPump() error {
c.dbusConn.Signal(c.signals)
if err := c.dbusConn.AddMatchSignal(
dbus.WithMatchObjectPath(c.clientPath),
dbus.WithMatchInterface(dbusGeoClueClientInterface),
dbus.WithMatchSender(dbusGeoClueClientLocationUpdated),
); err != nil {
return err
}
c.sigWG.Add(1)
go func() {
defer c.sigWG.Done()
clientObj := c.dbusConn.Object(dbusGeoClueService, c.clientPath)
clientObj.Call(dbusGeoClueClientTimeStart, 0)
defer clientObj.Call(dbusGeoClueClientTimeStop, 0)
for {
select {
case <-c.stopChan:
return
case sig, ok := <-c.signals:
if !ok {
return
}
if sig == nil {
continue
}
c.handleSignal(sig)
}
}
}()
return nil
}
func (c *GeoClueClient) handleSignal(sig *dbus.Signal) {
switch sig.Name {
case dbusGeoClueClientLocationUpdated:
if len(sig.Body) != 2 {
return
}
newLocationPath, ok := sig.Body[1].(dbus.ObjectPath)
if !ok {
return
}
if err := c.handleLocationUpdated(newLocationPath); err != nil {
log.Warn("GeoClue: Failed to handle location update: %v", err)
return
}
}
}
func (c *GeoClueClient) handleLocationUpdated(path dbus.ObjectPath) error {
locationObj := c.dbusConn.Object(dbusGeoClueService, path)
lat, err := locationObj.GetProperty(dbusGeoClueLocationLatitude)
if err != nil {
return err
}
long, err := locationObj.GetProperty(dbusGeoClueLocationLongitude)
if err != nil {
return err
}
c.locationMutex.Lock()
c.currLocation.Latitude = dbusutil.AsOr(lat, 0.0)
c.currLocation.Longitude = dbusutil.AsOr(long, 0.0)
c.locationMutex.Unlock()
c.notifySubscribers()
return nil
}
func (c *GeoClueClient) notifySubscribers() {
currentLocation, err := c.GetLocation()
if err != nil {
return
}
c.subscribers.Range(func(key string, ch chan Location) bool {
select {
case ch <- currentLocation:
default:
log.Warn("GeoClue: subscriber channel full, dropping update")
}
return true
})
}
func (c *GeoClueClient) GetLocation() (Location, error) {
c.locationMutex.RLock()
defer c.locationMutex.RUnlock()
if c.currLocation == nil {
return Location{
Latitude: 0.0,
Longitude: 0.0,
}, nil
}
stateCopy := *c.currLocation
return stateCopy, nil
}

View File

@@ -0,0 +1,90 @@
package geolocation
import (
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
)
type IpClient struct {
currLocation *Location
}
type ipAPIResponse struct {
Lat float64 `json:"lat"`
Lon float64 `json:"lon"`
City string `json:"city"`
}
func newIpClient() *IpClient {
return &IpClient{
currLocation: &Location{
Latitude: 0.0,
Longitude: 0.0,
},
}
}
func (c *IpClient) Subscribe(id string) chan Location {
ch := make(chan Location, 1)
if location, err := c.GetLocation(); err != nil {
ch <- location
} else {
close(ch)
}
return ch
}
func (c *IpClient) Unsubscribe(id string) {
// Stub
}
func (c *IpClient) Close() {
// Stub
}
func (c *IpClient) GetLocation() (Location, error) {
client := &http.Client{
Timeout: 10 * time.Second,
}
result := Location{
Latitude: 0.0,
Longitude: 0.0,
}
resp, err := client.Get("http://ip-api.com/json/")
if err != nil {
return result, fmt.Errorf("failed to fetch IP location: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return result, fmt.Errorf("ip-api.com returned status %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return result, fmt.Errorf("failed to read response: %w", err)
}
var data ipAPIResponse
if err := json.Unmarshal(body, &data); err != nil {
return result, fmt.Errorf("failed to parse response: %w", err)
}
if data.Lat == 0 && data.Lon == 0 {
return result, fmt.Errorf("missing location data in response")
}
log.Infof("Fetched IP-based location: %s (%.4f, %.4f)", data.City, data.Lat, data.Lon)
result.Latitude = data.Lat
result.Longitude = data.Lon
return result, nil
}

View File

@@ -0,0 +1,15 @@
package geolocation
type Location struct {
Latitude float64
Longitude float64
}
type Client interface {
GetLocation() (Location, error)
Subscribe(id string) chan Location
Unsubscribe(id string)
Close()
}

View File

@@ -0,0 +1,203 @@
// Code generated by mockery v2.53.5. DO NOT EDIT.
package mocks_geolocation
import (
geolocation "github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
mock "github.com/stretchr/testify/mock"
)
// MockClient is an autogenerated mock type for the Client type
type MockClient struct {
mock.Mock
}
type MockClient_Expecter struct {
mock *mock.Mock
}
func (_m *MockClient) EXPECT() *MockClient_Expecter {
return &MockClient_Expecter{mock: &_m.Mock}
}
// Close provides a mock function with no fields
func (_m *MockClient) Close() {
_m.Called()
}
// MockClient_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
type MockClient_Close_Call struct {
*mock.Call
}
// Close is a helper method to define mock.On call
func (_e *MockClient_Expecter) Close() *MockClient_Close_Call {
return &MockClient_Close_Call{Call: _e.mock.On("Close")}
}
func (_c *MockClient_Close_Call) Run(run func()) *MockClient_Close_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockClient_Close_Call) Return() *MockClient_Close_Call {
_c.Call.Return()
return _c
}
func (_c *MockClient_Close_Call) RunAndReturn(run func()) *MockClient_Close_Call {
_c.Run(run)
return _c
}
// GetLocation provides a mock function with no fields
func (_m *MockClient) GetLocation() (geolocation.Location, error) {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for GetLocation")
}
var r0 geolocation.Location
var r1 error
if rf, ok := ret.Get(0).(func() (geolocation.Location, error)); ok {
return rf()
}
if rf, ok := ret.Get(0).(func() geolocation.Location); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(geolocation.Location)
}
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockClient_GetLocation_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetLocation'
type MockClient_GetLocation_Call struct {
*mock.Call
}
// GetLocation is a helper method to define mock.On call
func (_e *MockClient_Expecter) GetLocation() *MockClient_GetLocation_Call {
return &MockClient_GetLocation_Call{Call: _e.mock.On("GetLocation")}
}
func (_c *MockClient_GetLocation_Call) Run(run func()) *MockClient_GetLocation_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockClient_GetLocation_Call) Return(_a0 geolocation.Location, _a1 error) *MockClient_GetLocation_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockClient_GetLocation_Call) RunAndReturn(run func() (geolocation.Location, error)) *MockClient_GetLocation_Call {
_c.Call.Return(run)
return _c
}
// Subscribe provides a mock function with given fields: id
func (_m *MockClient) Subscribe(id string) chan geolocation.Location {
ret := _m.Called(id)
if len(ret) == 0 {
panic("no return value specified for Subscribe")
}
var r0 chan geolocation.Location
if rf, ok := ret.Get(0).(func(string) chan geolocation.Location); ok {
r0 = rf(id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(chan geolocation.Location)
}
}
return r0
}
// MockClient_Subscribe_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Subscribe'
type MockClient_Subscribe_Call struct {
*mock.Call
}
// Subscribe is a helper method to define mock.On call
// - id string
func (_e *MockClient_Expecter) Subscribe(id interface{}) *MockClient_Subscribe_Call {
return &MockClient_Subscribe_Call{Call: _e.mock.On("Subscribe", id)}
}
func (_c *MockClient_Subscribe_Call) Run(run func(id string)) *MockClient_Subscribe_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *MockClient_Subscribe_Call) Return(_a0 chan geolocation.Location) *MockClient_Subscribe_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockClient_Subscribe_Call) RunAndReturn(run func(string) chan geolocation.Location) *MockClient_Subscribe_Call {
_c.Call.Return(run)
return _c
}
// Unsubscribe provides a mock function with given fields: id
func (_m *MockClient) Unsubscribe(id string) {
_m.Called(id)
}
// MockClient_Unsubscribe_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Unsubscribe'
type MockClient_Unsubscribe_Call struct {
*mock.Call
}
// Unsubscribe is a helper method to define mock.On call
// - id string
func (_e *MockClient_Expecter) Unsubscribe(id interface{}) *MockClient_Unsubscribe_Call {
return &MockClient_Unsubscribe_Call{Call: _e.mock.On("Unsubscribe", id)}
}
func (_c *MockClient_Unsubscribe_Call) Run(run func(id string)) *MockClient_Unsubscribe_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *MockClient_Unsubscribe_Call) Return() *MockClient_Unsubscribe_Call {
_c.Call.Return()
return _c
}
func (_c *MockClient_Unsubscribe_Call) RunAndReturn(run func(string)) *MockClient_Unsubscribe_Call {
_c.Run(run)
return _c
}
// NewMockClient creates a new instance of MockClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockClient(t interface {
mock.TestingT
Cleanup(func())
}) *MockClient {
mock := &MockClient{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@@ -0,0 +1,61 @@
package location
import (
"encoding/json"
"fmt"
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
)
type LocationEvent struct {
Type string `json:"type"`
Data State `json:"data"`
}
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
switch req.Method {
case "location.getState":
handleGetState(conn, req, manager)
case "location.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 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()
event := LocationEvent{
Type: "state_changed",
Data: initialState,
}
if err := json.NewEncoder(conn).Encode(models.Response[LocationEvent]{
ID: req.ID,
Result: &event,
}); err != nil {
return
}
for state := range stateChan {
event := LocationEvent{
Type: "state_changed",
Data: state,
}
if err := json.NewEncoder(conn).Encode(models.Response[LocationEvent]{
Result: &event,
}); err != nil {
return
}
}
}

View File

@@ -0,0 +1,174 @@
package location
import (
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
)
func NewManager(client geolocation.Client) (*Manager, error) {
currLocation, err := client.GetLocation()
if err != nil {
log.Warnf("Failed to get initial location: %v", err)
}
m := &Manager{
client: client,
dirty: make(chan struct{}),
state: &State{
Latitude: currLocation.Latitude,
Longitude: currLocation.Longitude,
},
}
if err := m.startSignalPump(); err != nil {
return nil, err
}
m.notifierWg.Add(1)
go m.notifier()
return m, nil
}
func (m *Manager) Close() {
close(m.stopChan)
m.notifierWg.Wait()
m.sigWG.Wait()
m.subscribers.Range(func(key string, ch chan State) bool {
close(ch)
m.subscribers.Delete(key)
return true
})
}
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 ch, ok := m.subscribers.LoadAndDelete(id); ok {
close(ch)
}
}
func (m *Manager) startSignalPump() error {
m.sigWG.Add(1)
go func() {
defer m.sigWG.Done()
subscription := m.client.Subscribe("locationManager")
defer m.client.Unsubscribe("locationManager")
for {
select {
case <-m.stopChan:
return
case location, ok := <-subscription:
if !ok {
return
}
m.handleLocationChange(location)
}
}
}()
return nil
}
func (m *Manager) handleLocationChange(location geolocation.Location) {
m.stateMutex.Lock()
defer m.stateMutex.Unlock()
m.state.Latitude = location.Latitude
m.state.Longitude = location.Longitude
m.notifySubscribers()
}
func (m *Manager) notifySubscribers() {
select {
case m.dirty <- struct{}{}:
default:
}
}
func (m *Manager) GetState() State {
m.stateMutex.RLock()
defer m.stateMutex.RUnlock()
if m.state == nil {
return State{
Latitude: 0.0,
Longitude: 0.0,
}
}
stateCopy := *m.state
return stateCopy
}
func (m *Manager) notifier() {
defer m.notifierWg.Done()
const minGap = 200 * time.Millisecond
timer := time.NewTimer(minGap)
timer.Stop()
var pending bool
for {
select {
case <-m.stopChan:
timer.Stop()
return
case <-m.dirty:
if pending {
continue
}
pending = true
timer.Reset(minGap)
case <-timer.C:
if !pending {
continue
}
currentState := m.GetState()
if m.lastNotified != nil && !stateChanged(m.lastNotified, &currentState) {
pending = false
continue
}
m.subscribers.Range(func(key string, ch chan State) bool {
select {
case ch <- currentState:
default:
log.Warn("Location: subscriber channel full, dropping update")
}
return true
})
stateCopy := currentState
m.lastNotified = &stateCopy
pending = false
}
}
}
func stateChanged(old, new *State) bool {
if old == nil || new == nil {
return true
}
if old.Latitude != new.Latitude {
return true
}
if old.Longitude != new.Longitude {
return true
}
return false
}

View File

@@ -0,0 +1,28 @@
package location
import (
"sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
)
type State struct {
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
}
type Manager struct {
state *State
stateMutex sync.RWMutex
client geolocation.Client
stopChan chan struct{}
sigWG sync.WaitGroup
subscribers syncmap.Map[string, chan State]
dirty chan struct{}
notifierWg sync.WaitGroup
lastNotified *State
}

View File

@@ -15,6 +15,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/extworkspace"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/freedesktop"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/location"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
@@ -192,6 +193,15 @@ func RouteRequest(conn net.Conn, req models.Request) {
return
}
if strings.HasPrefix(req.Method, "location.") {
if locationManager == nil {
models.RespondError(conn, req.ID, "location manager not initialized")
return
}
location.HandleRequest(conn, req, locationManager)
return
}
switch req.Method {
case "ping":
models.Respond(conn, req.ID, "pong")

View File

@@ -14,6 +14,7 @@ import (
"syscall"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/apppicker"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/bluez"
@@ -25,6 +26,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/extworkspace"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/freedesktop"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/location"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
@@ -70,6 +72,7 @@ var clipboardManager *clipboard.Manager
var dbusManager *serverDbus.Manager
var wlContext *wlcontext.SharedContext
var themeModeManager *thememode.Manager
var locationManager *location.Manager
const dbusClientID = "dms-dbus-client"
@@ -188,7 +191,7 @@ func InitializeFreedeskManager() error {
return nil
}
func InitializeWaylandManager() error {
func InitializeWaylandManager(geoClient geolocation.Client) error {
log.Info("Attempting to initialize Wayland gamma control...")
if wlContext == nil {
@@ -201,7 +204,7 @@ func InitializeWaylandManager() error {
}
config := wayland.DefaultConfig()
manager, err := wayland.NewManager(wlContext.Display(), config)
manager, err := wayland.NewManager(wlContext.Display(), geoClient, config)
if err != nil {
log.Errorf("Failed to initialize wayland manager: %v", err)
return err
@@ -382,14 +385,27 @@ func InitializeDbusManager() error {
return nil
}
func InitializeThemeModeManager() error {
manager := thememode.NewManager()
func InitializeThemeModeManager(geoClient geolocation.Client) error {
manager := thememode.NewManager(geoClient)
themeModeManager = manager
log.Info("Theme mode automation manager initialized")
return nil
}
func InitializeLocationManager(geoClient geolocation.Client) error {
manager, err := location.NewManager(geoClient)
if err != nil {
log.Warnf("Failed to initialize location manager: %v", err)
return err
}
locationManager = manager
log.Info("Location manager initialized")
return nil
}
func handleConnection(conn net.Conn) {
defer conn.Close()
@@ -537,6 +553,10 @@ func getServerInfo() ServerInfo {
caps = append(caps, "theme.auto")
}
if locationManager != nil {
caps = append(caps, "location")
}
if dbusManager != nil {
caps = append(caps, "dbus")
}
@@ -1307,6 +1327,9 @@ func cleanupManagers() {
if wlContext != nil {
wlContext.Close()
}
if locationManager != nil {
locationManager.Close()
}
}
func Start(printDocs bool) error {
@@ -1488,6 +1511,9 @@ func Start(printDocs bool) error {
log.Info(" clipboard.getConfig - Get clipboard configuration")
log.Info(" clipboard.setConfig - Set configuration (params: maxHistory?, maxEntrySize?, autoClearDays?, clearAtStartup?)")
log.Info(" clipboard.subscribe - Subscribe to clipboard state changes (streaming)")
log.Info("Location:")
log.Info(" location.getState - Get current location state")
log.Info(" location.subscribe - Subscribe to location changes (streaming)")
log.Info("")
}
log.Info("Initializing managers...")
@@ -1519,6 +1545,9 @@ func Start(printDocs bool) error {
loginctlReady := make(chan struct{})
freedesktopReady := make(chan struct{})
geoClient := geolocation.NewClient()
defer geoClient.Close()
go func() {
defer close(loginctlReady)
if err := InitializeLoginctlManager(); err != nil {
@@ -1563,7 +1592,7 @@ func Start(printDocs bool) error {
}
}()
if err := InitializeWaylandManager(); err != nil {
if err := InitializeWaylandManager(geoClient); err != nil {
log.Warnf("Wayland manager unavailable: %v", err)
}
@@ -1595,7 +1624,7 @@ func Start(printDocs bool) error {
log.Debugf("WlrOutput manager unavailable: %v", err)
}
if err := InitializeThemeModeManager(); err != nil {
if err := InitializeThemeModeManager(geoClient); err != nil {
log.Warnf("Theme mode manager unavailable: %v", err)
} else {
notifyCapabilityChange()
@@ -1608,6 +1637,12 @@ func Start(printDocs bool) error {
}()
}
if err := InitializeLocationManager(geoClient); err != nil {
log.Warnf("Location manager unavailable: %v", err)
} else {
notifyCapabilityChange()
}
fatalErrChan := make(chan error, 1)
if wlrOutputManager != nil {
go func() {

View File

@@ -5,6 +5,7 @@ import (
"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"
@@ -32,12 +33,14 @@ type Manager struct {
cachedIPLat *float64
cachedIPLon *float64
geoClient geolocation.Client
stopChan chan struct{}
updateTrigger chan struct{}
wg sync.WaitGroup
}
func NewManager() *Manager {
func NewManager(geoClient geolocation.Client) *Manager {
m := &Manager{
config: Config{
Enabled: false,
@@ -51,6 +54,7 @@ func NewManager() *Manager {
},
stopChan: make(chan struct{}),
updateTrigger: make(chan struct{}, 1),
geoClient: geoClient,
}
m.updateState(time.Now())
@@ -327,17 +331,17 @@ func (m *Manager) getLocation(config Config) (*float64, *float64) {
}
m.locationMutex.RUnlock()
lat, lon, err := wayland.FetchIPLocation()
location, err := m.geoClient.GetLocation()
if err != nil {
return nil, nil
}
m.locationMutex.Lock()
m.cachedIPLat = lat
m.cachedIPLon = lon
m.cachedIPLat = &location.Latitude
m.cachedIPLon = &location.Longitude
m.locationMutex.Unlock()
return lat, lon
return m.cachedIPLat, m.cachedIPLon
}
func statesEqual(a, b *State) bool {

View File

@@ -13,13 +13,14 @@ import (
"golang.org/x/sys/unix"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_gamma_control"
)
const animKelvinStep = 25
func NewManager(display wlclient.WaylandDisplay, config Config) (*Manager, error) {
func NewManager(display wlclient.WaylandDisplay, geoClient geolocation.Client, config Config) (*Manager, error) {
if err := config.Validate(); err != nil {
return nil, err
}
@@ -40,6 +41,7 @@ func NewManager(display wlclient.WaylandDisplay, config Config) (*Manager, error
updateTrigger: make(chan struct{}, 1),
dirty: make(chan struct{}, 1),
dbusSignal: make(chan *dbus.Signal, 16),
geoClient: geoClient,
}
if err := m.setupRegistry(); err != nil {
@@ -437,15 +439,16 @@ func (m *Manager) getLocation() (*float64, *float64) {
}
m.locationMutex.RUnlock()
lat, lon, err := FetchIPLocation()
location, err := m.geoClient.GetLocation()
if err != nil {
return nil, nil
}
m.locationMutex.Lock()
m.cachedIPLat = lat
m.cachedIPLon = lon
m.cachedIPLat = &location.Latitude
m.cachedIPLon = &location.Longitude
m.locationMutex.Unlock()
return lat, lon
return m.cachedIPLat, m.cachedIPLon
}
return nil, nil
}

View File

@@ -8,6 +8,7 @@ import (
"github.com/stretchr/testify/assert"
mocks_geolocation "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/geolocation"
mocks_wlclient "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlclient"
)
@@ -390,18 +391,20 @@ func TestNotifySubscribers_NonBlocking(t *testing.T) {
func TestNewManager_GetRegistryError(t *testing.T) {
mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t)
mockGeoclient := mocks_geolocation.NewMockClient(t)
mockDisplay.EXPECT().Context().Return(nil)
mockDisplay.EXPECT().GetRegistry().Return(nil, errors.New("failed to get registry"))
config := DefaultConfig()
_, err := NewManager(mockDisplay, config)
_, err := NewManager(mockDisplay, mockGeoclient, config)
assert.Error(t, err)
assert.Contains(t, err.Error(), "get registry")
}
func TestNewManager_InvalidConfig(t *testing.T) {
mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t)
mockGeoclient := mocks_geolocation.NewMockClient(t)
config := Config{
LowTemp: 500,
@@ -409,6 +412,6 @@ func TestNewManager_InvalidConfig(t *testing.T) {
Gamma: 1.0,
}
_, err := NewManager(mockDisplay, config)
_, err := NewManager(mockDisplay, mockGeoclient, config)
assert.Error(t, err)
}

View File

@@ -6,6 +6,7 @@ import (
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/godbus/dbus/v5"
@@ -97,6 +98,8 @@ type Manager struct {
dbusConn *dbus.Conn
dbusSignal chan *dbus.Signal
geoClient geolocation.Client
lastAppliedTemp int
lastAppliedGamma float64
}