Skip to main content

luxctl/
message.rs

1use colored::Colorize;
2use termimad::MadSkin;
3
4use crate::api::{PaginatedResponse, Project, Task, TaskStatus, Terminal};
5use crate::state::ActiveProject;
6use crate::tasks::{TestCase, TestResults};
7
8// status symbols for consistent output (matching ui.rs)
9const SYM_PASS: &str = "✓";
10const SYM_FAIL: &str = "✗";
11const SYM_PENDING: &str = "○";
12
13pub struct Message;
14
15impl Message {
16    pub fn greet(name: &str) {
17        let msg = format!(
18            "hello {}, welcome to {}!",
19            name.bold(),
20            "projectlighthouse".yellow()
21        );
22        println!("{}", msg);
23    }
24
25    pub fn say(msg: &str) {
26        println!("{}", msg);
27    }
28
29    pub fn cheer(msg: &str) {
30        println!("{}", msg.green());
31    }
32
33    pub fn complain(msg: &str) {
34        eprintln!("{}", msg.yellow());
35    }
36
37    pub fn oops(msg: &str) {
38        eprintln!("{}", msg.red());
39    }
40
41    pub fn print_projects(response: &PaginatedResponse<Project>) {
42        println!();
43        for (i, project) in response.data.iter().enumerate() {
44            Self::print_project(project, i);
45        }
46    }
47
48    fn print_project(project: &Project, index: usize) {
49        println!(
50            "{}. {} {}",
51            index,
52            project.name.bold(),
53            format!("/{}", project.slug).dimmed()
54        );
55        if let Some(desc) = &project.short_description {
56            println!("{}", desc);
57        }
58        println!();
59        println!();
60    }
61
62    pub fn print_project_detail(project: &Project) {
63        println!("  {} {}", "#".dimmed(), project.name.bold());
64
65        if let Some(desc) = &project.short_description {
66            println!("    {}", desc);
67        }
68
69        println!("    {} {}", "slug:".dimmed(), project.slug.dimmed());
70        println!("    {} {}", "url:".dimmed(), project.url().dimmed());
71
72        println!();
73
74        if let Some(tasks) = &project.tasks {
75            println!("  {} ({}):\n", "tasks".bold(), tasks.len());
76
77            let task_count = tasks.len();
78            for (index, task) in tasks.iter().enumerate() {
79                let is_last = index == task_count - 1;
80
81                let connector = if is_last { "└" } else { "├" };
82                let line_char = if is_last { " " } else { "│" };
83
84                let status_marker = match task.status {
85                    TaskStatus::ChallengeCompleted => format!(" {}", SYM_PASS).green().to_string(),
86                    TaskStatus::ChallengeFailed => format!(" {}", SYM_FAIL).red().to_string(),
87                    _ => String::new(),
88                };
89
90                println!(
91                    "    {}── {} {}  {}",
92                    connector.dimmed(),
93                    task.title.bold(),
94                    task.scores.dimmed(),
95                    status_marker
96                );
97
98                if !is_last {
99                    println!("    {}", line_char.dimmed());
100                }
101            }
102
103            if let Some(first_task) = tasks.first() {
104                println!();
105                println!("  {} {}", "next up:".dimmed(), first_task.title.bold());
106                println!();
107
108                let skin = MadSkin::default();
109                let rendered = format!("{}", skin.text(&first_task.description, None));
110                for line in rendered.lines() {
111                    println!("    {}", line);
112                }
113            }
114        }
115    }
116
117    pub fn print_terminals(terminals: &[Terminal]) {
118        if terminals.is_empty() {
119            println!("no terminals available");
120            return;
121        }
122
123        println!();
124        for (i, terminal) in terminals.iter().enumerate() {
125            println!(
126                "{}. {} {}",
127                i,
128                terminal.name.bold(),
129                format!("/{}", terminal.slug).dimmed()
130            );
131            if let Some(ref tier) = Some(&terminal.tier) {
132                println!("   {}", tier.dimmed());
133            }
134            println!();
135        }
136    }
137
138    pub fn print_terminal_detail(terminal: &Terminal) {
139        println!("  {} {}", "#".dimmed(), terminal.name.bold());
140        println!("    {} {}", "slug:".dimmed(), terminal.slug.dimmed());
141        println!("    {} {}", "tier:".dimmed(), terminal.tier.dimmed());
142
143        if terminal.has_blueprint() {
144            println!("    {} yes", "blueprint:".dimmed());
145        }
146        if terminal.has_test_files() {
147            println!("    {} yes", "test files:".dimmed());
148        }
149    }
150
151    pub fn print_task_header(task: &Task, detailed: bool) {
152        println!("{}", task.title.bold());
153
154        if detailed {
155            let skin = MadSkin::default();
156            let rendered = format!("{}", skin.text(&task.description, None));
157            for line in rendered.lines() {
158                println!("    {}", line);
159            }
160        }
161    }
162
163    pub fn print_task_detail(task: &Task, detailed: bool) {
164        println!("{}", task.title.bold());
165
166        if detailed {
167            println!();
168            let skin = MadSkin::default();
169            let rendered = format!("{}", skin.text(&task.description, None));
170            for line in rendered.lines() {
171                println!("  {}", line);
172            }
173        }
174    }
175
176    pub fn print_validators_start(_count: usize) {
177        println!("validating...");
178    }
179
180    pub fn print_test_case(test: &TestCase, index: usize) {
181        if test.passed() {
182            println!("{} #{} {}", SYM_PASS.green(), index + 1, test.name);
183        } else {
184            println!("{} #{} {}", SYM_FAIL.red(), index + 1, test.name.red());
185
186            if test.message() != test.name {
187                // truncate long error messages for display
188                let msg = test.message();
189                let display_msg = if msg.len() > 600 {
190                    format!("{}...", &msg[..600])
191                } else {
192                    msg.to_string()
193                };
194                println!("  {}", display_msg.red());
195            }
196        }
197    }
198
199    pub fn print_test_results(results: &TestResults) {
200        if results.all_passed() {
201            println!(
202                "{}",
203                format!("all {} tests passed!", results.total()).green()
204            );
205        } else {
206            println!(
207                "{}",
208                format!("{}/{} tests passed", results.passed(), results.total()).red()
209            );
210        }
211    }
212
213    pub fn print_connection_error(port: u16) {
214        Self::oops(&format!("could not connect to server on port {}", port));
215        println!("  make sure your server is running:");
216        println!("  {}", "./your-server".dimmed());
217    }
218
219    pub fn print_task_list(project: &ActiveProject) {
220        println!("tasks for: {}\n", project.name.bold());
221
222        println!(
223            "{}  {}  {}  {}",
224            "#".dimmed(),
225            format!("{:>6}", "Points").dimmed(),
226            "Status".dimmed(),
227            "Task".dimmed()
228        );
229
230        for (i, task) in project.tasks.iter().enumerate() {
231            let (status, status_color) = match task.status {
232                TaskStatus::ChallengeCompleted => (SYM_PASS.to_string(), "green"),
233                TaskStatus::ChallengeFailed => (SYM_FAIL.to_string(), "red"),
234                TaskStatus::Challenged => (SYM_PENDING.to_string(), "yellow"),
235                TaskStatus::ChallengeAwaits | TaskStatus::ChallengeAbandoned => {
236                    (SYM_PENDING.to_string(), "white")
237                }
238            };
239
240            let status_display = match status_color {
241                "green" => format!("  {}   ", status).green().to_string(),
242                "red" => format!("  {}   ", status).red().to_string(),
243                "yellow" => format!("  {}   ", status).yellow().to_string(),
244                _ => format!("  {}   ", status).dimmed().to_string(),
245            };
246
247            let index = format!("{:02}", i + 1);
248            let points = format!("{:>6}", task.points);
249            println!(
250                "{}  {}  {}  {}",
251                index.dimmed(),
252                points.bold(),
253                status_display,
254                task.title
255            );
256        }
257
258        println!();
259        println!(
260            "progress: {}/{} completed | {} XP earned",
261            project.completed_count().to_string().bold(),
262            project.tasks.len(),
263            format!("{}/{}", project.earned_points(), project.total_points()).bold()
264        );
265    }
266
267    pub fn print_points_earned(points: i32) {
268        if points > 0 {
269            println!("{}", format!("+{} XP", points).bold().green());
270        }
271    }
272}
273
274/// Welcome/greet the user
275#[macro_export]
276macro_rules! greet {
277    ($name:expr) => {
278        $crate::message::Message::greet($name)
279    };
280}
281
282/// General info message
283#[macro_export]
284macro_rules! say {
285    ($msg:expr) => {
286        $crate::message::Message::say($msg)
287    };
288    ($fmt:expr, $($arg:tt)*) => {
289        $crate::message::Message::say(&format!($fmt, $($arg)*))
290    };
291}
292
293/// Success message
294#[macro_export]
295macro_rules! cheer {
296    ($msg:expr) => {
297        $crate::message::Message::cheer($msg)
298    };
299    ($fmt:expr, $($arg:tt)*) => {
300        $crate::message::Message::cheer(&format!($fmt, $($arg)*))
301    };
302}
303
304/// Warning message
305#[macro_export]
306macro_rules! complain {
307    ($msg:expr) => {
308        $crate::message::Message::complain($msg)
309    };
310    ($fmt:expr, $($arg:tt)*) => {
311        $crate::message::Message::complain(&format!($fmt, $($arg)*))
312    };
313}
314
315/// Error message
316#[macro_export]
317macro_rules! oops {
318    ($msg:expr) => {
319        $crate::message::Message::oops($msg)
320    };
321    ($fmt:expr, $($arg:tt)*) => {
322        $crate::message::Message::oops(&format!($fmt, $($arg)*))
323    };
324}