diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-10 23:04:59 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-10 23:04:59 +0200 |
| commit | 5f246f7af40ff9875274624b7f392eff22313e77 (patch) | |
| tree | 2e1be54a6d7f6fb616b1c24533dc6db170c80c63 /internal/tui/keys_normalize.go | |
| parent | 27798b61d6ac4b14ea5129ac28131302a6c5cb30 (diff) | |
tui: split key normalization and help rendering (task 424)
Diffstat (limited to 'internal/tui/keys_normalize.go')
| -rw-r--r-- | internal/tui/keys_normalize.go | 97 |
1 files changed, 97 insertions, 0 deletions
diff --git a/internal/tui/keys_normalize.go b/internal/tui/keys_normalize.go new file mode 100644 index 0000000..173847e --- /dev/null +++ b/internal/tui/keys_normalize.go @@ -0,0 +1,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.lastKeyEventWasPress && keyID != "" && keyID == m.lastKeyEventID && time.Since(m.lastKeyEventAt) <= 500*time.Millisecond { + // Some terminals emit both press+release; avoid handling release as a duplicate. + m.lastKeyEventWasPress = 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.suppressPressKeyID == "" { + return false + } + if time.Now().After(m.suppressPressUntil) { + m.clearPressSuppression() + return false + } + if keyID == "" || keyID != m.suppressPressKeyID { + 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.suppressPressKeyID = keyID + m.suppressPressUntil = time.Now().Add(60 * time.Millisecond) +} + +func (m *Model) clearPressSuppression() { + m.suppressPressKeyID = "" + m.suppressPressUntil = time.Time{} +} + +func (m *Model) recordKeyEvent(msg tea.KeyPressMsg, wasPress bool) { + m.lastKeyEventID = keyEventID(msg) + m.lastKeyEventAt = time.Now() + m.lastKeyEventWasPress = 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 == " " +} |
