gralculator/spec.md
Grok 0c7473943d chore: initialize git repository and create docs/ with ARCHITECTURE.md and UI_DESIGN.md
- Capture key decisions (single BASE cycle with Tab to reserve A-F, decimal math + display formatting only, CERR policy on non-integers, two-row galculator-inspired display, gostations lipgloss patterns).
- Establish docs/ as home for architecture and design notes (per request).
- Include references to spec.md and gostations patterns.
- Paper trail begins here for backtracking.
2026-06-06 14:28:30 +01:00

19 KiB
Raw Permalink Blame History

Gralculator

Gralculator — Greg's Calculator. A terminal UI calculator.

Portmanteau of "Greg" + "calculator". Goal: a fast, keyboard-centric TUI whose display layout closely follows galculator (one large top display area for the number + one small row directly below it showing the current display base), while performing all calculations in decimal. A single "BASE" key cycles the display formatting (DEC → HEX → BIN → OCT). Bases are purely for formatting the displayed value.

Goals

  • Display layout that matches the galculator spirit: one large prominent area for the number (big visual weight) + one small bottom row inside or immediately under the display showing the current base ("DEC", "HEX", "BIN", or "OCT"), with the active one highlighted.
  • A single "BASE" button/key that cycles the display base. This reduces the number of keys that need to be laid out and rendered in the keypad.
  • Core arithmetic for MVP: +, -, *, /, and MOD (modulo). Plus C (clear entry) and AC (all clear).
  • All internal calculations are performed in decimal. The current base only changes how the result/value is formatted and shown in the large display (with CERR flash on non-integer values when BASE is pressed). No base-specific digit entry or bitwise math in v1.
  • Clean, keyboard-primary TUI that feels immediate.
  • Single static Go binary that launches from the REXX ncurses ~/.local/bin/apps launcher (exactly like gostations).
  • Reuse the proven TUI patterns and aesthetics from gostations (lipgloss-centered content-sized card, subtle inner borders, LCD-style display treatment, button cells, action flashes via 140 ms ticks + color 63, minimal non-wrapping hint row, proper centering on resize).
  • Small, pure, testable calculation engine separate from the Bubble Tea UI layer.
  • Project hygiene matching the rest of apps/ (gostations/grokkit): Makefile with build/install/test, easy make install to ~/.local/bin/gralculator.

Reference

Primary visual reference: galculator GTK (screenshot provided during design discussion).

Key elements observed and targeted for the TUI port:

  • Large, prominent top display area showing the current value (right-aligned in the reference).
  • Small base indicator row tightly associated with the display (just the current base name, e.g. "DEC"), since a single BASE key cycles through them. Current base is obvious at a glance.
  • Additional mode indicators (DEG/RAD/GRAD, ALG/RPN/FORM) — these are lower priority for the initial programmer focus.
  • C / AC (clear) and backspace controls near the display.
  • Wide, dense button grid that interleaves:
    • Scientific functions on the left (many can be deferred).
    • Bitwise/logic operators (AND, OR, XOR, CMP/NOT, LSH, RSH, MOD) mixed into the grid.
    • Hex digits AF placed directly in the numeric flow.
    • Memory operations (MS, MR, M+ with dropdown affordance in GUI).
    • Standard numeric keypad + arithmetic operators on the right.
  • Dark theme with clear visual hierarchy.
  • Everything important is visible without deep menus or mode switches.

In a TUI we cannot (and should not) do a pixel-perfect 1:1. We will capture the density and immediacy using character-cell grids, bordered panels, and highlighting.

MVP Scope (Target for First Usable Release)

Must have for v0.1 (narrowed per design discussion):

  • Display layout (directly from the galculator reference, simplified):

    • One large top area inside the display panel with prominent / large visual weight for the current number.
    • One small bottom row (inside or tightly coupled to the display) that simply shows the current base: "DEC", "HEX", "BIN", or "OCT". The current one is visually distinct (highlighted / bold / accent color).
  • All calculations are performed internally in decimal. The display base only affects formatting of the current value for the large number area.

  • Display base conversion rule (non-integer handling) — this is the explicit policy chosen over galculator's "truncate to int" behavior:

    • There is a single "BASE" key (on-screen button labeled BASE + a direct keyboard key). Pressing it cycles the display base: DEC → HEX → BIN → OCT → DEC.
    • When the user presses the BASE key (on-screen "BASE" button or Tab), the engine first checks if the current decimal value is effectively an integer.
    • Check: use math.Modf (or equivalent); if the fractional part is 0 (or within a tiny epsilon like 1e-10 to tolerate floating-point noise from operations like 1/3), treat it as integer.
    • If it is an integer: cycle to the next base and immediately reformat the (integer) value for the large display area in the new base. The small mode row updates to show the new current base.
    • If it has a fractional part: refuse the cycle. Do not change the display base. Instead, trigger a short error indication using the existing flash mechanism:
      • The large number area briefly shows "CERR" (for ~600 ms, with the color 63 flash style / blink for visibility).
      • After the flash, the display reverts to showing the value in the previous base (small row unchanged).
      • Example: 23 / 63.833333... (DEC). Press BASE → large display blinks "CERR", small row stays on DEC, then number returns.
    • This gives clear feedback without silently losing precision, which the user prefers over galculator's truncation. A single BASE key also reduces the number of keys that need to be rendered in the keypad grid.
  • Core operations:

    • + - * /
    • MOD (modulo / remainder)
  • Clear controls: [C] (clear current entry / value) and [AC] (all clear).

  • Standard supporting entry: decimal digits 0-9, decimal point . (for fractional results), +/- (sign change), backspace, and = / Enter to commit the pending operation.

  • Keyboard primary. Direct keys for everything above + Tab for the single BASE key (cycles DEC→HEX→BIN→OCT). On-screen "BASE" button is for visual reference and optional mouse support. Using a single BASE button (instead of four mode buttons) keeps the keypad sparse. Tab was chosen specifically to leave AF free for future hex digit entry.

  • Content-sized + centered main calculator card (lipgloss.Place + rounded outer border). Use the same subtle inner NormalBorder (color 238) treatment and "LCD" styling (dark bg ~235 + green ~46 or high-visibility fg) that was refined in gostations.

  • Action feedback: short flashes (140 ms ticks + color 63 highlight) on button/key actions, matching the volume/skip/stop flashes from the gostations player view.

  • Minimal non-wrapping hint row at the very bottom (same style as gostations).

  • gralculator -v / --version and clean exit behavior.

  • make install target that drops the binary into ~/.local/bin/gralculator.

Explicitly deferred (second major release or later):

  • % (percent)
  • Bitwise / logic: AND, OR, XOR, CMP (NOT / complement)
  • Memory (MS / MR / M+ / MC)
  • Any hex-aware entry or A-F digits (because we stay in decimal internally for v1)
  • Word size / bit masking
  • Scientific functions (the left side of the galculator grid)
  • RPN or other notations
  • History tape, clipboard, persistence of last base, etc.

This is intentionally a much smaller MVP than the initial broad spec. The goal is a working, polished-feeling decimal calculator with nice base display modes that looks and feels like the top half of the galculator window.

Proposed UI Layout (Updated per design discussion)

The display itself follows the galculator reference very closely, but simplified to a single cycling BASE:

  • One large top area (the main number display) with strong visual weight — taller, more padding, bolder/larger effective "font" treatment via lipgloss height + styling.
  • One small bottom row directly under the number (still inside the same bordered display panel or tightly coupled) that simply shows the current base: DEC (or HEX / BIN / OCT). The current one is highlighted (e.g. bold + background color 63 or the green accent). No need to show all four options at once since BASE cycles them.

Example sketch of the display portion only:

+-----------------------------------------------+
|                                               |
|                 1234.56                       |   <-- large / prominent number area
|                                               |
|                    DEC                        |   <-- small row: just the current base
+-----------------------------------------------+

(The keypad grid below will contain a single "BASE" button cell instead of four separate mode buttons. This directly addresses the request to reduce the number of keys we need to render.)

Below the display panel comes the keypad grid of button cells (using the same makeButton + Width(5).Align(Center) pattern that worked well for the control symbols in gostations playback).

The whole calculator is wrapped in a content-sized centered card (outer rounded border color 63, lipgloss.Place centering) exactly like the Winamp-style player view.

Keypad for MVP will be much sparser than the full galculator grid because we have removed memory, %, bitwise, hex letters, and scientific functions for v1. Something like a clean 4- or 5-column arithmetic keypad with MOD, C, AC, and a single "BASE" button (the direct key for BASE cycles the display formatting).

The entire view uses the gostations rendering idioms: lipgloss.JoinVertical / JoinHorizontal, inner subtle borders where helpful, flash styles on action, and a minimal hint bar at the very bottom of the window.

Display styling target (same LCD treatment refined in gostations):

  • Display panel background ~235, number foreground ~46 (classic green) or bright for visibility.
  • Subtle inner border (238).
  • The small mode row uses a dimmer style with the active base popped (bold or colored background).

Core State & Engine (Pure, Testable) — Simplified for this MVP

Because all calculations stay in decimal and bases are display-only:

  • Internal value: a simple float64 (or a small wrapper type) for the current result / accumulator. This keeps the engine tiny and familiar for basic arithmetic + modulo.
  • Pending operation state (the classic "lastOperator + lastValue" pattern used by most four-function calculators).
  • Current display base (an enum or string: "dec", "hex", "oct", "bin"). This is UI + formatting state, not math state.
  • No word size, no bit masks, no base-aware parsing for v1.

Separate the engine:

  • internal/calc package (pure Go, no Bubble Tea, easy to unit test).
  • Main types: current value (float64), pending op, displayBase.
  • Key methods: EnterDigit(d rune), EnterDecimalPoint(), SetOperator(op string), Equals(), ApplyMod(), ClearEntry(), AllClear(), ChangeSign(), Backspace(), SetDisplayBase(base).
  • FormatForDisplay() string — this is where the magic happens. It takes the internal decimal float and produces the string to show in the large display area according to the current base. For non-integer values in HEX/OCT/BIN we will need a clear policy (e.g. show integer part only + a note, or fall back to decimal, or use a fixed-point representation). This policy should be decided before the first spike.
  • The engine never cares what the current display base is until FormatForDisplay is called.

The Bubble Tea model (in internal/ui) holds:

  • A *calc.Engine (or the state it exposes).
  • Transient UI state only: current base for the small row, flash flags for recent key presses (using the same tea.Tick + clearMsg pattern as gostations), window size for centering the card. (Only one BASE button to track instead of four separate modes.)
  • The view renders the big display by calling engine.FormatForDisplay() and then a small mode row underneath it.

This is dramatically simpler than the original broad programmer-calc vision. The engine should be implementable in a few dozen lines plus tests.

The engine will expose something like:

  • CycleBase() error — advances to the next base in the cycle (DEC→HEX→BIN→OCT). Returns a special "conversion not possible" sentinel (or typed error) when the current value is non-integer. The UI layer uses the error to trigger the "CERR" flash in the big display and leaves the base unchanged.
  • FormatForDisplay() string
  • IsInteger() bool (helper, with the epsilon tolerance).
  • CurrentBase() string — returns "DEC", "HEX", "BIN", or "OCT" for the small row in the display.

MOD will use Go's math.Mod (floating-point modulo) because it is the simplest to implement initially; the user confirmed this is acceptable.

Keyboard Bindings (Initial Proposal — To Be Refined)

Global:

  • q / Ctrl+C / Esc — quit
  • Tab — BASE: cycle the display base (DEC → HEX → BIN → OCT → DEC). The small row updates to the new current base (or flashes "CERR" + stays put if the value is non-integer).
    • Tab was deliberately chosen (instead of a letter key) to reserve AF for future full hex digit entry support in a later release.

Entry:

  • 0-9 — digits (always decimal)
  • . — decimal point
  • Backspace / Delete — backspace last digit
  • c / C — clear entry (C button)
  • Ctrl+L or dedicated AC key — all clear (AC button)
  • +/- or s / S or n / N — change sign (we can pick the most natural during implementation)

Operations:

  • + - * /
  • = or Enter — commit / equals
  • m or M or % wait no — mod key or ; or a clear mnemonic for MOD (to be chosen; on-screen label will be "MOD")
  • Ctrl+C is already AC, so careful with overlap

The small bottom row simply shows the current base (e.g. "DEC"). The single "BASE" button in the keypad grid (plus the direct key) is what cycles it. The row does not need to be interactive since we only have one cycle action.

We will keep the hint row minimal and non-wrapping, exactly like the final gostations player hint.

Mouse: supported as a bonus via Bubble Tea mouse messages mapping clicks on the rendered button cells. Not required for first working version.

Project Structure (Minimal at Start, Grow to Match gostations)

apps/gralculator/
├── .gitignore
├── spec.md                 # this file
├── go.mod
├── main.go                 # entry, flags, tea.NewProgram
├── Makefile                # build, install, test, cross (copy/adapt from gostations)
├── internal/
│   ├── calc/
│   │   ├── calc.go         # engine
│   │   └── calc_test.go
│   ├── ui/
│   │   ├── ui.go           # bubbletea model + view + update (heavy custom render)
│   │   └── ui_test.go
│   └── version/
│       └── version.go
├── build/                  # (gitignored) dev builds
└── (later: README.md, CHANGELOG.md, todo/, release.sh, assets/, .gitea/...)

Follow the gostations pattern for:

  • Version injection via ldflags (Makefile + release workflow when we get there).
  • internal/version.
  • Precheck / nice errors if needed (none for a pure calc).
  • Tests from day one for the engine.

Integration Points

  • Binary name: gralculator
  • Launcher: once usable, add an entry in ~/.local/bin/apps (ProcessSelection and the two parallel arrays). We can choose a good letter later (current l is still tcalc).
  • Install target: ~/.local/bin/gralculator (exactly like gostations).
  • No external runtime dependencies beyond a modern terminal (kitty, wezterm, ghostty, etc. recommended for Unicode and 256 color, same as gostations).

Open Questions & Decisions Needed (Current as of this update)

The two most important policy questions have now been answered:

Resolved (per your latest message):

  • Non-integer base conversion: When the current decimal value has a fractional part, pressing the single BASE key (cycle) must not silently truncate (unlike galculator). Instead: keep the previous base, briefly flash "CERR" in the large display area (using the flash/tick + color 63 mechanism, held for ~600 ms), then revert to the number in the old base. Full rule (including the single BASE cycle) is documented in the "Display base conversion rule" subsection under MVP Scope.
  • MOD on floats: Use floating-point modulo (math.Mod) for the initial implementation — whichever is easiest.

Still open (ranked roughly by what we need for the first code):

  1. Classic entry model — "In-progress entry buffer" (you see the digits as you type them before committing with an operator or =) vs. immediately applying each digit to the accumulator? Classic four-function calculators usually use the buffer approach.

  2. Base change with a pending operation — If the display shows something like 123 + (or the pending left value) and you switch base, should we reformat the pending operand in the new base? (Almost certainly yes — it's just a view change.)

  3. Key for MOD — Natural key (or shifted key) for the MOD button? The on-screen label will say "MOD".

  4. Large display "big font" treatment — How do we give the number area strong visual weight? (Extra-tall with padding + bold green text, multiple lines, etc.)

  5. Spike order — Write the pure internal/calc engine first (including IsInteger() / epsilon check, TrySetBase that can return the CERR condition, and FormatForDisplay), or start with a quick single-file TUI rendering spike of the exact large-number + small-mode-row display (hard-coded values) so we can see the galculator layout live in the terminal immediately?

  6. When to scaffold the project — Add go.mod, internal/calc, internal/ui, Makefile, etc. now, or keep it to the absolute minimum source files until we have something running and pretty?

Minor tunables (exact epsilon for "is integer", whether "CERR" is the final string, flash duration, whether a tiny fractional indicator appears somewhere) can be adjusted during implementation.

Next Steps (Ready when you are)

With the display layout and MVP scope now pinned down, the logical next moves are:

  • Agree on the answers to the remaining open questions (especially entry model, spike order, and when to add go.mod + skeleton). The key decisions (CERR non-integer policy and floating-point MOD) are already documented in the MVP Scope "Display base conversion rule" subsection.
  • Stand up the minimal Go module + directory layout (internal/calc, internal/ui, main.go).
  • Implement the calc engine (very small for this scope) with good tests.
  • Build the first visual spike focused on the exact "large number + small mode row" display the user described, plus a basic keypad grid, using gostations lipgloss idioms + centering.
  • Wire just enough keys to do a full add/sub/mul/div/mod cycle and see bases switch the display.

Once that feels right, we can talk about wiring it into the REXX launcher (probably under a new letter or eventually l).

The spec has been updated in place to match exactly what you described in this message. Let me know the answers to any of the remaining questions (or changes), and whether you want me to create the Go skeleton + first engine or rendering spike next.


Spec updated 2026 to reflect narrowed MVP and galculator two-row display.