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

Remove OneDrive From Office 2016 Save Menu

Office 2016 always offers the option of saving your file to OneDrive (via File/Save or File/Save As). I hate that, because I don’t use OneDrive. To disable, set:

HKEY_CURRENT_USER\Software\Microsoft\Office\15.0\Common\SignIn\SignInOptions = DWORD 3

Whooshing From Chrome to Safari and Back

Chrome works better for me than Safari, particularly when I have many, many tabs open. However, when I run my Macbook on batteries, Chrome drains batteries too fast, so I’d like to use Safari when I’m on battery power. I created a couple of Applescripts to move my open tabs from Chrome to Safari and vice versa.

I found scripts online to do these, but they appear to rely on obsolete editions of OS X. My scripts below work on OS X High Sierra with the current Google Chrome as of 2018.01.01.

Conceptually, it is a simple task: Take these tabs in one browser and open them in the other. (“Whoosh them” from one browser to the other.) There are some challenges along the way:

  • The object model (“dictionary” in Apple parlance) for the browsers differ.
  • The object model is incompletely documented. There are many properties and methods which you just have to discover via Googling.
  • Pinned tabs behave differently between Chrome and Safai. Frankly, the Safari practice of putting your pinned tabs in every Safari window is just stupid.
  • You can’t pin a tab via the object model – you must resort to GUI scripting.
  • Bringing a window or tab to the foreground changes the order of the browser’s windows/tabs, so you can’t iterate through windows/tabs while bringing them to the foreground. You have to collect the IDs and then iterate through the IDs.
  • AppleScript is just an all-around peculiar language and environment.

Here is Chrome-to-Safari:

-- Move all tabs from Chrome to Safari.


-- Optionally close existing Safari tabs first
set question to display dialog "Close exisisting Safari tabs first?" buttons {"Yes", "No"} default button 1
set answer to button returned of question
if answer is equal to "Yes" then
	closeAllSafariTabs()
end if

tell application "Google Chrome"
	
	-- for every Chrome window, open a Safari Window
	repeat with theChromeWindow in windows
		set theSafariWindow to my openNewSafariWindow()
		
		-- for every tab in the Chrome window, open a tab in the Safari window
		repeat with theChromeTab in theChromeWindow's tabs
			my makeSafariTab(theSafariWindow, URL of theChromeTab)
		end repeat
	end repeat
end tell

pinSpecialTabsInSafari()

tell me to activate
set question to display dialog "Close old Chrome tabs?" buttons {"Yes", "No"} default button 2
set answer to button returned of question
if answer is equal to "Yes" then
	closeAllChromeTabs()
end if

tell application "Safari" to activate


-- Open a *new* Safari window.
on openNewSafariWindow()
	tell application "Safari"
		make new document at end of documents
		set theSafariWindow to window 1
	end tell
	return theSafariWindow
end openNewSafariWindow



-- I have a set of sites for which I used pinned tabs in Chrome.
-- Note that because pinning behaves differently in Chrome and Safari, I don't always pin my pin-desired tabs when I move them to Safari.
on pinSpecialTabsInSafari()
	
	-- Note: You can't simply iterate windows and tabs because activating a window/tab re-orders the collection.
	--       You have to get a collection of IDs and iterate the IDs because they don't change.
	
	tell application "Safari" to set windowIds to (id of every window)
	tell application "Safari" to activate -- have to activate Safari because I'm automating clicks on its menus.
	
	repeat with wid in windowIds
		tell application "Safari"
			set theSafariWindow to (first window whose id = wid)
			set index of theSafariWindow to 1 -- bring THIS Safari window to top of Safari.
			
			tell theSafariWindow
				-- if you don't do this, sometimes Safari is the front app, with the desired window in the foreground, but it isn't actually activated.
				set visible to false
				set visible to true
			end tell
			
			repeat with t in every tab of theSafariWindow
				tell theSafariWindow to set current tab to t
				my waitForFrontTabToLoad()
				set theURL to URL of t
				if my shouldPinMe(theURL) then
					my pinActiveTab()
				end if
			end repeat
		end tell
	end repeat
	
	
end pinSpecialTabsInSafari


-- Create a new Safari tab in the specified window.
on makeSafariTab(theSafariWindow, theURL)
	
	if (theURL as string) = "chrome://newtab/" then
		-- skip empty tabs.  (You'd need to change chrome://newtab/ to about:blank in order to open it in Safari.)
	else
		tell application "Safari"
			try
				tell window 1 to set current tab to make new tab at end of tabs of theSafariWindow with properties {URL:theURL}
			on error
				open location theURL
			end try
			
			-- Close empty 'Favorites' tab created when making new window.
			set thisTab to tab 1 of theSafariWindow
			if thisTab's name as string = "Favorites" then close tab 1 of window 1
			
		end tell
	end if
end makeSafariTab


-- Wait for the front-most tab of Safari to finish loading (at least until it has a title).
on waitForFrontTabToLoad()
	repeat until leftString((name of front document of application "Safari"), 8) is not "Untitled"
		delay 0.1
	end repeat
end waitForFrontTabToLoad


-- I always pin certain tabs.
-- Note that because pinning behaves differently in Chrome and Safari, I don't always pin my pin-desired tabs when I move them to Safari.
on shouldPinMe(uri)
	
	tell application "Safari" to set wCount to count of windows
	if wCount > 1 then return false -- pinned tabs are stupid in Safari.  Pinning a tab in one window will add it to ALL safari windows
	
	if (uri as string) = "missing value" then error "no URI in shouldPinMe"
	if uri contains "//pinboard.in" then return true
	if uri contains "//focus.nirvanahq.com" then return true
	if uri contains "//www.nirvanahq.com" then return true
	if uri contains "//workflowy.com" then return true
	if uri contains "//voice.google.com" then return true
	if uri contains "//calendar.google.com" then return true
	if uri contains "//mail.google.com" then return true
	
	return false
end shouldPinMe


-- Pin the tab which is currently front-most in Safari.
on pinActiveTab()
	-- delay 1
	tell application "System Events"
		tell process "Safari"
			set frontmost to true
			click menu item "Pin Tab" of menu "Window" of menu bar 1
		end tell
	end tell
end pinActiveTab


-- Close every Safari tab.
on closeAllSafariTabs()
	tell application "Safari"
		set wCount to count of windows
		repeat with i from 1 to wCount
			set theSafariWindow to window 1
			repeat with theSafariTab in theSafariWindow's tabs
				tell theSafariWindow to close tab 1
			end repeat
		end repeat
	end tell
end closeAllSafariTabs


-- Close every Chrome tab.
on closeAllChromeTabs()
	tell application "Google Chrome"
		set windowList to every tab of every window
		repeat with tabList in windowList
			set tabList to tabList as any
			repeat with tabItr in tabList
				set tabItr to tabItr as any
				delete tabItr
			end repeat
		end repeat
	end tell
end closeAllChromeTabs


-- Return the first n characters of string s.
on leftString(s, n)
	if length of s is less than n then
		return s
	else
		return text 1 thru n of s
	end if
end leftString

Here is Safari-to-Chrome:

-- Move all tabs from Safari to Chrome

-- Optionally close existing Chrome tabs first
tell me to activate
set question to display dialog "Close exisisting Chrome tabs first?" buttons {"Yes", "No"} default button 1
set answer to button returned of question
if answer is equal to "Yes" then
	closeAllChromeTabs()
end if


tell application "Safari"
	repeat with theSafariWindow in windows
		tell application "Google Chrome" to set theChromeWindow to make new window
		repeat with theSafariTab in theSafariWindow's tabs
			set theURL to URL of theSafariTab
			tell application "Google Chrome"
				set theChromeTab to make new tab at end of tabs of theChromeWindow
				set URL of theChromeTab to theURL
				
			end tell
		end repeat
		-- Close empty tab created when making new window.
		tell application "Google Chrome" to close tab 1 of theChromeWindow
		
		
		tell application "Google Chrome" to set tabCount to number of tabs in theChromeWindow
		repeat with i from 1 to tabCount
			tell application "Google Chrome" to set active tab index of first window to i
			tell application "Google Chrome" to set theURL to URL of active tab of front window
			if my ShouldPinMe(theURL) then
				my pinActiveTab()
			end if
		end repeat
		
	end repeat
end tell


tell me to activate
set question to display dialog "Close exisisting Safari tabs?" buttons {"Yes", "No"} default button 2
set answer to button returned of question
if answer is equal to "Yes" then
	closeAllSafariTabs()
end if


-- I always pin certain tabs.
on ShouldPinMe(uri)
	if uri contains "//pinboard.in" then return true
	if uri contains "//focus.nirvanahq.com" then return true
	if uri contains "//workflowy.com" then return true
	if uri contains "//voice.google.com" then return true
	if uri contains "//calendar.google.com" then return true
	if uri contains "//mail.google.com" then return true
	return false
end ShouldPinMe

on pinActiveTab()
	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
end pinActiveTab


on closeAllChromeTabs()
	tell application "Google Chrome"
		set windowList to every tab of every window
		repeat with tabList in windowList
			set tabList to tabList as any
			repeat with tabItr in tabList
				set tabItr to tabItr as any
				delete tabItr
			end repeat
		end repeat
	end tell
end closeAllChromeTabs


on closeAllSafariTabs()
	tell application "Safari"
		set wCount to count of windows
		repeat with i from 1 to wCount
			set theSafariWindow to window 1
			repeat with theSafariTab in theSafariWindow's tabs
				tell theSafariWindow to close tab 1
			end repeat
		end repeat
	end tell
end closeAllSafariTabs

How to Enter Special Characters on a Mac Keyboard

My wife was chatting with someone who asserted that he could not enter a ‘#’ because he’s using a Mac. He later refined his argument to be because he’s using a British Mac and his shifted-3 is a ‘£’. Finally, he refined his argument to be because he couldn’t be bothered to put forth the effort.

Spoiler: On British/American keyboards, use Shift-3 or Alt-3 to get # or £.

This led me to wonder how one enters special characters on a Mac. I’m familiar with the system character chooser on Windows. How does Mac do this?

One-time setup:

  • Go to ‘System Preferences’ and open the Keyboard applet. Select the ‘Input Sources’ tab.
  • Using the ‘+’ button, add any missing items from the following list:
    • British
    • Dvorak (If you use it. I do.)
    • U.S.
    • Unicode Hex Input
  • If you have trouble locating any of the items above, use the Search field.
  • Be sure to put a checkmark in the “Show input menu in menu bar”. This will add an icon to you menu bar.
    • Be sure you know which one it is. You can toggle the checkmark off and on to check which icon.
  • This is optional, but spiffy:
    • Select any one of the keyboards which you’ve added with the ‘+’. Mac will display that layout to the right. Ho hum.
    • Press and hold the Shift key. Mac will show you the shifted form of the layout.
    • Press and hold the Alt key. Mac will show what happens if you enter the Alt form of that keystroke.
    • You can do this for any of the installed keyboard layouts.

Using alternate keyboard layouts

One approach:

  • Click the input menu in the menu bar. (Mine says “DV” because I use Dvorak. Yours is probably the flag for your country.)
  • Choose “Show Keyboard Viewer”. You’ll see a keyboard image.
  • Hold down Alt to see the alt form of each key.
  • If you see the one you want, press it!

Another approach: This one works for esoteric keystrokes.

  • Click the input menu in the menu bar. Choose “Show emoji and symbols”.
  • Browse through the menus to find the symbol you want and double-click it to ‘type’ that symbol. 😀

Setting Up Eclipse

Setting up Eclipse for Java, Python, and Clojure

  • Ensure you have Java 8 JDK (run “javac -version”)
  • Download Eclipse from https://www.eclipse.org/downloads/
    • Select “Download Packages” and install “Eclipse IDE for Java Developers”.
    • I got the “Oxygen 1a” version.
  • Launch Eclipse
    • I set my workspace to ~/Sync/Code and I made it my default location.
    • Help > Eclipse Marketplace > search for pydev. Install pydev (all features). Restart when asked.
    • Help > Eclipse Marketplace > search for StartExplorer. Install. It is unsigned. Restart when asked.
    • Help > Eclipse Marketplace > search for counterclockwise. Install. It is unsigned. Restart when asked.
  • Note: It takes about 8 seconds to launch before installing Counterclockwise. CC adds another 3-4. Slow for an editor, but OK for an IDE.
  • General Eclipse Configuration:
    • In the upper-right corner, to the right of “Quick Access”, you’ll see some icons. Right-click on any of them except for the first one. Choose “Show text”. These icons allow you to quickly switch Eclipse “perspectives.”
    • Install “Eclipse Moonrise UI Theme” via Help > Eclipse Marketplace.
    • Install Eclipse Color Theme plugin.
    • Preferences > General > Appearance. Select “Appearance” and set the theme to “Moonrise (standalone)”. There is a “Color Theme” in the hierarchical menu. Don’t use that one for this step. Choose Appearance and ON THAT TAB select the theme.
    • Preferences > General > Appearance > Color Theme. Select Color Theme from the hierarchical menu; not on the Appearance tab. Select “Vibrant Ink” color theme. You have to do both themes in order to get the ancilliary panes to be dark. Vibrant Ink only colors the editor.
    • I was getting black on black in the Package Explorer.
      • Preferences > General > Appearance > Colors and Fonts. Search for “Uncommitted Change (Foreground)”. I set it to FFADA8. This fixed some nodes in Package Explorer. Editing “Ignored Resources (Foreground)” got the others.
    • Still about 12 seconds to launch Eclipse…
  • Clojure Eclipse Configuration:
    • Preferences > Clojure > Editor >
      • Editor text autoshift
      • Highlight matching brackets
      • Displayed tab width = 2
      • On file launch, switch the Repl…
      • Start editors in strict/paredit mode
      • DO NOT: Escape text when pasting…
      • Tab reindents the current line
      • Auto activate code completion
      • Display namespace in tabs instead of file name
    • Preferences > Clojure > General. Turn OFF “Launch REPLs with cider-nrepl”. Otherwise, your REPL will just die for no reason, after being idle for a few minutes and at random times. It is a known problem.
    • Mac users - REPL command history is via Ctrl-UpArrow and Ctrl-DownArrow. These have Mac meanings, so redefine them to use Cmd instead of Ctrl:
      • Preferences > General > Keys. Search for “command from repl”.
  • Eclipse elements to be familiar with:
    • Java perspective
      • Package Explorer - Left panel. Browses the file system.
        • To launch a REPL without loading a file: Select the project in the Package Explorer; Run > Run Configurations > Clojure > (pick a configuration) > Run.
          • Source code - Main panel in the middle. IF a repl is running, you get fly-over syntax help. (Clojure > Load file in repl. There are other ways to launch a repl, but you don’t seem to get fly-over unless you load the file in the repl.)
          • Outline (a.k.a. namespace browser) - Right panel. Lists the functions in the currently selected source file. Use the sort icon at the top, to choose between show-in-alpha-sequence and show-in-source-file-sequence.
          • Bunch-of-views-at-bottom-of-page: There are several here in a single panel.
            • The ones I find useful:
              • Console - shows output from println.
              • REPL
            • The ones I closed:
              • Problems, Javadoc, Declaration

Things to do or look into:

  • Alt-L brings up a Leiningen menu. You can Launch Headless REPL. You can run ‘lein anything here’ in the current project directory.
  • You can generate a new Clojure project via File > New > Clojure Project, but it uses the ‘default’ project, which is a library. You probably want to drop to the command prompt and use lein new app app-name-here
  • When you generate a new Clojure project from Eclipse, it appears to always put clojure 1.6 in the generated project.clj. Find this line in project.clj and set it to the Clojure version you are using: :dependencies [[org.clojure/clojure "1.6.0"]]
  • Note: It takes almost 20 seconds to launch a Clojure repl in Eclipse. Can I speed that up?
  • Refactor this page, to move getting-started-with [Java/Python/Clojure]-in-eclipse to separate pages.
  • Look into Clojure JUnit integration via https://github.com/mikera/cljunit