vibepup 1.0.2 → 1.0.3

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.
package/tui/main.go CHANGED
@@ -4,17 +4,52 @@ import (
4
4
  "context"
5
5
  "fmt"
6
6
  "os"
7
- "os/exec"
8
- "strings"
9
7
  "time"
10
8
 
9
+ "github.com/charmbracelet/bubbles/help"
10
+ "github.com/charmbracelet/bubbles/key"
11
+ "github.com/charmbracelet/bubbles/spinner"
11
12
  tea "github.com/charmbracelet/bubbletea"
12
- "github.com/charmbracelet/harmonica"
13
13
  "github.com/charmbracelet/huh"
14
14
  "github.com/charmbracelet/lipgloss"
15
- "github.com/charmbracelet/log"
15
+ "github.com/mattn/go-isatty"
16
+
17
+ "vibepup-tui/config"
18
+ "vibepup-tui/motion"
19
+ "vibepup-tui/persona"
20
+ "vibepup-tui/process"
21
+ "vibepup-tui/theme"
22
+ "vibepup-tui/ui"
16
23
  )
17
24
 
25
+ // --- Key Bindings ---
26
+
27
+ type KeyMap struct {
28
+ Quit key.Binding
29
+ Help key.Binding
30
+ NextTheme key.Binding
31
+ Pet key.Binding
32
+ }
33
+
34
+ func DefaultKeyMap() KeyMap {
35
+ return KeyMap{
36
+ Quit: key.NewBinding(key.WithKeys("q", "ctrl+c"), key.WithHelp("q", "quit")),
37
+ Help: key.NewBinding(key.WithKeys("?"), key.WithHelp("?", "help")),
38
+ NextTheme: key.NewBinding(key.WithKeys("t"), key.WithHelp("t", "theme")),
39
+ Pet: key.NewBinding(key.WithKeys("p"), key.WithHelp("p", "pet dog")),
40
+ }
41
+ }
42
+
43
+ func (k KeyMap) ShortHelp() []key.Binding {
44
+ return []key.Binding{k.Help, k.Quit, k.NextTheme, k.Pet}
45
+ }
46
+
47
+ func (k KeyMap) FullHelp() [][]key.Binding {
48
+ return [][]key.Binding{{k.Help, k.Quit, k.NextTheme, k.Pet}}
49
+ }
50
+
51
+ // --- Model ---
52
+
18
53
  type viewState int
19
54
 
20
55
  const (
@@ -25,257 +60,293 @@ const (
25
60
  )
26
61
 
27
62
  type model struct {
28
- state viewState
29
- start time.Time
30
- frame int
31
- args []string
32
- form *huh.Form
33
- newForm *huh.Form
34
- selected string
35
- newIdea string
36
- spring harmonica.Spring
37
- pos float64
38
- velocity float64
39
- targetPos float64
63
+ state viewState
64
+ width int
65
+ height int
66
+ ready bool
67
+
68
+ // Components
69
+ keys KeyMap
70
+ help help.Model
71
+ form *huh.Form
72
+ newForm *huh.Form
73
+ viewport ui.LogViewport
74
+ spinner spinner.Model
75
+
76
+ // Config & State
77
+ flags config.Flags
78
+ theme theme.Theme
79
+ styles lipgloss.Style // Simplified, use theme package directly where possible
80
+ snark persona.SnarkLevel
81
+
82
+ // Process
83
+ runner *process.Runner
84
+ selected string
85
+ newIdea string
86
+ args []string
87
+
88
+ // Animation
89
+ motion motion.Engine
90
+ frame int
91
+ dogState string // "sleeping", "running", "barking", "happy"
40
92
  }
41
93
 
42
- func initialModel() model {
43
- args := []string{"--watch"}
44
- if len(os.Args) > 1 {
45
- args = os.Args[1:]
46
- }
94
+ func initialModel(flags config.Flags) model {
95
+ th := theme.Get(flags.Theme)
96
+ snark := persona.ParseSnark(flags.Snark)
97
+
98
+ s := spinner.New()
99
+ s.Spinner = spinner.Points // More modern spinner
100
+ s.Style = lipgloss.NewStyle().Foreground(th.Highlight)
47
101
 
48
- spring := harmonica.NewSpring(harmonica.FPS(60), 6.0, 0.2)
102
+ m := model{
103
+ state: stateSplash,
104
+ keys: DefaultKeyMap(),
105
+ help: help.New(),
106
+ flags: flags,
107
+ theme: th,
108
+ snark: snark,
109
+ motion: motion.New(flags.PerfLow, flags.Quiet),
110
+ spinner: s,
111
+ dogState: "sleeping",
112
+ selected: "watch", // Default
113
+ args: os.Args[1:],
114
+ }
49
115
 
50
- m := model{state: stateSplash, start: time.Now(), args: args, selected: "watch", spring: spring, targetPos: 10}
116
+ // Setup Form
51
117
  m.form = huh.NewForm(
52
118
  huh.NewGroup(
53
119
  huh.NewSelect[string]().
54
- Title("♥ What shall we do today? ♥").
120
+ Title("♥ Pick your poison ♥").
55
121
  Options(
56
- huh.NewOption("Watch (recommended)", "watch"),
57
- huh.NewOption("Run 5 iterations", "run"),
58
- huh.NewOption("New project from idea", "new"),
59
- huh.NewOption("Free setup (opencode + auth)", "free"),
122
+ huh.NewOption("Watch Mode (Stalker vibes)", "watch"),
123
+ huh.NewOption("Run 5 Loops (Quickie)", "run"),
124
+ huh.NewOption("New Project (YOLO)", "new"),
125
+ huh.NewOption("Free Setup (Broke af)", "free"),
60
126
  ).
61
127
  Value(&m.selected),
62
128
  ),
63
- )
129
+ ).WithTheme(huh.ThemeDracula())
130
+
64
131
  m.newForm = huh.NewForm(
65
132
  huh.NewGroup(
66
133
  huh.NewInput().
67
- Title("Describe your project idea").
68
- Prompt("Idea: ").
69
- Placeholder("A vibe-coded project").
134
+ Title("Spill the tea ").
135
+ Prompt("Manifest: ").
136
+ Placeholder("Make me a unicorn...").
70
137
  Value(&m.newIdea),
71
138
  ),
72
- )
139
+ ).WithTheme(huh.ThemeDracula())
73
140
 
74
141
  return m
75
142
  }
76
143
 
77
144
  func (m model) Init() tea.Cmd {
78
- log.SetLevel(log.InfoLevel)
79
- log.SetReportCaller(false)
80
- log.SetTimeFormat("")
81
- return tea.Tick(time.Millisecond*80, func(t time.Time) tea.Msg { return t })
145
+ return tea.Batch(
146
+ m.motion.Next(),
147
+ m.spinner.Tick,
148
+ )
82
149
  }
83
150
 
84
151
  func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
152
+ var cmds []tea.Cmd
153
+ var cmd tea.Cmd
154
+
85
155
  switch msg := msg.(type) {
156
+ case tea.WindowSizeMsg:
157
+ m.width = msg.Width
158
+ m.height = msg.Height
159
+ m.help.Width = msg.Width
160
+
161
+ // Dynamic Layout Calculation
162
+ headerHeight := 10 // Approximation, should be measured
163
+ footerHeight := 3
164
+ vpHeight := msg.Height - headerHeight - footerHeight - 2 // Borders
165
+
166
+ if !m.ready {
167
+ m.viewport = ui.NewLogViewport(msg.Width-4, vpHeight)
168
+ m.ready = true
169
+ } else {
170
+ m.viewport.SetSize(msg.Width-4, vpHeight)
171
+ }
172
+
86
173
  case tea.KeyMsg:
87
- if msg.String() == "ctrl+c" || msg.String() == "esc" {
174
+ if m.state == stateRunning && m.runner != nil {
175
+ // Pass interactions to viewport if needed
176
+ }
177
+
178
+ switch {
179
+ case key.Matches(msg, m.keys.Quit):
180
+ if m.runner != nil {
181
+ m.runner.Kill() // ZOMBIE KILLER
182
+ }
88
183
  return m, tea.Quit
184
+ case key.Matches(msg, m.keys.Pet):
185
+ m.dogState = "happy"
186
+ cmds = append(cmds, tea.Tick(time.Second, func(t time.Time) tea.Msg {
187
+ return "dog_reset"
188
+ }))
89
189
  }
90
- case time.Time:
91
- switch m.state {
92
- case stateSplash:
93
- m.frame++
94
- if time.Since(m.start) > time.Second*2 {
95
- m.state = stateSetup
96
- return m, m.form.Init()
190
+
191
+ case string:
192
+ if msg == "dog_reset" {
193
+ if m.runner != nil {
194
+ m.dogState = "running"
195
+ } else {
196
+ m.dogState = "sleeping"
97
197
  }
98
- return m, tea.Tick(time.Millisecond*80, func(t time.Time) tea.Msg { return t })
99
- case stateRunning:
100
- m.frame++
101
- if m.frame%20 == 0 {
102
- if m.targetPos == 0 {
103
- m.targetPos = 10
198
+ }
199
+
200
+ case motion.TickMsg:
201
+ m.frame++
202
+ cmds = append(cmds, m.motion.Next())
203
+
204
+ case spinner.TickMsg:
205
+ m.spinner, cmd = m.spinner.Update(msg)
206
+ cmds = append(cmds, cmd)
207
+
208
+ case process.OutputMsg:
209
+ m.viewport.WriteLine(string(msg))
210
+ cmds = append(cmds, m.runner.WaitForOutput())
211
+
212
+ case process.DoneMsg:
213
+ m.dogState = "sleeping"
214
+ m.viewport.WriteLine("\n--- Process Finished ---")
215
+ if msg.Err != nil {
216
+ m.viewport.WriteLine(fmt.Sprintf("Error: %v", msg.Err))
217
+ m.dogState = "barking"
218
+ }
219
+ m.runner = nil
220
+ }
221
+
222
+ // Handle Forms
223
+ if m.state == stateSetup {
224
+ form, cmd := m.form.Update(msg)
225
+ if f, ok := form.(*huh.Form); ok {
226
+ m.form = f
227
+ if m.form.State == huh.StateCompleted {
228
+ if m.selected == "new" {
229
+ m.state = stateRunning
230
+ cmds = append(cmds, m.newForm.Init())
104
231
  } else {
105
- m.targetPos = 0
232
+ m.state = stateRunning
233
+ cmds = append(cmds, m.startProcess())
106
234
  }
107
235
  }
108
- m.pos, m.velocity = m.spring.Update(m.pos, m.velocity, m.targetPos)
109
- return m, tea.Tick(time.Millisecond*80, func(t time.Time) tea.Msg { return t })
110
236
  }
237
+ cmds = append(cmds, cmd)
111
238
  }
112
239
 
113
- switch m.state {
114
- case stateSetup:
115
- fm, cmd := m.form.Update(msg)
116
- m.form = fm.(*huh.Form)
117
- if m.form.State == huh.StateCompleted {
118
- if m.selected == "new" {
119
- m.state = stateRunning
120
- return m, m.newForm.Init()
121
- }
122
- m.state = stateRunning
123
- return m, launchCmd(m.selected, m.args)
124
- }
125
- return m, cmd
126
- case stateRunning:
127
- if m.selected == "new" {
128
- fm, cmd := m.newForm.Update(msg)
129
- m.newForm = fm.(*huh.Form)
240
+ if m.state == stateRunning && m.selected == "new" && m.runner == nil {
241
+ form, cmd := m.newForm.Update(msg)
242
+ if f, ok := form.(*huh.Form); ok {
243
+ m.newForm = f
130
244
  if m.newForm.State == huh.StateCompleted {
131
- idea := strings.TrimSpace(m.newIdea)
132
- if idea == "" {
133
- idea = "A vibe-coded project"
134
- }
135
- return m, launchCmd("new", append([]string{idea}, m.args...))
245
+ m.args = append(m.args, "new", m.newIdea)
246
+ cmds = append(cmds, m.startProcess())
136
247
  }
137
- return m, cmd
138
248
  }
249
+ cmds = append(cmds, cmd)
139
250
  }
140
-
141
- return m, nil
142
- }
143
-
144
- func (m model) View() string {
145
- // Theme Palette
146
- pink := lipgloss.Color("#FFB7C5") // Sakura Pink
147
- hotPink := lipgloss.Color("#FF69B4") // Hot Pink
148
- babyBlue := lipgloss.Color("#89CFF0") // Baby Blue
149
- lavender := lipgloss.Color("#E6E6FA") // Lavender
150
- gray := lipgloss.Color("245") // Muted Gray
151
-
152
- // Layout Styles
153
- boxStyle := lipgloss.NewStyle().
154
- Border(lipgloss.RoundedBorder()).
155
- BorderForeground(pink).
156
- Padding(1, 2).
157
- Margin(1, 1)
158
-
159
- titleStyle := lipgloss.NewStyle().
160
- Foreground(hotPink).
161
- Background(lavender).
162
- Bold(true).
163
- Padding(0, 1).
164
- MarginBottom(1)
165
-
166
- // Animation Frames
167
- frames := []string{
168
- "૮ ˶ᵔ ᵕ ᵔ˶ ა", // Happy
169
- "૮ ˶• ﻌ •˶ ა", // Alert
170
- "૮ ≧ ﻌ ≦ ა", // Blink
171
- "૮ / ˶ • ﻌ • ˶ \\ ა", // Paws up
251
+
252
+ // Auto-advance splash
253
+ if m.state == stateSplash && m.frame > 100 {
254
+ m.state = stateSetup
172
255
  }
173
256
 
174
- // Sparkle Animation
175
- sparkles := []string{"。・゚✧", "✧・゚。", "。・゚★", "☆・゚。"}
176
-
177
- // Frame Calculations
178
- // Slow down the dog animation (every 4th frame)
179
- idx := (m.frame / 4) % len(frames)
180
- // Fast sparkles (every 2nd frame)
181
- sIdx := (m.frame / 2) % len(sparkles)
257
+ // Update viewport
258
+ if m.ready {
259
+ m.viewport, cmd = m.viewport.Update(msg)
260
+ cmds = append(cmds, cmd)
261
+ }
182
262
 
183
- currentDog := frames[idx]
184
- currentSparkle := sparkles[sIdx]
263
+ return m, tea.Batch(cmds...)
264
+ }
185
265
 
186
- // Spring Animation (Horizontal movement)
187
- pad := ""
188
- if m.pos > 0 {
189
- pad = strings.Repeat(" ", int(m.pos))
266
+ func (m *model) startProcess() tea.Cmd {
267
+ if !m.flags.ForceRun && !isatty.IsTerminal(os.Stdout.Fd()) {
268
+ m.viewport.WriteLine("Error: Not a TTY. Use --force-run.")
269
+ return nil
190
270
  }
191
271
 
192
- // Common Elements
193
- dogRender := lipgloss.NewStyle().Foreground(pink).Render(pad + currentDog + " " + currentSparkle)
272
+ args := m.args
273
+ if m.selected == "watch" {
274
+ args = append([]string{"--watch"}, args...)
275
+ } else if m.selected == "run" {
276
+ args = append([]string{"5"}, args...)
277
+ } else if m.selected == "free" {
278
+ args = []string{"free"}
279
+ }
280
+
281
+ // Actually invoke the CLI (ralph.js -> ralph.sh mechanism, but we call 'vibepup' assuming it's in path or we call the shell script directly)
282
+ // For local dev, we might need to call the script directly if 'vibepup' isn't in PATH.
283
+ // But let's assume 'vibepup' is the command.
284
+ runCmd := "vibepup"
285
+ if m.flags.Runner != "" {
286
+ runCmd = m.flags.Runner
287
+ }
288
+
289
+ // If running locally from repo, we might want to call the script directly?
290
+ // The user said "run this project from the build".
291
+ // We'll stick to "vibepup" and assume it's linked or we can use absolute path if needed.
292
+ // Let's use the first arg as the command if provided, or default to "vibepup"
293
+
294
+ m.dogState = "running"
295
+ m.viewport.WriteLine("--- Starting Vibepup ---")
296
+
297
+ var cmd tea.Cmd
298
+ m.runner, cmd = process.Start(context.Background(), runCmd, args)
299
+
300
+ return tea.Batch(cmd, m.runner.WaitForOutput())
301
+ }
194
302
 
195
- // Splash Screen Content
196
- splashContent := lipgloss.JoinVertical(lipgloss.Center,
197
- titleStyle.Render("♥ Vibepup TUI ♥"),
198
- "",
199
- dogRender,
200
- "",
201
- lipgloss.NewStyle().Foreground(gray).Render("Loading cuteness..."),
202
- )
303
+ func (m model) View() string {
304
+ if !m.ready {
305
+ return "Initializing..."
306
+ }
203
307
 
204
- switch m.state {
205
- case stateSplash:
206
- return boxStyle.Render(splashContent)
207
-
208
- case stateSetup:
209
- return boxStyle.Render(
210
- lipgloss.JoinVertical(lipgloss.Left,
211
- lipgloss.NewStyle().Foreground(babyBlue).Bold(true).Render("♥ Setup Phase"),
212
- lipgloss.NewStyle().Foreground(gray).Render("Tip: choose Free setup to auto-configure opencode"),
213
- "",
214
- m.form.View(),
308
+ // 1. Splash
309
+ if m.state == stateSplash {
310
+ return ui.BoxStyle.Render(
311
+ lipgloss.JoinVertical(lipgloss.Center,
312
+ lipgloss.NewStyle().Foreground(m.theme.Accent).Render("♥ VIBEPUP TUI ♥"),
313
+ "\nLoading Vibes...\n",
314
+ m.spinner.View(),
215
315
  ),
216
316
  )
217
- case stateRunning:
317
+ }
318
+
319
+ // 2. Form
320
+ if m.state == stateSetup || (m.state == stateRunning && m.runner == nil && m.selected == "new" && m.newForm.State != huh.StateCompleted) {
321
+ form := m.form.View()
218
322
  if m.selected == "new" {
219
- return boxStyle.Render(
220
- lipgloss.JoinVertical(lipgloss.Left,
221
- lipgloss.NewStyle().Foreground(babyBlue).Bold(true).Render("♥ New Project"),
222
- lipgloss.NewStyle().Foreground(gray).Render("Describe what you want Vibepup to build"),
223
- "",
224
- m.newForm.View(),
225
- ),
226
- )
323
+ form = m.newForm.View()
227
324
  }
228
- status := lipgloss.NewStyle().Foreground(hotPink).Bold(true).Render("♥ Vibepup is Working ♥")
229
- mode := lipgloss.NewStyle().Foreground(babyBlue).Render("MODE: " + strings.ToUpper(m.selected))
230
- spinner := sparkles[m.frame%len(sparkles)]
231
-
232
- content := lipgloss.JoinVertical(lipgloss.Left,
233
- status,
234
- mode,
235
- "",
236
- lipgloss.NewStyle().Foreground(gray).Render(spinner+" Running engine... (check output below)"),
237
- "",
238
- dogRender,
239
- )
240
- return boxStyle.Render(content)
241
-
242
- case stateDone:
243
- return boxStyle.Render(
244
- lipgloss.NewStyle().Foreground(hotPink).Bold(true).Render("♥ All Done! Good Pup! ♥"),
245
- )
246
-
247
- default:
248
- return boxStyle.Render(splashContent)
325
+ return ui.BoxStyle.Render(lipgloss.JoinVertical(lipgloss.Left,
326
+ lipgloss.NewStyle().Foreground(m.theme.Accent).Render("SETUP"),
327
+ form,
328
+ ))
249
329
  }
250
- }
251
330
 
252
- func launchCmd(choice string, args []string) tea.Cmd {
253
- return func() tea.Msg {
254
- ctx := context.Background()
255
- if choice == "watch" {
256
- args = append([]string{"--watch"}, args...)
257
- }
258
- if choice == "run" {
259
- args = append([]string{"5"}, args...)
260
- }
261
- if choice == "new" {
262
- args = append([]string{"new", "A vibe-coded project"}, args...)
263
- }
331
+ // 3. Running
332
+ header := lipgloss.JoinVertical(lipgloss.Left,
333
+ lipgloss.NewStyle().Foreground(m.theme.Highlight).Render("♥ "+persona.GetStatus(m.selected, m.snark)+" ♥ "+m.spinner.View()),
334
+ motion.GetDogFrame(m.dogState, m.frame),
335
+ persona.RandomQuip(m.snark),
336
+ )
264
337
 
265
- log.Info("Starting Vibepup", "args", strings.Join(args, " "))
266
- cmd := exec.CommandContext(ctx, "bash", "-lc", "vibepup "+strings.Join(args, " "))
267
- cmd.Stdout = os.Stdout
268
- cmd.Stderr = os.Stderr
269
- cmd.Stdin = os.Stdin
270
- _ = cmd.Run()
271
- return tea.Quit()
272
- }
338
+ return ui.BoxStyle.Render(lipgloss.JoinVertical(lipgloss.Left,
339
+ header,
340
+ m.viewport.View(),
341
+ m.help.View(m.keys),
342
+ ))
273
343
  }
274
344
 
275
345
  func main() {
276
- p := tea.NewProgram(initialModel())
277
- if _, err := p.Run(); err != nil {
278
- fmt.Println("failed to start vibepup tui")
346
+ flags := config.Parse()
347
+ m := initialModel(flags)
348
+ if _, err := tea.NewProgram(m, tea.WithAltScreen()).Run(); err != nil {
349
+ fmt.Println("Error:", err)
279
350
  os.Exit(1)
280
351
  }
281
352
  }
@@ -0,0 +1,86 @@
1
+ package motion
2
+
3
+ import (
4
+ "time"
5
+
6
+ tea "github.com/charmbracelet/bubbletea"
7
+ "github.com/Nomadcxx/sysc-Go/animations"
8
+ )
9
+
10
+ type TickMsg time.Time
11
+
12
+ type Engine struct {
13
+ Quiet bool
14
+ LowPerf bool
15
+ Effect animations.Animation
16
+ }
17
+
18
+ func New(lowPerf, quiet bool) Engine {
19
+ // Initialize default effect (Matrix Rain)
20
+ // sysc-Go animations usually need initialization
21
+ // For now, we'll just placeholder it or use a simple one if available
22
+ return Engine{
23
+ Quiet: quiet,
24
+ LowPerf: lowPerf,
25
+ }
26
+ }
27
+
28
+ func (e Engine) Next() tea.Cmd {
29
+ d := time.Millisecond * 1000 / 60
30
+ if e.LowPerf {
31
+ d = time.Millisecond * 1000 / 15
32
+ }
33
+ return tea.Tick(d, func(t time.Time) tea.Msg {
34
+ return TickMsg(t)
35
+ })
36
+ }
37
+
38
+ // ASCII Dog Sprites
39
+ var DogSleeping = []string{
40
+ " z",
41
+ " z ",
42
+ " Z ",
43
+ "૮ – ﻌ – ა",
44
+ }
45
+
46
+ var DogRunning = []string{
47
+ " ",
48
+ " ",
49
+ " ",
50
+ "૮ ˶ᵔ ᵕ ᵔ˶ ა", // Frame 1
51
+ " ",
52
+ " ",
53
+ " ",
54
+ "૮ ˶• ﻌ •˶ ა", // Frame 2
55
+ }
56
+
57
+ var DogBarking = []string{
58
+ " WOOF! ",
59
+ " ",
60
+ " ",
61
+ "૮ ≧ ﻌ ≦ ა",
62
+ }
63
+
64
+ // GetDogFrame returns the current frame for the dog state
65
+ func GetDogFrame(state string, frame int) string {
66
+ switch state {
67
+ case "sleeping":
68
+ // Animate Zzz
69
+ zzz := []string{"", "z", "zz", "zzz"}
70
+ return " " + zzz[(frame/30)%4] + "\n" + DogSleeping[3]
71
+ case "barking":
72
+ if (frame/10)%2 == 0 {
73
+ return DogBarking[0] + "\n" + DogBarking[3]
74
+ }
75
+ return " \n" + DogBarking[3]
76
+ case "happy":
77
+ // Jump animation
78
+ if (frame/5)%2 == 0 {
79
+ return " \n" + "૮ ˶ᵔ ᵕ ᵔ˶ ა" // Up
80
+ }
81
+ return " \n" + "૮ ˶• ﻌ •˶ ა" // Down
82
+ default: // Running
83
+ frames := []string{"૮ ˶ᵔ ᵕ ᵔ˶ ა", "૮ ˶• ﻌ •˶ ა"}
84
+ return " \n" + frames[(frame/10)%len(frames)]
85
+ }
86
+ }