import java.util.Scanner; /** * AnsiShopTUI * ------------------------------------------------------------------ * A VERY simple demonstration of how ANSI terminal escape codes can be * used to build a text user interface (TUI), without any external * libraries (no Lanterna, no ncurses, no JLine -- just plain old * escape sequences printed to System.out). * * The idea: your terminal understands special sequences of characters * that start with the "ESC" character (ASCII 27) followed by "[". * These sequences tell the terminal to do things like clear the * screen, move the cursor to a specific row/column, or change text * color. We define those sequences as simple String constants below, * then wrap them in tiny helper methods (clearScreen, gotoXY, * printAt, etc.) so the rest of the program never has to deal with * raw escape codes directly. * * This is a "product lookup" toy app for a fake store that sells * keyboards, mice, monitors, and power strips. It has several * screens (Welcome -> Main Menu -> Category List -> Product Detail) * so you can see how a TUI moves the user between different "pages" * just by clearing the screen and redrawing. * * To run: * javac AnsiShopTUI.java * java AnsiShopTUI * * NOTE: This works in a real terminal (Terminal.app, iTerm, gnome * terminal, Windows Terminal, etc). It will NOT render correctly * inside some IDE "Run" consoles that don't support ANSI codes. * ------------------------------------------------------------------ */ public class AnsiShopTUI { // ================================================================ // 1. RAW ANSI ESCAPE CODES (as constants) // ================================================================ // Every ANSI escape sequence starts with this "Control Sequence // Introducer" (CSI): ESC + "[".  is the escape character. static final String ESC = "["; // --- Screen / cursor control --- static final String CLEAR_SCREEN = ESC + "2J"; // erase entire screen static final String CURSOR_HOME = ESC + "H"; // move cursor to row 1, col 1 static final String HIDE_CURSOR = ESC + "?25l"; // hide the blinking cursor static final String SHOW_CURSOR = ESC + "?25h"; // show the blinking cursor static final String CLEAR_LINE = ESC + "2K"; // erase the current line // --- Text style / color --- static final String RESET = ESC + "0m"; // reset all colors/styles to default static final String BOLD = ESC + "1m"; static final String REVERSE = ESC + "7m"; // swap foreground/background (highlight) static final String FG_RED = ESC + "31m"; static final String FG_GREEN = ESC + "32m"; static final String FG_YELLOW = ESC + "33m"; static final String FG_BLUE = ESC + "34m"; static final String FG_MAGENTA = ESC + "35m"; static final String FG_CYAN = ESC + "36m"; static final String FG_WHITE = ESC + "37m"; static final String BG_BLUE = ESC + "44m"; // ================================================================ // 2. SIMPLE WRAPPER METHODS // ================================================================ // These are the "building blocks" a beginner TUI is made of. // Everything below just prints raw text -- the escape codes are // what make the terminal treat that text specially. /** Clears the whole screen and puts the cursor back at (1,1). */ static void clearScreen() { System.out.print(CLEAR_SCREEN); System.out.print(CURSOR_HOME); System.out.flush(); } /** * Moves the cursor to a given row/column. Terminals are 1-indexed, * with (1,1) being the top-left corner of the screen. */ static void gotoXY(int row, int col) { System.out.print(ESC + row + ";" + col + "H"); } /** Prints plain text starting at a specific row/column. */ static void printAt(int row, int col, String text) { gotoXY(row, col); System.out.print(text); } /** Prints colored text starting at a specific row/column. */ static void printAt(int row, int col, String color, String text) { gotoXY(row, col); System.out.print(color + text + RESET); } /** Draws a simple rectangular box made of +, -, and | characters. */ static void drawBox(int row, int col, int width, int height) { printAt(row, col, "+" + "-".repeat(width - 2) + "+"); for (int r = 1; r < height - 1; r++) { printAt(row + r, col, "|"); printAt(row + r, col + width - 1, "|"); } printAt(row + height - 1, col, "+" + "-".repeat(width - 2) + "+"); } /** Waits for the user to press Enter before continuing. */ static void pause(Scanner scanner, int row, int col) { printAt(row, col, FG_WHITE, "Press ENTER to continue..."); scanner.nextLine(); } // ================================================================ // 3. FAKE PRODUCT DATA // ================================================================ // A tiny "database" of products, grouped into 4 categories. static class Product { String name; double price; int stockCount; Product(String name, double price, int stockCount) { this.name = name; this.price = price; this.stockCount = stockCount; } } static final String[] CATEGORY_NAMES = { "Keyboards", "Mice", "Monitors", "Power Strips" }; static final Product[][] CATALOG = { { // Keyboards new Product("ClickyClack Mechanical 87-key", 59.99, 14), new Product("QuietType Membrane Standard", 19.99, 42), new Product("WirelessGlide Compact", 34.50, 8), }, { // Mice new Product("PixelPoint Wired Optical", 9.99, 61), new Product("SmoothGlide Wireless", 24.99, 27), new Product("ErgoGrip Vertical Mouse", 32.00, 5), }, { // Monitors new Product("ViewMax 24in 1080p", 129.00, 11), new Product("UltraWide 34in Curved", 379.99, 3), new Product("BudgetView 21in", 89.50, 19), }, { // Power Strips new Product("SurgeGuard 6-Outlet", 14.99, 33), new Product("TravelStrip 3-Outlet + USB", 12.50, 20), new Product("HeavyDuty 8-Outlet Metal", 27.75, 9), }, }; // ================================================================ // 4. SCREENS ("pages" of the app) // ================================================================ /** Screen 1: Welcome / splash screen. */ static void showWelcomeScreen(Scanner scanner) { clearScreen(); drawBox(2, 5, 60, 7); printAt(4, 10, BOLD + FG_CYAN, "WELCOME TO THE ANSI TERMINAL SHOP"); printAt(6, 10, FG_WHITE, "A tiny demo of ANSI escape codes in Java."); printAt(7, 10, FG_WHITE, "No libraries. Just escape sequences + println."); pause(scanner, 11, 5); } /** Screen 2: Main menu -- pick a product category. */ static int showMainMenu(Scanner scanner) { clearScreen(); drawBox(1, 3, 50, CATEGORY_NAMES.length + 6); printAt(2, 6, BOLD + FG_YELLOW, "MAIN MENU - Product Categories"); printAt(3, 6, FG_WHITE, "------------------------------"); for (int i = 0; i < CATEGORY_NAMES.length; i++) { printAt(4 + i, 6, FG_GREEN, "[" + (i + 1) + "] " + CATEGORY_NAMES[i]); } int quitLine = 4 + CATEGORY_NAMES.length; printAt(quitLine, 6, FG_RED, "[0] Quit"); printAt(quitLine + 2, 6, FG_WHITE, "Choose an option: "); String input = scanner.nextLine().trim(); try { int choice = Integer.parseInt(input); if (choice >= 0 && choice <= CATEGORY_NAMES.length) { return choice; } } catch (NumberFormatException ignored) { // fall through to invalid choice handling below } printAt(quitLine + 3, 6, FG_RED, "Invalid choice: " + input); pause(scanner, quitLine + 5, 6); return -1; // signal "redraw menu" } /** Screen 3: Product list for a chosen category. */ static void showCategoryScreen(Scanner scanner, int categoryIndex) { boolean stayOnScreen = true; while (stayOnScreen) { Product[] products = CATALOG[categoryIndex]; clearScreen(); drawBox(1, 3, 66, products.length + 7); printAt(2, 6, BOLD + FG_CYAN, "CATEGORY: " + CATEGORY_NAMES[categoryIndex]); printAt(3, 6, FG_WHITE, "----------------------------------------"); printAt(4, 6, REVERSE, " # Product Price Stock "); for (int i = 0; i < products.length; i++) { Product p = products[i]; String line = String.format("[%d] %-30s $%-7.2f %3d", i + 1, p.name, p.price, p.stockCount); printAt(5 + i, 6, FG_WHITE, line); } int backLine = 5 + products.length; printAt(backLine, 6, FG_YELLOW, "[0] Back to Main Menu"); printAt(backLine + 2, 6, FG_WHITE, "View which product? "); String input = scanner.nextLine().trim(); if (input.equals("0")) { stayOnScreen = false; continue; } try { int choice = Integer.parseInt(input); if (choice >= 1 && choice <= products.length) { showProductDetailScreen(scanner, products[choice - 1]); } else { printAt(backLine + 3, 6, FG_RED, "No such product number."); pause(scanner, backLine + 5, 6); } } catch (NumberFormatException e) { printAt(backLine + 3, 6, FG_RED, "Please enter a number."); pause(scanner, backLine + 5, 6); } } } /** Screen 4: Detail view for a single product. */ static void showProductDetailScreen(Scanner scanner, Product p) { clearScreen(); drawBox(3, 8, 50, 9); printAt(5, 12, BOLD + FG_MAGENTA, "PRODUCT DETAIL"); printAt(6, 12, FG_WHITE, "Name: " + p.name); printAt(7, 12, FG_WHITE, "Price: $" + String.format("%.2f", p.price)); printAt(8, 12, FG_WHITE, "Stock: " + p.stockCount + " units available"); String status = p.stockCount == 0 ? "OUT OF STOCK" : p.stockCount < 10 ? "LOW STOCK" : "IN STOCK"; String statusColor = p.stockCount == 0 ? FG_RED : p.stockCount < 10 ? FG_YELLOW : FG_GREEN; printAt(9, 12, statusColor, "Status: " + status); pause(scanner, 11, 8); } // ================================================================ // 5. MAIN PROGRAM LOOP // ================================================================ public static void main(String[] args) { Scanner scanner = new Scanner(System.in); // Hide the blinking cursor while the app runs, restore it on exit. System.out.print(HIDE_CURSOR); showWelcomeScreen(scanner); boolean running = true; while (running) { int choice = showMainMenu(scanner); if (choice == 0) { running = false; } else if (choice >= 1 && choice <= CATEGORY_NAMES.length) { showCategoryScreen(scanner, choice - 1); } // choice == -1 means "invalid input" -- just loop and redraw menu } clearScreen(); printAt(1, 1, FG_CYAN, "Thanks for visiting the ANSI Terminal Shop. Goodbye!"); System.out.print(RESET); System.out.print(SHOW_CURSOR); System.out.println(); scanner.close(); } }