COMPANY SERVICE STAFF BLOG NEWS CONTACT

STAFF BLOG

スタッフブログ

TECHNICAL

テクログ

2023.05.09

Macの画面分割を「Hammerspoon」で自分に最適化させる

テクログmac

なぜ他の画面分割アプリではないのか?

Mac標準の画面分割といったら Split View ですが、ぽちぽちするのが面倒すぎますよね。

(Windowsは標準でショートカットを使用できますよね…)

なので専用のアプリを追加してショートカットで画面分割している人も多いと思います。

「Shiftit」「Magnet」「Rectangle」あたりが Macにおいては代表格でしょうか?

私もお仕事Macでは無料で使用できる「Shiftit」を愛用していましたが、macOSをMonterayにアップデート以降「Shiftit」の動作が不安定となってしまいました…

調べてみたら同じような人がTwitter上にいて「Hammerspoon」がおすすめされていたので試してみたところ、これがしっくりきました!

詳しくは後述しますが「Hammerspoon」は画面分割の専用アプリではなく、macOSの操作をLuaというスクリプト言語で行うためのツールです。

自分でスクリプトを書く必要がある分細かい制御が可能で、それ故専用のアプリ以上の使い勝手にもなり得るわけです。

「Hammerspoon」ってなに?

https://www.hammerspoon.org

↑詳しく知りたい方は公式のドキュメントを読んでいただきたいのですが、macOSの操作をLuaというスクリプト言語を用いて行えるアプリです。

なのでプログラミングがある程度できる人向けのツールです。

(後述のサンプルコードをコピペすれば画面分割アプリのように使用可能なので、プログラミング興味ないという方はコピペしていただければ🙏)

ドキュメントは英語のみですが、Macを操作するためのAPIが豊富でドキュメントも親切なので自分がやりたいと思ったことを色々と実現できます!

導入方法はドキュメントに記載してありますのでそちらをご参照ください。

今回のスクリプト実装方針

ウィンドウを左半分、右半分ぴったりになるように整列させるスクリプトは公式ドキュメントの Getting Started でも紹介されていますし、その他さまざまなブログで紹介されています。この機能だけで十分!という方はそちらをコピペすれOKです。

ただしせっかくなので今回の記事では今まで画面分割アプリで私が不満に思っていたことを解決することにします。

マルチディスプレイを使用していて便利な機能に、ショートカットでウィンドウを隣のディスプレイに移動するという機能があります。

ただ、3枚以上のディスプレイを配置していると「次のディスプレイに移動」「前のディスプレイに移動」だとわかりにくいです。

↑特にこういった配置で使用している場合どちらが次なのか直感的にわかりにくい…

また覚えるべきショートカットも増えるので慣れるまでに時間もかかります。

そこで今回は以下のようなショートカットでディスプレイ間のウィンドウ移動を行えるようにします。

1.「crtl + option + 矢印キー」でウィンドウサイズを画面の50%にして矢印キーの方向に移動

2. さらに「crtl +option + 1と同じ矢印キー」で矢印キーの方向のディスプレイにウィンドウを移動

図にするとこんな感じ

いざ実装!

以下のスクリプトの注意事項

私の環境だとOSがventura だと意図した通りに nextScreenを取得できますが、monterey だと一部ずれが発生します。(原因がはっきりしないのですが、致命的ではないので私はそのまま使用しています。)

それぞれのディスプレイが異なる解像度という前提での実装になっています。同一解像度のディスプレイにてマルチディスプレイ環境を構築している場合は一部修正が必要かもしれません。

hs.window.animationDuration = 0

units = {
  left50        = { x = 0.00, y = 0.00, w = 0.50, h = 1.00 },
  right50       = { x = 0.50, y = 0.00, w = 0.50, h = 1.00 },
  top50         = { x = 0.00, y = 0.00, w = 1.00, h = 0.50 },
  bot50         = { x = 0.00, y = 0.50, w = 1.00, h = 0.50 },
}

windowResizeOrPush = {
  previousRect = {
    -- windowリサイズ後、windowのrectサイズを代入する
    -- geometryインスタンスを引数なしで生成できなかったので初期値として意味はないけど引数を入れている
    up    = hs.geometry.rect(units.top50),
    down  = hs.geometry.rect(units.bot50),
    left  = hs.geometry.rect(units.left50),
    right = hs.geometry.rect(units.right50),
  },
  units = {
    up    = units.top50,
    down  = units.bot50,
    left  = units.left50,
    right = units.right50,
  },
}

function windowResizeOrPush:getNextScreen(window, cursor)
  local nextScreen = nil
  if cursor == 'up' then
    nextScreen = window:screen():toNorth()
  elseif cursor == 'down' then
    nextScreen = window:screen():toSouth()
  elseif cursor == 'left' then
    nextScreen = window:screen():toWest()
  elseif cursor == 'right' then
    nextScreen = window:screen():toEast()
  end
  return nextScreen
end

-- ウィンドウサイズをスクリーンの指定箇所50%にリサイズする
-- 既にリサイズ済みで表示中スクリーンの隣にスクリーンが存在する場合、そのスクリーンに移動する
function windowResizeOrPush:exec(cursor)
  return function()
    local window = hs.window.focusedWindow()
    local nextScreen = self:getNextScreen(window, cursor)

    if (window:frame():equals(self.previousRect[cursor]) and nextScreen ~= nil) then
      window:moveToScreen(nextScreen, false, true) -- リサイズあり、ウィンドウに収まるように上部のディスプレイに移動
    else
      window:moveToUnit(self.units[cursor])
      self.previousRect[cursor] = window:frame()
    end
  end
end

-- windowリサイズ、移動系キーバインド
mash = { 'ctrl', 'alt' }
hs.hotkey.bind(mash, 'left',   windowResizeOrPush:exec('left'))
hs.hotkey.bind(mash, 'right',  windowResizeOrPush:exec('right'))
hs.hotkey.bind(mash, 'up',     windowResizeOrPush:exec('up'))
hs.hotkey.bind(mash, 'down',   windowResizeOrPush:exec('down'))
hs.hotkey.bind(mash, 'return', function() hs.window.focusedWindow():maximize() end)

いきなり完成形です。

細かいことはいいやという方は上記のコードを init.lua にコピペし、Hammerspoonメニューバー内の「Reload Config」を実行すればショートカットによるウィンドウの移動ができるはずです。

詳しい解説が読みたいという方は読み進めていただければと思います。

スクリプトの解説

hs.window.animationDuration = 0

こちらはウィンドウが移動する際のアニメーションの速さを設定しています。

0にするとアニメーションなしです。


units = {
  left50        = { x = 0.00, y = 0.00, w = 0.50, h = 1.00 },
  right50       = { x = 0.50, y = 0.00, w = 0.50, h = 1.00 },
  top50         = { x = 0.00, y = 0.00, w = 1.00, h = 0.50 },
  bot50         = { x = 0.00, y = 0.50, w = 1.00, h = 0.50 },
}

units内に左半分、右半分、上半分、下半分それぞれのウィンドウジオメトリーの値を格納しています。

“w”がウィンドウの幅、”h”がウィンドウの高さ

“x”, “y”がそれぞれウィンドウの左上を起点とした座標を表しています。

比率でウィンドウサイズを指定しているので 16:9, 21:9 など比率の異なるディスプレイでマルチディスプレイ環境を構築している場合でもウィンドウサイズを画面の半分に合わせることができます。

(left50, right50 については hs.layout にてHammerspoonが用意した値が既にあるので、上下のウィンドウ移動が必要ない場合はそちらを使用した方がコードがすっきりします)


windowResizeOrPush = {
  previousRect = {
    -- windowリサイズ後、windowのrectサイズを代入する
    -- geometryインスタンスを引数なしで生成できなかったので初期値として意味はないけど引数を入れている
    up    = hs.geometry.rect(units.top50),
    down  = hs.geometry.rect(units.bot50),
    left  = hs.geometry.rect(units.left50),
    right = hs.geometry.rect(units.right50),
  },
  units = {
    up    = units.top50,
    down  = units.bot50,
    left  = units.left50,
    right = units.right50,
  },
}

windowResizeOrPush というオブジェクトをウィンドウのリサイズと移動を行う処理の起点とします。

windowResizeOrPush.previousRect にリサイズ後のウィンドウサイズを格納し、同様のウィンドウのリサイズが行われたかの判定に使用しています。

(↑執筆中に気づきましたが、同一解像度のディスプレイにてマルチディスプレイ環境を構築している場合はこのあたりの処理を見直す必要がありそうです)

windowResizeOrPush.units にて矢印キーの方向と units 内のウィンドウジオメトリーの値を連動させています。


function windowResizeOrPush:getNextScreen(window, cursor)
  local nextScreen = nil
  if cursor == 'up' then
    nextScreen = window:screen():toNorth()
  elseif cursor == 'down' then
    nextScreen = window:screen():toSouth()
  elseif cursor == 'left' then
    nextScreen = window:screen():toWest()
  elseif cursor == 'right' then
    nextScreen = window:screen():toEast()
  end
  return nextScreen
end

引数で与えられたウィンドウ(アクティブなウィンドウ)と、カーソルの方向から、隣のディスプレイを取得するメソッドを定義しています。

隣のディスプレイがない場合は nil を返していますが、JavaScriptやPHPでいうところの null と同じようなものです。

hs.window というAPIがウィンドウに関するAPIで、 hs.screen というAPIがディスプレイに関連するAPIです。ウィンドウ移動、リサイズのスクリプトをカスタマイズしたくなった場合、この辺りのドキュメントを読むとイメージが湧いてくるかもしれません。


-- ウィンドウサイズをスクリーンの指定箇所50%にリサイズする
-- 既にリサイズ済みで表示中スクリーンの隣にスクリーンが存在する場合、そのスクリーンに移動する
function windowResizeOrPush:exec(cursor)
  return function()
    local window = hs.window.focusedWindow()
    local nextScreen = self:getNextScreen(window, cursor)

    if (window:frame():equals(self.previousRect[cursor]) and nextScreen ~= nil) then
      window:moveToScreen(nextScreen, false, true) -- リサイズあり、ウィンドウに収まるように上部のディスプレイに移動
    else
      window:moveToUnit(self.units[cursor])
      self.previousRect[cursor] = window:frame()
    end
  end
end

if (window:frame():equals(self.previousRect[cursor]) and nextScreen ~= nil) then にてアクティブウィンドウのサイズと、windowResizeOrPush.previousRect のサイズを比較しています。

equals は hs.geometry APIのメソッドなのでウィンドウサイズの比較が必要な際はそちらのドキュメントを見ると参考になります。


-- windowリサイズ、移動系キーバインド
mash = { 'ctrl', 'alt' }
hs.hotkey.bind(mash, 'left',   windowResizeOrPush:exec('left'))
hs.hotkey.bind(mash, 'right',  windowResizeOrPush:exec('right'))
hs.hotkey.bind(mash, 'up',     windowResizeOrPush:exec('up'))
hs.hotkey.bind(mash, 'down',   windowResizeOrPush:exec('down')
hs.hotkey.bind(mash, 'return', function() hs.window.focusedWindow():maximize() end)

ウィンドウのリサイズ、移動のショートカットを設定しています。

ショートカットを他のキーに変更したい場合はこちらを変更すればokです。

「crtl + option + enter」でウィンドウを最大化するショートカットも追加しています。

ウィンドウを最大化するメソッドは window:maximize() にて既に用意されているのでそれを利用しています。

他にもいろいろできるよ Hammerspoon

一つのブログでは紹介しきれないほど Hammerspoon でできることは多いです。

(ウィンドウに関するスクリプトだけでも紹介しきれていない内容がまだあるのでそれはまたの機会で…)

Hammerspoonに興味を持っていただけたのであれば、公式の Getting Started Guide からコピペするだけでも便利な機能を手に入れることがでるのでぜひ覗いてみてください。

  • macをスリープさせないようにワンクリックで切り替え
  • 自宅のwifi圏外になったらスピーカーをミュート などなど

この記事を書いた人

はま

入社年2021年

出身地東京

業務内容web開発

特技・趣味キャンプ

テクログに関する記事一覧

TOP