This is a professional, cross-platform Text-based User Interface (TUI) application written in C++. It demonstrates advanced terminal control, a modular architecture based on SOLID principles, and a hardware abstraction layer to run natively on both Linux and Windows from a single C++ codebase.
The application starts with an animated loading screen, then launches a fully interactive menu that can be navigated using the keyboard (Up, Down, Home, End, Enter, Esc).
- Cross-Platform: Natively compiles and runs on both Linux (using
termios) and Windows (usingwindows.h). - SOLID Architecture: Code is decoupled into distinct "layers" (UI, Input, Terminal Control) following the Single Responsibility Principle (SRP).
- Interactive TUI:
- Full keyboard navigation (Up, Down, Home, End).
- Selection highlighting.
Escandqkeys for exiting.
- True Raw Mode: Disables canonical mode (
ICANON) and character echoing (ECHO) for full control over the terminal. - Smart Input Handling:
- Solves the "Esc vs. Arrow Key" problem using a non-blocking, timed-read mechanism.
- Handles different key codes from Linux (Escape sequences) and Windows (Scan codes).
- Clean Rendering: Features a flicker-free menu and an animated loading bar by using smart cursor positioning (
CURSOR_HOME) instead ofCLEAR_SCREENin the main loop.
This project is divided into multiple .cpp files and must be compiled together.
- Linux: A
g++compiler (e.g.,build-essential). - Windows: A
g++compiler for Windows (we recommend MinGW-w64).
This project is configured to build perfectly with VS Code's tasks.json.
-
Make sure you have the C/C++ extension for VS Code.
-
Create a folder named
.vscodein your project's root. -
Inside
.vscode, create a file namedtasks.jsonand paste the following:{ "version": "2.0.0", "tasks": [ { "label": "Build Project", "type": "shell", "command": "g++", "args": [ "-fdiagnostics-color=always", "-g", "main.cpp", "terminal.cpp", "ui.cpp", "input.cpp", "-o", "my_program" ], "group": { "kind": "build", "isDefault": true } } ] } -
Press
Ctrl+Shift+Bto build the project. -
Run the compiled program from your terminal:
- On Linux:
./my_program - On Windows:
.\my_program.exe
- On Linux:
Open your terminal in the project folder and run:
# On Linux
g++ main.cpp terminal.cpp ui.cpp input.cpp -o my_program
# On Windows (with MinGW)
g++ main.cpp terminal.cpp ui.cpp input.cpp -o my_program.exeThe core philosophy of this project is the Single Responsibility Principle (SRP) and Dependency Inversion (DIP). The code is decoupled into "layers" (modules), where each layer has only one job and "hides" its complexity from the other layers.
- Responsibility: Application Logic and State Management.
- Job: Contains the main
while(true)loop. It holds the "state" (e.g.,int selectedOption). It acts as a "conductor" or "maestro," telling the other layers what to do. - Hides: It knows nothing about how to draw, how to read keys, or how to enable raw mode. It only speaks the "abstract" language from the
.hppfiles.
- Responsibility: All screen rendering and drawing.
- Job: Knows how to draw the loading bar, the menu, and the action screens. It contains all
Ansi::color codes andmove()commands. It holds themenuOptionsarray, hiding the menu's content frommain. - Hides: It knows nothing about keyboard input or
termios.
- Responsibility: Low-level, system-specific terminal hardware control.
- Job: This is the core "plumbing." It's the only file that contains
#ifdef _WIN32. It knows how to enable/disable raw mode on Linux (usingtermios.h) and on Windows (usingwindows.h). It also provides theflushInputBuffer()utility. - Hides: It hides the entire complexity of system calls (
tcsetattr,SetConsoleMode) behind simple function names.
- Responsibility: Low-level, system-specific input abstraction.
- Job: This layer's job is to read raw, cryptic key codes from two different "languages" (Linux vs. Windows) and translate them into one universal "language" (our
enum KeyType). - Hides: It hides the complexity of
read(),_getch(), escape sequences, and scan codes.main.cppjust receives a simpleKEY_UPorKEY_HOME.
- Responsibility: A simple, static "dictionary" of ANSI escape codes.
- Job: Provides clean, readable names for all colors and cursor commands.
This design means we can support a new operating system (like macOS) by only modifying terminal.cpp and input.cpp. The main.cpp and ui.cpp files would not need to change at all.
This is the "magic" that makes the project work.
To read arrow keys, we must leave "Canonical Mode" (which waits for Enter) and enter "Non-Canonical (Raw) Mode".
-
On Linux (
termios.h):- We call
tcgetattr()to get the current settings. - We use bitwise "AND" (
&) with an inverted "mask" (~) to surgically turn OFF the flags:ICANON: Disables "line-buffering" (no longer waits forEnter).ECHO: Disables "echoing" (keys pressed are not printed to the screen).ISIG: Disables signals (soCtrl+Cdoesn't quit the program).
- We call
tcsetattr()to apply these new settings.
- We call
-
On Windows (
windows.h):- We get a "handle" (a remote control) to the console:
GetStdHandle(). - We get the current settings:
GetConsoleMode(). - We use the same bitwise
&= ~logic to turn OFF the Windows-equivalent flags:ENABLE_LINE_INPUT(the same asICANON).ENABLE_ECHO_INPUT(the same asECHO).
- We apply the settings:
SetConsoleMode().
- We get a "handle" (a remote control) to the console:
Reading keys in raw mode is different on each OS.
-
On Linux (
read()):- We set
termiostoVMIN = 0andVTIME = 1. This is the secret. It tellsread(): "Do not wait for a minimum number of characters. Wait only 0.1 seconds (1 decisecond) for a key. If nothing comes, return 0 (timeout)." - This is how we solve the "Esc vs. Arrow Key" puzzle:
read()gets\033(Esc).- We immediately call
read()again. - If it returns
0(timeout), the user only pressedEsc. We returnKEY_ESCAPE. - If it returns
[and thenA, the user pressed the Up Arrow. We returnKEY_UP.
- We set
-
On Windows (
conio.h):- Windows uses a different "polling" philosophy with two functions:
_kbhit(): A non-blocking check. It instantly returnstrueif a key is waiting,falseif not._getch(): A blocking read. It waits for and returns a key.- Our
readKey()function simulates the Linux timeout logic:- It calls
_kbhit(). - If
false, we immediately returnKEY_UNKNOWN(same as a Linux timeout). - If
true, we call_getch()to get the key.
- It calls
This is why the input layer is so crucial. The "languages" are completely different. main.cpp can't understand them, so input.cpp translates them into our universal enum KeyType.
| Key | Linux Code (Raw Bytes) | Windows Code (Raw Bytes) | Universal KeyType |
|---|---|---|---|
| Up Arrow | 27, 91, 65 (\033[A) |
224, 72 |
KEY_UP |
| Down Arrow | 27, 91, 66 (\033[B) |
224, 80 |
KEY_DOWN |
| Home | 27, 91, 72 (\033[H) |
224, 71 |
KEY_HOME |
| End | 27, 91, 70 (\033[F) |
224, 79 |
KEY_END |
| Enter | 10 (\n) |
13 (\r) |
KEY_ENTER |
| Esc | 27 (with read timeout) |
27 |
KEY_ESCAPE |
- Problem: When a user presses
Enterto open a screen, their key-press might be "repeated" by the OS (Key Repeat) or (on Windows) send two characters (\r\n). This "leftover" character in the buffer is immediately read by our "wait" loop, causing the screen to close instantly. - Solution (in
main.cpp):flushInputBuffer();: After detectingKEY_ENTER, we first call this "pump" function. It usestcflush(Linux) orFlushConsoleInputBuffer(Windows) to clear all "leftover" characters from the input buffer.while(readKey() == KEY_UNKNOWN);: This is the "brake." Now that the buffer is clean, this loop will safely run (checking every 0.1s) until a genuinely new key is pressed by the user.# employee-system