1use crate::state::MachineState;
8use crate::ui::gui::theme::{colors, rounding, spacing};
9use crate::ui::gui::typography::{self, FontSize, FontWeight};
10use crate::ui::shared::format_state_label;
12pub use crate::ui::shared::{
13 format_duration, format_duration_secs, format_relative_time, format_relative_time_secs,
14 format_run_duration, RunProgress, Status,
15};
16use eframe::egui::{self, Color32, Pos2, Rect, Rounding, Vec2};
17
18pub const STATUS_DOT_RADIUS: f32 = 4.0;
24
25#[derive(Debug, Clone, Copy)]
35pub struct StatusDot {
36 color: Color32,
38 radius: f32,
40}
41
42impl StatusDot {
43 pub fn from_status(status: Status) -> Self {
45 Self {
46 color: status.color(),
47 radius: STATUS_DOT_RADIUS,
48 }
49 }
50
51 pub fn from_machine_state(state: MachineState) -> Self {
53 Self::from_status(Status::from_machine_state(state))
54 }
55
56 pub fn with_color(color: Color32) -> Self {
58 Self {
59 color,
60 radius: STATUS_DOT_RADIUS,
61 }
62 }
63
64 pub fn with_radius(mut self, radius: f32) -> Self {
66 self.radius = radius;
67 self
68 }
69
70 pub fn radius(&self) -> f32 {
72 self.radius
73 }
74
75 pub fn color(&self) -> Color32 {
77 self.color
78 }
79
80 pub fn paint(&self, painter: &egui::Painter, center: Pos2) {
82 painter.circle_filled(center, self.radius, self.color);
83 }
84
85 pub fn paint_with_border(&self, painter: &egui::Painter, center: Pos2, border_color: Color32) {
87 painter.circle_filled(center, self.radius, self.color);
88 painter.circle_stroke(center, self.radius, egui::Stroke::new(1.0, border_color));
89 }
90}
91
92impl Default for StatusDot {
93 fn default() -> Self {
94 Self {
95 color: colors::STATUS_IDLE,
96 radius: STATUS_DOT_RADIUS,
97 }
98 }
99}
100
101pub trait StatusColors {
115 fn color(self) -> Color32;
117
118 fn background_color(self) -> Color32;
120}
121
122impl StatusColors for Status {
123 fn color(self) -> Color32 {
124 match self {
125 Status::Setup => colors::STATUS_IDLE,
126 Status::Running => colors::STATUS_RUNNING,
127 Status::Reviewing => colors::STATUS_WARNING,
128 Status::Correcting => colors::STATUS_CORRECTING,
129 Status::Success => colors::STATUS_SUCCESS,
130 Status::Warning => colors::STATUS_WARNING,
131 Status::Error => colors::STATUS_ERROR,
132 Status::Idle => colors::STATUS_IDLE,
133 }
134 }
135
136 fn background_color(self) -> Color32 {
137 match self {
138 Status::Setup => colors::STATUS_IDLE_BG,
139 Status::Running => colors::STATUS_RUNNING_BG,
140 Status::Reviewing => colors::STATUS_WARNING_BG,
141 Status::Correcting => colors::STATUS_CORRECTING_BG,
142 Status::Success => colors::STATUS_SUCCESS_BG,
143 Status::Warning => colors::STATUS_WARNING_BG,
144 Status::Error => colors::STATUS_ERROR_BG,
145 Status::Idle => colors::STATUS_IDLE_BG,
146 }
147 }
148}
149
150pub fn state_to_color(state: MachineState) -> Color32 {
154 Status::from_machine_state(state).color()
155}
156
157pub fn state_to_background_color(state: MachineState) -> Color32 {
159 Status::from_machine_state(state).background_color()
160}
161
162pub fn badge_background_color(status_color: Color32) -> Color32 {
168 let bg = colors::BACKGROUND;
171 let alpha = 0.15;
172
173 let r = (status_color.r() as f32 * alpha + bg.r() as f32 * (1.0 - alpha)) as u8;
174 let g = (status_color.g() as f32 * alpha + bg.g() as f32 * (1.0 - alpha)) as u8;
175 let b = (status_color.b() as f32 * alpha + bg.b() as f32 * (1.0 - alpha)) as u8;
176
177 Color32::from_rgb(r, g, b)
178}
179
180pub fn is_terminal_state(state: MachineState) -> bool {
190 matches!(
191 state,
192 MachineState::Completed | MachineState::Failed | MachineState::Idle
193 )
194}
195
196#[derive(Debug, Clone)]
206pub struct ProgressBar {
207 progress: f32,
209 height: f32,
211 background_color: Color32,
213 fill_color: Color32,
215 rounding: f32,
217}
218
219impl ProgressBar {
220 pub fn new(progress: f32) -> Self {
222 Self {
223 progress: progress.clamp(0.0, 1.0),
224 height: 6.0,
225 background_color: colors::SURFACE_HOVER,
226 fill_color: colors::ACCENT,
227 rounding: 3.0,
228 }
229 }
230
231 pub fn from_progress(progress: &RunProgress) -> Self {
233 Self::new(progress.fraction())
234 }
235
236 pub fn with_height(mut self, height: f32) -> Self {
238 self.height = height;
239 self
240 }
241
242 pub fn with_status_color(mut self, status: Status) -> Self {
244 self.fill_color = status.color();
245 self
246 }
247
248 pub fn with_colors(mut self, background: Color32, fill: Color32) -> Self {
250 self.background_color = background;
251 self.fill_color = fill;
252 self
253 }
254
255 pub fn with_rounding(mut self, rounding: f32) -> Self {
257 self.rounding = rounding;
258 self
259 }
260
261 pub fn progress(&self) -> f32 {
263 self.progress
264 }
265
266 pub fn paint(&self, painter: &egui::Painter, rect: Rect) {
268 painter.rect_filled(rect, Rounding::same(self.rounding), self.background_color);
270
271 if self.progress > 0.0 {
273 let fill_width = rect.width() * self.progress;
274 let fill_rect = Rect::from_min_size(rect.min, Vec2::new(fill_width, rect.height()));
275 painter.rect_filled(fill_rect, Rounding::same(self.rounding), self.fill_color);
276 }
277 }
278
279 pub fn show(&self, ui: &mut egui::Ui, width: f32) -> egui::Response {
281 let (rect, response) =
282 ui.allocate_exact_size(Vec2::new(width, self.height), egui::Sense::hover());
283 self.paint(ui.painter(), rect);
284 response
285 }
286}
287
288impl Default for ProgressBar {
289 fn default() -> Self {
290 Self::new(0.0)
291 }
292}
293
294pub fn truncate_with_ellipsis(s: &str, max_len: usize) -> String {
314 let char_count = s.chars().count();
315 if char_count <= max_len {
316 s.to_string()
317 } else if max_len <= 3 {
318 s.chars().take(max_len).collect()
319 } else {
320 let target_len = max_len - 3; let truncated: String = s.chars().take(target_len).collect();
322
323 let truncate_at = truncated.rfind(' ').unwrap_or(target_len);
325
326 if truncate_at == 0 {
327 format!("{}...", truncated.trim_end())
329 } else {
330 format!("{}...", truncated[..truncate_at].trim_end())
331 }
332 }
333}
334
335pub fn strip_worktree_prefix(branch_name: &str, project_name: &str) -> String {
351 let wt_prefix = format!("{}-wt-", project_name);
353 if let Some(stripped) = branch_name.strip_prefix(&wt_prefix) {
354 return stripped.to_string();
355 }
356
357 let wt_prefix_lower = format!("{}-wt-", project_name.to_lowercase());
359 if branch_name.to_lowercase().starts_with(&wt_prefix_lower) {
360 return branch_name[wt_prefix_lower.len()..].to_string();
362 }
363
364 branch_name.to_string()
366}
367
368pub const MAX_TEXT_LENGTH: usize = 40;
370
371pub const MAX_BRANCH_LENGTH: usize = 25;
373
374pub fn format_state(state: MachineState) -> &'static str {
384 format_state_label(state)
385}
386
387#[derive(Debug, Clone)]
393pub struct StatusLabel {
394 status: Status,
396 label: String,
398 dot_radius: f32,
400 spacing: f32,
402}
403
404impl StatusLabel {
405 pub fn new(status: Status, label: impl Into<String>) -> Self {
407 Self {
408 status,
409 label: label.into(),
410 dot_radius: STATUS_DOT_RADIUS,
411 spacing: 8.0,
412 }
413 }
414
415 pub fn from_machine_state(state: MachineState) -> Self {
417 Self::new(Status::from_machine_state(state), format_state(state))
418 }
419
420 pub fn with_dot_radius(mut self, radius: f32) -> Self {
422 self.dot_radius = radius;
423 self
424 }
425
426 pub fn with_spacing(mut self, spacing: f32) -> Self {
428 self.spacing = spacing;
429 self
430 }
431
432 pub fn status(&self) -> Status {
434 self.status
435 }
436
437 pub fn label(&self) -> &str {
439 &self.label
440 }
441
442 pub fn paint(
446 &self,
447 _ui: &egui::Ui,
448 painter: &egui::Painter,
449 pos: Pos2,
450 font: egui::FontId,
451 text_color: Color32,
452 ) -> f32 {
453 let dot = StatusDot::from_status(self.status).with_radius(self.dot_radius);
455 let dot_center = Pos2::new(pos.x + self.dot_radius, pos.y + self.dot_radius);
456 dot.paint(painter, dot_center);
457
458 let label_x = pos.x + self.dot_radius * 2.0 + self.spacing;
460 let galley = painter.layout_no_wrap(self.label.clone(), font, text_color);
461 painter.galley(
462 Pos2::new(label_x, pos.y),
463 galley.clone(),
464 Color32::TRANSPARENT,
465 );
466
467 self.dot_radius * 2.0 + self.spacing + galley.rect.width()
469 }
470
471 pub fn show(&self, ui: &mut egui::Ui) -> egui::Response {
473 let font = typography::font(FontSize::Caption, FontWeight::Medium);
474 let text_color = colors::TEXT_PRIMARY;
475
476 let text_galley =
478 ui.fonts(|f| f.layout_no_wrap(self.label.clone(), font.clone(), text_color));
479 let width = self.dot_radius * 2.0 + self.spacing + text_galley.rect.width();
480 let height = text_galley.rect.height().max(self.dot_radius * 2.0);
481
482 let (rect, response) =
483 ui.allocate_exact_size(Vec2::new(width, height), egui::Sense::hover());
484
485 if ui.is_rect_visible(rect) {
486 self.paint(ui, ui.painter(), rect.min, font, text_color);
487 }
488
489 response
490 }
491}
492
493pub struct CollapsibleSection<'a> {
516 id: &'a str,
518 title: &'a str,
520 default_expanded: bool,
522}
523
524impl<'a> CollapsibleSection<'a> {
525 pub fn new(id: &'a str, title: &'a str) -> Self {
530 Self {
531 id,
532 title,
533 default_expanded: true,
534 }
535 }
536
537 pub fn default_expanded(mut self, expanded: bool) -> Self {
542 self.default_expanded = expanded;
543 self
544 }
545
546 pub fn show<R>(
558 self,
559 ui: &mut egui::Ui,
560 collapsed_state: &mut std::collections::HashMap<String, bool>,
561 add_contents: impl FnOnce(&mut egui::Ui) -> R,
562 ) -> egui::Response {
563 let is_collapsed = *collapsed_state
565 .entry(self.id.to_string())
566 .or_insert(!self.default_expanded);
567
568 let header_response = self.render_header(ui, is_collapsed);
570
571 if header_response.clicked() {
573 collapsed_state.insert(self.id.to_string(), !is_collapsed);
574 }
575
576 if !is_collapsed {
578 ui.add_space(spacing::SM);
579 add_contents(ui);
580 }
581
582 header_response
583 }
584
585 fn render_header(&self, ui: &mut egui::Ui, is_collapsed: bool) -> egui::Response {
587 let available_width = ui.available_width();
588
589 let header_height = typography::line_height(FontSize::Body) + spacing::XS * 2.0;
591
592 let (rect, response) = ui.allocate_exact_size(
593 Vec2::new(available_width, header_height),
594 egui::Sense::click(),
595 );
596
597 if ui.is_rect_visible(rect) {
598 let painter = ui.painter();
599
600 if response.hovered() {
602 painter.rect_filled(rect, Rounding::same(rounding::SMALL), colors::SURFACE_HOVER);
603 }
604
605 let chevron_size = 8.0;
607 let chevron_x = rect.min.x + spacing::XS;
608 let chevron_y = rect.center().y;
609
610 let chevron_color = if response.hovered() {
611 colors::TEXT_PRIMARY
612 } else {
613 colors::TEXT_SECONDARY
614 };
615
616 if is_collapsed {
617 let points = [
620 Pos2::new(chevron_x, chevron_y - chevron_size / 2.0),
621 Pos2::new(chevron_x + chevron_size / 2.0, chevron_y),
622 Pos2::new(chevron_x, chevron_y + chevron_size / 2.0),
623 ];
624 painter.line_segment(
625 [points[0], points[1]],
626 egui::Stroke::new(1.5, chevron_color),
627 );
628 painter.line_segment(
629 [points[1], points[2]],
630 egui::Stroke::new(1.5, chevron_color),
631 );
632 } else {
633 let points = [
636 Pos2::new(chevron_x, chevron_y - chevron_size / 4.0),
637 Pos2::new(
638 chevron_x + chevron_size / 2.0,
639 chevron_y + chevron_size / 4.0,
640 ),
641 Pos2::new(chevron_x + chevron_size, chevron_y - chevron_size / 4.0),
642 ];
643 painter.line_segment(
644 [points[0], points[1]],
645 egui::Stroke::new(1.5, chevron_color),
646 );
647 painter.line_segment(
648 [points[1], points[2]],
649 egui::Stroke::new(1.5, chevron_color),
650 );
651 }
652
653 let title_x = chevron_x + chevron_size + spacing::SM;
655 let title_y = rect.center().y - typography::line_height(FontSize::Body) / 2.0;
656
657 let title_color = if response.hovered() {
658 colors::TEXT_PRIMARY
659 } else {
660 colors::TEXT_SECONDARY
661 };
662
663 let galley = painter.layout_no_wrap(
664 self.title.to_string(),
665 typography::font(FontSize::Body, FontWeight::Medium),
666 title_color,
667 );
668
669 painter.galley(Pos2::new(title_x, title_y), galley, Color32::TRANSPARENT);
670 }
671
672 if response.hovered() {
674 ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
675 }
676
677 response
678 }
679}
680
681#[cfg(test)]
686mod tests {
687 use super::*;
688
689 #[test]
694 fn test_status_colors() {
695 assert_eq!(Status::Setup.color(), colors::STATUS_IDLE);
697 assert_eq!(Status::Running.color(), colors::STATUS_RUNNING);
698 assert_eq!(Status::Reviewing.color(), colors::STATUS_WARNING);
699 assert_eq!(Status::Correcting.color(), colors::STATUS_CORRECTING);
700 assert_eq!(Status::Success.color(), colors::STATUS_SUCCESS);
701 assert_eq!(Status::Warning.color(), colors::STATUS_WARNING);
702 assert_eq!(Status::Error.color(), colors::STATUS_ERROR);
703 assert_eq!(Status::Idle.color(), colors::STATUS_IDLE);
704 }
705
706 #[test]
707 fn test_status_background_colors() {
708 assert_eq!(Status::Setup.background_color(), colors::STATUS_IDLE_BG);
709 assert_eq!(
710 Status::Running.background_color(),
711 colors::STATUS_RUNNING_BG
712 );
713 assert_eq!(
714 Status::Reviewing.background_color(),
715 colors::STATUS_WARNING_BG
716 );
717 assert_eq!(
718 Status::Correcting.background_color(),
719 colors::STATUS_CORRECTING_BG
720 );
721 assert_eq!(
722 Status::Success.background_color(),
723 colors::STATUS_SUCCESS_BG
724 );
725 assert_eq!(
726 Status::Warning.background_color(),
727 colors::STATUS_WARNING_BG
728 );
729 assert_eq!(Status::Error.background_color(), colors::STATUS_ERROR_BG);
730 assert_eq!(Status::Idle.background_color(), colors::STATUS_IDLE_BG);
731 }
732
733 #[test]
734 fn test_status_from_machine_state_setup_phase() {
735 assert_eq!(
737 Status::from_machine_state(MachineState::Initializing),
738 Status::Setup
739 );
740 assert_eq!(
741 Status::from_machine_state(MachineState::PickingStory),
742 Status::Setup
743 );
744 assert_eq!(
745 Status::from_machine_state(MachineState::LoadingSpec),
746 Status::Setup
747 );
748 assert_eq!(
749 Status::from_machine_state(MachineState::GeneratingSpec),
750 Status::Setup
751 );
752 }
753
754 #[test]
755 fn test_status_from_machine_state_running() {
756 assert_eq!(
758 Status::from_machine_state(MachineState::RunningClaude),
759 Status::Running
760 );
761 }
762
763 #[test]
764 fn test_status_from_machine_state_reviewing() {
765 assert_eq!(
767 Status::from_machine_state(MachineState::Reviewing),
768 Status::Reviewing
769 );
770 }
771
772 #[test]
773 fn test_status_from_machine_state_correcting() {
774 assert_eq!(
776 Status::from_machine_state(MachineState::Correcting),
777 Status::Correcting
778 );
779 }
780
781 #[test]
782 fn test_status_from_machine_state_success_path() {
783 assert_eq!(
785 Status::from_machine_state(MachineState::Committing),
786 Status::Success
787 );
788 assert_eq!(
789 Status::from_machine_state(MachineState::CreatingPR),
790 Status::Success
791 );
792 assert_eq!(
793 Status::from_machine_state(MachineState::Completed),
794 Status::Success
795 );
796 }
797
798 #[test]
799 fn test_status_from_machine_state_terminal() {
800 assert_eq!(
802 Status::from_machine_state(MachineState::Failed),
803 Status::Error
804 );
805 assert_eq!(Status::from_machine_state(MachineState::Idle), Status::Idle);
806 }
807
808 #[test]
809 fn test_state_to_color_semantic_mapping() {
810 assert_eq!(
812 state_to_color(MachineState::Initializing),
813 colors::STATUS_IDLE
814 );
815 assert_eq!(
816 state_to_color(MachineState::PickingStory),
817 colors::STATUS_IDLE
818 );
819
820 assert_eq!(
822 state_to_color(MachineState::RunningClaude),
823 colors::STATUS_RUNNING
824 );
825
826 assert_eq!(
828 state_to_color(MachineState::Reviewing),
829 colors::STATUS_WARNING
830 );
831
832 assert_eq!(
834 state_to_color(MachineState::Correcting),
835 colors::STATUS_CORRECTING
836 );
837
838 assert_eq!(
840 state_to_color(MachineState::Committing),
841 colors::STATUS_SUCCESS
842 );
843 assert_eq!(
844 state_to_color(MachineState::CreatingPR),
845 colors::STATUS_SUCCESS
846 );
847 assert_eq!(
848 state_to_color(MachineState::Completed),
849 colors::STATUS_SUCCESS
850 );
851
852 assert_eq!(state_to_color(MachineState::Failed), colors::STATUS_ERROR);
854
855 assert_eq!(state_to_color(MachineState::Idle), colors::STATUS_IDLE);
857 }
858
859 #[test]
860 fn test_state_to_background_color() {
861 assert_eq!(
862 state_to_background_color(MachineState::RunningClaude),
863 colors::STATUS_RUNNING_BG
864 );
865 assert_eq!(
866 state_to_background_color(MachineState::Completed),
867 colors::STATUS_SUCCESS_BG
868 );
869 assert_eq!(
870 state_to_background_color(MachineState::Failed),
871 colors::STATUS_ERROR_BG
872 );
873 assert_eq!(
874 state_to_background_color(MachineState::Correcting),
875 colors::STATUS_CORRECTING_BG
876 );
877 }
878
879 #[test]
884 fn test_status_dot_default() {
885 let dot = StatusDot::default();
886 assert_eq!(dot.radius(), STATUS_DOT_RADIUS);
887 assert_eq!(dot.color(), colors::STATUS_IDLE);
888 }
889
890 #[test]
891 fn test_status_dot_from_status() {
892 let dot = StatusDot::from_status(Status::Running);
893 assert_eq!(dot.color(), colors::STATUS_RUNNING);
894 }
895
896 #[test]
897 fn test_status_dot_from_machine_state() {
898 let dot = StatusDot::from_machine_state(MachineState::Completed);
899 assert_eq!(dot.color(), colors::STATUS_SUCCESS);
900 }
901
902 #[test]
903 fn test_status_dot_with_radius() {
904 let dot = StatusDot::default().with_radius(8.0);
905 assert_eq!(dot.radius(), 8.0);
906 }
907
908 #[test]
909 fn test_status_dot_with_color() {
910 let custom_color = Color32::from_rgb(255, 0, 128);
911 let dot = StatusDot::with_color(custom_color);
912 assert_eq!(dot.color(), custom_color);
913 }
914
915 #[test]
920 fn test_progress_bar() {
921 assert!((ProgressBar::new(0.5).progress() - 0.5).abs() < 0.001);
922 assert_eq!(ProgressBar::new(-0.5).progress(), 0.0); assert_eq!(ProgressBar::new(1.5).progress(), 1.0); assert!(
925 (ProgressBar::from_progress(&RunProgress::new(3, 10)).progress() - 0.3).abs() < 0.001
926 );
927 }
928
929 #[test]
937 fn test_truncate_with_ellipsis_short_string() {
938 let result = truncate_with_ellipsis("short", 10);
939 assert_eq!(result, "short");
940 }
941
942 #[test]
943 fn test_truncate_with_ellipsis_exact_length() {
944 let result = truncate_with_ellipsis("exactly10!", 10);
945 assert_eq!(result, "exactly10!");
946 }
947
948 #[test]
949 fn test_truncate_with_ellipsis_long_string_word_boundary() {
950 let result = truncate_with_ellipsis("this is a very long string", 15);
952 assert_eq!(result, "this is a...");
953 assert!(result.len() <= 15);
954 }
955
956 #[test]
957 fn test_truncate_with_ellipsis_very_short_max() {
958 let result = truncate_with_ellipsis("hello", 3);
959 assert_eq!(result, "hel");
960 }
961
962 #[test]
963 fn test_truncate_with_ellipsis_empty_string() {
964 let result = truncate_with_ellipsis("", 10);
965 assert_eq!(result, "");
966 }
967
968 #[test]
969 fn test_truncate_with_ellipsis_no_space_fallback() {
970 let result = truncate_with_ellipsis("superlongword", 10);
972 assert_eq!(result, "superlo...");
973 assert_eq!(result.len(), 10);
974 }
975
976 #[test]
977 fn test_truncate_with_ellipsis_word_boundary_exact() {
978 let result = truncate_with_ellipsis("hello world", 11);
980 assert_eq!(result, "hello world");
981 }
982
983 #[test]
984 fn test_truncate_with_ellipsis_word_boundary_just_over() {
985 let result = truncate_with_ellipsis("hello world test", 14);
988 assert_eq!(result, "hello...");
989 }
990
991 #[test]
992 fn test_truncate_with_ellipsis_single_word_too_long() {
993 let result = truncate_with_ellipsis("internationalization", 15);
996 assert_eq!(result, "internationa...");
997 assert!(result.len() <= 15);
998 }
999
1000 #[test]
1001 fn test_truncate_with_ellipsis_preserves_short_content() {
1002 let result = truncate_with_ellipsis("ok", 10);
1004 assert_eq!(result, "ok");
1005 }
1006
1007 #[test]
1008 fn test_truncate_with_ellipsis_multiple_spaces() {
1009 let result = truncate_with_ellipsis("one two three four five", 16);
1011 assert_eq!(result, "one two...");
1012 }
1013
1014 #[test]
1015 fn test_truncate_with_ellipsis_trailing_space_trimmed() {
1016 let result = truncate_with_ellipsis("hello world", 10);
1018 assert_eq!(result, "hello...");
1019 }
1020
1021 #[test]
1026 fn test_strip_worktree_prefix_no_prefix() {
1027 assert_eq!(
1029 strip_worktree_prefix("feature/login", "myproject"),
1030 "feature/login"
1031 );
1032 assert_eq!(strip_worktree_prefix("main", "myproject"), "main");
1033 assert_eq!(
1034 strip_worktree_prefix("develop/new-feature", "myproject"),
1035 "develop/new-feature"
1036 );
1037 }
1038
1039 #[test]
1040 fn test_strip_worktree_prefix_standard_wt_prefix() {
1041 assert_eq!(
1043 strip_worktree_prefix("myproject-wt-feature/login", "myproject"),
1044 "feature/login"
1045 );
1046 assert_eq!(
1047 strip_worktree_prefix("autom8-wt-feature/gui-tabs", "autom8"),
1048 "feature/gui-tabs"
1049 );
1050 assert_eq!(
1052 strip_worktree_prefix("MyProject-wt-feature/test", "myproject"),
1053 "feature/test"
1054 );
1055 }
1056
1057 #[test]
1062 fn test_format_state_all_states() {
1063 assert_eq!(format_state(MachineState::Idle), "Idle");
1064 assert_eq!(format_state(MachineState::LoadingSpec), "Loading Spec");
1065 assert_eq!(
1066 format_state(MachineState::GeneratingSpec),
1067 "Generating Spec"
1068 );
1069 assert_eq!(format_state(MachineState::Initializing), "Initializing");
1070 assert_eq!(format_state(MachineState::PickingStory), "Picking Story");
1071 assert_eq!(format_state(MachineState::RunningClaude), "Running Claude");
1072 assert_eq!(format_state(MachineState::Reviewing), "Reviewing");
1073 assert_eq!(format_state(MachineState::Correcting), "Correcting");
1074 assert_eq!(format_state(MachineState::Committing), "Committing");
1075 assert_eq!(format_state(MachineState::CreatingPR), "Creating PR");
1076 assert_eq!(format_state(MachineState::Completed), "Completed");
1077 assert_eq!(format_state(MachineState::Failed), "Failed");
1078 }
1079
1080 #[test]
1085 fn test_status_label_new() {
1086 let label = StatusLabel::new(Status::Running, "Test Label");
1087 assert_eq!(label.status(), Status::Running);
1088 assert_eq!(label.label(), "Test Label");
1089 }
1090
1091 #[test]
1092 fn test_status_label_from_machine_state() {
1093 let label = StatusLabel::from_machine_state(MachineState::RunningClaude);
1094 assert_eq!(label.status(), Status::Running);
1095 assert_eq!(label.label(), "Running Claude");
1096 }
1097
1098 #[test]
1099 fn test_status_label_with_dot_radius() {
1100 let label = StatusLabel::new(Status::Success, "Done").with_dot_radius(8.0);
1101 assert_eq!(label.status(), Status::Success);
1103 }
1104
1105 #[test]
1106 fn test_status_label_with_spacing() {
1107 let label = StatusLabel::new(Status::Error, "Failed").with_spacing(12.0);
1108 assert_eq!(label.status(), Status::Error);
1109 }
1110
1111 #[test]
1116 fn test_badge_background_color() {
1117 let running_bg = badge_background_color(colors::STATUS_RUNNING);
1118 let success_bg = badge_background_color(colors::STATUS_SUCCESS);
1119 let error_bg = badge_background_color(colors::STATUS_ERROR);
1120
1121 for (name, bg) in [
1123 ("running", running_bg),
1124 ("success", success_bg),
1125 ("error", error_bg),
1126 ] {
1127 let lum = bg.r() as u32 + bg.g() as u32 + bg.b() as u32;
1128 assert!(
1129 lum > 600,
1130 "{} badge bg should be light, got luminance {}",
1131 name,
1132 lum
1133 );
1134 }
1135
1136 assert_ne!(running_bg, success_bg);
1138 assert_ne!(success_bg, error_bg);
1139 assert_ne!(running_bg, error_bg);
1140 }
1141}