Skip to content

Commit 4eea5a8

Browse files
committed
Document future direction for presets
1 parent fc548f4 commit 4eea5a8

1 file changed

Lines changed: 190 additions & 0 deletions

File tree

‎Preset.md‎

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
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

Comments
 (0)