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:
@@ -26,11 +26,12 @@ Overview of what terminal-ui provides. All entry points are on `Terminal.*`.
|
|||||||
|
|
||||||
| API | Description |
|
| API | Description |
|
||||||
|-----|-------------|
|
|-----|-------------|
|
||||||
| `Terminal.prompt(String)` | Text input (`.ask()`, `.masked().ask()`) |
|
| `Terminal.prompt(String)` | Text input (`.ask()`, `.masked().ask()`, `.validate(Predicate).retryMessage(String)`) |
|
||||||
| `Terminal.confirm(String)` | Y/n confirmation |
|
| `Terminal.confirm(String)` | Y/n confirmation |
|
||||||
| `Terminal.menu()` | Numbered menu (`.option()`, `.select()`) |
|
| `Terminal.menu()` | Numbered menu (`.option()`, `.select()`) |
|
||||||
| `Terminal.selectList()` | List with arrow keys + Enter |
|
| `Terminal.selectList()` | List with arrow keys + Enter |
|
||||||
| `Terminal.pager()` | Paged output (Enter/arrows/q) |
|
| `Terminal.pager()` | Paged output (Enter/arrows/q) |
|
||||||
|
| `Terminal.help()` | CLI usage block (`.option(opt, desc)`, `.title()`) |
|
||||||
|
|
||||||
## Live / progress
|
## Live / progress
|
||||||
|
|
||||||
@@ -50,6 +51,13 @@ Overview of what terminal-ui provides. All entry points are on `Terminal.*`.
|
|||||||
| `Terminal.heatmap()` | Heatmap |
|
| `Terminal.heatmap()` | Heatmap |
|
||||||
| `Terminal.chart()` | Charts |
|
| `Terminal.chart()` | Charts |
|
||||||
|
|
||||||
|
## Terminal control (ANSI)
|
||||||
|
|
||||||
|
| API | Description |
|
||||||
|
|-----|-------------|
|
||||||
|
| `Terminal.clearScreen()` | Clear entire screen |
|
||||||
|
| `Terminal.cursorTo(row, col)` | Move cursor (1-based) |
|
||||||
|
|
||||||
## Other
|
## Other
|
||||||
|
|
||||||
| API | Description |
|
| API | Description |
|
||||||
|
|||||||
@@ -108,6 +108,13 @@ String name = Terminal.prompt("Name: ").ask();
|
|||||||
String secret = Terminal.prompt("Password: ").masked().ask();
|
String secret = Terminal.prompt("Password: ").masked().ask();
|
||||||
// With shared scanner (e.g. in a loop):
|
// With shared scanner (e.g. in a loop):
|
||||||
String line = Terminal.prompt("Input: ").ask(sharedScanner);
|
String line = Terminal.prompt("Input: ").ask(sharedScanner);
|
||||||
|
|
||||||
|
// With validation (asks again until input matches):
|
||||||
|
String age = Terminal.prompt("Age: ").validate(s -> s.matches("\\d+")).ask();
|
||||||
|
String email = Terminal.prompt("Email: ")
|
||||||
|
.validate(s -> s.contains("@"))
|
||||||
|
.retryMessage("Invalid email, try again: ")
|
||||||
|
.ask();
|
||||||
```
|
```
|
||||||
|
|
||||||
## Interactive: Confirm
|
## Interactive: Confirm
|
||||||
@@ -259,6 +266,26 @@ Terminal.code("java")
|
|||||||
.print(System.out);
|
.print(System.out);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Help (CLI usage)
|
||||||
|
|
||||||
|
```java
|
||||||
|
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);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Clear screen & cursor
|
||||||
|
|
||||||
|
```java
|
||||||
|
Terminal.clearScreen(); // Clear terminal (ANSI)
|
||||||
|
Terminal.cursorTo(1, 1); // Move cursor to row 1, col 1 (1-based)
|
||||||
|
```
|
||||||
|
|
||||||
|
No-op when ANSI is disabled.
|
||||||
|
|
||||||
## SysInfo
|
## SysInfo
|
||||||
|
|
||||||
```java
|
```java
|
||||||
|
|||||||
@@ -205,6 +205,27 @@ public final class Terminal {
|
|||||||
return new Pager(DEFAULT.getSupport());
|
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() {
|
TerminalSupport getSupport() {
|
||||||
return support;
|
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 */
|
/** Clear line */
|
||||||
public static final String ERASE_LINE = "\u001B[2K";
|
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() {}
|
private Ansi() {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ public final class Examples {
|
|||||||
columns();
|
columns();
|
||||||
steps();
|
steps();
|
||||||
breadcrumb();
|
breadcrumb();
|
||||||
|
helpBlock();
|
||||||
log();
|
log();
|
||||||
logWithMinLevel();
|
logWithMinLevel();
|
||||||
diff();
|
diff();
|
||||||
@@ -128,6 +129,17 @@ public final class Examples {
|
|||||||
System.out.println();
|
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() {
|
static void log() {
|
||||||
Terminal.print("=== Log ===").color(Color.CYAN).bold().println();
|
Terminal.print("=== Log ===").color(Color.CYAN).bold().println();
|
||||||
Terminal.log()
|
Terminal.log()
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import dev.jakub.terminal.core.TerminalSupport;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.PrintStream;
|
import java.io.PrintStream;
|
||||||
import java.util.Scanner;
|
import java.util.Scanner;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Input prompt for reading user input. Use via {@link Terminal#prompt(String)}.
|
* Input prompt for reading user input. Use via {@link Terminal#prompt(String)}.
|
||||||
@@ -18,6 +19,8 @@ public final class Prompt {
|
|||||||
private boolean masked;
|
private boolean masked;
|
||||||
private PrintStream out = System.out;
|
private PrintStream out = System.out;
|
||||||
private InputStream in = System.in;
|
private InputStream in = System.in;
|
||||||
|
private Predicate<String> validator;
|
||||||
|
private String retryMessage = "Invalid, try again: ";
|
||||||
|
|
||||||
public Prompt(String message, TerminalSupport support) {
|
public Prompt(String message, TerminalSupport support) {
|
||||||
this.message = message != null ? message : "";
|
this.message = message != null ? message : "";
|
||||||
@@ -48,33 +51,62 @@ public final class Prompt {
|
|||||||
return this;
|
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.
|
* 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() {
|
public String ask() {
|
||||||
out.print(message);
|
|
||||||
out.flush();
|
|
||||||
if (masked) {
|
if (masked) {
|
||||||
return readMasked();
|
return readMasked();
|
||||||
}
|
}
|
||||||
try (Scanner scan = new Scanner(in)) {
|
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
|
* 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.
|
* 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) {
|
public String ask(Scanner sharedScanner) {
|
||||||
if (sharedScanner == null) return ask();
|
if (sharedScanner == null) return ask();
|
||||||
|
if (masked && System.console() != null) {
|
||||||
out.print(message);
|
out.print(message);
|
||||||
out.flush();
|
out.flush();
|
||||||
if (masked && System.console() != null) {
|
|
||||||
char[] chars = System.console().readPassword();
|
char[] chars = System.console().readPassword();
|
||||||
return chars != null ? new String(chars) : "";
|
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() {
|
private String readMasked() {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import dev.jakub.terminal.interactive.Prompt;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.PrintStream;
|
import java.io.PrintStream;
|
||||||
import java.util.Scanner;
|
import java.util.Scanner;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fluent builder for {@link Prompt}. Use via {@link Terminal#prompt(String)}.
|
* Fluent builder for {@link Prompt}. Use via {@link Terminal#prompt(String)}.
|
||||||
@@ -19,6 +20,8 @@ public final class PromptBuilder {
|
|||||||
private boolean masked;
|
private boolean masked;
|
||||||
private PrintStream output = System.out;
|
private PrintStream output = System.out;
|
||||||
private InputStream input = System.in;
|
private InputStream input = System.in;
|
||||||
|
private Predicate<String> validator;
|
||||||
|
private String retryMessage = "Invalid, try again: ";
|
||||||
|
|
||||||
public PromptBuilder(String message, TerminalSupport support) {
|
public PromptBuilder(String message, TerminalSupport support) {
|
||||||
this.message = message != null ? message : "";
|
this.message = message != null ? message : "";
|
||||||
@@ -49,14 +52,27 @@ public final class PromptBuilder {
|
|||||||
return this;
|
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.
|
* Prompts and returns the entered string.
|
||||||
*/
|
*/
|
||||||
public String ask() {
|
public String ask() {
|
||||||
Prompt p = new Prompt(message, support);
|
Prompt p = buildPrompt();
|
||||||
if (masked) p.masked();
|
|
||||||
p.output(output);
|
|
||||||
p.input(input);
|
|
||||||
return p.ask();
|
return p.ask();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,10 +82,17 @@ public final class PromptBuilder {
|
|||||||
*/
|
*/
|
||||||
public String ask(Scanner sharedScanner) {
|
public String ask(Scanner sharedScanner) {
|
||||||
if (sharedScanner == null) return ask();
|
if (sharedScanner == null) return ask();
|
||||||
|
Prompt p = buildPrompt();
|
||||||
|
return p.ask(sharedScanner);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Prompt buildPrompt() {
|
||||||
Prompt p = new Prompt(message, support);
|
Prompt p = new Prompt(message, support);
|
||||||
if (masked) p.masked();
|
if (masked) p.masked();
|
||||||
p.output(output);
|
p.output(output);
|
||||||
p.input(input);
|
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