Add Help component for CLI usage and input validation support
- Introduced `Help` class for generating CLI help/usage blocks with options, descriptions, and customizable formatting. - Enhanced `Prompt` and `PromptBuilder` with validation capabilities using predicates and retry messages. - Added terminal control methods `clearScreen` and `cursorTo` with ANSI support. - Updated examples and documentation (`Examples.md`, `Components.md`) to showcase the new features. - Extended `Examples` class with a `helpBlock` method for demonstrating the `Help` usage.
This commit is contained in:
@@ -205,6 +205,27 @@ public final class Terminal {
|
||||
return new Pager(DEFAULT.getSupport());
|
||||
}
|
||||
|
||||
/** Returns a help/usage builder for CLI --help output. */
|
||||
public static Help help() {
|
||||
return new Help(DEFAULT.getSupport());
|
||||
}
|
||||
|
||||
/** Clears the terminal screen (ANSI). No-op if ANSI is disabled. */
|
||||
public static void clearScreen() {
|
||||
if (DEFAULT.getSupport().isAnsiEnabled()) {
|
||||
System.out.print(dev.jakub.terminal.core.Ansi.CLEAR_SCREEN);
|
||||
System.out.flush();
|
||||
}
|
||||
}
|
||||
|
||||
/** Moves the cursor to the given 1-based row and column (ANSI). No-op if ANSI is disabled. */
|
||||
public static void cursorTo(int row, int col) {
|
||||
if (DEFAULT.getSupport().isAnsiEnabled()) {
|
||||
System.out.print(dev.jakub.terminal.core.Ansi.cursorTo(row, col));
|
||||
System.out.flush();
|
||||
}
|
||||
}
|
||||
|
||||
TerminalSupport getSupport() {
|
||||
return support;
|
||||
}
|
||||
|
||||
87
src/main/java/dev/jakub/terminal/components/Help.java
Normal file
87
src/main/java/dev/jakub/terminal/components/Help.java
Normal file
@@ -0,0 +1,87 @@
|
||||
package dev.jakub.terminal.components;
|
||||
|
||||
import dev.jakub.terminal.Terminal;
|
||||
import dev.jakub.terminal.core.TerminalSupport;
|
||||
|
||||
import java.io.PrintStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* CLI help / usage block. Use via {@link Terminal#help()}.
|
||||
* Formats options and descriptions for a typical --help output.
|
||||
*/
|
||||
public final class Help {
|
||||
|
||||
private static final int DEFAULT_OPTION_WIDTH = 24;
|
||||
|
||||
private final TerminalSupport support;
|
||||
private final List<String> options = new ArrayList<>();
|
||||
private final List<String> descriptions = new ArrayList<>();
|
||||
private String title = "Usage";
|
||||
private int optionWidth = DEFAULT_OPTION_WIDTH;
|
||||
private PrintStream out = System.out;
|
||||
|
||||
public Help(TerminalSupport support) {
|
||||
this.support = support;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the title (e.g. "Usage" or "Options").
|
||||
*/
|
||||
public Help title(String title) {
|
||||
this.title = title != null ? title : "";
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an option with description (e.g. "-v, --verbose" and "Enable verbose output").
|
||||
*/
|
||||
public Help option(String option, String description) {
|
||||
options.add(option != null ? option : "");
|
||||
descriptions.add(description != null ? description : "");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the width of the option column (default 24).
|
||||
*/
|
||||
public Help optionWidth(int width) {
|
||||
this.optionWidth = Math.max(8, width);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets output stream (default: stdout).
|
||||
*/
|
||||
public Help output(PrintStream out) {
|
||||
this.out = out != null ? out : System.out;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the help block to the given stream.
|
||||
*/
|
||||
public void print(PrintStream stream) {
|
||||
if (stream == null) stream = System.out;
|
||||
if (!title.isEmpty()) {
|
||||
stream.println(title);
|
||||
stream.println();
|
||||
}
|
||||
int ow = Math.max(optionWidth, 8);
|
||||
for (int i = 0; i < options.size(); i++) {
|
||||
String opt = options.get(i);
|
||||
String desc = i < descriptions.size() ? descriptions.get(i) : "";
|
||||
String padded = opt.length() <= ow ? opt + " ".repeat(ow - opt.length()) : opt;
|
||||
stream.print(" " + padded + " ");
|
||||
stream.println(desc);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints to stdout.
|
||||
*/
|
||||
public void print() {
|
||||
print(out);
|
||||
}
|
||||
}
|
||||
@@ -61,5 +61,15 @@ public final class Ansi {
|
||||
/** Clear line */
|
||||
public static final String ERASE_LINE = "\u001B[2K";
|
||||
|
||||
/** Clear entire screen and move cursor to home (1,1) */
|
||||
public static final String CLEAR_SCREEN = "\u001B[H\u001B[2J";
|
||||
|
||||
/**
|
||||
* Cursor position (1-based row and column). ESC [ row ; col H
|
||||
*/
|
||||
public static String cursorTo(int row, int col) {
|
||||
return "\u001B[" + Math.max(1, row) + ";" + Math.max(1, col) + "H";
|
||||
}
|
||||
|
||||
private Ansi() {}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ public final class Examples {
|
||||
columns();
|
||||
steps();
|
||||
breadcrumb();
|
||||
helpBlock();
|
||||
log();
|
||||
logWithMinLevel();
|
||||
diff();
|
||||
@@ -128,6 +129,17 @@ public final class Examples {
|
||||
System.out.println();
|
||||
}
|
||||
|
||||
static void helpBlock() {
|
||||
Terminal.print("=== Help (CLI usage) ===").color(Color.CYAN).bold().println();
|
||||
Terminal.help()
|
||||
.title("Options")
|
||||
.option("-v, --verbose", "Enable verbose output")
|
||||
.option("-h, --help", "Show this help")
|
||||
.option("--file <path>", "Input file path")
|
||||
.print(System.out);
|
||||
System.out.println();
|
||||
}
|
||||
|
||||
static void log() {
|
||||
Terminal.print("=== Log ===").color(Color.CYAN).bold().println();
|
||||
Terminal.log()
|
||||
|
||||
@@ -6,6 +6,7 @@ import dev.jakub.terminal.core.TerminalSupport;
|
||||
import java.io.InputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.util.Scanner;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* Input prompt for reading user input. Use via {@link Terminal#prompt(String)}.
|
||||
@@ -18,6 +19,8 @@ public final class Prompt {
|
||||
private boolean masked;
|
||||
private PrintStream out = System.out;
|
||||
private InputStream in = System.in;
|
||||
private Predicate<String> validator;
|
||||
private String retryMessage = "Invalid, try again: ";
|
||||
|
||||
public Prompt(String message, TerminalSupport support) {
|
||||
this.message = message != null ? message : "";
|
||||
@@ -48,33 +51,62 @@ public final class Prompt {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates input: asks again until the predicate returns true. Null = no validation.
|
||||
*/
|
||||
public Prompt validate(Predicate<String> validator) {
|
||||
this.validator = validator;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Message shown when validation fails (default: "Invalid, try again: ").
|
||||
*/
|
||||
public Prompt retryMessage(String retryMessage) {
|
||||
this.retryMessage = retryMessage != null ? retryMessage : "Invalid, try again: ";
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompts and returns the entered string. Blocks until a line is read.
|
||||
* If {@link #validate(Predicate)} is set, repeats until the predicate accepts the input.
|
||||
*/
|
||||
public String ask() {
|
||||
out.print(message);
|
||||
out.flush();
|
||||
if (masked) {
|
||||
return readMasked();
|
||||
}
|
||||
try (Scanner scan = new Scanner(in)) {
|
||||
return scan.hasNextLine() ? scan.nextLine() : "";
|
||||
return askLoop(scan);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompts and returns the next line from the given scanner. Use this when
|
||||
* sharing one scanner (e.g. in an interactive app) so input blocks correctly.
|
||||
* If {@link #validate(Predicate)} is set, repeats until the predicate accepts the input.
|
||||
*/
|
||||
public String ask(Scanner sharedScanner) {
|
||||
if (sharedScanner == null) return ask();
|
||||
out.print(message);
|
||||
out.flush();
|
||||
if (masked && System.console() != null) {
|
||||
out.print(message);
|
||||
out.flush();
|
||||
char[] chars = System.console().readPassword();
|
||||
return chars != null ? new String(chars) : "";
|
||||
}
|
||||
return sharedScanner.hasNextLine() ? sharedScanner.nextLine() : "";
|
||||
return askLoop(sharedScanner);
|
||||
}
|
||||
|
||||
private String askLoop(Scanner scan) {
|
||||
while (true) {
|
||||
out.print(message);
|
||||
out.flush();
|
||||
if (!scan.hasNextLine()) return "";
|
||||
String line = scan.nextLine();
|
||||
if (line == null) line = "";
|
||||
if (validator == null || validator.test(line)) return line;
|
||||
out.print(retryMessage);
|
||||
out.flush();
|
||||
}
|
||||
}
|
||||
|
||||
private String readMasked() {
|
||||
|
||||
@@ -7,6 +7,7 @@ import dev.jakub.terminal.interactive.Prompt;
|
||||
import java.io.InputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.util.Scanner;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* Fluent builder for {@link Prompt}. Use via {@link Terminal#prompt(String)}.
|
||||
@@ -19,6 +20,8 @@ public final class PromptBuilder {
|
||||
private boolean masked;
|
||||
private PrintStream output = System.out;
|
||||
private InputStream input = System.in;
|
||||
private Predicate<String> validator;
|
||||
private String retryMessage = "Invalid, try again: ";
|
||||
|
||||
public PromptBuilder(String message, TerminalSupport support) {
|
||||
this.message = message != null ? message : "";
|
||||
@@ -49,14 +52,27 @@ public final class PromptBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates input: prompts again until the predicate returns true. Null = no validation.
|
||||
*/
|
||||
public PromptBuilder validate(Predicate<String> validator) {
|
||||
this.validator = validator;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Message shown when validation fails (default: "Invalid, try again: ").
|
||||
*/
|
||||
public PromptBuilder retryMessage(String retryMessage) {
|
||||
this.retryMessage = retryMessage != null ? retryMessage : "Invalid, try again: ";
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompts and returns the entered string.
|
||||
*/
|
||||
public String ask() {
|
||||
Prompt p = new Prompt(message, support);
|
||||
if (masked) p.masked();
|
||||
p.output(output);
|
||||
p.input(input);
|
||||
Prompt p = buildPrompt();
|
||||
return p.ask();
|
||||
}
|
||||
|
||||
@@ -66,10 +82,17 @@ public final class PromptBuilder {
|
||||
*/
|
||||
public String ask(Scanner sharedScanner) {
|
||||
if (sharedScanner == null) return ask();
|
||||
Prompt p = buildPrompt();
|
||||
return p.ask(sharedScanner);
|
||||
}
|
||||
|
||||
private Prompt buildPrompt() {
|
||||
Prompt p = new Prompt(message, support);
|
||||
if (masked) p.masked();
|
||||
p.output(output);
|
||||
p.input(input);
|
||||
return p.ask(sharedScanner);
|
||||
if (validator != null) p.validate(validator);
|
||||
p.retryMessage(retryMessage);
|
||||
return p;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user