[[🦉JINRAI]]のプランメモ。
> [!left-bubble] ![[chappy.webp]]
[[Codex CLI]] x [[GPT-5.3-Codex]] のプランです。
## Window Hints ナビゲーション拡張 実装計画
### 概要
Window Hints 表示中に、以下を追加する。
1. 任意キーで Focus Back 相当(直前アクティブウィンドウへ移動)
2. 任意キーで上下左右の隣接ウィンドウへ移動(例: hjkl)
3. 競合キーはヒント入力よりナビゲーション側を優先し、ヒントキー側は再割り当てで競合自体を回避
加えて、Focus Back 履歴は focus_back モジュールと共有し、focus_back が有効な場合のみ Window Hints 側の Focus Back キーを有効化する。
### 公開インターフェース変更
window_hints オプションに以下を追加する。
- focusBackKey: string|nil
- directionKeys: { left: string|nil, down: string|nil, up: string|nil, right: string|nil }|nil
仕様:
- いずれも「Window Hints モーダル表示中」にのみ有効。
- directionKeys / focusBackKey とヒント文字が競合する場合、競合文字はヒント文字集合から除外してキー生成する。
- focusBackKey は focus_back が有効なときのみ動作。
### 実装方針(決定事項)
1. 共有履歴トラッカーを新設する。
- 新規 focus_history.lua を追加し、currentWindow / previousWindow 更新ロジックを一元化。
- focus_back.lua は内部でこのトラッカーを利用して既存機能を維持。
- init.lua でトラッカーを生成し、focus_back と window_hints の両方へ注入する。
2. Window Hints のモーダル入力優先順位を明確化する。
- handleChar の前段で「予約キー判定」を実施。
- 優先順位は focusBackKey -> directionKeys -> 通常ヒント入力。
- 予約キーに一致した場合はヒント入力バッファへ文字を入れない。
3. ヒント文字再割り当てロジックを導入する。
- effectiveHintChars = hintChars - reservedKeys を作成。
- effectiveHintChars が空ならエラーを投げる。
- アプリ先頭prefixは以下で選ぶ。
- アプリ名を先頭から走査し、effectiveHintChars に含まれ、かつその時点で未使用の文字を優先採用。
- 見つからなければ従来相当(利用可能文字からフォールバック)。
- これにより「G 使用済みなら Google Chrome は O」を実現。
- その後の makeKeysPrefixFree は既存ロジックを継続利用。
4. 方向移動アルゴリズムを追加する。
- 基準: 現在アクティブウィンドウ中心点。
- 候補: 指定方向に中心点がある可視・標準ウィンドウ。
- スコア: 中心点間のユークリッド距離(2乗距離で比較可)。
- 同距離タイ時:
- 共有履歴の previousWindow が候補内なら最優先。
- それでも決まらなければ副軸差分が小さいもの。
- 最後はウィンドウID昇順で安定化。
- 見つからない場合は何もしない(モーダルは維持)。
5. 設定・READMEを更新する。
- README.ja.md / README.md に新オプション、競合時挙動、focus_back 依存条件を追記。
- Window Hints と Focus Back の関係を明記。
### 変更対象ファイル
- init.lua
- window_hints.lua
- focus_back.lua
- 新規: focus_history.lua
- README.ja.md
- README.md
- spec/window_hints_spec.lua
- spec/focus_back_spec.lua
- 必要に応じて spec/helpers に Window Hints 用モック追加
### テスト計画
1. window_hints 単体テスト追加
- 予約キーがヒント入力より優先される。
- hintChars から予約キー除外が効く。
- prefix 選択で「未使用の次候補文字」が使われる。
- 方向移動で最近傍が選ばれる。
- 最短距離複数時に previousWindow 優先が効く。
- 候補なし時に無変更。
- focus_back 無効時は focusBackKey が無効。
2. focus_back テスト更新
- 共有履歴トラッカー経由でも既存トグル挙動が不変。
- stateSync 条件下でも履歴更新が壊れない。
3. init テスト更新
- 共有トラッカーが必要モジュールへ正しく注入される。
- teardown順序と解放が従来どおり。
4. 回帰
- busted 全体実行で既存テスト含めて通過確認。
### 前提・採用デフォルト
- 方向移動は「中心点ベース最近傍」を採用。
- 最短距離タイ時は previousWindow 優先を採用。
- 競合回避は「ヒント文字集合から予約キー除外」を採用。
- キー設定形式は個別キー指定を採用。
- Window Hints 内 Focus Back は focus_back 有効時のみ有効。
## 課題
上下左右の移動が少し直感的ではなかったので改善。
## Directional Move 仕様改訂(直感優先)
![[2026-03-02-11-36-49.avif]]
### Summary
directionKeys の移動先選定を「中心点距離ベース」から、エッジ基準 + 同レーン前面優先に変更する。
狙いは、提示画像のようなケースで 1 -> 下 が 3、3 -> 上 が 2 になる直感的挙動に寄せること。
### Public API / Interface
- 既存オプションは変更なし(directionKeys のまま)
- 追加オプションなし(内部アルゴリズムのみ変更)
### 仕様(決定版)
1. 方向候補のゲート判定(下キー例)
- 候補 w が下候補になる条件: w.top >= current.bottom
- 上/左/右も同様に、中心ではなくウィンドウ境界で判定
2. 基本スコア(最小を採用)
- 主軸距離 primaryGap:
- 下: w.top - current.bottom
- 上: current.top - w.bottom
- 右: w.left - current.right
- 左: current.left - w.right
- 副軸ずれ crossOffset: 中心点の副軸差の絶対値
- 比較順: primaryGap 昇順 → crossOffset 昇順
3. 同レーン(near lane)判定
- primaryGap が最小候補を bestGap とし、primaryGap <= bestGap + laneTolerance を同レーンとする
- laneTolerance は固定値(例: 24px)
4. 同レーン内の前面優先
- 同レーン候補が複数ある場合、hs.window.orderedWindows() の前面順で最上位を優先
- その後、必要なら crossOffset で再比較
5. 最終タイブレーク
- 同率時は previousWindow を優先
- それでも同率なら window id 昇順で安定化
### Expected Behavior(例)
- 画像例: 1 から 下 → 3
- 画像例: 3 から 上 → 2(1 より前面のため)
### Test Cases
1. エッジ基準で方向候補を選べる(中心点基準との差分を検証)
2. primaryGap 最小を優先する
3. 同レーン内で前面ウィンドウを優先する
4. 前面優先でも同レーン外の遠い候補にはジャンプしない
5. 同率で previousWindow を優先する
6. 候補なしで nil(移動なし)
### Assumptions / Defaults
- laneTolerance = 24 をデフォルト固定値として採用
- 本改訂は内部挙動のみ変更し、設定APIは増やさない
## directionKeys 選択ロジック再設計(図ベース要件)
![[2026-03-02-12-10-48.avif]]
### Summary
現状は window_hints.lua の findDirectionalWindowTarget が「中心点方向 + 中心点距離最小」を採用しており、前面順は同距離時の補助しか効かない。
このため、図のような重なり/接触レイアウトで 1->右->2 や 2->左->1 が崩れうる。
要件に合わせ、重なり優先 + エッジ距離へ置換する。
### 現状確認(実装上の事実)
- 現在の選択規則:
1. 方向判定: 中心点比較
2. 優先: 中心点間距離(二乗)
3. 同値時: previousWindow -> 副軸差 -> id
- つまり ??? は「基本的に前面順では決まらない(中心距離が同値のときだけ間接的に影響)」。
### 変更方針(決定版)
findDirectionalWindowTarget を次のスコアリングへ変更する(公開API変更なし)。
1. 方向候補判定
- 現在ウィンドウ中心に対して、候補中心が指定方向側にあるものを候補とする。
- left: to.cx < from.cx
- right: to.cx > from.cx
- up: to.cy < from.cy
- down: to.cy > from.cy
2. 候補スコア計算
- primaryGap(主軸エッジ距離)
- left: max(0, from.left - to.right)
- right: max(0, to.left - from.right)
- up: max(0, from.top - to.bottom)
- down: max(0, to.top - from.bottom)
- 重なり/接触時は 0
- orthOverlap(副軸投影の重なり長)
- 水平移動時: Y投影重なり
- 垂直移動時: X投影重なり
- crossDelta(副軸中心差の絶対値)
3. 比較順(小さい/大きいの優先順)
1. primaryGap 昇順
2. orthOverlap 降順(重なり優先)
3. 前面順(hs.window.orderedWindows() の先頭に近い方)
4. crossDelta 昇順
5. previousWindow 一致優先
6. window id 昇順(安定化)
この順序により、図の ??? は「重なり・距離が同等なら前面に依存」になる。
### 重要な実装変更点
- window_hints.lua
- findDirectionalWindowTarget の置換
- 補助関数追加:
- directionalPrimaryGap(...)
- orthogonalOverlap(...)
- buildWindowZOrderIndex(...)
- runDirectionalAction から orderedWindows() を渡す
- _test エクスポートは必要最小限追加(新ヘルパーは非公開でも可)
### テストケース(追加/更新)
spec/window_hints_spec.lua に図準拠のケースを追加する。
1. 左図相当
- 1 -> 下 -> 3
- 1 -> 右 -> 2
- 2 -> 左 -> 1
- 2 -> 下 -> 3
- 3 -> 上 は 1/2 が候補になり、前面順で決まることを検証(orderedを入れ替えて結果が変わる)
2. 右図相当
- 1 -> 下 -> 4
- 1 -> 右 -> 2
- 2 -> 左 -> 1
- 2 -> 右 -> 3
- 2 -> 下 -> 4
- 3 -> 下 -> 4
- 3 -> 左 -> 2
- 4 -> 上 が前面順依存で変わることを検証(ordered入れ替え)
3. 回帰
- 候補なしで nil
- 同一id除外
- 既存 previousWindow タイブレークが最終段でのみ効くこと
### ドキュメント更新
- README.ja.md / README.md の方向移動説明を以下に合わせる:
- 「主軸エッジ距離優先」
- 「副軸投影重なり優先」
- 「同程度なら前面順で決定」
### Assumptions / Defaults
- 新しい設定項目は追加しない(既存 directionKeys の内部挙動変更のみ)。
- 「前面依存」は tie-break 相当ではなく、primaryGap 同値かつ重なり度が同等な局面で有効にする。
## directionKeys で「完全遮蔽ウィンドウ除外」を追加する計画
### 概要
directionKeys による移動候補から、完全に背面に隠れているウィンドウを除外する。
既存の遮蔽判定ロジック(isWindowOccluded / computeOcclusionSamplingGrid)を再利用し、Window Hints 表示時の判定基準と揃える。
### 変更方針
1. runDirectionalAction の候補収集で遮蔽除外を追加
- 現在は hs.window.visibleWindows() から isStandard() だけで候補化しているため、ここに遮蔽判定を追加する。
- orderedWins = hs.window.orderedWindows() を使い、各候補ウィンドウより手前にあるウィンドウの frame を coveringFrames として収集。
- 候補ウィンドウ frame に対して isWindowOccluded を実行し、true なら候補に入れない。
2. 判定条件の統一
- 完全遮蔽除外は showPreviewForOccluded とは独立して常に有効化する(プレビュー表示のON/OFFに挙動を依存させない)。
- サンプリンググリッドは既存 computeOcclusionSamplingGrid を利用し、occlusionSampling* 設定をそのまま反映する。
3. 実装の最小化
- 新しい公開オプションは追加しない。
- 既存の方向選択スコアリング(重なり量→主軸距離→前面順...)は変更しない。
- 変更は window_hints.lua の方向候補抽出ロジック周辺に限定する。
### 公開API/インターフェースへの影響
- API追加・削除なし。
- window_hints 設定項目の互換性維持。
- 挙動変更のみ: 完全遮蔽ウィンドウが directionKeys 遷移先にならなくなる。
### テスト計画
1. spec/window_hints_spec.lua に遮蔽除外テストを追加
- ケースA: 方向条件を満たす候補A(前面で完全遮蔽)と候補B(非遮蔽)があるとき、Bが選ばれる。
- ケースB: 方向条件を満たす候補がすべて完全遮蔽なら nil を返す。
- ケースC: 部分遮蔽(完全ではない)ウィンドウは候補に残る。
2. 回帰確認
- 既存の図ベース遷移テスト(左図/右図)を通す。
- busted 全件実行で成功を確認。
### ドキュメント更新
- README.ja.md / README.md の「Window Hints 内ナビゲーション」節に以下を追記:
- 「directionKeys の候補から完全遮蔽ウィンドウは除外される」
### 前提・デフォルト
- 「完全遮蔽」の定義は既存 isWindowOccluded のサンプル点判定に従う。
- 判定精度は既存 occlusionSampling* 設定値に依存する。
- showPreviewForOccluded の値に関係なく除外を適用する。
## 斜めへの移動も追加
上下移動のときに左右どちらをアクティブにすべきかの判定を自動で行うのが理論的に無理なので、斜め方向もサポートする。
## directionKeys の 8方向対応計画(4方向互換維持)
### Summary
window_hints.directionKeys を 4方向(left/down/up/right)から 8方向へ拡張し、斜め方向(upLeft/upRight/downLeft/downRight)を追加する。
既存4方向の挙動は維持し、斜め方向のみ新規ロジック(2軸合成ギャップ優先)を導入する。
### 公開インターフェース変更
directionKeys に以下4キーを追加:
- upLeft
- upRight
- downLeft
- downRight
例:
directionKeys = {
left = "h",
down = "j",
up = "k",
right = "l",
upLeft = "y",
upRight = "u",
downLeft = "b",
downRight = "n",
}
互換性:
- 既存4キーのみの設定はそのまま有効
- 既存の重複キーエラー判定(同じキーを複数方向に割当)は8方向全体で適用
### 実装方針
1. 方向定義テーブルの導入
- ALL_DIRECTIONS = { left, down, up, right, upLeft, upRight, downLeft, downRight } を定義
- normalizeDirectionKeys / buildDirectionKeyLookup の対象を ALL_DIRECTIONS に拡張
2. 候補判定の拡張
- 4方向: 既存ロジック維持(中心比較)
- 斜め4方向: 象限判定
- upLeft: to.cx < from.cx and to.cy < from.cy
- upRight: to.cx > from.cx and to.cy < from.cy
- downLeft: to.cx < from.cx and to.cy > from.cy
- downRight: to.cx > from.cx and to.cy > from.cy
3. 斜め方向のスコア計算(新規)
- xGap(左右エッジの分離距離、重なり時0)
- yGap(上下エッジの分離距離、重なり時0)
- diagGap = xGap + yGap を最優先
- 次順位:
1. diagGap 昇順
2. 前面順(orderedWindows)
3. 中心点距離(2乗)昇順
4. previousWindow 優先
5. window id 昇順
4. 4方向ロジックとの共存
- findDirectionalWindowTarget 内で
- カーディナル(4方向): 現行の比較順(副軸重なり→主軸距離→前面順→副軸ずれ→previous→id)
- 斜め(4方向): 上記 diagGap 比較順
を分岐する
5. 完全遮蔽除外はそのまま適用
- 既に入っている filterDirectionalCandidatesByOcclusion(...) を斜め方向でも共通適用
- runDirectionalAction の処理フローは変更せず、方向文字列の種類だけ増やす
### テスト計画
spec/window_hints_spec.lua に追加/更新:
1. 設定/バリデーション
- directionKeys.upLeft 等が受理される
- 8方向内でキー重複時にエラーになる
2. 斜め候補判定
- upRight で右上象限のみ候補になる
- 他象限候補は除外される
3. 斜め選択スコア
- diagGap が小さい候補を選ぶ
- diagGap 同値時に前面順で分岐
- それでも同値時に中心距離、previousWindow、id の順で決まる
4. 回帰
- 既存の4方向図ベーステスト(左図/右図)を維持
- 完全遮蔽除外テストを維持
- busted 全件成功
### ドキュメント更新
README.ja.md / README.md を更新:
- directionKeys 説明を「8方向対応」に変更
- 設定例に斜めキーを追加
- ナビゲーション仕様に「斜めは2軸合成ギャップ優先」を追記
### Assumptions / Defaults
- 斜めキー名は upLeft/upRight/downLeft/downRight
- 斜めの優先度は diagGap(xGap + yGap)最優先
- 新しい設定項目は追加しない(directionKeys 拡張のみ)