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