From d4191dd259819bd8841e1186fe97560b53caccb3 Mon Sep 17 00:00:00 2001 From: vandan0101 Date: Fri, 6 Feb 2026 16:12:23 +0530 Subject: [PATCH] feat: Add FPS input field to UI Introduces a new input field for 'Output FPS' in the main menu. This allows users to specify the desired frames per second for the output timelapse video, which is crucial for estimating the 'Recorded Time' accurately. - Added 'fps' textinput.Model to the application model struct. - Initialized 'fps' input with a default value of 30 in initialModel(). - Updated inputs slice size and focus handling in initialModel(). - Modified viewMenu() to render the 'Output FPS' input field. --- main.go | 80 +++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 63 insertions(+), 17 deletions(-) diff --git a/main.go b/main.go index 630207b..944005a 100644 --- a/main.go +++ b/main.go @@ -48,6 +48,7 @@ type model struct { interval textinput.Model duration textinput.Model output textinput.Model + fps textinput.Model focusIndex int inputs []textinput.Model spinner spinner.Model @@ -60,6 +61,8 @@ type model struct { recordingDone bool finalMessage string err error + intervalValue float64 + fpsValue float64 } // Styles @@ -102,7 +105,7 @@ var ( func initialModel() model { m := model{ state: stateMenu, - inputs: make([]textinput.Model, 3), + inputs: make([]textinput.Model, 4), spinner: spinner.New(), logs: make([]string, 0), } @@ -124,6 +127,13 @@ func initialModel() model { m.duration.Width = 30 m.duration.Prompt = "│ " + // Setup fps input + m.fps = textinput.New() + m.fps.Placeholder = "30" // Default to 30 FPS + m.fps.CharLimit = 10 + m.fps.Width = 30 + m.fps.Prompt = "│ " + // Setup output input m.output = textinput.New() m.output.Placeholder = "timelapse.mp4" @@ -133,7 +143,8 @@ func initialModel() model { m.inputs[0] = m.interval m.inputs[1] = m.duration - m.inputs[2] = m.output + m.inputs[2] = m.fps + m.inputs[3] = m.output m.spinner.Spinner = spinner.Dot m.spinner.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("#7D56F4")) @@ -214,26 +225,29 @@ func (m model) updateMenu(msg tea.KeyMsg) (tea.Model, tea.Cmd) { return m.startRecording() } - // Cycle through inputs + // Cycle through inputs and button if s == "up" || s == "shift+tab" { m.focusIndex-- } else { m.focusIndex++ } - if m.focusIndex > len(m.inputs) { + // Wrap around + if m.focusIndex > len(m.inputs) { // len(m.inputs) is 4. indices 0,1,2,3 for inputs, 4 for button m.focusIndex = 0 } else if m.focusIndex < 0 { - m.focusIndex = len(m.inputs) + m.focusIndex = len(m.inputs) // Go to button } - cmds := make([]tea.Cmd, len(m.inputs)) - for i := 0; i <= len(m.inputs)-1; i++ { + var cmds []tea.Cmd + for i := 0; i < len(m.inputs); i++ { if i == m.focusIndex { - cmds[i] = m.inputs[i].Focus() + // Set focused state + cmds = append(cmds, m.inputs[i].Focus()) m.inputs[i].PromptStyle = focusedStyle m.inputs[i].TextStyle = focusedStyle } else { + // Remove focused state m.inputs[i].Blur() m.inputs[i].PromptStyle = noStyle m.inputs[i].TextStyle = noStyle @@ -243,9 +257,13 @@ func (m model) updateMenu(msg tea.KeyMsg) (tea.Model, tea.Cmd) { return m, tea.Batch(cmds...) } - // Handle character input for focused field - cmd := m.updateInputs(msg) - return m, cmd + // Handle character input for focused field if it's an input field + if m.focusIndex < len(m.inputs) { + cmd := m.updateInputs(msg) + return m, cmd + } + + return m, nil } func (m model) updateRecording(msg tea.KeyMsg) (tea.Model, tea.Cmd) { @@ -294,25 +312,38 @@ func (m model) startRecording() (tea.Model, tea.Cmd) { return m, nil } - output := m.inputs[2].Value() + output := m.inputs[3].Value() if output == "" { output = "timelapse.mp4" } + fps := m.inputs[2].Value() + if fps == "" { + fps = "30" + } + fpsFloat, err := strconv.ParseFloat(fps, 64) + if err != nil || fpsFloat <= 0 { + m.state = stateError + m.finalMessage = "Invalid FPS value (use a positive number)" + return m, nil + } + // Change state to recording m.state = stateRecording m.startTime = time.Now() m.recordingDone = false + m.intervalValue = intervalFloat + m.fpsValue = fpsFloat // Start the Python subprocess return m, tea.Batch( m.spinner.Tick, tick(), - m.runTimelapse(intervalFloat, durationFloat, output), + m.runTimelapse(intervalFloat, durationFloat, fpsFloat, output), ) } -func (m model) runTimelapse(interval, duration float64, output string) tea.Cmd { +func (m model) runTimelapse(interval, duration, fps float64, output string) tea.Cmd { return func() tea.Msg { // Build command cmd := exec.Command( @@ -320,6 +351,7 @@ func (m model) runTimelapse(interval, duration float64, output string) tea.Cmd { "timelapse.py", "-i", fmt.Sprintf("%.2f", interval), "-d", fmt.Sprintf("%.2f", duration), + "-f", fmt.Sprintf("%.2f", fps), "-o", output, ) @@ -438,8 +470,8 @@ func (m model) viewMenu() string { b.WriteString(label + "\n") b.WriteString(m.inputs[1].View() + "\n\n") - // Output input - label = "Output file:" + // FPS input + label = "Output FPS:" if m.focusIndex == 2 { label = focusedStyle.Render("▸ " + label) } else { @@ -448,9 +480,19 @@ func (m model) viewMenu() string { b.WriteString(label + "\n") b.WriteString(m.inputs[2].View() + "\n\n") + // Output input + label = "Output file:" + if m.focusIndex == 3 { + label = focusedStyle.Render("▸ " + label) + } else { + label = blurredStyle.Render(" " + label) + } + b.WriteString(label + "\n") + b.WriteString(m.inputs[3].View() + "\n\n") + // Start button button := "[ Start Recording ]" - if m.focusIndex == 3 { + if m.focusIndex == 4 { button = focusedStyle.Render("▸ " + button) } else { button = blurredStyle.Render(" " + button) @@ -483,6 +525,10 @@ func (m model) viewRecording() string { } b.WriteString(fmt.Sprintf("\nElapsed: %s\n", elapsed.Round(time.Second))) + if m.fpsValue > 0 { + recordedTime := time.Duration(float64(m.progress.current)/m.fpsValue*float64(time.Second)) + b.WriteString(fmt.Sprintf("Recorded: %s\n", recordedTime.Round(time.Second))) + } // Show recent logs if len(m.logs) > 0 {