1use cli_core::render::{RenderError, WireValue, parse_wire, reparse_list};
8use ratatui::Frame;
9use ratatui::layout::{Constraint, Rect};
10use ratatui::style::{Color, Modifier, Style};
11use ratatui::widgets::{Block, Borders, Cell, Row, Table, TableState};
12use scripting::nomiscript::{Value, format_value, list_to_vec};
13
14pub const KNOWN_CONFIG_KEYS: &[&str] = &["locale", "userregistrytimeout"];
16
17#[derive(Debug, Clone, PartialEq)]
19pub enum ConfigCell {
20 Loading { id: i64 },
22 Loaded(String),
24 Unset,
26 Error(String),
28}
29
30pub struct ConfigTab {
31 pub entries: Vec<(String, ConfigCell)>,
32 pub selected: usize,
33}
34
35impl ConfigTab {
36 #[must_use]
37 pub fn new() -> Self {
38 Self {
39 entries: KNOWN_CONFIG_KEYS
40 .iter()
41 .map(|k| ((*k).to_string(), ConfigCell::Unset))
42 .collect(),
43 selected: 0,
44 }
45 }
46
47 #[must_use]
50 pub fn is_idle(&self) -> bool {
51 self.entries
52 .iter()
53 .all(|(_, c)| matches!(c, ConfigCell::Unset))
54 }
55
56 pub fn set_loading(&mut self, key: &str, id: i64) {
58 if let Some((_, cell)) = self.entries.iter_mut().find(|(k, _)| k == key) {
59 *cell = ConfigCell::Loading { id };
60 }
61 }
62
63 pub fn on_reply(&mut self, key: &str, wire: &str) {
68 let new_state = parse_config_reply(wire);
69 if let Some((_, cell)) = self.entries.iter_mut().find(|(k, _)| k == key) {
70 *cell = new_state;
71 }
72 }
73
74 pub fn reset(&mut self) {
76 for (_, cell) in &mut self.entries {
77 *cell = ConfigCell::Unset;
78 }
79 }
80
81 #[must_use]
83 pub fn any_loading(&self) -> bool {
84 self.entries
85 .iter()
86 .any(|(_, c)| matches!(c, ConfigCell::Loading { .. }))
87 }
88
89 #[must_use]
91 pub fn selected_key(&self) -> Option<&str> {
92 self.entries.get(self.selected).map(|(k, _)| k.as_str())
93 }
94
95 #[must_use]
97 pub fn selected_value(&self) -> &str {
98 match self.entries.get(self.selected) {
99 Some((_, ConfigCell::Loaded(v))) => v.as_str(),
100 _ => "",
101 }
102 }
103
104 pub fn select_next(&mut self) {
105 if self.selected + 1 < self.entries.len() {
106 self.selected += 1;
107 }
108 }
109
110 pub fn select_prev(&mut self) {
111 self.selected = self.selected.saturating_sub(1);
112 }
113
114 pub fn draw(&self, frame: &mut Frame, area: Rect) {
115 let block = Block::default().borders(Borders::ALL).title("Config");
116 let header_style = Style::default().add_modifier(Modifier::BOLD);
117 let selected_style = Style::default().bg(Color::DarkGray);
118
119 let header = Row::new(vec![
120 Cell::from("Key").style(header_style),
121 Cell::from("Value").style(header_style),
122 ]);
123
124 let rows: Vec<Row> = self
125 .entries
126 .iter()
127 .enumerate()
128 .map(|(i, (key, cell))| {
129 let display: &str = match cell {
130 ConfigCell::Loaded(v) => v.as_str(),
131 ConfigCell::Loading { .. } => "(loading\u{2026})",
132 ConfigCell::Unset => "(unset)",
133 ConfigCell::Error(e) => e.as_str(),
134 };
135 let style = if i == self.selected {
136 selected_style
137 } else {
138 Style::default()
139 };
140 Row::new(vec![Cell::from(key.as_str()), Cell::from(display)]).style(style)
141 })
142 .collect();
143
144 let widths = [Constraint::Percentage(40), Constraint::Percentage(60)];
145 let table = Table::new(rows, widths).header(header).block(block);
146 let mut state = TableState::default().with_selected(Some(self.selected));
147 frame.render_stateful_widget(table, area, &mut state);
148 }
149}
150
151impl Default for ConfigTab {
152 fn default() -> Self {
153 Self::new()
154 }
155}
156
157pub fn build_set_config_form(key: &str, value: &str) -> String {
159 use cli_core::eval::escape_str;
160 format!("(set-config {} {})", escape_str(key), escape_str(value))
161}
162
163fn parse_config_reply(wire: &str) -> ConfigCell {
164 match parse_wire(wire) {
169 Err(RenderError::Server { code, message }) => {
170 ConfigCell::Error(format!("[{code}] {message}"))
171 }
172 Err(e) => ConfigCell::Error(e.to_string()),
173 Ok(WireValue::Value(Value::String(ref s))) => extract_config_value(s),
174 Ok(WireValue::Value(ref other)) => {
175 ConfigCell::Error(format!("unexpected reply type: {}", format_value(other)))
176 }
177 }
178}
179
180fn extract_config_value(plist_str: &str) -> ConfigCell {
181 let parsed = match reparse_list(plist_str) {
182 Ok(v) => v,
183 Err(e) => return ConfigCell::Error(format!("parse error: {e}")),
184 };
185 match plist_config_value(&parsed) {
186 Some(Value::Nil) => ConfigCell::Unset,
188 Some(Value::String(s)) => ConfigCell::Loaded(s),
189 Some(other) => ConfigCell::Loaded(format_value(&other)),
190 None => ConfigCell::Error(format!("missing :config-value in: {plist_str}")),
191 }
192}
193
194fn plist_config_value(value: &Value) -> Option<Value> {
195 let items = list_to_vec(value)?;
196 let pos = items
197 .iter()
198 .position(|v| matches!(v, Value::Symbol(s) if s == ":config-value"))?;
199 items.get(pos + 1).cloned()
200}
201
202#[cfg(test)]
203mod tests;