From 810c47df3f37799524e620922bf0976e153263d4 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Tue, 20 May 2025 22:53:55 +0000
Subject: [PATCH] feat: Add Unix pipe support for draw command
This change allows the `tnoodle draw` command to read a scramble string
from standard input if the `-s` or `--scramble` parameter is not explicitly
provided. This enables piping the output of `tnoodle scramble` directly to
`tnoodle draw`.
Key changes:
- Modified `DrawCommand.java` to make the `scramble` parameter optional and
to read from `System.in` if the scramble is not given as an argument.
- Added robust error handling for cases such as empty stdin or invalid
scramble input.
- Introduced `DrawCommandTest.java` with unit tests:
- `testDrawCommand_readsFromStdin`: Verifies reading a scramble directly
from stdin.
- `testFullPipeWorkflow_scrambleToDraw`: Verifies the complete workflow of
piping `tnoodle scramble` output to `tnoodle draw`.
- Updated `README.md` to document the new piping feature with an example:
`tnoodle scramble -p three | tnoodle draw -p three > output.svg`.
---
README.md | 9 ++
.../java/tnoodlecli/commands/DrawCommand.java | 44 +++++-
.../tnoodlecli/commands/DrawCommandTest.java | 125 ++++++++++++++++++
3 files changed, 175 insertions(+), 3 deletions(-)
create mode 100644 src/test/java/tnoodlecli/commands/DrawCommandTest.java
diff --git a/README.md b/README.md
index 64ec8b0..58bee7e 100644
--- a/README.md
+++ b/README.md
@@ -68,6 +68,15 @@ L' F B2 L F2 B2 L' F2 L2 R B2 U' B' D' L2 D' F2 B2 R2 Fw2 U Rw2 Uw2 R' U' Rw2 R
```

+#### Piping Scrambles to Draw
+
+You can directly pipe the output of the `scramble` command to the `draw` command. This is useful for quickly visualizing a freshly generated scramble. When providing the scramble via standard input (stdin) like this, you can omit the `-s` or `--scramble` option for the `draw` command.
+
+```bash
+tnoodle scramble -p three | tnoodle draw -p three > output.svg
+```
+This command will generate a scramble for the 3x3x3 cube (`three`), send that scramble directly to the `draw` command (which also expects a 3x3x3 cube scramble), and save the resulting SVG image to `output.svg`.
+
## Legal
***Disclaimer:** This CLI tool is an **unofficial** project currently unaffiliated with the WCA. Scrambles generated by this tool are NOT authorized for use in any official WCA event. All such scrambles must be generated using the [official TNoodle program](https://www.worldcubeassociation.org/regulations/scrambles/).*
diff --git a/src/main/java/tnoodlecli/commands/DrawCommand.java b/src/main/java/tnoodlecli/commands/DrawCommand.java
index 139e990..a9ddc0c 100644
--- a/src/main/java/tnoodlecli/commands/DrawCommand.java
+++ b/src/main/java/tnoodlecli/commands/DrawCommand.java
@@ -19,7 +19,9 @@
import java.io.File;
import java.io.FileNotFoundException;
+import java.io.IOException;
import java.io.PrintWriter;
+import java.util.Scanner;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
@@ -47,7 +49,7 @@ public class DrawCommand implements Runnable {
@Parameter(
names = { "-s", "--scramble" },
description = "The scramble to draw.",
- required = true
+ required = false
)
private String scramble;
@@ -56,6 +58,40 @@ public class DrawCommand implements Runnable {
public void run() {
try {
+ if (scramble == null || scramble.isEmpty()) {
+ try {
+ if (System.in.available() > 0) {
+ Scanner scanner = new Scanner(System.in);
+ if (scanner.hasNextLine()) {
+ scramble = scanner.nextLine();
+ if (scramble.trim().isEmpty()) {
+ System.err.println("Error: Received empty scramble from standard input.");
+ System.exit(1);
+ }
+ } else {
+ // This case means stdin had available bytes but nothing was read by nextLine (e.g. only EOF)
+ System.err.println("Error: Standard input was open but no scramble string was provided.");
+ System.exit(1);
+ }
+ // We don't close the scanner for System.in here
+ } else {
+ // System.in.available() <= 0 means no input from stdin when -s is not used
+ System.err.println("Error: No scramble provided via -s/--scramble and no input detected on standard input.");
+ System.exit(1);
+ }
+ } catch (IOException e) {
+ // Handle IOException from System.in.available()
+ System.err.println("Error accessing standard input: " + e.getMessage());
+ System.exit(1);
+ }
+ }
+
+ // This check is now more of a safeguard, as the logic above should handle most empty/null cases.
+ if (scramble == null || scramble.trim().isEmpty()) {
+ System.err.println("Error: Scramble string not provided or is empty.");
+ System.exit(1);
+ }
+
Svg svg = PuzzleRegistry.valueOf(puzzle.toUpperCase()).getScrambler().drawScramble(scramble, null);
if (output == null) {
System.out.println(svg.toString());
@@ -64,11 +100,13 @@ public void run() {
try (PrintWriter writer = new PrintWriter(outputFile)){
writer.print(svg.toString());
} catch (FileNotFoundException e) {
- e.printStackTrace();
+ e.printStackTrace(); // Or a more user-friendly message + System.exit(1)
}
}
} catch (InvalidScrambleException e1) {
- e1.printStackTrace();
+ System.err.println("Error: Invalid scramble string: " + e1.getMessage());
+ // e1.printStackTrace(); // Optionally print stack trace for more detail
+ System.exit(1);
}
}
}
diff --git a/src/test/java/tnoodlecli/commands/DrawCommandTest.java b/src/test/java/tnoodlecli/commands/DrawCommandTest.java
new file mode 100644
index 0000000..12d77fb
--- /dev/null
+++ b/src/test/java/tnoodlecli/commands/DrawCommandTest.java
@@ -0,0 +1,125 @@
+package tnoodlecli.commands;
+
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.io.PrintStream;
+
+import com.beust.jcommander.JCommander;
+import tnoodlecli.commands.ScrambleCommand; // Added import
+
+/**
+ * Unit tests for the DrawCommand class.
+ */
+public class DrawCommandTest {
+
+ @Test
+ void testDrawCommand_readsFromStdin() {
+ String scrambleString = "R U R' U'";
+ String puzzleName = "THREE"; // Corresponds to 3x3x3 cube
+
+ InputStream originalIn = System.in;
+ PrintStream originalOut = System.out;
+
+ try {
+ // Simulate stdin
+ System.setIn(new ByteArrayInputStream(scrambleString.getBytes()));
+
+ // Capture stdout
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ System.setOut(new PrintStream(baos));
+
+ // Execute the command
+ DrawCommand drawCommand = new DrawCommand();
+
+ // Use JCommander to parse arguments and set the 'puzzle' field.
+ // 'scramble' field will remain null as it's not provided.
+ JCommander.newBuilder()
+ .addObject(drawCommand)
+ .build()
+ .parse("-p", puzzleName);
+
+ drawCommand.run();
+
+ // Assert the output
+ String output = baos.toString();
+ assertNotNull(output, "Output should not be null.");
+ assertFalse(output.isEmpty(), "Output should not be empty.");
+ assertTrue(output.contains("'.");
+ assertTrue(output.contains("width="), "Output should contain 'width='.");
+ assertTrue(output.contains("height="), "Output should contain 'height='.");
+ // Check for potential error messages instead of SVG content
+ assertFalse(output.contains("Error:"), "Output should not contain 'Error:'.");
+
+ } finally {
+ // Restore System.in and System.out
+ System.setIn(originalIn);
+ System.setOut(originalOut);
+ }
+ }
+}
+
+ @Test
+ void testFullPipeWorkflow_scrambleToDraw() {
+ String puzzleName = "THREE"; // Corresponds to 3x3x3 cube
+
+ InputStream originalIn = System.in;
+ PrintStream originalOut = System.out;
+ ByteArrayOutputStream scrambleBaos = new ByteArrayOutputStream();
+ ByteArrayOutputStream drawBaos = new ByteArrayOutputStream();
+
+ try {
+ // === ScrambleCommand Phase ===
+ System.setOut(new PrintStream(scrambleBaos));
+
+ ScrambleCommand scrambleCommand = new ScrambleCommand();
+ JCommander.newBuilder()
+ .addObject(scrambleCommand)
+ .build()
+ .parse("-p", puzzleName, "-c", "1"); // Set puzzle and count to 1
+
+ scrambleCommand.run();
+ String generatedScramble = scrambleBaos.toString().trim(); // Trim to remove trailing newline
+
+ // Restore System.out before DrawCommand potentially prints errors to console during setup
+ System.setOut(originalOut);
+
+ // Ensure scramble is not empty
+ assertNotNull(generatedScramble, "Generated scramble should not be null.");
+ assertFalse(generatedScramble.isEmpty(), "Generated scramble should not be empty.");
+
+ // === DrawCommand Phase ===
+ System.setIn(new ByteArrayInputStream(generatedScramble.getBytes()));
+ System.setOut(new PrintStream(drawBaos)); // Capture DrawCommand output
+
+ DrawCommand drawCommand = new DrawCommand();
+ JCommander.newBuilder()
+ .addObject(drawCommand)
+ .build()
+ .parse("-p", puzzleName); // Set puzzle, scramble comes from stdin
+
+ drawCommand.run();
+ String svgOutput = drawBaos.toString();
+
+ // Assert the SVG output
+ assertNotNull(svgOutput, "SVG output should not be null.");
+ assertFalse(svgOutput.isEmpty(), "SVG output should not be empty.");
+ assertTrue(svgOutput.contains("'.");
+ assertTrue(svgOutput.contains("width="), "SVG output should contain 'width='.");
+ assertTrue(svgOutput.contains("height="), "SVG output should contain 'height='.");
+ assertFalse(svgOutput.contains("Error:"), "SVG output should not contain 'Error:'.");
+
+ } finally {
+ // Restore System.in and System.out
+ System.setIn(originalIn);
+ System.setOut(originalOut);
+ }
+ }
+}