[[🦉JINRAI]]のプランメモ。
> [!left-bubble] ![[chappy.webp]]
[[Codex CLI]] x [[GPT-5.3-Codex]] のプランです。
## Window Hints: 修飾キー指定で「現在ウィンドウと対象ウィンドウの位置・サイズ入れ替え」機能を追加
### 概要
Window Hints でヒントキー確定時に、指定した修飾キー条件を満たす場合のみ、現在アクティブなウィンドウと対象ウィンドウの frame(位置・サイズ)を入れ替える。
デフォルトは無効。ユーザーは設定で修飾キーを自由に選べるようにする。
### 追加・変更する公開IF
1. window_hints オプションに新規追加
- swapWindowFrameSelectModifiers
- 型: nil | string[]
- デフォルト: nil(無効)
- 例:
- { "shift" }
- { "ctrl" }
- { "cmd", "alt" }
2. 旧案の swapWindowFrameOnShiftSelect は採用しない(導入しない)
### オプション仕様(決定事項)
1. swapWindowFrameSelectModifiers が nil の場合
- 常に通常選択(現行挙動)
2. swapWindowFrameSelectModifiers が配列の場合
- 各要素は hs.hotkey で使う修飾キー文字列(shift / ctrl / alt / cmd / fn)を想定
- 入力は小文字へ正規化
- 重複はエラー(設定ミスを早期検出)
- 空配列はエラー(意図不明を防ぐ)
3. 発動条件
- 「確定に使ったキーイベント」の押下修飾キー集合と swapWindowFrameSelectModifiers が完全一致した場合のみ swap
- 完全一致のため、余計な修飾キーが同時押しされている場合は swap しない
### 実装方針(ファイル別)
1. window_hints.lua
- DEFAULT_CONFIG に swapWindowFrameSelectModifiers = nil を追加
- normalize... 系に修飾キー配列の正規化・検証関数を追加
- modal:bind を「想定修飾キー組み合わせごと」に明示バインドする構造へ変更
- handleInputKey に「今回の入力で swap 対象かどうか」の情報を渡せるようにする
- selectWindow(win, opts) に opts.swapWithFocused を追加し、true 時のみ swap ロジック実行
2. swap ロジック詳細(selectWindow 内)
- focused = hs.window.focusedWindow() を取得
- ガード:
- focused or win が nil -> 通常選択へフォールバック
- focused:id() == win:id() -> 通常選択
- どちらかの frame() が取れない -> 通常選択
- 実行順:
- focusedFrame = focused:frame()
- targetFrame = win:frame()
- focused:setFrame(targetFrame)
- win:setFrame(focusedFrame)
- win:focus()
- 既存後処理(centerCursor, onSelect(win), closeHints(true))は現行同等で維持
3. 入力バインドの設計
- 既存: modal:bind({}, key, ...) と modal:bind({ "shift" }, key, ...)
- 変更後:
- 通常入力用: {}
- swap入力用: normalizedSwapModifiers
- 上記2種類を同一キーへバインドし、それぞれ handleInputKey(key, isSwap) で処理
- #key == 1 条件は継続(文字キーだけ修飾付きバインドを作る)
### 影響範囲
- 対象: ヒントキーによるウィンドウ確定
- 非対象: focusBackKey, directionKeys, escape, delete の挙動
- 既存設定との後方互換:
- 既存ユーザーは設定追加しない限り一切変化なし
### README更新
1. README.md
- Window Hints Options に swapWindowFrameSelectModifiers を追加
- 説明:
- nil で無効
- 指定修飾キー一致時に frame swap
- 完全一致条件で判定
2. README.ja.md
- 同内容を日本語で追加
- 設定例に { "shift" } の例を追記
### テスト計画
1. spec/window_hints_spec.lua にユニットテスト追加(_test 公開ヘルパーを必要最小限追加)
- 修飾キー設定の正規化
- nil は許容
- 小文字化
- 重複エラー
- 空配列エラー
- swap判定
- 設定 nil なら false
- 完全一致で true
- 余分修飾があれば false
- swap実行
- 正常時に双方 setFrame が呼ばれる
- 同一ウィンドウ時は呼ばれない
- frame取得不可時は呼ばれない
- 実行後に対象ウィンドウへ focus される
2. 回帰
- 既存 window_hints_spec.lua の既存ケースが維持されること
- リポジトリルートで busted を実行し全体パス
### 受け入れ基準
1. デフォルト状態(swapWindowFrameSelectModifiers=nil)で現行挙動と完全一致
2. swapWindowFrameSelectModifiers を設定したときのみ、該当修飾キー一致で swap が発動
3. swap 後は対象ウィンドウがアクティブ
4. focusBackKey / directionKeys / 通常選択の挙動に回帰なし
5. README(英日)に新オプションが明記されている
### 前提・デフォルト
- Hammerspoon 側で frame() / setFrame() / focus() が利用可能であること
- swap不能なケースはエラーを投げず通常選択へフォールバック
- onSelect のシグネチャは変更しない(onSelect(win) のまま)
## Window Hints: focusBackKey / directionKeys でも swap 修飾を有効化(A: トグル優先)
### 概要
既存の swapWindowFrameSelectModifiers をヒント確定だけでなく、focusBackKey と directionKeys にも適用する。
focusBackKey + swap は トグル優先 で実装し、要件どおり以下を保証する。
- 例: current=A, previous=B で swap+focusBack
- A と B の frame を入れ替える
- B をアクティブにする
- 直前履歴を A にする(次回 focusBack で戻れる)
### 変更対象
- window_hints.lua
- spec/window_hints_spec.lua
- 必要なら README(仕様追記のみ)
### 実装方針
1. 入力経路の統一
- handleInputKey(key, inputModifiers) で先に swapWithFocused を算出。
- その値を hint / direction / focusBack すべてへ渡す。
2. directionKeys の swap 対応
- runDirectionalAction(direction, swapWithFocused) に拡張。
- target 決定後は selectWindow(target, { swapWithFocused = swapWithFocused }) を使う。
- 既存のターゲット探索優先順位は不変。
3. focusBackKey の swap 対応(A: トグル優先)
- runFocusBackAction(swapWithFocused) に拡張。
- swapWithFocused=false: 現行どおり focusHistory:focusBack()。
- swapWithFocused=true:
- prev = focusHistory:getPreviousWindow() を取得。
- prev が有効なら selectWindow(prev, { swapWithFocused = true }) を実行。
- これにより OS のフォーカスイベントで current/previous が自然に反転し、トグル性を維持。
- prev 無効時は focusBack() フォールバック(既存互換)。
4. 修飾キーバインドの適用範囲拡張
- 現在の「文字キー限定」の追加修飾バインドを廃止。
- focusBackKey / directionKeys に対しても swapWindowFrameSelectModifiers の組をバインド。
- 既存互換のため {} バインドは維持。
5. 既存 swap 実装の利用
- frame 適用は既存の setFrameWithWorkarounds(..., 0) 優先ロジックを流用。
- 失敗時フォールバック/ロールバック方針は現状維持。
### 公開IF
- 追加なし(既存 swapWindowFrameSelectModifiers を流用)。
- 意味拡張:
- これまで「ヒント確定のみ」だった適用対象を
- 「ヒント確定 + focusBackKey + directionKeys」へ拡張。
### テスト計画
1. window_hints_spec.lua に追加
- directionKeys 経路で swapWithFocused=true が選択処理に伝播すること。
- focusBackKey 経路で swapWithFocused=true 時に getPreviousWindow() 優先分岐へ入ること。
- focusBackKey + swap の要件シナリオ(A/B 入替・B active・prev=A)をモックで検証。
- swapWindowFrameSelectModifiers が focusBackKey / directionKeys 入力でも判定されること。
2. 回帰
- 既存 swap テスト(workaround優先、duration=0、異常系)維持。
- 全体 busted 成功。
### 受け入れ基準
- swapWindowFrameSelectModifiers=nil では既存挙動と完全一致。
- directionKeys + swap修飾 で frame swap + 対象フォーカスが実行される。
- focusBackKey + swap修飾 で要件どおり A/B 入替後に B active、履歴 prev=A になる。
- アニメーションなし(swap適用は duration=0)。
- busted 全緑。
### 前提・デフォルト
- focusHistory が有効な場合に focusBackKey は動作(現状仕様どおり)。
- focusHistory:getPreviousWindow() が nil の場合は安全に既存 focusBack() へフォールバック。
- 競合キー解決(hintChars 予約除外)ルールは変更しない。