1use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
5use serde_json::{Map, Value};
6use typesafe::{Choice, Noul, Question, Score};
7
8use crate::session::value;
9use crate::words;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum Kind {
13 Noul,
14 Choice,
15 Score,
16}
17
18impl Kind {
19 pub fn label(self) -> &'static str {
20 match self {
21 Kind::Noul => "noul",
22 Kind::Choice => "choice",
23 Kind::Score => "score",
24 }
25 }
26
27 pub fn about(self) -> &'static str {
28 match self {
29 Kind::Noul => "probability that the statement is true (0–1)",
30 Kind::Choice => "one label out of the set you define",
31 Kind::Score => "a weighted position along ordered levels",
32 }
33 }
34
35 fn next(self) -> Self {
36 match self {
37 Kind::Noul => Kind::Choice,
38 Kind::Choice => Kind::Score,
39 Kind::Score => Kind::Noul,
40 }
41 }
42
43 fn prev(self) -> Self {
44 self.next().next()
45 }
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum Field {
51 State,
52 Name,
53 Kind,
54 Instructions,
55 Yes,
57 No,
58 OptionLabel(usize),
60 OptionDesc(usize),
61 Level(usize),
63}
64
65pub enum Outcome {
67 Open,
69 Cancel,
71 Commit(String, Box<Question>, String),
73}
74
75pub struct Builder {
76 pub state: String,
77 pub name: String,
78 pub kind: Kind,
79 pub instructions: String,
80 pub yes: String,
81 pub no: String,
82 pub options: Vec<(String, String)>,
83 pub levels: Vec<String>,
84 pub focus: usize,
85 pub cursor: usize,
86 pub message: Option<String>,
87 pub existing: Vec<String>,
89 replace_armed: bool,
91}
92
93impl Builder {
94 pub fn new(state: String, name: &str) -> Self {
97 let focus = if !name.is_empty() {
98 2
99 } else if state.trim().is_empty() {
100 0
101 } else {
102 1
103 };
104 Self {
105 state: state.clone(),
106 name: name.to_owned(),
107 kind: Kind::Noul,
108 instructions: String::new(),
109 yes: String::new(),
110 no: String::new(),
111 options: vec![
112 (String::new(), String::new()),
113 (String::new(), String::new()),
114 ],
115 levels: vec![String::new(), String::new(), String::new()],
116 focus,
117 cursor: if focus == 0 {
118 state.chars().count()
119 } else {
120 name.chars().count()
121 },
122 message: None,
123 existing: Vec::new(),
124 replace_armed: false,
125 }
126 }
127
128 pub fn of_kind(mut self, kind: Kind) -> Self {
133 self.kind = kind;
134 self.focus = self.focus.min(self.fields().len() - 1);
135 self
136 }
137
138 pub fn over(mut self, existing: Vec<String>) -> Self {
140 self.existing = existing;
141 self
142 }
143
144 pub fn fields(&self) -> Vec<Field> {
146 let mut f = vec![Field::State, Field::Name, Field::Kind, Field::Instructions];
147 match self.kind {
148 Kind::Noul => f.extend([Field::Yes, Field::No]),
149 Kind::Choice => {
150 for i in 0..self.options.len() {
151 f.push(Field::OptionLabel(i));
152 f.push(Field::OptionDesc(i));
153 }
154 }
155 Kind::Score => f.extend((0..self.levels.len()).map(Field::Level)),
156 }
157 f
158 }
159
160 pub fn focused(&self) -> Field {
161 let fields = self.fields();
162 fields[self.focus.min(fields.len() - 1)]
163 }
164
165 pub fn text(&self, field: Field) -> &str {
166 match field {
167 Field::State => &self.state,
168 Field::Name => &self.name,
169 Field::Instructions => &self.instructions,
170 Field::Yes => &self.yes,
171 Field::No => &self.no,
172 Field::OptionLabel(i) => self.options.get(i).map(|o| o.0.as_str()).unwrap_or(""),
173 Field::OptionDesc(i) => self.options.get(i).map(|o| o.1.as_str()).unwrap_or(""),
174 Field::Level(i) => self.levels.get(i).map(String::as_str).unwrap_or(""),
175 Field::Kind => self.kind.label(),
176 }
177 }
178
179 fn text_mut(&mut self, field: Field) -> Option<&mut String> {
180 match field {
181 Field::State => Some(&mut self.state),
182 Field::Name => Some(&mut self.name),
183 Field::Instructions => Some(&mut self.instructions),
184 Field::Yes => Some(&mut self.yes),
185 Field::No => Some(&mut self.no),
186 Field::OptionLabel(i) => self.options.get_mut(i).map(|o| &mut o.0),
187 Field::OptionDesc(i) => self.options.get_mut(i).map(|o| &mut o.1),
188 Field::Level(i) => self.levels.get_mut(i),
189 Field::Kind => None,
190 }
191 }
192
193 pub fn key(&mut self, key: KeyEvent) -> Outcome {
194 let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
195 let shift = key.modifiers.contains(KeyModifiers::SHIFT);
196 self.message = None;
197 if self.focused() != Field::Kind
199 && (words::is_word_left(&key) || words::is_word_right(&key))
200 {
201 let chars: Vec<char> = self.text(self.focused()).chars().collect();
202 self.cursor = if words::is_word_left(&key) {
203 words::word_left(&chars, self.cursor)
204 } else {
205 words::word_right(&chars, self.cursor)
206 };
207 return Outcome::Open;
208 }
209 match key.code {
210 KeyCode::Esc => return Outcome::Cancel,
211 KeyCode::Char('s') if ctrl => return self.commit(),
212 KeyCode::Char('x') if ctrl => self.delete_row(),
213 KeyCode::Char('o') if ctrl => self.add_row(),
214 KeyCode::Tab => self.move_focus(1),
215 KeyCode::BackTab => self.move_focus(-1),
216 KeyCode::Down => self.move_focus(1),
217 KeyCode::Up => self.move_focus(-1),
218 KeyCode::Enter => {
219 if self.on_last_row() {
220 self.add_row();
221 }
222 self.move_focus(1);
223 }
224 KeyCode::Left if self.focused() == Field::Kind => self.set_kind(self.kind.prev()),
225 KeyCode::Right if self.focused() == Field::Kind => self.set_kind(self.kind.next()),
226 KeyCode::Char(' ') if self.focused() == Field::Kind => self.set_kind(self.kind.next()),
227 KeyCode::Char(c) if self.focused() == Field::Kind => match c {
228 'n' | 'N' => self.set_kind(Kind::Noul),
229 'c' | 'C' => self.set_kind(Kind::Choice),
230 's' | 'S' => self.set_kind(Kind::Score),
231 _ => {}
232 },
233 KeyCode::Left => self.cursor = self.cursor.saturating_sub(1),
234 KeyCode::Right => self.cursor = (self.cursor + 1).min(self.len()),
235 KeyCode::Home => self.cursor = 0,
236 KeyCode::End => self.cursor = self.len(),
237 KeyCode::Backspace => {
238 if self.cursor > 0 {
239 let at = self.cursor - 1;
240 self.cursor = at;
241 self.remove(at);
242 }
243 }
244 KeyCode::Delete => {
245 let at = self.cursor;
246 self.remove(at);
247 }
248 KeyCode::Char(c) if words::is_typed(&key) => {
249 let _ = shift;
250 self.insert(c);
251 }
252 _ => {}
253 }
254 Outcome::Open
255 }
256
257 fn set_kind(&mut self, kind: Kind) {
258 if kind != self.kind {
259 self.replace_armed = false;
260 }
261 self.kind = kind;
262 self.focus = self.focus.min(self.fields().len() - 1);
263 }
264
265 fn move_focus(&mut self, delta: isize) {
266 let len = self.fields().len() as isize;
267 let next = (self.focus as isize + delta).rem_euclid(len) as usize;
268 self.focus = next;
269 self.cursor = self.len();
270 }
271
272 fn len(&self) -> usize {
273 self.text(self.focused()).chars().count()
274 }
275
276 fn insert(&mut self, c: char) {
277 let cursor = self.cursor;
278 let field = self.focused();
279 if field == Field::Name {
280 self.replace_armed = false;
281 }
282 if let Some(s) = self.text_mut(field) {
283 let at = byte_at(s, cursor);
284 s.insert(at, c);
285 self.cursor = cursor + 1;
286 }
287 }
288
289 fn remove(&mut self, index: usize) {
290 let field = self.focused();
291 if field == Field::Name {
292 self.replace_armed = false;
293 }
294 if let Some(s) = self.text_mut(field)
295 && index < s.chars().count()
296 {
297 let at = byte_at(s, index);
298 s.remove(at);
299 }
300 }
301
302 fn on_last_row(&self) -> bool {
303 match self.focused() {
304 Field::OptionDesc(i) => i + 1 == self.options.len(),
305 Field::Level(i) => i + 1 == self.levels.len(),
306 _ => false,
307 }
308 }
309
310 pub fn add_row(&mut self) {
312 match self.kind {
313 Kind::Choice => self.options.push((String::new(), String::new())),
314 Kind::Score => self.levels.push(String::new()),
315 Kind::Noul => self.message = Some("A noul has only `yes` and `no`.".into()),
316 }
317 }
318
319 pub fn delete_row(&mut self) {
321 match self.focused() {
322 Field::OptionLabel(i) | Field::OptionDesc(i) if self.options.len() > 1 => {
323 self.options.remove(i);
324 }
325 Field::Level(i) if self.levels.len() > 1 => {
326 self.levels.remove(i);
327 }
328 _ => self.message = Some("Nothing to remove here.".into()),
329 }
330 self.focus = self.focus.min(self.fields().len() - 1);
331 self.cursor = self.len();
332 }
333
334 pub fn preview(&self) -> Value {
336 let mut q = Map::new();
337 q.insert("type".into(), Value::String(self.kind.label().into()));
338 if !self.instructions.trim().is_empty() {
339 q.insert("instructions".into(), value(&self.instructions));
340 }
341 match self.kind {
342 Kind::Noul => {
343 let mut criteria = Map::new();
344 if !self.yes.trim().is_empty() {
345 criteria.insert("true".into(), value(&self.yes));
346 }
347 if !self.no.trim().is_empty() {
348 criteria.insert("false".into(), value(&self.no));
349 }
350 if !criteria.is_empty() {
351 q.insert("criteria".into(), Value::Object(criteria));
352 }
353 }
354 Kind::Choice => {
355 let criteria: Map<String, Value> = self
356 .options
357 .iter()
358 .filter(|(label, _)| !label.trim().is_empty())
359 .map(|(label, desc)| {
360 let desc = if desc.trim().is_empty() {
361 Value::Null
362 } else {
363 value(desc)
364 };
365 (label.trim().to_owned(), desc)
366 })
367 .collect();
368 q.insert("criteria".into(), Value::Object(criteria));
369 }
370 Kind::Score => {
371 let criteria: Vec<Value> = self
372 .levels
373 .iter()
374 .filter(|l| !l.trim().is_empty())
375 .map(|l| value(l))
376 .collect();
377 q.insert("criteria".into(), Value::Array(criteria));
378 }
379 }
380 let name = if self.name.trim().is_empty() {
381 "<name>"
382 } else {
383 self.name.trim()
384 };
385 serde_json::json!({ name: Value::Object(q) })
386 }
387
388 pub fn as_command(&self) -> String {
390 let name = if self.name.trim().is_empty() {
391 "<name>"
392 } else {
393 self.name.trim()
394 };
395 let instructions = self.instructions.trim();
396 match self.kind {
397 Kind::Noul => {
398 let mut s = format!(":noul {name} {instructions}");
399 if !self.yes.trim().is_empty() {
400 s.push_str(&format!(" | yes: {}", self.yes.trim()));
401 }
402 if !self.no.trim().is_empty() {
403 s.push_str(&format!(" | no: {}", self.no.trim()));
404 }
405 s
406 }
407 Kind::Choice => {
408 let mut s = format!(":choice {name} {instructions}");
409 for (label, desc) in self.options.iter().filter(|(l, _)| !l.trim().is_empty()) {
410 s.push_str(&format!(" | {}", label.trim()));
411 if !desc.trim().is_empty() {
412 s.push_str(&format!("={}", desc.trim()));
413 }
414 }
415 s
416 }
417 Kind::Score => {
418 let mut s = format!(":score {name} {instructions}");
419 for level in self.levels.iter().filter(|l| !l.trim().is_empty()) {
420 s.push_str(&format!(" | {}", level.trim()));
421 }
422 s
423 }
424 }
425 }
426
427 fn commit(&mut self) -> Outcome {
428 let name = self.name.trim().to_owned();
429 if name.is_empty() {
430 self.message = Some("Every question needs a name — answers come back under it.".into());
431 return Outcome::Open;
432 }
433 if name.split_whitespace().count() > 1 {
434 self.message = Some("Names cannot contain spaces.".into());
435 return Outcome::Open;
436 }
437 let instructions = self.instructions.trim();
438 if instructions.is_empty() {
439 self.message = Some("Instructions are what the model actually reads.".into());
440 return Outcome::Open;
441 }
442 if self.existing.iter().any(|n| n == &name) && !self.replace_armed {
443 self.replace_armed = true;
444 self.message = Some(format!(
445 "`{name}` is already a question — Ctrl-S again replaces it, or rename this one."
446 ));
447 return Outcome::Open;
448 }
449 let question: Question = match self.kind {
450 Kind::Noul => {
451 let mut q = Noul::new(value(instructions));
452 if !self.yes.trim().is_empty() {
453 q = q.when_true(value(&self.yes));
454 }
455 if !self.no.trim().is_empty() {
456 q = q.when_false(value(&self.no));
457 }
458 q.into()
459 }
460 Kind::Choice => {
461 let options: Vec<(String, String)> = self
462 .options
463 .iter()
464 .filter(|(l, _)| !l.trim().is_empty())
465 .cloned()
466 .collect();
467 if options.len() < 2 {
468 self.message = Some("A choice needs at least two options.".into());
469 return Outcome::Open;
470 }
471 let mut q = Choice::new(value(instructions));
472 for (label, desc) in options {
473 q = if desc.trim().is_empty() {
474 q.label(label.trim())
475 } else {
476 q.option(label.trim(), value(&desc))
477 };
478 }
479 q.into()
480 }
481 Kind::Score => {
482 let levels: Vec<Value> = self
483 .levels
484 .iter()
485 .filter(|l| !l.trim().is_empty())
486 .map(|l| value(l))
487 .collect();
488 if levels.len() < 2 {
489 self.message = Some("A score needs at least two ordered levels.".into());
490 return Outcome::Open;
491 }
492 Score::new(value(instructions), levels).into()
493 }
494 };
495 Outcome::Commit(name, Box::new(question), self.state.clone())
496 }
497}
498
499fn byte_at(s: &str, char_index: usize) -> usize {
500 s.char_indices()
501 .nth(char_index)
502 .map(|(i, _)| i)
503 .unwrap_or(s.len())
504}