summaryrefslogtreecommitdiff
path: root/internal/tui/keys_normalize.go
blob: 777749bc584283ad4a81cb02b0028e0791c7cec5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
package tui

import (
	"fmt"
	"time"

	tea "charm.land/bubbletea/v2"
)

func (m *Model) normalizeKeyEvent(msg tea.Msg) (tea.Msg, bool) {
	switch keyMsg := msg.(type) {
	case tea.KeyPressMsg:
		keyID := keyEventID(keyMsg)
		if m.shouldSuppressPress(keyID) {
			return nil, false
		}
		m.recordKeyEvent(keyMsg, true)
		return keyMsg, true
	case tea.KeyReleaseMsg:
		pressMsg := tea.KeyPressMsg(keyMsg)
		keyID := keyEventID(pressMsg)
		if m.kb.lastEventWasPress && keyID != "" && keyID == m.kb.lastEventID && time.Since(m.kb.lastEventAt) <= 500*time.Millisecond {
			// Some terminals emit both press+release; avoid handling release as a duplicate.
			m.kb.lastEventWasPress = false
			return nil, false
		}
		if !releaseHasIdentity(pressMsg) {
			// Ignore release messages that don't carry enough identity information.
			// Some terminals emit these before a usable press event.
			return nil, false
		}
		// Fallback: treat release as press for terminals that only emit release events.
		if shouldSuppressMatchingPressAfterRelease(pressMsg) {
			m.armPressSuppression(keyID)
		}
		m.recordKeyEvent(pressMsg, false)
		return pressMsg, true
	default:
		return msg, true
	}
}

func (m *Model) shouldSuppressPress(keyID string) bool {
	if m.kb.suppressID == "" {
		return false
	}
	if time.Now().After(m.kb.suppressUntil) {
		m.clearPressSuppression()
		return false
	}
	if keyID == "" || keyID != m.kb.suppressID {
		return false
	}
	m.clearPressSuppression()
	return true
}

func (m *Model) armPressSuppression(keyID string) {
	if keyID == "" {
		return
	}
	// Keep this short so fast repeated key presses still work naturally.
	m.kb.suppressID = keyID
	m.kb.suppressUntil = time.Now().Add(60 * time.Millisecond)
}

func (m *Model) clearPressSuppression() {
	m.kb.suppressID = ""
	m.kb.suppressUntil = time.Time{}
}

func (m *Model) recordKeyEvent(msg tea.KeyPressMsg, wasPress bool) {
	m.kb.lastEventID = keyEventID(msg)
	m.kb.lastEventAt = time.Now()
	m.kb.lastEventWasPress = wasPress
}

func keyEventID(msg tea.KeyPressMsg) string {
	return fmt.Sprintf("code:%d/mod:%d/key:%q/text:%q", msg.Code, msg.Mod, msg.String(), msg.Text)
}

func releaseHasIdentity(msg tea.KeyPressMsg) bool {
	if msg.Text != "" {
		return true
	}
	keyStr := msg.String()
	if keyStr != "" && keyStr != "\x00" {
		return true
	}
	// Some terminals emit release-only space events without text identity.
	return msg.Code == tea.KeySpace
}

func shouldSuppressMatchingPressAfterRelease(msg tea.KeyPressMsg) bool {
	keyStr := msg.String()
	return msg.Code == tea.KeySpace || keyStr == " " || keyStr == "space" || msg.Text == " "
}