Why I'm Abandoning Clojure

Clojure is a lovely language idea. It is elegant. It is clever.

Clojure is an abysmal language reality for my purposes.

  • 35 lines of java dump for a syntax error? It makes me miss the original Wirth Pascal compiler, which seemed to have the singular error message “Syntax error, possibly missing semicolon on line above.”
  • I really, really want compile-time type checking on my parameters.
  • If I run my program via “lein run”, I have no trouble reading a data file. If I package it up into an uberjar, the code fails. I’m supposed to master class loaders just to be able to read a file when I package my app as a jar? I’m sure that Clojure and Java are doing something that is absolutely essential for somebody, but the environment is making simple stuff hard, and I won’t tolerate that.
  • I insist on an interactive debugger. You can actually get one via the Cursive IDE, and you can sorta, kinda get one via a plugin for Visual Studio Code. The Cursive one actually works… until it freaks out and you have to either restart the REPL or you have to do something called “Invalidate Caches/Restart”. (There’s no real indication whether your observed bad behavior is a bug in your code or if you need to invalidate caches. It’s just a Hail Mary you try when you don’t like the results you’re getting.) The plugin for Visual Studio Code – I’m sure it works for its developer, but I really tried to make it work. Maybe someday. If the language doesn’t have an interactive debugger, it’s just plain immature in my book.
  • I want to be able to code a ‘premature return’ from a function. Yeah, I know it’s ‘unnecessary.’ But I’ll tell you, it really simplifies real-world code when I can code if-exceptional-condition-return-error. The Clojure way seems to be to just issue a stack trace on bad data.
  • There’s an awful lot of cognitive overhead for an occasional programmer. In addition to the language itself, you’ve got Leiningen and a baroque directory structure to re-master, every time you take 6 months off from programming. It reminds me of J2EE, and that’s the epitome of excess cognitive overhead.

Firefox Portable Incredibly Slow

Firefox Portable Launches and Loads Very Slow

Firefox Portable started taking forever to launch and load. I was running it from a USB flash drive, so I suspected drive problems. That wasn’t it.

I only use Firefox Portable to run an old Tiddlywiki Classic, with the Tiddlyfox extension. Clicking in the search box would often provoke “not responding” with a white/gray browser window, for long periods of time. Closing Firefox could take 10 minutes to complete. Launching Firefox with TiddlyWiki could take 10 minutes.

Something was making my prefs.js file huge (as in 300 MB). When I tried to open in from Notepad, it brought my machine to a crawl, even after I copied the file to C:\Temp, so I knew it wasn’t a flash drive problem.

Something was adding millions of “E:\FirefoxPortable\FirefoxPortable” to my prefs.js file. I could look at a history of that file and see it grow by KB per day. (My flash drive is E:.)

The solution:

  • Close (and kill, if necessary) all Firefoxes (portable and installed).
  • Delete E:\FirefoxPortable\Data\profile\prefs.js
  • Launch Firefox Portable. It will offer to download an update. Tell it NO.
  • Tools/Options (alt-t)
  • Press Alt-Shift-N to “Never check for updates”. (You must use Alt-Shift to acces an accelerator key on the Options tab.)
  • Exit and re-launch Firefox Portable, opening your TiddlyWiki.
  • Click the TiddlyFox icon and perma-enable saving changes
  • Save changes.

Checklist in LibreOffice

Sometimes I need an outline/bulleted list which looks like a checklist. Here’s how to create a checkbox bullet in LibreOffice (on the Mac).

  • Create an ordinary bulleted list. (Use the bullets tool on the toolbar.)
  • Put the insertion point (i-beam cursor) at the beginning of the text of the line with the bullet you want to turn into a checkbox.
  • Format > Bullets and Numbering > Options > Select… > Font = Wingdings
  • I like the the first empty box in the list. (End of the 5th row on my display.) Character value Unicode F06F.

Tiddlywiki Without Docker On Macbook

Tiddlywiki on Node.js Without Docker on My Macbook

Here’s my how-to for how I set up plain old Tiddlywiki on my Macbook. My plan is to

  • Use the ‘Node.js’ TW natively (without Docker, without the MultiUser plugin). Soak. Get really comfortable with it.
  • Add MultiUser plugin. Soak. Get really comfortable with it.
    • Documentation for the plugin is here: https://github.com/OokTech/TW5-MultiUser
  • Wrap it in Docker.

That way, if there is weirdness, I’ll recognize which piece of the puzzle to blame.

Note: node.js version 9.7.1 for Darwin was installed from https://nodejs.org/download/release/ on my machine before I started this process.


First, define some constants in .bash_profile, to reduce redundant typing:

export twd='/Users/kevin/Sync/Sites/tw-plain'
alias twd="cd $twd"

Then

. ~/.bash_profile 
mkdir $twd
cd $twd

Create runme.sh containing:

#!/bin/sh
# Abort immediately on shell errors
set -e

TWD=/Users/kevin/Sync/Sites/tw-plain
cd $TWD

# Ensure Tiddlywiki is installed.  Peg the version.  I don't want it changing by surprise.
if [ ! -d node_modules/tiddlywiki ]; then
  echo "Installing tiddlywiki node package locally. This will take several minutes..."
  npm install tiddlywiki@5.1.15
fi

#This is how you would initialize a non-plugin wiki
if [ ! -d $TWD/mywiki ]; then
  /usr/local/bin/node $TWD/node_modules/tiddlywiki/tiddlywiki.js mywiki --init server
fi

#if [ ! -d node_modules/tiddlywiki/editions/MultiUserWiki ]; then
#
#  echo "Installing tiddlywiki node package locally. This will take several minutes..."
#  npm install tiddlywiki@5.1.15
#
#  cd $TWD/node_modules
#  echo "CLONING MultiUser plugin"
#  git clone --depth=1 https://github.com/OokTech/TW5-MultiUser.git tiddlywiki/plugins/OokTech/MultiUser

#  echo "Copying starter wiki"
#  cp -r tiddlywiki/plugins/OokTech/MultiUser/MultiUserWiki tiddlywiki/editions/
#fi

cd $TWD

echo "Launching node"
#exec /usr/local/bin/node ./node_modules/tiddlywiki/tiddlywiki.js editions/MultiUserWiki  --wsserver 8080 ${USERNAME:-user} ${PASSWORD:-'wiki'} 0.0.0.0
exec /usr/local/bin/node $TWD/node_modules/tiddlywiki/tiddlywiki.js mywiki --server 8080 $:/core/save/all text/plain text/html 127.0.0.1

Be sure to: chmod +x runme.sh

Launch your wiki with:

./runme.sh

The first time you launch, you may see the following errors. They can safely be ignored:

npm WARN saveError ENOENT: no such file or directory, open '/Users/kevin/Sync/Sites/tw-plain/package.json'
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN enoent ENOENT: no such file or directory, open '/Users/kevin/Sync/Sites/tw-plain/package.json'
npm WARN tw-plain No description
npm WARN tw-plain No repository field.
npm WARN tw-plain No README data
npm WARN tw-plain No license field.

Note that if you want to start from a clean slate, you’ll find it handy to have a script. Create a file named make-clean.sh:

#!/bin/bash
echo "Run this only if you want to delete all the wiki content and start over. Press control-C to stop or Enter to continue."
read

rm -rf mywiki node_modules package-lock.json

Be sure to: chmod +x make-clean.sh

Suppressing an Annoyance:

See this for information on suppressing a superfluous unsaved-changes warning from your browser. The author says this is OK here.

Testing:

In addition to a smoke test of saving tiddlers and reloading the page to confirm they really got saved, confirm that the wiki is accessible via localhost:8080 and is not accessible at external-ip-address:8080.

Daemon:

Once you are satisfied that it works, you’ll want to set up LaunchCtl to launch it automagically.

Create a LaunchCtl file /Users/kevin/Library/LaunchAgents/com.kleinfelter.tiddlywiki.plist to specify how to run your container as a service:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.kleinfelter.tiddlywiki</string>
    <key>ProgramArguments</key>
    <array>
        <string>/bin/bash</string>
        <string>/Users/kevin/Sync/Sites/tw-plain/runme.sh</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
    <key>StandardOutPath</key>
    <string>/var/log/tiddlywiki/tiddlywiki.log</string>
    <key>StandardErrorPath</key>
    <string>/var/log/tiddlywiki/tiddlywiki.err</string>
</dict>
</plist>

Then:

sudo mkdir /var/log/tiddlywiki
sudo chown kevin /var/log/tiddlywiki

Then load your LaunchCtl with:

launchctl load /Users/kevin/Library/LaunchAgents/com.kleinfelter.tiddlywiki.plist

And check to be sure your service is working.

Customizing your wiki:

  • Control Panel:
    • Settings > Camel Case Wiki Links: Disable

I prefer a different look:

  • Go to http://j.d.material.tiddlyspot.com/
  • If you don’t get clear instructions, click the ‘hamburger menu’ to view the “Hello, world” page.
  • Follow the instructions on the page to install the theme and the ‘more material’ and the Roboto font, but NOT the swipe stuff.
    • Tip: Drag each icon from the source site into your TW, and then accept the import of all at once.
    • Tip: Download the roboto.tid and import it into your TW.
  • Go to the TW Control Panel, Appearance, Theme, and choose ‘Material’.
  • Note: IF you use FAB buttons, the secondary FABs only appear when you hover over the primary FAB.

Make My Own Palette:

  • Copy the Material palette into Material-KPK
  • It should have type=application/x-tiddler-dictionary
  • Tags=$:/tags/Palette
  • Fields:
    • name: Kevin’s Palette
    • description: Kevin’s Palette
  • Refresh, and it should show up with the other Palettes in Control Panel > Appearance.

Install Markdown:

  • There are two Markdown parsers available:
    • https://tiddlywiki.com/plugins/tiddlywiki/markdown - the official markdown plugin; supports PHP Extra dialect.
    • http://bjtools.tiddlyspot.com > MarKed - an unofficial markdown plugin; supports GFM dialect.
  • My standard Markdown dialect is GFM, and my Macbook tools are configured to support that. On the one hand, the official plugin is likely to be around as long as Tiddlywiki is, and there is some risk that BJttools could abandon his. OTOH, I’d like my tiddlers to use my standard Markdown dialect. That’s kinda key to my strategy of being able to access .tid files as if they were Markdown.
  • BJTools doesn’t include a new-Markdown button and the official plugin does. They use different content types, so I could actually install both. I’m going to install both and I’m going to tinker the official plugin’s button to create BJTools Markdown.
  • Install but do NOT use this markdown. It supports PHP Markdown Extra dialect. I have standardized on GFM (see my Macbook Configuration Management document.)
    • Drag and drop from https://tiddlywiki.com/plugins/tiddlywiki/markdown
    • Set dialect to Maruku by editing $:/config/markdown/dialect (shadow tiddler)
    • Adjust your Page toolbar to have the create-markdown button and NOT the create-wiki-markup button (via Control Panel > Appearance > Toolbars > Page Toolbar).
  • Install and do use this markdown. It supports GFM:
    • Drag and drop from http://bjtools.tiddlyspot.com > MarKed > $:/plugins/bj/plugins/marked
    • also import his Flexitype plugin.
  • Then edit $:/plugins/tiddlywiki/markdown/new-markdown-button (system tiddler) and change “text/x-markdown” (PHP) to “text/x-marked” (GFM)

Enable Selection and Close of View-mode Tiddlers:

Navigate to Sidebar > More > Shadows, and edit $:/core/ui/ViewTemplate. Replace it with the following:

\define frame-classes()
tc-tiddler-frame tc-tiddler-view-frame $(missingTiddlerClass)$ $(shadowTiddlerClass)$ $(systemTiddlerClass)$ $(tiddlerTagClasses)$
\end
\define folded-state()
$:/state/folded/$(currentTiddler)$
\end
<$set name="storyTiddler" value=<<currentTiddler>>><$set name="tiddlerInfoState" value=<<qualify "$:/state/popup/tiddler-info">>><$tiddler tiddler=<<currentTiddler>>><$keyboard key="alt-W" message="tm-close-tiddler"><div tabindex="1" class=<<frame-classes>>><$list filter="[all[shadows+tiddlers]tag[$:/tags/ViewTemplate]!has[draft.of]]" variable="listItem"><$transclude tiddler=<<listItem>>/></$list>
</div>
</$keyboard>
</$tiddler></$set></$set>

For the record (and for some future release, when you have to derive this anew) the changes to the original ViewTemplate are:

  • Add to the div tag: tabindex=1
  • Wrap a keyboard widget around the div, with key=”alt-W” and message=”tm-close-tiddler”

Next:

  • Edit $:/config/Search/AutoFocus (shadow tiddler) and set it to: false

This implements a not-quite-perfect “Alt-W closes the current view-mode tiddler.” It does, in fact, close the currently selected tiddler. The gap is that you often must click on the tiddler to select it. I’d like to modify TW to auto-select a tiddler when it is opened/scrolled-to. I think this might be do-able (particularly in zoomin mode), but I haven’t figured it out yet. One possibility would be to edit $:/core/templates/tiddlywiki5.html and add an onFocus method to <body>. The trick there is that body might get the focus before the first tiddler is displayed. Maybe I need to add an onKeypress to tranfer focus or just redirect keystrokes from the body to the first tiddler. Or I could set a timer…

Set Up Tabbed Tiddlers:

  • Install StoryTabs from http://bjtools.tiddlyspot.com/ (drag the $ link for StoryTabs from his site to your TW.)
  • Go to Control Panel and set Appearance > Story View = zoomin
  • A note about StoryTabs: It solely adds the tabs above the story river. The reason it looks like tabbed tiddlers is because zoomin mode hides all tiddlers except for the ‘top’ tiddler in the river.

Hide the Story-date Line:

  • Browse to the very slow-to-load http://designwritestudio.updog.co
  • Drag this tiddler to your TW: $:/_Menu/Home/Configuration/Options
  • Rename it to “Configure Tiddler Subtitle”
  • Visit that tiddler and turn off display of subtitles and tags.

Enable Double-click to Edit:

  • Go to https://danielorodriguez.com/TW5-2click2edit/
  • Drag and drop the 2click2edit plugin into my wiki

Tiddlywiki With Docker On Macbook

Tiddlywiki on Node.js With Docker on My Macbook

CAUTION: I haven’t implemented this with live data yet. I decided it was too much change at once. (I’m a TW Classic user.) The staged implementation plan is:

  • Use the ‘Node.js’ TW natively (without Docker, without the MultiUser plugin). Soak. Get really comfortable with it.
  • Add MultiUser plugin. Soak. Get really comfortable with it.
  • Wrap it in Docker.

That way, if there is weirdness, I’ll recognize which piece of the puzzle to blame.

That being said, here’s how my proof-of-concept project went…

Introduction:

I decided to run the Node.js edition of Tiddlywiki on my Macbook. I elected to use Docker so that all dependencies can be contained (i.e. in a container). That way, if I later decide to run a different version of Node.js for another project, Tiddlywiki’s Node.js remains unchanged. And I also threw in the MultiUser plugin. (I don’t care about multi-user. It also adds sub-wikis served from the same port.)

This document focuses on the Tiddlywiki and Node.js aspects. My general notes about Dockerfile and docker-compose are here.

Documentation for the plugin is here: https://github.com/OokTech/TW5-MultiUser


First, define some constants in .bash_profile, to reduce redundant typing:

export twd='/Users/kevin/Sync/Sites/tw-node'
alias twd="cd $twd"

Then

. ~/.bash_profile 
mkdir $twd
cd $twd

Some explanation about my approach:

I wanted to put all the setup in the Dockerfile, and it worked pretty well that way until I added MultiUser plugin. Without the plugin, you can put your wiki files anywhere you want. With the plugin, it wants your wiki(s) to live under node_modules/tiddlywiki/editions. If you want a persistent wiki, you have to mount your wiki directory on a host directory, but you can’t mount onto a host directory from the Dockerfile (because they want that directory to be flexible at runtime). You can’t update node_modules/tiddlywiki/editions at setup (from the Dockerfile) and also mount it at runtime.

So I do a lot of conditional ‘setup’ in the container-rc.sh script.

Continuing:

Create ‘Dockerfile’ containing:

# Use the node image, based on the alpine micro-Linux image as our starting point.
FROM node:alpine
RUN apk add --no-cache git

# Define a volume for mounting on host file system.  (i.e. Enable mounting this path.)
VOLUME /var/lib/tiddlywiki

# set the container's working directory for any 
# RUN, CMD, ENTRYPOINT, COPY and ADD commands *below*.
WORKDIR /var/lib/tiddlywiki

# Copy container-rc.sh from the Dockerfile directory into the image.
# Add a container startup script (runs inside the container.  e.g. AUTOEXEC.BAT)
ADD container-rc.sh /usr/local/bin/container-rc.sh

# These are the VM port.  At run-time, map them to host IP:ports
# We need two consecutive ports: One for the wiki server and the MultiUser plugin
# uses another port for its 'websockets'
EXPOSE 8080 8081

Create docker-compose.yml containing (reminder - no tabs in YAML):

version: "3"                    # Using version 3 docker-compose file format.
services:
    tw-node:                    # Name of the image? service? I'm creating.
        build: .                # Use the Dockerfile in . to build the image
        ports:
                - "127.0.0.1:8080:8080" # host:container.
                - "127.0.0.1:8081:8081" # host:container.
                                # Could also use "8080:8080" to not specify host IP.
        # volume mount syntax= /host/path:/container/path
        volumes:
           - /Users/kevin/Sync/Sites/tw-node:/var/lib/tiddlywiki
        command: /bin/sh /usr/local/bin/container-rc.sh

Build the image:

docker-compose build --no-cache

Create runme.sh containing:

#!/bin/sh
docker-compose up

Create container-rc.sh containing:

#!/bin/sh
# I have to do much of the setup in here because I want to put my files onto the mounted volume

# Abort immediately on shell errors
set -e

TWD=/var/lib/tiddlywiki
cd $TWD

# This is how you would initialize a non-plugin wiki
#if [ ! -d /var/lib/tiddlywiki/mywiki ]; then
#  /usr/local/bin/node /usr/local/bin/tiddlywiki mywiki --init server
#fi

if [ ! -d node_modules/tiddlywiki/editions/MultiUserWiki ]; then

  echo "Installing tiddlywiki node package locally. This will take several minutes..."
  npm install tiddlywiki@5.1.15

  cd $TWD/node_modules
  echo "CLONING MultiUser plugin"
  git clone --depth=1 https://github.com/OokTech/TW5-MultiUser.git tiddlywiki/plugins/OokTech/MultiUser

  echo "Copying starter wiki"
  cp -r tiddlywiki/plugins/OokTech/MultiUser/MultiUserWiki tiddlywiki/editions/
fi

cd $TWD/node_modules/tiddlywiki

echo "Launching node"
exec /usr/local/bin/node ./tiddlywiki.js editions/MultiUserWiki  --wsserver 8080 ${USERNAME:-user} ${PASSWORD:-'wiki'} 0.0.0.0

Note that this uses the latest edition of the MultiUser plugin. If you want to specify a version you’ll need to:

  • Clone the plugin repository to something like /tmp/MultiUser.
  • cd to that directory and run git log. Identify the commit hash of the version you want.
  • git clone commithash-here

Be sure to: chmod +x runme.sh and chmod +x container-rc.sh

Launch your wiki with:

./runme.sh

Note that if you want to start from a clean slate (i.e. re-run the init stuff), you’ll find it handy to have a script. Create a file named make-clean.sh:

#!/bin/bash
echo "Run this only if you want to delete all the wiki content and start over. Press control-C to stop or Enter to continue."
read
rm -rf /tmp/node_modules
rm -rf /tmp/package-lock.json

mv node_modules /tmp
mv package-lock.json /tmp

Be sure to: chmod +x make-clean.sh

You need not re-build the image after a make-clean… just start the container and it will re-run the init.

Suppressing an Annoyance:

See this for information on suppressing a superfluous unsaved-changes warning from your browser. The author says this is OK here.

A Real Problem:

When running under Docker, if I create a title-only tiddler (no body), it looks like it saved it (i.e. the red save button goes gray), but it doesn’t show up under the ‘Recent’ tab and it is unable to display it when the page is reloaded. You can make them show up by (in the host) navigating to that wiki’s data folder and running touch * (but that’s gonna set the time on every tiddler).

Plain Node.js TW doesn’t have this problem in a container. The plugin does not have this problem when run outside a container.

The plugin’s author says the tiddler is visible to the wiki - it just isn’t listed under Recents. I think he’s right. He says it’s a known bug.

Testing:

In addition to a smoke test of saving tiddlers (in the sub-wikis) and reloading the page to confirm they really got saved, confirm that the wiki is accessible via localhost:8080 and is not accessible at external-ip-address:8080.

Daemon:

Once you are satisfied that it works, you’ll want to set up LaunchCtl to launch it automagically.

Create a LaunchCtl file /Users/kevin/Library/LaunchAgents/com.kleinfelter.tw-node.plist to specify how to run your container as a service:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.kleinfelter.tw-node</string>
    <key>ProgramArguments</key>
    <array>
        <string>/bin/bash</string>
        <string>/Users/kevin/Sync/Sites/tw-node/runme.sh</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
    <key>StandardOutPath</key>
    <string>/var/log/tw-node.log</string>
    <key>StandardErrorPath</key>
    <string>/var/log/tw-node/tw-node.err</string>
</dict>
</plist>

Then:

sudo mkdir /var/log/tw-node
sudo chown kevin /var/log/tw-node

Then load your LaunchCtl with:

launchctl load /Users/kevin/Library/LaunchAgents/com.kleinfelter.tw-node.plist

And check to be sure your service is working.

Kevins Guide To Using Dockerfile

Kevin’s Guide to Using Dockerfile and docker-compose.yml

and generaly, how to docker-enable an application.

As a rule, don’t install libraries or scripting languages directly onto your host. e.g. The moment you install Ruby 1.9.1 onto your host, you’ll run into a must-have app which requires Ruby 1.8.5. Yes, there are ways to manage multiple Ruby (or multiple Python or multiple libc), but it is just safer and cleaner to put all that stuff inside a container.

A Dockerfile specifies how to build an image. It doesn’t address how to run that image as a container. docker-compose is a tool to run your images. Here’s how I use something new under Docker on my Mac.

Strictly speaking, docker-compose is about running multiple containers. However, you can use it to run a single container, and it makes the command line for that container simpler, by allowing you to put some of your options in the docker-compose file.

Note: This isn’t quite how I set up my Jekyll images. Those were done before I defined my canonical form.

  1. Create a folder for your app. We’ll refer to it as app-dir. Put everything shown below into this folder.

  2. Write the Dockerfile just to build the image, not to run it. e.g.

    # Use the specified image as a baseline.
    # See https://hub.docker.com/explore/ for official images.
    # Format is one of:
    #   FROM image
    #   FROM image:tag
    #   FROM image
    # The one below comes directly from the doc at https://hub.docker.com/_/node/
    FROM node:alpine
    
    # The RUN command says, "After you download the image, run this command to
    # customize the image to your needs.  Runs as part of building (not starting) the image
    #
    # The RUN below says:
    #   Install v 5.1.15 of Tiddlywiki (TW) into Node.js using npm (the Node package mgr).  #   You could specify a newer version (or no version, by omitting '@5.1.15') but you 
    #  may have to adapt these instructions if TW has major changes.
    # I recommend that you ALWAYS specify a version or a tag.
    RUN npm install -g tiddlywiki@5.1.15
    RUN /usr/local/bin/node /usr/local/bin/tiddlywiki wiki-data --init server
    
    # Define a volume for mounting on host file system.  (i.e. Enable mounting this path.)
    VOLUME /var/lib/tiddlywiki
    
    # set the container's working directory for any 
    # RUN, CMD, ENTRYPOINT, COPY and ADD commands *below*.
    # Reminder: TW writes its files to the working directory.
    WORKDIR /var/lib/tiddlywiki
    
    # Add a script which you will (later) launch inside the container to do tasks
    # you want done EACH time the container starts.
    ADD container-rc.sh /usr/local/bin/container-rc.sh
    
    # Expose any ports.  Note that on Win/Mac, Docker runs in a VM and this is the
    # VM port.  You need to map the VM port to a host port at runtime, if you are to
    # access it.
    EXPOSE 8080
    

  3. Write docker-compose.yml to specify how to run the image. (Reminder: No tabs in YAML.)

    version: "3"                    # Using version 3 docker-compose file format.
    services:
        tw-node:                    # Name of the image? service? I'm creating.
            build: .                # Use the Dockerfile in . to build the image
            ports:
                - "127.0.0.1:8080:8080"
                                    # host:container.  Quotes strongly recommended.
                                    # Two forms: hostPort:containerPort or 
                                    #     hostIP:hostPort:containerPort
            volumes:
                                    # /host/path:/container/path
                - /Users/kevin/Sync/Sites/tw-node:/var/lib/tiddlywiki
            command: /bin/sh /usr/local/bin/container-rc.sh
    
  4. Create runme.sh, to launch the container, containing

    #!/bin/bash
    cd /path/to/app-dir
    /usr/local/bin/docker-compose up
    
  5. Create container-rc.sh, which starts the processes inside the container:

    ??
    
  6. Be sure to:

    chmod +x runme.sh
    chmod +x container-rc.sh
    

  7. Build your image with:

    docker-compose build --no-cache
    
    • You can usually omit –no-cache, and it will use cached layers upon which this image depends. Adding –no-cache ensures that you don’t get a layer you cached which was built with special args. I don’t do a lot of building, so I don’t mind if it is slow.
  8. Test your image with:

    ./runme.sh
    
  9. It can take 15-20 seconds to start a simple image. You’ll usually see this when it is up: “Server running… press ctrl-c to stop.”

  10. Once you validate your service is working, press control-C to stop it.

  11. Create a LaunchCtl file /Users/kevin/Library/LaunchAgents/com.kleinfelter.SERVICE_NAME_HERE.plist to specify how to run your container as a service:

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
        <key>Label</key>
        <string>com.kleinfelter.SERVICE_NAME_HERE</string>
        <key>ProgramArguments</key>
        <array>
            <string>/bin/bash</string>
            <string>/path/to/app-dir/runme.sh</string>
        </array>
        <key>RunAtLoad</key>
        <true/>
        <key>KeepAlive</key>
        <true/>
        <key>StandardOutPath</key>
        <string>/var/log/SERVICE_NAME_HERE/SERVICE_NAME_HERE.log</string>
        <key>StandardErrorPath</key>
        <string>/var/log/SERVICE_NAME_HERE/SERVICE_NAME_HERE.err</string>
    </dict>
    </plist>
    
  12. sudo mkdir /var/log/SERVICE_NAME_HERE
    sudo chown kevin /var/log/SERVICE_NAME_HERE
    
  13. Then load your LaunchCtl with:

    launchctl load /Users/kevin/Library/LaunchAgents/com.kleinfelter.SERVICE_NAME_HERE.plist
    
  14. And check to be sure your service is working.

  15. Reminder: To open a bash prompt in your image (without installing ssh and using ssh):

    docker docker ps --format 'table ' #Identify the desired container name
    docker exec -it CONTAINER_NAME_HERE /bin/bash #-i gets STDIN, -t gets a pseudo-tty
    
  16. Rejoice!

Outlook 2016 Sending Empty Emails

My Outlook started sending empty emails. The really annoying thing was that I could spend an hour composing the perfect email, hit Send, and it would strip the content before sending. The email in the Sent folder was devoid of content too. Gone!

I dunno if the following really fixed it, or if it simply went away on its own at the same time:

  • File > Options > Mail > Compose messages in this format:
    • Set it to Rich Text
  • Send an email. It works!
  • Set format back ot HTML. It still works!

Leaving LastPass

I’m leaving LastPass. I was uncomfortable when LogMeIn bought them. Then it started flaking out. Little things, like crashing after login; the Mac version saying it was installing the binary shim but not actually doing it.

The final straw was when it started telling me that it couldn’t contact the server. The work-around included clearing the cache. When I started to clear the cache, LastPass warned me that doing so could lose data because of a ‘retry’ file. I contacted support, explained that I considered data loss unacceptable, and they said I should remove/reinstall the browser plugin. I did so. Whoops. Removing the browser plugin… removes the plugin data, including the retry file. So I lost some updates and I have no way to know what they were.

Naturally this happened less than a week after I had renewed my LastPass subscription!

There are only two things a password wallet must do: Keep my passwords confidential and give them to me when I ask. Losing data causes the ‘give to me when I ask’ to fail.

The trouble with all services is that you’re trusting them not to have bugs (or a deliberate, hidden leak).

I need:

  • Mac
  • Windows
  • Sync between a Mac and a Windows computer.
  • Local storage
  • Export and import of passwords

I want:

  • Not cloud. If online, it must be zero-knowledge and 2FA.
  • Audited security
  • Sync between multiple clients. (I could live with entry on just one device.)
  • Read access on Android.
  • High confidence in confidentiality and data integrity.
  • To avoid installing eleventy-three ‘extensions’ which don’t get audited as well as the main product.
  • YubiKey or other 2FA. (Key file on USB would be OK.)
  • Ability to turn OFF auto-update.
  • Some way to export passwords to my wife, and to read her passwords.

Some possibilities:

  • KeePass Family
    • Notes:
      • Import from LastPass: https://tibdex.github.io/lastpass-to-keepass/
      • If implemented right, a web data file with a local key file, should make it decryptable only by me.
      • It is really a database, without browser integration. To use it:
        • Navigate your browser to the login page
        • Bring up KeePass
        • Search KeePass for the right login record.
        • Press the auto-type keystroke. It will minimize itself and type ID, tab, password, Enter.
    • KeeWeb - cross platform. In browser, or as Electron app. Desktop and cell phone.
    • KeePass
      • Requires Mono on Linux and Mac. Reportedly has issues on Mac.
    • KeePassXC - cross platform. (KeePassX is obsolete.)
  • Bitwarden, self-hosted - (via Docker, or build from source)
  • Google Smart Lock - Google Chrome built-in password manager. It is ‘in the cloud’.

Improbable, but possible:

  • Text file - Let’s not reject this out of hand.
    • I do encrypt my disk drives. But if the data gets onto my work PC, my employer could read it - even if I use EFS.
    • I could use VeraCrypt portably on my work PC.
    • A text file, plus a shim to encrypt/unencrypt the current file open in my editor. (Risk of forgetting to encrypt before save!)
  • Sync a modern Excel file encrypted with SHA-512.
  • An Android-only Solution
    • I could store them solely on my cell phone (with a backup somewhere).
    • I’d want to be able to fingerprint or face-recognize to unlock the phone and the safe.
  • Keeper - multi-platform. Proprietary. Maybe a better LastPass, but similar in essence.
  • Text file in Linux VM, on a USB drive, with encrypted file system.

Rejected:

  • LastPass - nope. It lost some data.
  • Password Safe - the Mac version in unofficial. I don’t want ‘unofficial’ with my passwords unless it is 3rd-party audited.
  • SplashID - Looks interesting. It puts the program and the data on a USB stick. 42% 1-star (awful) ratings on Amazon!

Using AppleScript for GUI Scripting - Discovering the Interface

Sometimes you want to script a Mac OS X app, and you can’t really do it via the dictionary. At that point, you revert to something called “GUI scripting.” You automate the app’s user interface via the Accessibility interface. This is similar to what Autohotkey users do for Windows PCs.

Often, you want to do something like “Click on the Windows menu; then click on the first item.” The trick is that different apps implement menus which LOOK the same with different structure. You need to discover the actual structure of the menu in order to click it programmatically.

Here is a sample script to show all of the menu items on an application (Google Chrome) menu bar.

`osascript -sso > /private/tmp/StatusBarItems <&- <<EOF
tell application "System Events"
    get properties of every  menu bar item of menu bar 1  of process "Google Chrome"
end tell
EOF`

This will write the menu bar structure to /tmp/StatusBarItems. Buried in that file, you’ll see the following snippet. (The whole output is much larger because it dumps EVERY item in the menu.)

{minimum value:missing value, orientation:missing value, position:{470, 0}, class:menu bar item, accessibility description:missing value, role description:"menu bar item", focused:missing value, title:"Window", size:{70, 22}, help:missing value, entire contents:{}, enabled:true, maximum value:missing value, role:"AXMenuBarItem", value:missing value, subrole:missing value, selected:false, name:"Window", description:"menu bar item"}

Reformatting that, and dropping all of the “missing value” entries, it looks like:

{
    position:{470, 0}, 
		class:menu bar item,
		role description:"menu bar item",
		title:"Window", 
		size:{70, 22},
		entire contents:{}, 
		enabled:true,
		role:"AXMenuBarItem",
		selected:false,
		name:"Window", 
		description:"menu bar item"
}

The upshot of that is that you can use code to look for a “menu bar item” (in menu bar 1) where name = “Window”. You could probably search for the other attributes, but ‘menu item with name = x’ makes for more readable code than ‘menu item at 470,0.

The items under the Window menu have their own structure. Dump that structure with this code:

osascript -sso > /private/tmp/StatusBarItems <&- <<EOF
tell application "System Events"
    get properties of (first menu bar item where name is "Window") of menu bar 1  of process "Google Chrome"
end tell
EOF

That dumps the following to /tmp/StatusBarItems:

{minimum value:missing value, orientation:missing value, position:{470, 0}, class:menu bar item, accessibility description:missing value, role description:"menu bar item", focused:missing value, title:"Window", size:{70, 22}, help:missing value, entire contents:{}, enabled:true, maximum value:missing value, role:"AXMenuBarItem", value:missing value, subrole:missing value, selected:false, name:"Window", description:"menu bar item"}

and when you reformat and strip out missing values, it looks like:

{
    position:{470, 0}, 
		class:menu bar item,
		role description:"menu bar item",
		title:"Window", 
		size:{70, 22},
		entire contents:{}, 
		enabled:true,
		role:"AXMenuBarItem",
		selected:false,
		name:"Window", 
		description:"menu bar item"
}

which looks a whole lot like the output from the first dump. (In the first dump, which dumped everything in the menu, we visually located this menu item and manually extracted it. In this second dump, we used code to extract that one menu item.)

This information tells you that Google Chrome has:

  • A menu bar 1 containing a menu bar item named “Window”

You could dump the content of the Window menu bar item, and see the things that are in it. I wanted to click on the “Pin Tab” item in the Window menu. I happen to know that entries in a menu which don’t launch sub-menus are called “menu item”, so I didn’t take the time to write osascript to dump that menu.

Putting all of this information together, I knew I could write code like the following:

	tell application "Google Chrome" to activate
	tell window 1 of application "Google Chrome" to set visible to true
	tell application "System Events"
		tell process "Google Chrome"
			set m1 to (first menu bar item where name is "Window") of menu bar 1
			set mx to menu item "Pin Tab" of menu 1 of m1
			click mx
		end tell
	end tell