Skip to content

Instantly share code, notes, and snippets.

@958
Last active June 24, 2026 09:43
Show Gist options
  • Select an option

  • Save 958/f1336bf4f71f86e93e0cc2d6a0dcd7d4 to your computer and use it in GitHub Desktop.

Select an option

Save 958/f1336bf4f71f86e93e0cc2d6a0dcd7d4 to your computer and use it in GitHub Desktop.
Filedini migemo incremental search script

migemo インクリメンタル検索(Filedini 拡張スクリプト)

現在開いているフォルダのファイル/フォルダを、ローマ字入力(migemo)でインクリメンタルに検索し、 マッチした項目へカーソルをライブ移動させる Filedini 用 Python スクリプトです。

  • スクリプト本体: migemo_isearch.py
  • エントリ関数: migemo_isearch(host)

できること

  • ダイアログのローマ字入力欄に文字を打つたびに、現在フォルダ内で最初のマッチ項目へカーソルが移動します。
    • 例: tokyo → 「東京…」、nihongo → 「日本語…」、kanji → 「漢字…」
  • Next / Prev ボタンでマッチ候補を順送り/逆送りに循環移動します。
  • Close でダイアログを閉じます。閉じた時点のカーソル位置がそのまま残ります (項目を開いたり、元の位置へ戻したりはしません)。
  • migemo の正規表現は元のローマ字も含むため、英数字だけのファイル名も部分一致でヒットします。
  • 大文字小文字は区別しません。

動作要件

  • Windows 64bit
  • Filedini に埋め込まれた Python(3.14)上で実行されます
  • 初回セットアップ時のみインターネット接続(後述)

セットアップ(migemo エンジンの自動取得)

migemo の検索には C/Migemo の migemo.dll と UTF-8 辞書が必要です。スクリプトは初回実行時に 自動でダウンロード・展開します。

  1. 取得元: https://files.kaoriya.net/goto/cmigemo_w64(KaoriYa 配布の win64 zip へのリダイレクト)
  2. 展開先: スクリプトと同じ階層の migemo_runtime/
    • migemo_runtime/migemo.dll
    • migemo_runtime/dict/utf-8/migemo-dict(+ roma2hira.dat / hira2kata.dat / han2zen.dat / zen2han.dat

重要: dict/utf-8/ 内の変換テーブル(roma2hira.dat 等)は、migemo.dll が辞書と同じ フォルダから自動読込します。これらが揃っていないとローマ字→ひらがな変換が無効になり、 tokyo のような一部の語しか展開されません。そのため dict/utf-8/ 配下を丸ごと配置します。

一度展開されれば、以降はダウンロードせず migemo_runtime/ のファイルを使います。

自動取得に失敗する場合(手動配置)

ネットワーク制限などで自動取得できない場合は、手動でファイルを配置してください。

  1. C/Migemo の Windows 64bit 版を入手: https://www.kaoriya.net/software/cmigemo/
  2. zip 内の以下を script_host/migemo_runtime/ にコピー
    • migemo.dll
    • dict/utf-8/(フォルダごと。migemo-dict と各 *.dat を含む)

配置後にスクリプトを再実行すれば、ダウンロードをスキップして起動します。

使い方(登録と実行)

このスクリプトは samples/samples.json には登録されていません。コマンドとして使うには、 Filedini のスクリプト設定で次を登録してください。

  • スクリプトファイル: migemo_isearch.py(script_host からの相対)
  • 関数名: migemo_isearch
  • ホットキー: 任意(例として検索系の空きキーを割り当てると便利です)

登録後にコマンドを実行すると検索ダイアログが開きます。REPL から migemo_isearch(host) を直接 呼び出して試すこともできます。

操作

操作 動作
ローマ字を入力 入力のたびに再検索し、最初のマッチへカーソルをライブ移動
Next 次のマッチ候補へ循環移動
Prev 前のマッチ候補へ循環移動
Close ダイアログを閉じる(カーソルはそのまま残る)

キーボードのみで閉じたい場合は、ダイアログのクローズ操作(×/Esc 等、環境依存)をご利用ください。 Filedini のカスタムダイアログ API には生のキー押下イベントが無く、Ctrl+N / Ctrl+P などの 任意ショートカット割当はスクリプト側では実装できません。

構成ファイル

パス 役割
script_host/migemo_isearch.py 本体(エンジン層 / マッチ層 / UI 層)
script_host/migemo_runtime/ 自動取得される migemo 本体・辞書(初回生成)
script_host/tests/test_migemo_isearch.py マッチ層・zip 展開の単体テスト(ホスト非依存)

内部構成(概要)

  • エンジン層 (MigemoEngine, ensure_engine, _download, _extract_runtime) ctypesmigemo.dll絶対パスロード(PATH に依存しない)。migemo_open/query/release/close を直接呼びます。辞書・クエリ・返却正規表現はすべて UTF-8。
  • マッチ層 (_build_pattern, match_items) engine.query(romaji) → 正規表現 → re.IGNORECASE でコンパイルし、現在フォルダの各項目名に search。migemo が失敗した場合は 大文字小文字無視の部分一致にフォールバックします。
  • UI 層 (migemo_isearch) 入力欄の text_changed で再検索し host.folder_window.set_cursor(...) でライブ移動。 Next/Prev は候補インデックスを循環させて移動します。

トラブルシューティング

  • 一部の語しか日本語に展開されない(例: tokyo は東京になるが kanji が漢字にならない) → migemo_runtime/dict/utf-8/roma2hira.dat / hira2kata.dat が無い状態です。 dict/utf-8/ を丸ごと配置し直してください。
  • 「Migemo Setup Failed」ダイアログが出る → 自動ダウンロードに失敗しています。上記「手動配置」を行ってください。
  • 「Migemo Load Failed」ダイアログが出るmigemo.dll(64bit)の読み込みに失敗しています。配置場所と bit 数を確認してください。

権利表記

  • 検索エンジンには第三者ソフトウェアである C/Migemo(KaoriYa / koron、辞書を含む)を利用します。 ライセンス・再配布条件は配布元(https://www.kaoriya.net/software/cmigemo/ および https://github.com/koron/cmigemo)の規定に従ってください。本スクリプトは実行時にこれらを ダウンロードして利用するだけで、同梱はしていません。
from __future__ import annotations
from typing import TYPE_CHECKING, Optional, List
import os
import re
import io
import zipfile
import sys
import ctypes
import urllib.request
# ---------------------------------------------------------------------------
# Type checking block (editor IntelliSense only; not loaded at runtime).
# ---------------------------------------------------------------------------
if TYPE_CHECKING:
try:
from host_stubs import HostAPI
host: HostAPI = ...
except ImportError:
pass
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
DLL_NAME = "migemo.dll"
DICT_REL = os.path.join("dict", "utf-8", "migemo-dict")
# Runtime files live in a migemo_runtime/ folder next to this script.
_RUNTIME_DIR = os.path.normpath(
os.path.join(os.path.dirname(os.path.abspath(__file__)), "migemo_runtime")
)
# C/Migemo Windows distribution (migemo.dll + dict/utf-8/migemo-dict).
# The kaoriya "goto" link is a redirect to the current win64 zip; urllib follows it.
DOWNLOAD_URLS = [
"https://files.kaoriya.net/goto/cmigemo_w64",
]
# ---------------------------------------------------------------------------
# Match layer (pure logic, unit-tested)
# ---------------------------------------------------------------------------
def _build_pattern(engine, text: str) -> Optional["re.Pattern"]:
"""Build a case-insensitive regex from a romaji query via migemo.
Falls back to an escaped substring match when migemo is unavailable or
produces an invalid pattern. Returns None for empty input.
"""
if not text:
return None
regex = None
if engine is not None:
try:
regex = engine.query(text)
except Exception:
regex = None
if regex:
try:
return re.compile(regex, re.IGNORECASE)
except re.error:
pass
return re.compile(re.escape(text), re.IGNORECASE)
def match_items(items, engine, text: str) -> List:
"""Return items whose .name matches the migemo pattern, in display order."""
pattern = _build_pattern(engine, text)
if pattern is None:
return []
return [it for it in items if pattern.search(getattr(it, "name", "") or "")]
# ---------------------------------------------------------------------------
# Runtime extraction (pure logic, unit-tested)
# ---------------------------------------------------------------------------
def _find_by_basename(names, basename: str) -> Optional[str]:
target = basename.lower()
for n in names:
if n.replace("\\", "/").split("/")[-1].lower() == target:
return n
return None
def _extract_runtime(zip_bytes: bytes, dest_dir: str):
"""Extract migemo.dll and the whole UTF-8 dict dir from a cmigemo zip.
The entire ``dict/utf-8/`` directory is extracted (not just migemo-dict),
because migemo_open auto-loads the sibling conversion tables
(roma2hira.dat, hira2kata.dat, ...) from the dict's directory. Without
them, romaji-to-hiragana conversion is disabled.
Returns (dll_path, dict_path). Raises RuntimeError if the dll or the
migemo-dict are absent.
"""
os.makedirs(dest_dir, exist_ok=True)
dll_path = os.path.join(dest_dir, DLL_NAME)
utf8_dir = os.path.join(dest_dir, "dict", "utf-8")
dict_path = os.path.join(dest_dir, DICT_REL)
with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf:
names = zf.namelist()
dll_member = _find_by_basename(names, DLL_NAME)
utf8_members = [
n for n in names
if "/dict/utf-8/" in ("/" + n.replace("\\", "/").lower())
and not n.replace("\\", "/").endswith("/")
]
has_dict = any(
n.replace("\\", "/").lower().endswith("utf-8/migemo-dict")
for n in utf8_members
)
if not dll_member or not has_dict:
raise RuntimeError(
"zip does not contain expected migemo.dll / utf-8 migemo-dict"
)
os.makedirs(utf8_dir, exist_ok=True)
with zf.open(dll_member) as src, open(dll_path, "wb") as dst:
dst.write(src.read())
for member in utf8_members:
basename = member.replace("\\", "/").split("/")[-1]
with zf.open(member) as src, open(os.path.join(utf8_dir, basename), "wb") as dst:
dst.write(src.read())
return dll_path, dict_path
# ---------------------------------------------------------------------------
# Engine layer (ctypes wrapper + auto setup) — verified on a live host
# ---------------------------------------------------------------------------
class MigemoEngine:
"""Thin ctypes wrapper over C/Migemo (migemo.dll), loaded by absolute path."""
def __init__(self, dll_path: str, dict_path: str):
runtime_dir = os.path.dirname(os.path.abspath(dll_path))
# Ensure dependent DLL resolution works without relying on PATH.
if hasattr(os, "add_dll_directory"):
self._dll_dir = os.add_dll_directory(runtime_dir)
self._lib = ctypes.CDLL(dll_path)
self._lib.migemo_open.restype = ctypes.c_void_p
self._lib.migemo_open.argtypes = [ctypes.c_char_p]
self._lib.migemo_query.restype = ctypes.POINTER(ctypes.c_char)
self._lib.migemo_query.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
self._lib.migemo_release.restype = None
self._lib.migemo_release.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
self._lib.migemo_close.restype = None
self._lib.migemo_close.argtypes = [ctypes.c_void_p]
self._handle = self._lib.migemo_open(dict_path.encode("utf-8"))
if not self._handle:
raise RuntimeError("migemo_open returned NULL (dictionary load failed)")
def query(self, text: str) -> str:
ptr = self._lib.migemo_query(self._handle, text.encode("utf-8"))
if not ptr:
return ""
try:
value = ctypes.cast(ptr, ctypes.c_char_p).value
return value.decode("utf-8", "replace") if value else ""
finally:
self._lib.migemo_release(self._handle, ctypes.cast(ptr, ctypes.c_void_p))
def close(self) -> None:
if getattr(self, "_handle", None):
self._lib.migemo_close(self._handle)
self._handle = None
_engine_cache = None
def _download(urls, log) -> bytes:
last_error = None
for url in urls:
try:
log(f"migemo: downloading {url}")
with urllib.request.urlopen(url, timeout=60) as resp:
return resp.read()
except Exception as e: # noqa: BLE001 - report and try next mirror
last_error = e
log(f"migemo: download failed for {url}: {e}")
raise RuntimeError(f"all downloads failed: {last_error}")
def ensure_engine(host):
"""Load (and if needed auto-install) the migemo engine. Returns None on failure."""
global _engine_cache
if _engine_cache is not None:
return _engine_cache
dll_path = os.path.join(_RUNTIME_DIR, DLL_NAME)
dict_path = os.path.join(_RUNTIME_DIR, DICT_REL)
if not (os.path.exists(dll_path) and os.path.exists(dict_path)):
try:
data = _download(DOWNLOAD_URLS, host.log)
_extract_runtime(data, _RUNTIME_DIR)
except Exception as e: # noqa: BLE001
host.log(f"migemo setup failed: {e}", host.LogLevel.ERROR)
host.ui.ok_dialog(
"Migemo Setup Failed",
"migemo.dll と辞書を自動取得できませんでした。\n"
"次のフォルダに手動で配置してください:\n"
f"{_RUNTIME_DIR}\n"
f"- {DLL_NAME}\n"
f"- {DICT_REL}\n\n"
"入手元: https://www.kaoriya.net/software/cmigemo/",
)
return None
try:
_engine_cache = MigemoEngine(dll_path, dict_path)
except Exception as e: # noqa: BLE001
host.log(f"migemo load failed: {e}", host.LogLevel.ERROR)
host.ui.ok_dialog("Migemo Load Failed", f"migemo.dll の読み込みに失敗しました:\n{e}")
return None
return _engine_cache
# ---------------------------------------------------------------------------
# UI layer (live-cursor incremental search) — verified on a live host
# ---------------------------------------------------------------------------
def migemo_isearch(host):
"""Entry point: incremental migemo search over the current folder.
Type romaji; the pane cursor follows the first match live. Prev/Next cycle
candidates. Close dismisses the dialog, leaving the cursor on the last match.
"""
engine = ensure_engine(host)
if engine is None:
return # ensure_engine already reported the failure.
result = host.folder_window.get_items(limit=0)
items = list(result.items) if result else []
items = [it for it in items if it.name != ".." and getattr(it, "name", None) is not None]
if not items:
host.ui.ok_dialog("Migemo Search", "現在のフォルダに項目がありません。")
return
state = {"matches": [], "index": 0}
dlg = host.ui.dialog("Migemo Search")
tb = dlg.text("ローマ字:", "", initial_focus=True)
# OK is the primary (default) button, so Enter confirms it. The dialog
# API exposes no raw key events, so per-button keyboard shortcuts are not
# available.
buttons = dlg.group(host.ui.LayoutDirection.HORIZONTAL)
prev_btn = buttons.button("Prev")
next_btn = buttons.button("Next")
close_btn = buttons.button("Close")
def set_nav_enabled(enabled):
# Runs only while the dialog is shown (from text_changed handler).
try:
prev_btn.enabled = enabled
next_btn.enabled = enabled
except Exception as e: # noqa: BLE001
host.log(f"migemo: enable toggle failed: {e}")
def goto(idx):
if not state["matches"]:
return
state["index"] = idx % len(state["matches"])
try:
host.folder_window.set_cursor(state["matches"][state["index"]].path)
except Exception as e: # noqa: BLE001
host.log(f"migemo: set_cursor failed: {e}")
def on_changed(sender, value):
try:
state["matches"] = match_items(items, engine, value)
state["index"] = 0
if state["matches"]:
goto(0)
set_nav_enabled(True)
else:
set_nav_enabled(False)
except Exception as e: # noqa: BLE001
host.log(f"migemo: search error: {e}", host.LogLevel.ERROR)
tb.text_changed += on_changed
prev_btn.clicked += lambda s, e: goto(state["index"] - 1)
next_btn.clicked += lambda s, e: goto(state["index"] + 1)
close_btn.clicked += lambda s, e: dlg.close(host.ui.DialogResult.OK)
dlg.show_modal()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment