| name | calc-u-59-swift |
| description | Use this skill when working on the Swift/SwiftUI code of Calc-U-59 (App/, App/Views/, Bridge/). Covers EmulatorViewModel, the ObjC++ bridge, display, keyboard, debug panels, printer, card reader, settings, and state file I/O. Trigger on any task that touches App/*.swift, App/Views/*.swift, Bridge/, or the Xcode project. |
| user-invocable | true |
Calc-U-59 Swift GUI
This skill covers all work inside App/, App/Views/, and Bridge/.
For TI-59 domain background (keycodes, SCOM layout, solid-state modules, program source flag) see the calc-u-59-core skill's Domain Primer. For general SwiftUI best practices, the swiftui-pro skill applies.
Architecture Overview
EmulatorViewModel (App/EmulatorViewModel.swift, 103 KB)
│ @Observable, runs the 60 Hz step loop, owns all UI state
│
├─ TI59MachineWrapper (Bridge/TI59MachineWrapper.h + .mm)
│ ObjC++ shim — the only place Swift touches C++
│
└─ App/Views/ (SwiftUI, thin bindings only)
CalculatorView main container
KeyboardView calculator image + gesture hit-test + LED overlay
LEDDisplayView 12-digit BCD LED with afterglow animation
PrinterView PC-100C tape simulation
CueCardView reference card overlay
DebugView 3-tab container: CALCULATOR / CPU / LOG
LiveDebugView CALCULATOR tab — real-time program-step scroller
CPUInspectorView CPU tab — instruction ring, register/SCOM browser, ROM heatmap
StaticDebugContent LOG tab — raw SCOM dump, trace capture toggle
SettingsView debug level, trace enabled, playback speed, module selection
CardPickerView magnetic card library picker
Note: CPUDebugView.swift exists in the repository but is not embedded in the app (only referenced in its own SwiftUI preview). Do not wire it up or confuse it with CPUInspectorView.
All UI state flows through EmulatorViewModel. Views are thin bindings — no business logic in views.
Key Files
| File | Role |
|---|
App/EmulatorViewModel.swift | Step loop, display refresh, keyboard routing, state I/O, trace writer, breakpoints, freeze logic |
App/MachineModel.swift | Enum: TI-59 / TI-58 / TI-58C variant with capabilities |
App/ROMLoader.swift | ROM asset loading and variant matching |
App/StateFileLoader.swift | Deserialize .ti59 / .ti58 / .ti58c state files |
App/CardStorage.swift | Simulated magnetic card read/write |
App/AppSettings.swift | User preferences (debug level, trace enabled, …) |
App/TraceWriter.swift | Binary trace file serialization |
App/TI59KeyNames.swift | Keycode (0–99) → mnemonic mapping for program listings |
App/RegisterLineHelpers.swift | Register-line nibble↔byte encoding/decoding helpers |
App/CueCardContent.swift | Reference-card text content per machine variant |
App/PC100CFont.swift | PC-100C printer font — auto-generated by tools/gen_font.py, do not hand-edit |
Bridge/TI59MachineWrapper.h | C interface: DisplaySnapshot, CpuFrame, TraceFlags |
Bridge/TI59MachineWrapper.mm | ObjC++ implementation; thread safety; marshalling |
60 Hz Loop and Value Timing
EmulatorViewModel runs a 60 Hz display-link loop.
- All register values shown in the debug panel are pre-execution — what exists before the current instruction runs.
- Frozen display: when frozen,
current step = decodedPC − 1 (last executed instruction); registers show post-execution state of that instruction.
- Next statement: shows the ROM address and mnemonic of the not-yet-executed instruction; register fields are empty/blank.
Program Sources and the Solid-State Display Path
ProgramSource (top of EmulatorViewModel.swift) mirrors the firmware's PRG SOURCE flag (SCOM[0] nibble 3): userProgram = 0, solidState = 1, solidStateReturn = 2, fastMode = 4, rom = 8.
Solid-state (library module) programs are shown live in the PROGRAM STEPS view:
cacheModuleImage() parses the module header into moduleProgramRanges (program n, 1-based = ranges[n-1]); it is called at both module-load sites.
libraryProgramStep() maps the core's latched libExecPC (bridge property; 0xFFFF = none) to (program, step, range). Steps are program-relative: step 000 = first keycode of that program.
- Frozen view:
frozenLibCache / frozenLibProgram mirror the RAM/ROM freeze caches and rebuild in stepKeycode when the Pgm number changes.
- The current Pgm number also lives in SCOM[9] digits 4 (tens) / 3 (units); the latch-derived program is cross-checked against it (mismatches debug-logged).
- PRG SOURCE = 2 ("Solid State Return (PC Reload)"): a one-single-step transitional state after a RTN back into the module. The previous frozen cache is held and the highlight advances +1 onto the just-executed RTN (
advanceCachedProgramCurrent()). During this step SCOM[0] nibbles 4–7 hold a raw CROM address, not a step number — never decode it as one.
- Colors: solid-state steps highlight blue; return-stack levels with source 2 show purple, like library levels.
Bridge Optimization
- Fetching all 100 data registers individually per frame is too slow. Use
nonZeroDataRegisterIndices() to get the set of non-zero registers in a single bridge call, then fetch only those.
- Never make 100 individual bridge calls per 60 Hz frame.
Dynamic Type Cap
CalculatorView.body intentionally limits Dynamic Type to .small … .large. Do not remove or widen this cap — at accessibility sizes XL and above, buttons become oversized and break the layout (especially the TI-59 card reader bar).
iOS and macOS: Both Platforms Must Work
Every change must leave both the iOS and macOS targets building and running correctly. When there is any doubt:
- Build for iOS Simulator:
xcodebuild -project Calc-U-59.xcodeproj -scheme "Calc-U-59" \
-destination "platform=iOS Simulator,name=iPhone 17e" build
- Build for macOS:
xcodebuild -project Calc-U-59.xcodeproj -scheme "Calc-U-59" \
-destination "platform=macOS" build
Both must succeed before the task is done.
Reference
reference/AppArchitecture.md — full app architecture documentation
reference/DebugAPI.md — debug panel and bridge API reference
Global Rules
-
Compile, don't run. Do not launch the Xcode iOS Simulator or run the built Mac app — that is the user's job. When runtime verification is needed, ask the user to launch the app and explain precisely what behaviour or output to look for.
-
Commit often, never push. Commit after each logical unit of work. Never run git push.
-
iOS and macOS both must work. See the build commands above. Both targets must succeed before declaring the task done.
-
Cite your sources. When referencing a hardware spec, standard, or external document, include the https URL (or a pointer to the local reference file) in the relevant code comment or documentation.