Decker: Responding to Responses

Decker has made the rounds for a few weeks, with mostly positive feedback. A few questions and comments keep coming up, so I thought I’d respond to them.

Can Decker open my old HyperCard stacks?

No. While Decker is aesthetically and functionally similar to HyperCard, it is not a “clone”, nor does it aim to be.

HyperCard rests on the foundations of the Macintosh Toolbox: the text rendering, the UI toolkit, the forked filesystem, and many other intricacies of the operating system. To fully and accurately recreate both HyperCard and the environment it expects to work is a monumental task largely orthogonal to Decker’s primary goal: being an accessible, easy-to-learn tool for creative expression. Stacks may communicate freely and depend upon one another or system resources in powerful, yet inconvenient and surprising ways, while decks are hermetically-sealed freestanding entities that will work anywhere and anywhen.

Decker does things HyperCard doesn’t. Aside from the obvious convenience of decks that you can diff via revision control and share anywhere you can post an .html file, Decker has tools for working with tabular and binary data, “Canvas” widgets with a flexible drawing API, user-defined transition animations, and a whole lot more.

Still want to do something with those old stacks? If you’re feeling sufficiently motivated, you could be the change you wish to see in the world and try your hand at writing a program that converts a reasonable subset of HyperCard stacks into Decker’s deck format. Along the way, you might gain a more visceral appreciation for how complex HyperCard documents are, and how different HyperCard is from Decker. On the other hand, maybe what you want is one of the HyperCard clone projects that already exist- some of them even work in your browser!

Why are there two implementations of Decker?

The Decker codebase includes both a C implementation which runs as a native application (native-decker) and a separately-maintained JavaScript implementation which runs as a webapp (web-decker). Native-decker has the ability to save documents as instances of web-decker, in a form of telescoping generations, and web-decker can reproduce modified copies of itself. In principle, having two parallel codebases doubles the maintenance cost of adding features. Some have suggested that it would be more efficient to transpile the C implementation for the web (via ASM.js or WebAssembly) or produce “native” builds of the JavaScript implementation via a wrapper like Electron.

While it may not be obvious at first glance, there are many small differences in functionality and design between native- and web-decker: some are spelled out here. The web version has to deal with constraints that do not exist for native applications. If I used some form of cross-compilation there would still be a mess of #ifdefs and platform-specific code needed to handle these differences, so it’s far from clear that unifying the ports would cut maintenance effort in half.

Electron might make shipping cross-platform binaries a bit easier, but it’s very heavyweight and resource-demanding; Decker would be unusable on a wide range of older machines it currently works fine on. The C version of Lilt doesn’t even depend on SDL, so it can run on positively ancient personal machines, cheap single-board computers, and limited web servers.

WASM has some promise, but I consider it an immature technology; there’s still a lot of churn in functionality and tooling that I’m not interested in dealing with. I have low confidence that a WASM-based project made with today’s tools would still compile and work properly without modification five years from now.

Having two codebases is good for hackability and user empowerment. If you know C, it’s fairly straightforward to glue custom functionality into Decker that calls native libraries. Likewise, if you know JavaScript, you could choose to hack up web-decker to expose browser APIs that aren’t normally available to Lil, like the Gamepad API or the ability to perform an XMLHttpRequest. There are lots of inherently “non-portable” features I don’t intend to add to mainline Decker which a given user might still want for a project, and letting them easily add things themselves is what open source is all about. The current version of web-decker is intentionally built without “minification” and doesn’t require anything more than a text editor to make this kind of modification. If it was shipped as an opaque wad of WASM bytecode a whole range of casual tinkering would be off the table and gated behind a complex build process and mandatory tooling.

Finally, a C implementation of Decker with minimal dependencies is a cheap insurance plan. Today, the web ecosystem is brimming with vitality. Nearly every modern device has a web browser, and delivering a tool as a webapp has unparalleled advantages in accessibility, allowing users to try it by simply clicking a hyperlink. A monolithic .html file is convenient even for offline use. The web of tomorrow, however, may be far less delightful and hackable.

The world’s most popular web browsers are developed by an advertising company and a hardware manufacturer which shows ever-increasing interest in the advertising business. The also-rans include a foundation primarily funded by the first advertising company, and some other jokers running a banner-ad protection racket while dabbling with cryptocurrency pyramid schemes. While all of these vendors grudgingly allow users to install ad-blocking software, none has seen fit to block ads by default, and the biggest is enthusiastically working to lock down extension APIs and neuter the effectiveness of any ad-blockers released in the future, weakly justified with promises of mild performance and security benefits. In short, web browsers are a landscape fully captured by companies which are financially incapable of having the best interests of users at heart. At the same time (and perhaps not by accident), web standards are so inscrutably complex and fast-moving that building and maintaining a new browser from scratch requires the resources of a medium-sized nation-state. Even a nominally open-source browser doesn’t solve the problem if the burden of keeping pace with the cutting edge is beyond the grasp of hobbyists.

In the long view, I would like Decker and the things people make with it to be able to outlive the web ecosystem as we know it. Avoiding hard-dependencies on web technology and having a plurality of freestanding implementations helps. Documentation and freely available source code helps, too. It’s possible that the web will continue to flourish indefinitely, and preparations to the contrary will prove to be unnecessary. On the other hand, I’ve already gone to the trouble to metaphorically dig and furnish the underground survivalist compound, so why not keep it stocked with canned food and Twinkies just in case?

Why does Decker use Lil instead of Lua?

Lua is a relatively small and simple scripting language which is often embedded within games and other applications to allow users to extend their functionality. Lua has battle-hardened open-source implementations and a great deal of public documentation available. In some ways, Lua seems a natural choice for a tool like Decker.

At time of writing, the Lua 5.4 codebase is about 30,000 lines of C. Meanwhile, the entire Decker codebase- including both the C and JavaScript implementations- is about 12,000 lines. Fengari, a JavaScript rewrite of Lua, is available as a minified wad of JS that weighs about 220kb. A Decker web build (which, remember, is not minified) is about 290kb as a baseline, of which about 40kb is the Lil implementation. Lua is much larger than Lil, whether you’re measuring the source code or the distributable size, and it is a complex external dependency. If I modified the design of Lua to suit my needs and preferences, it would no longer be Lua.

Lil does things Lua doesn’t. Lil springs from the tradition of APL-family languages, with implicit “conforming” iteration for arithmetic operators and a rich set of primitives for manipulating data. It also has first-class database-style tables (not to be confused with Lua’s one-size-fits-all collection type) with a SQL-style query language and join operators. Lil has co-evolved with the rest of Decker, and their design considerations feed into one another.

Is Lil a “better” language than Lua in general? Probably not. It’s certainly less mature, and it makes different tradeoffs. Lil is a good fit for Decker, and worth giving a chance. If you try it, you might like it.

Does Decker have networking support? Will it?

In our densely-interconnected world, it is often appealing and occasionally even useful for an application on one computer to communicate with an application on another computer.

Decks can, with direct user permission, request that a browser tab be opened at a specific URL. In the case of native-decker, this leverages SDL_OpenURL:

go["https://beyondloom.com/decker/"]

Should the user’s browser (if any) fetch that URL, unseen and distant mechanisms may spring into action to record the event, update numbers in a database, or dispatch a large pepperoni pizza to your home. The request might even be routed to an instance of lilt, running a Lil script that produces a deck on the fly:

r:readdeck["template.deck"]
r.cards.home.widgets.timestamp.text:sys.now
writedeck["rendered.html" r]

Decks cannot, however, arbitrarily and independently communicate with local or remote processes, curl an HTTP endpoint, broadcast telemetry, or any similar shenanigans. Access to the local filesystem is, like URL opening, gated behind deliberate and informed user interaction, making it reasonably trustworthy by construction.

Part of this design decision is strictly a practical one: for decks to be portable, web-decker and native-decker must expose the same functionality. Web browsers have rigid and complex constraints on network IO and filesystem access, so Decker must restrict itself to behavior permitted under that security model.

A more ideological justification is that external dependencies are inherently brittle. A normal website is an ephemeral collection of resources and processes, potentially spread across multiple servers or continents, and fetched in a piecemeal fashion by a user-agent. Despite heroic efforts, a normal website is inherently difficult or even impossible to archive with fidelity. A deck is a single file, delivered in one piece. By its nature, a deck is trivial to archive, and will be as usable tomorrow on my machine as it was yesterday on yours. If a deck could speak with a remote server, the application would not truly reside on any one machine, and preserving it for future generations and distant locales might be challenging or impossible. Lost media is an avoidable tragedy, and the solution starts with the form-factor.

As a user, it’s possible that you disagree with some of the constraints Decker provides out of the box, and that’s OK! As discussed previously, I have taken pains to allow you to hack on Decker, punch holes in the bulkheads, and accomplish whatever you wish to accomplish, starting from these safe and portable defaults.1

Why isn’t Decker more Modern-looking?

For consistent and future-proof cross-platform results, Decker needs a custom UI toolkit. The implementation needs to be small, so it needs to look simple. I chose System 6-era MacOS as a model for my widgets because it is visually coherent, functional, and it provides me, personally, the fellow who spent an undue amount of time implementing all of this, warm fuzzies to look at.

From a purely objective evaluation, Decker’s UI is high-contrast, inherently colorblind-friendly, and legible on low-resolution displays. Widgets offer clear affordance. At a glance, a radio button stands apart from a checkbox, buttons are visually distinct from fields, and active widgets are not prone to confusion with their disabled or default appearance. Decker’s user interface is minimally adorned, with consistent design language and honesty to its underlying digital materials. If it is not modern, it is nonetheless modernist.

The simplicity of Decker’s look-and-feel has other benefits, too. If you are using Decker as a prototyping tool, it can be a relief to know that your mockups will never be confused by a layperson with a “polished” finished product, and you can focus on substance over style. Background drawings on cards look like they belong with Decker’s widgets, and indeed the drawing tools Decker provides can easily reproduce the appearance of widgets with perfect fidelity. Instead of trying to dazzle with slickness, Decker hands the user a crayon and invites them to have a scribble.

If the 1-bit look isn’t for you, feel empowered to make a fork with widget styling more to your liking, whether that means low-contrast “flat design” where everything is a featureless rectangle, the Fisher-Price blobbiness of Windows XP, or even the rectangular pixels and brutal typography of the Amiga workbench. The Decker file format and corresponding Lil APIs are largely agnostic to the details of presentation; they only specify bounding boxes and a few relatively abstract styling hints.

While we’re here, on the off-chance that your actual complaint is that Decker’s color scheme (or lack thereof) grates your eyes, take heart in the knowledge that the palette, patterns, animated pattern sequences, and default fonts can all be overridden on the fly; converting Decker to an amber-warm dark mode is as simple as opening the listener and pasting a few lines of Lil:

patterns[colors.black]:"%h" parse "000000"
patterns[colors.white]:"%h" parse "FFAA00"

back


  1. update: as of v1.40, native–decker includes The Danger Zone, a standardized collection of potentially unsafe IO functions that can be opted into at compile time. Decker remains sandboxed by default, for all the reasons given above, but if you want to “punch holes in the bulkheads” you no longer need experience with C and the internals of the Decker codebase.  ↩︎