Skip to content

refactor: make tui size dynamic and make settings modular#336

Merged
SuperCoolPencil merged 6 commits intomainfrom
dynamic-tui-sizing
Apr 8, 2026
Merged

refactor: make tui size dynamic and make settings modular#336
SuperCoolPencil merged 6 commits intomainfrom
dynamic-tui-sizing

Conversation

@SuperCoolPencil
Copy link
Copy Markdown
Member

@SuperCoolPencil SuperCoolPencil commented Apr 8, 2026

  • refactor: centralize TUI layout calculations using dynamic lipgloss frame size helpers
  • refactor: move category-related settings into a dedicated Categories struct within Settings
  • fix: truncate long labels in settings view to prevent layout breakage

Greptile Summary

This PR refactors the TUI to replace magic numeric offsets with lipgloss frame-size helpers for dynamic layout calculations, and splits category-related settings (CategoryEnabled, Categories) out of GeneralSettings into a new top-level CategorySettings struct. It also adds label truncation to the settings list view to prevent layout overflow.

  • Breaking settings migration: Moving CategoryEnabled/Categories from JSON key "general" to a new top-level key "categories" means any existing settings.json that had category sorting enabled will silently reset to defaults (CategoryEnabled = false, Categories = DefaultCategories()) on next launch — no migration path is provided.
  • The label-truncation fix in view_settings.go uses len() (byte count) rather than lipgloss.Width() (display columns) for prefix and label measurement, causing the selected-row label to truncate slightly earlier than necessary.

Confidence Score: 4/5

Safe to merge with awareness of the settings migration break; the layout refactor and struct split are otherwise correct.

One P1 finding: moving CategorySettings to a new JSON top-level key with no migration silently drops existing users' category configuration on upgrade. All other changes (lipgloss frame-size helpers, label truncation) are functionally equivalent to what they replace and are well-tested.

internal/config/settings.go — backward-incompatible JSON key change with no migration path

Vulnerabilities

No security concerns identified. The refactor touches only UI layout, settings struct organization, and JSON serialization — no authentication, network, or input-validation paths are changed.

Important Files Changed

Filename Overview
internal/config/settings.go Introduces CategorySettings struct split from GeneralSettings; backward-incompatible JSON key change silently drops existing user category config on upgrade
internal/tui/view_settings.go New label truncation uses byte-length comparisons instead of display-width; all current labels are ASCII so no crash today, but selected-row prefix byte/rune mismatch causes premature truncation
internal/tui/components/box.go Replaces magic literal -1 with lipgloss.Width(horizontal) in remaining-width calculations; semantically equivalent and more self-documenting
internal/tui/components/confirmation_modal.go Replaces magic offsets (-4, -2, -10, -1) with lipgloss frame-size helpers; all values are equivalent to the originals
internal/processing/file_utils.go Updated to reference settings.Categories instead of settings.General for CategoryEnabled/Categories; logic is unchanged
internal/config/settings_test.go Adds tests for CategoryOrder count (4), empty-categories round-trip, and metadata coverage; thorough coverage of new CategorySettings struct
docs/SETTINGS.md Documents the new top-level categories section with category_enabled; accurate and consistent with the new struct layout

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[settings.json on disk] -->|LoadSettings| B{Top-level key present?}
    B -->|Old format: general.category_enabled| C[GeneralSettings - field removed, silently ignored]
    B -->|New format: categories.category_enabled| D[CategorySettings.CategoryEnabled OK]
    C --> E[CategorySettings falls back to defaults, CategoryEnabled=false]
    D --> F[Category routing works correctly]
    E -->|User impact| G[Category sorting silently disabled, Custom categories lost]

    subgraph NewStruct[Refactored Settings Struct]
        H[Settings]
        H --> I[GeneralSettings - json: general]
        H --> J[NetworkSettings - json: network]
        H --> K[PerformanceSettings - json: performance]
        H --> L[CategorySettings NEW - json: categories]
        L --> M[CategoryEnabled bool]
        L --> N[Categories slice]
    end
Loading

Comments Outside Diff (3)

  1. internal/config/settings.go, line 223-243 (link)

    P1 Silent data loss for users with existing category settings

    Moving category_enabled and categories from the "general" JSON key to a new top-level "categories" key is a breaking schema change. LoadSettings() initialises from DefaultSettings() (which resets CategoryEnabled to false and Categories to DefaultCategories()) and then unmarshals the stored JSON on top. Any existing file that contains "general": {"category_enabled": true, "categories": [...]} will have no "categories" top-level key, so the unmarshal leaves those fields at their defaults — silently discarding the user's configuration.

    A one-time migration in LoadSettings() would prevent data loss, for example:

    // After unmarshaling, migrate legacy general.category_enabled / general.categories
    type legacySettings struct {
        General struct {
            CategoryEnabled bool       `json:"category_enabled"`
            Categories      []Category `json:"categories"`
        } `json:"general"`
    }
    var legacy legacySettings
    if json.Unmarshal(data, &legacy) == nil && (legacy.General.CategoryEnabled || len(legacy.General.Categories) > 0) {
        settings.Categories.CategoryEnabled = legacy.General.CategoryEnabled
        if len(legacy.General.Categories) > 0 {
            settings.Categories.Categories = legacy.General.Categories
        }
    }
    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: internal/config/settings.go
    Line: 223-243
    
    Comment:
    **Silent data loss for users with existing category settings**
    
    Moving `category_enabled` and `categories` from the `"general"` JSON key to a new top-level `"categories"` key is a breaking schema change. `LoadSettings()` initialises from `DefaultSettings()` (which resets `CategoryEnabled` to `false` and `Categories` to `DefaultCategories()`) and then unmarshals the stored JSON on top. Any existing file that contains `"general": {"category_enabled": true, "categories": [...]}` will have no `"categories"` top-level key, so the unmarshal leaves those fields at their defaults — silently discarding the user's configuration.
    
    A one-time migration in `LoadSettings()` would prevent data loss, for example:
    
    ```go
    // After unmarshaling, migrate legacy general.category_enabled / general.categories
    type legacySettings struct {
        General struct {
            CategoryEnabled bool       `json:"category_enabled"`
            Categories      []Category `json:"categories"`
        } `json:"general"`
    }
    var legacy legacySettings
    if json.Unmarshal(data, &legacy) == nil && (legacy.General.CategoryEnabled || len(legacy.General.Categories) > 0) {
        settings.Categories.CategoryEnabled = legacy.General.CategoryEnabled
        if len(legacy.General.Categories) > 0 {
            settings.Categories.Categories = legacy.General.Categories
        }
    }
    ```
    
    How can I resolve this? If you propose a fix, please make it concise.
  2. docs/SETTINGS.md, line 14-29 (link)

    P1 docs/SETTINGS.md not updated for new categories section

    The documented example JSON still shows only general, network, and performance. After this PR, category settings live under a top-level "categories" key and the guide omits that section entirely, leaving users with no reference for the new structure.

    The example should be updated to:

    {
      "general": { ... },
      "network": { ... },
      "performance": { ... },
      "categories": {
        "category_enabled": true
      }
    }

    A new Category Settings table should also be added documenting category_enabled (the only user-editable key in that section).

    Context Used: docs/SETTINGS.md (source)

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: docs/SETTINGS.md
    Line: 14-29
    
    Comment:
    **`docs/SETTINGS.md` not updated for new `categories` section**
    
    The documented example JSON still shows only `general`, `network`, and `performance`. After this PR, category settings live under a top-level `"categories"` key and the guide omits that section entirely, leaving users with no reference for the new structure.
    
    The example should be updated to:
    
    ```json
    {
      "general": { ... },
      "network": { ... },
      "performance": { ... },
      "categories": {
        "category_enabled": true
      }
    }
    ```
    
    A new **Category Settings** table should also be added documenting `category_enabled` (the only user-editable key in that section).
    
    **Context Used:** docs/SETTINGS.md ([source](https://app.greptile.com/review/custom-context?memory=c9c02a40-4d2f-4e1b-8b2c-d98b2c265e1f))
    
    How can I resolve this? If you propose a fix, please make it concise.
  3. internal/tui/view_settings.go, line 393-413 (link)

    P2 Remaining hardcoded layout constants inconsistent with PR goals

    renderSettingsTwoColumn still hardcodes two offsets that were explicitly the target of this refactor:

    • rightRows := bodyHeight - 2 — the rightBox style is Padding(1, 2) (vertical frame = 2), so this should use the style's GetVerticalFrameSize().
    • rightContent := m.renderSettingsDetailBlock(..., rightWidth-4, ...) — the same Padding(1, 2) gives a horizontal frame of 4, so GetHorizontalFrameSize() would be clearer.

    Suggested approach:

    rightBoxStyle := lipgloss.NewStyle().Width(rightWidth).Padding(1, 2)
    rightRows := bodyHeight - rightBoxStyle.GetVerticalFrameSize()
    if rightRows < 1 {
        rightRows = 1
    }
    rightContent := m.renderSettingsDetailBlock(
        settingsMeta, selectedRow, settingsValues,
        rightWidth-rightBoxStyle.GetHorizontalFrameSize(), rightRows)
    rightBox := rightBoxStyle.Render(rightContent)
    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: internal/tui/view_settings.go
    Line: 393-413
    
    Comment:
    **Remaining hardcoded layout constants inconsistent with PR goals**
    
    `renderSettingsTwoColumn` still hardcodes two offsets that were explicitly the target of this refactor:
    
    - `rightRows := bodyHeight - 2` — the `rightBox` style is `Padding(1, 2)` (vertical frame = 2), so this should use the style's `GetVerticalFrameSize()`.
    - `rightContent := m.renderSettingsDetailBlock(..., rightWidth-4, ...)` — the same `Padding(1, 2)` gives a horizontal frame of 4, so `GetHorizontalFrameSize()` would be clearer.
    
    Suggested approach:
    
    ```go
    rightBoxStyle := lipgloss.NewStyle().Width(rightWidth).Padding(1, 2)
    rightRows := bodyHeight - rightBoxStyle.GetVerticalFrameSize()
    if rightRows < 1 {
        rightRows = 1
    }
    rightContent := m.renderSettingsDetailBlock(
        settingsMeta, selectedRow, settingsValues,
        rightWidth-rightBoxStyle.GetHorizontalFrameSize(), rightRows)
    rightBox := rightBoxStyle.Render(rightContent)
    ```
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: internal/config/settings.go
Line: 43-46

Comment:
**Breaking backward compatibility — silent settings loss on upgrade**

Moving `CategoryEnabled` and `Categories` from `GeneralSettings` (JSON key `"general"`) to a new top-level `CategorySettings` (JSON key `"categories"`) breaks backward compatibility with every existing `settings.json`. On the next launch the Go JSON decoder ignores the old `"general"."category_enabled"` and `"general"."categories"` fields, and because there is no `"categories"` top-level key in the old file, `CategorySettings` falls back to its defaults: `CategoryEnabled = false` and `Categories = DefaultCategories()`.

Users who had category sorting enabled and custom categories defined will silently:
1. Have category routing disabled.
2. Lose all custom category rules, replaced by the defaults.

Consider adding a one-time migration in `LoadSettings` that reads the old field locations and promotes them to the new structure before the first save, similar to what many apps do with a schema-version field.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: internal/tui/view_settings.go
Line: 291-305

Comment:
**Byte length used where display width is needed**

`len(prefix)` counts bytes, not terminal columns. The selected-row prefix `"▸ "` is `"▸"` (U+25B8, 3 bytes in UTF-8) + `" "` = 4 bytes but only 2 display columns, so `maxLabelLen` ends up 2 columns narrower than it should be — causing the label to truncate slightly earlier than necessary for the focused row.

Likewise, `len(label)` and `label[:maxLabelLen-3]` use byte indexing; if a future label contained a multibyte character at the cut point this would panic or produce garbled output.

```suggestion
		maxLabelLen := innerWidth - lipgloss.Width(prefix)
		if maxLabelLen < 0 {
			maxLabelLen = 0
		}

		// Truncate to avoid line wrapping which breaks parent height constraints
		if lipgloss.Width(label) > maxLabelLen {
			runes := []rune(label)
			if maxLabelLen > 3 {
				label = string(runes[:maxLabelLen-3]) + "..."
			} else {
				label = string(runes[:maxLabelLen])
			}
		}
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (2): Last reviewed commit: "style: fix some inconsistent hardcoded v..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 8, 2026

Binary Size Analysis

⚠️ Size Increased

Version Human Readable Raw Bytes
Main 18.91 MB 19829028
PR 18.95 MB 19874084
Difference 44.00 KB 45056

Comment on lines +43 to +46
type CategorySettings struct {
CategoryEnabled bool `json:"category_enabled" ui_label:"Manage Categories" ui_desc:"Sort downloads into subfolders by file type. Press Enter to open Category Manager."`
Categories []Category `json:"categories" ui_ignored:"true"`
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Breaking backward compatibility — silent settings loss on upgrade

Moving CategoryEnabled and Categories from GeneralSettings (JSON key "general") to a new top-level CategorySettings (JSON key "categories") breaks backward compatibility with every existing settings.json. On the next launch the Go JSON decoder ignores the old "general"."category_enabled" and "general"."categories" fields, and because there is no "categories" top-level key in the old file, CategorySettings falls back to its defaults: CategoryEnabled = false and Categories = DefaultCategories().

Users who had category sorting enabled and custom categories defined will silently:

  1. Have category routing disabled.
  2. Lose all custom category rules, replaced by the defaults.

Consider adding a one-time migration in LoadSettings that reads the old field locations and promotes them to the new structure before the first save, similar to what many apps do with a schema-version field.

Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/config/settings.go
Line: 43-46

Comment:
**Breaking backward compatibility — silent settings loss on upgrade**

Moving `CategoryEnabled` and `Categories` from `GeneralSettings` (JSON key `"general"`) to a new top-level `CategorySettings` (JSON key `"categories"`) breaks backward compatibility with every existing `settings.json`. On the next launch the Go JSON decoder ignores the old `"general"."category_enabled"` and `"general"."categories"` fields, and because there is no `"categories"` top-level key in the old file, `CategorySettings` falls back to its defaults: `CategoryEnabled = false` and `Categories = DefaultCategories()`.

Users who had category sorting enabled and custom categories defined will silently:
1. Have category routing disabled.
2. Lose all custom category rules, replaced by the defaults.

Consider adding a one-time migration in `LoadSettings` that reads the old field locations and promotes them to the new structure before the first save, similar to what many apps do with a schema-version field.

How can I resolve this? If you propose a fix, please make it concise.

@SuperCoolPencil SuperCoolPencil merged commit bc497df into main Apr 8, 2026
15 checks passed
@SuperCoolPencil SuperCoolPencil deleted the dynamic-tui-sizing branch April 8, 2026 19:52
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.

1 participant