summaryrefslogtreecommitdiff
path: root/internal/tui/pidpicker
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-06 17:32:24 +0200
committerPaul Buetow <paul@buetow.org>2026-03-06 17:32:24 +0200
commit1561987330cb898f5ff64383a9c78e7e6559f118 (patch)
tree69a823e8f98dce572566c97e6879c11c9d591bda /internal/tui/pidpicker
parent96225fb6159212a8851043a08d781aba721b4e78 (diff)
parent110a193e04b81abb8d8e159abd73f9f6ed1acd7e (diff)
Merge branch 'feat/bubbletea-v2-migration'
Diffstat (limited to 'internal/tui/pidpicker')
-rw-r--r--internal/tui/pidpicker/doc.go2
-rw-r--r--internal/tui/pidpicker/model.go59
-rw-r--r--internal/tui/pidpicker/model_test.go29
3 files changed, 65 insertions, 25 deletions
diff --git a/internal/tui/pidpicker/doc.go b/internal/tui/pidpicker/doc.go
new file mode 100644
index 0000000..5f863c0
--- /dev/null
+++ b/internal/tui/pidpicker/doc.go
@@ -0,0 +1,2 @@
+// Package pidpicker implements PID and TID selection workflows before tracing starts.
+package pidpicker
diff --git a/internal/tui/pidpicker/model.go b/internal/tui/pidpicker/model.go
index 73f21ae..f4bb414 100644
--- a/internal/tui/pidpicker/model.go
+++ b/internal/tui/pidpicker/model.go
@@ -2,14 +2,15 @@ package pidpicker
import (
"fmt"
+ "strings"
+
common "ior/internal/tui/common"
"ior/internal/tui/messages"
- "strings"
- "github.com/charmbracelet/bubbles/key"
- "github.com/charmbracelet/bubbles/textinput"
- tea "github.com/charmbracelet/bubbletea"
- "github.com/charmbracelet/lipgloss"
+ "charm.land/bubbles/v2/key"
+ "charm.land/bubbles/v2/textinput"
+ tea "charm.land/bubbletea/v2"
+ "charm.land/lipgloss/v2"
)
const allPIDsLabel = "All PIDs"
@@ -50,6 +51,14 @@ var (
errorStyle = common.ErrorStyle
)
+func syncPickerStyles() {
+ screenStyle = common.ScreenStyle
+ headerStyle = common.HeaderStyle
+ helpBarStyle = common.HelpBarStyle
+ highlightStyle = common.HighlightStyle
+ errorStyle = common.ErrorStyle
+}
+
type processesLoadedMsg struct {
processes []ProcessInfo
err error
@@ -67,6 +76,7 @@ type Model struct {
height int
keys KeyMap
lastErr error
+ isDark bool
}
// New creates a PID picker model with default shared key bindings.
@@ -81,12 +91,14 @@ func NewWithKeys(keys KeyMap) Model {
// NewPIDWithKeys creates a PID picker model with the provided key bindings.
func NewPIDWithKeys(keys KeyMap) Model {
+ syncPickerStyles()
input := textinput.New()
input.Prompt = "Filter: "
input.Placeholder = "pid, comm, or cmdline"
input.Focus()
input.CharLimit = 0
- input.Width = 40
+ input.SetWidth(40)
+ input.SetStyles(textinput.DefaultStyles(true))
return Model{
input: input,
@@ -94,6 +106,7 @@ func NewPIDWithKeys(keys KeyMap) Model {
filtered: []ProcessInfo{},
mode: PickerModePID,
targetPID: -1,
+ isDark: true,
}
}
@@ -117,14 +130,18 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
- m.input.Width = clamp(msg.Width-16, 10, 100)
+ inputWidth := msg.Width - 16
+ if inputWidth < 10 {
+ inputWidth = 10
+ }
+ m.input.SetWidth(inputWidth)
return m, nil
case processesLoadedMsg:
m.processes = msg.processes
m.lastErr = msg.err
m.applyFilter()
return m, nil
- case tea.KeyMsg:
+ case tea.KeyPressMsg:
return m.updateKey(msg)
}
@@ -134,21 +151,21 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, cmd
}
-func (m Model) updateKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
+func (m Model) updateKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
switch {
case key.Matches(msg, m.keys.Esc):
return m, tea.Quit
- case msg.Type == tea.KeyCtrlR:
+ case msg.Key().Mod&tea.ModCtrl != 0 && (msg.Key().Code == 'r' || msg.Key().Code == 'R'):
return m, m.scanCmd()
case key.Matches(msg, m.keys.Enter):
return m, m.emitSelection()
- case msg.Type == tea.KeyUp:
+ case msg.Key().Code == tea.KeyUp:
if m.selectedIndex > 0 {
m.selectedIndex--
}
m.input.Blur()
return m, nil
- case msg.Type == tea.KeyDown:
+ case msg.Key().Code == tea.KeyDown:
maxIndex := len(m.filtered)
if m.selectedIndex < maxIndex {
m.selectedIndex++
@@ -157,7 +174,7 @@ func (m Model) updateKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
return m, nil
}
- if msg.Type == tea.KeyRunes && !m.input.Focused() {
+ if msg.Key().Text != "" && !m.input.Focused() {
if key.Matches(msg, m.keys.Refresh) {
return m, m.scanCmd()
}
@@ -240,7 +257,7 @@ func cloneProcesses(in []ProcessInfo) []ProcessInfo {
}
// View renders the PID picker with filter input, list, and help bar.
-func (m Model) View() string {
+func (m Model) View() tea.View {
var b strings.Builder
if m.mode == PickerModeTID {
if m.targetPID > 0 {
@@ -264,8 +281,18 @@ func (m Model) View() string {
}
b.WriteString("\n")
- b.WriteString(helpBarStyle.Render(renderHelp(m.keys.PickerShortHelp())))
- return screenStyle.Render(b.String())
+ viewWidth, _ := common.EffectiveViewport(m.width, m.height)
+ helpStyle := helpBarStyle.Copy().Width(viewWidth)
+ b.WriteString(helpStyle.Render(renderHelp(m.keys.PickerShortHelp())))
+ return tea.NewView(screenStyle.Render(b.String()))
+}
+
+// SetDarkMode updates picker theme and text input styles.
+func (m Model) SetDarkMode(isDark bool) Model {
+ m.isDark = isDark
+ syncPickerStyles()
+ m.input.SetStyles(textinput.DefaultStyles(isDark))
+ return m
}
func (m Model) renderRows() string {
diff --git a/internal/tui/pidpicker/model_test.go b/internal/tui/pidpicker/model_test.go
index 2d76508..695e5bd 100644
--- a/internal/tui/pidpicker/model_test.go
+++ b/internal/tui/pidpicker/model_test.go
@@ -1,11 +1,12 @@
package pidpicker
import (
- "ior/internal/tui/messages"
"strings"
"testing"
- tea "github.com/charmbracelet/bubbletea"
+ "ior/internal/tui/messages"
+
+ tea "charm.land/bubbletea/v2"
)
func TestApplyFilterByPIDCommAndCmdline(t *testing.T) {
@@ -39,7 +40,7 @@ func TestEnterEmitsAllPIDsAndSelectedPID(t *testing.T) {
m.processes = []ProcessInfo{{Pid: 7, Comm: "vim"}, {Pid: 9, Comm: "top"}}
m.applyFilter()
- modelAny, cmdAny := m.Update(tea.KeyMsg{Type: tea.KeyEnter})
+ modelAny, cmdAny := m.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
_ = modelAny
msgAny := cmdAny()
pidAny, ok := msgAny.(messages.PidSelectedMsg)
@@ -51,7 +52,7 @@ func TestEnterEmitsAllPIDsAndSelectedPID(t *testing.T) {
}
m.selectedIndex = 2
- modelOne, cmdOne := m.Update(tea.KeyMsg{Type: tea.KeyEnter})
+ modelOne, cmdOne := m.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
_ = modelOne
msgOne := cmdOne()
pidOne, ok := msgOne.(messages.PidSelectedMsg)
@@ -71,7 +72,7 @@ func TestEnterEmitsAllTIDsAndSelectedTIDInTIDMode(t *testing.T) {
}
m.applyFilter()
- modelAny, cmdAny := m.Update(tea.KeyMsg{Type: tea.KeyEnter})
+ modelAny, cmdAny := m.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
_ = modelAny
msgAny := cmdAny()
tidAny, ok := msgAny.(messages.TidSelectedMsg)
@@ -86,7 +87,7 @@ func TestEnterEmitsAllTIDsAndSelectedTIDInTIDMode(t *testing.T) {
}
m.selectedIndex = 2
- modelOne, cmdOne := m.Update(tea.KeyMsg{Type: tea.KeyEnter})
+ modelOne, cmdOne := m.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
_ = modelOne
msgOne := cmdOne()
tidOne, ok := msgOne.(messages.TidSelectedMsg)
@@ -104,7 +105,7 @@ func TestEnterEmitsAllTIDsAndSelectedTIDInTIDMode(t *testing.T) {
func TestEscQuitsAndRefreshTriggersScan(t *testing.T) {
m := NewWithKeys(DefaultKeyMap())
- _, escCmd := m.Update(tea.KeyMsg{Type: tea.KeyEsc})
+ _, escCmd := m.Update(tea.KeyPressMsg{Code: tea.KeyEsc})
if escCmd == nil {
t.Fatalf("expected esc to return quit cmd")
}
@@ -112,7 +113,7 @@ func TestEscQuitsAndRefreshTriggersScan(t *testing.T) {
t.Fatalf("expected quit msg from esc, got %T", msg)
}
- _, refreshCmd := m.Update(tea.KeyMsg{Type: tea.KeyCtrlR})
+ _, refreshCmd := m.Update(tea.KeyPressMsg{Code: rune('r'), Text: "r", Mod: tea.ModCtrl})
if refreshCmd == nil {
t.Fatalf("expected refresh cmd")
}
@@ -124,7 +125,7 @@ func TestEscQuitsAndRefreshTriggersScan(t *testing.T) {
func TestRuneRDoesNotTriggerRefreshWhileFilterFocused(t *testing.T) {
m := NewWithKeys(DefaultKeyMap())
- next, cmd := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'r'}})
+ next, cmd := m.Update(tea.KeyPressMsg{Code: []rune{'r'}[0], Text: string([]rune{'r'})})
if cmd == nil {
t.Fatalf("expected textinput update cmd")
}
@@ -152,3 +153,13 @@ func TestRenderRowsKeepsSelectionVisible(t *testing.T) {
t.Fatalf("expected selected row to remain visible, got:\n%s", rows)
}
}
+
+func TestWindowSizeDoesNotCapInputWidthOnWideTerminals(t *testing.T) {
+ m := NewWithKeys(DefaultKeyMap())
+ next, _ := m.Update(tea.WindowSizeMsg{Width: 160, Height: 40})
+ updated := next.(Model)
+
+ if got, want := updated.input.Width(), 144; got != want {
+ t.Fatalf("expected input width %d for 160-col terminal, got %d", want, got)
+ }
+}