1use serde_json::Value;
2
3use crate::common::Colors;
4use crate::common::ValueExt;
5use crate::ipc::ClientError;
6
7pub trait Presenter {
12 fn present_success(&self, message: &str, warning: Option<&str>);
14
15 fn present_error(&self, message: &str);
17
18 fn present_value(&self, value: &Value);
20
21 fn present_client_error(&self, error: &ClientError);
23
24 fn present_kv(&self, key: &str, value: &str);
26
27 fn present_session_id(&self, session_id: &str, label: Option<&str>);
29
30 fn present_element_ref(&self, element_ref: &str, info: Option<&str>);
32
33 fn present_list_header(&self, title: &str);
35
36 fn present_list_item(&self, item: &str);
38
39 fn present_info(&self, message: &str);
41
42 fn present_header(&self, text: &str);
44
45 fn present_raw(&self, text: &str);
47
48 fn present_wait_result(&self, result: &WaitResult);
50
51 fn present_assert_result(&self, result: &AssertResult);
53
54 fn present_health(&self, health: &HealthResult);
56
57 fn present_cleanup(&self, result: &CleanupResult);
59
60 fn present_find(&self, result: &FindResult);
62}
63
64pub struct WaitResult {
66 pub found: bool,
67 pub elapsed_ms: u64,
68}
69
70impl WaitResult {
71 pub fn from_json(value: &Value) -> Self {
73 Self {
74 found: value.bool_or("found", false),
75 elapsed_ms: value.u64_or("elapsed_ms", 0),
76 }
77 }
78}
79
80pub struct AssertResult {
82 pub passed: bool,
83 pub condition: String,
84}
85
86pub struct HealthResult {
88 pub status: String,
89 pub pid: u64,
90 pub uptime_ms: u64,
91 pub session_count: u64,
92 pub version: String,
93 pub socket_path: Option<String>,
94 pub pid_file_path: Option<String>,
95}
96
97impl HealthResult {
98 pub fn from_json(value: &Value, verbose: bool) -> Self {
101 use crate::ipc::socket_path;
102
103 let (socket, pid_file) = if verbose {
104 let socket = socket_path();
105 let pid_file = socket.with_extension("pid");
106 (
107 Some(socket.display().to_string()),
108 Some(pid_file.display().to_string()),
109 )
110 } else {
111 (None, None)
112 };
113
114 Self {
115 status: value.str_or("status", "unknown").to_string(),
116 pid: value.u64_or("pid", 0),
117 uptime_ms: value.u64_or("uptime_ms", 0),
118 session_count: value.u64_or("session_count", 0),
119 version: value.str_or("version", "?").to_string(),
120 socket_path: socket,
121 pid_file_path: pid_file,
122 }
123 }
124}
125
126pub struct CleanupResult {
128 pub cleaned: usize,
129 pub failures: Vec<CleanupFailure>,
130}
131
132pub struct CleanupFailure {
134 pub session_id: String,
135 pub error: String,
136}
137
138pub struct FindResult {
140 pub count: u64,
141 pub elements: Vec<ElementInfo>,
142}
143
144impl FindResult {
145 pub fn from_json(value: &Value) -> Self {
147 let count = value.u64_or("count", 0);
148 let elements = value
149 .get("elements")
150 .and_then(|v| v.as_array())
151 .map(|arr| {
152 arr.iter()
153 .map(|el| ElementInfo {
154 element_ref: el.str_or("ref", "").to_string(),
155 element_type: el.str_or("type", "").to_string(),
156 label: el.str_or("label", "").to_string(),
157 focused: el.bool_or("focused", false),
158 })
159 .collect()
160 })
161 .unwrap_or_default();
162
163 Self { count, elements }
164 }
165}
166
167pub struct ElementInfo {
169 pub element_ref: String,
170 pub element_type: String,
171 pub label: String,
172 pub focused: bool,
173}
174
175pub struct TextPresenter;
177
178impl Presenter for TextPresenter {
179 fn present_success(&self, message: &str, warning: Option<&str>) {
180 println!("{} {}", Colors::success("✓"), message);
181 if let Some(w) = warning {
182 eprintln!("{} {}", Colors::dim("Warning:"), w);
183 }
184 }
185
186 fn present_error(&self, message: &str) {
187 eprintln!("{} {}", Colors::error("Error:"), message);
188 }
189
190 fn present_value(&self, value: &Value) {
191 if let Some(s) = value.as_str() {
192 println!("{}", s);
193 } else if let Some(n) = value.as_u64() {
194 println!("{}", n);
195 } else if let Some(b) = value.as_bool() {
196 println!("{}", b);
197 } else {
198 println!(
199 "{}",
200 serde_json::to_string_pretty(value).unwrap_or_default()
201 );
202 }
203 }
204
205 fn present_client_error(&self, error: &ClientError) {
206 eprintln!("{} {}", Colors::error("Error:"), error);
207 if let Some(suggestion) = error.suggestion() {
208 eprintln!("{} {}", Colors::dim("Suggestion:"), suggestion);
209 }
210 if error.is_retryable() {
211 eprintln!(
212 "{}",
213 Colors::dim("(This error may be transient - retry may succeed)")
214 );
215 }
216 }
217
218 fn present_kv(&self, key: &str, value: &str) {
219 println!(" {}: {}", key, value);
220 }
221
222 fn present_session_id(&self, session_id: &str, label: Option<&str>) {
223 if let Some(l) = label {
224 println!("{} {}", l, Colors::session_id(session_id));
225 } else {
226 println!("{}", Colors::session_id(session_id));
227 }
228 }
229
230 fn present_element_ref(&self, element_ref: &str, info: Option<&str>) {
231 if let Some(i) = info {
232 println!("{} {}", Colors::element_ref(element_ref), i);
233 } else {
234 println!("{}", Colors::element_ref(element_ref));
235 }
236 }
237
238 fn present_list_header(&self, title: &str) {
239 println!("{}", Colors::bold(title));
240 }
241
242 fn present_list_item(&self, item: &str) {
243 println!(" {}", item);
244 }
245
246 fn present_info(&self, message: &str) {
247 println!("{}", Colors::dim(message));
248 }
249
250 fn present_header(&self, text: &str) {
251 println!("{}", Colors::bold(text));
252 }
253
254 fn present_raw(&self, text: &str) {
255 println!("{}", text);
256 }
257
258 fn present_wait_result(&self, result: &WaitResult) {
259 if result.found {
260 println!("Found after {}ms", result.elapsed_ms);
261 } else {
262 eprintln!("Timeout after {}ms - not found", result.elapsed_ms);
263 std::process::exit(1);
264 }
265 }
266
267 fn present_assert_result(&self, result: &AssertResult) {
268 if result.passed {
269 println!(
270 "{} Assertion passed: {}",
271 Colors::success("✓"),
272 result.condition
273 );
274 } else {
275 eprintln!(
276 "{} Assertion failed: {}",
277 Colors::error("✗"),
278 result.condition
279 );
280 std::process::exit(1);
281 }
282 }
283
284 fn present_health(&self, health: &HealthResult) {
285 println!(
286 "{} {}",
287 Colors::bold("Daemon status:"),
288 Colors::success(&health.status)
289 );
290 println!(" PID: {}", health.pid);
291 println!(" Uptime: {}", format_uptime_ms(health.uptime_ms));
292 println!(" Sessions: {}", health.session_count);
293 println!(" Version: {}", Colors::dim(&health.version));
294
295 if let (Some(socket), Some(pid_file)) = (&health.socket_path, &health.pid_file_path) {
296 println!();
297 println!("{}", Colors::bold("Connection:"));
298 println!(" Socket: {}", socket);
299 println!(" PID file: {}", pid_file);
300 }
301 }
302
303 fn present_cleanup(&self, result: &CleanupResult) {
304 if result.cleaned > 0 {
305 println!(
306 "{} Cleaned up {} session(s)",
307 Colors::success("Done:"),
308 result.cleaned
309 );
310 } else if result.failures.is_empty() {
311 println!("{}", Colors::dim("No sessions to clean up"));
312 }
313
314 if !result.failures.is_empty() {
315 eprintln!();
316 eprintln!(
317 "{} Failed to clean up {} session(s):",
318 Colors::error("Error:"),
319 result.failures.len()
320 );
321 for failure in &result.failures {
322 eprintln!(
323 " {}: {}",
324 Colors::session_id(&failure.session_id),
325 failure.error
326 );
327 }
328 }
329 }
330
331 fn present_find(&self, result: &FindResult) {
332 if result.count == 0 {
333 println!("{}", Colors::dim("No elements found"));
334 } else {
335 println!(
336 "{} Found {} element(s):",
337 Colors::success("✓"),
338 result.count
339 );
340 for el in &result.elements {
341 let focused = if el.focused {
342 Colors::success(" *focused*")
343 } else {
344 String::new()
345 };
346 println!(
347 " {} [{}:{}]{}",
348 Colors::element_ref(&el.element_ref),
349 el.element_type,
350 el.label,
351 focused
352 );
353 }
354 }
355 }
356}
357
358fn format_uptime_ms(uptime_ms: u64) -> String {
360 let secs = uptime_ms / 1000;
361 let mins = secs / 60;
362 let hours = mins / 60;
363 if hours > 0 {
364 format!("{}h {}m {}s", hours, mins % 60, secs % 60)
365 } else if mins > 0 {
366 format!("{}m {}s", mins, secs % 60)
367 } else {
368 format!("{}s", secs)
369 }
370}
371
372pub struct JsonPresenter;
374
375impl Presenter for JsonPresenter {
376 fn present_success(&self, message: &str, warning: Option<&str>) {
377 let mut output = serde_json::json!({
378 "success": true,
379 "message": message
380 });
381 if let Some(w) = warning {
382 output["warning"] = serde_json::json!(w);
383 }
384 println!(
385 "{}",
386 serde_json::to_string_pretty(&output).unwrap_or_default()
387 );
388 }
389
390 fn present_error(&self, message: &str) {
391 let output = serde_json::json!({
392 "success": false,
393 "error": message
394 });
395 eprintln!(
396 "{}",
397 serde_json::to_string_pretty(&output).unwrap_or_default()
398 );
399 }
400
401 fn present_value(&self, value: &Value) {
402 println!(
403 "{}",
404 serde_json::to_string_pretty(value).unwrap_or_default()
405 );
406 }
407
408 fn present_client_error(&self, error: &ClientError) {
409 eprintln!("{}", error.to_json());
410 }
411
412 fn present_kv(&self, key: &str, value: &str) {
413 let output = serde_json::json!({ key: value });
414 println!(
415 "{}",
416 serde_json::to_string_pretty(&output).unwrap_or_default()
417 );
418 }
419
420 fn present_session_id(&self, session_id: &str, label: Option<&str>) {
421 let output = if let Some(l) = label {
422 serde_json::json!({ "label": l, "session_id": session_id })
423 } else {
424 serde_json::json!({ "session_id": session_id })
425 };
426 println!(
427 "{}",
428 serde_json::to_string_pretty(&output).unwrap_or_default()
429 );
430 }
431
432 fn present_element_ref(&self, element_ref: &str, info: Option<&str>) {
433 let output = if let Some(i) = info {
434 serde_json::json!({ "ref": element_ref, "info": i })
435 } else {
436 serde_json::json!({ "ref": element_ref })
437 };
438 println!(
439 "{}",
440 serde_json::to_string_pretty(&output).unwrap_or_default()
441 );
442 }
443
444 fn present_list_header(&self, _title: &str) {
445 }
447
448 fn present_list_item(&self, item: &str) {
449 println!("\"{}\"", item);
452 }
453
454 fn present_info(&self, message: &str) {
455 let output = serde_json::json!({ "info": message });
456 println!(
457 "{}",
458 serde_json::to_string_pretty(&output).unwrap_or_default()
459 );
460 }
461
462 fn present_header(&self, _text: &str) {
463 }
465
466 fn present_raw(&self, text: &str) {
467 let output = serde_json::json!({ "output": text });
469 println!(
470 "{}",
471 serde_json::to_string_pretty(&output).unwrap_or_default()
472 );
473 }
474
475 fn present_wait_result(&self, result: &WaitResult) {
476 let output = serde_json::json!({
477 "found": result.found,
478 "elapsed_ms": result.elapsed_ms
479 });
480 println!(
481 "{}",
482 serde_json::to_string_pretty(&output).unwrap_or_default()
483 );
484 }
485
486 fn present_assert_result(&self, result: &AssertResult) {
487 let output = serde_json::json!({
488 "condition": result.condition,
489 "passed": result.passed
490 });
491 println!(
492 "{}",
493 serde_json::to_string_pretty(&output).unwrap_or_default()
494 );
495 }
496
497 fn present_health(&self, health: &HealthResult) {
498 let mut output = serde_json::json!({
499 "status": health.status,
500 "pid": health.pid,
501 "uptime_ms": health.uptime_ms,
502 "session_count": health.session_count,
503 "version": health.version
504 });
505 if let Some(socket) = &health.socket_path {
506 output["socket_path"] = serde_json::json!(socket);
507 }
508 if let Some(pid_file) = &health.pid_file_path {
509 output["pid_file_path"] = serde_json::json!(pid_file);
510 }
511 println!(
512 "{}",
513 serde_json::to_string_pretty(&output).unwrap_or_default()
514 );
515 }
516
517 fn present_cleanup(&self, result: &CleanupResult) {
518 let failures: Vec<_> = result
519 .failures
520 .iter()
521 .map(|f| {
522 serde_json::json!({
523 "session": f.session_id,
524 "error": f.error
525 })
526 })
527 .collect();
528 let output = serde_json::json!({
529 "sessions_cleaned": result.cleaned,
530 "sessions_failed": result.failures.len(),
531 "failures": failures
532 });
533 println!(
534 "{}",
535 serde_json::to_string_pretty(&output).unwrap_or_default()
536 );
537 }
538
539 fn present_find(&self, result: &FindResult) {
540 let elements: Vec<_> = result
541 .elements
542 .iter()
543 .map(|el| {
544 serde_json::json!({
545 "ref": el.element_ref,
546 "type": el.element_type,
547 "label": el.label,
548 "focused": el.focused
549 })
550 })
551 .collect();
552 let output = serde_json::json!({
553 "count": result.count,
554 "elements": elements
555 });
556 println!(
557 "{}",
558 serde_json::to_string_pretty(&output).unwrap_or_default()
559 );
560 }
561}
562
563pub fn create_presenter(format: &crate::commands::OutputFormat) -> Box<dyn Presenter> {
565 match format {
566 crate::commands::OutputFormat::Json => Box::new(JsonPresenter),
567 crate::commands::OutputFormat::Text => Box::new(TextPresenter),
568 }
569}
570
571pub struct SpawnResult {
573 pub session_id: String,
574 pub pid: u32,
575}
576
577impl SpawnResult {
578 pub fn present(&self, presenter: &dyn Presenter) {
579 presenter.present_session_id(&self.session_id, Some(&Colors::success("Session started:")));
580 presenter.present_kv("PID", &self.pid.to_string());
581 }
582
583 pub fn to_json(&self) -> Value {
584 serde_json::json!({
585 "session_id": self.session_id,
586 "pid": self.pid
587 })
588 }
589}
590
591pub struct SessionListResult {
593 pub sessions: Vec<SessionListItem>,
594 pub active_session: Option<String>,
595}
596
597pub struct SessionListItem {
598 pub id: String,
599 pub command: String,
600 pub pid: u64,
601 pub running: bool,
602 pub cols: u64,
603 pub rows: u64,
604}
605
606impl SessionListResult {
607 pub fn present(&self, presenter: &dyn Presenter) {
608 if self.sessions.is_empty() {
609 presenter.present_info("No active sessions");
610 } else {
611 presenter.present_list_header("Active sessions:");
612 for session in &self.sessions {
613 let is_active = self.active_session.as_ref() == Some(&session.id);
614 let active_marker = if is_active {
615 Colors::success(" (active)")
616 } else {
617 String::new()
618 };
619 let status = if session.running {
620 Colors::success("running")
621 } else {
622 Colors::error("stopped")
623 };
624 let item = format!(
625 "{} - {} [{}] {}x{} pid:{}{}",
626 Colors::session_id(&session.id),
627 session.command,
628 status,
629 session.cols,
630 session.rows,
631 session.pid,
632 active_marker
633 );
634 presenter.present_list_item(&item);
635 }
636 }
637 }
638}
639
640pub struct ElementView<'a>(pub &'a Value);
644
645impl ElementView<'_> {
646 pub fn ref_str(&self) -> &str {
648 self.0.str_or("ref", "")
649 }
650
651 pub fn el_type(&self) -> &str {
653 self.0.str_or("type", "")
654 }
655
656 pub fn label(&self) -> &str {
658 self.0.str_or("label", "")
659 }
660
661 pub fn focused(&self) -> bool {
663 self.0.bool_or("focused", false)
664 }
665
666 pub fn selected(&self) -> bool {
668 self.0.bool_or("selected", false)
669 }
670
671 pub fn value(&self) -> Option<&str> {
673 self.0.get("value").and_then(|v| v.as_str())
674 }
675
676 pub fn position(&self) -> (u64, u64) {
678 let pos = self.0.get("position");
679 let row = pos
680 .and_then(|p| p.get("row"))
681 .and_then(|v| v.as_u64())
682 .unwrap_or(0);
683 let col = pos
684 .and_then(|p| p.get("col"))
685 .and_then(|v| v.as_u64())
686 .unwrap_or(0);
687 (row, col)
688 }
689
690 pub fn focused_indicator(&self) -> String {
692 if self.focused() {
693 Colors::success(" *focused*")
694 } else {
695 String::new()
696 }
697 }
698
699 pub fn selected_indicator(&self) -> String {
701 if self.selected() {
702 Colors::info(" *selected*")
703 } else {
704 String::new()
705 }
706 }
707
708 pub fn label_suffix(&self) -> String {
710 if self.label().is_empty() {
711 String::new()
712 } else {
713 format!(":{}", self.label())
714 }
715 }
716}
717
718#[cfg(test)]
719mod tests {
720 use super::*;
721
722 #[test]
723 fn test_text_presenter_success() {
724 let presenter = TextPresenter;
725 presenter.present_success("Test message", None);
727 presenter.present_success("Test with warning", Some("Warning text"));
728 }
729
730 #[test]
731 fn test_json_presenter_success() {
732 let presenter = JsonPresenter;
733 presenter.present_success("Test message", None);
735 presenter.present_success("Test with warning", Some("Warning text"));
736 }
737
738 #[test]
739 fn test_text_presenter_error() {
740 let presenter = TextPresenter;
741 presenter.present_error("Test error");
742 }
743
744 #[test]
745 fn test_json_presenter_error() {
746 let presenter = JsonPresenter;
747 presenter.present_error("Test error");
748 }
749
750 #[test]
751 fn test_spawn_result_to_json() {
752 let result = SpawnResult {
753 session_id: "abc123".to_string(),
754 pid: 1234,
755 };
756 let json = result.to_json();
757 assert_eq!(json["session_id"], "abc123");
758 assert_eq!(json["pid"], 1234);
759 }
760
761 #[test]
762 fn test_wait_result_struct() {
763 let result = WaitResult {
764 found: true,
765 elapsed_ms: 150,
766 };
767 assert!(result.found);
768 assert_eq!(result.elapsed_ms, 150);
769 }
770
771 #[test]
772 fn test_assert_result_struct() {
773 let result = AssertResult {
774 passed: true,
775 condition: "text:hello".to_string(),
776 };
777 assert!(result.passed);
778 assert_eq!(result.condition, "text:hello");
779 }
780
781 #[test]
782 fn test_health_result_struct() {
783 let result = HealthResult {
784 status: "healthy".to_string(),
785 pid: 1234,
786 uptime_ms: 60000,
787 session_count: 5,
788 version: "0.3.0".to_string(),
789 socket_path: Some("/tmp/agent-tui.sock".to_string()),
790 pid_file_path: None,
791 };
792 assert_eq!(result.status, "healthy");
793 assert_eq!(result.session_count, 5);
794 }
795
796 #[test]
797 fn test_cleanup_result_struct() {
798 let result = CleanupResult {
799 cleaned: 3,
800 failures: vec![CleanupFailure {
801 session_id: "sess1".to_string(),
802 error: "session not found".to_string(),
803 }],
804 };
805 assert_eq!(result.cleaned, 3);
806 assert_eq!(result.failures.len(), 1);
807 }
808
809 #[test]
810 fn test_find_result_struct() {
811 let result = FindResult {
812 count: 2,
813 elements: vec![
814 ElementInfo {
815 element_ref: "@btn1".to_string(),
816 element_type: "button".to_string(),
817 label: "Submit".to_string(),
818 focused: true,
819 },
820 ElementInfo {
821 element_ref: "@btn2".to_string(),
822 element_type: "button".to_string(),
823 label: "Cancel".to_string(),
824 focused: false,
825 },
826 ],
827 };
828 assert_eq!(result.count, 2);
829 assert_eq!(result.elements.len(), 2);
830 assert!(result.elements[0].focused);
831 }
832
833 #[test]
834 fn test_json_presenter_wait_result() {
835 let presenter = JsonPresenter;
836 let result = WaitResult {
837 found: true,
838 elapsed_ms: 100,
839 };
840 presenter.present_wait_result(&result);
842 }
843
844 #[test]
845 fn test_json_presenter_assert_result() {
846 let presenter = JsonPresenter;
847 let result = AssertResult {
848 passed: true,
849 condition: "element:@btn1".to_string(),
850 };
851 presenter.present_assert_result(&result);
853 }
854
855 #[test]
856 fn test_json_presenter_health() {
857 let presenter = JsonPresenter;
858 let health = HealthResult {
859 status: "healthy".to_string(),
860 pid: 1234,
861 uptime_ms: 60000,
862 session_count: 2,
863 version: "0.3.0".to_string(),
864 socket_path: None,
865 pid_file_path: None,
866 };
867 presenter.present_health(&health);
869 }
870
871 #[test]
872 fn test_json_presenter_cleanup() {
873 let presenter = JsonPresenter;
874 let result = CleanupResult {
875 cleaned: 2,
876 failures: vec![],
877 };
878 presenter.present_cleanup(&result);
880 }
881
882 #[test]
883 fn test_json_presenter_find() {
884 let presenter = JsonPresenter;
885 let result = FindResult {
886 count: 1,
887 elements: vec![ElementInfo {
888 element_ref: "@inp1".to_string(),
889 element_type: "input".to_string(),
890 label: "Email".to_string(),
891 focused: false,
892 }],
893 };
894 presenter.present_find(&result);
896 }
897
898 #[test]
899 fn test_format_uptime_ms_seconds() {
900 assert_eq!(format_uptime_ms(5000), "5s");
901 assert_eq!(format_uptime_ms(45000), "45s");
902 }
903
904 #[test]
905 fn test_format_uptime_ms_minutes() {
906 assert_eq!(format_uptime_ms(60000), "1m 0s");
907 assert_eq!(format_uptime_ms(90000), "1m 30s");
908 assert_eq!(format_uptime_ms(300000), "5m 0s");
909 }
910
911 #[test]
912 fn test_format_uptime_ms_hours() {
913 assert_eq!(format_uptime_ms(3600000), "1h 0m 0s");
914 assert_eq!(format_uptime_ms(5400000), "1h 30m 0s");
915 assert_eq!(format_uptime_ms(7265000), "2h 1m 5s");
916 }
917}