diff options
Diffstat (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/evdev')
5 files changed, 913 insertions, 0 deletions
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/evdev/handlers.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/evdev/handlers.go new file mode 100644 index 0000000..54a7bca --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/evdev/handlers.go @@ -0,0 +1,20 @@ +package evdev + +import ( + "net" + + "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" +) + +func HandleRequest(conn net.Conn, req models.Request, m *Manager) { + switch req.Method { + case "evdev.getState": + handleGetState(conn, req, m) + default: + models.RespondError(conn, req.ID, "unknown method: "+req.Method) + } +} + +func handleGetState(conn net.Conn, req models.Request, m *Manager) { + models.Respond(conn, req.ID, m.GetState()) +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/evdev/handlers_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/evdev/handlers_test.go new file mode 100644 index 0000000..f9bd21d --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/evdev/handlers_test.go @@ -0,0 +1,130 @@ +package evdev + +import ( + "bytes" + "encoding/json" + "errors" + "net" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + mocks "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/evdev" + "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" +) + +type mockNetConn struct { + net.Conn + readBuf *bytes.Buffer + writeBuf *bytes.Buffer + closed bool +} + +func newMockNetConn() *mockNetConn { + return &mockNetConn{ + readBuf: &bytes.Buffer{}, + writeBuf: &bytes.Buffer{}, + } +} + +func (m *mockNetConn) Read(b []byte) (n int, err error) { + return m.readBuf.Read(b) +} + +func (m *mockNetConn) Write(b []byte) (n int, err error) { + return m.writeBuf.Write(b) +} + +func (m *mockNetConn) Close() error { + m.closed = true + return nil +} + +func TestHandleRequest(t *testing.T) { + t.Run("getState request", func(t *testing.T) { + mockDevice := mocks.NewMockEvdevDevice(t) + mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe() + + m := &Manager{ + devices: []EvdevDevice{mockDevice}, + state: State{Available: true, CapsLock: true}, + closeChan: make(chan struct{}), + } + + conn := newMockNetConn() + req := models.Request{ + ID: 123, + Method: "evdev.getState", + Params: map[string]any{}, + } + + HandleRequest(conn, req, m) + + var resp models.Response[State] + err := json.NewDecoder(conn.writeBuf).Decode(&resp) + require.NoError(t, err) + + assert.Equal(t, 123, resp.ID) + assert.NotNil(t, resp.Result) + assert.True(t, resp.Result.Available) + assert.True(t, resp.Result.CapsLock) + }) + + t.Run("unknown method", func(t *testing.T) { + mockDevice := mocks.NewMockEvdevDevice(t) + mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe() + + m := &Manager{ + devices: []EvdevDevice{mockDevice}, + state: State{Available: true, CapsLock: false}, + closeChan: make(chan struct{}), + } + + conn := newMockNetConn() + req := models.Request{ + ID: 456, + Method: "evdev.unknownMethod", + Params: map[string]any{}, + } + + HandleRequest(conn, req, m) + + var resp models.Response[any] + err := json.NewDecoder(conn.writeBuf).Decode(&resp) + require.NoError(t, err) + + assert.Equal(t, 456, resp.ID) + assert.NotEmpty(t, resp.Error) + assert.Contains(t, resp.Error, "unknown method") + }) +} + +func TestHandleGetState(t *testing.T) { + mockDevice := mocks.NewMockEvdevDevice(t) + mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe() + + m := &Manager{ + devices: []EvdevDevice{mockDevice}, + state: State{Available: true, CapsLock: false}, + closeChan: make(chan struct{}), + } + + conn := newMockNetConn() + req := models.Request{ + ID: 789, + Method: "evdev.getState", + Params: map[string]any{}, + } + + handleGetState(conn, req, m) + + var resp models.Response[State] + err := json.NewDecoder(conn.writeBuf).Decode(&resp) + require.NoError(t, err) + + assert.Equal(t, 789, resp.ID) + assert.NotNil(t, resp.Result) + assert.True(t, resp.Result.Available) + assert.False(t, resp.Result.CapsLock) +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/evdev/manager.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/evdev/manager.go new file mode 100644 index 0000000..b578481 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/evdev/manager.go @@ -0,0 +1,413 @@ +package evdev + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "sync" + "time" + + "github.com/AvengeMedia/DankMaterialShell/core/internal/log" + "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap" + "github.com/fsnotify/fsnotify" + evdev "github.com/holoplot/go-evdev" +) + +const ( + evKeyType = 0x01 + evLedType = 0x11 + keyCapslockKey = 58 + ledCapslockKey = 1 + keyStateOn = 1 +) + +type EvdevDevice interface { + Name() (string, error) + Path() string + Close() error + ReadOne() (*evdev.InputEvent, error) + State(t evdev.EvType) (evdev.StateMap, error) +} + +type Manager struct { + devices []EvdevDevice + devicesMutex sync.RWMutex + monitoredPaths map[string]bool + state State + stateMutex sync.RWMutex + subscribers syncmap.Map[string, chan State] + closeChan chan struct{} + closeOnce sync.Once + watcher *fsnotify.Watcher +} + +func NewManager() (*Manager, error) { + devices, err := findKeyboards() + if err != nil { + return nil, fmt.Errorf("failed to find keyboards: %w", err) + } + + initialCapsLock := readInitialCapsLockState(devices[0]) + + watcher, err := fsnotify.NewWatcher() + if err != nil { + log.Warnf("Failed to create fsnotify watcher, hotplug detection disabled: %v", err) + watcher = nil + } else if err := watcher.Add("/dev/input"); err != nil { + log.Warnf("Failed to watch /dev/input, hotplug detection disabled: %v", err) + watcher.Close() + watcher = nil + } + + monitoredPaths := make(map[string]bool) + for _, device := range devices { + monitoredPaths[device.Path()] = true + } + + m := &Manager{ + devices: devices, + monitoredPaths: monitoredPaths, + state: State{Available: true, CapsLock: initialCapsLock}, + + closeChan: make(chan struct{}), + watcher: watcher, + } + + for i, device := range devices { + go m.monitorDevice(device, i) + } + + if watcher != nil { + go m.watchForNewKeyboards() + } + + return m, nil +} + +func readInitialCapsLockState(device EvdevDevice) bool { + ledStates, err := device.State(evLedType) + if err != nil { + log.Debugf("Could not read LED state: %v", err) + return false + } + + return ledStates[ledCapslockKey] +} + +func findKeyboards() ([]EvdevDevice, error) { + pattern := "/dev/input/event*" + matches, err := filepath.Glob(pattern) + if err != nil { + return nil, fmt.Errorf("failed to glob input devices: %w", err) + } + + if len(matches) == 0 { + return nil, fmt.Errorf("no input devices found") + } + + var keyboards []EvdevDevice + for _, path := range matches { + device, err := evdev.Open(path) + if err != nil { + continue + } + + if !isKeyboard(device) { + device.Close() + continue + } + + deviceName, _ := device.Name() + log.Debugf("Found keyboard: %s at %s", deviceName, path) + keyboards = append(keyboards, device) + } + + if len(keyboards) == 0 { + return nil, fmt.Errorf("no keyboard device found") + } + + return keyboards, nil +} + +func isKeyboard(device EvdevDevice) bool { + deviceName, err := device.Name() + if err != nil { + return false + } + + name := strings.ToLower(deviceName) + + switch { + case strings.Contains(name, "keyboard"): + return true + case strings.Contains(name, "kbd"): + return true + case strings.Contains(name, "input") && strings.Contains(name, "key"): + return true + } + + keyStates, err := device.State(evKeyType) + if err != nil { + return false + } + + hasKeyA := len(keyStates) > 30 + hasKeyZ := len(keyStates) > 44 + hasEnter := len(keyStates) > 28 + + return hasKeyA && hasKeyZ && hasEnter && len(keyStates) > 100 +} + +func (m *Manager) watchForNewKeyboards() { + defer func() { + if r := recover(); r != nil { + log.Errorf("Panic in keyboard hotplug monitor: %v", r) + } + }() + + for { + select { + case <-m.closeChan: + return + case event, ok := <-m.watcher.Events: + if !ok { + return + } + + if !strings.HasPrefix(filepath.Base(event.Name), "event") { + continue + } + + if event.Op&fsnotify.Create == fsnotify.Create { + time.Sleep(100 * time.Millisecond) + + m.devicesMutex.Lock() + if m.monitoredPaths[event.Name] { + m.devicesMutex.Unlock() + continue + } + + device, err := evdev.Open(event.Name) + if err != nil { + m.devicesMutex.Unlock() + continue + } + + if !isKeyboard(device) { + device.Close() + m.devicesMutex.Unlock() + continue + } + + deviceName, _ := device.Name() + log.Debugf("Hotplugged keyboard: %s at %s", deviceName, event.Name) + + m.devices = append(m.devices, device) + m.monitoredPaths[event.Name] = true + deviceIndex := len(m.devices) - 1 + m.devicesMutex.Unlock() + + go m.monitorDevice(device, deviceIndex) + } else if event.Op&fsnotify.Remove == fsnotify.Remove { + m.devicesMutex.Lock() + if !m.monitoredPaths[event.Name] { + m.devicesMutex.Unlock() + continue + } + + delete(m.monitoredPaths, event.Name) + for i, device := range m.devices { + if device != nil && device.Path() == event.Name { + log.Debugf("Keyboard removed: %s", event.Name) + device.Close() + m.devices[i] = nil + break + } + } + m.devicesMutex.Unlock() + } + + case err, ok := <-m.watcher.Errors: + if !ok { + return + } + log.Warnf("Keyboard hotplug watcher error: %v", err) + } + } +} + +func (m *Manager) monitorDevice(device EvdevDevice, deviceIndex int) { + defer func() { + if r := recover(); r != nil { + log.Errorf("Panic in evdev monitor: %v", r) + } + }() + + for { + select { + case <-m.closeChan: + return + default: + } + + event, err := device.ReadOne() + if err != nil { + if isClosedError(err) { + return + } + log.Warnf("Failed to read evdev event: %v", err) + time.Sleep(100 * time.Millisecond) + continue + } + + if event == nil { + continue + } + + if event.Type == evKeyType && event.Code == keyCapslockKey && event.Value == keyStateOn { + time.Sleep(50 * time.Millisecond) + m.readAndUpdateCapsLockState(deviceIndex) + } else if event.Type == evLedType && event.Code == ledCapslockKey { + capsLockState := event.Value == keyStateOn + m.updateCapsLockStateDirect(capsLockState) + } + } +} + +func isClosedError(err error) bool { + if err == nil { + return false + } + + errStr := err.Error() + switch { + case strings.Contains(errStr, "closed"): + return true + case strings.Contains(errStr, "bad file descriptor"): + return true + default: + return false + } +} + +func (m *Manager) readAndUpdateCapsLockState(deviceIndex int) { + m.devicesMutex.RLock() + if deviceIndex >= len(m.devices) { + m.devicesMutex.RUnlock() + return + } + device := m.devices[deviceIndex] + m.devicesMutex.RUnlock() + + ledStates, err := device.State(evLedType) + if err != nil { + log.Warnf("Failed to read LED state: %v", err) + return + } + + if len(ledStates) == 0 { + log.Debug("No LED state available (empty map)") + + // This means the device either: + // - doesn't support LED reporting at all, or + // - the kernel returned an empty state + return + } + + capsLockState := ledStates[ledCapslockKey] + m.updateCapsLockStateDirect(capsLockState) +} + +func (m *Manager) updateCapsLockStateDirect(capsLockState bool) { + m.stateMutex.Lock() + if m.state.CapsLock == capsLockState { + m.stateMutex.Unlock() + return + } + + m.state.CapsLock = capsLockState + newState := m.state + m.stateMutex.Unlock() + + log.Debugf("Caps lock state: %v", newState.CapsLock) + m.notifySubscribers(newState) +} + +func (m *Manager) GetState() State { + m.stateMutex.RLock() + defer m.stateMutex.RUnlock() + return m.state +} + +func (m *Manager) Subscribe(id string) chan State { + ch := make(chan State, 16) + 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) notifySubscribers(state State) { + m.subscribers.Range(func(key string, ch chan State) bool { + select { + case ch <- state: + default: + } + return true + }) +} + +func (m *Manager) Close() { + m.closeOnce.Do(func() { + close(m.closeChan) + + if m.watcher != nil { + m.watcher.Close() + } + + m.devicesMutex.Lock() + for _, device := range m.devices { + if device == nil { + continue + } + if err := device.Close(); err != nil && !isClosedError(err) { + log.Warnf("Error closing evdev device: %v", err) + } + } + m.devicesMutex.Unlock() + + m.subscribers.Range(func(key string, ch chan State) bool { + close(ch) + m.subscribers.Delete(key) + return true + }) + }) +} + +func InitializeManager() (*Manager, error) { + if os.Getuid() != 0 && !hasInputGroupAccess() { + return nil, fmt.Errorf("insufficient permissions to access input devices") + } + + return NewManager() +} + +func hasInputGroupAccess() bool { + pattern := "/dev/input/event*" + matches, err := filepath.Glob(pattern) + if err != nil || len(matches) == 0 { + return false + } + + testFile, err := os.Open(matches[0]) + if err != nil { + return false + } + testFile.Close() + return true +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/evdev/manager_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/evdev/manager_test.go new file mode 100644 index 0000000..667c078 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/evdev/manager_test.go @@ -0,0 +1,344 @@ +package evdev + +import ( + "errors" + "testing" + + evdev "github.com/holoplot/go-evdev" + "github.com/stretchr/testify/assert" + + mocks "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/evdev" +) + +func TestManager_Creation(t *testing.T) { + t.Run("manager created successfully with caps lock off", func(t *testing.T) { + mockDevice := mocks.NewMockEvdevDevice(t) + mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe() + + m := &Manager{ + devices: []EvdevDevice{mockDevice}, + state: State{Available: true, CapsLock: false}, + closeChan: make(chan struct{}), + } + + assert.NotNil(t, m) + assert.True(t, m.state.Available) + assert.False(t, m.state.CapsLock) + }) + + t.Run("manager created successfully with caps lock on", func(t *testing.T) { + mockDevice := mocks.NewMockEvdevDevice(t) + mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe() + + m := &Manager{ + devices: []EvdevDevice{mockDevice}, + state: State{Available: true, CapsLock: true}, + closeChan: make(chan struct{}), + } + + assert.NotNil(t, m) + assert.True(t, m.state.Available) + assert.True(t, m.state.CapsLock) + }) +} + +func TestManager_GetState(t *testing.T) { + mockDevice := mocks.NewMockEvdevDevice(t) + mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe() + + m := &Manager{ + devices: []EvdevDevice{mockDevice}, + monitoredPaths: make(map[string]bool), + state: State{Available: true, CapsLock: false}, + closeChan: make(chan struct{}), + } + + state := m.GetState() + assert.True(t, state.Available) + assert.False(t, state.CapsLock) +} + +func TestManager_Subscribe(t *testing.T) { + mockDevice := mocks.NewMockEvdevDevice(t) + mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe() + + m := &Manager{ + devices: []EvdevDevice{mockDevice}, + monitoredPaths: make(map[string]bool), + state: State{Available: true, CapsLock: false}, + closeChan: make(chan struct{}), + } + + ch := m.Subscribe("test-client") + assert.NotNil(t, ch) + count := 0 + m.subscribers.Range(func(key string, ch chan State) bool { + count++ + return true + }) + assert.Equal(t, 1, count) +} + +func TestManager_Unsubscribe(t *testing.T) { + mockDevice := mocks.NewMockEvdevDevice(t) + mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe() + + m := &Manager{ + devices: []EvdevDevice{mockDevice}, + monitoredPaths: make(map[string]bool), + state: State{Available: true, CapsLock: false}, + closeChan: make(chan struct{}), + } + + ch := m.Subscribe("test-client") + count := 0 + m.subscribers.Range(func(key string, ch chan State) bool { + count++ + return true + }) + assert.Equal(t, 1, count) + + m.Unsubscribe("test-client") + count = 0 + m.subscribers.Range(func(key string, ch chan State) bool { + count++ + return true + }) + assert.Equal(t, 0, count) + + select { + case _, ok := <-ch: + assert.False(t, ok, "channel should be closed") + default: + t.Error("channel should be closed") + } +} + +func TestManager_UpdateCapsLock(t *testing.T) { + mockDevice := mocks.NewMockEvdevDevice(t) + mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe() + + m := &Manager{ + devices: []EvdevDevice{mockDevice}, + monitoredPaths: make(map[string]bool), + state: State{Available: true, CapsLock: false}, + closeChan: make(chan struct{}), + } + + ch := m.Subscribe("test-client") + + ledStateOn := evdev.StateMap{ledCapslockKey: true} + mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(ledStateOn, nil).Once() + + go func() { + m.readAndUpdateCapsLockState(0) + }() + + newState := <-ch + assert.True(t, newState.CapsLock) + + ledStateOff := evdev.StateMap{ledCapslockKey: false} + mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(ledStateOff, nil).Once() + + go func() { + m.readAndUpdateCapsLockState(0) + }() + + newState = <-ch + assert.False(t, newState.CapsLock) +} + +func TestManager_Close(t *testing.T) { + mockDevice := mocks.NewMockEvdevDevice(t) + mockDevice.EXPECT().Close().Return(nil).Once() + mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe() + + m := &Manager{ + devices: []EvdevDevice{mockDevice}, + monitoredPaths: make(map[string]bool), + state: State{Available: true, CapsLock: false}, + closeChan: make(chan struct{}), + } + + ch1 := m.Subscribe("client1") + ch2 := m.Subscribe("client2") + + m.Close() + + select { + case _, ok := <-ch1: + assert.False(t, ok, "channel 1 should be closed") + default: + t.Error("channel 1 should be closed") + } + + select { + case _, ok := <-ch2: + assert.False(t, ok, "channel 2 should be closed") + default: + t.Error("channel 2 should be closed") + } + + count := 0 + m.subscribers.Range(func(key string, ch chan State) bool { + count++ + return true + }) + assert.Equal(t, 0, count) + + m.Close() +} + +func TestIsKeyboard(t *testing.T) { + tests := []struct { + name string + devName string + expected bool + }{ + {"keyboard in name", "AT Translated Set 2 keyboard", true}, + {"kbd in name", "USB kbd", true}, + {"input and key", "input key device", true}, + {"random device", "Mouse", false}, + {"empty name", "", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockDevice := mocks.NewMockEvdevDevice(t) + mockDevice.EXPECT().Name().Return(tt.devName, nil).Once() + + if !tt.expected { + mockDevice.EXPECT().State(evdev.EvType(evKeyType)).Return(evdev.StateMap{}, nil).Maybe() + } + + result := isKeyboard(mockDevice) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestIsKeyboard_ErrorHandling(t *testing.T) { + mockDevice := mocks.NewMockEvdevDevice(t) + mockDevice.EXPECT().Name().Return("", errors.New("device error")).Once() + + result := isKeyboard(mockDevice) + assert.False(t, result) +} + +func TestManager_MonitorDevice(t *testing.T) { + t.Run("caps lock key press updates state", func(t *testing.T) { + mockDevice := mocks.NewMockEvdevDevice(t) + + capsLockEvent := &evdev.InputEvent{ + Type: evKeyType, + Code: keyCapslockKey, + Value: keyStateOn, + } + + ledState := evdev.StateMap{ledCapslockKey: true} + + mockDevice.EXPECT().ReadOne().Return(capsLockEvent, nil).Once() + mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(ledState, nil).Once() + mockDevice.EXPECT().ReadOne().Return(nil, errors.New("stop")).Maybe() + mockDevice.EXPECT().Close().Return(nil).Maybe() + + m := &Manager{ + devices: []EvdevDevice{mockDevice}, + state: State{Available: true, CapsLock: false}, + closeChan: make(chan struct{}), + } + + ch := m.Subscribe("test") + + go m.monitorDevice(mockDevice, 0) + + state := <-ch + assert.True(t, state.CapsLock) + + m.Close() + }) +} + +func TestIsClosedError(t *testing.T) { + tests := []struct { + name string + err error + expected bool + }{ + {"nil error", nil, false}, + {"closed error", errors.New("device closed"), true}, + {"bad file descriptor", errors.New("bad file descriptor"), true}, + {"other error", errors.New("some other error"), false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := isClosedError(tt.err) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestNotifySubscribers(t *testing.T) { + mockDevice := mocks.NewMockEvdevDevice(t) + mockDevice.EXPECT().ReadOne().Return(nil, errors.New("test")).Maybe() + mockDevice.EXPECT().Close().Return(nil).Maybe() + + m := &Manager{ + devices: []EvdevDevice{mockDevice}, + monitoredPaths: make(map[string]bool), + state: State{Available: true, CapsLock: false}, + closeChan: make(chan struct{}), + } + + ch1 := m.Subscribe("client1") + ch2 := m.Subscribe("client2") + + newState := State{Available: true, CapsLock: true} + go m.notifySubscribers(newState) + + state1 := <-ch1 + state2 := <-ch2 + + assert.Equal(t, newState, state1) + assert.Equal(t, newState, state2) + + m.Close() +} + +func TestReadInitialCapsLockState(t *testing.T) { + t.Run("caps lock is on", func(t *testing.T) { + mockDevice := mocks.NewMockEvdevDevice(t) + ledState := evdev.StateMap{ + ledCapslockKey: true, + } + mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(ledState, nil).Once() + + result := readInitialCapsLockState(mockDevice) + assert.True(t, result) + }) + + t.Run("caps lock is off", func(t *testing.T) { + mockDevice := mocks.NewMockEvdevDevice(t) + ledState := evdev.StateMap{ + ledCapslockKey: false, + } + mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(ledState, nil).Once() + + result := readInitialCapsLockState(mockDevice) + assert.False(t, result) + }) + + t.Run("error reading LED state", func(t *testing.T) { + mockDevice := mocks.NewMockEvdevDevice(t) + mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(nil, errors.New("read error")).Once() + + result := readInitialCapsLockState(mockDevice) + assert.False(t, result) + }) +} + +func TestHasInputGroupAccess(t *testing.T) { + result := hasInputGroupAccess() + t.Logf("hasInputGroupAccess: %v", result) +} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/evdev/models.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/evdev/models.go new file mode 100644 index 0000000..df8ec9e --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/evdev/models.go @@ -0,0 +1,6 @@ +package evdev + +type State struct { + Available bool `json:"available"` + CapsLock bool `json:"capsLock"` +} |