emacsclientをmacOSで起動して
最前面にする
(AppleScript)

emacs-plusをHomebrewでインストールして使っている方なら、emacsclientでサーバーに接続して素早くEmacsを開きたいと思うはずです。
ただ、LaunchPadから起動すると、ウィンドウがアクティブにならない問題に遭遇しました。

関連記事

1. emacsclientがフォーカスされない

emacs-plusをインストールすると、Emacs daemonを起動してemacsclientで接続する使い方ができます1
ターミナルからemacsclient -cを実行すれば新しいフレーム(ウィンドウ)が開きますが、macOSのLaunchPadやSpotlightから起動すると、ウィンドウは開いても前面に来ないという問題がありました。

解決策:AppleScriptでアプリを作る AS code A Emma.app 作成手順 1 Script Editorを開く 2 スクリプトを書く 3 アプリケーション形式で書き出す メリット • 軽量で高速起動 • 確実なフォーカス制御

2. スクリプトの仕組み

結論から言うと、AppleScriptでアプリケーションを作るのが最適でした。

2. スクリプトの仕組み

Script Editor(スクリプトエディタ)を開いて、以下のスクリプトを書きます2

do shell script "
EMACSCLIENT='/Applications/Emacs.app/Contents/MacOS/bin/emacsclient'
EMACS='/Applications/Emacs.app/Contents/MacOS/Emacs'

server_alive() {
  \"$EMACSCLIENT\" -e '(progn t)' >/dev/null 2>&1
}

# 1) デーモンがいなければ起動
if ! server_alive; then
  \"$EMACS\" --daemon >/dev/null 2>&1 &
  # 起動待ち(最大3秒)
  i=0
  while ! server_alive; do
    i=$((i+1))
    [ $i -ge 30 ] && break
    sleep 0.1
  done
fi

# 2) それでもダメならエラー終了(アラートに出る)
if ! server_alive; then
  echo 'Error: Could not start the Emacs daemon.' 1>&2
  exit 1
fi

# 3) 既存フレームがあればフォーカス、なければ新規フレーム作成
FRAME_COUNT=$(\"$EMACSCLIENT\" -e '(length (frame-list))' 2>/dev/null | tr -d '\"' || echo 1)

if [ \"$FRAME_COUNT\" -gt 1 ]; then
  \"$EMACSCLIENT\" -n -e '(select-frame-set-input-focus (selected-frame))' >/dev/null 2>&1
else
  \"$EMACSCLIENT\" -n -c >/dev/null 2>&1
fi
"

tell application "System Events"
	repeat 50 times
		set candidates to (processes whose name contains "Emacs")
		repeat with p in candidates
			if (count of windows of p) > 0 then
				set frontmost of p to true
				return
			end if
		end repeat
		delay 0.1
	end repeat
end tell

Code language: PHP (php)

ファイルメニューから「書き出す」を選び、ファイルフォーマットを「アプリケーション」にして、Applicationsフォルダに保存します。
名前は入力しやすいように、「Emma.app」としました。

2. スクリプトの仕組み

このスクリプトは、3つのステップで動いています。

スクリプトの仕組み:3つのステップ 1 フレーム確認 frame-list GUIウィンドウの 有無を判定 2 動作選択 既存 あり 既存 なし 再利用 or 新規作成 3 フォーカス 前面に 多重起動を防ぎ、既存ウィンドウを確実に前面へ

2.1. GUIフレームの存在確認

当初は、既にウィンドウが開いているのに新しいフレームが追加で作成されてしまう問題がありました。
emacsclient -cは無条件に新しいフレームを作るので、既存ウィンドウの有無を確認する必要があったのです。

そこで、「デーモンが生きているか」を先に判定し、死んでいたら Emacs --daemon で起動してます。
そして、emacsclient -e '(length (frame-list))'で、現在開いているEmacsのフレーム数を取得します3
Emacs daemonだけが起動している状態だとフレーム数は1ですが、GUIウィンドウが開いていれば2以上になります。

ここがポイントで、単にdaemonが起動しているかだけでなく、実際にウィンドウが開いているかを判定しています。

2.2. 既存フレームの再利用か新規作成か

フレーム数が2以上なら既存のウィンドウがあるので、select-frame-set-input-focusでフォーカスを当てるだけです。
フレーム数が1以下なら、-cオプションで新しいGUIフレームを作成します4

-a ''オプションは、daemonが起動していない場合に自動で起動してくれる保険です。

2.3. macOSでウィンドウを前面に

最後に、AppleScriptのSystem Eventsを使ってEmacsプロセスを前面に持ってきます。
これで確実にウィンドウがアクティブになります。

tell application "System Events"
	repeat 50 times
		set candidates to (processes whose name contains "Emacs")
		repeat with p in candidates
			if (count of windows of p) > 0 then
				set frontmost of p to true
				return
			end if
		end repeat
		delay 0.1
	end repeat
end tell
Code language: PHP (php)

System Events では Emacs プロセスが複数存在し得るため、first process に依存するとウィンドウのないサーバを掴む可能性があります。
そのため、まず name で候補プロセスを列挙し、各プロセスの windows 数を個別に確認すします。
ウィンドウを持つプロセスが見つかった時点で前面化することで、レースや型エラーを回避しました。

あと、アクセシビリティの権限がいるので、スクリプトエディタで変更後には、改めてアクセシビリティの一覧からアプリを消して、追加し直す必要があります。

3. 注意点:emacsclientのパス

スクリプト内では、 /Applications/Emacs.app/Contents/MacOS/bin/emacsclientとフルパスを指定しています5
これはemacs-plusの標準的なインストール場所ですが、環境によって異なる場合があります。

ターミナルでwhich emacsclientを実行して、正しいパスを確認してください。
Intel MacとApple Siliconでも場所が違うことがあります。

3.1. emma.appのアイコンを変更する

Emacsのアイコンを、emma.appにも適用しました。

# アイコンのパスを探す
find_icon() {
  local p
  for p in \
    "/Applications/Emacs Client.app/Contents/Resources/applet.icns" \
    "/Applications/Emacs.app/Contents/Resources/Emacs.icns" \
    "/Applications/Emacs.app/Contents/Resources/applet.icns"
  do
    [[ -f "$p" ]] && { printf '%s\n' "$p"; return 0; }
  done

  if command -v brew >/dev/null 2>&1; then
    local brew_prefix
    brew_prefix="$(brew --prefix 2>/dev/null || true)"
    p="$(find "${brew_prefix}" -type f \
      \( -path '*/Emacs Client.app/Contents/Resources/applet.icns' \
      -o -path '*/Emacs.app/Contents/Resources/Emacs.icns' \
      -o -path '*/Emacs.app/Contents/Resources/applet.icns' \) \
      2>/dev/null | head -n 1 || true)"
    [[ -n "${p}" ]] && { printf '%s\n' "$p"; return 0; }
  fi

  return 1
}

APP_PATH="/Applications/Emma.app"
PLIST="${APP_PATH}/Contents/Info.plist"
ICON_SRC="$(find_icon)"

cp "${ICON_SRC}" "${APP_PATH}/Contents/Resources/applet.icns"

/usr/libexec/PlistBuddy -c "Set :CFBundleIconFile applet" "${PLIST}" 2>/dev/null \
  || /usr/libexec/PlistBuddy -c "Add :CFBundleIconFile string applet" "${PLIST}"

codesign --force --deep --sign - "${APP_PATH}" >/dev/null 2>&1 || true

touch "${APP_PATH}"

echo "完了: ${ICON_SRC}${APP_PATH}"Code language: Bash (bash)

3.2. 「補助アクセス」のアクセシビリティ権限

「補助アクセス」のアクセシビリティ権限が必要なので、「システム設定」から許可をします。

3.3. さらなる高速化:daemon自動起動

起動速度をさらに改善したい場合は、ログイン時にEmacs daemonを自動起動する設定が有効です6

~/Library/LaunchAgents/gnu.emacs.daemon.plistを作成して、以下の内容を記述します。

<?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>gnu.emacs.daemon</string>
  <key>ProgramArguments</key>
  <array>
    <string>/Applications/Emacs.app/Contents/MacOS/Emacs</string>
    <string>--daemon</string>
  </array>
  <key>RunAtLoad</key>
  <true/>
  <key>KeepAlive</key>
  <true/>
</dict>
</plist>Code language: HTML, XML (xml)

ターミナルで以下を実行して有効化します7

launchctl load ~/Library/LaunchAgents/gnu.emacs.daemon.plistCode language: JavaScript (javascript)

これでログイン時にdaemonが起動し、emacsclientの接続がほぼ瞬時に完了します。

4. 【補足】Automatorでラッパーを作る案

ちなみに、まず思いついたのは、Automatorでシェルスクリプトを実行するアプリを作ることでした。
emacsclientを呼び出して、その後macOSのosascriptコマンドでEmacsをアクティブにする方法です。

#!/bin/bash
export PATH="/usr/local/bin:/opt/homebrew/bin:$PATH"
# 既存のGUIフレームがあるか確認
if pgrep -x "Emacs" > /dev/null && emacsclient -e "(> (length (frame-list)) 1)" 2>/dev/null | grep -q "t"; then
  # 既存フレームにフォーカス
  osascript -e 'tell application "Emacs" to activate'
  emacsclient -n -e '(select-frame-set-input-focus (selected-frame))'
else
  # 新規フレーム作成
  emacsclient -c -n -a "" -e '(select-frame-set-input-focus (selected-frame))'
fiCode language: PHP (php)

ただ、これには2つの問題がありました。

補足:Automator vs AppleScript VS Automator ✗ 起動が遅い (0.5〜1秒) ✗ PATH設定が必要 環境変数未設定 ✗ オーバーヘッド大 AS AppleScript ✓ 高速起動 ほぼ瞬時 ✓ フルパス指定 PATH不要 ✓ 軽量 AppleScript推奨

LaunchPadから起動すると、ターミナルのようにPATH環境変数が設定されていません。
emacsclientがどこにあるか分からず、コマンドが見つからないエラーになります。

また、Automator自体の起動に時間がかかり、体感で0.5〜1秒ほどの遅延が発生します8
せっかく軽量なemacsclientを使っているのに、これでは意味がありません。

  1. emacs-plusは公式のGNU EmacsにmacOS向けの機能拡張を加えたHomebrewフォーミュラです。ネイティブコンパイル、SVGサポート、Retina対応などの機能が含まれています。 – emacs-plus GitHub Repository
  2. Script EditorはmacOSに標準搭載されているAppleScriptの開発環境です。アプリケーションフォルダのユーティリティフォルダ内、またはSpotlight検索で「Script Editor」と入力すると起動できます。
  3. frame-listはEmacs Lispの組み込み関数で、現在存在する全てのフレーム(ウィンドウ)のリストを返します。daemon起動時には不可視のフレームが1つ作成されるため、GUIフレームがない状態でもカウントは1になります。 – Emacs Lisp Reference Manual – Frames
  4. select-frame-set-input-focusはEmacsのフレームを選択し、ウィンドウマネージャーレベルでフォーカスを設定する関数です。macOSのウィンドウシステムと連携して、確実にフレームをアクティブにします。 – Emacs Lisp Reference Manual – Input Focus
  5. LaunchPadやSpotlightから起動されたアプリケーションは、ターミナルと異なり環境変数PATHが正しく設定されていません。そのため、which emacsclientで表示されるパスをフルパスで指定する必要があります。Intel MacとApple Siliconでは、Homebrewのインストール場所が異なる点にも注意が必要です(Intel: /usr/local/bin、Apple Silicon: /opt/homebrew/bin)。
  6. LaunchAgentsはmacOSのlaunchd機構を使った自動起動の仕組みです。ユーザーがログインした時点でプログラムをバックグラウンドで起動し、常駐させることができます。 – Apple Developer – Creating Launch Daemons and Agents
  7. launchctl loadコマンドでplistファイルを読み込むと、即座にdaemonが起動し、次回ログイン時からも自動的に起動するようになります。無効化したい場合はlaunchctl unloadコマンドを使用します。また、macOS Ventura以降ではlaunchctl bootstrapコマンドの使用が推奨されています。
  8. Automatorは手軽にワークフローを作成できる反面、起動オーバーヘッドが大きいという欠点があります。AppleScriptで直接アプリケーションを作成する方が軽量で、起動時間を大幅に短縮できます。シンプルなタスクであればAppleScript、複雑なワークフローが必要な場合はAutomatorと使い分けるのが良いでしょう。