11package inputmethod
22
33import (
4+ "bufio"
45"context"
56"encoding/json"
67"fmt"
8+ "net"
9+ "os"
710"os/exec"
11+ "path/filepath"
812"regexp"
913"strings"
1014"time"
@@ -21,7 +25,7 @@ type Switcher struct {
2125rime * Rime
2226notifier interface {
2327ShowInputMethodSwitch (inputMethod string , clientInfo * config.WindowInfo )
24- } // Add notifier interface
28+ }
2529}
2630
2731type ClientInfo struct {
@@ -56,21 +60,212 @@ func (s *Switcher) SetNotifier(notifier interface {
5660func (s * Switcher ) MonitorAndSwitch (ctx context.Context ) error {
5761logger .Info ("Starting Hyprland input method switcher..." )
5862
59- ticker := time .NewTicker (200 * time .Millisecond )
60- defer ticker .Stop ()
63+ // Process initial window
64+ if err := s .processCurrentWindow (); err != nil {
65+ logger .Debugf ("Error processing initial window: %v" , err )
66+ }
67+
68+ // Start IPC event monitoring
69+ return s .monitorHyprlandEvents (ctx )
70+ }
71+
72+ func (s * Switcher ) monitorHyprlandEvents (ctx context.Context ) error {
73+ // Get Hyprland IPC socket path
74+ socketPath := s .getHyprlandEventSocket ()
75+ if socketPath == "" {
76+ return fmt .Errorf ("failed to get Hyprland event socket path" )
77+ }
78+
79+ logger .Infof ("Connecting to Hyprland event socket: %s" , socketPath )
6180
6281for {
6382select {
6483case <- ctx .Done ():
6584logger .Info ("Stopping input method switcher..." )
6685return nil
86+ default :
87+ }
88+
89+ // Connect to socket
90+ conn , err := net .Dial ("unix" , socketPath )
91+ if err != nil {
92+ logger .Errorf ("Failed to connect to Hyprland event socket: %v" , err )
93+ // Retry after delay
94+ select {
95+ case <- ctx .Done ():
96+ return nil
97+ case <- time .After (5 * time .Second ):
98+ continue
99+ }
100+ }
101+
102+ logger .Info ("Connected to Hyprland event socket" )
103+
104+ // Monitor events
105+ err = s .handleEvents (ctx , conn )
106+ conn .Close ()
107+
108+ if err != nil && err != context .Canceled {
109+ logger .Errorf ("Event monitoring error: %v" , err )
110+ // Retry after delay
111+ select {
112+ case <- ctx .Done ():
113+ return nil
114+ case <- time .After (5 * time .Second ):
115+ continue
116+ }
117+ }
118+
119+ if ctx .Err () != nil {
120+ return nil
121+ }
122+ }
123+ }
124+
125+ func (s * Switcher ) handleEvents (ctx context.Context , conn net.Conn ) error {
126+ scanner := bufio .NewScanner (conn )
127+
128+ for scanner .Scan () {
129+ select {
130+ case <- ctx .Done ():
131+ return context .Canceled
132+ default :
133+ }
134+
135+ line := scanner .Text ()
136+ if line == "" {
137+ continue
138+ }
139+
140+ // Parse event
141+ parts := strings .SplitN (line , ">>" , 2 )
142+ if len (parts ) != 2 {
143+ continue
144+ }
145+
146+ eventType := parts [0 ]
147+ eventData := parts [1 ]
148+
149+ logger .Debugf ("Received event: %s >> %s" , eventType , eventData )
150+
151+ // Handle window focus events - prefer activewindowv2 for better info
152+ switch eventType {
153+ case "activewindowv2" :
154+ if err := s .handleActiveWindowV2Event (eventData ); err != nil {
155+ logger .Debugf ("Error handling activewindowv2 event: %v" , err )
156+ }
157+ case "activewindow" :
158+ // Fallback for older Hyprland versions
159+ if err := s .handleActiveWindowEvent (eventData ); err != nil {
160+ logger .Debugf ("Error handling activewindow event: %v" , err )
161+ }
162+ }
163+ }
164+
165+ return scanner .Err ()
166+ }
167+
168+ func (s * Switcher ) handleActiveWindowV2Event (eventData string ) error {
169+ // eventData format: "windowaddress" (hex address like 0x12345678)
170+ windowAddress := strings .TrimSpace (eventData )
171+ if windowAddress == "" {
172+ logger .Debugf ("Empty activewindowv2 event data" )
173+ return nil
174+ }
175+
176+ logger .Debugf ("Active window changed to address: %s" , windowAddress )
177+
178+ // Check if this is the same window we're already tracking
179+ if windowAddress == s .currentClient .Address {
180+ logger .Debugf ("Same window address, skipping: %s" , windowAddress )
181+ return nil
182+ }
183+
184+ // Get full client info for the active window
185+ clientInfo , err := s .getCurrentClient ()
186+ if err != nil {
187+ return fmt .Errorf ("failed to get current client: %w" , err )
188+ }
189+
190+ // Verify the event matches current window address
191+ if clientInfo .Address != windowAddress {
192+ logger .Debugf ("Event address mismatch: got %s, expected %s" , windowAddress , clientInfo .Address )
193+ return nil
194+ }
195+
196+ // Process window change
197+ return s .processWindowChange (clientInfo )
198+ }
199+
200+ func (s * Switcher ) handleActiveWindowEvent (eventData string ) error {
201+ // eventData format: "class,title"
202+ parts := strings .SplitN (eventData , "," , 2 )
203+ if len (parts ) < 2 {
204+ logger .Debugf ("Invalid activewindow event data: %s" , eventData )
205+ return nil
206+ }
207+
208+ class := parts [0 ]
209+ title := parts [1 ]
210+
211+ logger .Debugf ("Active window changed: class=%s, title=%s" , class , title )
212+
213+ // Get full client info for the active window
214+ clientInfo , err := s .getCurrentClient ()
215+ if err != nil {
216+ return fmt .Errorf ("failed to get current client: %w" , err )
217+ }
218+
219+ // Verify the event matches current window
220+ if clientInfo .Class != class {
221+ logger .Debugf ("Event class mismatch: got %s, expected %s" , class , clientInfo .Class )
222+ return nil
223+ }
224+
225+ // Check if this is the same window we're already tracking
226+ if clientInfo .Address == s .currentClient .Address {
227+ logger .Debugf ("Same window address, skipping: %s" , clientInfo .Address )
228+ return nil
229+ }
230+
231+ // Process window change
232+ return s .processWindowChange (clientInfo )
233+ }
234+
235+ func (s * Switcher ) processWindowChange (clientInfo * ClientInfo ) error {
236+ // Update current client info
237+ s .currentClient = clientInfo
238+
239+ // Get current input method status
240+ currentIM := s .GetCurrent ()
241+
242+ // Determine target input method
243+ targetIM := s .getTargetInputMethod (clientInfo )
244+
245+ logger .Infof ("Window changed: %s - %s (address: %s)" , clientInfo .Class , clientInfo .Title , clientInfo .Address )
246+ logger .Infof ("Current IM: %s -> Target IM: %s" , currentIM , targetIM )
67247
68- case <- ticker .C :
69- if err := s .processCurrentWindow (); err != nil {
70- logger .Debugf ("Error processing current window: %v" , err )
248+ // If input method needs to be switched
249+ if currentIM != targetIM && currentIM != "unknown" {
250+ if err := s .Switch (targetIM ); err != nil {
251+ return fmt .Errorf ("failed to switch input method to %s: %w" , targetIM , err )
252+ }
253+
254+ logger .Infof ("Switched input method to: %s" , targetIM )
255+ s .currentIM = targetIM
256+
257+ // Show notification if notifier is available and enabled
258+ if s .notifier != nil && s .config .Notifications .ShowOnSwitch {
259+ // Convert ClientInfo to config.WindowInfo
260+ windowInfo := & config.WindowInfo {
261+ Class : clientInfo .Class ,
262+ Title : clientInfo .Title ,
71263}
264+ s .notifier .ShowInputMethodSwitch (targetIM , windowInfo )
72265}
73266}
267+
268+ return nil
74269}
75270
76271func (s * Switcher ) processCurrentWindow () error {
@@ -79,41 +274,105 @@ func (s *Switcher) processCurrentWindow() error {
79274return fmt .Errorf ("failed to get current client: %w" , err )
80275}
81276
82- // If window changed (different address means different window)
83- if clientInfo .Address != s .currentClient .Address {
84- s .currentClient = clientInfo
277+ return s .processWindowChange (clientInfo )
278+ }
279+
280+ func (s * Switcher ) getHyprlandEventSocket () string {
281+ logger .Debugf ("Searching for Hyprland IPC socket..." )
85282
86- // Get current input method status
87- currentIM := s .GetCurrent ()
283+ // Get XDG_RUNTIME_DIR
284+ runtimeDir := os .Getenv ("XDG_RUNTIME_DIR" )
285+ if runtimeDir == "" {
286+ logger .Debugf ("XDG_RUNTIME_DIR not set, falling back to /tmp" )
287+ runtimeDir = "/tmp"
288+ }
289+ logger .Debugf ("Using runtime directory: %s" , runtimeDir )
290+
291+ // Try environment variables first
292+ if hyprInstance := os .Getenv ("HYPRLAND_INSTANCE_SIGNATURE" ); hyprInstance != "" {
293+ logger .Debugf ("Found HYPRLAND_INSTANCE_SIGNATURE: %s" , hyprInstance )
294+ socketPath := fmt .Sprintf ("%s/hypr/%s/.socket2.sock" , runtimeDir , hyprInstance )
295+ logger .Debugf ("Checking socket path: %s" , socketPath )
296+ if _ , err := os .Stat (socketPath ); err == nil {
297+ logger .Infof ("Found Hyprland IPC socket via environment: %s" , socketPath )
298+ return socketPath
299+ } else {
300+ logger .Debugf ("Hyprland IPC Socket not found via environment: %v" , err )
301+ }
302+ } else {
303+ logger .Debugf ("HYPRLAND_INSTANCE_SIGNATURE not set" )
304+ }
88305
89- // Determine target input method
90- targetIM := s .getTargetInputMethod (clientInfo )
306+ // Check if hypr directory exists in runtime dir
307+ hyprDir := fmt .Sprintf ("%s/hypr" , runtimeDir )
308+ if _ , err := os .Stat (hyprDir ); os .IsNotExist (err ) {
309+ logger .Errorf ("Hyprland directory %s does not exist. Is Hyprland running?" , hyprDir )
310+ return ""
311+ }
91312
92- logger .Infof ("Window changed: %s - %s (address: %s)" , clientInfo .Class , clientInfo .Title , clientInfo .Address )
93- logger .Infof ("Current IM: %s -> Target IM: %s" , currentIM , targetIM )
313+ // List all directories in runtime/hypr/
314+ entries , err := os .ReadDir (hyprDir )
315+ if err != nil {
316+ logger .Errorf ("Failed to read Hyprland directory %s: %v" , hyprDir , err )
317+ return ""
318+ }
94319
95- // If input method needs to be switched
96- if currentIM != targetIM && currentIM != "unknown" {
97- if err := s .Switch (targetIM ); err != nil {
98- return fmt .Errorf ("failed to switch input method to %s: %w" , targetIM , err )
320+ logger .Debugf ("Found %d entries in %s" , len (entries ), hyprDir )
321+ for _ , entry := range entries {
322+ if entry .IsDir () {
323+ socketPath := fmt .Sprintf ("%s/%s/.socket2.sock" , hyprDir , entry .Name ())
324+ logger .Debugf ("Checking socket: %s" , socketPath )
325+ if _ , err := os .Stat (socketPath ); err == nil {
326+ logger .Infof ("Found Hyprland event socket: %s" , socketPath )
327+ return socketPath
328+ } else {
329+ logger .Debugf ("Socket not found: %v" , err )
99330}
331+ }
332+ }
333+
334+ // Fallback: try to find socket using glob pattern in runtime dir
335+ globPattern := fmt .Sprintf ("%s/hypr/*/.socket2.sock" , runtimeDir )
336+ logger .Debugf ("Trying glob pattern: %s" , globPattern )
337+ matches , err := filepath .Glob (globPattern )
338+ if err != nil {
339+ logger .Errorf ("Glob pattern failed: %v" , err )
340+ return ""
341+ }
100342
101- logger .Infof ("Switched input method to: %s" , targetIM )
102- s .currentIM = targetIM
103-
104- // Show notification if notifier is available and enabled
105- if s .notifier != nil && s .config .Notifications .ShowOnSwitch {
106- // Convert ClientInfo to config.WindowInfo
107- windowInfo := & config.WindowInfo {
108- Class : clientInfo .Class ,
109- Title : clientInfo .Title ,
110- }
111- s .notifier .ShowInputMethodSwitch (targetIM , windowInfo )
343+ logger .Debugf ("Glob found %d matches" , len (matches ))
344+ for _ , match := range matches {
345+ logger .Debugf ("Glob match: %s" , match )
346+ }
347+
348+ if len (matches ) == 0 {
349+ logger .Error ("No Hyprland event sockets found. Please check:" )
350+ logger .Error ("1. Is Hyprland running?" )
351+ logger .Error ("2. Are you running this inside a Hyprland session?" )
352+ logger .Errorf ("3. Check if %s/hypr directory exists and contains instance directories" , runtimeDir )
353+
354+ // List what's actually in runtime/hypr if it exists
355+ if entries , err := os .ReadDir (hyprDir ); err == nil {
356+ logger .Errorf ("Contents of %s:" , hyprDir )
357+ for _ , entry := range entries {
358+ logger .Errorf (" - %s (dir: %v)" , entry .Name (), entry .IsDir ())
112359}
113360}
361+
362+ // Also check for legacy /tmp/hypr path
363+ logger .Debugf ("Checking legacy /tmp/hypr path..." )
364+ legacyPattern := "/tmp/hypr/*/.socket2.sock"
365+ if legacyMatches , err := filepath .Glob (legacyPattern ); err == nil && len (legacyMatches ) > 0 {
366+ logger .Infof ("Found legacy socket: %s" , legacyMatches [0 ])
367+ return legacyMatches [0 ]
368+ }
369+
370+ return ""
114371}
115372
116- return nil
373+ // Use the first available socket
374+ logger .Infof ("Using first available socket: %s" , matches [0 ])
375+ return matches [0 ]
117376}
118377
119378func (s * Switcher ) getCurrentClient () (* ClientInfo , error ) {
0 commit comments