Hammerspoon: one Swiss Army Knife on macOS

I may believe that most computers contain unique customizations, which may be why most people hate reinstalling the operating systems or setting up a new computer.

Such as my laptop, there are a bunch of tools that improve my experience using a computer. Here is a non-exhaustive list:

AppUsage
SizeUpCan easily attach the windows to the sides or corners of the screen. If the screen is big enough, you may put the documentation on the left and IDE on the right.
Pap.erIt can periodically and automatically set a beautiful sightseeing image as your wallpaper.
StretchlyTimely covers your screen to make you have a rest.
Lexico.comThe official site of the Oxford dictionary.
CheatsheetIt can show you the list of the selected application’s hotkeys.
New Terminal HereOpen a terminal with the current folder path.
One non-exhaustive list of my favorite tools

Besides improving my experience in using a computer, these tools also cause a few inconveniences, especially when I need to reinstall the OS and set up a new laptop. I have to look for an alternative because sometimes one is no longer free of charge, or sometimes one no longer supports the latest OS. This didn’t bother me much until I changed my Macbook Pro thrice in half a year. So I tried to find a way to minimize the number of tools I relied on, and I found Hammerspoon. I can say this is really a Swiss Army Knife on macOS.

Actually, there are some similar tools, but Hammerspoon finds the best balance between usage simplicity and functionality complexity. The users only need a little knowledge of Lua before customizing with Hammerspoon. Under the situation of no experience in Lua, I quickly finished the customizing based on the official documentation.

We can customize with the official spoons, such as windows manipulation.

-- We can quickly implement this with the WinWin spoon
hs.loadSpoon("WinWin")

-- Check whether WinWin is loaded and bind hotkeys to different directions.
if spoon.WinWin then
  -- Supporting symbol characters in the message of Hammerspoon is quite helpful. We will talk about this later. I will talk about this later.
  -- Side
  hs.hotkey.bind({"cmd", "alt", "ctrl"}, "left", "Window ⬅", function() spoon.WinWin:moveAndResize("halfleft") end) 
  hs.hotkey.bind({"cmd", "alt", "ctrl"}, "right", "Window ➡", function() spoon.WinWin:moveAndResize("halfright") end) 
  hs.hotkey.bind({"cmd", "alt", "ctrl"}, "up", "Window ⬆", function() spoon.WinWin:moveAndResize("halfup") end) 
  hs.hotkey.bind({"cmd", "alt", "ctrl"}, "down", "Window ⬇", function() spoon.WinWin:moveAndResize("halfdown") end) 
  -- Corner
  hs.hotkey.bind({"shift", "alt", "ctrl"}, "left", "Window ↖", function() spoon.WinWin:moveAndResize("cornerNW") end) 
  hs.hotkey.bind({"shift", "alt", "ctrl"}, "right", "Window ↘", function() spoon.WinWin:moveAndResize("cornerSE") end) 
  hs.hotkey.bind({"shift", "alt", "ctrl"}, "up", "Window ↗", function() spoon.WinWin:moveAndResize("cornerNE") end) 
  hs.hotkey.bind({"shift", "alt", "ctrl"}, "down", "Window ↙", function() spoon.WinWin:moveAndResize("cornerSW") end) 
  -- Stretch
  hs.hotkey.bind({"cmd", "alt", "ctrl"}, "C", "Window Center", function() spoon.WinWin:moveAndResize("center") end) 
  hs.hotkey.bind({"cmd", "alt", "ctrl"}, "M", "Window ↕↔", function() spoon.WinWin:moveAndResize("maximize") end) 
  -- Screen
  hs.hotkey.bind({"alt", "ctrl"}, "right", "Window ➡ 🖥", function() spoon.WinWin:moveToScreen("next") end) 
  -- Other
  hs.hotkey.bind({"cmd", "alt", "ctrl"}, "/", "Window Undo", function() spoon.WinWin:undo() end) 
endCode language: PHP (php)

It is pretty easy, right? Only several hotkey bindings to functionality are enough.

We can also create spoons with the API, such as BreakTime.

To implement this, we need to periodically use a page or image to cover the whole screen to make the guys in front of the PC take a rest.

Firstly, we can quickly start a timer with hs.timer:

obj.Timer = hs.timer.new(60, refresh)
obj.Timer:start()Code language: PHP (php)

Then, we can create a page covering the whole screen with hs.webview. Using a transparent image to make the page translucent is also not bad.

function makeBrowserOfBreakTime ()
  local screen = require"hs.screen"
  local webview = require"hs.webview"

  local mainScreenFrame = screen:primaryScreen():frame()
  browserFrame = {
     x = mainScreenFrame.x,
     y = mainScreenFrame.y,
     h = mainScreenFrame.h,
     w = mainScreenFrame.w
  }

  local options = {
      developerExtrasEnabled = true,
  }

  -- local browser = webview.new(browserFrame, options):windowStyle(1+2+4+8)
  local browser = webview.new(browserFrame, options):windowStyle(1+2+128)
    :closeOnEscape(true)
    :deleteOnClose(true)
    :bringToFront(true)
    :allowTextEntry(true)
    :transparent(true)

  return browser
endCode language: PHP (php)

Next, we can show and remove the cover with browser:show() and browser:delete() periodically with the timer.

function refresh()

  obj.curTime = obj.curTime + 1
  if obj.curTime > obj.microbreakInterval then
  
    obj.curMicrobreakCount = obj.curMicrobreakCount + 1
    if obj.curMicrobreakCount > obj.microbreakCount then
      hs.alert.show(obj.breakTime .. " minute break starts")

      local browser = makeBrowserOfBreakTime();
      browser:url("file://" .. hs.spoons.scriptPath() .. "BreakTime.html?time=" .. (obj.breakTime * 60 - 1)):show()
      hs.timer.doAfter(obj.breakTime * 60, function()
        if browser ~= nil then 
          browser:delete(); 
        end 
      end)

      obj.curMicrobreakCount = 0
    else
      hs.alert.show(obj.microbreakTime .. " second microbreak starts")

      local browser = makeBrowserOfBreakTime();
      browser:url("file://" .. hs.spoons.scriptPath() .. "BreakTime.html?time=" .. (obj.microbreakTime - 1)):show()
      hs.timer.doAfter(obj.microbreakTime, function() 
        if browser ~= nil then 
          browser:delete(); 
        end 
      end)
    end

    obj.curTime = obj.curTime - obj.microbreakInterval

  end
endCode language: JavaScript (javascript)

Load the BreakTime spoon in the same way to load the WinWin spoon, and start it. Done!

We can also customize the taskbar menu to improve the user experience.

Take BreakTime as an example. We can show the timeline of the next break.

BreakTime's Menu
BreakTime’s Menu

Besides this, we can also show all the hotkeys defined with Hammerspoon.

Menu showing all the hotkeys
Menu showing all the hotkeys

Hammerspoon supports symbol characters quite well, so users can make the menu items more attractive. Isn’t this amazing? 😄

obj.menubar:setTitle("⌨️")
obj.menubar:setTooltip("Hot Key Info")

local hotkeys = hs.hotkey.getHotkeys()
local menuItem = {}
  
for key, value in pairs(hotkeys) do  
  local item = { title = value["msg"] }
  table.insert(menuItem, item)
end 

obj.menubar:setMenu(menuItem)Code language: JavaScript (javascript)

Besides all the above, Hammerspoon still has much that needs going through. I am also keeping going through to make my customization more additional. Here’s mine.

BTW, not only reinstalling these tools, but I also need to reinstall all of the common applications such as VS Code, Plex, Sublime Text, VIM, etc. This is also terrible. Now I am maintaining a tap list containing the applications with homebrew. 😂

ImgCache 0.2.1 Released

I promised 12 years ago that I would enable the HTTPS support in the next release. And now, I have finally released a new version of ImgCache. I also made fun of myself for this in my first blog post. Now I have made my word and implemented this in this version. Better late than never 😂

Released ImgCache 0.2.1 after 12 years
Released ImgCache 0.2.1 after 12 years

Besides enabling the HTTPS support, I upgraded the Snoopy class, updated the code format and the README file, and fixed two interesting bugs.

The first one is about the suffix extraction of the cache images. When I coded for the HTTPS support functionality, I passed the test with PNG images. But when I tried with SVG images, the cached images didn’t show on the pages because the suffix of the cached image file was not .svg but .svg+xml.

To be honest, I could hardly remember how I confirmed the suffix in this 12-year-old ancient code. So I went through the code and found the suffix was defined based on the Content-Type inside the HTTP header fields when accessing the image’s link. The HTTP header will contain a field named Content-Type like this Content-Type: image/png when we access the image’s URL. This field helps clients confirm the MIME type of the response, so we can use the former part before / to verify whether this is an image and use the later part after / to get the suffix. Because I was the one who mostly used this plugin, I only tested with several typical image types and released it when I passed all the cases

But the Content-Type of SVG is image/svg+xml not image/svg. The cached image doesn’t show on the pages because we set .svn+xml as the suffix of the cached image file.

It says on Wikipedia that SVG is an XML file containing all the vector graphic information. That makes sense.

Scalable Vector Graphics (SVG) is an XML-based vector image format for two-dimensional graphics with support for interactivity and animation. The SVG specification is an open standard developed by the World Wide Web Consortium (W3C) since 1999.

SVG images are defined in a vector graphics format and stored in XML text files. SVG images can thus be scaled in size without loss of quality, and SVG files can be searched, indexed, scripted, and compressed. The XML text files can be created and edited with text editors or vector graphics editors, and are rendered by the most-used web browsers.

https://en.wikipedia.org/wiki/Scalable_Vector_Graphics

Therefore, I must split the Content-Type’s value to confirm the suffix. Actually, SVG is not the only one, and I will handle the other types in the following release.

The second bug was due to the IMG tag generated by the WordPress’ new editor. The image tags generated by the WordPress’ new editor are not closed. And there is no space between the last attribute and the tag end as below.

<meta http-equiv="content-type" content="text/html; charset=utf-8"><img src="http://nginx.org/nginx.png">
Code language: HTML, XML (xml)

At that moment, in my opinion, all the HTML tags should be closed, and there must be a space between the attribute and the end of the tag as below.

<meta http-equiv="content-type" content="text/html; charset=utf-8"><img src="http://nginx.org/nginx.png" />
<img src="http://nginx.org/nginx.png" ></img>
Code language: HTML, XML (xml)

Based on this understanding, the REGEX for URL extraction in the previous version was not robust enough to handle this. So the URL extracted was invalid, and the plugin would stop caching this image. 😅

So much for the stories about this version. Users may be able to set customized expiration times of cached images in the next version. I used this plugin to show the Feedburner subscribers’ count images. Because the count kept changing, I set the expiration time as one hour, which is 3600 seconds in the code. Now the usage changes, a customized expiration time may make sense.

Alright! Let’s see the next release.

Blog Reopens

The blog finally reopened!

You may be aware that this is not the first time I have created a blog based on the title, and I think this is good timing for me to recall the history of my blog. Since it happened a long time ago, the timings mentioned may shift a little from reality. Here is the evolution of my blog:

Sina Blog –> Live Space –> Wordpress
Blog Service Blog Service Self-built with WordPress

The blog started to become popular in China when I was a sophomore, so most of the famous web portals released the blog services. I also registered for one on the Sina Blog service to catch the trend. But I didn’t manage it well, and I just regarded it as a notebook.

After a short while, I found that Sina Blog services set up too many limitations. As a CS student, I couldn’t stand that. Therefore I decided to transfer my blog to Live Space which Microsoft just released. There were not many posts, so I manually moved them. At that moment, I began to post more about myself on my blog and regarded it as a place where I could share my emotions other than as a notebook.

When I was a second-year graduate student, self-built blogs on VPC became the trend. Based on the recommendation from Solrex, one of my classmates, I registered a domain name and began to continue with my blog by using WordPress on Hostmonster VPC provided by Jun Gu. I choose iron-feet.cn as the domain name of my blog. I never expected that selecting a CN domain name would be really a huge failure, and I will share with you why later.

At that moment, there were lots of posts. Manually transferring them one by one was really a mission impossible for me, such a lazy guy, not to mention that I need to retain all the tags, updated times, etc. I implemented a GUI tool to move the whole blog from Live Space to WordPress to finish the transfer rapidly. I didn’t open-source this tool due to a lack of sense of open source then. And I lost the code when I switched to a new laptop once. What a pity, If I open-sourced this, this tool could help others a lot when Microsoft shut down Live Space.

Since I used WordPress, my blog changed a lot in my mind. I started to contribute a lot to manage it. I made sure that I posted once every week and quoted others’ excellent parts indicating the source instead of directly quoting the whole post. And most of the posts should be technique-related.

In comparison to other CMSs, WordPress was quite good. However it still had many issues since the version I was using was quite old, such as encoding issues, dirty DB data caused by version upgrading, backend hung due to plugin upgrading, code reverted caused by upgrading, etc. If I detailedly introduced them one by one, I could publish several more posts. 😄

RSS was once quite a popular way of subscribing to blogs, so many bloggers liked to use RSS subscriber count to show the quality and popularity of their blogs. I was also one of them. But the build-in statics of WordPress is totally a mess, so we preferred to use Feedburner to burn a feed. And Feedburner could also fix some XML syntax issues in the original RSS.

The bloggers also show the Feedburner subscriber count as below on the page.

Feedburner Subscriber Count example shown on blog
Feedburner Subscriber Count

it is pretty hard for the bloggers whose audiences are mainly in China to show this fantastic counter. This counter is inaccessible from China because Feedburner is under the Google domain, which is google.com.

To resolve this, I had to implement a plugin named ImgCache. Suppose the attribute “ref=imgcache4wordpress” is added to an <img> tag. In that case, ImgCache will automatically persist the image into the local disk and replace the link with the local path so that this image becomes accessible from China. I used this way to show images provided by Feedburner and Twitter.

The latest updated time was 12 years ago. These days I went through this plugin and really could hardly bear my poor English and coding. It shows I will resolve the known issue in the next release. So it seems nothing needs to be fixed if no newer version is released 😅. Maybe I should take some time to release a new version to fix that.

Besides ImaCache, I also implemented another plugin called Custom URL Shorter. I knew shorter should be shortener, ignore please 🤦‍♂️. The plugin name could not be updated, and it is not sure whether it can be updated now. A single slip may cause lasting sorrow. (Developers can update the plugin name now, and I have updated the name to Custom URL Shortener.)

My blog meant a lot to me, but I made a tough decision three years after I graduated from the graduate university: shut down my blog.

I kept being annoyed by these:

  1. The blacklist way of GFW: Once a site under IP outside China contained any sensitive data, that IP would be blacklisted. That was to say, all the sites using this IP would be banned from China. So I had to ask the VPS provider to change the IP repeatedly. No clue that the situation could be better.
    I was not sure whether that was due to GFW. Accessing my blog from China became unstable, but accessing from outside China is quite good.
  2. The complicated registration: the instruction for registration kept changing. The MIIT always asked for new files and documents, asked to renew the information, and asked to add further information on the pages.
    Every time one Email would be received with a very close deadline. If you could not meet the requirement in time, your site would be shut down immediately
  3. Cyber attacks: After joining an Internet company, I became swamped, so I always forgot to upgrade WordPress to apply the security patch. Therefore, my blog was attacked several times. Although I had backups, restoring still took much time.

OT in the daytime, dealing with the none technique stuff such as GFW, registration, etc., in the evening really made me tired. Finally, I gave up and chose to shut down. Shutting down my blog has always been in my mind, and it isn’t easy to erase.

Actually, I already planned to reopen the blog when I arrived in Singapore. But first time joining a foreign company delayed this plan. The arrival of my wife and baby kid delayed it again. I never thought that taking care of the baby without the support of elders would be so difficult. Finally, I have decided to reopen the blog now. Cause I have not touched this for a long time, I don’t know much about the VPS providers. Thank Xintao Lai, for recommending DigitalOcean to me.