summaryrefslogtreecommitdiff
path: root/internal/tui/tui.go
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-06 13:36:51 +0200
committerPaul Buetow <paul@buetow.org>2026-03-06 13:36:51 +0200
commitef12ce837176bd21deb455eb50a6c839af02b510 (patch)
treec262ceeda0b419236a4b0b1826df8eb5e418b852 /internal/tui/tui.go
parent10c5d48413afaef88626419d8c4bf9fbf6f1c902 (diff)
Add live flamegraph test modes and dynamic synthetic live feed
Diffstat (limited to 'internal/tui/tui.go')
-rw-r--r--internal/tui/tui.go106
1 files changed, 106 insertions, 0 deletions
diff --git a/internal/tui/tui.go b/internal/tui/tui.go
index 7918c0f..0381784 100644
--- a/internal/tui/tui.go
+++ b/internal/tui/tui.go
@@ -166,6 +166,16 @@ func RunWithTraceStarter(starter TraceStarter) error {
return err
}
+// RunTestFlamesWithTraceStarter starts the TUI directly on dashboard/flame view
+// with a synthetic static flamegraph source.
+func RunTestFlamesWithTraceStarter(starter TraceStarter) error {
+ cfg := flags.Get()
+ model := newModelWithRuntimeConfig(1, 1, cfg.TUIExportEnable, starter)
+ program := tea.NewProgram(model)
+ _, err := program.Run()
+ return err
+}
+
// Model is the top-level Bubble Tea model that routes between PID picker and dashboard.
type Model struct {
screen Screen
@@ -195,6 +205,15 @@ type Model struct {
keyboardEnhancements tea.KeyboardEnhancementsMsg
keyboardEnhancementsKnown bool
+
+ lastKeyEventID string
+ lastKeyEventAt time.Time
+ lastKeyEventWasPress bool
+ // Some terminals emit release+press for a single physical key event.
+ // When we fallback-handle a release as a press, suppress the immediate
+ // matching press to avoid double-handling.
+ suppressPressKeyID string
+ suppressPressUntil time.Time
}
// NewModel creates the top-level TUI model.
@@ -269,6 +288,12 @@ func initialWindowSizeCmd() tea.Cmd {
// Update routes messages, transitions screens, and manages tracing startup state.
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ normalizedMsg, ok := m.normalizeKeyEvent(msg)
+ if !ok {
+ return m, nil
+ }
+ msg = normalizedMsg
+
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width = msg.Width
@@ -382,6 +407,87 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m.updateActiveModel(msg)
}
+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.
+ 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", msg.Code, msg.Mod)
+}
+
+func releaseHasIdentity(msg tea.KeyPressMsg) bool {
+ if msg.Code != 0 {
+ return true
+ }
+ if msg.Text != "" {
+ return true
+ }
+ keyStr := msg.String()
+ return keyStr != "" && keyStr != "\x00"
+}
+
func (m Model) updateActiveModel(msg tea.Msg) (tea.Model, tea.Cmd) {
switch m.screen {
case ScreenPIDPicker: