Skip to content

Commit 4860014

Browse files
committed
feat(tui): add jump component for navigation by steps
- add `Jump` struct with number input and directional navigation - support vim-style (`h`/`l`) and arrow key navigation controls - include visual overlay with input display and help text - handle escape and ctrl+c for canceling jump operation
1 parent 0601661 commit 4860014

File tree

1 file changed

+143
-0
lines changed

1 file changed

+143
-0
lines changed

internal/tui/jump.go

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package tui
2+
3+
import (
4+
"strconv"
5+
6+
tea "github.com/charmbracelet/bubbletea"
7+
"github.com/charmbracelet/lipgloss"
8+
)
9+
10+
type Jump struct {
11+
numberInput string
12+
jumpSteps int
13+
quitting bool
14+
showing bool
15+
width int
16+
height int
17+
}
18+
19+
func NewJump() Jump {
20+
return Jump{
21+
numberInput: "",
22+
showing: false,
23+
}
24+
}
25+
26+
func (m Jump) Init() tea.Cmd {
27+
return nil
28+
}
29+
30+
func (m Jump) Update(msg tea.Msg) (Jump, tea.Cmd) {
31+
switch msg := msg.(type) {
32+
case tea.WindowSizeMsg:
33+
m.width = msg.Width
34+
m.height = msg.Height
35+
return m, nil
36+
case tea.KeyMsg:
37+
switch msg.String() {
38+
case "esc", "ctrl+c":
39+
m.quitting = true
40+
m.showing = false
41+
return m, nil
42+
case "1", "2", "3", "4", "5", "6", "7", "8", "9":
43+
if len(m.numberInput) < 4 {
44+
m.numberInput += msg.String()
45+
}
46+
return m, nil
47+
case "backspace":
48+
if len(m.numberInput) > 0 {
49+
m.numberInput = m.numberInput[:len(m.numberInput)-1]
50+
}
51+
return m, nil
52+
case "h", "left":
53+
if m.numberInput != "" {
54+
if steps, err := strconv.Atoi(m.numberInput); err == nil {
55+
m.jumpSteps = -steps
56+
}
57+
m.quitting = true
58+
m.showing = false
59+
return m, tea.Quit
60+
}
61+
return m, nil
62+
case "l", "right":
63+
if m.numberInput != "" {
64+
if steps, err := strconv.Atoi(m.numberInput); err == nil {
65+
m.jumpSteps = steps
66+
}
67+
m.quitting = true
68+
m.showing = false
69+
return m, tea.Quit
70+
}
71+
return m, nil
72+
}
73+
}
74+
return m, nil
75+
}
76+
77+
func (m Jump) Show(slideView string, width, height int) string {
78+
displayNumber := m.numberInput
79+
if displayNumber == "" {
80+
displayNumber = "_"
81+
}
82+
83+
statusContent := lipgloss.NewStyle().
84+
Background(lipgloss.Color("#3C3C3C")).
85+
Foreground(lipgloss.Color("#DDDDDD")).
86+
Padding(0, 1).
87+
Render(displayNumber)
88+
89+
statusWidth := lipgloss.Width(statusContent)
90+
statusX := (width - statusWidth) / 2
91+
statusY := height - 1
92+
93+
leftHelp := lipgloss.JoinHorizontal(
94+
lipgloss.Center,
95+
mutedStyle.Render("←/h"),
96+
" ",
97+
veryMutedStyle.Render("• previous"),
98+
)
99+
100+
rightHelp := lipgloss.JoinHorizontal(
101+
lipgloss.Center,
102+
mutedStyle.Render("→/l"),
103+
" ",
104+
veryMutedStyle.Render("• next"),
105+
)
106+
107+
escHelp := lipgloss.JoinHorizontal(
108+
lipgloss.Center,
109+
mutedStyle.Render("esc/ctrl+c"),
110+
" ",
111+
veryMutedStyle.Render("• cancel"),
112+
)
113+
114+
helpText := lipgloss.JoinHorizontal(lipgloss.Center, leftHelp, " ", rightHelp, " ", escHelp)
115+
helpWidth := lipgloss.Width(helpText)
116+
helpX := (width - helpWidth) / 2
117+
helpY := height - 3
118+
119+
viewWithStatus := placeOverlay(statusX, statusY, statusContent, slideView)
120+
return placeOverlay(helpX, helpY, helpText, viewWithStatus)
121+
}
122+
123+
func (m Jump) IsShowing() bool {
124+
return m.showing
125+
}
126+
127+
func (m Jump) SetShowing(showing bool) Jump {
128+
m.showing = showing
129+
if showing {
130+
m.numberInput = ""
131+
m.jumpSteps = 0
132+
m.quitting = false
133+
}
134+
return m
135+
}
136+
137+
func (m Jump) Quitting() bool {
138+
return m.quitting
139+
}
140+
141+
func (m Jump) JumpSteps() int {
142+
return m.jumpSteps
143+
}

0 commit comments

Comments
 (0)