netget/tui/modal/
text_editor.rs1use tui_textarea::TextArea;
9
10use crate::tui::hit::ModalAction;
11
12pub struct TextEditorModel {
13 pub label: String,
14 pub help: String,
15 pub textarea: TextArea<'static>,
16 pub json: bool,
18 pub error: Option<String>,
19 pub focused_button: Option<usize>,
21}
22
23impl std::fmt::Debug for TextEditorModel {
24 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25 f.debug_struct("TextEditorModel")
26 .field("label", &self.label)
27 .field("json", &self.json)
28 .field("error", &self.error)
29 .field("focused_button", &self.focused_button)
30 .finish()
31 }
32}
33
34impl Clone for TextEditorModel {
35 fn clone(&self) -> Self {
36 Self {
37 label: self.label.clone(),
38 help: self.help.clone(),
39 textarea: self.textarea.clone(),
40 json: self.json,
41 error: self.error.clone(),
42 focused_button: self.focused_button,
43 }
44 }
45}
46
47impl TextEditorModel {
48 pub fn new(label: &str, help: &str, initial: &str, json: bool) -> Self {
49 let lines: Vec<String> = if initial.is_empty() {
50 vec![String::new()]
51 } else {
52 initial.split('\n').map(|l| l.to_string()).collect()
53 };
54 Self {
55 label: label.to_string(),
56 help: help.to_string(),
57 textarea: TextArea::new(lines),
58 json,
59 error: None,
60 focused_button: None,
61 }
62 }
63
64 pub fn buttons(&self) -> Vec<ModalAction> {
66 vec![ModalAction::EditorAccept, ModalAction::EditorCancel]
67 }
68
69 pub fn focused_action(&self) -> Option<ModalAction> {
70 self.focused_button
71 .and_then(|index| self.buttons().get(index).copied())
72 }
73
74 pub fn cycle_focus(&mut self, backward: bool) {
76 let buttons = self.buttons().len();
77 let current = self.focused_button.map(|i| i + 1).unwrap_or(0);
79 let total = buttons + 1;
80 let next = if backward {
81 (current + total - 1) % total
82 } else {
83 (current + 1) % total
84 };
85 self.focused_button = if next == 0 { None } else { Some(next - 1) };
86 }
87
88 pub fn text(&self) -> String {
89 self.textarea.lines().join("\n")
90 }
91
92 pub fn accept(&mut self) -> Option<String> {
94 let text = self.text();
95 if self.json && !text.trim().is_empty() {
96 if let Err(e) = serde_json::from_str::<serde_json::Value>(&text) {
97 self.error = Some(format!("invalid JSON: {e}"));
98 return None;
99 }
100 }
101 self.error = None;
102 Some(text)
103 }
104}