Остання активність 1784730207

VERY simple demonstration of how ANSI terminal escape codes can be * used to build a text user interface (TUI)

AnsiShopTUI.java Неформатований
1import java.util.Scanner;
2
3/**
4 * AnsiShopTUI
5 * ------------------------------------------------------------------
6 * A VERY simple demonstration of how ANSI terminal escape codes can be
7 * used to build a text user interface (TUI), without any external
8 * libraries (no Lanterna, no ncurses, no JLine -- just plain old
9 * escape sequences printed to System.out).
10 *
11 * The idea: your terminal understands special sequences of characters
12 * that start with the "ESC" character (ASCII 27) followed by "[".
13 * These sequences tell the terminal to do things like clear the
14 * screen, move the cursor to a specific row/column, or change text
15 * color. We define those sequences as simple String constants below,
16 * then wrap them in tiny helper methods (clearScreen, gotoXY,
17 * printAt, etc.) so the rest of the program never has to deal with
18 * raw escape codes directly.
19 *
20 * This is a "product lookup" toy app for a fake store that sells
21 * keyboards, mice, monitors, and power strips. It has several
22 * screens (Welcome -> Main Menu -> Category List -> Product Detail)
23 * so you can see how a TUI moves the user between different "pages"
24 * just by clearing the screen and redrawing.
25 *
26 * To run:
27 * javac AnsiShopTUI.java
28 * java AnsiShopTUI
29 *
30 * NOTE: This works in a real terminal (Terminal.app, iTerm, gnome
31 * terminal, Windows Terminal, etc). It will NOT render correctly
32 * inside some IDE "Run" consoles that don't support ANSI codes.
33 * ------------------------------------------------------------------
34 */
35public class AnsiShopTUI {
36
37 // ================================================================
38 // 1. RAW ANSI ESCAPE CODES (as constants)
39 // ================================================================
40 // Every ANSI escape sequence starts with this "Control Sequence
41 // Introducer" (CSI): ESC + "[".  is the escape character.
42 static final String ESC = "[";
43
44 // --- Screen / cursor control ---
45 static final String CLEAR_SCREEN = ESC + "2J"; // erase entire screen
46 static final String CURSOR_HOME = ESC + "H"; // move cursor to row 1, col 1
47 static final String HIDE_CURSOR = ESC + "?25l"; // hide the blinking cursor
48 static final String SHOW_CURSOR = ESC + "?25h"; // show the blinking cursor
49 static final String CLEAR_LINE = ESC + "2K"; // erase the current line
50
51 // --- Text style / color ---
52 static final String RESET = ESC + "0m"; // reset all colors/styles to default
53 static final String BOLD = ESC + "1m";
54 static final String REVERSE = ESC + "7m"; // swap foreground/background (highlight)
55
56 static final String FG_RED = ESC + "31m";
57 static final String FG_GREEN = ESC + "32m";
58 static final String FG_YELLOW = ESC + "33m";
59 static final String FG_BLUE = ESC + "34m";
60 static final String FG_MAGENTA = ESC + "35m";
61 static final String FG_CYAN = ESC + "36m";
62 static final String FG_WHITE = ESC + "37m";
63
64 static final String BG_BLUE = ESC + "44m";
65
66 // ================================================================
67 // 2. SIMPLE WRAPPER METHODS
68 // ================================================================
69 // These are the "building blocks" a beginner TUI is made of.
70 // Everything below just prints raw text -- the escape codes are
71 // what make the terminal treat that text specially.
72
73 /** Clears the whole screen and puts the cursor back at (1,1). */
74 static void clearScreen() {
75 System.out.print(CLEAR_SCREEN);
76 System.out.print(CURSOR_HOME);
77 System.out.flush();
78 }
79
80 /**
81 * Moves the cursor to a given row/column. Terminals are 1-indexed,
82 * with (1,1) being the top-left corner of the screen.
83 */
84 static void gotoXY(int row, int col) {
85 System.out.print(ESC + row + ";" + col + "H");
86 }
87
88 /** Prints plain text starting at a specific row/column. */
89 static void printAt(int row, int col, String text) {
90 gotoXY(row, col);
91 System.out.print(text);
92 }
93
94 /** Prints colored text starting at a specific row/column. */
95 static void printAt(int row, int col, String color, String text) {
96 gotoXY(row, col);
97 System.out.print(color + text + RESET);
98 }
99
100 /** Draws a simple rectangular box made of +, -, and | characters. */
101 static void drawBox(int row, int col, int width, int height) {
102 printAt(row, col, "+" + "-".repeat(width - 2) + "+");
103 for (int r = 1; r < height - 1; r++) {
104 printAt(row + r, col, "|");
105 printAt(row + r, col + width - 1, "|");
106 }
107 printAt(row + height - 1, col, "+" + "-".repeat(width - 2) + "+");
108 }
109
110 /** Waits for the user to press Enter before continuing. */
111 static void pause(Scanner scanner, int row, int col) {
112 printAt(row, col, FG_WHITE, "Press ENTER to continue...");
113 scanner.nextLine();
114 }
115
116 // ================================================================
117 // 3. FAKE PRODUCT DATA
118 // ================================================================
119 // A tiny "database" of products, grouped into 4 categories.
120 static class Product {
121 String name;
122 double price;
123 int stockCount;
124
125 Product(String name, double price, int stockCount) {
126 this.name = name;
127 this.price = price;
128 this.stockCount = stockCount;
129 }
130 }
131
132 static final String[] CATEGORY_NAMES = {
133 "Keyboards", "Mice", "Monitors", "Power Strips"
134 };
135
136 static final Product[][] CATALOG = {
137 { // Keyboards
138 new Product("ClickyClack Mechanical 87-key", 59.99, 14),
139 new Product("QuietType Membrane Standard", 19.99, 42),
140 new Product("WirelessGlide Compact", 34.50, 8),
141 },
142 { // Mice
143 new Product("PixelPoint Wired Optical", 9.99, 61),
144 new Product("SmoothGlide Wireless", 24.99, 27),
145 new Product("ErgoGrip Vertical Mouse", 32.00, 5),
146 },
147 { // Monitors
148 new Product("ViewMax 24in 1080p", 129.00, 11),
149 new Product("UltraWide 34in Curved", 379.99, 3),
150 new Product("BudgetView 21in", 89.50, 19),
151 },
152 { // Power Strips
153 new Product("SurgeGuard 6-Outlet", 14.99, 33),
154 new Product("TravelStrip 3-Outlet + USB", 12.50, 20),
155 new Product("HeavyDuty 8-Outlet Metal", 27.75, 9),
156 },
157 };
158
159 // ================================================================
160 // 4. SCREENS ("pages" of the app)
161 // ================================================================
162
163 /** Screen 1: Welcome / splash screen. */
164 static void showWelcomeScreen(Scanner scanner) {
165 clearScreen();
166 drawBox(2, 5, 60, 7);
167 printAt(4, 10, BOLD + FG_CYAN, "WELCOME TO THE ANSI TERMINAL SHOP");
168 printAt(6, 10, FG_WHITE, "A tiny demo of ANSI escape codes in Java.");
169 printAt(7, 10, FG_WHITE, "No libraries. Just escape sequences + println.");
170 pause(scanner, 11, 5);
171 }
172
173 /** Screen 2: Main menu -- pick a product category. */
174 static int showMainMenu(Scanner scanner) {
175 clearScreen();
176 drawBox(1, 3, 50, CATEGORY_NAMES.length + 6);
177 printAt(2, 6, BOLD + FG_YELLOW, "MAIN MENU - Product Categories");
178 printAt(3, 6, FG_WHITE, "------------------------------");
179
180 for (int i = 0; i < CATEGORY_NAMES.length; i++) {
181 printAt(4 + i, 6, FG_GREEN, "[" + (i + 1) + "] " + CATEGORY_NAMES[i]);
182 }
183 int quitLine = 4 + CATEGORY_NAMES.length;
184 printAt(quitLine, 6, FG_RED, "[0] Quit");
185
186 printAt(quitLine + 2, 6, FG_WHITE, "Choose an option: ");
187 String input = scanner.nextLine().trim();
188
189 try {
190 int choice = Integer.parseInt(input);
191 if (choice >= 0 && choice <= CATEGORY_NAMES.length) {
192 return choice;
193 }
194 } catch (NumberFormatException ignored) {
195 // fall through to invalid choice handling below
196 }
197
198 printAt(quitLine + 3, 6, FG_RED, "Invalid choice: " + input);
199 pause(scanner, quitLine + 5, 6);
200 return -1; // signal "redraw menu"
201 }
202
203 /** Screen 3: Product list for a chosen category. */
204 static void showCategoryScreen(Scanner scanner, int categoryIndex) {
205 boolean stayOnScreen = true;
206 while (stayOnScreen) {
207 Product[] products = CATALOG[categoryIndex];
208 clearScreen();
209 drawBox(1, 3, 66, products.length + 7);
210 printAt(2, 6, BOLD + FG_CYAN, "CATEGORY: " + CATEGORY_NAMES[categoryIndex]);
211 printAt(3, 6, FG_WHITE, "----------------------------------------");
212 printAt(4, 6, REVERSE, " # Product Price Stock ");
213
214 for (int i = 0; i < products.length; i++) {
215 Product p = products[i];
216 String line = String.format("[%d] %-30s $%-7.2f %3d",
217 i + 1, p.name, p.price, p.stockCount);
218 printAt(5 + i, 6, FG_WHITE, line);
219 }
220
221 int backLine = 5 + products.length;
222 printAt(backLine, 6, FG_YELLOW, "[0] Back to Main Menu");
223 printAt(backLine + 2, 6, FG_WHITE, "View which product? ");
224 String input = scanner.nextLine().trim();
225
226 if (input.equals("0")) {
227 stayOnScreen = false;
228 continue;
229 }
230
231 try {
232 int choice = Integer.parseInt(input);
233 if (choice >= 1 && choice <= products.length) {
234 showProductDetailScreen(scanner, products[choice - 1]);
235 } else {
236 printAt(backLine + 3, 6, FG_RED, "No such product number.");
237 pause(scanner, backLine + 5, 6);
238 }
239 } catch (NumberFormatException e) {
240 printAt(backLine + 3, 6, FG_RED, "Please enter a number.");
241 pause(scanner, backLine + 5, 6);
242 }
243 }
244 }
245
246 /** Screen 4: Detail view for a single product. */
247 static void showProductDetailScreen(Scanner scanner, Product p) {
248 clearScreen();
249 drawBox(3, 8, 50, 9);
250 printAt(5, 12, BOLD + FG_MAGENTA, "PRODUCT DETAIL");
251 printAt(6, 12, FG_WHITE, "Name: " + p.name);
252 printAt(7, 12, FG_WHITE, "Price: $" + String.format("%.2f", p.price));
253 printAt(8, 12, FG_WHITE, "Stock: " + p.stockCount + " units available");
254
255 String status = p.stockCount == 0 ? "OUT OF STOCK"
256 : p.stockCount < 10 ? "LOW STOCK"
257 : "IN STOCK";
258 String statusColor = p.stockCount == 0 ? FG_RED
259 : p.stockCount < 10 ? FG_YELLOW
260 : FG_GREEN;
261 printAt(9, 12, statusColor, "Status: " + status);
262
263 pause(scanner, 11, 8);
264 }
265
266 // ================================================================
267 // 5. MAIN PROGRAM LOOP
268 // ================================================================
269 public static void main(String[] args) {
270 Scanner scanner = new Scanner(System.in);
271
272 // Hide the blinking cursor while the app runs, restore it on exit.
273 System.out.print(HIDE_CURSOR);
274
275 showWelcomeScreen(scanner);
276
277 boolean running = true;
278 while (running) {
279 int choice = showMainMenu(scanner);
280
281 if (choice == 0) {
282 running = false;
283 } else if (choice >= 1 && choice <= CATEGORY_NAMES.length) {
284 showCategoryScreen(scanner, choice - 1);
285 }
286 // choice == -1 means "invalid input" -- just loop and redraw menu
287 }
288
289 clearScreen();
290 printAt(1, 1, FG_CYAN, "Thanks for visiting the ANSI Terminal Shop. Goodbye!");
291 System.out.print(RESET);
292 System.out.print(SHOW_CURSOR);
293 System.out.println();
294 scanner.close();
295 }
296}
297
README.md Неформатований

Understanding AnsiShopTUI: A Beginner's Guide to Terminal UIs

This is a companion guide to AnsiShopTUI.java, a small Java program that builds a menu-driven "shop" app entirely inside a plain terminal window. No graphics library, no windows, no mouse. If you're new to Java and have never thought about how terminal programs draw text on screen, this document explains the ideas behind the code before you dive into it.

What is a TUI?

TUI stands for Text User Interface. It's the older, text-only cousin of the GUI (Graphical User Interface) you're used to on a phone or desktop. Instead of buttons, windows, and icons made of pixels, a TUI uses only text characters arranged on a grid, but that text can still be colored, positioned precisely, and updated dynamically to feel like a real app with menus and screens.

Old-school examples include DOS-era program installers, the vim and nano text editors, top (the process viewer on Mac/Linux), and old BBS (bulletin board) systems from the 1980s and 90s. They all do the same basic trick this program does: they place characters at exact positions on the screen instead of just printing line after line.

A Little History: Why Terminals Understand "Escape Codes"

Long before computers had screens with pixels, people used physical machines called terminals (think: a keyboard attached to a printer or a simple screen) to talk to a faraway computer. One very influential terminal, made by a company called DEC in the 1970s, was called the VT100. It defined a set of special commands you could send to the terminal so it would do things like move its cursor, clear the screen, or print in a different color, instead of only printing new lines forever.

Those commands became a de facto standard, and today they're documented as the ANSI escape codes (ANSI is the American National Standards Institute, which formalized this set of terminal control sequences). Even though physical terminals are long gone, virtually every terminal program you use today, Terminal.app on a Mac, the Windows Terminal, iTerm, a Linux console, still understands these same codes, because emulating an old VT100-style terminal became the universal baseline everyone agreed to support. That's why a language like Java, running on a modern computer, can use decades-old escape codes and still control the terminal window in real time.

What Is an "Escape Code," Concretely?

An escape code is just a short sequence of ordinary text characters that the terminal treats as an instruction instead of something to display. It always starts with one invisible character called ESC (short for "Escape," and it's ASCII character number 27), immediately followed by a left bracket [. Everything after that up to a letter is the instruction.

For example, the text sequence ESC[2J means "clear the entire screen," and ESC[10;5H means "move the cursor to row 10, column 5." When your Java program prints these exact characters to the screen, nothing visible appears for the escape code itself, but the terminal reacts by clearing itself or jumping the cursor, and then whatever text you print next appears in the new spot.

This ESC + [ combination is called the CSI, the Control Sequence Introducer. Nearly every code in AnsiShopTUI.java is built the same way: CSI, followed by some numbers, followed by a single letter that says what to do (H for cursor position, J for clearing, m for color/style).

How the Class Organizes This Idea

The file is deliberately split into layers, from lowest-level to highest-level, so you can see how a handful of raw text codes eventually become a working menu app.

Layer 1: Raw constants. Near the top, you'll see constants like CLEAR_SCREEN, FG_GREEN, and BOLD. Each one is just a literal string containing the escape sequence for that effect. This layer has zero logic, it is only naming the magic strings so the rest of the code never has to remember what ESC[32m means.

Layer 2: Simple wrapper methods. Methods like clearScreen(), gotoXY(row, col), and printAt(row, col, text) take those raw constants and turn them into small, reusable actions. gotoXY is worth understanding closely: it builds a fresh escape code every time using whatever row and column you pass in, because cursor position is the one code that needs actual numbers plugged in rather than being a fixed constant. Everything above this layer never touches a raw escape code directly again.

Layer 3: Fake data. The Product class and the CATALOG array are ordinary Java data, no terminal concepts here at all. This is just the inventory the app displays.

Layer 4: Screens. Methods like showWelcomeScreen, showMainMenu, showCategoryScreen, and showProductDetailScreen are where it all comes together. Each one clears the screen, then uses the Layer 2 wrappers to draw a box, print some labeled text at specific coordinates, and ask the user for input with a Scanner. A "screen" in this program is really just: clear everything, redraw a specific layout, then wait for a choice.

Layer 5: The main loop. main() ties the screens together. It shows the welcome screen once, then loops through the main menu, opening a category screen when you pick one, until you choose to quit.

Why "Clear, Then Redraw" Instead of Just Printing More Text?

A beginner's first instinct is usually to keep System.out.println-ing new lines forever, the way a typical console program does. A TUI behaves differently: every time you move to a new "page," the program clears the whole screen and repaints it from scratch at fixed coordinates. That's why the screen feels like a page changing, similar to clicking a link in a GUI, rather than a long scrolling log of everything that has ever happened. This is the central trick that separates a TUI from a plain command-line program: precise, repeatable positioning instead of endless scrolling.

Rows, Columns, and the "1-Indexed" Surprise

One detail that trips people up: terminal coordinates start counting at 1, not 0. The very top-left character cell on the screen is row 1, column 1, not row 0, column 0 like a Java array. That's why you'll see gotoXY(1, 1) in this code rather than gotoXY(0, 0). This convention comes straight from the original hardware terminals and has simply never changed.

Why No External Libraries?

There are polished Java libraries (like Lanterna or JLine) that handle terminal UIs for you with much more power: mouse support, resizing, cross-platform quirks, and so on. This project intentionally avoids all of that. The goal here is to see the actual mechanism, plain strings sent to System.out, so that when you eventually do use a real TUI library, you'll understand what it's doing underneath for you.

Things to Try Once You Understand the Basics

A few small changes are good exercises for building confidence with this code: add a fifth product category, change the color scheme by swapping which FG_* constant a screen uses, add a "search by name" option to the main menu, or add a background color constant and use it behind the title text. Each of these only requires working within the layers already described, no new terminal concepts required.

Quick Reference: Codes Used in This Program

Constant Escape Sequence Effect
CLEAR_SCREEN ESC[2J Erases everything on screen
CURSOR_HOME ESC[H Moves cursor to row 1, col 1
HIDE_CURSOR / SHOW_CURSOR ESC[?25l / ESC[?25h Toggles the blinking cursor
gotoXY(row, col) ESC[row;colH Moves cursor to a specific position
RESET ESC[0m Turns off all color/style
BOLD ESC[1m Makes following text bold
REVERSE ESC[7m Swaps foreground/background color
FG_RED, FG_GREEN, etc. ESC[3Xm Sets text color

If you keep this table nearby while reading AnsiShopTUI.java, every constant in the source file should map directly back to something explained above.