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