個人的 MacOS で skim で PDF をリロードするためのスクリプトを書きました!


背景

MacOS の PDF viewer は何を使用していますか? 自分は、PDF でドキュメントや出力を必要とすることはなかったので、特に何も使用していませんでしたが、最近、業務でデータが集計できてきたので合わせて、統計等をかじり始めまして R 言語というものに手を出し始めたのですが、R の Preview が Rstudio 一強な感じでしたが、Vim で編集してシンプルに Preview だけ開きたいという制約があるので、RStudio 使いません。plot の Preview とかを PDF に出力して見たいと言う要件になりました。同時に Latex や RMarkdown も環境構築だけしましたが、こちらも Preview の出力が PDF みたいな感じでしたので CLI base でプレビューするソフトウェアが必要となりました。MacOS の標準の Preview app では、コマンドが提供されているかわからないですし、PDF 再出力後に一度 Previewr app に focus しなければならないという感じでした。 Skim というものを見つけて情報が多そうなのと機能が良さそうなので使い始めました。機能自体まだ全然使いこなせていませんが、以下のような機能があるそうです。

\ Viewing PDFs \ Adding and editing notes \ Highlighting important text, including one-swipe highlight modes \ Making "snapshots" for easy reference \ Navigation using table of contents or thumbnails, with visual history \ View all your notes and highlights \ Convenient reading in full screen \ Giving powerful presentations, with built-in transitions \ Handy preview of internal links \ Focus using a reading bar \ Magnification tool \ Smart cropping tools \ Extensive AppleScript support \ Bookmarks \ And much more... \

Skim を使い始めたはいいものの、PDF 再出力後に SKim Application にフォーカスしても表示内容が出力されないのです。以下のドキュメントを読むに Skim はファイルの場所ではなくファイルオブジェクトを追跡するため、PDF ファイルが置き換えられる前に削除されると、Skim の自動ファイル更新メカニズムは無効になります。 との事です、確実にリロードをしたいなら、PDF 再作成後に手動で開き直すか、スクリプトで開き直す必要があるそうです。とにかく、個人的には、以下の sample の applescript を参考に wrap して、Vim から呼び出せばいいという結論になりました。

Skim / Wiki / TeX_and_PDF_Synchronization

Reload updated PDF files
Skim can recognize when the PDF file is updated on disk, for example by a LaTeX process. Skim then offers to reload the file. If you choose Auto from the dialog, Skim will reload this document without asking for future updates.
Use this feature with care, as reloading the file will typically lose any notes. If you have unsaved edits, Skim will always ask you whether to reload, even if you have previously chosen Auto. When you choose No, you can still reload manually by choosing Revert from the File menu.
This feature should be turned on in the LaTeX preferences.
Even though we offer this feature, we discourage you from using it. In general, the only reliable way to automatically reload the document is by triggering the reload yourself from a script as the one below, which therefore is the preferred approach.
Note: Skim's automatic file updating mechanism gets disabled when the PDF file is deleted before it is replaced, because Skim tracks the file object rather than the location of the file. This is by design for good reasons, and it is the way any document based Cocoa application works. Some TeX scripts (e.g. simpdftex) remove the PDF file, and therefore Skim will not automatically reload the file produced by such processes.
Note: The auto-reload functionality will not work properly when you have to run a latex process more than once, for example to support references generated by bibtex, because Skim will be trying to reload the document when the second latex process is busy. You could instead run latex and bibtex together with forcing a Skim reload using the script below.

サンプルのスクリプト

Current directory の *.tex ファイルを pdflatex && bibtex して、 Current directory *.pdf に出力します。その後、pdffile を開き直します。このままでは、Current directory に PDF 以外のファイル(*.aux *.bbl *ablg *.log *.pdf)等、色々出力されて少し面倒なので、/tmp に出力したほうがいいなと思いました。 また、Skim を activate するとフォーカスされ、毎回ターミナルに にフォーカスを戻さないといけないので、それも自動でやってしましたいですね、あとは、Latex 以外の変換も提供したスクリプトが必要だなと思いました。

!/bin/bash

# the first argument should be the tex file, either with or without extension
file="$1"
[ "${file:0:1}" == "/" ] || file="${PWD}/${file}"
pdffile="${file%.tex}.pdf"

# run pdflatex and bibtex, and open or reload the pdf  in Skim
pdflatex "${file}" && bibtex "${file}" && pdflatex "${file}" && pdflatex "${file}" && \
/usr/bin/osascript << EOF
  set theFile to POSIX file "${pdffile}" as alias
  tell application "Skim"
  activate
  set theDocs to get documents whose path is (get POSIX path of theFile)
  if (count of theDocs) > 0 then revert theDocs
  open theFile
  end tell
EOF

イメージ

skim gif

AppleScript

osacript とは(man osacript)、execute AppleScripts and other OSA language scripts MacOS がアプリケーションを操作するための、AppleScript を実行するためのコマンドです。
Apple script では、いろいろなことができるようです、

# 出力
osascript -e 'return "Hello, World"'

# クリップボードの中身を出力
osascript -e 'get the clipboard'

# Finder アプリケーションにダイアログを出させる
osascript -e 'tell Application "Finder" to display dialog'

# bluetooth 接続とか
# [GitHub - Rasukarusan/fzf-bluetooth-connect: Fuzzy search and connect bluetooth devices via the terminal] \
# (https://github.com/Rasukarusan/fzf-bluetooth-connect)

以下か実際に使用するスクリプトでは、 current_term() で、今アクティブ(一番前にいる) アプリケーション名を取得します。open_skim() では、PDF_FILE を skim で開き ${FOCUS} = false のときは、current_term() にフォーカスを戻します。

#!/bin/bash
FOCUS=false
PDF_FILE=""

open_skim() {
	/usr/bin/osascript <<EOF
  set theFile to POSIX file "$PDF_FILE" as alias
  tell application "Skim"
    activate
    set theDocs to get documents whose path is (get POSIX path of theFile)
    if (count of theDocs) > 0 then revert theDocs
      open theFile
  end tell
  tell application "$CURRENT_TERM"
    if ${FOCUS} then
    else
      activate
    end if
  end tell
EOF
}

current_term() {
	/usr/bin/osascript <<EOF
  tell application "System Events"
    set activeApp to name of first application process whose frontmost is true
  end tell
  return {activeApp}
EOF
}

Vim

skim_reload コマンドを呼び出す時は、asyncrun を使用して、非同期で実行します、これにより Vim での編集を中断する事なくスクリプトを実行できます。
GitHub - skywind3000/asyncrun.vim: Run Async Shell Commands in Vim 8.0 / NeoVim and Output to the Quickfix Window !!

autocmd を変更してファイルの保存時に実行してもいいと思います。現在では、nnoremap sk をタイプした時に filtype ごとにスクリプトのコンパイルを非同期で実行します。

function! s:skimPDFLatex()
  let absolutePath=expand('%:p')
  let cmd = "AsyncRun skim_reload -s pdflatex " . absolutePath
  silent execute cmd
endfunction

function! s:skimRPlot()
  let absolutePath=expand('%:p')
  let cmd = "AsyncRun skim_reload -s rplot " . absolutePath
  silent execute cmd
endfunction

function! s:skimRMarkdown()
  let absolutePath=expand('%:p')
  let cmd = "AsyncRun skim_reload -s rmd " . absolutePath
  silent execute cmd
endfunction

autocmd BufEnter *.rmd nnoremap <silent> sk :call <SID>skimRMarkdown()<CR>
autocmd BufEnter *.r nnoremap <silent> sk :call <SID>skimRPlot()<CR>
autocmd BufEnter *.tex nnoremap <silent> sk :call <SID>skimPDFLatex()<CR>

おわりに、スクリプト全行

スクリプト全行です。/usr/local/bin/skim_reload とかに置いて使用しています。とりあえず、pdflatex, rplot, rmarkdown をコンパイルできる用にしていますが、エラーハンドリングとか拡張性等まだまだ課題はあるのか、といった感じですが、とりあえずしばらく使ってみて改善していこうと思います。シェルスクリプトに関しては、自明ですが --silent, --focus オプションフラグの解決、各環境ごとのファイルパスの解決等、基本的なものしか追加していません。

#!/bin/bash

# this script convert .pdf and reload skim.app.
# you can expansion, resolve path, customize converter.
# referenced: https://sourceforge.net/p/skim-app/wiki/TeX_and_PDF_Synchronization.

open_skim() {
	/usr/bin/osascript <<EOF
  set theFile to POSIX file "$PDF_FILE" as alias
  tell application "Skim"
    activate
    set theDocs to get documents whose path is (get POSIX path of theFile)
    if (count of theDocs) > 0 then revert theDocs
      open theFile
  end tell
  tell application "$CURRENT_TERM"
    if ${FOCUS} then
    else
      activate
    end if
  end tell
EOF
}

current_term() {
	/usr/bin/osascript <<EOF
  tell application "System Events"
    set activeApp to name of first application process whose frontmost is true
  end tell
  return {activeApp}
EOF
}

# Initialize global variables
SILENT=false
CURRENT_TERM="$(current_term)"
FOCUS=false
POSITIONAL_ARGS=()
FILE=""
FILE_BASE=""
FILE_NAME=""
FILE_EXTENSION=""
DIR_BASE=""
PDF_FILE=""

# parse flags
while [[ $# -gt 0 ]]; do
	case $1 in
	-s | --silent)
		SILENT=true
		shift
		;;
	-f | --focus)
		FOCUS=true
		shift
		;;
	*)
		POSITIONAL_ARGS+=("$1")
		shift
		;;
	esac
done

set -- "${POSITIONAL_ARGS[@]}"

run_skim() {
	FILE="$2"
	if [[ ! -e "$FILE" ]]; then
		printf "File not found %s" "$FILE" 1>&2
	fi

	DIR_BASE="$3"
	if [[ -z "$DIR_BASE" ]]; then
		DIR_BASE="/tmp"
	fi
	FILE="$(realpath "$FILE")"
	FILE_NAME="$(basename "${FILE%.*}")"
	FILE_EXTENSION="${FILE##*.}"

	case "$1" in
	"pdflatex")
		if [[ $(echo "$FILE_EXTENSION" | tr "[:lower:]" "[:upper:]") != "TEX" ]]; then
			printf "File extension is not .tex %s" "$FILE_BASE" 1>&2
			exit 1
		fi
		FILE_BASE="$FILE_NAME.pdf"
		PDF_FILE="$DIR_BASE/$FILE_BASE"
		pdflatex -output-directory=${DIR_BASE} -interaction nonstopmode "$FILE" && bibtex "$FILE"
		if [[ -e "$PDF_FILE" ]]; then
			open_skim
		else
			printf "File not found %s" "$PDF_FILE" 1>&2
		fi
		;;
	"rplot")
		if [[ $(echo "$FILE_EXTENSION" | tr "[:lower:]" "[:upper:]") != "R" ]]; then
			printf "File extension is not .r %s" "$FILE_BASE" 1>&2
			exit 1
		fi
		cp -f "$FILE" "$DIR_BASE"
		FILE="$FILE_NAME.r"
		FILE_BASE="$FILE_NAME.pdf"
		PDF_FILE="$DIR_BASE/Rplots.pdf"
		# shellcheck disable=SC2164
		cd "$DIR_BASE"
		rscript "$FILE"
		if [[ -e "$PDF_FILE" ]]; then
			open_skim
		else
			printf "File not found %s" "$PDF_FILE" 1>&2
		fi
		;;
	"rmd")
		if [[ $(echo "$FILE_EXTENSION" | tr "[:lower:]" "[:upper:]") != "RMD" ]]; then
			printf "File type not .rmd %s" "$FILE_BASE" 1>&2
			exit 1
		fi
		FILE_BASE="$FILE_NAME.pdf"
		PDF_FILE="$DIR_BASE/$FILE_NAME.pdf"
		local q
		q="$(
			cat <<EOF
rmarkdown::render("$FILE", rmarkdown::pdf_document(), "$DIR_BASE/$FILE_NAME")
EOF
		)"
		R -e "$q"
		if [[ -e "$PDF_FILE" ]]; then
			open_skim
		else
			printf "File not found %s" "$PDF_FILE" 1>&2
		fi
		;;
	*)
		printf "Type not found %s\n" "$1" 1>&2
		exit 0
		;;
	esac
}

if [[ $SILENT == true ]]; then
	run_skim "$@" >/dev/null
else
	run_skim "$@"
fi