fconvert 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fconvert",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Fast local CLI conversion tool with graph-based anything-to-anything routing",
5
5
  "type": "module",
6
6
  "main": "src/cli/main.ts",
@@ -1,24 +1,35 @@
1
1
  module github.com/timmy6942025/convert/tools/format-picker
2
2
 
3
- go 1.22
3
+ go 1.24.2
4
4
 
5
- require github.com/charmbracelet/bubbletea v1.3.4
5
+ toolchain go1.24.4
6
6
 
7
7
  require (
8
+ github.com/charmbracelet/bubbles v1.0.0
9
+ github.com/charmbracelet/bubbletea v1.3.10
10
+ github.com/charmbracelet/lipgloss v1.1.0
11
+ )
12
+
13
+ require (
14
+ github.com/atotto/clipboard v0.1.4 // indirect
8
15
  github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
9
- github.com/charmbracelet/lipgloss v1.0.0 // indirect
10
- github.com/charmbracelet/x/ansi v0.8.0 // indirect
11
- github.com/charmbracelet/x/term v0.2.1 // indirect
16
+ github.com/charmbracelet/colorprofile v0.4.1 // indirect
17
+ github.com/charmbracelet/x/ansi v0.11.6 // indirect
18
+ github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
19
+ github.com/charmbracelet/x/term v0.2.2 // indirect
20
+ github.com/clipperhouse/displaywidth v0.9.0 // indirect
21
+ github.com/clipperhouse/stringish v0.1.1 // indirect
22
+ github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
12
23
  github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
13
- github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
24
+ github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
14
25
  github.com/mattn/go-isatty v0.0.20 // indirect
15
26
  github.com/mattn/go-localereader v0.0.1 // indirect
16
- github.com/mattn/go-runewidth v0.0.16 // indirect
27
+ github.com/mattn/go-runewidth v0.0.19 // indirect
17
28
  github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
18
29
  github.com/muesli/cancelreader v0.2.2 // indirect
19
- github.com/muesli/termenv v0.15.2 // indirect
30
+ github.com/muesli/termenv v0.16.0 // indirect
20
31
  github.com/rivo/uniseg v0.4.7 // indirect
21
- golang.org/x/sync v0.11.0 // indirect
22
- golang.org/x/sys v0.30.0 // indirect
32
+ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
33
+ golang.org/x/sys v0.38.0 // indirect
23
34
  golang.org/x/text v0.3.8 // indirect
24
35
  )
@@ -1,37 +1,56 @@
1
+ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
2
+ github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
1
3
  github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
2
4
  github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
3
- github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI=
4
- github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo=
5
- github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg=
6
- github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo=
7
- github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
8
- github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
9
- github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
10
- github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
5
+ github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
6
+ github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
7
+ github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
8
+ github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
9
+ github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
10
+ github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
11
+ github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
12
+ github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
13
+ github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
14
+ github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
15
+ github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
16
+ github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
17
+ github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
18
+ github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
19
+ github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
20
+ github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
21
+ github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
22
+ github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
23
+ github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
24
+ github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=
25
+ github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
26
+ github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
27
+ github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
28
+ github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
11
29
  github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
12
30
  github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
13
- github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
14
- github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
31
+ github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
32
+ github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
15
33
  github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
16
34
  github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
17
35
  github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
18
36
  github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
19
- github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
20
- github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
37
+ github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
38
+ github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
21
39
  github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
22
40
  github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
23
41
  github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
24
42
  github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
25
- github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
26
- github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
27
- github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
43
+ github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
44
+ github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
28
45
  github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
29
46
  github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
30
- golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
31
- golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
47
+ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
48
+ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
49
+ golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
50
+ golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
32
51
  golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
33
52
  golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
34
- golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
35
- golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
53
+ golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
54
+ golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
36
55
  golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
37
56
  golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
@@ -8,7 +8,11 @@ import (
8
8
  "sort"
9
9
  "strings"
10
10
 
11
+ "github.com/charmbracelet/bubbles/help"
12
+ "github.com/charmbracelet/bubbles/key"
13
+ "github.com/charmbracelet/bubbles/textinput"
11
14
  tea "github.com/charmbracelet/bubbletea"
15
+ "github.com/charmbracelet/lipgloss"
12
16
  )
13
17
 
14
18
  type option struct {
@@ -29,9 +33,54 @@ type scoredOption struct {
29
33
  score int
30
34
  }
31
35
 
36
+ type keyMap struct {
37
+ Up key.Binding
38
+ Down key.Binding
39
+ PageUp key.Binding
40
+ PageDown key.Binding
41
+ Accept key.Binding
42
+ Quit key.Binding
43
+ }
44
+
45
+ func (k keyMap) ShortHelp() []key.Binding {
46
+ return []key.Binding{k.Up, k.Down, k.Accept, k.Quit}
47
+ }
48
+
49
+ func (k keyMap) FullHelp() [][]key.Binding {
50
+ return [][]key.Binding{{k.Up, k.Down, k.PageUp, k.PageDown}, {k.Accept, k.Quit}}
51
+ }
52
+
53
+ func defaultKeys() keyMap {
54
+ return keyMap{
55
+ Up: key.NewBinding(
56
+ key.WithKeys("up", "k", "ctrl+p"),
57
+ key.WithHelp("up", "move up"),
58
+ ),
59
+ Down: key.NewBinding(
60
+ key.WithKeys("down", "j", "ctrl+n"),
61
+ key.WithHelp("down", "move down"),
62
+ ),
63
+ PageUp: key.NewBinding(
64
+ key.WithKeys("pgup", "u"),
65
+ key.WithHelp("pgup", "page up"),
66
+ ),
67
+ PageDown: key.NewBinding(
68
+ key.WithKeys("pgdown", "d"),
69
+ key.WithHelp("pgdown", "page down"),
70
+ ),
71
+ Accept: key.NewBinding(
72
+ key.WithKeys("enter"),
73
+ key.WithHelp("enter", "select"),
74
+ ),
75
+ Quit: key.NewBinding(
76
+ key.WithKeys("esc", "ctrl+c"),
77
+ key.WithHelp("esc", "cancel"),
78
+ ),
79
+ }
80
+ }
81
+
32
82
  type model struct {
33
83
  prompt string
34
- query string
35
84
  preferred string
36
85
  all []option
37
86
  filtered []option
@@ -40,10 +89,45 @@ type model struct {
40
89
  cancelled bool
41
90
  width int
42
91
  height int
92
+ input textinput.Model
93
+ help help.Model
94
+ keys keyMap
43
95
  }
44
96
 
97
+ var (
98
+ appStyle = lipgloss.NewStyle().
99
+ BorderStyle(lipgloss.RoundedBorder()).
100
+ BorderForeground(lipgloss.AdaptiveColor{Light: "251", Dark: "238"}).
101
+ Padding(0, 1)
102
+
103
+ titleStyle = lipgloss.NewStyle().
104
+ Bold(true)
105
+
106
+ mutedStyle = lipgloss.NewStyle().
107
+ Foreground(lipgloss.AdaptiveColor{Light: "245", Dark: "240"})
108
+
109
+ promptStyle = lipgloss.NewStyle().
110
+ Foreground(lipgloss.AdaptiveColor{Light: "63", Dark: "111"})
111
+
112
+ hintStyle = lipgloss.NewStyle().
113
+ Foreground(lipgloss.AdaptiveColor{Light: "245", Dark: "240"})
114
+
115
+ selectedStyle = lipgloss.NewStyle().
116
+ Bold(true).
117
+ Foreground(lipgloss.AdaptiveColor{Light: "39", Dark: "117"})
118
+
119
+ rowStyle = lipgloss.NewStyle().
120
+ Foreground(lipgloss.AdaptiveColor{Light: "250", Dark: "252"})
121
+
122
+ footerStyle = lipgloss.NewStyle().
123
+ Foreground(lipgloss.AdaptiveColor{Light: "245", Dark: "240"})
124
+
125
+ emptyStyle = lipgloss.NewStyle().
126
+ Foreground(lipgloss.AdaptiveColor{Light: "203", Dark: "210"})
127
+ )
128
+
45
129
  func (m model) Init() tea.Cmd {
46
- return nil
130
+ return textinput.Blink
47
131
  }
48
132
 
49
133
  func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
@@ -51,67 +135,76 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
51
135
  case tea.WindowSizeMsg:
52
136
  m.width = msg.Width
53
137
  m.height = msg.Height
138
+ if m.width > 0 {
139
+ m.input.Width = maxInt(12, m.width-24)
140
+ }
54
141
  return m, nil
55
142
  case tea.KeyMsg:
56
- switch msg.String() {
57
- case "ctrl+c", "esc":
143
+ switch {
144
+ case key.Matches(msg, m.keys.Quit):
58
145
  m.cancelled = true
59
146
  return m, tea.Quit
60
- case "enter":
147
+ case key.Matches(msg, m.keys.Accept):
61
148
  if len(m.filtered) == 0 {
62
149
  return m, nil
63
150
  }
64
151
  selected := m.filtered[m.cursor]
65
152
  m.selected = &selected
66
153
  return m, tea.Quit
67
- case "up", "k", "ctrl+p":
154
+ case key.Matches(msg, m.keys.Up):
68
155
  if m.cursor > 0 {
69
156
  m.cursor--
70
157
  }
71
158
  return m, nil
72
- case "down", "j", "ctrl+n":
159
+ case key.Matches(msg, m.keys.Down):
73
160
  if m.cursor < len(m.filtered)-1 {
74
161
  m.cursor++
75
162
  }
76
163
  return m, nil
77
- case "backspace", "ctrl+h":
78
- if len(m.query) == 0 {
79
- return m, nil
164
+ case key.Matches(msg, m.keys.PageUp):
165
+ step := m.visibleRows()
166
+ m.cursor = maxInt(0, m.cursor-step)
167
+ return m, nil
168
+ case key.Matches(msg, m.keys.PageDown):
169
+ step := m.visibleRows()
170
+ if len(m.filtered) > 0 {
171
+ m.cursor = minInt(len(m.filtered)-1, m.cursor+step)
80
172
  }
81
- m.query = m.query[:len(m.query)-1]
82
- m.refilter()
83
173
  return m, nil
84
174
  }
175
+ }
85
176
 
86
- if msg.Type == tea.KeyRunes {
87
- m.query += msg.String()
88
- m.refilter()
89
- }
177
+ previousQuery := m.input.Value()
178
+ var cmd tea.Cmd
179
+ m.input, cmd = m.input.Update(msg)
180
+ if previousQuery != m.input.Value() {
181
+ m.refilter()
90
182
  }
91
183
 
92
- return m, nil
184
+ return m, cmd
93
185
  }
94
186
 
95
187
  func (m model) View() string {
96
188
  var builder strings.Builder
97
- builder.WriteString(fmt.Sprintf("%s > %s\n", m.prompt, m.query))
98
- if m.query == "" && m.preferred != "" {
99
- builder.WriteString(fmt.Sprintf(" hint: original extension '.%s' is ranked first\n", m.preferred))
189
+ builder.WriteString(titleStyle.Render("fconvert format picker"))
190
+ builder.WriteString("\n")
191
+ builder.WriteString(promptStyle.Render(m.prompt))
192
+ builder.WriteString("\n")
193
+ builder.WriteString(m.input.View())
194
+ builder.WriteString("\n")
195
+ if strings.TrimSpace(m.input.Value()) == "" && m.preferred != "" {
196
+ builder.WriteString(hintStyle.Render(fmt.Sprintf("hint: original extension '.%s' is ranked first", m.preferred)))
197
+ builder.WriteString("\n")
100
198
  }
101
199
 
102
200
  if len(m.filtered) == 0 {
103
- builder.WriteString(" no matches\n")
104
- builder.WriteString(" enter to keep typing, esc to cancel")
105
- return builder.String()
201
+ builder.WriteString(emptyStyle.Render("no matches"))
202
+ builder.WriteString("\n")
203
+ builder.WriteString(footerStyle.Render(m.help.View(m.keys)))
204
+ return appStyle.Render(builder.String())
106
205
  }
107
206
 
108
- maxRows := 8
109
- if m.height > 6 {
110
- maxRows = m.height - 4
111
- }
112
- if maxRows < 4 {
113
- maxRows = 4
114
- }
207
+ maxRows := m.visibleRows()
115
208
  if maxRows > len(m.filtered) {
116
209
  maxRows = len(m.filtered)
117
210
  }
@@ -128,19 +221,45 @@ func (m model) View() string {
128
221
 
129
222
  for index := start; index < end; index++ {
130
223
  prefix := " "
224
+ style := rowStyle
131
225
  if index == m.cursor {
132
- prefix = "> "
226
+ prefix = "->"
227
+ style = selectedStyle
133
228
  }
134
229
  item := m.filtered[index]
135
- builder.WriteString(fmt.Sprintf("%s%-10s .%-6s %s\n", prefix, item.ID, item.Extension, item.Name))
230
+ extra := ""
231
+ if strings.EqualFold(item.Extension, m.preferred) {
232
+ extra = mutedStyle.Render(" same-ext")
233
+ }
234
+ line := fmt.Sprintf("%s %-10s .%-6s %s%s", prefix, item.ID, item.Extension, item.Name, extra)
235
+ builder.WriteString(style.Render(line))
236
+ builder.WriteString("\n")
237
+ }
238
+
239
+ status := fmt.Sprintf("%d shown / %d total", len(m.filtered), len(m.all))
240
+ if len(m.filtered) != len(m.all) {
241
+ status = fmt.Sprintf("%d matches / %d total", len(m.filtered), len(m.all))
136
242
  }
243
+ builder.WriteString(footerStyle.Render(status))
244
+ builder.WriteString("\n")
245
+ builder.WriteString(footerStyle.Render(m.help.View(m.keys)))
246
+
247
+ return appStyle.Render(builder.String())
248
+ }
137
249
 
138
- builder.WriteString(" enter select esc cancel")
139
- return builder.String()
250
+ func (m model) visibleRows() int {
251
+ rows := 10
252
+ if m.height > 0 {
253
+ rows = m.height - 11
254
+ }
255
+ if rows < 4 {
256
+ rows = 4
257
+ }
258
+ return rows
140
259
  }
141
260
 
142
261
  func (m *model) refilter() {
143
- query := strings.ToLower(strings.TrimSpace(m.query))
262
+ query := strings.ToLower(strings.TrimSpace(m.input.Value()))
144
263
  scored := make([]scoredOption, 0, len(m.all))
145
264
 
146
265
  for _, item := range m.all {
@@ -235,6 +354,20 @@ func fuzzyScore(query string, item option, preferred string) int {
235
354
  return score
236
355
  }
237
356
 
357
+ func minInt(a int, b int) int {
358
+ if a < b {
359
+ return a
360
+ }
361
+ return b
362
+ }
363
+
364
+ func maxInt(a int, b int) int {
365
+ if a > b {
366
+ return a
367
+ }
368
+ return b
369
+ }
370
+
238
371
  func readPayload(path string) (payload, error) {
239
372
  raw, err := os.ReadFile(path)
240
373
  if err != nil {
@@ -249,6 +382,10 @@ func readPayload(path string) (payload, error) {
249
382
  if data.Prompt == "" {
250
383
  data.Prompt = "output format"
251
384
  }
385
+ if data.Query != "" {
386
+ data.Query = strings.TrimSpace(data.Query)
387
+ }
388
+ data.Prompt = strings.TrimSpace(data.Prompt)
252
389
  return data, nil
253
390
  }
254
391
 
@@ -270,9 +407,25 @@ func main() {
270
407
 
271
408
  picker := model{
272
409
  prompt: data.Prompt,
273
- query: data.Query,
274
410
  preferred: strings.TrimSpace(data.Preferred),
275
411
  all: data.Options,
412
+ keys: defaultKeys(),
413
+ help: help.New(),
414
+ }
415
+ picker.input = textinput.New()
416
+ picker.input.Prompt = ""
417
+ picker.input.Placeholder = "type to fuzzy filter"
418
+ picker.input.CharLimit = 128
419
+ picker.input.SetValue(data.Query)
420
+ picker.input.Focus()
421
+ picker.help.ShowAll = false
422
+ picker.help.Styles.ShortKey = mutedStyle
423
+ picker.help.Styles.ShortDesc = mutedStyle
424
+ picker.help.Styles.FullKey = mutedStyle
425
+ picker.help.Styles.FullDesc = mutedStyle
426
+
427
+ if picker.prompt == "" {
428
+ picker.prompt = "output format"
276
429
  }
277
430
  picker.refilter()
278
431