1// focus_state_machine.go - Centralized focus state management with queue-based serialization
2package browser
3
4import (
5 "container/heap"
6 "fmt"
7 "log"
8 "runtime/debug"
9 "sync"
10 "time"
11
12 "github.com/bnema/dumber/pkg/webkit"
13 "github.com/diamondburned/gotk4/pkg/gtk/v4"
14)
15
16// FocusState represents the current state of the focus management system
17type FocusState string
18
19const (
20 StateInitializing FocusState = "initializing" // System startup, determining initial focus
21 StateIdle FocusState = "idle" // No focus transition in progress, system stable
22 StateTransitioning FocusState = "transitioning" // Processing a focus change request
23 StateFocused FocusState = "focused" // Focus successfully applied, awaiting next request
24 StateReconciling FocusState = "reconciling" // Fixing inconsistencies detected in CSS/focus state
25)
26
27// FocusSource identifies where a focus request originated
28type FocusSource string
29
30const (
31 SourceKeyboard FocusSource = "keyboard" // User keyboard navigation
32 SourceMouse FocusSource = "mouse" // Mouse hover/click
33 SourceProgrammatic FocusSource = "programmatic" // API call
34 SourceSystem FocusSource = "system" // System initialization
35 SourceStackNav FocusSource = "stack-nav" // Stack navigation
36 SourceSplit FocusSource = "split" // Pane split operation
37 SourceClose FocusSource = "close" // Pane close operation
38)
39
40// Priority levels for focus requests
41const (
42 PriorityLow = 10
43 PriorityNormal = 50
44 PriorityHigh = 90
45 PriorityUrgent = 100
46)
47
48// FocusRequest represents a request to change focus to a specific pane
49type FocusRequest struct {
50 ID string // Unique request identifier
51 TargetNode *paneNode // Target pane to focus
52 Source FocusSource // Where the request originated
53 Priority int // Request priority (higher = more urgent)
54 Timestamp time.Time // When the request was created
55 Context map[string]interface{} // Additional metadata
56}
57
58// FocusTransition records a completed focus change for debugging
59type FocusTransition struct {
60 From *paneNode // Previous focus target
61 To *paneNode // New focus target
62 State FocusState // State during transition
63 Source FocusSource // Request source
64 Timestamp time.Time // When transition occurred
65 Success bool // Whether transition succeeded
66 Error error // Error if transition failed
67 Stack []byte // Stack trace for debugging
68}
69
70// RingBuffer provides a thread-safe circular buffer for focus history
71type RingBuffer[T any] struct {
72 buffer []T
73 head int
74 tail int
75 size int
76 cap int
77 mu sync.RWMutex
78}
79
80// NewRingBuffer creates a new ring buffer with the specified capacity
81func NewRingBuffer[T any](capacity int) *RingBuffer[T] {
82 return &RingBuffer[T]{
83 buffer: make([]T, capacity),
84 cap: capacity,
85 }
86}
87
88// Add inserts an item into the ring buffer
89func (rb *RingBuffer[T]) Add(item T) {
90 rb.mu.Lock()
91 defer rb.mu.Unlock()
92
93 rb.buffer[rb.head] = item
94 rb.head = (rb.head + 1) % rb.cap
95
96 if rb.size < rb.cap {
97 rb.size++
98 } else {
99 rb.tail = (rb.tail + 1) % rb.cap
100 }
101}
102
103// GetAll returns all items in the ring buffer in chronological order
104func (rb *RingBuffer[T]) GetAll() []T {
105 rb.mu.RLock()
106 defer rb.mu.RUnlock()
107
108 if rb.size == 0 {
109 return nil
110 }
111
112 result := make([]T, rb.size)
113 for i := 0; i < rb.size; i++ {
114 idx := (rb.tail + i) % rb.cap
115 result[i] = rb.buffer[idx]
116 }
117 return result
118}
119
120// FocusPriorityQueue implements a priority queue for focus requests
121type FocusPriorityQueue []*FocusRequest
122
123func (pq FocusPriorityQueue) Len() int { return len(pq) }
124
125func (pq FocusPriorityQueue) Less(i, j int) bool {
126 // Higher priority value = higher priority (processed first)
127 return pq[i].Priority > pq[j].Priority
128}
129
130func (pq FocusPriorityQueue) Swap(i, j int) {
131 pq[i], pq[j] = pq[j], pq[i]
132}
133
134func (pq *FocusPriorityQueue) Push(x interface{}) {
135 *pq = append(*pq, x.(*FocusRequest))
136}
137
138func (pq *FocusPriorityQueue) Pop() interface{} {
139 old := *pq
140 n := len(old)
141 item := old[n-1]
142 *pq = old[0 : n-1]
143 return item
144}
145
146// RequestDeduplicator prevents duplicate requests from flooding the queue
147type RequestDeduplicator struct {
148 mu sync.RWMutex
149 recentSigs map[string]time.Time
150 ttl time.Duration
151}
152
153// NewRequestDeduplicator creates a new request deduplicator
154func NewRequestDeduplicator(ttl time.Duration) *RequestDeduplicator {
155 return &RequestDeduplicator{
156 recentSigs: make(map[string]time.Time),
157 ttl: ttl,
158 }
159}
160
161// IsDuplicate checks if a request is a duplicate within the TTL window
162func (rd *RequestDeduplicator) IsDuplicate(req FocusRequest, isActiveTarget bool) bool {
163 rd.mu.Lock()
164 defer rd.mu.Unlock()
165
166 // Clean up expired signatures
167 now := time.Now()
168 for sig, timestamp := range rd.recentSigs {
169 if now.Sub(timestamp) > rd.ttl {
170 delete(rd.recentSigs, sig)
171 }
172 }
173
174 // Generate signature for this request
175 sig := fmt.Sprintf("%p:%s", req.TargetNode, req.Source)
176
177 // Always record latest timestamp so future checks use fresh window
178 // Prevent duplicates only when the target is already active; otherwise we
179 // want to process the request to move focus back to that pane.
180 if timestamp, exists := rd.recentSigs[sig]; exists {
181 if isActiveTarget && now.Sub(timestamp) <= rd.ttl {
182 rd.recentSigs[sig] = now
183 return true
184 }
185 }
186
187 rd.recentSigs[sig] = now
188 return false
189}
190
191// FocusStateMachine manages all focus operations through a centralized state machine
192type FocusStateMachine struct {
193 mu sync.RWMutex
194 wm *WorkspaceManager // Reference to workspace manager
195 currentState FocusState // Current state of the focus system
196 activePane *paneNode // Currently focused pane
197 requestQueue chan FocusRequest // Queue for serializing focus requests
198 priorityQueue *FocusPriorityQueue // Priority queue for request ordering
199 pendingRequests chan struct{} // Signal channel for pending requests
200 historyRing *RingBuffer[FocusTransition] // History of focus transitions
201 validators []FocusValidator // Request validators
202 settlingTimer *time.Timer // Timer for state settling
203 queueProcessor *sync.WaitGroup // Wait group for queue processor
204 shutdownChan chan struct{} // Shutdown signal
205 requestIDCounter uint64 // Counter for generating request IDs
206
207 // Request coalescing for rapid focus changes
208 lastRequestTime time.Time // Time of last request
209 coalescingWindow time.Duration // Window for coalescing requests
210 deduplicator *RequestDeduplicator // Request deduplication
211
212 // Performance metrics tracking
213 metrics FocusMetrics // Performance metrics
214 processTimes []time.Duration // Recent process times for averaging
215 maxProcessTimes int // Max number of process times to keep
216
217 // Debug configuration
218 debugEnabled bool // Enable focus debug logging
219 metricsEnabled bool // Enable metrics tracking
220 // Reconciliation loop prevention
221 lastReconcileTime time.Time // Last reconciliation timestamp
222 reconcileCount int // Recent reconciliation count
223 maxReconcileAttempts int // Max reconcile attempts per second
224}
225
226// FocusValidator validates focus requests before processing
227type FocusValidator func(request FocusRequest) error
228
229// Predefined errors
230var (
231 ErrFocusQueueFull = fmt.Errorf("focus request queue is full")
232 ErrInvalidTarget = fmt.Errorf("invalid focus target")
233 ErrInvalidStackOperation = fmt.Errorf("invalid stack operation")
234 ErrStateMachineShutdown = fmt.Errorf("focus state machine is shutting down")
235)
236
237// NewFocusStateMachine creates a new focus state machine
238func NewFocusStateMachine(wm *WorkspaceManager) *FocusStateMachine {
239 pq := &FocusPriorityQueue{}
240 heap.Init(pq)
241
242 fsm := &FocusStateMachine{
243 wm: wm,
244 currentState: StateInitializing,
245 requestQueue: make(chan FocusRequest, 100), // Buffered channel with capacity 100
246 priorityQueue: pq,
247 pendingRequests: make(chan struct{}, 1), // Signal for priority queue processing
248 historyRing: NewRingBuffer[FocusTransition](50), // Keep last 50 transitions
249 validators: make([]FocusValidator, 0),
250 queueProcessor: &sync.WaitGroup{},
251 shutdownChan: make(chan struct{}),
252 coalescingWindow: 50 * time.Millisecond, // 50ms coalescing window
253 deduplicator: NewRequestDeduplicator(200 * time.Millisecond), // 200ms dedup TTL
254 maxProcessTimes: 100, // Keep last 100 process times for averaging
255 // Debug flags will be set via ConfigureDebug()
256 debugEnabled: false,
257 metricsEnabled: false,
258 // Reconciliation loop prevention
259 maxReconcileAttempts: 3, // Max 3 reconciliation attempts per second
260 }
261
262 // Add default validators
263 fsm.addDefaultValidators()
264
265 return fsm
266}
267
268// ConfigureDebug configures debug settings for the focus state machine
269func (fsm *FocusStateMachine) ConfigureDebug(focusDebug, _ bool, metricsEnabled bool) {
270 fsm.mu.Lock()
271 defer fsm.mu.Unlock()
272
273 fsm.debugEnabled = focusDebug
274 fsm.metricsEnabled = metricsEnabled
275
276 if focusDebug {
277 log.Printf("[FSM] Focus debug logging enabled")
278 }
279 if metricsEnabled {
280 log.Printf("[FSM] Focus metrics tracking enabled")
281 }
282}
283
284// Initialize sets up the focus state machine and determines initial focus
285func (fsm *FocusStateMachine) Initialize() error {
286 fsm.mu.Lock()
287 defer fsm.mu.Unlock()
288
289 log.Printf("[FSM] Initializing focus state machine")
290 fsm.currentState = StateInitializing
291
292 // Determine initial focus
293 leaves := fsm.wm.collectLeaves()
294 if len(leaves) == 0 {
295 log.Printf("[FSM] No panes found during initialization")
296 fsm.currentState = StateIdle
297 return nil
298 }
299
300 initialPane := fsm.determineInitialFocus(leaves)
301 if initialPane == nil {
302 log.Printf("[FSM] Could not determine initial focus target")
303 fsm.currentState = StateIdle
304 return nil
305 }
306
307 // Phase 3: Apply initial focus and CSS
308 if err := fsm.applyInitialFocus(initialPane); err != nil {
309 log.Printf("[FSM] Failed to apply initial focus: %v", err)
310 fsm.currentState = StateIdle
311 return err
312 }
313
314 // Phase 4: Attach GTK focus controllers to all panes
315 fsm.attachGTKControllersToAllPanes()
316
317 // Phase 5: Start queue processor
318 fsm.queueProcessor.Add(1)
319 go fsm.processQueue()
320
321 // Phase 6: Start CSS verification loop
322 go fsm.startCSSVerificationLoop()
323
324 // Phase 7: Transition to idle state
325 fsm.currentState = StateIdle
326
327 log.Printf("[FSM] Focus state machine initialized successfully with pane %p", initialPane)
328 return nil
329}
330
331// Shutdown gracefully shuts down the focus state machine
332func (fsm *FocusStateMachine) Shutdown() {
333 fsm.mu.Lock()
334 defer fsm.mu.Unlock()
335
336 log.Printf("[FSM] Shutting down focus state machine")
337
338 // Signal shutdown
339 close(fsm.shutdownChan)
340
341 // Wait for queue processor to finish
342 fsm.queueProcessor.Wait()
343
344 // Clean up timers
345 if fsm.settlingTimer != nil {
346 fsm.settlingTimer.Stop()
347 }
348
349 log.Printf("[FSM] Focus state machine shutdown complete")
350}
351
352// RequestFocus queues a focus change request with priority and deduplication
353func (fsm *FocusStateMachine) RequestFocus(node *paneNode, source FocusSource) error {
354 if node == nil {
355 return ErrInvalidTarget
356 }
357
358 request := FocusRequest{
359 ID: fsm.generateRequestID(),
360 TargetNode: node,
361 Source: source,
362 Priority: fsm.calculatePriority(source),
363 Timestamp: time.Now(),
364 Context: make(map[string]interface{}),
365 }
366
367 // Track total requests
368 if fsm.metricsEnabled {
369 fsm.mu.Lock()
370 fsm.metrics.TotalRequests++
371 fsm.mu.Unlock()
372 }
373
374 activePane := fsm.GetActivePane()
375
376 // Check for duplicates (only dedupe when the request targets the currently
377 // active pane to avoid suppressing necessary focus transitions).
378 if fsm.deduplicator.IsDuplicate(request, activePane == node) {
379 if fsm.metricsEnabled {
380 fsm.mu.Lock()
381 fsm.metrics.DuplicateRequests++
382 fsm.mu.Unlock()
383 }
384 if fsm.debugEnabled {
385 log.Printf("[FSM] Duplicate request ignored: %s -> %p", request.Source, request.TargetNode)
386 }
387 return nil
388 }
389
390 // Check for coalescing opportunity
391 if fsm.shouldCoalesce(request) {
392 if fsm.metricsEnabled {
393 fsm.mu.Lock()
394 fsm.metrics.CoalescedRequests++
395 fsm.mu.Unlock()
396 }
397 if fsm.debugEnabled {
398 log.Printf("[FSM] Request coalesced: %s -> %p", request.Source, request.TargetNode)
399 }
400 return nil
401 }
402
403 // Add to priority queue instead of simple channel
404 fsm.mu.Lock()
405 heap.Push(fsm.priorityQueue, &request)
406 fsm.lastRequestTime = request.Timestamp
407
408 // Track queue depth metrics
409 currentDepth := fsm.priorityQueue.Len()
410 if currentDepth > fsm.metrics.MaxQueueDepth {
411 fsm.metrics.MaxQueueDepth = currentDepth
412 }
413 fsm.mu.Unlock()
414
415 // Signal that a request is pending
416 select {
417 case fsm.pendingRequests <- struct{}{}:
418 default: // Channel is already signaled
419 }
420
421 return nil
422}
423
424// GetActivePane returns the currently active pane
425func (fsm *FocusStateMachine) GetActivePane() *paneNode {
426 fsm.mu.RLock()
427 defer fsm.mu.RUnlock()
428 return fsm.activePane
429}
430
431// InvalidateActivePane clears the active pane if it matches the provided node.
432// This is used when panes are destroyed so the focus state machine doesn't keep
433// references to invalid widgets.
434func (fsm *FocusStateMachine) InvalidateActivePane(node *paneNode) {
435 if node == nil {
436 return
437 }
438
439 fsm.mu.Lock()
440 defer fsm.mu.Unlock()
441
442 if fsm.activePane != node {
443 return
444 }
445
446 log.Printf("[FSM] Active pane invalidated: %p", node)
447 fsm.activePane = nil
448
449 if fsm.wm != nil && fsm.wm.app != nil {
450 fsm.wm.app.activePane = nil
451 }
452
453 if fsm.currentState == StateFocused {
454 fsm.currentState = StateIdle
455 }
456}
457
458// GetCurrentState returns the current state of the focus system
459func (fsm *FocusStateMachine) GetCurrentState() FocusState {
460 fsm.mu.RLock()
461 defer fsm.mu.RUnlock()
462 return fsm.currentState
463}
464
465// AddValidator adds a focus request validator
466func (fsm *FocusStateMachine) AddValidator(validator FocusValidator) {
467 fsm.mu.Lock()
468 defer fsm.mu.Unlock()
469 fsm.validators = append(fsm.validators, validator)
470}
471
472// Private methods
473
474// generateRequestID creates a unique request identifier
475func (fsm *FocusStateMachine) generateRequestID() string {
476 fsm.requestIDCounter++
477 return fmt.Sprintf("req-%d-%d", time.Now().UnixNano(), fsm.requestIDCounter)
478}
479
480// calculatePriority determines request priority based on source
481func (fsm *FocusStateMachine) calculatePriority(source FocusSource) int {
482 switch source {
483 case SourceKeyboard:
484 return PriorityHigh
485 case SourceMouse:
486 return PriorityNormal
487 case SourceStackNav:
488 return PriorityHigh
489 case SourceSystem:
490 return PriorityUrgent
491 case SourceSplit, SourceClose:
492 return PriorityHigh
493 default:
494 return PriorityLow
495 }
496}
497
498// addDefaultValidators adds standard focus request validators
499func (fsm *FocusStateMachine) addDefaultValidators() {
500 // Validate target node exists and is a leaf
501 fsm.validators = append(fsm.validators, func(request FocusRequest) error {
502 if request.TargetNode == nil {
503 return ErrInvalidTarget
504 }
505 if !request.TargetNode.isLeaf {
506 return fmt.Errorf("target node is not a leaf pane")
507 }
508 if request.TargetNode.pane == nil || request.TargetNode.pane.webView == nil {
509 return fmt.Errorf("target node has no valid webview")
510 }
511 return nil
512 })
513
514 // Validate target node still exists in workspace
515 fsm.validators = append(fsm.validators, func(request FocusRequest) error {
516 if fsm.wm.viewToNode[request.TargetNode.pane.webView] != request.TargetNode {
517 return fmt.Errorf("target node no longer exists in workspace")
518 }
519 return nil
520 })
521}
522
523// validateRequest runs all validators on a focus request
524func (fsm *FocusStateMachine) validateRequest(request FocusRequest) error {
525 for _, validator := range fsm.validators {
526 if err := validator(request); err != nil {
527 return err
528 }
529 }
530 return nil
531}
532
533// determineInitialFocus selects the initial focus target based on priority rules
534func (fsm *FocusStateMachine) determineInitialFocus(leaves []*paneNode) *paneNode {
535 if len(leaves) == 0 {
536 return nil
537 }
538
539 // Priority 1: First pane with user content (not about:blank)
540 for _, leaf := range leaves {
541 if leaf.pane != nil && leaf.pane.webView != nil {
542 url := leaf.pane.webView.GetCurrentURL()
543 if url != "" && url != "about:blank" {
544 log.Printf("[FSM] Selected pane with content as initial focus: %s", url)
545 return leaf
546 }
547 }
548 }
549
550 // Priority 2: Top-left pane (geometrically)
551 if topLeft := fsm.findTopLeftPane(leaves); topLeft != nil {
552 log.Printf("[FSM] Selected top-left pane as initial focus")
553 return topLeft
554 }
555
556 // Priority 3: First pane in tree order
557 log.Printf("[FSM] Selected first pane as initial focus")
558 return leaves[0]
559}
560
561// findTopLeftPane finds the geometrically top-left pane
562func (fsm *FocusStateMachine) findTopLeftPane(leaves []*paneNode) *paneNode {
563 var bestPane *paneNode
564 var bestScore float64 = 1e9
565
566 for _, leaf := range leaves {
567 if leaf.container == nil {
568 continue
569 }
570
571 x, y, _, _ := webkit.WidgetGetAllocation(leaf.container)
572
573 // Score based on distance from top-left corner (0,0)
574 score := float64(x) + float64(y)
575 if score < bestScore {
576 bestScore = score
577 bestPane = leaf
578 }
579 }
580
581 return bestPane
582}
583
584// applyInitialFocus applies focus to the initial target without going through the queue
585func (fsm *FocusStateMachine) applyInitialFocus(node *paneNode) error {
586 if node == nil {
587 return ErrInvalidTarget
588 }
589
590 log.Printf("[FSM] Applying initial focus to pane %p", node)
591
592 // Apply GTK focus
593 if err := fsm.applyGTKFocus(node); err != nil {
594 return fmt.Errorf("failed to apply GTK focus: %w", err)
595 }
596
597 // Apply initial visual border
598 if fsm.wm != nil {
599 fsm.wm.applyActivePaneBorder(node)
600 }
601
602 // Update state
603 fsm.activePane = node
604
605 // Record transition
606 transition := FocusTransition{
607 From: nil,
608 To: node,
609 State: StateInitializing,
610 Source: SourceSystem,
611 Timestamp: time.Now(),
612 Success: true,
613 Stack: debug.Stack(),
614 }
615 fsm.historyRing.Add(transition)
616
617 // Notify workspace manager
618 if fsm.wm != nil && fsm.wm.app != nil && node.pane != nil {
619 fsm.wm.app.activePane = node.pane
620 }
621
622 // Notify JS runtime about focus state
623 if fsm.wm != nil {
624 fsm.wm.DispatchPaneFocusEvent(node, true)
625 }
626
627 return nil
628}
629
630// applyGTKFocus applies GTK focus to the specified pane
631func (fsm *FocusStateMachine) applyGTKFocus(node *paneNode) error {
632 if node.pane == nil || node.pane.webView == nil {
633 return fmt.Errorf("pane has no valid webview")
634 }
635
636 viewWidget := node.pane.webView.Widget()
637 if viewWidget == nil {
638 return fmt.Errorf("webview has no valid widget")
639 }
640
641 webkit.WidgetGrabFocus(viewWidget)
642 return nil
643}
644
645// processQueue processes focus requests from the priority queue
646func (fsm *FocusStateMachine) processQueue() {
647 defer fsm.queueProcessor.Done()
648
649 log.Printf("[FSM] Focus request queue processor started")
650
651 for {
652 select {
653 case <-fsm.pendingRequests:
654 // Process all pending requests from priority queue
655 fsm.processPendingRequests()
656 case request := <-fsm.requestQueue:
657 // Legacy channel for backward compatibility
658 fsm.handleFocusRequest(request)
659 case <-fsm.shutdownChan:
660 log.Printf("[FSM] Focus request queue processor shutting down")
661 return
662 }
663 }
664}
665
666// processPendingRequests processes all requests in the priority queue
667func (fsm *FocusStateMachine) processPendingRequests() {
668 for {
669 fsm.mu.Lock()
670 if fsm.priorityQueue.Len() == 0 {
671 fsm.mu.Unlock()
672 break
673 }
674
675 // Get highest priority request
676 request := heap.Pop(fsm.priorityQueue).(*FocusRequest)
677 fsm.mu.Unlock()
678
679 // Check if a higher priority request has arrived while processing
680 if fsm.hasHigherPriorityRequest(*request) {
681 // Requeue current request and process higher priority one
682 fsm.requeue(*request)
683 continue
684 }
685
686 fsm.handleFocusRequest(*request)
687 }
688}
689
690// handleFocusRequest processes a single focus request
691func (fsm *FocusStateMachine) handleFocusRequest(request FocusRequest) {
692 startTime := time.Now()
693
694 // Validate request
695 if err := fsm.validateRequest(request); err != nil {
696 fsm.mu.Lock()
697 fsm.metrics.FailedRequests++
698 fsm.mu.Unlock()
699 log.Printf("[FSM] Invalid focus request %s: %v", request.ID, err)
700 return
701 }
702
703 // Check if we're already focused on the target
704 fsm.mu.RLock()
705 currentActive := fsm.activePane
706 fsm.mu.RUnlock()
707
708 if currentActive == request.TargetNode {
709 log.Printf("[FSM] Focus request %s: already focused on target", request.ID)
710 fsm.recordProcessTime(time.Since(startTime))
711 return
712 }
713
714 log.Printf("[FSM] Processing focus request %s: %s -> %p",
715 request.ID, request.Source, request.TargetNode)
716
717 // GTK operations must run on main thread - always use IdleAdd for simplicity
718 var err error
719 done := make(chan struct{})
720 _ = webkit.IdleAdd(func() bool {
721 err = fsm.executeFocusChange(request)
722 close(done)
723 return false
724 })
725 <-done
726
727 if err != nil {
728 fsm.mu.Lock()
729 fsm.metrics.FailedRequests++
730 fsm.mu.Unlock()
731 log.Printf("[FSM] Focus request %s failed: %v", request.ID, err)
732 } else {
733 fsm.mu.Lock()
734 fsm.metrics.SuccessfulRequests++
735 fsm.mu.Unlock()
736 }
737
738 fsm.recordProcessTime(time.Since(startTime))
739}
740
741// executeFocusChange executes a focus change with proper state management
742func (fsm *FocusStateMachine) executeFocusChange(request FocusRequest) error {
743 fsm.mu.Lock()
744 defer fsm.mu.Unlock()
745
746 // Transition to processing state
747 oldState := fsm.currentState
748 fsm.currentState = StateTransitioning
749
750 oldPane := fsm.activePane
751 newPane := request.TargetNode
752
753 // Record transition start
754 transition := FocusTransition{
755 From: oldPane,
756 To: newPane,
757 Source: request.Source,
758 Timestamp: request.Timestamp,
759 Stack: debug.Stack(),
760 }
761
762 // Apply GTK focus
763 if err := fsm.applyGTKFocus(newPane); err != nil {
764 // Rollback state
765 fsm.currentState = oldState
766 transition.Success = false
767 transition.Error = err
768 fsm.historyRing.Add(transition)
769 return fmt.Errorf("failed to apply GTK focus: %w", err)
770 }
771
772 // Apply visual border changes
773 if fsm.wm != nil {
774 // Remove border from old pane
775 if oldPane != nil {
776 fsm.wm.removeActivePaneBorder(oldPane)
777 }
778 // Add border to new pane
779 fsm.wm.applyActivePaneBorder(newPane)
780 }
781
782 // Update state
783 fsm.activePane = newPane
784 fsm.currentState = StateFocused
785
786 // Record successful transition
787 transition.Success = true
788 fsm.historyRing.Add(transition)
789
790 // Notify workspace manager
791 if fsm.wm != nil && fsm.wm.app != nil && newPane.pane != nil {
792 fsm.wm.app.activePane = newPane.pane
793 }
794
795 // Notify JS runtimes about focus change
796 if fsm.wm != nil {
797 if oldPane != nil {
798 fsm.wm.DispatchPaneFocusEvent(oldPane, false)
799 }
800 if newPane != nil {
801 fsm.wm.DispatchPaneFocusEvent(newPane, true)
802 }
803 }
804
805 // Start settling timer
806 fsm.startSettlingTimer()
807
808 log.Printf("[FSM] Focus change completed: %p -> %p", oldPane, newPane)
809 return nil
810}
811
812// startSettlingTimer starts a timer to transition back to idle state
813func (fsm *FocusStateMachine) startSettlingTimer() {
814 if fsm.settlingTimer != nil {
815 fsm.settlingTimer.Stop()
816 }
817
818 fsm.settlingTimer = time.AfterFunc(50*time.Millisecond, func() {
819 fsm.mu.Lock()
820 if fsm.currentState == StateFocused {
821 fsm.currentState = StateIdle
822 }
823 fsm.mu.Unlock()
824 })
825}
826
827// startCSSVerificationLoop starts the CSS verification background loop
828func (fsm *FocusStateMachine) startCSSVerificationLoop() {
829 ticker := time.NewTicker(100 * time.Millisecond)
830
831 go func() {
832 defer ticker.Stop()
833
834 for {
835 select {
836 case <-ticker.C:
837 fsm.verifyCSSState()
838 case <-fsm.shutdownChan:
839 return
840 }
841 }
842 }()
843}
844
845// verifyCSSState checks and corrects CSS class inconsistencies
846func (fsm *FocusStateMachine) verifyCSSState() {
847 fsm.mu.RLock()
848 activePane := fsm.activePane
849 fsm.mu.RUnlock()
850
851 if activePane == nil || fsm.wm == nil {
852 return
853 }
854
855 var issues []string
856
857 if len(issues) > 0 {
858 // Prevent infinite reconciliation loops
859 fsm.mu.Lock()
860 now := time.Now()
861 if now.Sub(fsm.lastReconcileTime) < time.Second {
862 fsm.reconcileCount++
863 } else {
864 fsm.reconcileCount = 1
865 fsm.lastReconcileTime = now
866 }
867
868 if fsm.reconcileCount > fsm.maxReconcileAttempts {
869 log.Printf("[FSM] WARNING: Reconciliation loop detected, skipping (attempt %d)", fsm.reconcileCount)
870 fsm.mu.Unlock()
871 return
872 }
873 fsm.mu.Unlock()
874
875 log.Printf("[FSM] CSS inconsistencies found: %v", issues)
876 log.Printf("[FSM] Triggering CSS reconciliation (attempt %d)", fsm.reconcileCount)
877
878 // Track reconciliation in metrics
879 fsm.mu.Lock()
880 fsm.metrics.ReconciliationCount++
881 fsm.mu.Unlock()
882 }
883}
884
885// shouldCoalesce checks if a request should be coalesced with recent requests
886func (fsm *FocusStateMachine) shouldCoalesce(request FocusRequest) bool {
887 fsm.mu.RLock()
888 defer fsm.mu.RUnlock()
889
890 // Only coalesce if we have a recent request and it's to the same target
891 if time.Since(fsm.lastRequestTime) > fsm.coalescingWindow {
892 return false
893 }
894
895 // Check if there's already a pending request to the same target
896 for i := 0; i < fsm.priorityQueue.Len(); i++ {
897 pending := (*fsm.priorityQueue)[i]
898 if pending.TargetNode == request.TargetNode {
899 log.Printf("[FSM] Coalescing request to same target: %p", request.TargetNode)
900 return true
901 }
902 }
903
904 return false
905}
906
907// hasHigherPriorityRequest checks if there's a higher priority request in the queue
908func (fsm *FocusStateMachine) hasHigherPriorityRequest(current FocusRequest) bool {
909 fsm.mu.RLock()
910 defer fsm.mu.RUnlock()
911
912 if fsm.priorityQueue.Len() == 0 {
913 return false
914 }
915
916 // Check the highest priority request (at top of heap)
917 highest := (*fsm.priorityQueue)[0]
918 return highest.Priority > current.Priority
919}
920
921// requeue adds a request back to the priority queue
922func (fsm *FocusStateMachine) requeue(request FocusRequest) {
923 fsm.mu.Lock()
924 defer fsm.mu.Unlock()
925
926 heap.Push(fsm.priorityQueue, &request)
927 log.Printf("[FSM] Requeued request due to higher priority: %s", request.ID)
928
929 // Signal that a request is pending
930 select {
931 case fsm.pendingRequests <- struct{}{}:
932 default: // Channel is already signaled
933 }
934}
935
936// attachGTKController attaches a GTK focus controller to a pane node
937func (fsm *FocusStateMachine) attachGTKController(node *paneNode) {
938 if node == nil || node.pane == nil || node.pane.webView == nil {
939 return
940 }
941
942 if node.focusControllerToken != 0 {
943 log.Printf("[FSM] Controller already attached to pane %p", node)
944 return
945 }
946
947 widget := node.pane.webView.Widget()
948 if widget == nil {
949 return
950 }
951
952 // Create focus controller for GTK4
953 controller := gtk.NewEventControllerFocus()
954
955 // Set propagation phase to TARGET only to avoid duplicate events from parent widgets
956 // GTK_PHASE_TARGET means we only get events for this specific widget, not bubbled events
957 controller.SetPropagationPhase(gtk.PhaseTarget)
958
959 // Connect focus enter/leave callbacks with timestamp-based deduplication
960 controller.ConnectEnter(func() {
961 // Deduplicate: WebKitGTK nested widgets can fire multiple events in same millisecond
962 now := time.Now().UnixMilli()
963 if node.lastFocusEnterTime == now {
964 return // Duplicate event in same millisecond
965 }
966 node.lastFocusEnterTime = now
967
968 log.Printf("[FSM] GTK focus enter: %p", node)
969 // Don't automatically change focus on GTK enter - let user interactions drive this
970 // This prevents infinite loops with our own focus changes
971 })
972
973 controller.ConnectLeave(func() {
974 // Deduplicate: WebKitGTK nested widgets can fire multiple events in same millisecond
975 now := time.Now().UnixMilli()
976 if node.lastFocusLeaveTime == now {
977 return // Duplicate event in same millisecond
978 }
979 node.lastFocusLeaveTime = now
980
981 log.Printf("[FSM] GTK focus leave: %p", node)
982 // Similarly, don't react to GTK leave events automatically
983 })
984
985 // Add controller to widget
986 webkit.WidgetAddController(widget, controller)
987
988 // Store controller pointer as token for later removal
989 node.focusControllerToken = uintptr(controller.Native())
990 if node.focusControllerToken != 0 {
991 log.Printf("[FSM] Attached GTK focus controller to pane %p with token %d", node, node.focusControllerToken)
992 }
993}
994
995// detachGTKController removes a GTK focus controller from a pane node
996func (fsm *FocusStateMachine) detachGTKController(node *paneNode, token uintptr) {
997 if node == nil || node.pane == nil || node.pane.webView == nil || token == 0 {
998 return
999 }
1000
1001 if node.focusControllerToken != token {
1002 log.Printf("[FSM] Token mismatch, skipping detach for pane %p", node)
1003 return
1004 }
1005
1006 node.focusControllerToken = 0
1007
1008 // Note: In GTK4, controllers are automatically removed when widget is destroyed
1009 // We just need to clear our token reference
1010 log.Printf("[FSM] Detached GTK focus controller from pane %p", node)
1011}
1012
1013// attachGTKControllersToAllPanes attaches focus controllers to all existing panes
1014func (fsm *FocusStateMachine) attachGTKControllersToAllPanes() {
1015 if fsm.wm == nil {
1016 return
1017 }
1018
1019 leaves := fsm.wm.collectLeaves()
1020 for _, leaf := range leaves {
1021 fsm.attachGTKController(leaf)
1022 }
1023
1024 log.Printf("[FSM] Attached GTK focus controllers to %d panes", len(leaves))
1025}
1026
1027// FocusDebugInfo contains comprehensive debug information about the focus system
1028type FocusDebugInfo struct {
1029 CurrentState FocusState `json:"current_state"`
1030 ActivePane *paneNode `json:"active_pane,omitempty"`
1031 QueueLength int `json:"queue_length"`
1032 PriorityQueueSize int `json:"priority_queue_size"`
1033 History []FocusTransition `json:"history"`
1034 CSSState map[string]interface{} `json:"css_state"`
1035 ValidationErrors []string `json:"validation_errors"`
1036 Metrics FocusMetrics `json:"metrics"`
1037 RecentRequests []string `json:"recent_requests"`
1038}
1039
1040// FocusMetrics tracks performance metrics for the focus system
1041type FocusMetrics struct {
1042 TotalRequests uint64 `json:"total_requests"`
1043 SuccessfulRequests uint64 `json:"successful_requests"`
1044 FailedRequests uint64 `json:"failed_requests"`
1045 CoalescedRequests uint64 `json:"coalesced_requests"`
1046 DuplicateRequests uint64 `json:"duplicate_requests"`
1047 AverageProcessTime time.Duration `json:"average_process_time"`
1048 MaxQueueDepth int `json:"max_queue_depth"`
1049 ReconciliationCount uint64 `json:"reconciliation_count"`
1050}
1051
1052// DumpState returns comprehensive debug information about the focus system
1053func (fsm *FocusStateMachine) DumpState() FocusDebugInfo {
1054 fsm.mu.RLock()
1055 defer fsm.mu.RUnlock()
1056
1057 info := FocusDebugInfo{
1058 CurrentState: fsm.currentState,
1059 ActivePane: fsm.activePane,
1060 QueueLength: len(fsm.requestQueue),
1061 PriorityQueueSize: fsm.priorityQueue.Len(),
1062 History: fsm.historyRing.GetAll(),
1063 ValidationErrors: fsm.getValidationErrors(),
1064 RecentRequests: fsm.getRecentRequestSignatures(),
1065 Metrics: fsm.GetMetrics(),
1066 }
1067
1068 return info
1069}
1070
1071// ForceValidation manually triggers CSS consistency check and validation
1072func (fsm *FocusStateMachine) ForceValidation() []string {
1073 log.Printf("[FSM] Force validation requested")
1074
1075 // Trigger CSS verification
1076 fsm.verifyCSSState()
1077
1078 // Run all validators on current state
1079 var issues []string
1080
1081 fsm.mu.RLock()
1082 activePane := fsm.activePane
1083 fsm.mu.RUnlock()
1084
1085 if activePane != nil {
1086 dummyRequest := FocusRequest{
1087 ID: "validation-check",
1088 TargetNode: activePane,
1089 Source: SourceSystem,
1090 Priority: PriorityLow,
1091 Timestamp: time.Now(),
1092 }
1093
1094 if err := fsm.validateRequest(dummyRequest); err != nil {
1095 issues = append(issues, fmt.Sprintf("Active pane validation failed: %v", err))
1096 }
1097 }
1098
1099 log.Printf("[FSM] Force validation completed: %d issues found", len(issues))
1100 return issues
1101}
1102
1103// GetQueueStatus returns detailed information about pending requests
1104func (fsm *FocusStateMachine) GetQueueStatus() map[string]interface{} {
1105 fsm.mu.RLock()
1106 defer fsm.mu.RUnlock()
1107
1108 status := map[string]interface{}{
1109 "channel_queue_length": len(fsm.requestQueue),
1110 "priority_queue_length": fsm.priorityQueue.Len(),
1111 "channel_queue_cap": cap(fsm.requestQueue),
1112 "processing_state": fsm.currentState,
1113 "last_request_time": fsm.lastRequestTime,
1114 "coalescing_window": fsm.coalescingWindow,
1115 }
1116
1117 // Add priority queue details
1118 if fsm.priorityQueue.Len() > 0 {
1119 priorities := make([]int, 0, fsm.priorityQueue.Len())
1120 targets := make([]string, 0, fsm.priorityQueue.Len())
1121
1122 for i := 0; i < fsm.priorityQueue.Len(); i++ {
1123 req := (*fsm.priorityQueue)[i]
1124 priorities = append(priorities, req.Priority)
1125 targets = append(targets, fmt.Sprintf("%p", req.TargetNode))
1126 }
1127
1128 status["pending_priorities"] = priorities
1129 status["pending_targets"] = targets
1130 }
1131
1132 return status
1133}
1134
1135// ClearHistory clears the focus transition history (useful for debugging sessions)
1136func (fsm *FocusStateMachine) ClearHistory() {
1137 fsm.mu.Lock()
1138 defer fsm.mu.Unlock()
1139
1140 fsm.historyRing = NewRingBuffer[FocusTransition](50)
1141 log.Printf("[FSM] Focus transition history cleared")
1142}
1143
1144// GetMetrics returns performance metrics for the focus system
1145func (fsm *FocusStateMachine) GetMetrics() FocusMetrics {
1146 fsm.mu.RLock()
1147 defer fsm.mu.RUnlock()
1148
1149 // Copy current metrics with calculated average
1150 metrics := fsm.metrics
1151 metrics.AverageProcessTime = fsm.calculateAverageProcessTime()
1152
1153 return metrics
1154}
1155
1156// recordProcessTime adds a process time to the metrics and maintains the sliding window
1157func (fsm *FocusStateMachine) recordProcessTime(duration time.Duration) {
1158 fsm.mu.Lock()
1159 defer fsm.mu.Unlock()
1160
1161 // Add to the list
1162 fsm.processTimes = append(fsm.processTimes, duration)
1163
1164 // Maintain sliding window
1165 if len(fsm.processTimes) > fsm.maxProcessTimes {
1166 fsm.processTimes = fsm.processTimes[1:]
1167 }
1168}
1169
1170// calculateAverageProcessTime calculates the average of recent process times
1171func (fsm *FocusStateMachine) calculateAverageProcessTime() time.Duration {
1172 if len(fsm.processTimes) == 0 {
1173 return 0
1174 }
1175
1176 var total time.Duration
1177 for _, t := range fsm.processTimes {
1178 total += t
1179 }
1180
1181 return total / time.Duration(len(fsm.processTimes))
1182}
1183
1184// getValidationErrors checks for various validation issues
1185func (fsm *FocusStateMachine) getValidationErrors() []string {
1186 var errors []string
1187
1188 // Check if we have an active pane but it's not valid
1189 if fsm.activePane != nil {
1190 if !fsm.activePane.isLeaf {
1191 errors = append(errors, "active pane is not a leaf node")
1192 }
1193 if fsm.activePane.pane == nil || fsm.activePane.pane.webView == nil {
1194 errors = append(errors, "active pane has no valid webview")
1195 }
1196 if fsm.wm != nil && fsm.wm.viewToNode[fsm.activePane.pane.webView] != fsm.activePane {
1197 errors = append(errors, "active pane not found in workspace view map")
1198 }
1199 }
1200
1201 // Check for orphaned requests in queue
1202 if fsm.wm != nil {
1203 validNodes := make(map[*paneNode]bool)
1204 for _, node := range fsm.wm.viewToNode {
1205 validNodes[node] = true
1206 }
1207
1208 for i := 0; i < fsm.priorityQueue.Len(); i++ {
1209 req := (*fsm.priorityQueue)[i]
1210 if !validNodes[req.TargetNode] {
1211 errors = append(errors, fmt.Sprintf("orphaned request for invalid node: %s", req.ID))
1212 }
1213 }
1214 }
1215
1216 return errors
1217}
1218
1219// getRecentRequestSignatures returns recent request signatures for debugging
1220func (fsm *FocusStateMachine) getRecentRequestSignatures() []string {
1221 if fsm.deduplicator == nil {
1222 return nil
1223 }
1224
1225 fsm.deduplicator.mu.RLock()
1226 defer fsm.deduplicator.mu.RUnlock()
1227
1228 sigs := make([]string, 0, len(fsm.deduplicator.recentSigs))
1229 for sig, timestamp := range fsm.deduplicator.recentSigs {
1230 sigs = append(sigs, fmt.Sprintf("%s (age: %v)", sig, time.Since(timestamp)))
1231 }
1232
1233 return sigs
1234}