├── demos ├── basics └── basics.nim ├── media └── sample.png ├── textalot.nimble ├── LICENSE ├── .gitignore ├── readme.md └── textalot.nim /demos/basics: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erayzesen/textalot/HEAD/demos/basics -------------------------------------------------------------------------------- /media/sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erayzesen/textalot/HEAD/media/sample.png -------------------------------------------------------------------------------- /textalot.nimble: -------------------------------------------------------------------------------- 1 | # textalot.nimble 2 | version = "0.1.0" 3 | author = "Eray Zesen " 4 | description = "A High-Performance Terminal I/O & TUI Engine written in Nim" 5 | license = "MIT" 6 | srcDir = "" 7 | bin = @["textalot"] 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Eray Zesen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ============================== 2 | # Nim / Nimble project .gitignore 3 | # ============================== 4 | 5 | # === Build artifacts === 6 | # Compiled binaries and libraries 7 | *.exe 8 | *.dll 9 | *.so 10 | *.dylib 11 | *.out 12 | textalot 13 | 14 | # === Nim build cache === 15 | # Directory created by the Nim compiler for intermediate files 16 | nimcache/ 17 | 18 | # === Build output directories === 19 | # Common folders for build outputs or binaries 20 | build/ 21 | bin/ 22 | dist/ 23 | obj/ 24 | 25 | # === Nimble files === 26 | # Lock file created by Nimble (can be regenerated) 27 | *.nimble.lock 28 | nimbledeps/ 29 | deps/ 30 | 31 | # === Logs / temporary files === 32 | # General temporary or log files 33 | *.log 34 | *.tmp 35 | *.bak 36 | 37 | # === Editor / IDE settings === 38 | # VSCode, JetBrains IDEs, Sublime, etc. 39 | .vscode/ 40 | .idea/ 41 | *.sublime-workspace 42 | *.sublime-project 43 | 44 | # === OS / file manager junk === 45 | # macOS and Windows metadata files 46 | .DS_Store 47 | Thumbs.db 48 | desktop.ini 49 | 50 | # === Test / coverage artifacts === 51 | # Temporary folders created by test runs 52 | coverage/ 53 | test_output/ 54 | reports/ 55 | 56 | # === Miscellaneous === 57 | # Backup files created by editors or diff tools 58 | *~ 59 | 60 | -------------------------------------------------------------------------------- /demos/basics.nim: -------------------------------------------------------------------------------- 1 | # MIT License - Copyright (c) 2025 Eray Zesen 2 | # Github: 3 | # License information: 4 | 5 | 6 | 7 | # File: basics.nim 8 | # Description: A comprehensive example demonstrating the core features of the Textalot TUI Backend/Engine, 9 | # including drawing text/shapes with various styles and colors, and handling input events. 10 | 11 | import ../textalot, os, unicode 12 | 13 | # --- Cleanup and Initialization --- 14 | 15 | proc onExit() {.noconv.} = 16 | ## Function to clean up the terminal state (show cursor, restore settings) before exiting. 17 | deinitTextalot() 18 | quit(0) 19 | 20 | # Set a hook to ensure onExit runs when Ctrl+C is pressed, preventing a corrupted terminal state. 21 | setControlCHook(onExit) 22 | 23 | # Initialize the Textalot TUI: enables raw mode, hides cursor, and sets up buffers. 24 | initTextalot() 25 | 26 | # --- Main Application Loop --- 27 | 28 | while true: 29 | # Update: Reads input events (Key, Mouse, Resize) and performs differential rendering. 30 | updateTextalot() 31 | 32 | 33 | # Display the application title (Bold, White text on Yellow background). 34 | drawText("TEXTALOT", 2, 3, FG_COLOR_WHITE, BG_COLOR_YELLOW, STYLE_BOLD) 35 | drawText("♥", 12, 3, FG_COLOR_RED, BG_COLOR_DEFAULT) 36 | 37 | # Draw a horizontal separator line. 38 | drawText("_________________________________", 2, 4, FG_COLOR_CYAN, BG_COLOR_DEFAULT) 39 | 40 | # Display the project description. 41 | drawText(" A High-Performance TUI Backend/Engine written in Nim!", 2, 6, FG_COLOR_YELLOW, BG_COLOR_DEFAULT) 42 | 43 | # Header for the events test area. 44 | drawText("Events Test:", 2, 8, FG_COLOR_WHITE, BG_COLOR_DEFAULT, STYLE_BOLD) 45 | 46 | # Draw a cyan-filled rectangle as a background for style examples (coordinates: 2,15 to 24,24). 47 | drawRectangle(2, 15, 24, 24, FG_COLOR_DEFAULT, BG_COLOR_CYAN) 48 | 49 | # Demonstrate various text styles defined in Textalot. 50 | drawText("Style None", 5, 16, FG_COLOR_DEFAULT, BG_COLOR_CYAN, STYLE_NONE) 51 | drawText("Style Bold", 5, 17, FG_COLOR_DEFAULT, BG_COLOR_CYAN, STYLE_BOLD) 52 | drawText("Style Italic", 5, 18, FG_COLOR_DEFAULT, BG_COLOR_CYAN, STYLE_ITALIC) 53 | drawText("Style Blink", 5, 19, FG_COLOR_DEFAULT, BG_COLOR_CYAN, STYLE_BLINK) 54 | drawText("Style Faint", 5, 20, FG_COLOR_DEFAULT, BG_COLOR_CYAN, STYLE_FAINT) 55 | drawText("Style Reverse", 5, 21, FG_COLOR_DEFAULT, BG_COLOR_CYAN, STYLE_REVERSE) 56 | drawText("Style Strike", 5, 22, FG_COLOR_DEFAULT, BG_COLOR_CYAN, STYLE_STRIKE) 57 | drawText("Style Underline", 5, 23, FG_COLOR_DEFAULT, BG_COLOR_CYAN, STYLE_UNDERLINE) 58 | 59 | # Demonstrate standard and bright background colors for all ANSI palettes. 60 | drawText("Red ", 26, 15, FG_COLOR_DEFAULT, BG_COLOR_RED) 61 | drawText("Red Bright ", 35, 15, FG_COLOR_DEFAULT, BG_COLOR_RED_BRIGHT) 62 | drawText("Green ", 26, 16, FG_COLOR_DEFAULT, BG_COLOR_GREEN) 63 | drawText("Green Bright ", 35, 16, FG_COLOR_DEFAULT, BG_COLOR_GREEN_BRIGHT) 64 | drawText("Blue ", 26, 17, FG_COLOR_DEFAULT, BG_COLOR_BLUE) 65 | drawText("Blue Bright ", 35, 17, FG_COLOR_DEFAULT, BG_COLOR_BLUE_BRIGHT) 66 | drawText("Yellow ", 26, 18, FG_COLOR_DEFAULT, BG_COLOR_YELLOW) 67 | drawText("Yellow Bright ", 35, 18, FG_COLOR_DEFAULT, BG_COLOR_YELLOW_BRIGHT) 68 | drawText("Magenta ", 26, 19, FG_COLOR_DEFAULT, BG_COLOR_MAGENTA) 69 | drawText("Magenta Bright", 35, 19, FG_COLOR_DEFAULT, BG_COLOR_MAGENTA_BRIGHT) 70 | drawText("Cyan ", 26, 20, FG_COLOR_DEFAULT, BG_COLOR_CYAN) 71 | drawText("Cyan Bright ", 35, 20, FG_COLOR_DEFAULT, BG_COLOR_CYAN_BRIGHT) 72 | drawText("Black ", 26, 21, FG_COLOR_DEFAULT, BG_COLOR_BLACK) 73 | drawText("Black Bright ", 35, 21, FG_COLOR_DEFAULT, BG_COLOR_BLACK_BRIGHT) 74 | drawText("White ", 26, 22, FG_COLOR_DEFAULT, BG_COLOR_WHITE) 75 | drawText("White Bright ", 35, 22, FG_COLOR_DEFAULT, BG_COLOR_WHITE_BRIGHT) 76 | 77 | # --- Event Handling Section --- 78 | # Handles all events captured by the TUI engine (Mouse, Key, Resize). 79 | 80 | if texalotEvent of MouseEvent: 81 | var mouseEvent = MouseEvent(texalotEvent) 82 | removeArea(4, 10, 64, 11) 83 | 84 | if mouseEvent.key == EVENT_MOUSE_MOVE: 85 | drawText("- Mouse moving - x:" & $mouseEvent.x & " y:" & $mouseEvent.y, 4, 10, FG_COLOR_WHITE, BG_COLOR_DEFAULT) 86 | 87 | elif mouseEvent.key == EVENT_MOUSE_LEFT: 88 | drawText("- Mouse clicked - x:" & $mouseEvent.x & " y:" & $mouseEvent.y, 4, 10, FG_COLOR_WHITE, BG_COLOR_DEFAULT) 89 | 90 | drawChar(mouseEvent.x, mouseEvent.y, "😀", FG_COLOR_WHITE, BG_COLOR_DEFAULT) 91 | 92 | elif texalotEvent of KeyEvent: 93 | var keyEvent = KeyEvent(texalotEvent) 94 | removeArea(4, 11, 64, 11) 95 | 96 | drawText("- Key pressed - key:" & $Rune(keyEvent.key), 4, 11, FG_COLOR_WHITE, BG_COLOR_DEFAULT) #We're using Rune type for unicode character support. 97 | 98 | 99 | elif texalotEvent of ResizeEvent: 100 | removeArea(4, 12, 64, 12) 101 | 102 | drawText("- Window Resized - w:" & $getTerminalWidth() & " h:" & $getTerminalHeight(), 4, 12, FG_COLOR_WHITE, BG_COLOR_DEFAULT) 103 | 104 | # Introduce a small sleep to limit the CPU usage and control the frame rate (~100 FPS) 105 | os.sleep(10) 106 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | ***Hey, that's a lot of text here!*** 3 | 4 | **textalot** is the no-nonsense Terminal I/O and TUI Engine, written entirely in Nim, specifically optimized for POSIX systems (Linux, macOS, etc.). It’s the muscle and mind for your terminal application, delivering high-speed, flicker-free rendering and rock-solid event handling. Minimal code, maximum performance. 5 | 6 | ![textalot](media/sample.png) 7 | 8 | ## Overview 9 | Textalot is the focused, highly efficient little engine for your TUI. While it won't hand you pre-built widgets or menus, it manages the critical terminal mechanics: handling events (keyboard, mouse, resize) instantly and ensuring flicker-free rendering using an intelligent screen buffer. It’s the simple, fast, and native foundation in Nim that allows you to focus purely on your application logic. 10 | 11 | ## Key Features 12 | * Pure Nim, Pure POSIX: No external baggage, no massive runtime. Textalot is built 100% in Nim for native speed, a tiny footprint, and strict POSIX compliance (Linux/macOS). It just works. 13 | 14 | * The Jitter Killer (Flicker-Free): We use an efficient internal screen buffer. This means Textalot only sends the changes to the terminal, ensuring your display is smooth, professional, and completely free of annoying flicker. 15 | 16 | * Full ANSI Styling: Reliable support for all critical ANSI SGR codes for styling and coloring—because terminal apps deserve to look good too. 17 | 18 | * Styles: Bold, Faint/Dim, Italic, Underline, Blink, Reverse, and Strikethrough. 19 | 20 | * Colors: Full 8/16-color palette support for both foreground and background. 21 | 22 | * Event Whisperer: No input sneaks past. Textalot accurately captures everything: Key presses, full Mouse tracking (Clicks, Dragging, Scrolling), and vital Terminal Resize events (SIGWINCH). 23 | 24 | * Bare Metal Primitives: Offers direct drawText, drawChar, and drawRectangle functions for complete control over your application's canvas. You are the pixel master here. 25 | 26 | 27 | 28 | ## Usage / Getting Started 29 | Integrating Textalot is as simple as it gets. Designed to be the reliable bedrock, you can drop it into your Nim project and start drawing immediately. 30 | 31 | 1. Include the File: Place the `textalot.nim` file directly into your Nim project's source directory. 32 | 2. Import: Just tell Nim where the engine is: 33 | ```nim 34 | import os 35 | import textalot 36 | ``` 37 | 38 | 3. A basic textalot application loop is shown below. This foundational structure handles initialization, the continuous event/render loop, and a clean shutdown. 39 | ```nim 40 | import textalot, os 41 | 42 | 43 | proc onExit() {.noconv.} = 44 | ## Function to clean up the terminal state (show cursor, restore settings) before exiting. 45 | deinitTextalot() 46 | quit(0) 47 | 48 | # Set a hook to ensure onExit runs when Ctrl+C is pressed, preventing a corrupted terminal state. 49 | setControlCHook(onExit) 50 | 51 | # Initialize the Textalot TUI: enables raw mode, hides cursor, and sets up buffers. 52 | initTextalot() 53 | 54 | # --- Main Application Loop --- 55 | 56 | while true: 57 | # Update: Reads input events (Key, Mouse, Resize) and performs differential rendering. 58 | updateTextalot() 59 | 60 | 61 | # Display the application title (Bold, White text on Yellow background). 62 | drawText("TEXTALOT", 2, 3, FG_COLOR_WHITE, BG_COLOR_YELLOW, STYLE_BOLD) 63 | 64 | # Draw a horizontal separator line. 65 | drawText("_________________________________", 2, 4, FG_COLOR_CYAN, BG_COLOR_DEFAULT) 66 | 67 | # Display the project description. 68 | drawText(" A High-Performance TUI Backend/Engine written in Nim!", 2, 6, FG_COLOR_YELLOW, BG_COLOR_DEFAULT) 69 | 70 | # Header for the events test area. 71 | drawText("Events Test:", 2, 8, FG_COLOR_WHITE, BG_COLOR_DEFAULT, STYLE_BOLD) 72 | 73 | # Draw a cyan-filled rectangle as a background for style examples (coordinates: 2,15 to 24,24). 74 | drawRectangle(2, 15, 24, 24, FG_COLOR_DEFAULT, BG_COLOR_CYAN) 75 | 76 | # --- Style Examples --- 77 | drawText("Style None", 5, 16, FG_COLOR_DEFAULT, BG_COLOR_CYAN, STYLE_NONE) 78 | drawText("Style Bold", 5, 17, FG_COLOR_DEFAULT, BG_COLOR_CYAN, STYLE_BOLD) 79 | drawText("Style Italic", 5, 18, FG_COLOR_DEFAULT, BG_COLOR_CYAN, STYLE_ITALIC) 80 | drawText("Style Blink", 5, 19, FG_COLOR_DEFAULT, BG_COLOR_CYAN, STYLE_BLINK) 81 | drawText("Style Faint", 5, 20, FG_COLOR_DEFAULT, BG_COLOR_CYAN, STYLE_FAINT) 82 | drawText("Style Reverse", 5, 21, FG_COLOR_DEFAULT, BG_COLOR_CYAN, STYLE_REVERSE) 83 | drawText("Style Strike", 5, 22, FG_COLOR_DEFAULT, BG_COLOR_CYAN, STYLE_STRIKE) 84 | drawText("Style Underline", 5, 23, FG_COLOR_DEFAULT, BG_COLOR_CYAN, STYLE_UNDERLINE) 85 | 86 | 87 | # --- Event Handling Section --- 88 | # Handles all events captured by the TUI engine (Mouse, Key, Resize). 89 | 90 | if texalotEvent of MouseEvent: 91 | var mouseEvent = MouseEvent(texalotEvent) 92 | removeArea(4, 10, 64, 10) 93 | 94 | if mouseEvent.key == EVENT_MOUSE_MOVE: 95 | drawText("- Mouse moving - x:" & $mouseEvent.x & " y:" & $mouseEvent.y, 4, 10, FG_COLOR_WHITE, BG_COLOR_DEFAULT) 96 | 97 | elif mouseEvent.key == EVENT_MOUSE_LEFT: 98 | drawText("- Mouse clicked - x:" & $mouseEvent.x & " y:" & $mouseEvent.y, 4, 10, FG_COLOR_WHITE, BG_COLOR_DEFAULT) 99 | 100 | drawChar(mouseEvent.x, mouseEvent.y, '?', FG_COLOR_WHITE, BG_COLOR_MAGENTA) 101 | 102 | elif texalotEvent of KeyEvent: 103 | var keyEvent = KeyEvent(texalotEvent) 104 | removeArea(4, 11, 64, 11) 105 | 106 | # Check for exit key (ESC) 107 | if keyEvent.key == EVENT_KEY_ESC: 108 | break 109 | 110 | drawText("- Key pressed - key:" & $chr(keyEvent.key), 4, 11, FG_COLOR_WHITE, BG_COLOR_DEFAULT) 111 | 112 | elif texalotEvent of ResizeEvent: 113 | removeArea(4, 12, 64, 12) 114 | 115 | drawText("- Window Resized - w:" & $getTerminalWidth() & " h:" & $getTerminalHeight(), 4, 12, FG_COLOR_WHITE, BG_COLOR_DEFAULT) 116 | 117 | # Introduce a small sleep to limit the CPU usage and control the frame rate (~100 FPS) 118 | os.sleep(10) 119 | ``` 120 | 121 | ### API 122 | 123 | We don't need a whole separate page—textalot is simple. Everything this library offers is right here. 124 | 125 | | Function/Variable | Description | 126 | | :---- | :---- | 127 | | initTextalot() | **The absolute must-have.** Initializes the terminal for TUI mode (enables raw mode, hides cursor, and sets up buffers). Always call this at the start of your application. | 128 | | deinitTextalot() | **The Terminal Savior.** Restores the terminal to its original state before exiting the application (shows the cursor, restores raw mode, and disables mouse tracking). This must be called at the end of your program to prevent a corrupted terminal session. | 129 | | updateTexalot() | **Must be called at the beginning of every loop iteration.** This is crucial as it handles both reading input events and performing the differential screen rendering for that cycle. | 130 | | textalotEvent | After updateTexalot() is called, this global variable holds the event that occurred during that step. You check this variable to perform event-driven logic. It can be a NoneEvent, MouseEvent, KeyEvent, or ResizeEvent. 131 | | drawText(text:string, x:int, y:int, fgColor:uint32, bgColor:uint32, style:uint16) | Prints a string at the given position (x, y) using the specified foreground color, background color, and style. | 132 | | drawRectangle(x1:int, y1:int, x2:int, y2:int, fgColor:uint32, bgColor:uint32, ch:char, style:uint16) | Fills a rectangular area defined by two corners (x1, y1 to x2, y2) using a specific character and given colors/styles. Useful for drawing UI elements or clearing areas. | 133 | | drawChar(x:int, y: int, ch: char, fg:uint32, bg: uint32, style:uint16) | Draws a single character at the specified coordinates and updates the cell's properties (color, style) in the back buffer. | 134 | | getTerminalWidth() | Returns the current width (number of columns) of the terminal window. | 135 | | getTerminalHeight() | Returns the current height (number of rows) of the terminal window. | 136 | -------------------------------------------------------------------------------- /textalot.nim: -------------------------------------------------------------------------------- 1 | # MIT License - Copyright (c) 2025 Eray Zesen 2 | # Github: https://github.com/erayzesen/textalot 3 | # License information: https://github.com/erayzesen/textalot/blob/master/LICENSE 4 | 5 | 6 | import posix,strutils,Termios 7 | import unicode 8 | 9 | #Event Mouse 10 | 11 | const 12 | EVENT_MOUSE_NONE*:uint16=0 13 | EVENT_MOUSE_LEFT*:uint16=1 14 | EVENT_MOUSE_RIGHT*:uint16=2 15 | EVENT_MOUSE_MIDDLE*:uint16=3 16 | EVENT_MOUSE_MOVE*:uint16=4 17 | EVENT_MOUSE_RELEASE*:uint16=5 18 | EVENT_MOUSE_WHEEL_UP*:uint16=6 19 | EVENT_MOUSE_WHEEL_DOWN*:uint16=7 20 | EVENT_MOUSE_LEFT_DRAG*:uint16=8 21 | EVENT_MOUSE_MIDDLE_DRAG*:uint16=9 22 | EVENT_MOUSE_RIGHT_DRAG*:uint16=10 23 | 24 | 25 | #Event Keys 26 | const 27 | EVENT_KEY_NONE*: uint16 = 0 28 | EVENT_KEY_CTRL_TILDE*: uint16 = 1 # old CTRL_TILDE (0x00 / NUL) 29 | EVENT_KEY_CTRL_A*: uint16 = 2 30 | EVENT_KEY_CTRL_B*: uint16 = 3 31 | EVENT_KEY_CTRL_C*: uint16 = 4 32 | EVENT_KEY_CTRL_D*: uint16 = 5 33 | EVENT_KEY_CTRL_E*: uint16 = 6 34 | EVENT_KEY_CTRL_F*: uint16 = 7 35 | EVENT_KEY_CTRL_G*: uint16 = 8 36 | EVENT_KEY_BACKSPACE*: uint16 = 9 37 | EVENT_KEY_TAB*: uint16 = 10 38 | EVENT_KEY_ENTER*: uint16 = 11 39 | EVENT_KEY_CTRL_K*: uint16 = 12 40 | EVENT_KEY_CTRL_L*: uint16 = 13 41 | EVENT_KEY_CTRL_N*: uint16 = 15 42 | EVENT_KEY_CTRL_O*: uint16 = 16 43 | EVENT_KEY_CTRL_P*: uint16 = 17 44 | EVENT_KEY_CTRL_Q*: uint16 = 18 45 | EVENT_KEY_CTRL_R*: uint16 = 19 46 | EVENT_KEY_CTRL_S*: uint16 = 20 47 | EVENT_KEY_CTRL_T*: uint16 = 21 48 | EVENT_KEY_CTRL_U*: uint16 = 22 49 | EVENT_KEY_CTRL_V*: uint16 = 23 50 | EVENT_KEY_CTRL_W*: uint16 = 24 51 | EVENT_KEY_CTRL_X*: uint16 = 25 52 | EVENT_KEY_CTRL_Y*: uint16 = 26 53 | EVENT_KEY_CTRL_Z*: uint16 = 27 54 | EVENT_KEY_ESC*: uint16 = 28 55 | EVENT_KEY_CTRL_4*: uint16 = 29 56 | EVENT_KEY_CTRL_5*: uint16 = 30 57 | EVENT_KEY_CTRL_6*: uint16 = 31 58 | EVENT_KEY_CTRL_7*: uint16 = 32 59 | EVENT_KEY_SPACE*: uint16 = 33 60 | EVENT_KEY_BACKSPACE2*: uint16 = 34 61 | EVENT_KEY_ARROW_UP*: uint16 = 35 62 | EVENT_KEY_ARROW_DOWN*: uint16 = 36 63 | EVENT_KEY_ARROW_RIGHT*: uint16 = 37 64 | EVENT_KEY_ARROW_LEFT*: uint16 = 38 65 | EVENT_KEY_HOME*: uint16 = 39 66 | EVENT_KEY_INSERT*: uint16 = 40 67 | EVENT_KEY_DELETE*: uint16 = 41 68 | EVENT_KEY_END*: uint16 = 42 69 | EVENT_KEY_PGUP*: uint16 = 43 70 | EVENT_KEY_PGDN*: uint16 = 44 71 | EVENT_KEY_F1*: uint16 = 45 72 | EVENT_KEY_F2*: uint16 = 46 73 | EVENT_KEY_F3*: uint16 = 47 74 | EVENT_KEY_F4*: uint16 = 48 75 | EVENT_KEY_F5*: uint16 = 49 76 | EVENT_KEY_F6*: uint16 = 50 77 | EVENT_KEY_F7*: uint16 = 51 78 | EVENT_KEY_F8*: uint16 = 52 79 | EVENT_KEY_F9*: uint16 = 53 80 | EVENT_KEY_F10*: uint16 = 54 81 | EVENT_KEY_F11*: uint16 = 55 82 | EVENT_KEY_F12*: uint16 = 56 83 | 84 | 85 | 86 | #Terminal Foreground Colors 87 | const 88 | FG_COLOR_BLACK*:uint32=30 89 | FG_COLOR_RED*:uint32=31 90 | FG_COLOR_GREEN*:uint32=32 91 | FG_COLOR_YELLOW*:uint32=33 92 | FG_COLOR_BLUE*:uint32=34 93 | FG_COLOR_MAGENTA*:uint32=35 94 | FG_COLOR_CYAN*:uint32=36 95 | FG_COLOR_WHITE*:uint32=37 96 | FG_COLOR_DEFAULT*:uint32=39 97 | 98 | FG_COLOR_BLACK_BRIGHT*:uint32=90 99 | FG_COLOR_RED_BRIGHT*:uint32=91 100 | FG_COLOR_GREEN_BRIGHT*:uint32=92 101 | FG_COLOR_YELLOW_BRIGHT*:uint32=93 102 | FG_COLOR_BLUE_BRIGHT*:uint32=94 103 | FG_COLOR_MAGENTA_BRIGHT*:uint32=95 104 | FG_COLOR_CYAN_BRIGHT*:uint32=96 105 | FG_COLOR_WHITE_BRIGHT*:uint32=97 106 | 107 | #Terminal Background Colors 108 | const 109 | BG_COLOR_BLACK*:uint32=40 110 | BG_COLOR_RED*:uint32=41 111 | BG_COLOR_GREEN*:uint32=42 112 | BG_COLOR_YELLOW*:uint32=43 113 | BG_COLOR_BLUE*:uint32=44 114 | BG_COLOR_MAGENTA*:uint32=45 115 | BG_COLOR_CYAN*:uint32=46 116 | BG_COLOR_WHITE*:uint32=47 117 | BG_COLOR_DEFAULT*:uint32=49 118 | 119 | BG_COLOR_BLACK_BRIGHT*:uint32=100 120 | BG_COLOR_RED_BRIGHT*:uint32=101 121 | BG_COLOR_GREEN_BRIGHT*:uint32=102 122 | BG_COLOR_YELLOW_BRIGHT*:uint32=103 123 | BG_COLOR_BLUE_BRIGHT*:uint32=104 124 | BG_COLOR_MAGENTA_BRIGHT*:uint32=105 125 | BG_COLOR_CYAN_BRIGHT*:uint32=106 126 | BG_COLOR_WHITE_BRIGHT*:uint32=107 127 | 128 | #Terminal styles 129 | const 130 | STYLE_NONE*: uint16 = 0 131 | STYLE_BOLD*: uint16 = 1 shl 0 # SGR Code 1 (Bold/Increased intensity) 132 | STYLE_FAINT*: uint16 = 1 shl 1 # SGR Code 2 (Faint/Dim/Decreased intensity) 133 | STYLE_ITALIC*: uint16 = 1 shl 2 # SGR Code 3 (Italic) 134 | STYLE_UNDERLINE*: uint16 = 1 shl 3 # SGR Code 4 (Underline) 135 | STYLE_REVERSE*: uint16 = 1 shl 4 # SGR Code 7 (Reverse/Invert) 136 | STYLE_STRIKE*: uint16 = 1 shl 5 # SGR Code 9 (Strikethrough/Crossed-out) 137 | STYLE_BLINK*: uint16 = 1 shl 6 # SGR Code 5 (Blink - Slow) 138 | 139 | 140 | 141 | var origTermios: Termios 142 | 143 | #Terminal Resize Operations 144 | var isResized: bool = false 145 | 146 | const SIGWINCH*:cint = 28 147 | 148 | proc handleResize(signum: cint) {.noconv.} = 149 | ## SIGWINCH sinyalini yakalar ve global bayrağı ayarlar. 150 | if signum == SIGWINCH: 151 | isResized = true 152 | 153 | 154 | proc setupSignalHandler() = 155 | ## Uygulama başlangıcında sinyal yakalamayı kurar. 156 | var action: SigAction # Yeni Sigaction yapımızı tanımlıyoruz 157 | 158 | # sa_handler'a bizim handleResize fonksiyonumuzu atıyoruz 159 | action.sa_handler = handleResize 160 | action.sa_flags = 0 161 | 162 | # Üçüncü argüman olan 'olact' (eski ayarı saklama), 163 | # eğer nil geçilmesi gerekiyorsa bu şekilde yapılır: 164 | let nullPtr = cast[ptr Sigaction](0) 165 | 166 | # sigaction(sinyal, yeni_ayarlar, eski_ayarları_sakla) 167 | let result = sigaction(SIGWINCH, action, nullPtr) 168 | 169 | if result != 0: 170 | # Hata kontrolü ekleyebiliriz 171 | echo "Hata: SIGWINCH işleyicisi kurulamadı." 172 | 173 | proc hideCursor*() = 174 | stdout.write("\x1b[?25l") 175 | stdout.flushFile() 176 | 177 | proc showCursor*() = 178 | stdout.write("\x1b[?25h") 179 | stdout.flushFile() 180 | 181 | proc enableMouseTracking() = 182 | stdout.write("\x1b[?1003h\x1b[?1006h") 183 | stdout.flushFile() 184 | 185 | proc disableMouseTracking() = 186 | stdout.write("\x1b[?1003l\x1b[?1006l") 187 | stdout.flushFile() 188 | 189 | proc enableRawMode() = 190 | discard tcgetattr(STDIN_FILENO, origTermios.addr) 191 | var raw = origTermios 192 | raw.c_lflag = raw.c_lflag and not (ICANON or ECHO) 193 | raw.c_cc[VMIN] = char(1) 194 | raw.c_cc[VTIME] = char(0) 195 | discard tcsetattr(STDIN_FILENO, TCSAFLUSH, raw.addr) 196 | stdout.write("\x1b[?1049h") 197 | stdout.flushFile() 198 | 199 | 200 | # Disable raw mode 201 | proc disableRawMode() = 202 | discard tcsetattr(STDIN_FILENO, TCSAFLUSH, origTermios.addr) 203 | stdout.write("\x1b[?1049l") 204 | stdout.flushFile() 205 | 206 | #Get Terminal Size 207 | type 208 | Winsize = object 209 | ws_row: cushort 210 | ws_col: cushort 211 | ws_xpixel: cushort 212 | ws_ypixel: cushort 213 | 214 | proc getTerminalWidth*: int = 215 | var ws: Winsize 216 | if ioctl(STDOUT_FILENO, TIOCGWINSZ, addr ws) == -1: 217 | return 0 218 | return int(ws.ws_col) 219 | 220 | proc getTerminalHeight*: int = 221 | var ws: Winsize 222 | if ioctl(STDOUT_FILENO, TIOCGWINSZ, addr ws) == -1: 223 | return 0 224 | return int(ws.ws_row) 225 | 226 | # Clear Screen 227 | proc clearScreen*() = 228 | stdout.write("\x1b[49m\x1b[39m\x1b[2J\x1b[H") 229 | stdout.flushFile() 230 | 231 | ### BUFFERS ### 232 | type 233 | Cell = object 234 | bg:uint32 235 | fg:uint32 236 | ch:string=" " 237 | style:uint16=STYLE_NONE # Style attributes (Bold, Underline, etc.) 238 | Buffer = object 239 | width :int 240 | height :int 241 | data:seq[Cell] 242 | 243 | proc newBuffer(w,h:int) :Buffer = 244 | result.width=w 245 | result.height=h 246 | result.data=newSeq[Cell](w*h) 247 | var defaultCell=Cell(bg: BG_COLOR_DEFAULT, fg: FG_COLOR_DEFAULT, ch:" ") 248 | 249 | for i in 0..w*h-1 : 250 | result.data[i]=defaultCell 251 | 252 | 253 | var textalotFrontBuffer*:Buffer 254 | var textalotBackBuffer*:Buffer 255 | 256 | proc recreateBuffers*() = 257 | let w = max(getTerminalWidth(),10) 258 | let h = max(getTerminalHeight(),10) 259 | textalotBackBuffer=newBuffer(w,h ) 260 | textalotFrontBuffer=newBuffer(w,h ) 261 | clearScreen() 262 | 263 | ### END OF BUFFERS ### 264 | 265 | 266 | 267 | ### INITIALIZERS ### 268 | 269 | proc initTextalot*() = 270 | recreateBuffers() 271 | hideCursor() 272 | enableRawMode() 273 | setupSignalHandler() #Catching resize events 274 | enableMouseTracking() 275 | clearScreen() 276 | 277 | 278 | 279 | proc deinitTextalot*() = 280 | showCursor() 281 | disableRawMode() 282 | disableMouseTracking() 283 | 284 | ### END OF INITIALIZERS ### 285 | 286 | 287 | ### RENDERING ### 288 | # Move Curstor 289 | 290 | proc getMoveCursorCode(x,y:int):string = 291 | return "\x1b[" & $y & ";" & $x & "H" 292 | 293 | 294 | proc texalotRender*() = 295 | ## Compares the front and back buffers and draws only the differences to the terminal. 296 | ## This reduces screen flickering and improves performance. 297 | 298 | # Ensure buffers have the same dimensions before proceeding 299 | # If dimensions mismatch, something went wrong, or terminal size changed (needs resize handling) 300 | if textalotBackBuffer.width != textalotFrontBuffer.width or 301 | textalotBackBuffer.height != textalotFrontBuffer.height: 302 | return 303 | 304 | let bufferSize = textalotBackBuffer.data.len 305 | 306 | # Use a StringBuilder or collect data to write in bulk for better terminal performance 307 | var output = "" 308 | var appliedReset = false 309 | 310 | var lastFg: uint32 = FG_COLOR_DEFAULT 311 | var lastBg: uint32 = BG_COLOR_DEFAULT 312 | var lastStyle: uint16 = STYLE_NONE 313 | 314 | # Iterate over every cell in the buffer 315 | for i in 0..= h: 386 | return 387 | for ch in text.runes: 388 | if currentX >= 0 and currentX < w: 389 | let index = currentY * w + currentX 390 | textalotBackBuffer.data[index] = Cell( 391 | ch: ch.toUTF8(), 392 | fg: fg, 393 | bg: bg, 394 | style:style 395 | ) 396 | currentX+=1 397 | 398 | proc drawChar*(x, y: int, ch: string, fg, bg: uint32,style:uint16=STYLE_NONE) = 399 | let w = textalotBackBuffer.width 400 | let h = textalotBackBuffer.height 401 | 402 | 403 | if x >= 0 and x < w and y >= 0 and y < h: 404 | let index = y * w + x 405 | 406 | var fch=if ch=="" : " " else : ch 407 | 408 | textalotBackBuffer.data[index] = Cell( 409 | ch: fch.runeAt(0).toUTF8(), 410 | fg: fg, 411 | bg: bg, 412 | style:style 413 | ) 414 | 415 | proc drawRectangle*(x1,y1,x2,y2:int,bg,fg:uint32,ch:string=" ",style:uint16=STYLE_NONE) = 416 | let startX = min(x1, x2) 417 | let endX = max(x1, x2) 418 | let startY = min(y1, y2) 419 | let endY = max(y1, y2) 420 | 421 | let w = textalotBackBuffer.width 422 | let h = textalotBackBuffer.height 423 | 424 | var fch=if ch=="" : " " else : ch 425 | 426 | let fillCell = Cell(bg: bg, fg: fg, ch: fch.runeAt(0).toUTF8(),style:style) 427 | 428 | for y in startY..= 0 and x < w and y >= 0 and y < h: 431 | let index = y * w + x 432 | textalotBackBuffer.data[index] = fillCell 433 | 434 | 435 | proc removeArea*(x1,y1,x2,y2:int) = 436 | drawRectangle(x1,y1,x2,y2,BG_COLOR_DEFAULT,FG_COLOR_DEFAULT," ") 437 | 438 | 439 | 440 | ### END OF RENDERING ### 441 | 442 | 443 | 444 | ### EVENTS ### 445 | 446 | type 447 | Event* = ref object of RootObj 448 | 449 | ResizeEvent* = ref object of Event 450 | 451 | KeyEvent* = ref object of Event 452 | key*: uint32 453 | 454 | MouseEvent* = ref object of Event 455 | key*: uint32 456 | x*: int 457 | y*: int 458 | shift*: bool 459 | ctrl*: bool 460 | alt*: bool 461 | 462 | NoneEvent* = ref object of Event 463 | 464 | #We're using queue based system of the event handling 465 | # --- Internal Event Queue --- 466 | var eventQueue: seq[Event] = @[] 467 | 468 | proc enqueue(ev: Event) = 469 | if not ev.isNil: eventQueue.add(ev) 470 | 471 | proc dequeue(): Event = 472 | if eventQueue.len > 0: 473 | result = eventQueue[0] 474 | eventQueue.delete(0) 475 | else: 476 | result = new(NoneEvent) 477 | 478 | 479 | # ---Main Non-blocking Reader --- 480 | proc readEvent*(): Event = 481 | if eventQueue.len > 0: 482 | return dequeue() 483 | 484 | if isResized: 485 | isResized = false 486 | return new(ResizeEvent) 487 | 488 | var readfds: TFdSet 489 | FD_ZERO(readfds) 490 | FD_SET(STDIN_FILENO, readfds) 491 | var tv: Timeval 492 | tv.tv_sec = posix.Time(0) 493 | tv.tv_usec = 1000 494 | 495 | let sel = select(STDIN_FILENO + 1, addr readfds, nil, nil, addr tv) 496 | if sel <= 0: 497 | return new(NoneEvent) 498 | 499 | var buf = newString(256) 500 | let n = read(STDIN_FILENO, buf[0].addr, buf.len) 501 | if n <= 0: 502 | return new(NoneEvent) 503 | 504 | var s = buf[0 ..< n] 505 | 506 | while s.len > 0: 507 | var parsed = false 508 | var key: uint32 = EVENT_KEY_NONE 509 | 510 | # --- SGR Mouse --- 511 | if s.startsWith("\x1b[<"): 512 | let endPos = s.find({'M','m'}) 513 | if endPos > 0: 514 | let seq = s[3 ..< endPos] 515 | let finalKind = s[endPos] 516 | let parts = seq.split(';') 517 | if parts.len >= 3: 518 | let cb = parseInt(parts[0]) 519 | let cx = parseInt(parts[1]) - 1 520 | let cy = parseInt(parts[2]) - 1 521 | let button = cb and 3 522 | let isMotion = (cb and 32) != 0 523 | let isCtrl = (cb and 16) != 0 524 | let isMeta = (cb and 8) != 0 525 | let isShift = (cb and 4) != 0 526 | let isWheelUp = cb == 64 527 | let isWheelDown = cb == 65 528 | 529 | var ev = new(MouseEvent) 530 | ev.x = cx 531 | ev.y = cy 532 | ev.ctrl = isCtrl 533 | ev.alt = isMeta 534 | ev.shift = isShift 535 | 536 | if isWheelUp: 537 | ev.key = EVENT_MOUSE_WHEEL_UP 538 | elif isWheelDown: 539 | ev.key = EVENT_MOUSE_WHEEL_DOWN 540 | elif isMotion: 541 | ev.key = case button 542 | of 0: EVENT_MOUSE_LEFT_DRAG 543 | of 1: EVENT_MOUSE_MIDDLE_DRAG 544 | of 2: EVENT_MOUSE_RIGHT_DRAG 545 | else: EVENT_MOUSE_MOVE 546 | else: 547 | case button 548 | of 0: ev.key = if finalKind == 'M': EVENT_MOUSE_LEFT else: EVENT_MOUSE_RELEASE 549 | of 1: ev.key = if finalKind == 'M': EVENT_MOUSE_MIDDLE else: EVENT_MOUSE_RELEASE 550 | of 2: ev.key = if finalKind == 'M': EVENT_MOUSE_RIGHT else: EVENT_MOUSE_RELEASE 551 | else: ev.key = EVENT_MOUSE_NONE 552 | enqueue(ev) 553 | s = s[endPos+1 .. ^1] 554 | parsed = true 555 | 556 | # --- X10 Mouse --- 557 | elif s.len >= 6 and s.startsWith("\x1b[M"): 558 | let cb = ord(s[3]) - 32 559 | let x = ord(s[4]) - 33 560 | let y = ord(s[5]) - 33 561 | var ev = new(MouseEvent) 562 | ev.x = x 563 | ev.y = y 564 | ev.key = case cb and 3 565 | of 0: EVENT_MOUSE_LEFT 566 | of 1: EVENT_MOUSE_MIDDLE 567 | of 2: EVENT_MOUSE_RIGHT 568 | of 3: EVENT_MOUSE_RELEASE 569 | else: EVENT_MOUSE_NONE 570 | enqueue(ev) 571 | s = s[6..^1] 572 | parsed = true 573 | 574 | # --- Single-byte keys / Ctrl keys --- 575 | elif s.len == 1: 576 | case ord(s[0]) 577 | of 0x00: key = EVENT_KEY_CTRL_TILDE 578 | of 0x01: key = EVENT_KEY_CTRL_A 579 | of 0x02: key = EVENT_KEY_CTRL_B 580 | of 0x03: key = EVENT_KEY_CTRL_C 581 | of 0x04: key = EVENT_KEY_CTRL_D 582 | of 0x05: key = EVENT_KEY_CTRL_E 583 | of 0x06: key = EVENT_KEY_CTRL_F 584 | of 0x07: key = EVENT_KEY_CTRL_G 585 | of 0x08, 0x7F: key = EVENT_KEY_BACKSPACE 586 | of 0x09: key = EVENT_KEY_TAB 587 | of 0x0A, 0x0D: key = EVENT_KEY_ENTER 588 | of 0x0B: key = EVENT_KEY_CTRL_K 589 | of 0x0C: key = EVENT_KEY_CTRL_L 590 | of 0x0E: key = EVENT_KEY_CTRL_N 591 | of 0x0F: key = EVENT_KEY_CTRL_O 592 | of 0x10: key = EVENT_KEY_CTRL_P 593 | of 0x11: key = EVENT_KEY_CTRL_Q 594 | of 0x12: key = EVENT_KEY_CTRL_R 595 | of 0x13: key = EVENT_KEY_CTRL_S 596 | of 0x14: key = EVENT_KEY_CTRL_T 597 | of 0x15: key = EVENT_KEY_CTRL_U 598 | of 0x16: key = EVENT_KEY_CTRL_V 599 | of 0x17: key = EVENT_KEY_CTRL_W 600 | of 0x18: key = EVENT_KEY_CTRL_X 601 | of 0x19: key = EVENT_KEY_CTRL_Y 602 | of 0x1A: key = EVENT_KEY_CTRL_Z 603 | of 0x1B: key = EVENT_KEY_ESC 604 | of 0x1C: key = EVENT_KEY_CTRL_4 605 | of 0x1D: key = EVENT_KEY_CTRL_5 606 | of 0x1E: key = EVENT_KEY_CTRL_6 607 | of 0x1F: key = EVENT_KEY_CTRL_7 608 | of 0x20: key = EVENT_KEY_SPACE 609 | else: key = uint32(s.toRunes()[0].ord) 610 | s = s[1..^1] 611 | parsed = true 612 | 613 | # --- ESC sequences (arrows, F keys, Home/End, Insert/Delete, PgUp/PgDn) --- 614 | elif s.startsWith("\x1b"): 615 | if s.len >= 3 and s[1] == '[': 616 | if s[2] in {'A','B','C','D','H','F'}: 617 | case s[2] 618 | of 'A': key = EVENT_KEY_ARROW_UP 619 | of 'B': key = EVENT_KEY_ARROW_DOWN 620 | of 'C': key = EVENT_KEY_ARROW_RIGHT 621 | of 'D': key = EVENT_KEY_ARROW_LEFT 622 | of 'H': key = EVENT_KEY_HOME 623 | of 'F': key = EVENT_KEY_END 624 | else: discard 625 | s = s[3 .. ^1] 626 | parsed = true 627 | else: 628 | let endTilde = s.find('~') 629 | if endTilde >= 0: 630 | case s[2..endTilde-1] 631 | of "1": key = EVENT_KEY_HOME 632 | of "2": key = EVENT_KEY_INSERT 633 | of "3": key = EVENT_KEY_DELETE 634 | of "4": key = EVENT_KEY_END 635 | of "5": key = EVENT_KEY_PGUP 636 | of "6": key = EVENT_KEY_PGDN 637 | of "15": key = EVENT_KEY_F5 638 | of "17": key = EVENT_KEY_F6 639 | of "18": key = EVENT_KEY_F7 640 | of "19": key = EVENT_KEY_F8 641 | of "20": key = EVENT_KEY_F9 642 | of "21": key = EVENT_KEY_F10 643 | of "23": key = EVENT_KEY_F11 644 | of "24": key = EVENT_KEY_F12 645 | else: discard 646 | s = s[endTilde+1 .. ^1] 647 | parsed = true 648 | elif s.len >= 3 and s[1] == 'O': 649 | case s[2] 650 | of 'P': key = EVENT_KEY_F1 651 | of 'Q': key = EVENT_KEY_F2 652 | of 'R': key = EVENT_KEY_F3 653 | of 'S': key = EVENT_KEY_F4 654 | else: discard 655 | s = s[3..^1] 656 | parsed = true 657 | else: 658 | key = EVENT_KEY_ESC 659 | s = s[1..^1] 660 | parsed = true 661 | 662 | # --- Unicode / multi-byte UTF-8 fallback --- 663 | else: 664 | if s[0] != '\x1b': 665 | try: 666 | let firstRune = s.toRunes()[0] 667 | if firstRune.ord >= 0x20 and firstRune.ord <= 0x10FFFF: 668 | key = uint32(firstRune.ord) 669 | let runeByteLen = firstRune.toUTF8.len 670 | s = s[min(runeByteLen, s.len)..^1] 671 | parsed = true 672 | except: 673 | discard 674 | 675 | if key != EVENT_KEY_NONE: 676 | let ev = new(KeyEvent) 677 | ev.key = key 678 | enqueue(ev) 679 | 680 | if not parsed: 681 | s = s[1..^1] 682 | 683 | if eventQueue.len > 0: 684 | return dequeue() 685 | else: 686 | return new(NoneEvent) 687 | 688 | ### END OF EVENTS ### 689 | 690 | 691 | ### UPDATE ### 692 | var texalotEvent*:Event=NoneEvent() 693 | proc updateTextalot*() = 694 | texalotEvent=readEvent() # Update Events 695 | texalotRender() #Update Render 696 | if texalotEvent of ResizeEvent : 697 | recreateBuffers() 698 | 699 | 700 | ### END OF UPDATE ### 701 | 702 | 703 | 704 | --------------------------------------------------------------------------------