|
| 1 | +# preset の今後の方針 |
| 2 | + |
| 3 | +> [!NOTE] |
| 4 | +> この文書は設計上の提案です。現状と推奨方針を整理するものであり、この文書だけをもって公開 API を非推奨化・削除するものではありません。 |
| 5 | +
|
| 6 | +## 背景 |
| 7 | + |
| 8 | +promkit は今後、再利用可能な widget を増やすことを中心に発展させたいと考えています。現在の workspace は、主に次の3レイヤーで構成されています。 |
| 9 | + |
| 10 | +- [`promkit-core`](./promkit-core/): 描画、レイアウト、terminal への出力、hit testing |
| 11 | +- [`promkit-widgets`](./promkit-widgets/): widget の状態と、状態から view への projection |
| 12 | +- [`promkit`](./promkit/): `Prompt` のライフサイクルと高レベルな preset |
| 13 | + |
| 14 | +現在の責務境界は [`Concept.md`](./Concept.md) に記載されています。特に、widget は event loop のポリシーを持たず、event をどの操作に対応づけるかは application または preset が決定する設計です。 |
| 15 | + |
| 16 | +## 現状 |
| 17 | + |
| 18 | +### preset は複数のポリシーを一体化している |
| 19 | + |
| 20 | +preset は単なる widget の alias ではありません。一般に、次の要素をまとめて提供しています。 |
| 21 | + |
| 22 | +- widget の構築とデフォルトの style |
| 23 | +- key と mouse の binding |
| 24 | +- renderer の初期化と更新 |
| 25 | +- terminal size の取り扱い |
| 26 | +- 終了条件とエラー条件 |
| 27 | +- 最終的な返り値 |
| 28 | +- widget の一部の設定を公開する builder method |
| 29 | + |
| 30 | +これは one-shot prompt としては便利ですが、各 preset が固有の公開 API を持つ E2E の UI 実装になることも意味します。 |
| 31 | + |
| 32 | +### widget と preset は一対一である必要がない |
| 33 | + |
| 34 | +現在の feature 定義でも、widget と preset は一対一ではありません。たとえば `spinner` と `table` は `promkit` の feature として公開されていますが、対応する preset module はありません。 |
| 35 | + |
| 36 | +したがって、widget を追加するたびに preset の設計、実装、ドキュメント、保守まで必須にする必要はありません。 |
| 37 | + |
| 38 | +### preset は widget API の一部を重複して公開している |
| 39 | + |
| 40 | +widget に設定項目を追加した場合、それを preset から利用可能にするために、builder method、ドキュメント、転送処理のテストを追加することがあります。このため、widget 本体とは別に、同期が必要な第二の API surface が生まれます。 |
| 41 | + |
| 42 | +このコストは widget と設定項目の増加に比例して大きくなります。preset 固有の振る舞いが少ない場合でも同様です。 |
| 43 | + |
| 44 | +### 組み込みの event loop が対象とする範囲は狭い |
| 45 | + |
| 46 | +[`Prompt::run`](./promkit/src/lib.rs) は、単純で逐次的な prompt には有用です。terminal の setup と teardown を行い、singleton の event stream から event を読み、終了するまで一つの prompt に渡します。 |
| 47 | + |
| 48 | +一方、規模の大きな terminal application では、次のような application 固有の制御が必要になります。 |
| 49 | + |
| 50 | +- 複数 component 間の focus 管理 |
| 51 | +- background task と channel |
| 52 | +- terminal event と application event を処理する `tokio::select!` |
| 53 | +- resize event の明示的な処理 |
| 54 | +- 設定可能な key binding |
| 55 | +- alternate screen の切り替え |
| 56 | +- application 固有の終了処理とエラー処理 |
| 57 | + |
| 58 | +[`csv` example](./examples/csv/) は、単純な `Prompt` lifecycle を利用しつつ、widget 固有の振る舞いを preset の外側で実装しています。[`repl` example](./examples/repl/) と [jnv](https://github.com/ynqa/jnv) は、さらに進んで独自の event loop を構築しています。 |
| 59 | + |
| 60 | +これらすべての application model を `Prompt::run` に取り込もうとすると、promkit は汎用 TUI framework に近づきます。その結果、高レベルな `promkit` crate の実装コストが増える一方で、widget の再利用性には必ずしも寄与しません。 |
| 61 | + |
| 62 | +## 評価 |
| 63 | + |
| 64 | +現在の preset model は、widget を中心に機能を増やしていく方針とは相性がよくありません。 |
| 65 | + |
| 66 | +問題は preset が存在すること自体ではなく、すべての有用な widget に対して安定した E2E の preset も提供すべきだという期待です。この期待により、widget の追加が次の要素と結びつきます。 |
| 67 | + |
| 68 | +- 特定の event loop |
| 69 | +- 特定の操作ポリシー |
| 70 | +- 重複した設定 API |
| 71 | +- 追加の example と regression test |
| 72 | + |
| 73 | +複数の操作モデルに参加できる widget や、高度な利用者を対象とする場合は、application 側で widget を組み立てる方が適しています。 |
| 74 | + |
| 75 | +## 推奨方針 |
| 76 | + |
| 77 | +### widget ごとの preset を必須にしない |
| 78 | + |
| 79 | +新しい widget は、対応する `promkit::preset` module がなくても、`promkit-widgets` と `promkit-core` から完全に利用できる状態を目指します。 |
| 80 | + |
| 81 | +widget の実装では、主に次を完成条件とします。 |
| 82 | + |
| 83 | +- 状態遷移と意味的な操作 |
| 84 | +- 状態から view への projection |
| 85 | +- 必要に応じた viewport の処理 |
| 86 | +- 必要に応じた hit testing |
| 87 | +- 対象を絞った unit test と rendering test |
| 88 | + |
| 89 | +デフォルトの event loop や key binding 自体が開発対象でない限り、それらを widget の完成条件には含めません。 |
| 90 | + |
| 91 | +### E2E の組み立ては recipe に移す |
| 92 | + |
| 93 | +デフォルトの key binding や完全な対話フローは、主に compile・test される example として提供します。example であれば、特定の方針を持つ構成を示しつつ、それを永続的な library abstraction にする必要がありません。 |
| 94 | + |
| 95 | +recipe では、少なくとも次の二つの integration level を扱います。 |
| 96 | + |
| 97 | +1. CSV viewer のように、単純な `Prompt` lifecycle を利用する小規模な application |
| 98 | +2. REPL のように、`promkit-core` と `promkit-widgets` を利用して独自の event loop を構築する application |
| 99 | + |
| 100 | +### 複雑な application に合わせて組み込み loop を汎用化しない |
| 101 | + |
| 102 | +組み込みの event loop は、逐次的な one-shot interaction のための小さな convenience API として残せます。ただし、jnv のような application まで表現できるように汎用化することは目指しません。 |
| 103 | + |
| 104 | +複雑な application は、crossterm、Tokio、channel、promkit の描画 primitive を直接組み合わせられる状態を維持します。 |
| 105 | + |
| 106 | +### 小さく、ポリシーを持たない支援 API を優先する |
| 107 | + |
| 108 | +example 間で機械的なコードの重複が確認できた場合は、次のような限定的な helper の抽出を検討します。 |
| 109 | + |
| 110 | +- raw mode、cursor visibility、mouse capture、alternate screen を復元する RAII terminal session guard |
| 111 | +- 特定の action を割り当てず、共通する key press の形だけを判定する helper |
| 112 | +- terminal backend の差異に対応する event normalization utility |
| 113 | +- 単純な runner に event source を注入する仕組み |
| 114 | +- 使用例の蓄積によって安定した境界が確認できた場合の、小さな renderer 初期化・更新 helper |
| 115 | + |
| 116 | +これらの helper は terminal と event の plumbing を減らすものであり、どの key で widget を移動、選択、確定、終了させるかは決定しません。 |
| 117 | + |
| 118 | +## 目標とする責務境界 |
| 119 | + |
| 120 | +### `promkit-core` |
| 121 | + |
| 122 | +- terminal の描画と復元に関する primitive |
| 123 | +- layout と viewport の計算 |
| 124 | +- renderer の保持と更新 |
| 125 | +- hit testing と座標変換 |
| 126 | + |
| 127 | +### `promkit-widgets` |
| 128 | + |
| 129 | +- widget の状態 |
| 130 | +- 移動、toggle、挿入、scroll などの意味的な状態操作 |
| 131 | +- 状態から grapheme への projection |
| 132 | +- widget 固有の設定 |
| 133 | + |
| 134 | +widget は引き続き、event loop とデフォルトの global key binding を所有しません。 |
| 135 | + |
| 136 | +### `promkit` |
| 137 | + |
| 138 | +- 単純な prompt のための、任意で利用できる小さな runtime |
| 139 | +- 明確な価値がある場合の re-export または integration helper |
| 140 | +- widget ごとの E2E wrapper は提供要件としない |
| 141 | + |
| 142 | +### `examples` |
| 143 | + |
| 144 | +- 完全な event mapping |
| 145 | +- 複数 widget の組み立て |
| 146 | +- application の終了ポリシー |
| 147 | +- terminal mode の選択 |
| 148 | +- 単純な application と高度な application の実用的な recipe |
| 149 | + |
| 150 | +## プロダクト上のトレードオフ |
| 151 | + |
| 152 | +preset を削除すると、`Readline`、`Confirm`、`Password` のように、少量のコードで結果を取得できる利便性も失われます。これらは、一貫した one-shot のユーザー体験を表現しているため、viewer 系の preset よりも独立したプロダクトとしての性質が強いものです。 |
| 153 | + |
| 154 | +すべての preset を削除する前に、次のどちらを目指すか明示的に決める必要があります。 |
| 155 | + |
| 156 | +- **toolkit のみにする:** E2E の prompt API の保守を終了し、すべての操作ポリシーを example に移す |
| 157 | +- **二層構成にする:** widget ごとの preset は約束せず、少数の one-shot prompt のみを、必要であれば別 crate で維持する |
| 158 | + |
| 159 | +widget を中心とし、E2E の prompt behavior を保守しない方針であれば、長期的には toolkit のみにする方が責務境界は明確です。preset を別 crate に移す方法は利用者の利便性を維持できますが、保守コストの多くも残ります。 |
| 160 | + |
| 161 | +## 移行時の考慮事項 |
| 162 | + |
| 163 | +preset の削除は `promkit/src/preset/` directory だけでは完結しません。 |
| 164 | + |
| 165 | +- README の example は、現在 preset を主要機能として紹介しています。 |
| 166 | +- `promkit/Cargo.toml` の feature wiring には、preset の feature と widget の forwarding feature が混在しています。 |
| 167 | +- `suggest` と `validate` は主に Readline 関連の preset から使われているため、移動先または代替が必要です。 |
| 168 | +- [`promkit-derive`](./promkit-derive/) は `promkit::preset::form::Form` を直接構築するコードを生成しています。 |
| 169 | +- Readline の terminal regression test は、現在 preset API を対象にしています。 |
| 170 | + |
| 171 | +これらは preset directory だけを削除するのではなく、意図的な breaking change の一部として対応する必要があります。 |
| 172 | + |
| 173 | +## 移行手順案 |
| 174 | + |
| 175 | +1. widget の追加に preset の追加は必須ではないことを明文化する |
| 176 | +2. JSON、Listbox、Readline などの代表的な example を、widget を直接組み立てる形に変更する |
| 177 | +3. それらの example から機械的な重複を特定し、安定した、ポリシーを持たない helper だけを抽出する |
| 178 | +4. one-shot prompt をサポート対象として残すか決定する |
| 179 | +5. preset API を非推奨化し、同等の recipe を示す migration guide を公開する |
| 180 | +6. `promkit-derive` を再設計または終了し、preset 固有の support module を移動する |
| 181 | +7. 明示的な breaking release で preset を削除し、feature wiring を単純化する |
| 182 | +8. 目標とする architecture が実装された時点で [`Concept.md`](./Concept.md) と README を更新する |
| 183 | + |
| 184 | +## 対象外 |
| 185 | + |
| 186 | +- すべての widget にデフォルトの preset を提供すること |
| 187 | +- key binding のポリシーを widget state に移すこと |
| 188 | +- `Prompt::run` ですべての非同期 TUI architecture を扱うこと |
| 189 | +- example を安定した公開 API として扱うこと |
| 190 | +- 利用例の重複から有用な境界が確認できる前に helper を抽出すること |
0 commit comments