Mimsy Were the Borogoves

Hacks: Articles about programming in Python, Perl, Swift, BASIC, and whatever else I happen to feel like hacking at.

A HyperCard Time Machine

Jerry Stratton, July 19, 2023

Superhero stack opening card: Main “control panel” card for the Men & Supermen superhero character sheet HyperCard stack.; Men & Supermen; HyperCard

This was a tool for an early role-playing game. And looks it.

My second programming job out of college started as a HyperCard stack. HyperCard was an amazing tool not just for programmers, however, but especially for the weekend programmer or even the weekend non-programmer. It didn’t require any programming to get started. Just put some text placeholders on a background card, some buttons, maybe add a pre-created action to a button such as “move to next card”, and you had a very simple visually-oriented application that responded in a manner customized for you and your data.

There were a lot of non-programmers who used HyperCard and graduated to weekend programming. Because after you wrote your text or placed your graphics or added a button with a standard action, if you wanted to add code to that object, you could. You might start with slinging messages from object to object, or performing simple math on one of the text placeholders.

HyperTalk was not object-oriented in the sense that programmers mean when they talk about object-oriented programming. But it was object-oriented in the sense that programmers explain object-oriented programming to non-programmers. Every object on the screen could contain code that responded to specific actions. Each card could respond to keystrokes. A text box could respond to typing. Cards, text boxes, buttons, and images could respond to mouse actions. Keystrokes, mouse actions, and custom functions were all messages. And each object could send messages to other objects.

HyperCard was often used for databases; the card metaphor was especially apt for that. For example, I had a database of superheroes and supervillains in the Men & Supermen superhero role-playing game.

The game used meters, but clicking on the word “Height” on the character sheet would calculate and then display the character’s height in feet and inches:

[toggle code]

  • on mouseUp
    • put the short name of me into TheName
    • delete last char of TheName
    • if the optionkey is down then
      • put word 1 of background field TheName into MYHEIGHT
      • divide MYHEIGHT by .3048
      • put trunc(MYHEIGHT) into MYFEET
      • put Digits((MYHEIGHT-MYFEET)*12,2) into MYINCHES
      • if MYINCHES >= 12 then
        • add 1 to MYFEET
        • subtract 12 from MYINCHES
      • end if
      • answer MYFEET&"'"&&MYINCHES&quote
    • end if
  • end mouseUp

“The short name of me” is the name of the text box; in this case, “Height:”. The code deletes the last character to get rid of the colon, so that it can ask for the value of the field named “Height”, which is the field that contains the actual height in meters. In fact, it contains a phrase, such as “1.75 m”. Word 1 of that is “1.75”.

Like many interpreted scripting languages, HyperTalk converted freely between numbers and strings, so that you can get word 1 of “1.75 m” and then immediately do math on “1.75”.

Messages bubbled up. If you sent a message to a text field, and the field didn’t handle that message, the message would bubble up to the card, to the background, to the stack. In the above code, Digits is one of those messages. It took the given number and returned it with that many digits in it. So that, for example, 1.21231 became 1.2, or .3593 became .36.

[toggle code]

  • function Digits TheNum,TheDigits
    • if TheNum is a number and TheNum is not empty and TheNum is not 0 then
      • put 0 into NumMods
      • if TheNum < 10^(TheDigits-1) then
        • repeat while TheNum < 10^(TheDigits-1)
          • add 1 to NumMods
          • multiply TheNum by 10
        • end repeat
        • put round(TheNum) into TheNum
        • divide TheNum by 10^NumMods
      • else
        • repeat while TheNum > 10^TheDigits
          • divide TheNum by 10
          • add 1 to NumMods
        • end repeat
        • put round(TheNum) into TheNum
        • multiply TheNum by 10^NumMods
      • end if
    • else
      • put 0 into TheNum
    • end if
    • return TheNum
  • end Digits

I stored the Digits function in the overall stack; as long as no intervening object contained a Digits function, the Digits message would bubble up to the stack and perform that function.

Similarly, each of the four die icons on the main page rolled dice; the dice-rolling script was in the stack, not in each die image. The six-sider with three pips rolled three six-sided dice using this code:

[toggle code]

  • on mouseUp
    • put Dice ("6,6,6") into card field DiceShow
  • end mouseUp

The Dice function looped through each of those numbers and rolled them as dice.1

[toggle code]

  • function Dice TheDice
    • put 0 into TheRoll
    • repeat with Count=1 to the number of items of TheDice
      • add random(item Count of TheDice) to TheRoll
    • end repeat
    • return TheRoll
  • end Dice

In some ways, HyperTalk was even more intuitive than AppleScript. Thirty years later, and I still occasionally try to use the HyperTalk syntax of “add number to variable” when I’m writing an AppleScript.

You can see these functions work in HyperCard Simulator. Choose “edit simulator script” and then “show the message box”. Then paste the Dice function or the Digits function into the “simulator script”. In the message box, type Dice(“10,10,10”) to roll 3d10, or Digits(5.151233, 2) to convert a number that is a long string of digits to a shorter string of digits, rounding in the process.2

What brought me down this memory lane is that I just discovered Pierre Lorenzi’s HyperCardPreview which allowed me to open up my Men & Supermen character database and see the characters as well as the underlying scripts. It doesn’t run stacks, but it does allow viewing the cards as they last appeared.

Cyber Men & Supermen character sheet: Character sheet for the player character hero “Cyber” in Men & Supermen, from an old HyperCard Stack.; Men & Supermen; HyperCard

It was very easy to create databases without even thinking of them as databases, but rather as sequential documents, such as a series of character sheets.

Now that I’m able to see these cards again, I wanted to make sure I don’t lose them again, by taking a screenshot of every card in the stack—that’s 161 cards (four backgrounds, but a whole lot of Men & Supermen characters). Sadly, HyperCardPreview itself is not scriptable. But AppleScript’s System Events can script it.

The basic logic of this AppleScript is:

  1. Use System Events to get HyperCardPreview’s window position and size.
  2. Repeat for each card, using the current card number and total number of cards from HyperCardPreview’s window title.
  3. Use /usr/sbin/screencapture to capture the screen area covered by HyperCardPreview’s window position and size.

[toggle code]

  • --do a screen capture of all cards in a Hypercard Stack
  • --Jerry Stratton, astoundingscripts.com
  • --choose folder to save into
  • set destination to POSIX path of (choose folder with prompt "Where do you want to save the screen shots?")
  • --bring HyperCardPreview to the top and get window information
  • tell application "System Events"
    • tell application process "HyperCardPreview"
      • if (count of windows) is 0 then
        • display dialog "Open and size the stack first."
        • return
      • end if
      • set frontmost to true
      • tell first window
        • set windowPosition to position
        • set windowSize to size
      • end tell
    • end tell
    • --determine the rect to copy
    • set windowRect to item 1 of windowPosition & "," & item 2 of windowPosition & "," & item 1 of windowSize & "," & item 2 of windowSize as string
    • --screenshot all remaining cards
    • repeat
      • --determine filename from title of HyperCardPreview window
      • tell first window of application process "HyperCardPreview" to set stackTitle to title
      • --get the current card number and the total card count from the title
      • set titleReversed to reverse of characters of stackTitle as string
      • set slashLocation to (length of stackTitle) - (offset of " / " in titleReversed)
      • set spacesLocation to (length of stackTitle) - (offset of "  " in titleReversed)
      • set totalCards to characters (slashLocation + 1) thru -1 of stackTitle as string as number
      • set currentCard to characters (spacesLocation) thru (slashLocation - 1) of stackTitle as string as number
      • --create a filename using the title and the current card number
      • set cardTitle to characters 1 thru (spacesLocation - 2) of stackTitle as string
      • set cardTitle to (currentCard as string) & " " & cardTitle & ".png"
      • --take a screenshot of the stack window
      • do shell script "/usr/sbin/screencapture -R " & windowRect & " " & quoted form of destination & quoted form of cardTitle
      • --are we done?
      • if currentCard is totalCards then exit repeat
      • --advance to next card
      • tell application process "HyperCardPreview"
        • click menu item "Go to Next Card" of menu "Browse" of menu bar 1
      • end tell
    • end repeat
  • end tell
AppleScript Editor: Enable Script Menu: Enable the Script Menu in AppleScript Editor’s preferences.; AppleScript

Enable the script menu inside of Script Editor’s preferences.

Paste this into Script Editor, save it as an application, and call it something like “Capture All Cards”. Use Script Menu while using HyperCardPreview to open the HyperCardPreview scripts folder, and drag or save the script there.

The script uses Do Shell Script to call screencapture on the command line to perform the screen capture. Using System Events to take the screenshot turned out to be troublesome. While I was able to get it to screenshot the correct window using cliclick to move the mouse and System Events to type the correct keystrokes to invoke screen capture, all of the screenshots ended up on the Desktop without any identifying name.

I also considered using JXA, because it would have made parsing the title easier. I’ll have more about JXA vs. AppleScript for this task in a later post.

HyperCardPreview also allows saving the stack as a JSON file, with all of the data and scripts intact as text. This file can be iterated through with any scripting language that supports JSON, such as Python or Perl. If you still have any old HyperCard stacks lying around, I recommend exporting now, whether you think you’ll need the data later or not.

HyperCardPreview script menu: The script menu for HyperCardPreview, when script menu is enabled in Script Editor.; AppleScript; HyperCard

Open HyperCardPreview’s script menu from the script menu.

Capture All Cards needs to be saved as an application, so that you can grant it permission to control applications on your computer. At various points macOS may ask you to grant Capture All Cards or Script Editor access to control your computer. That’s necessary for System Events to work. I’ve found that if you later edit an AppleScript application, you’ll need to manually remove it from both the Accessibility list and the Screen Recording list under the Privacy pane of the Security & Privacy settings. It no longer has permission3 but macOS doesn’t ask to add it.4

So whenever you update the script for your own purposes, delete Capture All Cards from both of those lists, and then drag the app back in via the Finder. You can display the app in the Finder from the Script Menu while using HyperCardPreview if you saved it there.

August 9, 2023: JXA and AppleScript compared via HyperCard
Superhero stack opening card: Main “control panel” card for the Men & Supermen superhero character sheet HyperCard stack.; Men & Supermen; HyperCard

All this for a script that will be used no more than a handful of times, to restore long-replaced data from the nineties. It’s time to quote Douglas Adams again.

In A HyperCard Time Machine I wrote that I also seriously considered writing the script as a JavaScript (JXA) app. In fact, I did write the script as a JavaScript app. It was a toss-up to the end which version I was going to use, so they’re both fully-working versions.

One advantage the JXA version has is not requiring old-school, BASIC-style manipulation of strings to extract the current card number and the total card count from the window title. Instead of grabbing the location of the final slash, which required serious gymnastics in AppleScript, the JavaScript solution can use a regular expression that is both shorter and more reliable:

[toggle code]

  • titleRegex = new RegExp("^(.+[^ ])  *([1-9][0-9]*) *\/ *([1-9][0-9]*)$");
  • titleParts = titleRegex.exec(title);
  • filename = titleParts[1];
  • currentCard = titleParts[2];
  • totalCards = titleParts[3];

JXA also doesn’t use tell blocks. It places the application in a variable, and the things we want to tell it to do can be handled at any point without worrying about how a tell block affects the syntax. At the beginning of the app, I did:

  • app = Application.currentApplication();
  • app.includeStandardAdditions = true;
  • system = Application("System Events");

This gave me the current application and System Events. The current application is necessary for dialog with the user, which is why I’ve also included the Standard Additions on it. The Standard Additions include Display Dialog and Choose Folder.

  1. Men & Supermen used a lot of strange dice combinations.

  2. The simulator appears to be able to open stacks that HyperCardPreview cannot. I have one stack (of AD&D magic spells) that crashes HyperCardPreview, but the simulator loads it fine.

  3. Probably because the signature has changed due to the code change.

  4. Oddly, if the script is not in the Screen Recording list, it will still do the screen capture, but it will also pop up a dialog asking to give it permission while the screen captures are all snapping in the background!

  1. <- Searchable PDFs
  2. macOS parent mailboxes ->