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
8const 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 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#[macro_export]
276macro_rules! greet {
277 ($name:expr) => {
278 $crate::message::Message::greet($name)
279 };
280}
281
282#[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#[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#[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#[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}