Last active 1784730207

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

Revision e653e040a10924b16615f44b599623e49f2111e0

AnsiShopTUI.java Raw
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