Hammerspoon で童貞を殺すセーターのお姉さんを表示

2021年2月6日

Hammerspoonで童貞を殺すセーターのお姉さんを表示

先日、Hammerspoon で今いる場所の緯度と経度を求め、現在地の天気と気温をデスクトップに表示する、というようなことを試した。

🔗 Mac でロケーションごとの気温を表示する | 林檎コンピュータ

その際、デスクトップの表示は GeekTool を用いたが、よくよく考えると Hammerspoon でも同じようなことが出来るはずである。
 さっそく試してみた。

デスクトップに文字列を表示

デスクトップに文字列を表示する場合、かつては hs.drawing というモジュールが使われていたようだ。現在は廃止が視野に入っているらしく、非推奨になっている。
 代わりに hs.canvas というのを使えばいいみたいだ。

local c = require("hs.canvas")
local a = c.new{ x = 100, y = 100, h = 200, w = 400 }:show()
a[1] = { type = "text", text = '文字列' }
a:sendToBack()

書き方はいろいろあるようだ。筆者はこれが一番わかりやすかった。
 筆者の印象であり、正確な表現ではないと思う。思うのだが印象として、hs.canvas というのは lua のテーブルで、そのテーブルの中に、エレメントと呼ばれるテーブルを入れる。エレメントで表示したい文字列だとか、画像だとかを指定する感じ。ひとつのエレメントがレイヤーみたいになるっぽい。
 エレメントのテーブル内の定義は、アトリビュートとか呼ばれており、最低でも「type」を定義する必要がある、とのこと。

新規 hs.canvas オブジェクト「 a 」で、x,y の座標と h,w の縦横の大きさを指定している。最後の:show()は表示を命ずるメソッドだ。

a[1] = { type = "text", text = '文字列' }でもって、表示したい項目を定義したことになる。表示したいのはテキスト、テキストの内容は「文字列」でお願い、という意味だ。
 a:sendToBack()で、canvas をデスクトップピクチャより上、アイコンよりは下、という深い階層まで送れる。これは GeekTool と同じ深さだと思われる。
 デフォルトだと、ウィンドウや Dock より上の高い階層に設定されており、隠せなくなる。

表示された状態

ちょっと見づらいので、文字色を黒にしてみる。文字色はtextColor = { black = 1.0 }というように指定する。

a[1] = { type = "text", text = '文字列', textColor = { black = 1.0 } }
文字色を黒く

あまり見易くなってないので、いろいろ設定してみる。

a[1] = { type = "text", text = '文字列', textColor = { black = 1.0 }, textFont = "Keifont", textSize = 36.0, textAlignment = "center" }
フォントを変える

フォントはtextFont = "フォント名"、サイズはtextSize = 数値、文字列の揃え方は、textAlignment = "left,right,centerなど"となっている。
 これらの設定項目は、Hammerspoon docs: hs.canvas に掲載されている。
 細かい字で英語がずらっと出てくるが、なんとなくの理解で気長に試していくのがいいと思う。

GeekTool みたいに時計を表示したいなら、以下のようなスクリプトが考えられる。

local c = require("hs.canvas")
local a = c.new{ x = 100, y = 100, h = 200, w = 400 }:show()
a[1] = { type = "text", text = '文字列', textColor = { black = 1.0 }, textFont = "Keifont", textSize = 36.0, textAlignment = "center" }
a:sendToBack()
function tokei()
    local now = hs.execute("date +%H:%M:%S")
    a[1].text = now
end
dtTokei = hs.timer.new(1, tokei)
dtTokei:start()

 hs.canvas オブジェクト「 a 」はテーブルなので、下から四行目のa[1].textみたいに書くと、text キーの値を変更できるのだ。
 今回はhs.executeでシェルのコマンドdateを実行して時間を取得し、a[1]の text に代入している。

デジタル時計を表示

画像の表示

画像も表示してみたい。
 以下のような画像を作った。

時計の背景

画像はパス等で指定して、hs.image でオブジェクト化する。
 local img = hs.image.imageFromPath(os.getenv("HOME").."/Desktop/tokei.png")
 hs.canvas オブジェクト「 a 」に新たなテーブルを追加する。
 a[2] = { type = "image", image = img }
 imgというのは先ほど作った hs.image である。

local c = require("hs.canvas")
local a = c.new{ x = 100, y = 100, h = 210, w = 410 }:show()
local img = hs.image.imageFromPath(os.getenv("HOME").."/Desktop/tokei.png")
a[1] = { type = "text", text = '文字列', textColor = { white = 1.0 }, textFont = "Keifont", textSize = 36.0, textAlignment = "center" }
a[2] = { type = "image", image = img }
a:sendToBack()
function tokei()
    local now = hs.execute("date +%H:%M:%S")
    a[1].text = now
end
dtTokei = hs.timer.new(1, tokei)
dtTokei:start()

reload config して実行すると、時計の数字が画像の下に隠れてしまっている。
 エレメントはテーブルの番号順に生成されるみたいだ。

失敗した例

これを修正し、さらに時計の位置や大きさを微調整した。

local c = require("hs.canvas")
local a = c.new{ x = 100, y = 100, h = 105, w = 205 }:show()
local img = hs.image.imageFromPath(os.getenv("HOME").."/Desktop/tokei.png")
a[1] = { type = "image", image = img }
a[2] = { type = "text", text = '文字列', textColor = { white = 1.0 }, textFont = "Keifont", textSize = 24.0, textAlignment = "center", frame = { x = "0", y = "0.3", h = "1", w = "1" } }
a:sendToBack()
function tokei()
    local now = hs.execute("date +%H:%M:%S")
    a[2].text = now
end
dtTokei = hs.timer.new(1, tokei)
dtTokei:start()
微調整した状態

いまいち微調整しきれてない感もあるが、まぁこんな感じだろう。
 時計の数字の位置を決めるframeという定義は、x,y,h,w すべてパーセントで指定する。1 なら 100 パーセント。0.5 なら 50 パーセントということになる。
 また、この程度の枠なら、画像を用意しなくても、Hammerspoon で描画できると思う。
 矩形や円を描く定義がどっさりあるのだ。
 下記のリンクに例文がある。

🔗 hs.canvas.examples · asmagill/hammerspoon Wiki · GitHub

ここまでやって思うのは「GeekToolのほうが楽なんじゃねぇかな」ということだ。
 しかし Hammerspoon だからこそ出来ることもあるはずだ。

童貞を殺すセーターのお姉さんを表示する

Hammerspoon なら、キー操作と hs.canvas の表示を組み合わせることも可能なはずだ。んで、いろいろ考えてみた。
 こう、ちょっとえっちな感じなお姉さんが、ちょっとした情報を教えてくれる、そんなスクリプトを構想してみた。
「いま何時」だとか、「きょう何曜日」だとか。
 そういうのを教えてくれるわけだ。
 使うのは hs.hotkey.modal というモジュールだ。
 とあるキーの組み合わせで「お姉さんモード」に入り、「お姉さんモード」の時にいろんなキーを押すことで、ちょっとした情報を返す。
 ここでは著作権の問題もあって自分でお姉さんの絵を描くが、ちゃんとした絵師の絵を個人利用の範囲で使用させてもらえば、そこそこ良くなるのではなかろうか。

教えてよ、お姉さん、と。ぼく、知りたいよ、もっともっと教えてよ、と。
 そんな思いで作ったのが以下のスクリプトだ。

-- oneesanMode
oneesan = nil
oneesanMode = hs.hotkey.modal.new('cmd-alt-ctrl', 'up')
function oneesanMode:entered()
    if oneesan then
        oneesan:show(0.5)
        oneesan:bringToFront()
    else
        local img = hs.image.imageFromPath(os.getenv("HOME").."/.hammerspoon/oneesan_1024.png")
        
        local c = require("hs.canvas")
        oneesan = c.new{ x = 1040, y = 525, h = 400, w = 400 }:show()
        oneesan[1] = { type = "image", image = img }
        oneesan[2] = { type = "text", text = '', frame = { x = "0.58", y = "0.34", h = "0.2", w = "0.4" }, textColor = { black = 1.0, alpha = 0.0 }, textFont = "07YasashisaGothic", textSize = 27.0, textAlignment = "center" }
        oneesan[3] = { type = "text", text = '時刻は', frame = { x = "0.58", y = "0.26", h = "0.2", w = "0.4" }, textColor = { black = 1.0 }, textFont = "07YasashisaGothic", textSize = 20.0, textAlignment = "center" }
        oneesan[4] = { type = "text", text = '', frame = { x = "0.58", y = "0.34", h = "0.2", w = "0.4" }, textColor = { black = 1.0, alpha = 1.0 }, textFont = "Keifont", textSize = 24.0, textAlignment = "center" }
        
        function tokei()
            local now = hs.execute("date +%H:%M:%S")
            oneesan[4].text = now
        end
        dtTokei = hs.timer.new(1, tokei)
        dtTokei:start()
    end
end
oneesanFlag = nil
local function oneesanClock()
    oneesan[2].textColor.alpha = 0.0
    oneesan[4].textColor.alpha = 1.0
    oneesan[3].text = '時刻は'
end
function oneesanMode:exited()
    if not oneesanFlag then
        oneesan:hide(0.5)
    end
    if oneesanFlag == 'back' then
        oneesan:sendToBack()
        oneesanFlag = nil
        oneesanClock()
    elseif oneesanFlag == 'front' then
        oneesanClock()
        oneesanFlag = nil
    end
end

function oneesanMoji(size)
    oneesan[2].textSize = size
    oneesan[2].textColor.alpha = 1.0
    oneesan[4].textColor.alpha = 0.0
end

oneesanMode:bind('', 'escape', function()
    oneesanMode:exit()
end)
oneesanMode:bind('', 'q', function()
    oneesanMode:exit()
end)
oneesanMode:bind('', 'space', function()
    oneesanMode:exit()
end)
oneesanMode:bind('cmd-alt-ctrl', 'up', function()
    oneesanMode:exit()
end)
oneesanMode:bind('', 'f', function()
    oneesanFlag = 'front'
    oneesanMode:exit()
end)
oneesanMode:bind('', 'b', function()
    oneesanFlag = 'back'
    oneesanMode:exit()
end)

oneesanMode:bind('', 'k', function()
    oneesanMoji(30.0)
    local kion = hs.execute("cat ~/Documents/weather/kion.txt")
    oneesan[2].text = kion
    oneesan[3].text = '気温は'
end)
oneesanMode:bind('', 't', function()
    oneesanMoji(27.0)
    local tenki = hs.execute("cat ~/Documents/weather/tenki.txt")
    oneesan[2].text = tenki
    oneesan[3].text = '天気は'
end)
oneesanMode:bind('', 'w', function()
    local ssid = hs.wifi.currentNetwork()
    oneesanMoji(18.0)
    oneesan[2].text = ssid
    oneesan[3].text = 'wi-fiは'
end)
oneesanMode:bind('', 'm', function()
    oneesanMoji(27.0)
    local method = hs.keycodes.currentMethod()
    if not method then
        method = hs.keycodes.currentLayout()
    end
    oneesan[2].text = method
    oneesan[3].text = '入力は'
end)
oneesanMode:bind('', 'j', function()
    oneesanClock()
end)
oneesanMode:bind('', 'y', function()
    oneesanMoji(27.0)
    local week = hs.execute("date +%w")
    week = tonumber(week) + 1
    local yobi = {'日曜日', '月曜日', '火曜日', '水曜日', '木曜日', '金曜日', '土曜日'}
    oneesan[2].text = yobi[week]
    oneesan[3].text = 'きょうは'
end)
oneesanMode:bind('', 'i', function()
    oneesanMoji(18.0)
    local ip = hs.execute("networksetup -getinfo Wi-Fi | grep '^IP address:' | awk '{print $3}'")
    oneesan[2].text = ip
    oneesan[3].text = 'ipは'
end)
oneesanMode:bind('', 'g', function()
    oneesanMoji(18.0)
    local global = hs.execute("curl inet-ip.info")
    oneesan[2].text = global
    oneesan[3].text = 'ipは'
end)
oneesanMode:bind('', 'n', function()
    oneesanMoji(30.0)
    local day = hs.execute("date +%m月%d日")
    oneesan[2].text = day
    oneesan[3].text = '今日は'
end)

command + option + control + でお姉さんモードに入る。
 同じホットキーでお姉さんモードから抜ける。
 escapeQキーを押してもお姉さんモードから抜けることにした。

お姉さんモードでJキーを押すと時間を表示する。
 Yキーで曜日を表示する。

Tキーで天気、Kキーで気温を表示する。これは、「Mac でロケーションごとの気温を表示する | 林檎コンピュータ」で使用したシェルスクリプトを利用している。
 上記リンク先のシェルスクリプトの最後に、

echo $tenki > $HOME/Documents/weather/tenki.txt
echo $kion > $HOME/Documents/weather/kion.txt

などと書きこんでテキストを吐き出し、それを表示している。

Wキーで wi-fi の ssid を表示し、Mキーで入力ソースを表示することにした。

追記 21.02.05
さらにIキーでローカルな IP アドレスを、Gキーでグローバルな IP アドレスを表示させてみた。
 また、Bキーでお姉さんをウィンドウの背後に送ってお姉さんモードを終了、Fキーでお姉さんを前面に残しながらモードを終了することにした。これらの場合は、表示は時計になる。
 このように長くなったスクリプトは、別ファイルにコピペして適当な名前をつけて保存し、init.lua に以下のように書くと外部化できる。

-- ~/.hammerspoon/douteiSlayer.lua の場合
require('douteiSlayer')

追記はここまで。

出来上がったのがこれ。

童貞を殺せなかったgif

いやー、微妙っすなぁ。
 これだったら素直にテキストだけ表示したほうがマシか。
 というか、ssid とか入力ソースって、いうほど表示したいか、という話もあるな。

筆者が、この男子中学生が作るようなスクリプトでなにをいいたかったかというと、
 oneesan[2].text = '文字列'
 などとやれば、表示を容易に変更することができる、ということなのである。
 これは GeekTool にはない強みであり、インタラクティブなスクリプトが Hammerspoon なら実現できる、ということなのだ。

このお姉さんにファイルをドラッグ&ドロップして、なにかの処理をさせる、みたいなことが出来るかもしれない。document にそれっぽいことが書いてあって、ちょっと試したけどよくわからなかった。
 仮にそれが出来るなら、別の展開もあるかもしれませんね。