1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
//! Plain fallback shown when bare `rproj` is not attached to a terminal.
/// Command rows. Kept as data so the column stays aligned by construction -
/// hand-padded columns in a string literal drift the moment one row
/// changes, and nothing fails when they do.
const COMMANDS: &[(&str, &str, &[&str])] = &[
(
"๐",
"rproj new <name>",
&[
"Scaffold a project. Sets your machine up first if it",
"hasn't been, then asks only about this project's packages.",
"--like <setup> reuses a saved selection, --save-setup saves one",
],
),
(
"๐ง",
"rproj setup",
&[
"Install or change the machine-wide tools: system apps, CLI",
"tools, Studio plugins, editor extensions",
],
),
(
// Every icon here is a plain double-width emoji with no variation
// selector. A selector-carrying one (โ๏ธ, ๐ ๏ธ) is two `char`s wide but
// one or two *columns* depending on the console, so no padding
// arithmetic can keep the column straight for it.
"๐ฉ",
"rproj configure [key]",
&[
"Walk through a tool's settings one at a time - StyLua,",
"Selene, luau-lsp - or edit the tree future projects inherit",
],
),
(
"๐",
"rproj upgrade",
&[
"Re-apply rproj's generated config to a project you already made,",
"so it picks up fixes shipped since it was scaffolded",
],
),
(
"๐",
"rproj watch",
&["Resume the dev loop: install what's missing, watch the sourcemap"],
),
(
"๐งช",
"rproj test [args]",
&["Restore dependencies and run the project's selected test runner"],
),
(
"๐",
"rproj copy",
&["Copy every file under src/ to the clipboard, with path headers"],
),
(
"๐",
"rproj info [key]",
&["Look up what a tool or package does, and how to use it"],
),
];
pub fn run() {
let version = env!("CARGO_PKG_VERSION");
let (rocket, book, sparkle) = ("๐ฎ ", "๐ ", "โจ ");
println!("\n{rocket}rproj {version} - guided bootstrap-to-game-dev CLI for Roblox\n");
println!(" Takes a fresh PC all the way to a working Roblox dev setup, then");
println!(" scaffolds projects on top of it. Explains every tool and package");
println!(" as it goes, so you end up knowing why your setup looks like it does.\n");
println!("{book}Commands\n");
let width = COMMANDS
.iter()
.map(|(_, name, _)| name.len())
.max()
.unwrap_or(0);
// Terminal *columns* the icon occupies, not `char`s: every icon above is
// one char and two columns wide, plus two spaces of gutter.
let icon_columns = 4;
for (icon, name, lines) in COMMANDS {
for (i, line) in lines.iter().enumerate() {
let lead = match i {
0 => format!("{icon} "),
// Continuation rows leave the icon and name columns blank,
// so a multi-line description reads as one paragraph rather
// than as several commands.
_ => " ".repeat(icon_columns),
};
let name = if i == 0 { *name } else { "" };
println!(" {lead}{name:<width$} {line}");
}
println!();
}
println!("{sparkle}New here? rproj new my-first-game sets your machine up and");
println!(" scaffolds your first project in one go.\n");
}
#[cfg(test)]
mod tests {
use super::*;
/// The icon column is padded by a fixed number of terminal columns, on
/// the assumption that every icon is one `char` and two columns wide.
/// A variation-selector emoji (โ๏ธ, ๐ ๏ธ) is two `char`s and an
/// unpredictable number of columns, and adding one silently bends the
/// description column out of line on every continuation row.
#[test]
fn every_icon_is_a_single_char() {
for (icon, name, _) in COMMANDS {
assert_eq!(
icon.chars().count(),
1,
"{name}'s icon {icon} is not a single char"
);
}
}
/// Every command clap accepts should be findable here - the welcome
/// screen is the only place someone who typed `rproj` learns they exist.
#[test]
fn every_command_is_listed() {
for command in [
"new",
"setup",
"configure",
"upgrade",
"watch",
"test",
"copy",
"info",
] {
assert!(
COMMANDS
.iter()
.any(|(_, name, _)| name.starts_with(&format!("rproj {command}"))),
"`{command}` is missing from the welcome screen"
);
}
}
/// An empty description row would print a name with nothing after it.
#[test]
fn every_command_explains_itself() {
for (_, name, lines) in COMMANDS {
assert!(!lines.is_empty(), "{name} has no description");
assert!(
lines.iter().all(|l| !l.trim().is_empty()),
"{name} has a blank line"
);
}
}
}