-
Notifications
You must be signed in to change notification settings - Fork 2
Feat: Display Estimated Recorded Time and Add FPS Input #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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,32 +312,46 @@ 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)" | ||
|
Comment on lines
+324
to
+327
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: Using a float for FPS and formatting with Right now FPS is parsed as Suggested implementation: if fps == "" {
fps = "30"
}
fpsInt, err := strconv.Atoi(fps)
if err != nil || fpsInt <= 0 {
m.state = stateError
m.finalMessage = "Invalid FPS value (use a positive integer)"
return m, nil
}
|
||
| 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( | ||
| "python3", | ||
| "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 { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: The hardcoded indices for inputs (0,1,2,3,4) make the menu brittle to future changes.
Multiple places now assume specific positions in
inputs(e.g. FPS at 2, output at 3, button at 4 viam.inputs[3].Value(),if m.focusIndex == 4, etc.). This will be fragile if the number or order of inputs changes.Defining named index constants (e.g.
const idxInterval = iota; idxDuration; idxFPS; idxOutput; idxButton) and deriving the button index fromlen(m.inputs)where possible would keep the logic consistent as the slice evolves.Suggested implementation:
You should also:
Search the rest of
main.gofor any remaining hardcoded uses of indices:m.focusIndex == 0,== 1,== 2,== 3and replace them withidxInterval,idxDuration,idxFPS,idxOutputrespectively.m.focusIndex == 4,m.focusIndex = 4, or comparisons like> 4,< 4should be revised to usem.buttonIndex()orlen(m.inputs)as appropriate.m.inputs[0],m.inputs[1],m.inputs[2],m.inputs[3]that are not caught by the simple patterns above (e.g. inside composite expressions) should be updated manually to useidxInterval,idxDuration,idxFPS, andidxOutput.If you ever add more inputs or change their order in the
inputsslice, only the slice construction and the index constants (if the semantic mapping changes) should need to be updated; the button will remain correctly derived fromlen(m.inputs).