Skip to content

Feat: Display Estimated Recorded Time and Add FPS Input#4

Open
vandan0101 wants to merge 1 commit into
xevrion:mainfrom
vandan0101:fix/display-time
Open

Feat: Display Estimated Recorded Time and Add FPS Input#4
vandan0101 wants to merge 1 commit into
xevrion:mainfrom
vandan0101:fix/display-time

Conversation

@vandan0101

@vandan0101 vandan0101 commented Feb 6, 2026

Copy link
Copy Markdown

Problem Statement:
Users lacked direct feedback on the estimated duration of their final timelapse video during recording, making it difficult to
plan and assess progress, especially with varying capture intervals and output FPS.

Solution:
This PR introduces a "Recorded Time" indicator to the live progress output in the TUI, which estimates the final video
duration based on the number of frames captured and the specified output FPS. To facilitate this, a new input field for
"Output FPS" has been added to the main menu.

Key Changes:

  • main.go:
    • Added a new textinput.Model for "Output FPS" in the model struct.
    • Integrated the FPS input into the UI's menu (initialModel, updateMenu, viewMenu), allowing users to specify the output
      video's frames per second.
    • Modified startRecording to parse and validate the FPS input.
    • Updated runTimelapse to pass the user-defined FPS to the timelapse.py script via a new -f command-line argument.
    • Stored intervalValue and fpsValue in the model struct for use in calculations.
    • Implemented the calculation and display of "Recorded Time" in viewRecording, showing the estimated duration based on
      frames_captured / output_fps.
  • timelapse.py:
    • No changes were required in timelapse.py as it already supported receiving an fps argument for video compilation.

How to Test:

  1. Run Chronapse (./chronapse).
  2. In the main menu, navigate to the new "Output FPS" field. Enter a value (e.g., 24 or 30).
  3. Set "Interval" and "Duration" as desired.
  4. Start recording.
  5. Observe the "Recorded" time displayed in the TUI during the recording phase. Verify that it updates correctly based on the
    number of captured frames and the FPS value you entered.
  6. Compare the estimated "Recorded" time with the actual duration of the generated video after compilation (e.g., using
    ffprobe or a media player).

Use Cases Addressed:

  • Long-duration timelapses: Users can now see a real-time estimate of the final video length.
  • Experimenting with settings: Instant feedback on how interval/FPS changes affect final duration.
  • Resource planning: Better estimation of output size and duration.
  • Live monitoring: Provides intuitive progress feedback in TUI dashboards.

Summary by Sourcery

Add an output FPS configuration to the TUI and surface an estimated final video duration during recording.

New Features:

  • Introduce an Output FPS field in the main menu so users can configure the frame rate of the generated timelapse video.
  • Display a Recorded time indicator during recording that estimates final video duration based on captured frames and configured FPS.

Enhancements:

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.
@sourcery-ai

sourcery-ai Bot commented Feb 6, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds an FPS input to the main menu, wires it through to the timelapse subprocess, stores parsed interval/FPS values on the model, and uses them to display an estimated recorded video duration during recording in the TUI.

Sequence diagram for starting recording with FPS and displaying recorded time

sequenceDiagram
    actor User
    participant TUIApp
    participant TimelapseProcess

    User->>TUIApp: Navigate menu and enter interval, duration, fps, output
    User->>TUIApp: Press Start Recording

    TUIApp->>TUIApp: startRecording()
    TUIApp->>TUIApp: Parse interval and duration
    TUIApp->>TUIApp: Parse fps input
    TUIApp->>TUIApp: Validate fpsFloat > 0
    alt invalid fps
        TUIApp->>User: Show error state "Invalid FPS value"
    else valid fps
        TUIApp->>TUIApp: Set stateRecording, startTime
        TUIApp->>TUIApp: Store intervalValue, fpsValue
        TUIApp->>TimelapseProcess: runTimelapse(interval,duration,fps,output)
    end

    loop During recording
        TimelapseProcess-->>TUIApp: Progress update (frames captured)
        TUIApp->>TUIApp: Update m.progress.current
        TUIApp->>TUIApp: Compute recordedTime = frames / fpsValue
        TUIApp->>User: Render viewRecording with Elapsed and Recorded times
    end
Loading

Class diagram for updated model struct and related methods

classDiagram
    class model {
        %% Fields
        textinput.Model interval
        textinput.Model duration
        textinput.Model output
        textinput.Model fps
        int focusIndex
        []textinput.Model inputs
        spinner.Model spinner
        bool recordingDone
        string finalMessage
        error err
        float64 intervalValue
        float64 fpsValue

        %% Methods (key ones touched by this PR)
        startRecording() (tea.Model, tea.Cmd)
        runTimelapse(interval float64, duration float64, fps float64, output string) tea.Cmd
        updateMenu(msg tea.KeyMsg) (tea.Model, tea.Cmd)
        viewMenu() string
        viewRecording() string
    }

    class initialModel {
        +initialModel() model
    }

    initialModel ..> model : constructs

    %% Show relationships to external types (simplified)
    class textinputModel {
        +New() textinput.Model
        +Placeholder string
        +CharLimit int
        +Width int
        +Prompt string
        +Focus() tea.Cmd
        +Blur()
        +View() string
        +Value() string
    }

    class spinnerModel {
        +New() spinner.Model
        +Tick tea.Cmd
    }

    model *-- textinputModel : uses interval,duration,output,fps
    model *-- spinnerModel : uses spinner
Loading

File-Level Changes

Change Details Files
Add an FPS text input field to the menu and integrate it into focus handling.
  • Extend the model struct with an fps textinput.Model and increase the inputs slice size from 3 to 4.
  • Initialize the fps input in initialModel with placeholder, limits, and prompt, and insert it into the inputs slice between duration and output.
  • Adjust focus cycling logic in updateMenu to account for four inputs plus the start button and only send key input to text fields when focusIndex refers to an input.
main.go
Parse and validate FPS on start, pass it into the timelapse subprocess, and persist numeric interval/FPS values on the model.
  • In startRecording, read the FPS field (defaulting to 30 when empty), parse it as a positive float, and surface an error state for invalid values.
  • Store parsed interval and FPS values on the model (intervalValue, fpsValue) when recording starts.
  • Update runTimelapse to accept an fps parameter and forward it as a -f argument to timelapse.py alongside interval, duration, and output.
main.go
Display estimated recorded video time during recording based on frames captured and FPS.
  • Extend the model with fpsValue and use it in viewRecording to compute a time.Duration from progress.current / fpsValue.
  • Render a new "Recorded" line in the recording view, rounded to the nearest second, whenever fpsValue is greater than zero.
main.go

Possibly linked issues

  • #[Feature]: Display the recorded video time in TUI: PR directly adds the requested TUI 'Recorded Time' indicator plus an FPS input to control duration estimation.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • The input handling relies heavily on hard-coded indices (e.g., inputs[2] as FPS, inputs[3] as output, and focusIndex == 4 for the button); consider introducing named constants or small helper methods for these positions to make future changes less error-prone.
  • intervalValue is stored on the model but not used anywhere in the updated code; if it's not needed for the recorded time calculation or other logic yet, removing it would simplify the model and reduce confusion.
  • FPS is parsed as a float and passed to timelapse.py with two decimal places even though the UI suggests integer values (e.g., 30 FPS); you may want to either restrict this to integers or clarify and consistently treat FPS as a float across the UI and CLI call.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The input handling relies heavily on hard-coded indices (e.g., `inputs[2]` as FPS, `inputs[3]` as output, and `focusIndex == 4` for the button); consider introducing named constants or small helper methods for these positions to make future changes less error-prone.
- `intervalValue` is stored on the model but not used anywhere in the updated code; if it's not needed for the recorded time calculation or other logic yet, removing it would simplify the model and reduce confusion.
- FPS is parsed as a float and passed to `timelapse.py` with two decimal places even though the UI suggests integer values (e.g., 30 FPS); you may want to either restrict this to integers or clarify and consistently treat FPS as a float across the UI and CLI call.

## Individual Comments

### Comment 1
<location> `main.go:51-54` </location>
<code_context>
 	interval   textinput.Model
 	duration   textinput.Model
 	output     textinput.Model
+	fps        textinput.Model
 	focusIndex int
 	inputs     []textinput.Model
</code_context>

<issue_to_address>
**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 via `m.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 from `len(m.inputs)` where possible would keep the logic consistent as the slice evolves.

Suggested implementation:

```golang
// buttonIndex returns the index representing the submit/confirm button.
// It is derived from the current number of inputs to avoid hardcoding.
func (m model) buttonIndex() int {
	return len(m.inputs)
}

// Styles
func initialModel() model {

```

```golang
m.inputs[idxInterval]

```

```golang
m.inputs[idxDuration]

```

```golang
m.inputs[idxFPS]

```

```golang
m.inputs[idxOutput]

```

```golang
if m.focusIndex == m.buttonIndex() {

```

```golang
m.focusIndex = m.buttonIndex()

```

You should also:

1. Search the rest of `main.go` for any remaining hardcoded uses of indices:
   - `m.focusIndex == 0`, `== 1`, `== 2`, `== 3` and replace them with `idxInterval`, `idxDuration`, `idxFPS`, `idxOutput` respectively.
   - Any `m.focusIndex == 4`, `m.focusIndex = 4`, or comparisons like `> 4`, `< 4` should be revised to use `m.buttonIndex()` or `len(m.inputs)` as appropriate.
   - Any other occurrences of `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 use `idxInterval`, `idxDuration`, `idxFPS`, and `idxOutput`.

2. If you ever add more inputs or change their order in the `inputs` slice, only the slice construction and the index constants (if the semantic mapping changes) should need to be updated; the button will remain correctly derived from `len(m.inputs)`.
</issue_to_address>

### Comment 2
<location> `main.go:324-327` </location>
<code_context>
+	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
+	}
</code_context>

<issue_to_address>
**suggestion:** Using a float for FPS and formatting with `%.2f` may be unnecessarily permissive and could lead to surprising values.

Right now FPS is parsed as `float64` and formatted to two decimals before passing to the Python script, so inputs like `29.9999` end up as `30.00` without the user realizing. If FPS should be integral, parsing as `int` (e.g. `strconv.Atoi`) would clarify intent and avoid rounding. If fractional FPS is expected, consider either preserving the original validated string when building the command or tightening validation to constrain allowed decimal places so the rounding behavior is explicit and predictable.

Suggested implementation:

```golang
	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
	}

```

1. Anywhere `fpsFloat` is used later in this file (e.g. when building the Python command), update the code to use either:
   * `fpsInt` (and format it with `%d`), or
   * the validated `fps` string directly without `"%.2f"` formatting.
2. Remove any `"%.2f"` formatting of the FPS value to avoid hidden rounding and keep the behavior aligned with the integer validation.
3. If there are function signatures or struct fields that currently use `float64` for FPS, consider changing them to `int` to keep the type consistent across the codebase.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread main.go
Comment on lines +51 to 54
fps textinput.Model
focusIndex int
inputs []textinput.Model
spinner spinner.Model

Copy link
Copy Markdown

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 via m.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 from len(m.inputs) where possible would keep the logic consistent as the slice evolves.

Suggested implementation:

// buttonIndex returns the index representing the submit/confirm button.
// It is derived from the current number of inputs to avoid hardcoding.
func (m model) buttonIndex() int {
	return len(m.inputs)
}

// Styles
func initialModel() model {
m.inputs[idxInterval]
m.inputs[idxDuration]
m.inputs[idxFPS]
m.inputs[idxOutput]
if m.focusIndex == m.buttonIndex() {
m.focusIndex = m.buttonIndex()

You should also:

  1. Search the rest of main.go for any remaining hardcoded uses of indices:

    • m.focusIndex == 0, == 1, == 2, == 3 and replace them with idxInterval, idxDuration, idxFPS, idxOutput respectively.
    • Any m.focusIndex == 4, m.focusIndex = 4, or comparisons like > 4, < 4 should be revised to use m.buttonIndex() or len(m.inputs) as appropriate.
    • Any other occurrences of 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 use idxInterval, idxDuration, idxFPS, and idxOutput.
  2. If you ever add more inputs or change their order in the inputs slice, only the slice construction and the index constants (if the semantic mapping changes) should need to be updated; the button will remain correctly derived from len(m.inputs).

Comment thread main.go
Comment on lines +324 to +327
fpsFloat, err := strconv.ParseFloat(fps, 64)
if err != nil || fpsFloat <= 0 {
m.state = stateError
m.finalMessage = "Invalid FPS value (use a positive number)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Using a float for FPS and formatting with %.2f may be unnecessarily permissive and could lead to surprising values.

Right now FPS is parsed as float64 and formatted to two decimals before passing to the Python script, so inputs like 29.9999 end up as 30.00 without the user realizing. If FPS should be integral, parsing as int (e.g. strconv.Atoi) would clarify intent and avoid rounding. If fractional FPS is expected, consider either preserving the original validated string when building the command or tightening validation to constrain allowed decimal places so the rounding behavior is explicit and predictable.

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
	}
  1. Anywhere fpsFloat is used later in this file (e.g. when building the Python command), update the code to use either:
    • fpsInt (and format it with %d), or
    • the validated fps string directly without "%.2f" formatting.
  2. Remove any "%.2f" formatting of the FPS value to avoid hidden rounding and keep the behavior aligned with the integer validation.
  3. If there are function signatures or struct fields that currently use float64 for FPS, consider changing them to int to keep the type consistent across the codebase.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Display the recorded video time in TUI

1 participant