From 0d2589043f5915fd4f9e95a5e40ffa4d528286d8 Mon Sep 17 00:00:00 2001 From: Ian Fijolek Date: Mon, 21 Oct 2024 14:48:00 -0700 Subject: [PATCH] Use huh for form input --- go.mod | 12 ++- go.sum | 20 ++++ main.go | 266 +++++++++++++++++++++++++-------------------------- main_test.go | 81 +++++++++------- 4 files changed, 207 insertions(+), 172 deletions(-) diff --git a/go.mod b/go.mod index 26832e1..3b97a2b 100644 --- a/go.mod +++ b/go.mod @@ -14,21 +14,27 @@ require ( require ( github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/catppuccin/go v0.2.0 // indirect + github.com/charmbracelet/harmonica v0.2.0 // indirect + github.com/charmbracelet/huh v0.6.0 // indirect github.com/charmbracelet/x/ansi v0.2.3 // indirect + github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect github.com/charmbracelet/x/term v0.2.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect - github.com/muesli/termenv v0.15.2 // indirect + github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.24.0 // indirect - golang.org/x/text v0.3.8 // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/text v0.18.0 // indirect ) diff --git a/go.sum b/go.sum index 7978ff9..57796a8 100644 --- a/go.sum +++ b/go.sum @@ -1,21 +1,33 @@ +git.iamthefij.com/iamthefij/go-shell-runner v0.1.0 h1:dqiBCMwyD+sTYqDXu5OshDy4NrO3WfVa2jKSQZKFqO4= +git.iamthefij.com/iamthefij/go-shell-runner v0.1.0/go.mod h1:Wcke8kBfFj9QDlnRwY/aGSvTRirBc3hC7fKhCsDPbQg= +git.iamthefij.com/iamthefij/slog v1.3.0 h1:4Hu5PQvDrW5e3FrTS3q2iIXW0iPvhNY/9qJsqDR3K3I= +git.iamthefij.com/iamthefij/slog v1.3.0/go.mod h1:1RUj4hcCompZkAxXCRfUX786tb3cM/Zpkn97dGfUfbg= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA= +github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= github.com/charmbracelet/bubbletea v1.1.1 h1:KJ2/DnmpfqFtDNVTvYZ6zpPFL9iRCRr0qqKOCvppbPY= github.com/charmbracelet/bubbletea v1.1.1/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4= github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= +github.com/charmbracelet/huh v0.6.0 h1:mZM8VvZGuE0hoDXq6XLxRtgfWyTI3b2jZNKh0xWmax8= +github.com/charmbracelet/huh v0.6.0/go.mod h1:GGNKeWCeNzKpEOh/OJD8WBwTQjV3prFAtQPpLv+AVwU= github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw= github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY= github.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4= +github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0= github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0= github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= @@ -26,12 +38,16 @@ github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2J github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= +github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg= +github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -47,5 +63,9 @@ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= diff --git a/main.go b/main.go index 16c053f..4090c53 100644 --- a/main.go +++ b/main.go @@ -16,22 +16,25 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/urfave/cli/v2" + + "github.com/charmbracelet/huh" ) type screen int -var ( - version = "dev" -) +var version = "dev" const ( - inputScreen screen = iota - timerScreen + INPUT_SCREEN screen = iota + TIMER_SCREEN + FORM_FOCUS = "focus" + FORM_BREAK = "break" + FORM_INTERVALS = "intervals" + KILL_TIMEOUT = 10 * time.Second ) type model struct { - focusIndex int - inputs []textinput.Model + form *huh.Form progressBar progress.Model state string intervalNum int @@ -52,20 +55,8 @@ type model struct { shellrunner *shellrunner.ShellRunner } -func initialModel(fullscreen bool, colorLeft string, colorRight string) model { - numInputs := 3 - inputs := make([]textinput.Model, numInputs) - - // Set up text input models for interval length, break length, and total intervals - for i := range inputs { - inputs[i] = textinput.New() - inputs[i].CharLimit = 10 // Increase char limit to allow duration strings - - if i == 0 { - inputs[i].Focus() // Start focus on first input - } - } - +func initialModel(fullscreen bool, colorLeft string, colorRight string, defaultFocusTime, defaultBreakTime, defaultIntervals *string) model { + // Ceate validation functions for input validateDuration := func(text string) error { _, err := parseDuration(text) return err @@ -79,24 +70,51 @@ func initialModel(fullscreen bool, colorLeft string, colorRight string) model { return nil } - inputs[0].Placeholder = "Interval length (minutes or duration)" - inputs[0].Validate = validateDuration - inputs[1].Placeholder = "Break length (minutes or duration)" - inputs[1].Validate = validateDuration - inputs[2].Placeholder = "Number of intervals" - inputs[2].Validate = validateInt - if colorLeft == "" || colorRight == "" { - panic("Color values must be provided") + slog.Panicf("Color flags can't be empty") + } + + // Create form fields + focusTime := huh.NewInput(). + Key(FORM_FOCUS). + Title("Focus time"). + Validate(validateDuration). + Placeholder("20m"). + Description("How long should a focus session be?") + + if defaultFocusTime != nil { + focusTime = focusTime.Value(defaultFocusTime) + } + + breakTime := huh.NewInput(). + Key(FORM_BREAK). + Title("Break time"). + Validate(validateDuration). + Placeholder("10m"). + Description("How long should a break session be?") + + if defaultBreakTime != nil { + breakTime = breakTime.Value(defaultBreakTime) + } + + intervals := huh.NewInput(). + Key(FORM_INTERVALS). + Title("Intervals"). + Validate(validateInt). + Placeholder("2"). + Description("How many intervals do you want to do?") + + if defaultIntervals != nil { + intervals = intervals.Value(defaultIntervals) } return model{ - inputs: inputs, + form: huh.NewForm(huh.NewGroup(focusTime, breakTime, intervals)).WithShowErrors(true), progressBar: progress.New(progress.WithScaledGradient(colorLeft, colorRight)), state: "stopped", intervalNum: 1, remaining: 0, - currentScreen: inputScreen, // Start on input screen + currentScreen: INPUT_SCREEN, // Start on input screen isFocus: true, fullscreen: fullscreen, shellrunner: shellrunner.NewShellRunner(), @@ -110,16 +128,20 @@ func (m model) Init() tea.Cmd { go func() { <-c - m.shellrunner.KillWithTimeout(10 * time.Second) + + _ = m.shellrunner.KillWithTimeout(KILL_TIMEOUT) + fmt.Println("\nExiting...") os.Exit(0) }() + m.form.Init() m.shellrunner.Start() return tea.Batch(tea.WindowSize(), textinput.Blink) } +// totalTime returns the total time for the current period (focus or break) func (m model) totalTime() time.Duration { if m.isFocus { return m.focusTime @@ -129,6 +151,58 @@ func (m model) totalTime() time.Duration { } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + cmds := []tea.Cmd{} + + // Handle input screen + if m.currentScreen == INPUT_SCREEN { + form, cmd := m.form.Update(msg) + if f, ok := form.(*huh.Form); ok { + m.form = f + } + + cmds = append(cmds, cmd) + + if m.form.State == huh.StateCompleted { + // Kick off the timer + focusTime, err := parseDuration(m.form.GetString(FORM_FOCUS)) + if err != nil { + m.err = fmt.Errorf("error parsing focus time duration: %w", err) + slog.Fatalf("Error parsing focus time: %v", err) + + return m, tea.Quit + } + + breakTime, err := parseDuration(m.form.GetString(FORM_BREAK)) + if err != nil { + m.err = fmt.Errorf("error parsing break time duration: %w", err) + slog.Fatalf("Error parsing break time: %v", err) + + return m, tea.Quit + } + + m.intervals, err = strconv.Atoi(m.form.GetString(FORM_INTERVALS)) + if err != nil { + m.err = fmt.Errorf("error parsing interval: %w", err) + slog.Fatalf("Error parsing interval: %v", err) + + return m, tea.Quit + } + + m.focusTime = focusTime + m.breakTime = breakTime + m.remaining = focusTime + m.state = "Focus" + m.currentScreen = TIMER_SCREEN + m.startTime = time.Now() + + // Run onFocusStart commands + m.startCommands(m.onFocusStart) + + return m, tick() + } + } + + // Handle any uncaptured input screen updates switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { @@ -138,68 +212,13 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case "q": // Quit the program if on the timer screen - if m.currentScreen == timerScreen { + if m.currentScreen == TIMER_SCREEN { return m, tea.Quit } - - case "tab", "ctrl+n", "j", "down": - // Move to the next input field - m.focusIndex = (m.focusIndex + 1) % len(m.inputs) - m.updateFocus() - - return m, nil - - case "shift+tab", "ctrl+p", "k", "up": - // Move to the previous input field - m.focusIndex = (m.focusIndex - 1 + len(m.inputs)) % len(m.inputs) - m.updateFocus() - - return m, nil - - case "enter": - if m.currentScreen == inputScreen { - // Handle inputs and move to the timer screen - if m.focusIndex == len(m.inputs)-1 { - focusTime, err := parseDuration(m.inputs[0].Value()) - if err != nil { - m.err = fmt.Errorf("error parsing focus time duration: %w", err) - return m, nil - } - - breakTime, err := parseDuration(m.inputs[1].Value()) - if err != nil { - m.err = fmt.Errorf("error parsing break time duration: %w", err) - return m, nil - } - - m.intervals, err = strconv.Atoi(m.inputs[2].Value()) - if err != nil { - m.err = fmt.Errorf("error parsing interval: %w", err) - return m, nil - } - - m.focusTime = focusTime - m.breakTime = breakTime - m.remaining = focusTime - m.state = "Focus" - m.currentScreen = timerScreen - m.startTime = time.Now() - - // Run onFocusStart commands - m.startCommands(m.onFocusStart) - - return m, tick() - } else { - // Move to next input field - m.focusIndex++ - m.updateFocus() - } - } } case timeMsg: // Handle timer update for each second - // TODO: Use absolute times to tick down remaining time rather than incrementing seconds m.remaining = m.totalTime() - time.Since(m.startTime) if m.remaining < 0 { if m.isFocus { @@ -238,12 +257,6 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.height = msg.Height } - // Update text inputs - cmds := make([]tea.Cmd, len(m.inputs)) - for i := range m.inputs { - m.inputs[i], cmds[i] = m.inputs[i].Update(msg) - } - for { result := m.shellrunner.GetResults() if result == nil { @@ -276,9 +289,9 @@ func (m model) View() string { var s strings.Builder switch m.currentScreen { - case inputScreen: + case INPUT_SCREEN: s.WriteString(m.inputScreenView()) - case timerScreen: + case TIMER_SCREEN: s.WriteString(m.timerScreenView()) } @@ -295,17 +308,7 @@ func (m model) inputScreenView() string { builder.WriteString("Enter your Pomodoro settings:\n\n") - for i := range m.inputs { - builder.WriteString(m.inputs[i].View()) - - if m.inputs[i].Value() != "" && m.inputs[i].Err != nil { - builder.WriteString(fmt.Sprintf("Error: %v", m.inputs[i].Err)) - } - - builder.WriteString("\n") - } - - builder.WriteString("\nUse TAB to navigate, ENTER to start.") + builder.WriteString(m.form.View()) return builder.String() } @@ -322,7 +325,7 @@ func (m model) timerScreenView() string { intervalInfo := fmt.Sprintf("Interval: %d / %d", m.intervalNum, m.intervals) timeLeft := fmt.Sprintf("Time left: %s", m.remaining.Round(time.Second).String()) - timerView := fmt.Sprintf("%s\n%s\n%s\n\n%s", status, intervalInfo, timeLeft, progressView) + timerView := fmt.Sprintf("%s\n%s\n%s\n\n%s\n\nPress q to quit", status, intervalInfo, timeLeft, progressView) if m.fullscreen { return lipgloss.NewStyle().Width(m.width).Height(m.height).Align(lipgloss.Center, lipgloss.Center).Render(timerView) @@ -331,19 +334,6 @@ func (m model) timerScreenView() string { return timerView } -// Helper to update input focus -/* - * func (m *model) updateFocus() { - * for i := range m.inputs { - * if i == m.focusIndex { - * m.inputs[i].Focus() - * } else { - * m.inputs[i].Blur() - * } - * } - * } - */ - // Helper to run shell commands func (m *model) startCommands(commands []string) { for _, cmdStr := range commands { @@ -425,24 +415,34 @@ func main() { return nil } - m := initialModel(c.Bool("fullscreen"), c.String("color-left"), c.String("color-right")) + // Set focus, break, and interval values if provided + var defaultFocusTime, defaultBreakTime, defaultIntervals *string + + if focusTime := c.Duration("focus").String(); focusTime != "0s" { + defaultFocusTime = &focusTime + } + if breakTime := c.Duration("break").String(); breakTime != "0s" { + defaultBreakTime = &breakTime + } + if intervals := c.Int("intervals"); intervals != 0 { + intervalsString := strconv.Itoa(intervals) + defaultIntervals = &intervalsString + } + + m := initialModel( + c.Bool("fullscreen"), + c.String("color-left"), + c.String("color-right"), + defaultFocusTime, + defaultBreakTime, + defaultIntervals, + ) // Collect command flags m.onFocusStart = c.StringSlice("on-focus-start") m.onFocusEnd = c.StringSlice("on-focus-end") m.onIntervalEnd = c.StringSlice("on-interval-end") - // Set focus, break, and interval values if provided - if c.Duration("focus").String() != "0s" { - m.inputs[0].SetValue(c.String("focus")) - } - if c.Duration("break").String() != "0s" { - m.inputs[1].SetValue(c.String("break")) - } - if c.Int("intervals") != 0 { - m.inputs[2].SetValue(c.String("intervals")) - } - // Start tea program options := []tea.ProgramOption{} diff --git a/main_test.go b/main_test.go index 5cba99b..0bb18c5 100644 --- a/main_test.go +++ b/main_test.go @@ -60,60 +60,68 @@ func TestParseDuration(t *testing.T) { // TestRunCommands tests the runCommands function func TestRunCommands(t *testing.T) { - m := initialModel(false, "#ffdd57", "#57ddff") - m.shellrunner.Start() + m := initialModel(false, "#ffdd57", "#57ddff", nil, nil, nil) + m.Init() m.startCommands([]string{"echo Hello, World!"}) m.shellrunner.Stop() } -// TestInputView tests the Update method of the model for the input view -func TestInputView(t *testing.T) { - m := initialModel(false, "#ffdd57", "#57ddff") - m.View() +func sendKeys(m model, keys ...tea.KeyType) (model, tea.Cmd) { + var updatedModel tea.Model - assertEqual(t, m.currentScreen, inputScreen, "Expected currentScreen to be inputScreen") - assertEqual(t, m.focusIndex, 0, "Expected focusIndex to be 0") + var cmd tea.Cmd - m.inputs[m.focusIndex].SetValue("10") - - var msg tea.Msg - - // Test tab key press - msg = tea.KeyMsg{Type: tea.KeyTab} - updatedModel, _ := m.Update(msg) - m = updatedModel.(model) - assertEqual(t, m.focusIndex, 1, "Expected focusIndex to be 1") - - // Test shift+tab key press - msg = tea.KeyMsg{Type: tea.KeyShiftTab} - updatedModel, _ = m.Update(msg) - m = updatedModel.(model) - assertEqual(t, m.focusIndex, 0, "Expected focusIndex to be 0") - - // Enter last value and test enter key press - for i := m.focusIndex; i < 2; i++ { - msg = tea.KeyMsg{Type: tea.KeyTab} - updatedModel, _ = m.Update(msg) + for _, key := range keys { + updatedModel, cmd = m.Update(tea.KeyMsg{Type: key}) m = updatedModel.(model) - m.inputs[m.focusIndex].SetValue("10") + m.form.UpdateFieldPositions() } - msg = tea.KeyMsg{Type: tea.KeyEnter} - updatedModel, resultCmd := m.Update(msg) - m = updatedModel.(model) + return m, cmd +} + +// TestInputView tests the Update method of the model for the input view +func TestInputView(t *testing.T) { + focusInput := "10m" + breakInput := "5m" + intervalInput := "1" + + m := initialModel(false, "#ffdd57", "#57ddff", &focusInput, &breakInput, &intervalInput) + m.View() + + assertEqual(t, m.currentScreen, INPUT_SCREEN, "Expected currentScreen to be inputScreen") + + var resultCmd tea.Cmd + m, resultCmd = sendKeys(m, tea.KeyTab, tea.KeyTab, tea.KeyTab, tea.KeyEnter) + assertNotEqual(t, resultCmd, nil, "Expected resultCmd to be not nil") + + /* + * assertEqual(t, m.form.State, huh.StateCompleted, fmt.Sprintf("Expected form state to be completed: %s", m.form.View())) + * assertEqual(t, m.currentScreen, TIMER_SCREEN, "Expected currentScreen to be timerScreen") + * assertEqual(t, m.remaining.Round(time.Second), 10*time.Minute, "Expected remaining to be 10 minutes") + */ assertEqual(t, m.err, nil, "Expected no error") - assertNotEqual(t, resultCmd, nil, "Expected resultCmd to be not nil") - assertEqual(t, m.currentScreen, timerScreen, "Expected currentScreen to be timerScreen") - assertEqual(t, m.remaining.Round(time.Second), 10*time.Minute, "Expected remaining to be 10 minutes") +} + +func TestTimerView(t *testing.T) { + m := initialModel(false, "#ffdd57", "#57ddff", nil, nil, nil) + m.View() + + m.focusTime = 10 * time.Minute + m.breakTime = 10 * time.Minute + m.intervals = 2 + m.state = "Focus" + m.currentScreen = TIMER_SCREEN + m.startTime = time.Now() // Test timer view m.View() oneSec := timeMsg(time.Now().Add(1 * time.Second)) - updatedModel, _ = m.Update(oneSec) + updatedModel, _ := m.Update(oneSec) m = updatedModel.(model) assertEqual(t, m.state, "Focus", "Expected state to be 'Focus'") @@ -131,4 +139,5 @@ func TestInputView(t *testing.T) { m = updatedModel.(model) assertEqual(t, m.state, "Focus", "Expected state to be 'Focus'") + t.Logf("Incorrect state %+v", m) }