Skip to content

Commit 1ba8be9

Browse files
committed
feat: Add dochia legend command
1 parent 52336a1 commit 1ba8be9

3 files changed

Lines changed: 233 additions & 1 deletion

File tree

src/main/java/dev/dochia/cli/core/command/DochiaCommand.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@
6565
TestCommand.class,
6666
InfoCommand.class,
6767
RandomCommand.class,
68-
ExplainCommand.class
68+
ExplainCommand.class,
69+
LegendCommand.class
6970
})
7071
@Unremovable
7172
@Singleton
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package dev.dochia.cli.core.command;
2+
3+
import picocli.CommandLine.Command;
4+
import picocli.CommandLine.Option;
5+
import picocli.CommandLine.ParentCommand;
6+
7+
import java.nio.charset.StandardCharsets;
8+
import java.time.Instant;
9+
import java.util.Random;
10+
import java.util.concurrent.Callable;
11+
12+
@Command(
13+
name = "legend",
14+
description = "Tells the Dochia legend. Short version.",
15+
mixinStandardHelpOptions = true,
16+
hidden = true,
17+
showDefaultValues = true
18+
)
19+
public class LegendCommand implements Callable<Integer> {
20+
21+
@Option(names = "--haiku", description = "Force haiku output.")
22+
boolean haiku;
23+
24+
@Option(names = "--ascii", description = "Force ASCII mini output.")
25+
boolean ascii;
26+
27+
@Option(names = "--seed", description = "Deterministic selection seed.")
28+
Long seed;
29+
30+
@Option(names = "--no-color", description = "Disable ANSI colors (or set NO_COLOR env).")
31+
boolean noColor;
32+
33+
@Option(names = "--width", description = "Target output width (for boxes).", defaultValue = "60")
34+
int width;
35+
36+
@ParentCommand
37+
Object parent; // optional: access parent config if needed later
38+
39+
private static final String[][] STORY_EN = new String[][]{
40+
{
41+
"─────────────────────────────",
42+
" Dochia’s Legend",
43+
"─────────────────────────────",
44+
"Baba Dochia climbed the mountain,",
45+
"wearing nine coats, one on top of another.",
46+
"She thought spring had come,",
47+
"so she shed them one by one.",
48+
"",
49+
"Winter came back. She froze.",
50+
"… and turned to stone. 🪨",
51+
"",
52+
"Moral of the story?",
53+
"Never trust the weather forecast,",
54+
"and always test your edge cases. 😉",
55+
"",
56+
"─────────────────────────────",
57+
"Tip: Run `dochia --chaos` to shed",
58+
"some coats of your own.",
59+
"─────────────────────────────"
60+
}
61+
};
62+
63+
private static final String[][] HAIKU_EN = new String[][]{
64+
{"Nine coats on the hill,", "Spring fooled her, winter returned —", "Stone remembers all."}
65+
};
66+
67+
private static final String[][] ASCII_EN = new String[][]{
68+
{
69+
" /\\ Baba Dochia climbed high,",
70+
" / \\ dropping her nine coats…",
71+
" / ❄ \\ …then winter laughed.",
72+
"/______\\ Moral: never trust inputs. 😉"
73+
}
74+
};
75+
76+
@Override
77+
public Integer call() throws Exception {
78+
final boolean ansi = !noColor && System.getenv("NO_COLOR") == null;
79+
80+
81+
Variant chosen = chooseVariant();
82+
String[][] lines = resolveLines(chosen);
83+
84+
printBoxed(lines, ansi);
85+
return 0;
86+
}
87+
88+
private enum Variant {STORY, HAIKU, ASCII}
89+
90+
private Variant chooseVariant() {
91+
if (haiku && ascii) return Variant.HAIKU; // prefer explicit & deterministic
92+
if (haiku) return Variant.HAIKU;
93+
if (ascii) return Variant.ASCII;
94+
95+
// Random choice for the easter egg when no explicit style is set
96+
Random rnd = (seed != null) ? new Random(seed) : seededRandom();
97+
int pick = rnd.nextInt(3);
98+
return pick == 0 ? Variant.STORY : (pick == 1 ? Variant.HAIKU : Variant.ASCII);
99+
}
100+
101+
private Random seededRandom() {
102+
// Derive a seed from current time, PID hash & some bytes to keep variety but reproducible per run
103+
long t = Instant.now().toEpochMilli();
104+
long mix = t ^ System.identityHashCode(this);
105+
return new Random(mix);
106+
}
107+
108+
private String[][] resolveLines(Variant v) {
109+
return switch (v) {
110+
case STORY -> STORY_EN;
111+
case HAIKU -> HAIKU_EN;
112+
case ASCII -> ASCII_EN;
113+
};
114+
}
115+
116+
private void printBoxed(String[][] content, boolean ansi) {
117+
final String top = repeat("─", Math.max(20, Math.min(120, width)));
118+
// Only add colored title bars for STORY; keep HAIKU/ASCII clean
119+
boolean banner = content[0].length > 8; // crude but effective
120+
121+
if (banner && ansi) {
122+
System.out.println("\u001B[2m" + top + "\u001B[0m");
123+
} else if (banner) {
124+
System.out.println(top);
125+
}
126+
127+
for (String line : content[0]) {
128+
System.out.println(line);
129+
}
130+
131+
if (banner && ansi) {
132+
System.out.println("\u001B[2m" + top + "\u001B[0m");
133+
} else if (banner) {
134+
System.out.println(top);
135+
}
136+
}
137+
138+
private static String repeat(String s, int n) {
139+
byte[] unit = s.getBytes(StandardCharsets.UTF_8);
140+
byte[] out = new byte[unit.length * n];
141+
for (int i = 0; i < n; i++) {
142+
System.arraycopy(unit, 0, out, i * unit.length, unit.length);
143+
}
144+
return new String(out, StandardCharsets.UTF_8);
145+
}
146+
}
147+
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package dev.dochia.cli.core.command;
2+
3+
import io.quarkus.test.junit.QuarkusTest;
4+
import org.junit.jupiter.api.AfterEach;
5+
import org.junit.jupiter.api.BeforeEach;
6+
import org.junit.jupiter.api.Test;
7+
import picocli.CommandLine;
8+
9+
import java.io.ByteArrayOutputStream;
10+
import java.io.PrintStream;
11+
import java.nio.charset.StandardCharsets;
12+
13+
import static org.assertj.core.api.Assertions.assertThat;
14+
15+
@QuarkusTest
16+
class LegendCommandTest {
17+
18+
private PrintStream originalOut;
19+
private ByteArrayOutputStream outContent;
20+
21+
@BeforeEach
22+
void setUp() {
23+
originalOut = System.out;
24+
outContent = new ByteArrayOutputStream();
25+
System.setOut(new PrintStream(outContent, true, StandardCharsets.UTF_8));
26+
}
27+
28+
@AfterEach
29+
void tearDown() {
30+
System.setOut(originalOut);
31+
}
32+
33+
private String run(String... args) {
34+
LegendCommand cmd = new LegendCommand();
35+
int exit = new CommandLine(cmd).execute(args);
36+
assertThat(exit).isZero();
37+
return outContent.toString(StandardCharsets.UTF_8);
38+
}
39+
40+
@Test
41+
void haiku_en_outputsExpectedThreeLines_andNoAnsiWhenNoColor() {
42+
String out = run("--haiku", "--no-color");
43+
44+
// Assert key haiku lines are present
45+
assertThat(out)
46+
.contains("Nine coats on the hill,")
47+
.contains("Spring fooled her, winter returned —")
48+
.contains("Stone remembers all.");
49+
50+
// Ensure no ANSI escape sequences when --no-color is set
51+
assertThat(out).doesNotContain("\u001B[");
52+
}
53+
54+
@Test
55+
void ascii_en_containsAsciiArt_andPunchline_andNoAnsiWhenNoColor() {
56+
String out = run("--ascii", "--no-color");
57+
58+
assertThat(out)
59+
.contains(" /\\ Baba Dochia climbed high,")
60+
.contains(" / \\ dropping her nine coats…")
61+
.contains(" / ❄ \\ …then winter laughed.")
62+
.contains("/______\\ Moral: never trust inputs. 😉");
63+
64+
assertThat(out).doesNotContain("\u001B[");
65+
}
66+
67+
@Test
68+
void defaultVariant_en_isOneOfKnownOutputs_andNoAnsiWhenNoColor() {
69+
String out = run("--no-color", "--seed", "42");
70+
71+
assertThat(out).satisfiesAnyOf(
72+
s -> assertThat(s).contains("Dochia’s Legend")
73+
.contains("Baba Dochia climbed the mountain,")
74+
.contains("Never trust the weather forecast,"),
75+
s -> assertThat(s).contains("Nine coats on the hill,")
76+
.contains("Stone remembers all."),
77+
s -> assertThat(s).contains("Baba Dochia climbed high,")
78+
.contains("Moral: never trust inputs.")
79+
);
80+
81+
assertThat(out).doesNotContain("\u001B[");
82+
}
83+
}
84+

0 commit comments

Comments
 (0)