1#![forbid(unsafe_code)]
30#![deny(missing_docs)]
31
32mod input;
33mod render;
34
35pub use input::{KeyInput, intent_from_key, palette_intent_from_colon};
36pub use render::render_scene;
37
38use sim_lib_view::SurfaceCaps;
39use sim_lib_view::palette::{Command, palette_scene};
40
41pub fn render_palette(commands: &[Command], filter: &str) -> String {
49 let scene = palette_scene(commands, filter);
50 render_scene(&scene, &cli_caps("tty.palette"))
51}
52
53pub fn cli_caps(client_id: &str) -> SurfaceCaps {
59 SurfaceCaps::from_preset("cli", client_id).expect("cli is a built-in surface preset")
60}
61
62pub fn tui_caps(client_id: &str) -> SurfaceCaps {
67 SurfaceCaps::from_preset("tui", client_id).expect("tui is a built-in surface preset")
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73 use sim_kernel::Expr;
74 use sim_lib_intent::{intent_kind_of, validate_intent};
75 use sim_lib_scene::build::{stack, text_node};
76 use sim_lib_scene::node;
77 use sim_value::build::{list, sym, text};
78
79 fn composed_scene() -> Expr {
82 let table = node(
83 "table",
84 vec![
85 ("columns", list(vec![text("name"), text("kind")])),
86 (
87 "rows",
88 list(vec![
89 list(vec![text("alpha"), sym("scene/text")]),
90 list(vec![text("beta"), sym("scene/button")]),
91 ]),
92 ),
93 ],
94 );
95 let button = node("button", vec![("label", text("Run"))]);
96 stack("column", vec![text_node("Surfaces"), table, button])
97 }
98
99 #[test]
100 fn renders_composed_scene_to_exact_text() {
101 let caps = cli_caps("tty.local.1");
102 let text = render_scene(&composed_scene(), &caps);
103 let expected = [
104 "Surfaces",
105 "name | kind",
106 "alpha | scene/text",
107 "beta | scene/button",
108 "[Run]",
109 ]
110 .join("\n");
111 assert_eq!(text, expected);
112 }
113
114 #[test]
115 fn renders_field_and_badge_spellings() {
116 let caps = cli_caps("tty.local.1");
117 let field = node(
118 "field",
119 vec![("label", text("name")), ("value", text("alpha"))],
120 );
121 assert_eq!(render_scene(&field, &caps), "name: alpha");
122 let badge = node(
123 "badge",
124 vec![("status", sym("ok")), ("label", text("done"))],
125 );
126 assert_eq!(render_scene(&badge, &caps), "<ok: done>");
127 }
128
129 #[test]
130 fn unknown_kind_degrades_to_marker() {
131 let caps = cli_caps("tty.local.1");
132 let graph = node("graph", vec![("nodes", list(Vec::new()))]);
134 assert_eq!(render_scene(&graph, &caps), "[graph]");
135 }
136
137 fn intent_kind_name(intent: &Expr) -> String {
138 intent_kind_of(intent)
139 .expect("intent is kind-tagged")
140 .name
141 .to_string()
142 }
143
144 fn assert_valid(key: &KeyInput, expected_kind: &str) -> Expr {
145 let intent =
146 intent_from_key(key, "main", "node-1", "value", 7).expect("key maps to an intent");
147 validate_intent(&intent).expect("produced intent validates");
148 assert_eq!(intent_kind_name(&intent), expected_kind);
149 intent
150 }
151
152 #[test]
153 fn enter_maps_to_invoke() {
154 assert_valid(&KeyInput::Enter, "invoke");
155 }
156
157 #[test]
158 fn arrows_map_to_select_and_move() {
159 assert_valid(&KeyInput::Up, "select");
160 assert_valid(&KeyInput::Down, "select");
161 assert_valid(&KeyInput::Left, "move");
162 assert_valid(&KeyInput::Right, "move");
163 }
164
165 #[test]
166 fn char_maps_to_edit_field() {
167 assert_valid(&KeyInput::Char('x'), "edit-field");
168 }
169
170 #[test]
171 fn char_edit_targets_focused_field_not_root() {
172 let intent = intent_from_key(&KeyInput::Char('x'), "main", "node-1", "title", 7)
173 .expect("char with a focused field maps to an intent");
174 let path = sim_value::access::field(&intent, "path").expect("edit-field carries a path");
175 assert_ne!(
177 path,
178 &Expr::List(Vec::new()),
179 "char edit must not overwrite the root path []"
180 );
181 let parsed = sim_value::path::Path::from_expr(path).expect("path parses");
182 assert_eq!(
183 parsed,
184 sim_value::path::Path::new().key(Expr::String("title".to_owned())),
185 "char edit binds to the focused field key"
186 );
187 }
188
189 #[test]
190 fn char_without_focused_field_does_not_edit_root() {
191 assert!(
192 intent_from_key(&KeyInput::Char('x'), "main", "node-1", "", 7).is_none(),
193 "a char with no focused field must not clobber the root value"
194 );
195 }
196
197 #[test]
198 fn colon_maps_to_invoke() {
199 assert_valid(&KeyInput::Colon("quit".to_owned()), "invoke");
200 }
201
202 #[test]
203 fn escape_maps_to_cancel() {
204 let intent = assert_valid(&KeyInput::Escape, "cancel");
205 assert_eq!(sim_value::access::field_str(&intent, "pane"), Some("main"));
206 }
207
208 #[test]
209 fn backspace_has_no_mapping() {
210 assert!(intent_from_key(&KeyInput::Backspace, "main", "node-1", "value", 7).is_none());
211 }
212
213 fn palette_commands() -> Vec<Command> {
214 use sim_kernel::Symbol;
215 use sim_lib_view::palette::CommandKind;
216 vec![
217 Command {
218 id: Symbol::new("run"),
219 label: "Run validation".to_owned(),
220 kind: CommandKind::Invoke,
221 },
222 Command {
223 id: Symbol::new("open-readme"),
224 label: "Open README".to_owned(),
225 kind: CommandKind::Open,
226 },
227 ]
228 }
229
230 #[test]
231 fn palette_render_is_deterministic_ascii() {
232 let commands = palette_commands();
233 let first = render_palette(&commands, "");
234 let second = render_palette(&commands, "");
235 assert_eq!(first, second, "palette render must be deterministic");
236 assert!(first.is_ascii(), "palette render must be ASCII");
237 assert_eq!(first, ["[Run validation]", "[Open README]"].join("\n"));
238 assert_eq!(render_palette(&commands, "open"), "[Open README]");
240 }
241
242 #[test]
243 fn tui_and_web_palette_intent_are_identical() {
244 let commands = palette_commands();
245 let via_tui = palette_intent_from_colon(&commands, "run", "main", 3)
247 .expect("colon entry selects a command");
248 let via_web = sim_lib_view::palette::palette_intent(&commands[0], "main", 3)
250 .expect("command reduces");
251 assert_eq!(via_tui, via_web, "both surfaces drive one palette model");
252 validate_intent(&via_tui).expect("shared palette intent validates");
253 }
254
255 #[test]
256 fn cli_and_tui_caps_differ_in_input_and_color() {
257 let cli = cli_caps("tty.local.1");
258 let tui = tui_caps("tty.local.1");
259 assert_eq!(cli.preset_name(), "cli");
260 assert_eq!(tui.preset_name(), "tui");
261 assert!(!cli.input_flag("pointer"));
263 assert!(tui.input_flag("pointer"));
264 assert!(cli.input_flag("keyboard") && tui.input_flag("keyboard"));
266 assert_eq!(cli.client_id, "tty.local.1");
267 }
268}