`(blog ,garaemon)

ポップカルチャーを摂取して、コードを吐き出す機械

Macで右クリックからpdf2zhを呼び出せるようにする

pdf2zhをより便利に活用する

pdf2zhは、PDFのレイアウトを保持したまま翻訳を行うソフトウェア。ollamaを含む様々なLLMを翻訳に利用できる。 オリジナルと翻訳済みのページを左右に並べて表示するPDFも生成でき、その有用性は高い。

Attention is All You Needをpdf2zhとGeminiで翻訳した例

Terminalからの実行はちょっと手間なので、右クリックメニューから呼び出せるよう設定する。

Automatorを利用して右クリックから呼び出す

MacAutomatorを使えば、シェルスクリプトを右クリックメニューから簡単に実行可能。

右クリックからpdf2zhを利用したQuick Actionを呼び出す様子

Automatorでpdf2zhを呼び出すQuick Actionを作成している様子

設定手順は以下の通り。

  1. Automatorで「クイックアクション」を新規作成。

  2. "Workflow receives current"を「PDFファイル」に指定し、「アプリケーション」を「Finder」に設定。

  3. 左メニューから「シェルスクリプトを実行」を選択。

  4. 右上の"Pass input"で"as arguments"を選択。

  5. シェルスクリプトを記述。以下はGeminiとGemma3をOllamaで使用する例。

    #!/bin/zsh
    
    PDF2ZH_PATH="$HOME/.local/bin/pdf2zh"
    
    notification(){
      /usr/bin/osascript -e "display notification \"$@\" with title \"pdf2ja-gemini\""
    }
    
    if [ -z "$@" ]; then
      notification The arguments are empty
      exit 1
    fi
    
    # Load zshrc file which defines GEMINI_API_KEY
    if [ -e "$HOME/.zshrc.mine" ]; then
      source $HOME/.zshrc.mine
    fi
    
    if ! command -v ${PDF2ZH_PATH} &> /dev/null; then
      notifiaction Error: pdf2zh is not installed
      exit 1
    fi
    
    if [ -z "${GEMINI_API_KEY}" ]; then
      notification Error: GEMINI_API_KEY is empty
      exit 1
    fi
    
    
    for f in "$@"
    do
      notification "Translating: $f"
    
      "$PDF2ZH_PATH" -lo ja -li en -s gemini:gemini-2.5-flash "$f" --output $(dirname "$f") >>/tmp/pdf2zh.log 2>&1
      if [ "$?" != "0" ]; then
        notification "Failed to convert $f"
        exit 1
      fi
      notification "Done: $f"
    done
    

    ollamaを使うならこんな感じ。

    #!/bin/zsh
    
    PDF2ZH_PATH="$HOME/.local/bin/pdf2zh"
    OLLAMA_MODEL="gemma3:4b"
    args="$@"
    
    notification(){
      /usr/bin/osascript -e "display notification \"$@\" with title \"pdf2ja-gemma3\""
    }
    
    if [ -z "$@" ]; then
      notification The arguments are empty
      exit 1
    fi
    
    if ! command -v ${PDF2ZH_PATH} &> /dev/null; then
      notification Error: pdf2zh is not installed
      exit 1
    fi
    
    if ! command -v ollama &> /dev/null; then
      notification Error: ollama is not installed. Please install ollama from ollama.com
      return 1
    fi
    
    # Check if ollama is running
    if ! ollama ps &> /dev/null; then
      notification "Error: ollama server is not running. Please start ollama."
      return 1
    fi
    
    for f in "$args"
    do
      notification "Translating: $f"
    
      "$PDF2ZH_PATH" -lo ja -li en -s ollama:"$OLLAMA_MODEL" --output $(dirname "$f") "$f" >>/tmp/pdf2zh.log 2>&1
      if [ "$?" != "0" ]; then
        notification "Failed to convert $f"
        exit 1
      fi
      notification "Done: $f"
    done
    

Emacsのスペルチェッカーをjinxに乗り換えた

これまでEmacsのスペルチェックにはflyspellを使っていたが、いくつか不満な点があったのでjinxに乗り換えた。

flyspellで困っていたこと

flyspell の一番の不満点は、辞書にない単語を登録する際の挙動だった。 GUI環境だとマウス操作を前提としたコンテキストメニューが表示されてしまい、キーボード中心のワークフローが崩れてしまう。 キーボードだけで操作することも不可能ではないけど,やりづらい。

flyspellのcontextメニュー

また、flyspellのチェック範囲はカーソル周辺に限られるため、バッファ全体のスペルミスを見逃しがちという問題もあった。

jinxへの乗り換え

jinx というパッケージをflyspellの代わりに使うようにしてみた。

(use-package jinx
  :ensure t
  :config
  ;; Ignore non-English words.
  (add-to-list 'jinx-exclude-regexps '(t ".*[^[:ascii:]].*"))
  :bind
  ("\C-cd" . jinx-correct)
  ;; just executing (global-jinx-mode) does not turn on jinx.
  ;; We have to add a hook to emacs-start-hook
  :hook ('emacs-startup-hook . 'global-jinx-mode)
  )

導入当初、日本語の単語がエラーとして扱われる問題があったが、これは jinx-exclude-regexps で非ASCII文字を除外することで解決できた。

単語登録も、 jinx-correct を呼び出せば, \"@\"をおすと単語登録に移れる。キーボードから手を離さずに完結できるようになった。 この例ではvertico-posframeを使っているので,posframeで表示されている。

jinxの単語修正・追加のUI (vertico-posframeを利用)

jinxに乗り換えたことで、スペルチェック周りのストレスがなくなり、快適になった気がする。 しばらくはこの設定で使ってみようと思う。

Journellyにorg-captureから書き込む

Journelly

最近,JournellyというアプリをiPhoneで使い始めた。

Journelly は有料のアプリなんだけど,Twitterみたいな感覚で個人のノートを書き込めるというアプリ。 ちょっとしたメモを書き込むときに便利。

ポイントは,保存フォーマットが org-mode になっているということ。 Journellyは iCloud に保存することができる。 なので,macEmacsを使っている時に,直接ファイルに書き込むことができる。

org-captureでJournellyに書き込む

Journellyのファイルは ~/Library/Mobile Documents/iCloud~com~xenodium~Journelly/Documents/Journelly.org からアクセスできる。

org-capture-templatesに以下を追加することで, org-capture起動->j で Journellyに書き込み可能。

(setq org-capture-templates
  (("j" "Journelly" entry (file (expand-file-name "~/Library/Mobile Documents/iCloud~com~xenodium~Journelly/Documents/Journelly.org"))
        "* %T @ %(system-name) by %(user-login-name)\n%?"
        :prepend t
        :jump-to-captured t
        )))

Google Docsみたいに,org-mode上で@で補完候補を出るようにする

Google Docsの@からの補完は便利

Google Docs上で @ を打つと,いろんな補完候補が表示されるのが便利。似たような機能はNotionだと / で呼び出すことができる。

これと似たようなことをorg-mode上でやるためには,yasnippet+yasnippet-capf+corfuでできそうだったので組んでみた。 yasnippetのテンプレートに関しては,以前の記事に書いた。

@ を補完対象に含める

yasnippet,というか completion-at-point は @ を単語の一部として認識しないので,少し修正する必要がある。 modify-syntax-entry というので @ も単語の一部ということにする。

(defun my-add-at-sign-to-syntax ()
  "Add @ to word syntax."
  (modify-syntax-entry ?@ "w"))
(add-hook 'org-mode-hook #'my-add-at-sign-to-syntax)

実際の挙動

前述のものも含めて,こんな感じにしている。

(use-package yasnippet :ensure t
  :config
  (setq yas-snippet-dirs '("~/.emacs.d/snippets"
                           "~/.emacs.d/yasnippet-snippets/snippets"))
  (setq yas-trigger-key "Enter")
  (yas-global-mode 1)
  )

(use-package yasnippet-capf
  :ensure t
  :after cape
  :config
  (add-to-list 'completion-at-point-functions #'yasnippet-capf)
  (defun my-add-at-sign-to-syntax ()
    "Add @ to word syntax."
    (modify-syntax-entry ?@ "w"))
  (add-hook 'org-mode-hook #'my-add-at-sign-to-syntax)
  )

(use-package corfu
  :ensure t
  :config
  (global-corfu-mode)
  (corfu-popupinfo-mode)
  )

corfuは一部抜粋。

実行時はこんな感じ。

Yasnippetでorg-mode用のキーワード展開を行う

Google Docsを使っていると @ を打った後に色々出てくるのが便利。そこで,org-modeでも似たようなことをしたくて yasnippetを使ってみた。

Google Docs@ の展開は,例えば, @today とか @yesterday とかが日付に変換される。 主にそれを参考にこれらのテンプレートを作ってみた。

@date

# -*- mode: snippet -*-
# name: today
# key: @today
# --
`(format-time-string "<%Y-%m-%d>")`

@yesterday

# -*- mode: snippet -*-
# name: yesterday
# key: @yesterday
# --
`(format-time-string "<%Y-%m-%d>" (time-subtract (current-time) (seconds-to-time (* 24 60 60))))`

@tomorrow

# -*- mode: snippet -*-
# name: tomorrow
# key: @tomorrow
# --
`(format-time-string "<%Y-%m-%d>" (time-add (current-time) (seconds-to-time (* 24 60 60))))`

@now

# -*- mode: snippet -*-
# name: now
# key: @now
# --
`(format-time-string "<%Y-%m-%d %a %H:%M>")`

@date

# -*- mode: snippet -*-
# name: Org Date from Calendar
# key: @date
# --
<`(org-read-date)`>

org-modeでblog向けのテンプレートを作成する

前回の記事でorg modeで書かれたファイルからはてなブログに投稿できるものを作った。

より便利にするため,ちょっとしたelispで簡単にblog記事をかけるようにしておく。

(defun my-create-dated-org-file (title)
  "Create a new org file with the current date and a user-provided title.
The filename will be in the format 'YYYY-MM-DD-your-title.org'.
Spaces in the title are replaced with hyphens.
If the file is new, it will be populated with a default template."
  ;; Using (interactive "s...") receives string input from the user
  ;; and binds it to the function's argument `title`.
  (interactive "sEnter file title: ")
  (let* (
         ;; Get the current date as a string in "YYYY-MM-DD" format.
         (date-str (format-time-string "%Y-%m-%d"))
         ;; Replace all spaces in the user-provided title with hyphens.
         (processed-title (replace-regexp-in-string " " "-" title))
         ;; Construct the filename in the format "date-title.org".
         (filename (concat date-str "-" processed-title ".org")))

    ;; find-file opens the file if it exists, or creates a new one if it doesn't.
    (find-file filename)

    ;; Check if the buffer size is zero.
    ;; If it's zero, it means the file was newly created.
    (when (zerop (buffer-size))
      ;; For a new file, insert the specified template.
      ;; Insert the original user-provided title into #+TITLE:.
      (insert (format "#+TITLE: %s\n" title))
      (insert "#+FILETAGS:\n"))))

うまくyasnippetとかを使ってtemplateをかけるといいんだけど。もしかしたらorg-captureの枠組みを使ったほうが楽なのかもしれない。

hatena blogをorg modeから投稿する

hatena-blog-org

Claudeにorg modeで書かれたファイルをhatena blogに投稿するツールを作ってもらった。 garaemon/hatena-blog-org

この記事は実際にこのツールによって投稿されている。 そう,直接emacsからhatena blogに投稿しているわけではない。

それにしても,Claude Codeすごい。ツールの概要を指示しただけで,本当に動くものができた。