1use std::fmt::Write;
20
21#[cfg(feature = "repl")]
22use nu_ansi_term::{Color, Style};
23#[cfg(feature = "repl")]
24use reedline::Prompt;
25
26use super::session::ReplSession;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum HealthStatus {
31 Healthy,
33 Warning,
35 Critical,
37 None,
39}
40
41impl HealthStatus {
42 #[must_use]
44 pub fn from_grade(grade: char) -> Self {
45 match grade {
46 'A' | 'B' => Self::Healthy,
47 'C' | 'D' => Self::Warning,
48 'F' => Self::Critical,
49 _ => Self::None,
50 }
51 }
52
53 #[cfg(feature = "repl")]
55 #[must_use]
56 pub fn color(&self) -> Color {
57 match self {
58 Self::Healthy => Color::Green,
59 Self::Warning => Color::Yellow,
60 Self::Critical => Color::Red,
61 Self::None => Color::Default,
62 }
63 }
64
65 #[must_use]
67 pub fn indicator(&self) -> &'static str {
68 match self {
69 Self::Warning => "!",
70 Self::Critical => "!!",
71 Self::Healthy | Self::None => "",
72 }
73 }
74}
75
76pub struct AndonPrompt {
78 #[allow(dead_code)]
80 base: String,
81}
82
83impl Default for AndonPrompt {
84 fn default() -> Self {
85 Self::new()
86 }
87}
88
89impl AndonPrompt {
90 #[must_use]
92 pub fn new() -> Self {
93 Self {
94 base: "alimentar".to_string(),
95 }
96 }
97
98 #[must_use]
100 pub fn render(session: &ReplSession) -> String {
101 let mut prompt = String::from("alimentar");
102
103 if let Some(name) = session.active_name() {
104 prompt.push_str(" [");
105 prompt.push_str(&name);
106
107 if let Some(rows) = session.active_row_count() {
108 let _ = write!(prompt, ": {} rows", rows);
109 }
110
111 if let Some(grade) = session.active_grade() {
112 let status =
113 HealthStatus::from_grade(grade.to_string().chars().next().unwrap_or(' '));
114 let _ = write!(prompt, ", {}{}", grade, status.indicator());
115 }
116
117 prompt.push(']');
118 }
119
120 prompt.push_str(" > ");
121 prompt
122 }
123
124 #[cfg(feature = "repl")]
126 #[must_use]
127 pub fn render_colored(session: &ReplSession) -> String {
128 let mut prompt = Style::new().bold().paint("alimentar").to_string();
129
130 if let Some(name) = session.active_name() {
131 prompt.push_str(" [");
132 prompt.push_str(&name);
133
134 if let Some(rows) = session.active_row_count() {
135 let _ = write!(prompt, ": {} rows", rows);
136 }
137
138 if let Some(grade) = session.active_grade() {
139 let grade_char = grade.to_string().chars().next().unwrap_or(' ');
140 let status = HealthStatus::from_grade(grade_char);
141 let grade_colored =
142 status
143 .color()
144 .bold()
145 .paint(format!("{}{}", grade, status.indicator()));
146 let _ = write!(prompt, ", {}", grade_colored);
147 }
148
149 prompt.push(']');
150 }
151
152 prompt.push_str(" > ");
153 prompt
154 }
155}
156
157#[cfg(feature = "repl")]
158impl Prompt for AndonPrompt {
159 fn render_prompt_left(&self) -> std::borrow::Cow<'_, str> {
160 std::borrow::Cow::Borrowed("alimentar > ")
163 }
164
165 fn render_prompt_right(&self) -> std::borrow::Cow<'_, str> {
166 std::borrow::Cow::Borrowed("")
167 }
168
169 fn render_prompt_indicator(
170 &self,
171 _prompt_mode: reedline::PromptEditMode,
172 ) -> std::borrow::Cow<'_, str> {
173 std::borrow::Cow::Borrowed("")
174 }
175
176 fn render_prompt_multiline_indicator(&self) -> std::borrow::Cow<'_, str> {
177 std::borrow::Cow::Borrowed("... ")
178 }
179
180 fn render_prompt_history_search_indicator(
181 &self,
182 _history_search: reedline::PromptHistorySearch,
183 ) -> std::borrow::Cow<'_, str> {
184 std::borrow::Cow::Borrowed("(search) ")
185 }
186}
187
188#[cfg(feature = "repl")]
190#[allow(dead_code)]
191pub struct SessionPrompt<'a> {
192 session: &'a ReplSession,
193}
194
195#[cfg(feature = "repl")]
196#[allow(dead_code)]
197impl<'a> SessionPrompt<'a> {
198 #[must_use]
200 pub fn new(session: &'a ReplSession) -> Self {
201 Self { session }
202 }
203}
204
205#[cfg(feature = "repl")]
206impl Prompt for SessionPrompt<'_> {
207 fn render_prompt_left(&self) -> std::borrow::Cow<'_, str> {
208 std::borrow::Cow::Owned(AndonPrompt::render_colored(self.session))
209 }
210
211 fn render_prompt_right(&self) -> std::borrow::Cow<'_, str> {
212 std::borrow::Cow::Borrowed("")
213 }
214
215 fn render_prompt_indicator(
216 &self,
217 _prompt_mode: reedline::PromptEditMode,
218 ) -> std::borrow::Cow<'_, str> {
219 std::borrow::Cow::Borrowed("")
220 }
221
222 fn render_prompt_multiline_indicator(&self) -> std::borrow::Cow<'_, str> {
223 std::borrow::Cow::Borrowed("... ")
224 }
225
226 fn render_prompt_history_search_indicator(
227 &self,
228 _history_search: reedline::PromptHistorySearch,
229 ) -> std::borrow::Cow<'_, str> {
230 std::borrow::Cow::Borrowed("(search) ")
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use std::sync::Arc;
237
238 use arrow::{
239 array::{Float64Array, Int32Array, StringArray},
240 datatypes::{DataType, Field, Schema as ArrowSchema},
241 record_batch::RecordBatch,
242 };
243
244 use super::*;
245 use crate::ArrowDataset;
246
247 fn create_test_dataset() -> ArrowDataset {
248 let schema = Arc::new(ArrowSchema::new(vec![
249 Field::new("id", DataType::Int32, false),
250 Field::new("name", DataType::Utf8, true),
251 Field::new("value", DataType::Float64, true),
252 ]));
253
254 let batch = RecordBatch::try_new(
255 schema.clone(),
256 vec![
257 Arc::new(Int32Array::from(vec![1, 2, 3, 4, 5])),
258 Arc::new(StringArray::from(vec![
259 Some("a"),
260 Some("b"),
261 None,
262 Some("d"),
263 Some("e"),
264 ])),
265 Arc::new(Float64Array::from(vec![1.0, 2.0, 3.0, 4.0, 5.0])),
266 ],
267 )
268 .unwrap();
269
270 ArrowDataset::new(vec![batch]).unwrap()
271 }
272
273 #[test]
275 fn test_health_status_from_grade_a() {
276 assert_eq!(HealthStatus::from_grade('A'), HealthStatus::Healthy);
277 }
278
279 #[test]
280 fn test_health_status_from_grade_b() {
281 assert_eq!(HealthStatus::from_grade('B'), HealthStatus::Healthy);
282 }
283
284 #[test]
285 fn test_health_status_from_grade_c() {
286 assert_eq!(HealthStatus::from_grade('C'), HealthStatus::Warning);
287 }
288
289 #[test]
290 fn test_health_status_from_grade_d() {
291 assert_eq!(HealthStatus::from_grade('D'), HealthStatus::Warning);
292 }
293
294 #[test]
295 fn test_health_status_from_grade_f() {
296 assert_eq!(HealthStatus::from_grade('F'), HealthStatus::Critical);
297 }
298
299 #[test]
300 fn test_health_status_from_grade_unknown() {
301 assert_eq!(HealthStatus::from_grade('X'), HealthStatus::None);
302 }
303
304 #[test]
305 fn test_health_status_indicator_healthy() {
306 assert_eq!(HealthStatus::Healthy.indicator(), "");
307 }
308
309 #[test]
310 fn test_health_status_indicator_warning() {
311 assert_eq!(HealthStatus::Warning.indicator(), "!");
312 }
313
314 #[test]
315 fn test_health_status_indicator_critical() {
316 assert_eq!(HealthStatus::Critical.indicator(), "!!");
317 }
318
319 #[test]
320 fn test_health_status_indicator_none() {
321 assert_eq!(HealthStatus::None.indicator(), "");
322 }
323
324 #[cfg(feature = "repl")]
325 #[test]
326 fn test_health_status_color_healthy() {
327 assert_eq!(HealthStatus::Healthy.color(), Color::Green);
328 }
329
330 #[cfg(feature = "repl")]
331 #[test]
332 fn test_health_status_color_warning() {
333 assert_eq!(HealthStatus::Warning.color(), Color::Yellow);
334 }
335
336 #[cfg(feature = "repl")]
337 #[test]
338 fn test_health_status_color_critical() {
339 assert_eq!(HealthStatus::Critical.color(), Color::Red);
340 }
341
342 #[cfg(feature = "repl")]
343 #[test]
344 fn test_health_status_color_none() {
345 assert_eq!(HealthStatus::None.color(), Color::Default);
346 }
347
348 #[test]
350 fn test_andon_prompt_new() {
351 let prompt = AndonPrompt::new();
352 assert_eq!(prompt.base, "alimentar");
353 }
354
355 #[test]
356 fn test_andon_prompt_default() {
357 let prompt = AndonPrompt::default();
358 assert_eq!(prompt.base, "alimentar");
359 }
360
361 #[test]
362 fn test_andon_prompt_render_no_dataset() {
363 let session = ReplSession::new();
364 let rendered = AndonPrompt::render(&session);
365 assert_eq!(rendered, "alimentar > ");
366 }
367
368 #[test]
369 fn test_andon_prompt_render_with_dataset() {
370 let mut session = ReplSession::new();
371 session.load_dataset("test.parquet", create_test_dataset());
372 let rendered = AndonPrompt::render(&session);
373 assert!(rendered.starts_with("alimentar [test.parquet"));
374 assert!(rendered.contains("5 rows"));
375 assert!(rendered.ends_with("] > "));
376 }
377
378 #[cfg(feature = "repl")]
379 #[test]
380 fn test_andon_prompt_render_colored_no_dataset() {
381 let session = ReplSession::new();
382 let rendered = AndonPrompt::render_colored(&session);
383 assert!(rendered.contains("alimentar"));
384 assert!(rendered.ends_with(" > "));
385 }
386
387 #[cfg(feature = "repl")]
388 #[test]
389 fn test_andon_prompt_render_colored_with_dataset() {
390 let mut session = ReplSession::new();
391 session.load_dataset("data.parquet", create_test_dataset());
392 let rendered = AndonPrompt::render_colored(&session);
393 assert!(rendered.contains("data.parquet"));
394 assert!(rendered.contains("5 rows"));
395 }
396
397 #[cfg(feature = "repl")]
399 #[test]
400 fn test_andon_prompt_render_prompt_left() {
401 use reedline::Prompt;
402 let prompt = AndonPrompt::new();
403 assert_eq!(prompt.render_prompt_left().as_ref(), "alimentar > ");
404 }
405
406 #[cfg(feature = "repl")]
407 #[test]
408 fn test_andon_prompt_render_prompt_right() {
409 use reedline::Prompt;
410 let prompt = AndonPrompt::new();
411 assert_eq!(prompt.render_prompt_right().as_ref(), "");
412 }
413
414 #[cfg(feature = "repl")]
415 #[test]
416 fn test_andon_prompt_render_prompt_indicator() {
417 use reedline::Prompt;
418 let prompt = AndonPrompt::new();
419 assert_eq!(
420 prompt
421 .render_prompt_indicator(reedline::PromptEditMode::Default)
422 .as_ref(),
423 ""
424 );
425 }
426
427 #[cfg(feature = "repl")]
428 #[test]
429 fn test_andon_prompt_render_multiline() {
430 use reedline::Prompt;
431 let prompt = AndonPrompt::new();
432 assert_eq!(prompt.render_prompt_multiline_indicator().as_ref(), "... ");
433 }
434
435 #[cfg(feature = "repl")]
436 #[test]
437 fn test_andon_prompt_render_history_search() {
438 use reedline::Prompt;
439 let prompt = AndonPrompt::new();
440 let search = reedline::PromptHistorySearch::new(
441 reedline::PromptHistorySearchStatus::Passing,
442 "test".to_string(),
443 );
444 assert_eq!(
445 prompt
446 .render_prompt_history_search_indicator(search)
447 .as_ref(),
448 "(search) "
449 );
450 }
451
452 #[cfg(feature = "repl")]
454 #[test]
455 fn test_session_prompt_new() {
456 let session = ReplSession::new();
457 let _prompt = SessionPrompt::new(&session);
458 }
459
460 #[cfg(feature = "repl")]
461 #[test]
462 fn test_session_prompt_render_prompt_left() {
463 use reedline::Prompt;
464 let session = ReplSession::new();
465 let prompt = SessionPrompt::new(&session);
466 let rendered = prompt.render_prompt_left();
467 assert!(rendered.contains("alimentar"));
468 }
469
470 #[cfg(feature = "repl")]
471 #[test]
472 fn test_session_prompt_render_prompt_right() {
473 use reedline::Prompt;
474 let session = ReplSession::new();
475 let prompt = SessionPrompt::new(&session);
476 assert_eq!(prompt.render_prompt_right().as_ref(), "");
477 }
478
479 #[cfg(feature = "repl")]
480 #[test]
481 fn test_session_prompt_render_prompt_indicator() {
482 use reedline::Prompt;
483 let session = ReplSession::new();
484 let prompt = SessionPrompt::new(&session);
485 assert_eq!(
486 prompt
487 .render_prompt_indicator(reedline::PromptEditMode::Default)
488 .as_ref(),
489 ""
490 );
491 }
492
493 #[cfg(feature = "repl")]
494 #[test]
495 fn test_session_prompt_render_multiline() {
496 use reedline::Prompt;
497 let session = ReplSession::new();
498 let prompt = SessionPrompt::new(&session);
499 assert_eq!(prompt.render_prompt_multiline_indicator().as_ref(), "... ");
500 }
501
502 #[cfg(feature = "repl")]
503 #[test]
504 fn test_session_prompt_render_history_search() {
505 use reedline::Prompt;
506 let session = ReplSession::new();
507 let prompt = SessionPrompt::new(&session);
508 let search = reedline::PromptHistorySearch::new(
509 reedline::PromptHistorySearchStatus::Passing,
510 "test".to_string(),
511 );
512 assert_eq!(
513 prompt
514 .render_prompt_history_search_indicator(search)
515 .as_ref(),
516 "(search) "
517 );
518 }
519
520 #[cfg(feature = "repl")]
521 #[test]
522 fn test_session_prompt_with_dataset() {
523 use reedline::Prompt;
524 let mut session = ReplSession::new();
525 session.load_dataset("mydata.csv", create_test_dataset());
526 let prompt = SessionPrompt::new(&session);
527 let rendered = prompt.render_prompt_left();
528 assert!(rendered.contains("mydata.csv"));
529 assert!(rendered.contains("5 rows"));
530 }
531}