1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
// SPDX-License-Identifier: MIT OR Apache-2.0
//! 2026-09-26: The Config form's editing model: rows, the option picker, and adding, removing and restoring settings.
//!
//! The form's fields live on [`LibState`]; this file holds the behaviour.
//! - Pick: Enter on a field with a closed value set (`lib_fields`) opens a
//! picker instead of a text buffer.
//! - Add: `a` lists every serve flag the form does not already carry.
//! - Remove: `x` un-pins a recipe setting, so the flag is not passed and the
//! server's default applies. The row stays listed, and `x` or Enter
//! restores it.
//!
//! Owner: server tui.
//! Invariants:
//! - An edit, removal or restore is committed only after
//! `Recipe::serve_args_edited` accepts the whole edited config; on failure
//! `error` is set and the overrides and removals are unchanged. Un-adding an
//! added row and `reset_overrides` do not validate.
use crate::tui::lib_fields::{self, FieldSpec};
use crate::tui::lib_keys::Outcome;
use crate::tui::lib_modal::ConfigModal;
use crate::tui::lib_state::{LibState, problem_line};
/// 2026-09-26: One row of the form.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ConfigRow {
pub key: String,
/// 2026-09-26: The override if edited, the recipe value otherwise.
pub value: String,
pub changed: bool,
pub removed: bool,
/// 2026-09-26: Not in the recipe's `defaults:`; added in this form.
pub added: bool,
}
/// 2026-09-26: The closed value set for a form key, or `None` for free text.
fn field_options(key: &str) -> Option<Vec<String>> {
let spec = lib_fields::spec_for_key(key)?;
(!spec.options.is_empty()).then(|| spec.options.clone())
}
impl LibState {
/// 2026-09-26: Every recipe key in key order (`Recipe::defaults` is a `BTreeMap`, not the file's order), removed ones included, then the added settings, also in key order.
pub fn config_rows(&self) -> Vec<ConfigRow> {
let Some(recipe) = self.config_recipe() else {
return Vec::new();
};
let mut rows: Vec<ConfigRow> = recipe
.defaults
.iter()
.map(|(key, value)| {
let edited = self.overrides.get(key);
ConfigRow {
key: key.clone(),
value: edited.unwrap_or(value).clone(),
changed: edited.is_some(),
removed: self.removed.contains(key),
added: false,
}
})
.collect();
rows.extend(
self.overrides
.iter()
.filter(|(key, _)| !recipe.defaults.contains_key(*key))
.map(|(key, value)| ConfigRow {
key: key.clone(),
value: value.clone(),
changed: true,
removed: false,
added: true,
}),
);
rows
}
/// 2026-09-26: Enter on the current row: restore it if removed, open the picker for a closed-set field,
/// else the text editor seeded with the current value.
pub fn open_value_editor(&mut self) -> Outcome {
let Some(row) = self.config_rows().into_iter().nth(self.row) else {
return Outcome::None;
};
if row.removed {
return self.restore_key(&row.key);
}
match field_options(&row.key) {
Some(options) => {
let selected = options.iter().position(|o| *o == row.value).unwrap_or(0);
self.modal = Some(ConfigModal::Options {
key: row.key,
options,
selected,
});
}
None => {
self.edit_buffer = row.value;
self.editing = true;
}
}
Outcome::None
}
/// 2026-09-26: `a`: list every serve flag not already on the form.
///
/// Rows are matched through the flag they render to, so a recipe's
/// `max_model_len` also hides `max_seq_len`.
pub fn open_add_modal(&mut self) -> Outcome {
if self.config_recipe().is_none() {
return Outcome::None;
}
let present: Vec<String> = self
.config_rows()
.iter()
.filter_map(|r| crate::recipe::schema::flag_for(&r.key))
.collect();
let fields: Vec<&'static FieldSpec> = lib_fields::serve_fields()
.iter()
.filter(|s| !present.contains(&s.flag))
.collect();
if fields.is_empty() {
return Outcome::Toast {
text: "every serve flag is already on the form".into(),
error: false,
};
}
self.modal = Some(ConfigModal::Add {
fields,
selected: 0,
help_scroll: 0,
});
Outcome::None
}
/// 2026-09-26: `x`: remove the setting under the cursor, restore a removed one, or drop an added one.
pub fn toggle_removed(&mut self) -> Outcome {
let Some(row) = self.config_rows().into_iter().nth(self.row) else {
return Outcome::None;
};
if row.added {
self.overrides.remove(&row.key);
self.error = None;
self.row = self.row.min(self.config_rows().len().saturating_sub(1));
return Outcome::Toast {
text: format!("{} removed", row.key),
error: false,
};
}
if row.removed {
return self.restore_key(&row.key);
}
let Some(recipe) = self.config_recipe().cloned() else {
return Outcome::None;
};
let mut overrides = self.overrides.clone();
overrides.remove(&row.key);
let mut removed = self.removed.clone();
removed.insert(row.key.clone());
match recipe.serve_args_edited(&overrides, &removed) {
Ok(_) => {
self.overrides = overrides;
self.removed = removed;
self.error = None;
let text = match lib_fields::spec_for_key(&row.key).and_then(|s| s.default.clone())
{
Some(d) => format!("{} removed — server default {d} applies", row.key),
None => format!("{} removed — the flag is not passed", row.key),
};
Outcome::Toast { text, error: false }
}
Err(e) => {
self.error = Some(problem_line(&format!("{e:#}")));
Outcome::None
}
}
}
fn restore_key(&mut self, key: &str) -> Outcome {
let Some(recipe) = self.config_recipe().cloned() else {
return Outcome::None;
};
let mut removed = self.removed.clone();
removed.remove(key);
// 2026-09-26: Validated too: an override on another row may be legal only without this flag.
match recipe.serve_args_edited(&self.overrides, &removed) {
Ok(_) => {
self.removed = removed;
self.error = None;
Outcome::Toast {
text: format!("{key} restored"),
error: false,
}
}
Err(e) => {
self.error = Some(problem_line(&format!("{e:#}")));
Outcome::None
}
}
}
/// 2026-09-26: Commit the edit buffer through `LibState::try_set`, which validates the whole config.
///
/// The whole config, because `validate_serve_args` checks flags against
/// each other (for example `--ep-size` against `--world-size`). An empty
/// buffer is refused; for a pending add it abandons the add.
pub fn commit_edit(&mut self) {
self.editing = false;
let raw = self.edit_buffer.trim().to_string();
if let Some(key) = self.pending_add.take() {
if raw.is_empty() {
self.error = Some(format!("{key} was not added — no value given"));
return;
}
if self.try_set(&key, &raw) {
self.select_row(&key);
}
return;
}
let Some(row) = self.config_rows().into_iter().nth(self.row) else {
return;
};
if raw.is_empty() {
self.error = Some(format!("{} must not be empty", row.key));
return;
}
self.try_set(&row.key, &raw);
}
/// 2026-09-26: Leave editing with nothing committed, dropping a pending add.
pub fn cancel_edit(&mut self) {
self.editing = false;
self.edit_buffer.clear();
self.pending_add = None;
}
/// 2026-09-26: Commit `overrides ∪ {key: raw}` if `serve_args_edited` accepts it; else set `error`.
///
/// Returns whether the value was committed.
pub(super) fn try_set(&mut self, key: &str, raw: &str) -> bool {
let Some(recipe) = self.config_recipe().cloned() else {
return false;
};
let mut candidate = self.overrides.clone();
candidate.insert(key.to_string(), raw.to_string());
match recipe.serve_args_edited(&candidate, &self.removed) {
Ok(_) => {
self.overrides = candidate;
self.error = None;
true
}
Err(e) => {
self.error = Some(problem_line(&format!("{e:#}")));
false
}
}
}
/// 2026-09-26: Put the cursor on `key`'s row, if it is on the form.
pub(super) fn select_row(&mut self, key: &str) {
if let Some(i) = self.config_rows().iter().position(|r| r.key == key) {
self.row = i;
}
}
/// 2026-09-26: Back to the recipe: drop every override, removal, pending add, borrow record and error.
pub fn reset_overrides(&mut self) {
self.overrides.clear();
self.removed.clear();
self.pending_add = None;
self.borrowed = None;
self.error = None;
}
/// 2026-09-26: The argv this form would launch, from the current overrides and removals.
pub fn preview_argv(&self) -> Option<Vec<String>> {
self.config_recipe()?
.argv_edited(&self.overrides, &self.removed)
.ok()
}
}
#[cfg(test)]
#[path = "lib_config_tests.rs"]
mod tests;