Superseded (2026-07-11): This document is the original build plan, kept for history. The living plan, including the fidelity diagnosis, task specs, and per-task model recommendations, is
/ROADMAP.md.
We are building a native, cross-platform HyperCard stack viewer in C# / .NET 8 that opens classic Mac HyperCard files directly — no Mac emulation. The project must handle stacks arriving as raw files, StuffIt archives, or disk images. It must support B&W and color display modes, embedded QuickTime MOV playback via LibVLC, and a HyperTalk script interpreter. The goal is to create a usable foundation that attracts community contributors interested in retro computing preservation.
Target version: HyperCard 2.4.1. This is the last version Apple shipped and the most widely distributed — the vast majority of community stacks were created with it. Full HC 2.4.1 compatibility is the primary correctness bar for every subsystem (parser, renderer, interpreter, container formats). HyperCard 1.x is a secondary target: detect the older format version and surface a warning; full 1.x support is deferred to Phase 15. Password-protected stacks (HC 2.4 XOR encryption) are out of scope for decryption — detect them and show a clear user message.
Three sample files drive initial development:
NEUROBLAST_HyperCard— raw HC 2.x stack (STAK magic, version 10, ~70 cards)NEUROBLAST_Cyberdelia.sit— StuffIt archive containing a stackneuroblast.img— DiskCopy 4.2 disk image with HFS filesystem
Key existing references:
- hypercard4net (giawa/hypercard4net) — partial C# HyperCard parser
- HyperCardPreview (Pierre Lorenzi, Swift) — most thorough binary format documentation and WOBA decoder
- ViperCard — browser-based HyperCard reimplementation
- OpenXION — open source HyperTalk interpreter (Java)
HyperCardSharp/
├── HyperCardSharp.sln
├── src/
│ ├── HyperCardSharp.Core/ # Binary parsing, stack model, containers
│ │ ├── Binary/
│ │ │ ├── BigEndianReader.cs # Span<byte> + BinaryPrimitives wrapper
│ │ │ ├── MagicDetector.cs # Auto-detect format from magic bytes
│ │ │ └── BlockHeader.cs # 16-byte block header record
│ │ ├── Stack/
│ │ │ ├── StackFile.cs # Top-level parsed stack model
│ │ │ ├── StackParser.cs # Block enumeration + dispatch
│ │ │ ├── StackBlock.cs # STAK header (version, card count, patterns)
│ │ │ ├── MasterBlock.cs # MAST offset index
│ │ │ ├── ListBlock.cs / PageBlock.cs
│ │ │ ├── CardBlock.cs / BackgroundBlock.cs
│ │ │ ├── BitmapBlock.cs # BMAP block (WOBA compressed)
│ │ │ ├── StyleTableBlock.cs / FontTableBlock.cs
│ │ │ └── UnknownBlock.cs # Catch-all, logs offset+size
│ │ ├── Parts/
│ │ │ ├── Part.cs / ButtonPart.cs / FieldPart.cs / PartContent.cs
│ │ ├── Bitmap/
│ │ │ ├── WobaDecoder.cs # WOBA decompression (hardest algorithm)
│ │ │ └── BitmapImage.cs # Decoded 1-bit bitmap
│ │ ├── Containers/
│ │ │ ├── IContainerExtractor.cs
│ │ │ ├── StuffItExtractor.cs # SIT! native LZW decompression
│ │ │ ├── DiskCopyExtractor.cs # DiskCopy 4.2 header parsing
│ │ │ ├── HfsReader.cs # Native HFS filesystem + B-tree
│ │ │ ├── MacBinaryExtractor.cs / AppleSingleExtractor.cs
│ │ │ └── ResourceForkParser.cs
│ │ └── Resources/
│ │ ├── PictDecoder.cs / SoundDecoder.cs / IconDecoder.cs
│ │ └── AddColorDecoder.cs # HCcd/HCbg color overlay data
│ │
│ ├── HyperCardSharp.HyperTalk/ # Lexer, parser, interpreter
│ │ ├── Lexer/ (Token.cs, TokenType.cs, Lexer.cs)
│ │ ├── Parser/ (Ast/*.cs, Parser.cs)
│ │ ├── Interpreter/ (Interpreter.cs, Environment.cs, MessagePassing.cs, BuiltInCommands.cs)
│ │ └── Xcmd/ (IXcmdHandler.cs, XcmdStubHandler.cs, XcmdRegistry.cs)
│ │
│ ├── HyperCardSharp.Rendering/ # SkiaSharp card rendering
│ │ ├── CardRenderer.cs / PartRenderer.cs / BitmapRenderer.cs
│ │ ├── ColorRenderer.cs / TextRenderer.cs / FontMapper.cs
│ │ └── RenderMode.cs # Enum: BlackAndWhite, Color
│ │
│ └── HyperCardSharp.App/ # AvaloniaUI desktop application
│ ├── Views/ (MainWindow, CardView, MessageLogView)
│ ├── ViewModels/ (MainWindowViewModel, CardViewModel, StackViewModel)
│ ├── Controls/ (SkiaCardControl.cs — ICustomDrawOperation)
│ └── Services/ (FileOpenService.cs, MediaService.cs)
│
├── tests/
│ ├── HyperCardSharp.Core.Tests/
│ ├── HyperCardSharp.HyperTalk.Tests/
│ └── HyperCardSharp.Rendering.Tests/
│
└── docs/
├── PLAN.md # This file
├── stack-format.md # Evolving binary format documentation
└── hypertalk-coverage.md # HyperTalk command coverage tracker
| Package | Project | Purpose |
|---|---|---|
| Avalonia (11.x) | App | UI framework |
| Avalonia.Desktop | App | Desktop support |
| Avalonia.Themes.Fluent | App | Theme |
| SkiaSharp (2.88+) | Rendering | Bitmap/canvas rendering |
| LibVLCSharp + LibVLCSharp.Avalonia | App | Video/audio playback |
| VideoLAN.LibVLC.Windows | App | LibVLC native binaries |
| CommunityToolkit.Mvvm | App | MVVM source generators |
| Microsoft.Extensions.Logging.Abstractions | Core | Structured logging |
| xunit + FluentAssertions | Tests | Unit testing |
Install .NET 8 SDK. Create solution, all projects, wire references. Minimal Avalonia window at 512x342. dotnet build + dotnet run succeed. Commit.
BigEndianReader— Span wrapper with BinaryPrimitives for big-endian readsBlockHeader— 16-byte record (size, type, ID, filler)MagicDetector— identify STAK, SIT!, DiskCopy from first bytesStackParser.EnumerateBlocks()— walk file, yield headers, log unknownsStackBlock— parse STAK (version at +0x10, card count, dimensions, patterns)MasterBlock— parse MAST offset table- Unit tests with sample file
Milestone: Print block inventory from NEUROBLAST_HyperCard. Commit.
CardBlock/BackgroundBlock— header + part iterationButtonPart/FieldPart— rect, style flags, name, script textPartContent— text + style referencesFontTableBlock/StyleTableBlock— font ID mapping, style runsListBlock/PageBlock— card ordering
Milestone: Extract all card names, button labels, field text, scripts. Commit.
BitmapBlock— parse BMAP header (dirty rect, mask/image sizes)WobaDecoder— implement WOBA (RLE + XOR delta + bit-packing). Reference HyperCardPreview's Swift decoderBitmapImage— decoded 1-bit pixel data
Milestone: Decode all BMAPs, export as PNG, visually verify. Commit.
BitmapRenderer— 1-bit BitmapImage to SKBitmapCardRenderer— composite background + card bitmaps + part overlaysPartRenderer— button outlines, field bordersTextRenderer+FontMapper— styled text with Mac font substitutionSkiaCardControl— ICustomDrawOperation for Avalonia- Keyboard nav (arrow keys = prev/next card)
Milestone: View all NEUROBLAST cards with bitmaps and button/field rendering. Commit.
- Lexer: keywords, identifiers, strings, numbers, operators,
--comments - Parser: recursive descent to AST (HandlerNode, CommandNode, IfNode, RepeatNode, ExpressionNode, chunk expressions)
- Parse all scripts from sample stack
Milestone: Parse 100% of NEUROBLAST scripts into AST with zero errors. Commit.
- Tree-walking interpreter
- Variable scoping (local, global,
it) - Message hierarchy: button -> card -> background -> stack
- Built-in commands:
go to card,visual effect,put,set,answer,ask XcmdStubHandler— log unsupported commands, never crash- Wire UI: click -> hit-test parts -> dispatch mouseUp
Milestone: Click buttons in NEUROBLAST -> cards navigate via HyperTalk. Commit.
MacBinaryExtractor/AppleSingleExtractor— header parsing, extract data + resource forksResourceForkParser— 256-byte header, type list, reference list, data sectionStuffItExtractor— native C# LZW decompression (reference: thecloudexpanse/sit C implementation)DiskCopyExtractor— parse DiskCopy 4.2 header (84 bytes), extract raw disk dataHfsReader— native C# HFS filesystem parser: Master Directory Block, catalog B-tree traversal, file extraction by type code. Reference: libfshfs documentation + HFSExplorer Java sourceMagicDetectorchains: detect container -> extract -> detect inner -> parse- No external tool dependencies — fully self-contained
Milestone: Open all 3 sample files (raw, .sit, .img) and render cards from each. Commit.
AddColorDecoder— parse HCcd/HCbg resources from resource forkColorRenderer— composite color overlay onto B&W bitmapRenderModetoggle in UI menu (View -> Black & White / Color)- B&W mode: threshold quantize all rendering to 1-bit
PictDecoder— partial QuickDraw replay (common opcodes)
Milestone: Toggle B&W / Color mode. PICT resources render. Commit.
SoundDecoder— parse Mac snd resource, extract PCM samplesMediaService— LibVLCSharp wrapper for MOV + audio playback- Wire
playHyperTalk command to audio system - Embed VideoView in Avalonia for MOV files
Milestone: play command triggers audio. MOV files play in embedded viewer. Commit.
- Extended HyperTalk:
repeat with/while/until,do,send, string/math/date functions - Chunk expressions:
char,word,item,lineof containers - Visual effects: dissolve, wipe, iris, checkerboard (SkiaSharp transitions)
- Scrolling fields, Find command
- Menu bar, drag-and-drop, recent files
- Error display in status bar / log panel
These phases address stubs and gaps identified during v0.1.0 development. Each is a discrete vertical slice of work.
Resolve all interpreter stubs so scripts behave as they would in real HyperCard.
show/hidecommands — togglePart.Visibleon the live card model and request a redraw (currently log-only)click at <x,y>command — hit-test parts at the given point and synthesise amouseUpdispatch (currently log-only)type <text>command — append text to the focused field (currently log-only)wait <n> [ticks|seconds|milliseconds]— implement async delay usingTask.Delay(currently skipped)send <msg> to <target>command — script is retrieved but never executed; wireExecuteHandler()call after lookupset <property> of <part>full coverage — currently onlyhilite,text,visiblework; addname,rect,style,textFont,textSize,textStyle,enabled- Field text mutation —
SetFieldText/SetPartVisiblecurrently log "deferred"; make card/background part content mutable and trigger re-render - Button hilite read-back —
GetButtonHilitereturnsnull; read from live part state
Milestone: All HyperTalk VM commands execute real behavior. Re-run NEUROBLAST — all script interactions work without log stubs. Commit.
Styled text run data is parsed and stored but completely ignored during rendering.
StyleTableBlock+FontTableBlocklookup — wire intoTextRenderer.DrawFieldTextandTextRenderer.DrawButtonLabel- Per-run font/size/style application — apply
SKTypeface,TextSize, bold/italic/underline perStyleRunspan - Mac font ID → system font mapping — extend
FontMapperwith Geneva (12/9pt special-case), Chicago, Monaco, Palatino, Times, Helvetica substitution table - Mixed-style layout — measure each run individually; line-wrap across run boundaries; handle superscript/subscript offsets
set textFont/textSize/textStyle of field— update style runs at runtime when set via HyperTalk
Milestone: Open a stack with styled fields — different fonts, sizes, bold/italic render correctly. Commit.
The play command callback exists but has a TODO — no audio output.
SoundDecoder.cs— implement Macsndresource PCM extraction: parse sound header (format 1/2), extract 8-bit µ-law or raw PCM samples, write to a temp WAV or pipeMediaService.cs— LibVLCSharp wrapper:PlayAudio(byte[] pcm, int sampleRate)usingLibVLC.Mediafrom stream; alsoPlayFile(string path)for external MOV- Wire
PlaySoundcallback inStackViewModel— look upsndresource by name inStackFile.Resources, decode, hand toMediaService stop soundcommand — add to interpreter and stop active media- MOV playback — wire
VideoViewinto the card area when aplay moviecommand targets a rect
Milestone: play "boing" triggers the correct sampled sound. MOV resources play inline. Commit.
Color mode currently returns the B&W bitmap unchanged — AddColorDecoder is a complete stub.
AddColorDecoder.cs— parseHCcd(card color) andHCbg(background color) resources: 4-byte header, list of color regions (partId,rect,fill color,frame color)ColorRenderer.cs— apply color regions as filled rectangles composited over the B&W base layer using SkiaSharpSKPaintwithSKBlendMode.Multiply/SrcOverAddColorpart-level color — matchpartIdto rendered part rect and tint button/field backgroundsPICTresource rendering — implementPictDecoder.cscovering the opcodes found in real stacks:0x0001ClipRect,0x0011VersionOp,0x001E/0x001FDefHilite,0x0098PackBitsRect,0x009ADirectBitsRect,0x00FFEndPic; log any unrecognised opcode rather than crashing- Resource fork → rendering pipeline integration — ensure
StackFileexposes color resources soCardRenderercan access them in Color mode
Milestone: Toggle to Color mode — AddColor stacks show filled color regions. PICT backgrounds render. Commit.
Gaps in format handling that cause silent failures or crashes on real-world stacks.
- HyperCard 1.x stack support — detect format version ≤ 7 in
StackBlock; map the different field offsets for card count, card size, and list block location; log a warning for any 1.x-only feature - Mac Roman encoding — replace Latin-1 proxy in
PartContent.cswith a proper Mac Roman → UTF-16 lookup table (characters 0x80–0xFF differ) go home/ Home stack — resolve to a configured home stack path or open the file picker rather than logging a no-op- Unknown block tracing — ensure every unknown block type logs its 4-char type code, byte offset, and size (already partial; audit and harden)
- Large stack stability — test with stacks > 100 cards; verify
LIST/PAGEB-tree walk handles multi-page card lists without index errors - Password-protected stacks — detect encrypted STAK flag; surface a readable "This stack is password-protected" message instead of garbage rendering
Milestone: Open a diverse set of real-world stacks without crashes. 1.x stacks display a version warning rather than corrupting. Commit.
Non-functional work to make the project welcoming to contributors and end-users.
- README.md — feature overview, screenshots, download instructions, contributor guide, link to
docs/ docs/hypertalk-coverage.md— table of every HyperTalk command and function with ✅/⚠️ /❌ statusdocs/stack-format.md— update with all block types encountered, field offsets confirmed against real stacks- GitHub Issues — file issues for each Phase 11–15 stub so the community can contribute
- CI workflow —
.github/workflows/build.yml:dotnet build+dotnet teston push/PR for Windows, macOS, Linux - Self-contained publish — verify
dotnet publish -r win-x64 --self-containedproduces a single-folder app with LibVLC bundled - App icon + About dialog polish — replace placeholder icon; About dialog shows version from assembly metadata
Milestone: Project is publicly presentable. CI is green. Single-folder redistributable builds for all three platforms. Commit + tag v0.2.0.
Milestone: Polished, usable viewer for community release. Commit + tag v0.1.0.
These phases close the remaining gaps in docs/hypertalk-coverage.md to achieve full HyperCard 2.4.1 script compatibility.
HyperCard delivered a steady stream of system messages that scripts could intercept. Many stacks depend on these for animation, rollovers, and auto-advance.
idlemessage — fire on a timer (every ~100 ms) when no user interaction is pending; dispatch through card → background → stack hierarchymouseEnter/mouseLeave— track mouse position over parts; fire on enter/leave; enables rollover highlight stacksmouseWithin— fire repeatedly while mouse stays inside a partkeyDown/tabKey/returnKey/enterKey/arrowKey— dispatch keyboard events as system messages with key value inthe paramListnewCard/deleteCard— fire on card creation/deletion (player mode:newCardis informational)suspendStack/resumeStack— fire on window focus changes- Mouse/keyboard properties — implement
the mouse,the mouseH,the mouseV,the mouseClick,the clickLoc,the key,the keyCodeas live-read properties from the UI layer
Milestone: idle-based animation stacks auto-advance. mouseEnter/mouseLeave stacks highlight on hover. Keyboard messages dispatch. Commit.
The message hierarchy is incomplete — the stack script itself and the HyperCard level are missing.
- Stack-level script — parse the STAK block's script field; add it to the message dispatch chain after background and before HyperCard
- HyperCard-level handlers — implement a built-in "Home stack" script context at the top of the hierarchy (after stack); stub common Home handlers like
doMenu passsemantics — verifypass <handler>correctly bubbles through all five levels: part → card → background → stack → HyperCardthe target— return the part that originally received the messagethe params/the paramList— return handler arguments as passed
Milestone: Stacks with handlers in the stack script (not just card/background) work correctly. Full 5-level message hierarchy operational. Commit.
Chunk expressions are the string-processing backbone of HyperTalk. Nested chunks are used in almost every non-trivial stack.
- Nested chunk reads —
word 2 of line 3 of field "data"evaluates inner-to-outer; implement recursive chunk resolution in expression evaluator - Nested chunk writes —
put "x" into word 2 of line 3 of myVarreconstructs the string with the inner chunk replaced the number ofchunks —the number of words of line 3 of x; chain with nested chunks- Range chunk writes —
put "x" into char 3 to 5 of line 2 of myVar last/any/middlechunk qualifiers —last word of x,any line of x,middle char of xthe message box— implement as a special container;put x into messageshows a message log/text area
Milestone: Complex chunk expressions like word 2 of line 3 of field "data" evaluate and assign correctly. Commit.
Many stacks read and write part properties dynamically. Property read-back has several gaps.
visible of partread-back — return actualPart.Visiblestate instead of alwaystruethe result— set to the outcome of the last command (emptyon success, error message on failure); particularly important forfind,go,playanswerbutton read-back — setitto the label of the button the user clicked inanswerdialogsset script of/set cursor to/set userLevel to— implement or stub remainingsettargetsthe screenRect— return the rendering area dimensionsthe tool— return"browse"(player mode only)the userLevel— return5(scripting level, read-only in player)text of card— concatenate all field text on the current cardhilite of buttonread-back — ensure bidirectional:setandgetboth work on live state
Milestone: Property-dependent stacks (especially those that read the result or visible) work correctly. Commit.
The find command is central to many educational and reference stacks.
find "text" in field X— parsein fieldscope clause; restrict search to named field- Find mode qualifiers — implement
find whole,find chars,find word,find stringwith distinct matching semantics (whole word, substring, word-start, exact) - Find highlight — visually highlight the found text on the card (box around matching text, as original HC did)
findacross backgrounds — search continues across background boundariesthe foundText/the foundChunk/the foundLine/the foundField— properties that report whatfindmatched
Milestone: find whole "mitochondria" in field "glossary" works with correct scoping and visual highlight. Commit.
XCMDs (external commands) and XFCNs (external functions) were HyperCard's plugin system. Many stacks used common XCMDs for color, sound, and file access.
- XCMD registry wiring — connect the existing
XcmdRegistryto the interpreter soExecCommandfalls through to registered handlers before logging "unknown command" - Built-in XCMD emulations — implement common XCMDs as native C# handlers:
AddColor— already handled via rendering; register name so scripts expecting a response get oneFlash— briefly invert the screen/card regionResCopy/GetResource— resource fork access (return resource data or empty)FileName/FileIO— basic file dialog / read-only file access (sandboxed)Palette— color palette window (stub with log)
- XFCN return values — ensure XFCNs can return a string value via
itorthe result - Unknown XCMD handling — graceful "XCMD not available" dialog matching original HC behavior
Milestone: Stacks that call common XCMDs get reasonable behavior instead of silent no-ops. Commit.
Remaining HyperTalk commands that real stacks use.
open file/close file/read from file/write to file— sandboxed file I/O (read-only by default; write requires user confirmation)show cards/show all cards— rapid flip-through of all cards (slideshow mode)drag from <point> to <point>— synthesize mouse-drag eventschoose <tool>— log or ignore in player mode; some stacks testthe toolprint card— render current card to printer or PDFdoMenu <menuItem>— handle common menu commands (New Card,Delete Card,Copy,Paste, etc.) or log unsupported- Remaining math functions —
atan(),exp2(),annuity(),compound()
Milestone: File-reading stacks, slideshow stacks, and stacks using doMenu work. Commit.
Validate all previous work against a broad corpus of real stacks.
- Stack test harness — automated loader that opens each stack in
samples/, navigates all cards, verifies no exceptions - Script execution smoke tests — extract scripts from sample stacks, parse and interpret each, verify no unhandled errors
- Rendering regression tests — render first/last card of each sample stack, compare against golden reference bitmaps (pixel hash comparison)
- Coverage report — log every unrecognized command, unknown block type, and missing property across the full test corpus; feed results back into gap tracking
- Community stack corpus — curated collection of diverse stacks: educational, games, multimedia, database, with notes on expected behavior
Milestone: 90%+ of community stacks load and navigate without errors. Known gaps documented per-stack. Commit + tag v0.3.0.
Phase 0 -> Phase 1 -+-> Phase 2 -> Phase 5 -> Phase 6 -> Phase 10
+-> Phase 3 -> Phase 4 -> Phase 8 -> Phase 9
+-> Phase 7 (independent, can parallel with 2-6)
Phase 6 requires Phase 4 (UI wiring)
Tracks A (rendering), B (parsing/interpreter), and C (containers) can be developed in parallel after Phase 1.
| Risk | Severity | Mitigation |
|---|---|---|
| WOBA decompression complexity | High | Line-by-line reference HyperCardPreview Swift decoder. Extensive unit tests |
| HFS B-tree traversal | High | Reference libfshfs docs + HFSExplorer Java source. Test against sample .img |
| StuffIt LZW implementation | Medium | Port from thecloudexpanse/sit C reference. LZW is well-documented |
| PICT opcode coverage | Medium | Implement only opcodes found in real stacks. Log unsupported |
| HyperTalk language breadth | Medium | Target sample stack commands first. Expand based on real-world testing |
| Font fidelity | Low | Bundle open-source Chicago/Geneva alternatives if licensing allows |
- Language: C# 12 / .NET 8 (LTS)
- Container extraction: All native C#, no external tool dependencies
- HC version support: HyperCard 2.4.1 is the explicit target. HC 2.x ≥ 2.0 should work. HC 1.x: detect and warn (Phase 15). Encryption: detect and report, no decryption.
- Binary parsing: Span + BinaryPrimitives (big-endian), no BinaryReader
After each phase, verify by:
dotnet build— zero errors, zero warningsdotnet test— all unit tests passdotnet run --project src/HyperCardSharp.App— app launches- Manual test: open
samples/NEUROBLAST_HyperCardand verify expected behavior for that phase - After Phase 7: also test with
samples/NEUROBLAST_Cyberdelia.sitandsamples/neuroblast.img