summaryrefslogtreecommitdiff
path: root/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/geolocation
diff options
context:
space:
mode:
Diffstat (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/geolocation')
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/geolocation/client.go42
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/geolocation/client_geoclue.go243
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/geolocation/client_ip.go91
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/geolocation/types.go15
4 files changed, 391 insertions, 0 deletions
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/geolocation/client.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/geolocation/client.go
new file mode 100644
index 0000000..5b44121
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/geolocation/client.go
@@ -0,0 +1,42 @@
+package geolocation
+
+import "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
+
+func NewClient() Client {
+ geoclueClient, err := newGeoClueClient()
+ if err != nil {
+ log.Warnf("GeoClue2 unavailable: %v", err)
+ return newSeededIpClient()
+ }
+
+ loc, _ := geoclueClient.GetLocation()
+ if loc.Latitude != 0 || loc.Longitude != 0 {
+ log.Info("Using GeoClue2 location")
+ return geoclueClient
+ }
+
+ log.Info("GeoClue2 has no fix yet, seeding with IP location")
+ ipLoc, err := fetchIPLocation()
+ if err != nil {
+ log.Warnf("IP location seed failed: %v", err)
+ return geoclueClient
+ }
+
+ log.Info("Seeded GeoClue2 with IP location")
+ geoclueClient.SeedLocation(Location{Latitude: ipLoc.Latitude, Longitude: ipLoc.Longitude})
+ return geoclueClient
+}
+
+func newSeededIpClient() *IpClient {
+ client := newIpClient()
+ ipLoc, err := fetchIPLocation()
+ if err != nil {
+ log.Warnf("IP location also failed: %v", err)
+ return client
+ }
+
+ log.Info("Using IP location")
+ client.currLocation.Latitude = ipLoc.Latitude
+ client.currLocation.Longitude = ipLoc.Longitude
+ return client
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/geolocation/client_geoclue.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/geolocation/client_geoclue.go
new file mode 100644
index 0000000..a06aadf
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/geolocation/client_geoclue.go
@@ -0,0 +1,243 @@
+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) SeedLocation(loc Location) {
+ c.locationMutex.Lock()
+ defer c.locationMutex.Unlock()
+ c.currLocation.Latitude = loc.Latitude
+ c.currLocation.Longitude = loc.Longitude
+}
+
+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
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/geolocation/client_ip.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/geolocation/client_ip.go
new file mode 100644
index 0000000..0e68cae
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/geolocation/client_ip.go
@@ -0,0 +1,91 @@
+package geolocation
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "time"
+)
+
+type IpClient struct {
+ currLocation *Location
+}
+
+type ipLocationResult struct {
+ Location
+ City string
+}
+
+type ipAPIResponse struct {
+ Status string `json:"status"`
+ Lat float64 `json:"lat"`
+ Lon float64 `json:"lon"`
+ City string `json:"city"`
+}
+
+func newIpClient() *IpClient {
+ return &IpClient{
+ currLocation: &Location{},
+ }
+}
+
+func (c *IpClient) Subscribe(id string) chan Location {
+ ch := make(chan Location, 1)
+ if location, err := c.GetLocation(); err == nil {
+ ch <- location
+ }
+ return ch
+}
+
+func (c *IpClient) Unsubscribe(id string) {}
+
+func (c *IpClient) Close() {}
+
+func (c *IpClient) GetLocation() (Location, error) {
+ if c.currLocation.Latitude != 0 || c.currLocation.Longitude != 0 {
+ return *c.currLocation, nil
+ }
+
+ result, err := fetchIPLocation()
+ if err != nil {
+ return Location{}, err
+ }
+
+ c.currLocation.Latitude = result.Latitude
+ c.currLocation.Longitude = result.Longitude
+ return *c.currLocation, nil
+}
+
+func fetchIPLocation() (ipLocationResult, error) {
+ client := &http.Client{Timeout: 10 * time.Second}
+
+ resp, err := client.Get("http://ip-api.com/json/")
+ if err != nil {
+ return ipLocationResult{}, fmt.Errorf("failed to fetch IP location: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return ipLocationResult{}, fmt.Errorf("ip-api.com returned status %d", resp.StatusCode)
+ }
+
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return ipLocationResult{}, fmt.Errorf("failed to read response: %w", err)
+ }
+
+ var data ipAPIResponse
+ if err := json.Unmarshal(body, &data); err != nil {
+ return ipLocationResult{}, fmt.Errorf("failed to parse response: %w", err)
+ }
+
+ if data.Status == "fail" || (data.Lat == 0 && data.Lon == 0) {
+ return ipLocationResult{}, fmt.Errorf("ip-api.com returned no location data")
+ }
+
+ return ipLocationResult{
+ Location: Location{Latitude: data.Lat, Longitude: data.Lon},
+ City: data.City,
+ }, nil
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/geolocation/types.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/geolocation/types.go
new file mode 100644
index 0000000..bf71041
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/geolocation/types.go
@@ -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()
+}