Files

162 lines
4.9 KiB
Go

// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package input
import (
"strings"
"sync"
"time"
)
// Intent records only the kind of input, never the text the user typed.
type Intent uint8
const (
NoIntent Intent = iota
TypingIntent
LineIntent
WordIntent
CharacterIntent
DeleteIntent
OtherIntent
)
type Activity struct {
// InputSequence counts every key press without expiring with presentation
// intent. Virtual navigation uses it to distinguish
// native input from delayed application events, even after a quiet period.
InputSequence uint64
// Unlike presentation intent, this counter never expires. Deferred titles
// may be waiting for application settings after ordinary input has aged out.
TitleInterruptionSequence uint64
PlainCharacterArrow bool
PlainEnter bool
Selecting bool
Intent Intent
Sequence uint64
Clipboard ClipboardAction
}
type ClipboardAction uint8
const (
NoClipboardAction ClipboardAction = iota
CopyAction
CutAction
PasteAction
)
type IntentTracker struct {
titleInterruptionSequence uint64
sequence uint64
mutex sync.Mutex
held map[string]bool
intent Intent
clipboard ClipboardAction
plainCharacterArrow bool
plainEnter bool
selecting bool
at time.Time
}
func (tracker *IntentTracker) Observe(event Event) {
tracker.mutex.Lock()
defer tracker.mutex.Unlock()
if event.Pressed {
tracker.sequence++
}
if ShouldInterruptTitle(event) {
tracker.titleInterruptionSequence++
}
if tracker.held == nil {
tracker.held = make(map[string]bool)
}
if event.Kind == KeyModifier || event.Name == "Super_L" || event.Name == "Super_R" {
tracker.held[event.Name] = event.Pressed
if event.Pressed {
tracker.intent, tracker.at = OtherIntent, time.Now()
tracker.clipboard = NoClipboardAction
tracker.selecting = false
tracker.plainCharacterArrow = false
tracker.plainEnter = false
}
return
}
if !event.Pressed {
return
}
ctrl := tracker.held["Control_L"] || tracker.held["Control_R"]
shift := tracker.held["Shift_L"] || tracker.held["Shift_R"]
alt := tracker.held["Alt_L"] || tracker.held["Alt_R"] || tracker.held["Meta_L"] || tracker.held["Meta_R"]
intent := OtherIntent
tracker.clipboard = NoClipboardAction
name := strings.TrimPrefix(event.Name, "KP_")
if !alt {
if ctrl && !shift {
switch strings.ToLower(name) {
case "c":
tracker.clipboard = CopyAction
case "x":
tracker.clipboard = CutAction
case "v":
tracker.clipboard = PasteAction
}
}
switch name {
case "Up", "Down", "Page_Up", "Page_Down", "Prior", "Next":
intent = LineIntent
case "Left", "Right":
intent = CharacterIntent
if ctrl {
intent = WordIntent
}
case "Home", "End":
intent = CharacterIntent
if ctrl {
intent = LineIntent
}
case "Insert", "Caps_Lock", "Num_Lock", "Scroll_Lock", "Super_L", "Super_R":
intent = OtherIntent
case "BackSpace", "Delete":
intent = DeleteIntent
default:
if !ctrl && (event.Name != "" && event.Kind == KeyUnknown) {
intent = TypingIntent
}
}
}
tracker.plainCharacterArrow = (name == "Left" || name == "Right") && !ctrl && !shift && !alt &&
!tracker.held["Super_L"] && !tracker.held["Super_R"] &&
!tracker.held["ISO_Level3_Shift"] && !tracker.held["ISO_Level5_Shift"]
tracker.plainEnter = (name == "Return" || name == "Enter") && !ctrl && !shift && !alt &&
!tracker.held["Super_L"] && !tracker.held["Super_R"] &&
!tracker.held["ISO_Level3_Shift"] && !tracker.held["ISO_Level5_Shift"]
tracker.selecting = shift && (intent == CharacterIntent || intent == WordIntent || intent == LineIntent)
tracker.intent, tracker.at = intent, time.Now()
}
func (tracker *IntentTracker) RecentIntent() Intent { return tracker.RecentInput().Intent }
// Reset retires held modifiers and presentation intent across secure input.
func (tracker *IntentTracker) Reset() {
tracker.mutex.Lock()
defer tracker.mutex.Unlock()
clear(tracker.held)
tracker.intent, tracker.at = OtherIntent, time.Time{}
tracker.clipboard, tracker.selecting = NoClipboardAction, false
tracker.plainCharacterArrow = false
tracker.plainEnter = false
}
func (tracker *IntentTracker) RecentInput() Activity {
tracker.mutex.Lock()
defer tracker.mutex.Unlock()
if time.Since(tracker.at) > time.Second {
return Activity{InputSequence: tracker.sequence, TitleInterruptionSequence: tracker.titleInterruptionSequence}
}
return Activity{PlainCharacterArrow: tracker.plainCharacterArrow, PlainEnter: tracker.plainEnter, InputSequence: tracker.sequence, Intent: tracker.intent, Sequence: tracker.sequence, Clipboard: tracker.clipboard, Selecting: tracker.selecting,
TitleInterruptionSequence: tracker.titleInterruptionSequence}
}