From b0a8f1ae8df1726c4473e4950c8aeee9b0e31679 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 1 Mar 2026 12:24:10 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20Add=20cursor=20hiding?= =?UTF-8?q?=20and=20start=20prompt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 💡 What: - Hides the blinking terminal cursor (`\033[?25l`) during gameplay. - Restores the cursor on both graceful exit and signal termination (Ctrl+C). - Adds a "Press any key to start..." prompt before entering the game loop. 🎯 Why: - The blinking cursor was visually distracting during fast-paced play. - Without a start prompt, users were dropped directly into a timed loop, leading to initial failure and poor UX. ♿ Accessibility: - Reduces visual clutter (cursor) which can aid focus. - Gives users explicit control over when the real-time interaction begins. Co-authored-by: EiJackGH <172181576+EiJackGH@users.noreply.github.com> --- .Jules/palette.md | 4 +++ src/main.cpp | 10 ++++-- verify_ux.py | 82 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 verify_ux.py diff --git a/.Jules/palette.md b/.Jules/palette.md index 8aed79d..3138a56 100644 --- a/.Jules/palette.md +++ b/.Jules/palette.md @@ -17,3 +17,7 @@ ## 2026-02-13 - Tactile Feedback in CLI **Learning:** In terminal-based games, users expect immediate visual feedback for their actions. Relying on a periodic "tick" to update the UI creates a laggy feel. Using `poll()` with a dynamic timeout allows the application to remain idle yet wake up instantly to process and render user input. **Action:** Always trigger a UI refresh immediately after processing user input in CLI applications, and use efficient waiting mechanisms (like `poll`) that can be interrupted by input. + +## 2024-10-24 - Cursor Visibility and Readiness in CLI Games +**Learning:** A visible blinking cursor in a fast-paced CLI game is visually distracting. Additionally, throwing users directly into a timed game loop without a readiness prompt leads to immediate initial failure and a poor UX. +**Action:** Always hide the cursor (`\033[?25l`) during gameplay (restoring it gracefully on normal or interrupted exits) and implement a "Press any key to start..." prompt before entering active timed loops to ensure the user is prepared. diff --git a/src/main.cpp b/src/main.cpp index 775bf66..bc84610 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -25,8 +25,8 @@ struct termios oldt; void restore_terminal(int signum) { tcsetattr(STDIN_FILENO, TCSANOW, &oldt); // Use write() and _exit() because they are async-signal-safe - const char* msg = "\033[0m\n\nGame interrupted. Terminal settings restored.\n"; - write(STDOUT_FILENO, msg, 52); + const char msg[] = "\033[?25h\033[0m\n\nGame interrupted. Terminal settings restored.\n"; + write(STDOUT_FILENO, msg, sizeof(msg) - 1); _exit(signum); } @@ -47,10 +47,15 @@ int main() { } long long score = 0; bool hardMode = false; char input; + std::cout << "\033[?25l"; // Hide cursor std::cout << CLR_CTRL << "==========================\n SPEED CLICKER\n==========================\n" << CLR_RESET << "Controls:\n " << CLR_CTRL << "[h]" << CLR_RESET << " Toggle Hard Mode (10x Speed!)\n " << CLR_CTRL << "[q]" << CLR_RESET << " Quit Game\n " << CLR_CTRL << "[Any key]" << CLR_RESET << " Click!\n\n"; + std::cout << "Press any key to start..." << std::flush; + read(STDIN_FILENO, &input, 1); + std::cout << "\r \r" << std::flush; // Clear the start prompt + struct pollfd fds[1] = {{STDIN_FILENO, POLLIN, 0}}; auto last_tick = std::chrono::steady_clock::now(); bool updateUI = true; @@ -86,6 +91,7 @@ int main() { } } tcsetattr(STDIN_FILENO, TCSANOW, &oldt); + std::cout << "\033[?25h"; // Restore cursor std::cout << "\n\n" << CLR_SCORE << "Final Score: " << score << CLR_RESET << "\nThanks for playing!\n"; return 0; } diff --git a/verify_ux.py b/verify_ux.py new file mode 100644 index 0000000..89ff3b2 --- /dev/null +++ b/verify_ux.py @@ -0,0 +1,82 @@ +import pty +import os +import select +import time + +def verify_ux(): + master, slave = pty.openpty() + pid = os.fork() + + if pid == 0: + # Child process: run the game + os.setsid() + os.dup2(slave, 0) + os.dup2(slave, 1) + os.dup2(slave, 2) + os.close(slave) + os.close(master) + os.execl('./game', './game') + else: + # Parent process + os.close(slave) + output = b'' + + # Read the initial prompt and wait for it + timeout = 2.0 + start_time = time.time() + while time.time() - start_time < timeout: + r, _, _ = select.select([master], [], [], 0.1) + if r: + data = os.read(master, 1024) + if not data: + break + output += data + if b'Press any key to start...' in output: + break + + # We need to send a key to start the game + os.write(master, b' ') + + # Read until we see the first score or wait a little + start_time = time.time() + while time.time() - start_time < timeout: + r, _, _ = select.select([master], [], [], 0.1) + if r: + data = os.read(master, 1024) + if not data: + break + output += data + if b'Score: 0' in output: + break + + # Send quit command + os.write(master, b'q') + + # Read the rest of the output + while True: + r, _, _ = select.select([master], [], [], 0.5) + if r: + try: + data = os.read(master, 1024) + if not data: + break + output += data + except OSError: + break + else: + break + + os.close(master) + os.waitpid(pid, 0) + + output_str = output.decode('utf-8', errors='ignore') + print("Captured output:") + print(repr(output_str)) + + assert '\033[?25l' in output_str, "Cursor hide sequence missing!" + assert '\033[?25h' in output_str, "Cursor restore sequence missing!" + assert 'Press any key to start...' in output_str, "Start prompt missing!" + print("All UX checks passed!") + +if __name__ == '__main__': + verify_ux()