├── .gitignore ├── README.md ├── dub.json └── source ├── consoled.d └── terminal.d /.gitignore: -------------------------------------------------------------------------------- 1 | .dub 2 | dub.selections.json 3 | docs.json 4 | __dummy.html 5 | *.o 6 | *.obj 7 | *.a 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## ConsoleD 2 | 3 | ### License 4 | 5 | This library is licensed under Boost License. 6 | 7 | ### About 8 | 9 | ConsoleD is open-source, small library written in [D Programming Language](http://dlang.org) that 10 | helps you add colors and formatting to your console output. Work on both Windows and Posix operating systems. 11 | 12 | #### Important notes: 13 | 14 | * Font styles(underline, strikethrough) have no effect on Windows OS. 15 | * Light background colors are not supported on Posix, Non-light equivalents are used. 16 | * _Temponary_: Because `core.sys.posix.sys.ioctl` module was added recently, you must compile project with this [file](https://github.com/D-Programming-Language/druntime/blob/master/src/core/sys/posix/sys/ioctl.d). 17 | 18 | ### Featues 19 | 20 | - Setting and Getting console colors 21 | - Clearing screen 22 | - Setting console title 23 | - Getting console size 24 | - Moving the console cursor around as well as getting its position 25 | - Handling the close event 26 | - Getting input with not echo and without line buffering. 27 | 28 | ### Todo 29 | 30 | - Better input handling 31 | - Mouse input? 32 | 33 | ### Examples 34 | 35 | #### Adding colors 36 | 37 | ```D 38 | import std.stdio, consoled; 39 | 40 | void main() 41 | { 42 | foreground = Color.red; 43 | writeln("foo"); // Fg: Red | Bg: Default 44 | 45 | background = Color.blue; 46 | writeln("foo"); // Fg: Red | Bg: Blue 47 | 48 | resetColors(); // Bring back initial state 49 | } 50 | ``` 51 | 52 | or: 53 | 54 | ```D 55 | import std.stdio, consoled; 56 | 57 | void main() 58 | { 59 | setColors(Fg.red, Bg.blue); /// Order does not matter as long parameters are Fg or Bg. 60 | writeln("foo"); // Color: Red | Bg: Blue 61 | 62 | resetColors(); // Bring back initial state 63 | } 64 | ``` 65 | 66 | 67 | #### Current Foreground/Background 68 | 69 | To get current foreground and background colors, simply use `foreground` or `background` properties 70 | 71 | ```D 72 | import std.stdio, consoled; 73 | 74 | void main() 75 | { 76 | auto currentFg = foreground; 77 | auto currentBg = background; 78 | } 79 | ``` 80 | 81 | 82 | #### Font Styles 83 | 84 | You can change font styles, like `strikethrough`, `underline` and `bold`. This feature is Posix only, when called on windows, nothing happens. 85 | 86 | ```D 87 | import std.stdio, consoled; 88 | 89 | void main() 90 | { 91 | fontStyle = FontStyle.underline | FontStyle.strikethrough | FontStyle.bold; 92 | writeln("foo"); 93 | resetFontStyle(); // Or just fontStyle = FontStyle.none; 94 | } 95 | ``` 96 | 97 | #### Easy colored messages 98 | 99 | You can use helper function `writec` or `writecln` to easily colored messages. 100 | 101 | ```D 102 | import std.stdio, consoled; 103 | 104 | void main() 105 | { 106 | writecln("Hello ", Fg.blue, FontStyle.bold, "World", Bg.red, "!"); 107 | resetColors(); 108 | } 109 | ``` 110 | 111 | #### Console Size 112 | 113 | You can get console size using `size` property which return tuple containg width and height of the console. 114 | 115 | ```D 116 | import std.stdio, consoled; 117 | 118 | void main() 119 | { 120 | writeln(size); 121 | } 122 | ``` 123 | 124 | #### Cursor manipulation 125 | 126 | You can set cursor position using `setCursorPos()`: 127 | 128 | ```D 129 | import std.stdio, consoled; 130 | 131 | void main() 132 | { 133 | // 6 is half of "insert coin" length. 134 | setCursorPos(size.x / 2 - 6, size.y / 2); 135 | writeln("Insert coin"); 136 | } 137 | ``` 138 | 139 | #### Clearing the screen 140 | 141 | You can clear console screen using `clearScreen()` function: 142 | 143 | ```D 144 | import std.stdio, consoled, core.thread; 145 | 146 | void main() 147 | { 148 | // Fill whole screen with hashes 149 | fillArea(ConsolePoint(0, 0), size, '#'); 150 | 151 | // Wait 3 seconds 152 | Thread.sleep(dur!"seconds"(3)); 153 | 154 | // Clear the screen 155 | clearScreen(); 156 | } 157 | ``` 158 | 159 | 160 | #### Setting the title 161 | 162 | To set console title, use `title` property: 163 | 164 | 165 | ```D 166 | import std.stdio, consoled; 167 | 168 | void main() 169 | { 170 | title = "My new title"; 171 | } 172 | ``` 173 | 174 | 175 | #### Setting exit handler 176 | 177 | It is possible to handle some close events, such as Ctrl+C key combination using `addCloseHandler()`: 178 | 179 | ```D 180 | import std.stdio, consoled; 181 | void main() 182 | { 183 | setCloseHandler((i){ 184 | switch(i.type) 185 | { 186 | case CloseType.Other: 187 | writeln("Other"); 188 | break; 189 | 190 | case CloseType.Interrupt: 191 | writeln("Ctrl+C"); 192 | break; 193 | 194 | // Ctrl+Break for windows, Ctrl+Z for posix 195 | case CloseType.Stop: 196 | writeln("Ctrl+Break or Ctrl+Z"); 197 | break; 198 | 199 | 200 | // Posix only 201 | case CloseType.Quit: 202 | writeln(`Ctrl+\`); 203 | break; 204 | 205 | default: 206 | } 207 | 208 | writeln(i.isBlockable); 209 | }); 210 | 211 | while(true){} 212 | } 213 | 214 | ``` 215 | 216 | 217 | #### Color themes 218 | 219 | You can easy write colored messages with Color themes: 220 | 221 | ```D 222 | import std.stdio, consoled; 223 | 224 | alias Error = ColorTheme!(Color.red, Color.black); 225 | 226 | void main() 227 | { 228 | writeln(Error("foobar error")); 229 | } 230 | ``` 231 | -------------------------------------------------------------------------------- /dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "consoled", 3 | "description": "Because colors are awesome.", 4 | "authors": ["robik", "quickfur", "Adam D. Ruppe", "Ianis G. Vasilev"], 5 | "targetType": "library", 6 | "license": "Boost License", 7 | "dependencies": { 8 | "arsd-official:terminal":"~master" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /source/consoled.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Using arsd.terminal.d is recommended as it is more mature and stable. 3 | * 4 | * Provides simple API for coloring and formatting text in arsd.terminal. 5 | * On Windows OS it uses WinAPI functions, on POSIX systems it uses mainly ANSI codes. 6 | * 7 | * 8 | * $(B Important notes): 9 | * $(UL 10 | * $(LI Font styles have no effect on windows platform.) 11 | * $(LI Light background colors are not supported. Non-light equivalents are used on Posix platforms.) 12 | * ) 13 | * 14 | * License: 15 | * Boost License 16 | * Authors: 17 | * Robert 'Robik' Pasiński 18 | */ 19 | module consoled; 20 | 21 | import std.typecons, std.algorithm; 22 | 23 | 24 | /// Console output stream 25 | enum ConsoleOutputStream 26 | { 27 | /// Standard output 28 | stdout, 29 | 30 | /// Standard error output 31 | stderr 32 | } 33 | 34 | 35 | /** 36 | * Console font output style 37 | * 38 | * Does nothing on windows. 39 | */ 40 | enum FontStyle 41 | { 42 | none = 0, /// Default 43 | underline = 1, /// Underline 44 | strikethrough = 2, /// Characters legible, but marked for deletion. Not widely supported. 45 | bold = 4 /// Bold 46 | } 47 | 48 | alias void delegate(CloseEvent) @system CloseHandler; 49 | 50 | /** 51 | * Represents close event. 52 | */ 53 | struct CloseEvent 54 | { 55 | /// Close type 56 | CloseType type; 57 | 58 | /// Is close event blockable? 59 | bool isBlockable; 60 | } 61 | 62 | /** 63 | * Close type. 64 | */ 65 | enum CloseType 66 | { 67 | Interrupt, // User pressed Ctrl+C key combination. 68 | Stop, // User pressed Ctrl+Break key combination. On posix it is Ctrl+Z. 69 | Quit, // Posix only. User pressed Ctrl+\ key combination. 70 | Other // Other close reasons. Probably unblockable. 71 | } 72 | 73 | /** 74 | * Console input mode 75 | */ 76 | struct ConsoleInputMode 77 | { 78 | /// Echo printed characters? 79 | bool echo = true; 80 | 81 | /// Enable line buffering? 82 | bool line = true; 83 | 84 | /** 85 | * Creates new ConsoleInputMode instance 86 | * 87 | * Params: 88 | * echo = Echo printed characters? 89 | * line = Use Line buffering? 90 | */ 91 | this(bool echo, bool line) 92 | { 93 | this.echo = echo; 94 | this.line = line; 95 | } 96 | 97 | /** 98 | * Console input mode with no feature enabled 99 | */ 100 | static ConsoleInputMode None = ConsoleInputMode(false, false); 101 | } 102 | 103 | /** 104 | * Represents point in console. 105 | */ 106 | alias Tuple!(int, "x", int, "y") ConsolePoint; 107 | 108 | /// Special keys 109 | enum SpecialKey 110 | { 111 | home = 512, /// Home key 112 | pageUp, /// Page Up key 113 | pageDown, /// Page Down key 114 | end, /// End key 115 | delete_, /// Delete key 116 | insert, /// Insert key 117 | up, /// Arrow up key 118 | down, /// Arrow down key 119 | left, /// Arrow left key 120 | right, /// Arrow right key 121 | 122 | escape = 27,/// Escape key 123 | tab = 9, /// Tab key 124 | } 125 | 126 | //////////////////////////////////////////////////////////////////////// 127 | version(Windows) 128 | { 129 | private enum BG_MASK = 0xf0; 130 | private enum FG_MASK = 0x0f; 131 | 132 | import core.sys.windows.windows, std.stdio, std.string; 133 | 134 | /// 135 | enum Color : ushort 136 | { 137 | black = 0, /// The black color. 138 | blue = 1, /// The blue color. 139 | green = 2, /// The green color. 140 | cyan = 3, /// The cyan color. (blue-green) 141 | red = 4, /// The red color. 142 | magenta = 5, /// The magenta color. (dark pink like) 143 | yellow = 6, /// The yellow color. 144 | lightGray = 7, /// The light gray color. (silver) 145 | 146 | gray = 8, /// The gray color. 147 | lightBlue = 9, /// The light blue color. 148 | lightGreen = 10, /// The light green color. 149 | lightCyan = 11, /// The light cyan color. (light blue-green) 150 | lightRed = 12, /// The light red color. 151 | lightMagenta = 13, /// The light magenta color. (pink) 152 | lightYellow = 14, /// The light yellow color. 153 | white = 15, /// The white color. 154 | 155 | bright = 8, /// Bright flag. Use with dark colors to make them light equivalents. 156 | initial = 256 /// Default color. 157 | } 158 | 159 | 160 | private __gshared 161 | { 162 | CONSOLE_SCREEN_BUFFER_INFO info; 163 | HANDLE hOutput = null, hInput = null; 164 | 165 | Color fg, bg, defFg, defBg; 166 | CloseHandler[] closeHandlers; 167 | } 168 | 169 | 170 | shared static this() 171 | { 172 | loadDefaultColors(ConsoleOutputStream.stdout); 173 | SetConsoleCtrlHandler(cast(PHANDLER_ROUTINE)&defaultCloseHandler, true); 174 | } 175 | 176 | private void loadDefaultColors(ConsoleOutputStream cos) 177 | { 178 | uint handle; 179 | 180 | if(cos == ConsoleOutputStream.stdout) { 181 | handle = STD_OUTPUT_HANDLE; 182 | } else if(cos == ConsoleOutputStream.stderr) { 183 | handle = STD_ERROR_HANDLE; 184 | } else { 185 | assert(0, "Invalid console output stream specified"); 186 | } 187 | 188 | 189 | hOutput = GetStdHandle(handle); 190 | hInput = GetStdHandle(STD_INPUT_HANDLE); 191 | 192 | // Get current colors 193 | GetConsoleScreenBufferInfo( hOutput, &info ); 194 | 195 | // Background are first 4 bits 196 | defBg = cast(Color)((info.wAttributes & (BG_MASK)) >> 4); 197 | 198 | // Rest are foreground 199 | defFg = cast(Color) (info.wAttributes & (FG_MASK)); 200 | 201 | fg = Color.initial; 202 | bg = Color.initial; 203 | } 204 | 205 | private ushort buildColor(Color fg, Color bg) 206 | { 207 | if(fg == Color.initial) { 208 | fg = defFg; 209 | } 210 | 211 | if(bg == Color.initial) { 212 | bg = defBg; 213 | } 214 | 215 | return cast(ushort)(fg | bg << 4); 216 | } 217 | 218 | private void updateColor() 219 | { 220 | stdout.flush(); 221 | SetConsoleTextAttribute(hOutput, buildColor(fg, bg)); 222 | } 223 | 224 | 225 | /** 226 | * Current console font color 227 | * 228 | * Returns: 229 | * Current foreground color set 230 | */ 231 | Color foreground() @property 232 | { 233 | return fg; 234 | } 235 | 236 | /** 237 | * Current console background color 238 | * 239 | * Returns: 240 | * Current background color set 241 | */ 242 | Color background() @property 243 | { 244 | return bg; 245 | } 246 | 247 | /** 248 | * Sets console foreground color 249 | * 250 | * Flushes stdout. 251 | * 252 | * Params: 253 | * color = Foreground color to set 254 | */ 255 | void foreground(Color color) @property 256 | { 257 | fg = color; 258 | updateColor(); 259 | } 260 | 261 | 262 | /** 263 | * Sets console background color 264 | * 265 | * Flushes stdout. 266 | * 267 | * Params: 268 | * color = Background color to set 269 | */ 270 | void background(Color color) @property 271 | { 272 | bg = color; 273 | updateColor(); 274 | } 275 | 276 | /** 277 | * Sets new console output stream 278 | * 279 | * This function sets default colors 280 | * that are used when function is called. 281 | * 282 | * Params: 283 | * cos = New console output stream 284 | */ 285 | void outputStream(ConsoleOutputStream cos) @property 286 | { 287 | loadDefaultColors(cos); 288 | } 289 | 290 | /** 291 | * Sets console font style 292 | * 293 | * Does nothing on windows. 294 | * 295 | * Params: 296 | * fs = Font style to set 297 | */ 298 | void fontStyle(FontStyle fs) @property {} 299 | 300 | /** 301 | * Returns console font style 302 | * 303 | * Returns: 304 | * Font style, always none on windows. 305 | */ 306 | FontStyle fontStyle() @property 307 | { 308 | return FontStyle.none; 309 | } 310 | 311 | 312 | /** 313 | * Console size 314 | * 315 | * Returns: 316 | * Tuple containing console rows and cols. 317 | */ 318 | ConsolePoint size() @property 319 | { 320 | GetConsoleScreenBufferInfo( hOutput, &info ); 321 | 322 | int cols, rows; 323 | 324 | cols = (info.srWindow.Right - info.srWindow.Left + 1); 325 | rows = (info.srWindow.Bottom - info.srWindow.Top + 1); 326 | 327 | return ConsolePoint(cols, rows); 328 | } 329 | 330 | /** 331 | * Sets console position 332 | * 333 | * Params: 334 | * x = X coordinate of cursor postion 335 | * y = Y coordinate of cursor position 336 | */ 337 | void setCursorPos(int x, int y) 338 | { 339 | COORD coord = { 340 | cast(short)min(width, max(0, x)), 341 | cast(short)max(0, y) 342 | }; 343 | stdout.flush(); 344 | SetConsoleCursorPosition(hOutput, coord); 345 | } 346 | 347 | /** 348 | * Gets cursor position 349 | * 350 | * Returns: 351 | * Cursor position 352 | */ 353 | ConsolePoint cursorPos() @property 354 | { 355 | GetConsoleScreenBufferInfo( hOutput, &info ); 356 | return ConsolePoint( 357 | info.dwCursorPosition.X, 358 | min(info.dwCursorPosition.Y, height) // To keep same behaviour with posix 359 | ); 360 | } 361 | 362 | 363 | 364 | /** 365 | * Sets console title 366 | * 367 | * Params: 368 | * title = Title to set 369 | */ 370 | void title(string title) @property 371 | { 372 | SetConsoleTitleA(toStringz(title)); 373 | } 374 | 375 | 376 | /** 377 | * Adds handler for console close event. 378 | * 379 | * Params: 380 | * closeHandler = New close handler 381 | */ 382 | void addCloseHandler(CloseHandler closeHandler) 383 | { 384 | closeHandlers ~= closeHandler; 385 | } 386 | 387 | /** 388 | * Moves cursor by specified offset 389 | * 390 | * Params: 391 | * x = X offset 392 | * y = Y offset 393 | */ 394 | private void moveCursor(int x, int y) 395 | { 396 | stdout.flush(); 397 | auto pos = cursorPos(); 398 | setCursorPos(max(pos.x + x, 0), max(0, pos.y + y)); 399 | } 400 | 401 | /** 402 | * Moves cursor up by n rows 403 | * 404 | * Params: 405 | * n = Number of rows to move 406 | */ 407 | void moveCursorUp(int n = 1) 408 | { 409 | moveCursor(0, -n); 410 | } 411 | 412 | /** 413 | * Moves cursor down by n rows 414 | * 415 | * Params: 416 | * n = Number of rows to move 417 | */ 418 | void moveCursorDown(int n = 1) 419 | { 420 | moveCursor(0, n); 421 | } 422 | 423 | /** 424 | * Moves cursor left by n columns 425 | * 426 | * Params: 427 | * n = Number of columns to move 428 | */ 429 | void moveCursorLeft(int n = 1) 430 | { 431 | moveCursor(-n, 0); 432 | } 433 | 434 | /** 435 | * Moves cursor right by n columns 436 | * 437 | * Params: 438 | * n = Number of columns to move 439 | */ 440 | void moveCursorRight(int n = 1) 441 | { 442 | moveCursor(n, 0); 443 | } 444 | 445 | /** 446 | * Gets console mode 447 | * 448 | * Returns: 449 | * Current console mode 450 | */ 451 | ConsoleInputMode mode() @property 452 | { 453 | ConsoleInputMode cim; 454 | DWORD m; 455 | GetConsoleMode(hInput, &m); 456 | 457 | cim.echo = !!(m & ENABLE_ECHO_INPUT); 458 | cim.line = !!(m & ENABLE_LINE_INPUT); 459 | 460 | return cim; 461 | } 462 | 463 | /** 464 | * Sets console mode 465 | * 466 | * Params: 467 | * New console mode 468 | */ 469 | void mode(ConsoleInputMode cim) @property 470 | { 471 | DWORD m; 472 | 473 | (cim.echo) ? (m |= ENABLE_ECHO_INPUT) : (m &= ~ENABLE_ECHO_INPUT); 474 | (cim.line) ? (m |= ENABLE_LINE_INPUT) : (m &= ~ENABLE_LINE_INPUT); 475 | 476 | SetConsoleMode(hInput, m); 477 | } 478 | 479 | /** 480 | * Reads character without line buffering 481 | * 482 | * Params: 483 | * echo = Print typed characters 484 | */ 485 | int getch(bool echo = false) 486 | { 487 | INPUT_RECORD ir; 488 | DWORD count; 489 | auto m = mode; 490 | 491 | mode = ConsoleInputMode.None; 492 | 493 | do { 494 | ReadConsoleInputA(hInput, &ir, 1, &count); 495 | } while((ir.EventType != KEY_EVENT || !ir.KeyEvent.bKeyDown) && kbhit()); 496 | // the extra kbhit is to filter out events AFTER the keydown 497 | // to ensure next time we call this, we're back on a fresh keydown 498 | // event. Without that, the key up event will trigger kbhit, then 499 | // you call getch(), and it blocks because it read keyup then looped 500 | // and is waiting for another keydown. 501 | 502 | mode = m; 503 | 504 | return ir.KeyEvent.wVirtualKeyCode; 505 | } 506 | 507 | /** 508 | * Checks if any key is pressed. 509 | * 510 | * Shift, Ctrl and Alt keys are not detected. 511 | * 512 | * Returns: 513 | * True if any key is pressed, false otherwise. 514 | */ 515 | bool kbhit() 516 | { 517 | return WaitForSingleObject(hInput, 0) == WAIT_OBJECT_0; 518 | } 519 | 520 | /** 521 | * Sets cursor visibility 522 | * 523 | * Params: 524 | * visible = Cursor visibility 525 | */ 526 | void cursorVisible(bool visible) @property 527 | { 528 | CONSOLE_CURSOR_INFO cci; 529 | GetConsoleCursorInfo(hOutput, &cci); 530 | cci.bVisible = visible; 531 | SetConsoleCursorInfo(hOutput, &cci); 532 | } 533 | 534 | private CloseEvent idToCloseEvent(ulong i) 535 | { 536 | CloseEvent ce; 537 | 538 | switch(i) 539 | { 540 | case 0: 541 | ce.type = CloseType.Interrupt; 542 | break; 543 | 544 | case 1: 545 | ce.type = CloseType.Stop; 546 | break; 547 | 548 | default: 549 | ce.type = CloseType.Other; 550 | } 551 | 552 | ce.isBlockable = (ce.type != CloseType.Other); 553 | 554 | return ce; 555 | } 556 | 557 | private bool defaultCloseHandler(ulong reason) 558 | { 559 | foreach(closeHandler; closeHandlers) 560 | { 561 | closeHandler(idToCloseEvent(reason)); 562 | } 563 | 564 | return true; 565 | } 566 | } 567 | //////////////////////////////////////////////////////////////////////// 568 | else version(Posix) 569 | { 570 | static import arsd.terminal; 571 | import std.stdio, 572 | std.conv, 573 | std.string, 574 | core.sys.posix.unistd, 575 | core.sys.posix.sys.ioctl, 576 | core.sys.posix.termios, 577 | core.sys.posix.fcntl, 578 | core.sys.posix.sys.time; 579 | enum SIGINT = 2; 580 | enum SIGTSTP = 20; 581 | enum SIGQUIT = 3; 582 | extern(C) void signal(int, void function(int) @system); 583 | 584 | enum 585 | { 586 | UNDERLINE_ENABLE = 4, 587 | UNDERLINE_DISABLE = 24, 588 | 589 | STRIKE_ENABLE = 9, 590 | STRIKE_DISABLE = 29, 591 | 592 | BOLD_ENABLE = 1, 593 | BOLD_DISABLE = 21 594 | } 595 | 596 | /// 597 | enum Color : ushort 598 | { 599 | black = 30, /// The black color. 600 | red = 31, /// The red color. 601 | green = 32, /// The green color. 602 | yellow = 33, /// The yellow color. 603 | blue = 34, /// The blue color. 604 | magenta = 35, /// The magenta color. (dark pink like) 605 | cyan = 36, /// The cyan color. (blue-green) 606 | lightGray = 37, /// The light gray color. (silver) 607 | 608 | gray = 94, /// The gray color. 609 | lightRed = 95, /// The light red color. 610 | lightGreen = 96, /// The light green color. 611 | lightYellow = 97, /// The light yellow color. 612 | lightBlue = 98, /// The light red color. 613 | lightMagenta = 99, /// The light magenta color. (pink) 614 | lightCyan = 100, /// The light cyan color.(light blue-green) 615 | white = 101, /// The white color. 616 | 617 | bright = 64, /// Bright flag. Use with dark colors to make them light equivalents. 618 | initial = 256 /// Default color 619 | } 620 | 621 | 622 | private __gshared 623 | { 624 | Color fg = Color.initial; 625 | Color bg = Color.initial; 626 | File stream; 627 | int stdinFd; 628 | FontStyle currentFontStyle; 629 | 630 | CloseHandler[] closeHandlers; 631 | SpecialKey[string] specialKeys; 632 | } 633 | 634 | shared static this() 635 | { 636 | stream = stdout; 637 | signal(SIGINT, &defaultCloseHandler); 638 | signal(SIGTSTP, &defaultCloseHandler); 639 | signal(SIGQUIT, &defaultCloseHandler); 640 | stdinFd = fileno(stdin.getFP); 641 | 642 | specialKeys = [ 643 | "[A" : SpecialKey.up, 644 | "[B" : SpecialKey.down, 645 | "[C" : SpecialKey.right, 646 | "[D" : SpecialKey.left, 647 | 648 | "OH" : SpecialKey.home, 649 | "[5~": SpecialKey.pageUp, 650 | "[6~": SpecialKey.pageDown, 651 | "OF" : SpecialKey.end, 652 | "[3~": SpecialKey.delete_, 653 | "[2~": SpecialKey.insert, 654 | 655 | "\033":SpecialKey.escape 656 | ]; 657 | } 658 | 659 | 660 | private bool isRedirected() 661 | { 662 | return isatty( fileno(stream.getFP) ) != 1; 663 | } 664 | 665 | private void printAnsi() 666 | { 667 | stream.writef("\033[%d;%d;%d;%d;%d;%dm", 668 | fg & Color.bright ? 1 : 0, 669 | fg & ~Color.bright, 670 | (bg & ~Color.bright) + 10, // Background colors are normal + 10 671 | 672 | currentFontStyle & FontStyle.underline ? UNDERLINE_ENABLE : UNDERLINE_DISABLE, 673 | currentFontStyle & FontStyle.strikethrough ? STRIKE_ENABLE : STRIKE_DISABLE, 674 | currentFontStyle & FontStyle.bold ? BOLD_ENABLE : BOLD_DISABLE 675 | ); 676 | } 677 | 678 | /** 679 | * Sets console foreground color 680 | * 681 | * Params: 682 | * color = Foreground color to set 683 | */ 684 | void foreground(Color color) @property 685 | { 686 | if(isRedirected()) { 687 | return; 688 | } 689 | 690 | fg = color; 691 | printAnsi(); 692 | } 693 | 694 | /** 695 | * Sets console background color 696 | * 697 | * Params: 698 | * color = Background color to set 699 | */ 700 | void background(Color color) @property 701 | { 702 | if(isRedirected()) { 703 | return; 704 | } 705 | 706 | bg = color; 707 | printAnsi(); 708 | } 709 | 710 | /** 711 | * Current console background color 712 | * 713 | * Returns: 714 | * Current foreground color set 715 | */ 716 | Color foreground() @property 717 | { 718 | return fg; 719 | } 720 | 721 | /** 722 | * Current console font color 723 | * 724 | * Returns: 725 | * Current background color set 726 | */ 727 | Color background() @property 728 | { 729 | return bg; 730 | } 731 | 732 | /** 733 | * Sets new console output stream 734 | * 735 | * Params: 736 | * cos = New console output stream 737 | */ 738 | void outputStream(ConsoleOutputStream cos) @property 739 | { 740 | if(cos == ConsoleOutputStream.stdout) { 741 | stream = stdout; 742 | } else if(cos == ConsoleOutputStream.stderr) { 743 | stream = stderr; 744 | } else { 745 | assert(0, "Invalid consone output stream specified"); 746 | } 747 | } 748 | 749 | 750 | /** 751 | * Sets console font style 752 | * 753 | * Params: 754 | * fs = Font style to set 755 | */ 756 | void fontStyle(FontStyle fs) @property 757 | { 758 | currentFontStyle = fs; 759 | printAnsi(); 760 | } 761 | 762 | /** 763 | * Console size 764 | * 765 | * Returns: 766 | * Tuple containing console rows and cols. 767 | */ 768 | ConsolePoint size() @property 769 | { 770 | winsize w; 771 | terminal.ioctl(STDOUT_FILENO, terminal.TIOCGWINSZ, &w); 772 | 773 | return ConsolePoint(cast(int)w.ws_col, cast(int)w.ws_row); 774 | } 775 | 776 | /** 777 | * Sets console position 778 | * 779 | * Params: 780 | * x = X coordinate of cursor postion 781 | * y = Y coordinate of cursor position 782 | */ 783 | void setCursorPos(int x, int y) 784 | { 785 | stdout.flush(); 786 | writef("\033[%d;%df", y + 1, x + 1); 787 | } 788 | 789 | /** 790 | * Gets cursor position 791 | * 792 | * Returns: 793 | * Cursor position 794 | */ 795 | ConsolePoint cursorPos() @property 796 | { 797 | termios told, tnew; 798 | char[] buf; 799 | 800 | tcgetattr(0, &told); 801 | tnew = told; 802 | tnew.c_lflag &= ~ECHO & ~ICANON; 803 | tcsetattr(0, TCSANOW, &tnew); 804 | 805 | write("\033[6n"); 806 | stdout.flush(); 807 | foreach(i; 0..8) 808 | { 809 | char c; 810 | c = cast(char)getch(); 811 | buf ~= c; 812 | if(c == 'R') 813 | break; 814 | } 815 | tcsetattr(0, TCSANOW, &told); 816 | 817 | buf = buf[2..$-1]; 818 | auto tmp = buf.split(";"); 819 | 820 | return ConsolePoint(to!int(tmp[1]) - 1, to!int(tmp[0]) - 1); 821 | } 822 | 823 | /** 824 | * Sets console title 825 | * 826 | * Params: 827 | * title = Title to set 828 | */ 829 | void title(string title) @property 830 | { 831 | stdout.flush(); 832 | writef("\033]0;%s\007", title); // TODO: Check if supported 833 | } 834 | 835 | /** 836 | * Adds handler for console close event. 837 | * 838 | * Params: 839 | * closeHandler = New close handler 840 | */ 841 | void addCloseHandler(CloseHandler closeHandler) 842 | { 843 | closeHandlers ~= closeHandler; 844 | } 845 | 846 | /** 847 | * Moves cursor up by n rows 848 | * 849 | * Params: 850 | * n = Number of rows to move 851 | */ 852 | void moveCursorUp(int n = 1) 853 | { 854 | writef("\033[%dA", n); 855 | } 856 | 857 | /** 858 | * Moves cursor down by n rows 859 | * 860 | * Params: 861 | * n = Number of rows to move 862 | */ 863 | void moveCursorDown(int n = 1) 864 | { 865 | writef("\033[%dB", n); 866 | } 867 | 868 | /** 869 | * Moves cursor left by n columns 870 | * 871 | * Params: 872 | * n = Number of columns to move 873 | */ 874 | void moveCursorLeft(int n = 1) 875 | { 876 | writef("\033[%dD", n); 877 | } 878 | 879 | /** 880 | * Moves cursor right by n columns 881 | * 882 | * Params: 883 | * n = Number of columns to move 884 | */ 885 | void moveCursorRight(int n = 1) 886 | { 887 | writef("\033[%dC", n); 888 | } 889 | 890 | /** 891 | * Gets console mode 892 | * 893 | * Returns: 894 | * Current console mode 895 | */ 896 | ConsoleInputMode mode() @property 897 | { 898 | ConsoleInputMode cim; 899 | termios tio; 900 | ubyte[100] hack; 901 | 902 | tcgetattr(stdinFd, &tio); 903 | cim.echo = !!(tio.c_lflag & ECHO); 904 | cim.line = !!(tio.c_lflag & ICANON); 905 | 906 | return cim; 907 | } 908 | 909 | /** 910 | * Sets console mode 911 | * 912 | * Params: 913 | * New console mode 914 | */ 915 | void mode(ConsoleInputMode cim) @property 916 | { 917 | termios tio; 918 | ubyte[100] hack; 919 | 920 | tcgetattr(stdinFd, &tio); 921 | 922 | (cim.echo) ? (tio.c_lflag |= ECHO) : (tio.c_lflag &= ~ECHO); 923 | (cim.line) ? (tio.c_lflag |= ICANON) : (tio.c_lflag &= ~ICANON); 924 | tcsetattr(stdinFd, TCSANOW, &tio); 925 | } 926 | 927 | /** 928 | * Reads character without line buffering 929 | * 930 | * Params: 931 | * echo = Print typed characters 932 | */ 933 | int getch(bool echo = false) 934 | { 935 | import std.ascii : toUpper; 936 | 937 | int c; 938 | string buf; 939 | ConsoleInputMode m; 940 | 941 | m = mode; 942 | mode = ConsoleInputMode(echo, false); 943 | c = getchar(); 944 | 945 | if(c == SpecialKey.escape) 946 | { 947 | while(kbhit()) 948 | { 949 | buf ~= getchar(); 950 | } 951 | writeln(buf); 952 | if(buf in specialKeys) { 953 | c = specialKeys[buf]; 954 | } else { 955 | c = -1; 956 | } 957 | } 958 | 959 | mode = m; 960 | 961 | return c.toUpper(); 962 | } 963 | 964 | /** 965 | * Checks if anykey is pressed. 966 | * 967 | * Shift, Ctrl and Alt keys are not detected. 968 | * 969 | * Returns: 970 | * True if anykey is pressed, false otherwise. 971 | */ 972 | bool kbhit() 973 | { 974 | ConsoleInputMode m; 975 | int c; 976 | int old; 977 | 978 | m = mode; 979 | mode = ConsoleInputMode.None; 980 | 981 | old = fcntl(STDIN_FILENO, F_GETFL, 0); 982 | fcntl(STDIN_FILENO, F_SETFL, old | O_NONBLOCK); 983 | 984 | c = getchar(); 985 | 986 | fcntl(STDIN_FILENO, F_SETFL, old); 987 | mode = m; 988 | 989 | if(c != EOF) 990 | { 991 | ungetc(c, stdin.getFP); 992 | return true; 993 | } 994 | 995 | return false; 996 | } 997 | 998 | /** 999 | * Sets cursor visibility 1000 | * 1001 | * Params: 1002 | * visible = Cursor visibility 1003 | */ 1004 | void cursorVisible(bool visible) @property 1005 | { 1006 | char c; 1007 | if(visible) 1008 | c = 'h'; 1009 | else 1010 | c = 'l'; 1011 | 1012 | writef("\033[?25%c", c); 1013 | } 1014 | 1015 | private CloseEvent idToCloseEvent(ulong i) 1016 | { 1017 | CloseEvent ce; 1018 | 1019 | switch(i) 1020 | { 1021 | case SIGINT: 1022 | ce.type = CloseType.Interrupt; 1023 | break; 1024 | 1025 | case SIGQUIT: 1026 | ce.type = CloseType.Quit; 1027 | break; 1028 | 1029 | case SIGTSTP: 1030 | ce.type = CloseType.Stop; 1031 | break; 1032 | 1033 | default: 1034 | ce.type = CloseType.Other; 1035 | } 1036 | 1037 | ce.isBlockable = (ce.type != CloseType.Other); 1038 | 1039 | return ce; 1040 | } 1041 | 1042 | private extern(C) void defaultCloseHandler(int reason) @system 1043 | { 1044 | foreach(closeHandler; closeHandlers) 1045 | { 1046 | closeHandler(idToCloseEvent(reason)); 1047 | } 1048 | } 1049 | } 1050 | 1051 | /** 1052 | * Console width 1053 | * 1054 | * Returns: 1055 | * Console width as number of columns 1056 | */ 1057 | @property int width() 1058 | { 1059 | return size.x; 1060 | } 1061 | 1062 | /** 1063 | * Console height 1064 | * 1065 | * Returns: 1066 | * Console height as number of rows 1067 | */ 1068 | @property int height() 1069 | { 1070 | return size.y; 1071 | } 1072 | 1073 | 1074 | /** 1075 | * Reads password from user 1076 | * 1077 | * Params: 1078 | * mask = Typed character mask 1079 | * 1080 | * Returns: 1081 | * Password 1082 | */ 1083 | string readPassword(char mask = '*') 1084 | { 1085 | // import terminal; 1086 | static import arsd.terminal; 1087 | auto term = Terminal(ConsoleOutputType.linear); 1088 | auto input = RealTimeConsoleInput(&term, ConsoleInputFlags.raw); 1089 | string pass; 1090 | dchar ch; 1091 | ch = input.getch(); 1092 | while(ch != '\r' && ch != '\n') 1093 | { 1094 | import std.range; 1095 | if(ch == '\b' && !pass.empty) 1096 | { 1097 | pass = pass[0..$-1]; 1098 | write('\b'); 1099 | stdout.flush; 1100 | } 1101 | else 1102 | { 1103 | pass ~= ch; 1104 | write(mask); 1105 | stdout.flush(); 1106 | } 1107 | ch = input.getch(); 1108 | } 1109 | return pass; 1110 | } 1111 | 1112 | 1113 | /** 1114 | * Fills area with specified character 1115 | * 1116 | * Params: 1117 | * p1 = Top-Left corner coordinates of area 1118 | * p2 = Bottom-Right corner coordinates of area 1119 | * fill = Character to fill area 1120 | */ 1121 | void fillArea(ConsolePoint p1, ConsolePoint p2, char fill) 1122 | { 1123 | foreach(i; p1.y .. p2.y + 1) 1124 | { 1125 | setCursorPos(p1.x, i); 1126 | write( replicate((&fill)[0..1], p2.x - p1.x)); 1127 | // ^ Converting char to char[] 1128 | stdout.flush(); 1129 | } 1130 | } 1131 | 1132 | /** 1133 | * Draws box with specified border character 1134 | * 1135 | * Params: 1136 | * p1 = Top-Left corner coordinates of box 1137 | * p2 = Bottom-Right corner coordinates of box 1138 | * fill = Border character 1139 | */ 1140 | void drawBox(ConsolePoint p1, ConsolePoint p2, char border) 1141 | { 1142 | drawHorizontalLine(p1, p2.x - p1.x, border); 1143 | foreach(i; p1.y + 1 .. p2.y) 1144 | { 1145 | setCursorPos(p1.x, i); 1146 | write(border); 1147 | setCursorPos(p2.x - 1, i); 1148 | write(border); 1149 | } 1150 | drawHorizontalLine(ConsolePoint(p1.x, p2.y), p2.x - p1.x, border); 1151 | } 1152 | 1153 | /** 1154 | * Draws horizontal line with specified fill character 1155 | * 1156 | * Params: 1157 | * pos = Start coordinates 1158 | * length = Line width 1159 | * border = Border character 1160 | */ 1161 | void drawHorizontalLine(ConsolePoint pos, int length, char border) 1162 | { 1163 | setCursorPos(pos.x, pos.y); 1164 | write(replicate((&border)[0..1], length)); 1165 | } 1166 | 1167 | /** 1168 | * Draws horizontal line with specified fill character 1169 | * 1170 | * Params: 1171 | * pos = Start coordinates 1172 | * length = Line height 1173 | * border = Border character 1174 | */ 1175 | void drawVerticalLine(ConsolePoint pos, int length, char border) 1176 | { 1177 | foreach(i; pos.y .. length) 1178 | { 1179 | setCursorPos(pos.x, i); 1180 | write(border); 1181 | } 1182 | } 1183 | 1184 | /** 1185 | * Writes at specified position 1186 | * 1187 | * Params: 1188 | * point = Where to write 1189 | * data = Data to write 1190 | */ 1191 | void writeAt(T)(ConsolePoint point, T data) 1192 | { 1193 | setCursorPos(point.x, point.y); 1194 | write(data); 1195 | stdout.flush(); 1196 | } 1197 | 1198 | /** 1199 | * Clears console screen 1200 | */ 1201 | void clearScreen() 1202 | { 1203 | auto size = size; 1204 | short length = cast(short)(size.x * size.y); // Number of all characters to write 1205 | setCursorPos(0, 0); 1206 | import std.array : replicate; 1207 | write( replicate(" ", length)); 1208 | stdout.flush(); 1209 | } 1210 | 1211 | /** 1212 | * Brings default colors back 1213 | */ 1214 | void resetColors() 1215 | { 1216 | foreground = Color.initial; 1217 | background = Color.initial; 1218 | } 1219 | 1220 | 1221 | /** 1222 | * Brings font formatting to default 1223 | */ 1224 | void resetFontStyle() 1225 | { 1226 | fontStyle = FontStyle.none; 1227 | } 1228 | 1229 | 1230 | struct EnumTypedef(T, string _name) if(is(T == enum)) 1231 | { 1232 | public T val = T.init; 1233 | 1234 | this(T v) { val = v; } 1235 | 1236 | static EnumTypedef!(T, _name) opDispatch(string n)() 1237 | { 1238 | return EnumTypedef!(T, _name)(__traits(getMember, val, n)); 1239 | } 1240 | } 1241 | 1242 | /// Alias for color enum 1243 | alias EnumTypedef!(Color, "fg") Fg; 1244 | 1245 | /// ditto 1246 | alias EnumTypedef!(Color, "bg") Bg; 1247 | 1248 | 1249 | /** 1250 | * Represents color theme. 1251 | * 1252 | * Examples: 1253 | * ---- 1254 | * alias ThError = ColorTheme(Color.red, Color.black); 1255 | * writeln(ThError("string to write using Error theme(red foreground on black background)")); 1256 | * ---- 1257 | */ 1258 | struct ColorTheme(Color fg, Color bg) 1259 | { 1260 | string s; 1261 | this(string s) 1262 | { 1263 | this.s = s; 1264 | } 1265 | 1266 | void toString(scope void delegate(const(char)[]) sink) const 1267 | { 1268 | auto _fg = foreground; 1269 | auto _bg = background; 1270 | foreground = fg; 1271 | background = bg; 1272 | sink(s.dup); 1273 | foreground = _fg; 1274 | background = _bg; 1275 | } 1276 | } 1277 | 1278 | 1279 | /** 1280 | * Writes text to console and colorizes text 1281 | * 1282 | * Params: 1283 | * params = Text to write 1284 | */ 1285 | void writec(T...)(T params) 1286 | { 1287 | foreach(param; params) 1288 | { 1289 | static if(is(typeof(param) == Fg)) { 1290 | foreground = param.val; 1291 | } else static if(is(typeof(param) == Bg)) { 1292 | background = param.val; 1293 | } else static if(is(typeof(param) == FontStyle)) { 1294 | fontStyle = param; 1295 | } else { 1296 | write(param); 1297 | } 1298 | } 1299 | } 1300 | 1301 | /** 1302 | * Writes line to console and goes to newline 1303 | * 1304 | * Params: 1305 | * params = Text to write 1306 | */ 1307 | void writecln(T...)(T params) 1308 | { 1309 | writec(params); 1310 | writeln(); 1311 | } 1312 | -------------------------------------------------------------------------------- /source/terminal.d: -------------------------------------------------------------------------------- 1 | /++ 2 | Module for interacting with the user's terminal, including color output, cursor manipulation, and full-featured real-time mouse and keyboard input. Also includes high-level convenience methods, like [Terminal.getline], which gives the user a line editor with history, completion, etc. See the [#examples]. 3 | 4 | 5 | The main interface for this module is the Terminal struct, which 6 | encapsulates the output functions and line-buffered input of the terminal, and 7 | RealTimeConsoleInput, which gives real time input. 8 | 9 | Creating an instance of these structs will perform console initialization. When the struct 10 | goes out of scope, any changes in console settings will be automatically reverted. 11 | 12 | Note: on Posix, it traps SIGINT and translates it into an input event. You should 13 | keep your event loop moving and keep an eye open for this to exit cleanly; simply break 14 | your event loop upon receiving a UserInterruptionEvent. (Without 15 | the signal handler, ctrl+c can leave your terminal in a bizarre state.) 16 | 17 | As a user, if you have to forcibly kill your program and the event doesn't work, there's still ctrl+\ 18 | 19 | On Mac Terminal btw, a lot of hacks are needed and mouse support doesn't work. Most functions basically 20 | work now though. 21 | 22 | Future_Roadmap: 23 | $(LIST 24 | * The CharacterEvent and NonCharacterKeyEvent types will be removed. Instead, use KeyboardEvent 25 | on new programs. 26 | 27 | * The ScrollbackBuffer will be expanded to be easier to use to partition your screen. It might even 28 | handle input events of some sort. Its API may change. 29 | 30 | * getline I want to be really easy to use both for code and end users. It will need multi-line support 31 | eventually. 32 | 33 | * I might add an expandable event loop and base level widget classes. This may be Linux-specific in places and may overlap with similar functionality in simpledisplay.d. If I can pull it off without a third module, I want them to be compatible with each other too so the two modules can be combined easily. (Currently, they are both compatible with my eventloop.d and can be easily combined through it, but that is a third module.) 34 | 35 | * More advanced terminal features as functions, where available, like cursor changing and full-color functions. 36 | 37 | * The module will eventually be renamed to `arsd.terminal`. 38 | 39 | * More documentation. 40 | ) 41 | 42 | WHAT I WON'T DO: 43 | $(LIST 44 | * support everything under the sun. If it isn't default-installed on an OS I or significant number of other people 45 | might actually use, and isn't written by me, I don't really care about it. This means the only supported terminals are: 46 | $(LIST 47 | 48 | * xterm (and decently xterm compatible emulators like Konsole) 49 | * Windows console 50 | * rxvt (to a lesser extent) 51 | * Linux console 52 | * My terminal emulator family of applications https://github.com/adamdruppe/terminal-emulator 53 | ) 54 | 55 | Anything else is cool if it does work, but I don't want to go out of my way for it. 56 | 57 | * Use other libraries, unless strictly optional. terminal.d is a stand-alone module by default and 58 | always will be. 59 | 60 | * Do a full TUI widget set. I might do some basics and lay a little groundwork, but a full TUI 61 | is outside the scope of this module (unless I can do it really small.) 62 | ) 63 | +/ 64 | module arsd.terminal; 65 | 66 | /++ 67 | This example will demonstrate the high-level getline interface. 68 | 69 | The user will be able to type a line and navigate around it with cursor keys and even the mouse on some systems, as well as perform editing as they expect (e.g. the backspace and delete keys work normally) until they press enter. Then, the final line will be returned to your program, which the example will simply print back to the user. 70 | +/ 71 | unittest { 72 | import arsd.terminal; 73 | 74 | void main() { 75 | auto terminal = Terminal(ConsoleOutputType.linear); 76 | string line = terminal.getline(); 77 | terminal.writeln("You wrote: ", line); 78 | } 79 | } 80 | 81 | /++ 82 | This example demonstrates color output, using [Terminal.color] 83 | and the output functions like [Terminal.writeln]. 84 | +/ 85 | unittest { 86 | import arsd.terminal; 87 | void main() { 88 | auto terminal = Terminal(ConsoleOutputType.linear); 89 | terminal.color(Color.green, Color.black); 90 | terminal.writeln("Hello world, in green on black!"); 91 | terminal.color(Color.DEFAULT, Color.DEFAULT); 92 | terminal.writeln("And back to normal."); 93 | } 94 | } 95 | 96 | /* 97 | Widgets: 98 | tab widget 99 | scrollback buffer 100 | partitioned canvas 101 | */ 102 | 103 | // FIXME: ctrl+d eof on stdin 104 | 105 | // FIXME: http://msdn.microsoft.com/en-us/library/windows/desktop/ms686016%28v=vs.85%29.aspx 106 | 107 | version(Posix) { 108 | enum SIGWINCH = 28; 109 | __gshared bool windowSizeChanged = false; 110 | __gshared bool interrupted = false; /// you might periodically check this in a long operation and abort if it is set. Remember it is volatile. It is also sent through the input event loop via RealTimeConsoleInput 111 | __gshared bool hangedUp = false; /// similar to interrupted. 112 | 113 | version(with_eventloop) 114 | struct SignalFired {} 115 | 116 | extern(C) 117 | void sizeSignalHandler(int sigNumber) nothrow { 118 | windowSizeChanged = true; 119 | version(with_eventloop) { 120 | import arsd.eventloop; 121 | try 122 | send(SignalFired()); 123 | catch(Exception) {} 124 | } 125 | } 126 | extern(C) 127 | void interruptSignalHandler(int sigNumber) nothrow { 128 | interrupted = true; 129 | version(with_eventloop) { 130 | import arsd.eventloop; 131 | try 132 | send(SignalFired()); 133 | catch(Exception) {} 134 | } 135 | } 136 | extern(C) 137 | void hangupSignalHandler(int sigNumber) nothrow { 138 | hangedUp = true; 139 | version(with_eventloop) { 140 | import arsd.eventloop; 141 | try 142 | send(SignalFired()); 143 | catch(Exception) {} 144 | } 145 | } 146 | 147 | } 148 | 149 | // parts of this were taken from Robik's ConsoleD 150 | // https://github.com/robik/ConsoleD/blob/master/consoled.d 151 | 152 | // Uncomment this line to get a main() to demonstrate this module's 153 | // capabilities. 154 | //version = Demo 155 | 156 | version(Windows) { 157 | import core.sys.windows.windows; 158 | import std.string : toStringz; 159 | private { 160 | enum RED_BIT = 4; 161 | enum GREEN_BIT = 2; 162 | enum BLUE_BIT = 1; 163 | } 164 | } 165 | 166 | version(Posix) { 167 | import core.sys.posix.termios; 168 | import core.sys.posix.unistd; 169 | import unix = core.sys.posix.unistd; 170 | import core.sys.posix.sys.types; 171 | import core.sys.posix.sys.time; 172 | import core.stdc.stdio; 173 | private { 174 | enum RED_BIT = 1; 175 | enum GREEN_BIT = 2; 176 | enum BLUE_BIT = 4; 177 | } 178 | 179 | version(linux) { 180 | extern(C) int ioctl(int, int, ...); 181 | enum int TIOCGWINSZ = 0x5413; 182 | } else version(OSX) { 183 | import core.stdc.config; 184 | extern(C) int ioctl(int, c_ulong, ...); 185 | enum TIOCGWINSZ = 1074295912; 186 | } else static assert(0, "confirm the value of tiocgwinsz"); 187 | 188 | struct winsize { 189 | ushort ws_row; 190 | ushort ws_col; 191 | ushort ws_xpixel; 192 | ushort ws_ypixel; 193 | } 194 | 195 | // I'm taking this from the minimal termcap from my Slackware box (which I use as my /etc/termcap) and just taking the most commonly used ones (for me anyway). 196 | 197 | // this way we'll have some definitions for 99% of typical PC cases even without any help from the local operating system 198 | 199 | enum string builtinTermcap = ` 200 | # Generic VT entry. 201 | vg|vt-generic|Generic VT entries:\ 202 | :bs:mi:ms:pt:xn:xo:it#8:\ 203 | :RA=\E[?7l:SA=\E?7h:\ 204 | :bl=^G:cr=^M:ta=^I:\ 205 | :cm=\E[%i%d;%dH:\ 206 | :le=^H:up=\E[A:do=\E[B:nd=\E[C:\ 207 | :LE=\E[%dD:RI=\E[%dC:UP=\E[%dA:DO=\E[%dB:\ 208 | :ho=\E[H:cl=\E[H\E[2J:ce=\E[K:cb=\E[1K:cd=\E[J:sf=\ED:sr=\EM:\ 209 | :ct=\E[3g:st=\EH:\ 210 | :cs=\E[%i%d;%dr:sc=\E7:rc=\E8:\ 211 | :ei=\E[4l:ic=\E[@:IC=\E[%d@:al=\E[L:AL=\E[%dL:\ 212 | :dc=\E[P:DC=\E[%dP:dl=\E[M:DL=\E[%dM:\ 213 | :so=\E[7m:se=\E[m:us=\E[4m:ue=\E[m:\ 214 | :mb=\E[5m:mh=\E[2m:md=\E[1m:mr=\E[7m:me=\E[m:\ 215 | :sc=\E7:rc=\E8:kb=\177:\ 216 | :ku=\E[A:kd=\E[B:kr=\E[C:kl=\E[D: 217 | 218 | 219 | # Slackware 3.1 linux termcap entry (Sat Apr 27 23:03:58 CDT 1996): 220 | lx|linux|console|con80x25|LINUX System Console:\ 221 | :do=^J:co#80:li#25:cl=\E[H\E[J:sf=\ED:sb=\EM:\ 222 | :le=^H:bs:am:cm=\E[%i%d;%dH:nd=\E[C:up=\E[A:\ 223 | :ce=\E[K:cd=\E[J:so=\E[7m:se=\E[27m:us=\E[36m:ue=\E[m:\ 224 | :md=\E[1m:mr=\E[7m:mb=\E[5m:me=\E[m:is=\E[1;25r\E[25;1H:\ 225 | :ll=\E[1;25r\E[25;1H:al=\E[L:dc=\E[P:dl=\E[M:\ 226 | :it#8:ku=\E[A:kd=\E[B:kr=\E[C:kl=\E[D:kb=^H:ti=\E[r\E[H:\ 227 | :ho=\E[H:kP=\E[5~:kN=\E[6~:kH=\E[4~:kh=\E[1~:kD=\E[3~:kI=\E[2~:\ 228 | :k1=\E[[A:k2=\E[[B:k3=\E[[C:k4=\E[[D:k5=\E[[E:k6=\E[17~:\ 229 | :F1=\E[23~:F2=\E[24~:\ 230 | :k7=\E[18~:k8=\E[19~:k9=\E[20~:k0=\E[21~:K1=\E[1~:K2=\E[5~:\ 231 | :K4=\E[4~:K5=\E[6~:\ 232 | :pt:sr=\EM:vt#3:xn:km:bl=^G:vi=\E[?25l:ve=\E[?25h:vs=\E[?25h:\ 233 | :sc=\E7:rc=\E8:cs=\E[%i%d;%dr:\ 234 | :r1=\Ec:r2=\Ec:r3=\Ec: 235 | 236 | # Some other, commonly used linux console entries. 237 | lx|con80x28:co#80:li#28:tc=linux: 238 | lx|con80x43:co#80:li#43:tc=linux: 239 | lx|con80x50:co#80:li#50:tc=linux: 240 | lx|con100x37:co#100:li#37:tc=linux: 241 | lx|con100x40:co#100:li#40:tc=linux: 242 | lx|con132x43:co#132:li#43:tc=linux: 243 | 244 | # vt102 - vt100 + insert line etc. VT102 does not have insert character. 245 | v2|vt102|DEC vt102 compatible:\ 246 | :co#80:li#24:\ 247 | :ic@:IC@:\ 248 | :is=\E[m\E[?1l\E>:\ 249 | :rs=\E[m\E[?1l\E>:\ 250 | :eA=\E)0:as=^N:ae=^O:ac=aaffggjjkkllmmnnooqqssttuuvvwwxx:\ 251 | :ks=:ke=:\ 252 | :k1=\EOP:k2=\EOQ:k3=\EOR:k4=\EOS:\ 253 | :tc=vt-generic: 254 | 255 | # vt100 - really vt102 without insert line, insert char etc. 256 | vt|vt100|DEC vt100 compatible:\ 257 | :im@:mi@:al@:dl@:ic@:dc@:AL@:DL@:IC@:DC@:\ 258 | :tc=vt102: 259 | 260 | 261 | # Entry for an xterm. Insert mode has been disabled. 262 | vs|xterm|xterm-color|xterm-256color|vs100|xterm terminal emulator (X Window System):\ 263 | :am:bs:mi@:km:co#80:li#55:\ 264 | :im@:ei@:\ 265 | :cl=\E[H\E[J:\ 266 | :ct=\E[3k:ue=\E[m:\ 267 | :is=\E[m\E[?1l\E>:\ 268 | :rs=\E[m\E[?1l\E>:\ 269 | :vi=\E[?25l:ve=\E[?25h:\ 270 | :eA=\E)0:as=^N:ae=^O:ac=aaffggjjkkllmmnnooqqssttuuvvwwxx:\ 271 | :kI=\E[2~:kD=\E[3~:kP=\E[5~:kN=\E[6~:\ 272 | :k1=\EOP:k2=\EOQ:k3=\EOR:k4=\EOS:k5=\E[15~:\ 273 | :k6=\E[17~:k7=\E[18~:k8=\E[19~:k9=\E[20~:k0=\E[21~:\ 274 | :F1=\E[23~:F2=\E[24~:\ 275 | :kh=\E[H:kH=\E[F:\ 276 | :ks=:ke=:\ 277 | :te=\E[2J\E[?47l\E8:ti=\E7\E[?47h:\ 278 | :tc=vt-generic: 279 | 280 | 281 | #rxvt, added by me 282 | rxvt|rxvt-unicode:\ 283 | :am:bs:mi@:km:co#80:li#55:\ 284 | :im@:ei@:\ 285 | :ct=\E[3k:ue=\E[m:\ 286 | :is=\E[m\E[?1l\E>:\ 287 | :rs=\E[m\E[?1l\E>:\ 288 | :vi=\E[?25l:\ 289 | :ve=\E[?25h:\ 290 | :eA=\E)0:as=^N:ae=^O:ac=aaffggjjkkllmmnnooqqssttuuvvwwxx:\ 291 | :kI=\E[2~:kD=\E[3~:kP=\E[5~:kN=\E[6~:\ 292 | :k1=\E[11~:k2=\E[12~:k3=\E[13~:k4=\E[14~:k5=\E[15~:\ 293 | :k6=\E[17~:k7=\E[18~:k8=\E[19~:k9=\E[20~:k0=\E[21~:\ 294 | :F1=\E[23~:F2=\E[24~:\ 295 | :kh=\E[7~:kH=\E[8~:\ 296 | :ks=:ke=:\ 297 | :te=\E[2J\E[?47l\E8:ti=\E7\E[?47h:\ 298 | :tc=vt-generic: 299 | 300 | 301 | # Some other entries for the same xterm. 302 | v2|xterms|vs100s|xterm small window:\ 303 | :co#80:li#24:tc=xterm: 304 | vb|xterm-bold|xterm with bold instead of underline:\ 305 | :us=\E[1m:tc=xterm: 306 | vi|xterm-ins|xterm with insert mode:\ 307 | :mi:im=\E[4h:ei=\E[4l:tc=xterm: 308 | 309 | Eterm|Eterm Terminal Emulator (X11 Window System):\ 310 | :am:bw:eo:km:mi:ms:xn:xo:\ 311 | :co#80:it#8:li#24:lm#0:pa#64:Co#8:AF=\E[3%dm:AB=\E[4%dm:op=\E[39m\E[49m:\ 312 | :AL=\E[%dL:DC=\E[%dP:DL=\E[%dM:DO=\E[%dB:IC=\E[%d@:\ 313 | :K1=\E[7~:K2=\EOu:K3=\E[5~:K4=\E[8~:K5=\E[6~:LE=\E[%dD:\ 314 | :RI=\E[%dC:UP=\E[%dA:ae=^O:al=\E[L:as=^N:bl=^G:cd=\E[J:\ 315 | :ce=\E[K:cl=\E[H\E[2J:cm=\E[%i%d;%dH:cr=^M:\ 316 | :cs=\E[%i%d;%dr:ct=\E[3g:dc=\E[P:dl=\E[M:do=\E[B:\ 317 | :ec=\E[%dX:ei=\E[4l:ho=\E[H:i1=\E[?47l\E>\E[?1l:ic=\E[@:\ 318 | :im=\E[4h:is=\E[r\E[m\E[2J\E[H\E[?7h\E[?1;3;4;6l\E[4l:\ 319 | :k1=\E[11~:k2=\E[12~:k3=\E[13~:k4=\E[14~:k5=\E[15~:\ 320 | :k6=\E[17~:k7=\E[18~:k8=\E[19~:k9=\E[20~:kD=\E[3~:\ 321 | :kI=\E[2~:kN=\E[6~:kP=\E[5~:kb=^H:kd=\E[B:ke=:kh=\E[7~:\ 322 | :kl=\E[D:kr=\E[C:ks=:ku=\E[A:le=^H:mb=\E[5m:md=\E[1m:\ 323 | :me=\E[m\017:mr=\E[7m:nd=\E[C:rc=\E8:\ 324 | :sc=\E7:se=\E[27m:sf=^J:so=\E[7m:sr=\EM:st=\EH:ta=^I:\ 325 | :te=\E[2J\E[?47l\E8:ti=\E7\E[?47h:ue=\E[24m:up=\E[A:\ 326 | :us=\E[4m:vb=\E[?5h\E[?5l:ve=\E[?25h:vi=\E[?25l:\ 327 | :ac=aaffggiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~: 328 | 329 | # DOS terminal emulator such as Telix or TeleMate. 330 | # This probably also works for the SCO console, though it's incomplete. 331 | an|ansi|ansi-bbs|ANSI terminals (emulators):\ 332 | :co#80:li#24:am:\ 333 | :is=:rs=\Ec:kb=^H:\ 334 | :as=\E[m:ae=:eA=:\ 335 | :ac=0\333+\257,\256.\031-\030a\261f\370g\361j\331k\277l\332m\300n\305q\304t\264u\303v\301w\302x\263~\025:\ 336 | :kD=\177:kH=\E[Y:kN=\E[U:kP=\E[V:kh=\E[H:\ 337 | :k1=\EOP:k2=\EOQ:k3=\EOR:k4=\EOS:k5=\EOT:\ 338 | :k6=\EOU:k7=\EOV:k8=\EOW:k9=\EOX:k0=\EOY:\ 339 | :tc=vt-generic: 340 | 341 | `; 342 | } 343 | 344 | enum Bright = 0x08; 345 | 346 | /// Defines the list of standard colors understood by Terminal. 347 | enum Color : ushort { 348 | black = 0, /// . 349 | red = RED_BIT, /// . 350 | green = GREEN_BIT, /// . 351 | yellow = red | green, /// . 352 | blue = BLUE_BIT, /// . 353 | magenta = red | blue, /// . 354 | cyan = blue | green, /// . 355 | white = red | green | blue, /// . 356 | DEFAULT = 256, 357 | } 358 | 359 | /// When capturing input, what events are you interested in? 360 | /// 361 | /// Note: these flags can be OR'd together to select more than one option at a time. 362 | /// 363 | /// Ctrl+C and other keyboard input is always captured, though it may be line buffered if you don't use raw. 364 | /// The rationale for that is to ensure the Terminal destructor has a chance to run, since the terminal is a shared resource and should be put back before the program terminates. 365 | enum ConsoleInputFlags { 366 | raw = 0, /// raw input returns keystrokes immediately, without line buffering 367 | echo = 1, /// do you want to automatically echo input back to the user? 368 | mouse = 2, /// capture mouse events 369 | paste = 4, /// capture paste events (note: without this, paste can come through as keystrokes) 370 | size = 8, /// window resize events 371 | 372 | releasedKeys = 64, /// key release events. Not reliable on Posix. 373 | 374 | allInputEvents = 8|4|2, /// subscribe to all input events. Note: in previous versions, this also returned release events. It no longer does, use allInputEventsWithRelease if you want them. 375 | allInputEventsWithRelease = allInputEvents|releasedKeys, /// subscribe to all input events, including (unreliable on Posix) key release events. 376 | } 377 | 378 | /// Defines how terminal output should be handled. 379 | enum ConsoleOutputType { 380 | linear = 0, /// do you want output to work one line at a time? 381 | cellular = 1, /// or do you want access to the terminal screen as a grid of characters? 382 | //truncatedCellular = 3, /// cellular, but instead of wrapping output to the next line automatically, it will truncate at the edges 383 | 384 | minimalProcessing = 255, /// do the least possible work, skips most construction and desturction tasks. Only use if you know what you're doing here 385 | } 386 | 387 | /// Some methods will try not to send unnecessary commands to the screen. You can override their judgement using a ForceOption parameter, if present 388 | enum ForceOption { 389 | automatic = 0, /// automatically decide what to do (best, unless you know for sure it isn't right) 390 | neverSend = -1, /// never send the data. This will only update Terminal's internal state. Use with caution. 391 | alwaysSend = 1, /// always send the data, even if it doesn't seem necessary 392 | } 393 | 394 | // we could do it with termcap too, getenv("TERMCAP") then split on : and replace \E with \033 and get the pieces 395 | 396 | /// Encapsulates the I/O capabilities of a terminal. 397 | /// 398 | /// Warning: do not write out escape sequences to the terminal. This won't work 399 | /// on Windows and will confuse Terminal's internal state on Posix. 400 | struct Terminal { 401 | /// 402 | @disable this(); 403 | @disable this(this); 404 | private ConsoleOutputType type; 405 | 406 | version(Posix) { 407 | private int fdOut; 408 | private int fdIn; 409 | private int[] delegate() getSizeOverride; 410 | void delegate(in void[]) _writeDelegate; // used to override the unix write() system call, set it magically 411 | } 412 | 413 | version(Posix) { 414 | bool terminalInFamily(string[] terms...) { 415 | import std.process; 416 | import std.string; 417 | auto term = environment.get("TERM"); 418 | foreach(t; terms) 419 | if(indexOf(term, t) != -1) 420 | return true; 421 | 422 | return false; 423 | } 424 | 425 | // This is a filthy hack because Terminal.app and OS X are garbage who don't 426 | // work the way they're advertised. I just have to best-guess hack and hope it 427 | // doesn't break anything else. (If you know a better way, let me know!) 428 | bool isMacTerminal() { 429 | import std.process; 430 | import std.string; 431 | auto term = environment.get("TERM"); 432 | return term == "xterm-256color"; 433 | } 434 | 435 | static string[string] termcapDatabase; 436 | static void readTermcapFile(bool useBuiltinTermcap = false) { 437 | import std.file; 438 | import std.stdio; 439 | import std.string; 440 | 441 | if(!exists("/etc/termcap")) 442 | useBuiltinTermcap = true; 443 | 444 | string current; 445 | 446 | void commitCurrentEntry() { 447 | if(current is null) 448 | return; 449 | 450 | string names = current; 451 | auto idx = indexOf(names, ":"); 452 | if(idx != -1) 453 | names = names[0 .. idx]; 454 | 455 | foreach(name; split(names, "|")) 456 | termcapDatabase[name] = current; 457 | 458 | current = null; 459 | } 460 | 461 | void handleTermcapLine(in char[] line) { 462 | if(line.length == 0) { // blank 463 | commitCurrentEntry(); 464 | return; // continue 465 | } 466 | if(line[0] == '#') // comment 467 | return; // continue 468 | size_t termination = line.length; 469 | if(line[$-1] == '\\') 470 | termination--; // cut off the \\ 471 | current ~= strip(line[0 .. termination]); 472 | // termcap entries must be on one logical line, so if it isn't continued, we know we're done 473 | if(line[$-1] != '\\') 474 | commitCurrentEntry(); 475 | } 476 | 477 | if(useBuiltinTermcap) { 478 | foreach(line; splitLines(builtinTermcap)) { 479 | handleTermcapLine(line); 480 | } 481 | } else { 482 | foreach(line; File("/etc/termcap").byLine()) { 483 | handleTermcapLine(line); 484 | } 485 | } 486 | } 487 | 488 | static string getTermcapDatabase(string terminal) { 489 | import std.string; 490 | 491 | if(termcapDatabase is null) 492 | readTermcapFile(); 493 | 494 | auto data = terminal in termcapDatabase; 495 | if(data is null) 496 | return null; 497 | 498 | auto tc = *data; 499 | auto more = indexOf(tc, ":tc="); 500 | if(more != -1) { 501 | auto tcKey = tc[more + ":tc=".length .. $]; 502 | auto end = indexOf(tcKey, ":"); 503 | if(end != -1) 504 | tcKey = tcKey[0 .. end]; 505 | tc = getTermcapDatabase(tcKey) ~ tc; 506 | } 507 | 508 | return tc; 509 | } 510 | 511 | string[string] termcap; 512 | void readTermcap() { 513 | import std.process; 514 | import std.string; 515 | import std.array; 516 | 517 | string termcapData = environment.get("TERMCAP"); 518 | if(termcapData.length == 0) { 519 | termcapData = getTermcapDatabase(environment.get("TERM")); 520 | } 521 | 522 | auto e = replace(termcapData, "\\\n", "\n"); 523 | termcap = null; 524 | 525 | foreach(part; split(e, ":")) { 526 | // FIXME: handle numeric things too 527 | 528 | auto things = split(part, "="); 529 | if(things.length) 530 | termcap[things[0]] = 531 | things.length > 1 ? things[1] : null; 532 | } 533 | } 534 | 535 | string findSequenceInTermcap(in char[] sequenceIn) { 536 | char[10] sequenceBuffer; 537 | char[] sequence; 538 | if(sequenceIn.length > 0 && sequenceIn[0] == '\033') { 539 | if(!(sequenceIn.length < sequenceBuffer.length - 1)) 540 | return null; 541 | sequenceBuffer[1 .. sequenceIn.length + 1] = sequenceIn[]; 542 | sequenceBuffer[0] = '\\'; 543 | sequenceBuffer[1] = 'E'; 544 | sequence = sequenceBuffer[0 .. sequenceIn.length + 1]; 545 | } else { 546 | sequence = sequenceBuffer[1 .. sequenceIn.length + 1]; 547 | } 548 | 549 | import std.array; 550 | foreach(k, v; termcap) 551 | if(v == sequence) 552 | return k; 553 | return null; 554 | } 555 | 556 | string getTermcap(string key) { 557 | auto k = key in termcap; 558 | if(k !is null) return *k; 559 | return null; 560 | } 561 | 562 | // Looks up a termcap item and tries to execute it. Returns false on failure 563 | bool doTermcap(T...)(string key, T t) { 564 | import std.conv; 565 | auto fs = getTermcap(key); 566 | if(fs is null) 567 | return false; 568 | 569 | int swapNextTwo = 0; 570 | 571 | R getArg(R)(int idx) { 572 | if(swapNextTwo == 2) { 573 | idx ++; 574 | swapNextTwo--; 575 | } else if(swapNextTwo == 1) { 576 | idx --; 577 | swapNextTwo--; 578 | } 579 | 580 | foreach(i, arg; t) { 581 | if(i == idx) 582 | return to!R(arg); 583 | } 584 | assert(0, to!string(idx) ~ " is out of bounds working " ~ fs); 585 | } 586 | 587 | char[256] buffer; 588 | int bufferPos = 0; 589 | 590 | void addChar(char c) { 591 | import std.exception; 592 | enforce(bufferPos < buffer.length); 593 | buffer[bufferPos++] = c; 594 | } 595 | 596 | void addString(in char[] c) { 597 | import std.exception; 598 | enforce(bufferPos + c.length < buffer.length); 599 | buffer[bufferPos .. bufferPos + c.length] = c[]; 600 | bufferPos += c.length; 601 | } 602 | 603 | void addInt(int c, int minSize) { 604 | import std.string; 605 | auto str = format("%0"~(minSize ? to!string(minSize) : "")~"d", c); 606 | addString(str); 607 | } 608 | 609 | bool inPercent; 610 | int argPosition = 0; 611 | int incrementParams = 0; 612 | bool skipNext; 613 | bool nextIsChar; 614 | bool inBackslash; 615 | 616 | foreach(char c; fs) { 617 | if(inBackslash) { 618 | if(c == 'E') 619 | addChar('\033'); 620 | else 621 | addChar(c); 622 | inBackslash = false; 623 | } else if(nextIsChar) { 624 | if(skipNext) 625 | skipNext = false; 626 | else 627 | addChar(cast(char) (c + getArg!int(argPosition) + (incrementParams ? 1 : 0))); 628 | if(incrementParams) incrementParams--; 629 | argPosition++; 630 | inPercent = false; 631 | } else if(inPercent) { 632 | switch(c) { 633 | case '%': 634 | addChar('%'); 635 | inPercent = false; 636 | break; 637 | case '2': 638 | case '3': 639 | case 'd': 640 | if(skipNext) 641 | skipNext = false; 642 | else 643 | addInt(getArg!int(argPosition) + (incrementParams ? 1 : 0), 644 | c == 'd' ? 0 : (c - '0') 645 | ); 646 | if(incrementParams) incrementParams--; 647 | argPosition++; 648 | inPercent = false; 649 | break; 650 | case '.': 651 | if(skipNext) 652 | skipNext = false; 653 | else 654 | addChar(cast(char) (getArg!int(argPosition) + (incrementParams ? 1 : 0))); 655 | if(incrementParams) incrementParams--; 656 | argPosition++; 657 | break; 658 | case '+': 659 | nextIsChar = true; 660 | inPercent = false; 661 | break; 662 | case 'i': 663 | incrementParams = 2; 664 | inPercent = false; 665 | break; 666 | case 's': 667 | skipNext = true; 668 | inPercent = false; 669 | break; 670 | case 'b': 671 | argPosition--; 672 | inPercent = false; 673 | break; 674 | case 'r': 675 | swapNextTwo = 2; 676 | inPercent = false; 677 | break; 678 | // FIXME: there's more 679 | // http://www.gnu.org/software/termutils/manual/termcap-1.3/html_mono/termcap.html 680 | 681 | default: 682 | assert(0, "not supported " ~ c); 683 | } 684 | } else { 685 | if(c == '%') 686 | inPercent = true; 687 | else if(c == '\\') 688 | inBackslash = true; 689 | else 690 | addChar(c); 691 | } 692 | } 693 | 694 | writeStringRaw(buffer[0 .. bufferPos]); 695 | return true; 696 | } 697 | } 698 | 699 | version(Posix) 700 | /** 701 | * Constructs an instance of Terminal representing the capabilities of 702 | * the current terminal. 703 | * 704 | * While it is possible to override the stdin+stdout file descriptors, remember 705 | * that is not portable across platforms and be sure you know what you're doing. 706 | * 707 | * ditto on getSizeOverride. That's there so you can do something instead of ioctl. 708 | */ 709 | this(ConsoleOutputType type, int fdIn = 0, int fdOut = 1, int[] delegate() getSizeOverride = null) { 710 | this.fdIn = fdIn; 711 | this.fdOut = fdOut; 712 | this.getSizeOverride = getSizeOverride; 713 | this.type = type; 714 | 715 | readTermcap(); 716 | 717 | if(type == ConsoleOutputType.minimalProcessing) { 718 | _suppressDestruction = true; 719 | return; 720 | } 721 | 722 | if(type == ConsoleOutputType.cellular) { 723 | doTermcap("ti"); 724 | clear(); 725 | moveTo(0, 0, ForceOption.alwaysSend); // we need to know where the cursor is for some features to work, and moving it is easier than querying it 726 | } 727 | 728 | if(terminalInFamily("xterm", "rxvt", "screen")) { 729 | writeStringRaw("\033[22;0t"); // save window title on a stack (support seems spotty, but it doesn't hurt to have it) 730 | } 731 | } 732 | 733 | version(Windows) { 734 | HANDLE hConsole; 735 | CONSOLE_SCREEN_BUFFER_INFO originalSbi; 736 | } 737 | 738 | version(Windows) 739 | /// ditto 740 | this(ConsoleOutputType type) { 741 | if(type == ConsoleOutputType.cellular) { 742 | hConsole = CreateConsoleScreenBuffer(GENERIC_READ | GENERIC_WRITE, 0, null, CONSOLE_TEXTMODE_BUFFER, null); 743 | if(hConsole == INVALID_HANDLE_VALUE) { 744 | import std.conv; 745 | throw new Exception(to!string(GetLastError())); 746 | } 747 | 748 | SetConsoleActiveScreenBuffer(hConsole); 749 | /* 750 | http://msdn.microsoft.com/en-us/library/windows/desktop/ms686125%28v=vs.85%29.aspx 751 | http://msdn.microsoft.com/en-us/library/windows/desktop/ms683193%28v=vs.85%29.aspx 752 | */ 753 | COORD size; 754 | /* 755 | CONSOLE_SCREEN_BUFFER_INFO sbi; 756 | GetConsoleScreenBufferInfo(hConsole, &sbi); 757 | size.X = cast(short) GetSystemMetrics(SM_CXMIN); 758 | size.Y = cast(short) GetSystemMetrics(SM_CYMIN); 759 | */ 760 | 761 | // FIXME: this sucks, maybe i should just revert it. but there shouldn't be scrollbars in cellular mode 762 | //size.X = 80; 763 | //size.Y = 24; 764 | //SetConsoleScreenBufferSize(hConsole, size); 765 | 766 | clear(); 767 | } else { 768 | hConsole = GetStdHandle(STD_OUTPUT_HANDLE); 769 | } 770 | 771 | GetConsoleScreenBufferInfo(hConsole, &originalSbi); 772 | } 773 | 774 | // only use this if you are sure you know what you want, since the terminal is a shared resource you generally really want to reset it to normal when you leave... 775 | bool _suppressDestruction; 776 | 777 | version(Posix) 778 | ~this() { 779 | if(_suppressDestruction) { 780 | flush(); 781 | return; 782 | } 783 | if(type == ConsoleOutputType.cellular) { 784 | doTermcap("te"); 785 | } 786 | if(terminalInFamily("xterm", "rxvt", "screen")) { 787 | writeStringRaw("\033[23;0t"); // restore window title from the stack 788 | } 789 | showCursor(); 790 | reset(); 791 | flush(); 792 | 793 | if(lineGetter !is null) 794 | lineGetter.dispose(); 795 | } 796 | 797 | version(Windows) 798 | ~this() { 799 | flush(); // make sure user data is all flushed before resetting 800 | reset(); 801 | showCursor(); 802 | 803 | if(lineGetter !is null) 804 | lineGetter.dispose(); 805 | 806 | auto stdo = GetStdHandle(STD_OUTPUT_HANDLE); 807 | SetConsoleActiveScreenBuffer(stdo); 808 | if(hConsole !is stdo) 809 | CloseHandle(hConsole); 810 | } 811 | 812 | // lazily initialized and preserved between calls to getline for a bit of efficiency (only a bit) 813 | // and some history storage. 814 | LineGetter lineGetter; 815 | 816 | int _currentForeground = Color.DEFAULT; 817 | int _currentBackground = Color.DEFAULT; 818 | RGB _currentForegroundRGB; 819 | RGB _currentBackgroundRGB; 820 | bool reverseVideo = false; 821 | 822 | /++ 823 | Attempts to set color according to a 24 bit value (r, g, b, each >= 0 and < 256). 824 | 825 | 826 | This is not supported on all terminals. It will attempt to fall back to a 256-color 827 | or 8-color palette in those cases automatically. 828 | 829 | Returns: true if it believes it was successful (note that it cannot be completely sure), 830 | false if it had to use a fallback. 831 | +/ 832 | bool setTrueColor(RGB foreground, RGB background, ForceOption force = ForceOption.automatic) { 833 | if(force == ForceOption.neverSend) { 834 | _currentForeground = -1; 835 | _currentBackground = -1; 836 | _currentForegroundRGB = foreground; 837 | _currentBackgroundRGB = background; 838 | return true; 839 | } 840 | 841 | if(force == ForceOption.automatic && _currentForeground == -1 && _currentBackground == -1 && (_currentForegroundRGB == foreground && _currentBackgroundRGB == background)) 842 | return true; 843 | 844 | _currentForeground = -1; 845 | _currentBackground = -1; 846 | _currentForegroundRGB = foreground; 847 | _currentBackgroundRGB = background; 848 | 849 | version(Windows) { 850 | flush(); 851 | ushort setTob = cast(ushort) approximate16Color(background); 852 | ushort setTof = cast(ushort) approximate16Color(foreground); 853 | SetConsoleTextAttribute( 854 | hConsole, 855 | cast(ushort)((setTob << 4) | setTof)); 856 | return false; 857 | } else { 858 | // FIXME: if the terminal reliably does support 24 bit color, use it 859 | // instead of the round off. But idk how to detect that yet... 860 | 861 | // fallback to 16 color for term that i know don't take it well 862 | import std.process; 863 | import std.string; 864 | if(environment.get("TERM") == "rxvt" || environment.get("TERM") == "linux") { 865 | // not likely supported, use 16 color fallback 866 | auto setTof = approximate16Color(foreground); 867 | auto setTob = approximate16Color(background); 868 | 869 | writeStringRaw(format("\033[%dm\033[3%dm\033[4%dm", 870 | (setTof & Bright) ? 1 : 0, 871 | cast(int) (setTof & ~Bright), 872 | cast(int) (setTob & ~Bright) 873 | )); 874 | 875 | return false; 876 | } 877 | 878 | // otherwise, assume it is probably supported and give it a try 879 | writeStringRaw(format("\033[38;5;%dm\033[48;5;%dm", 880 | colorToXTermPaletteIndex(foreground), 881 | colorToXTermPaletteIndex(background) 882 | )); 883 | 884 | return true; 885 | } 886 | } 887 | 888 | /// Changes the current color. See enum Color for the values. 889 | void color(int foreground, int background, ForceOption force = ForceOption.automatic, bool reverseVideo = false) { 890 | if(force != ForceOption.neverSend) { 891 | version(Windows) { 892 | // assuming a dark background on windows, so LowContrast == dark which means the bit is NOT set on hardware 893 | /* 894 | foreground ^= LowContrast; 895 | background ^= LowContrast; 896 | */ 897 | 898 | ushort setTof = cast(ushort) foreground; 899 | ushort setTob = cast(ushort) background; 900 | 901 | // this isn't necessarily right but meh 902 | if(background == Color.DEFAULT) 903 | setTob = Color.black; 904 | if(foreground == Color.DEFAULT) 905 | setTof = Color.white; 906 | 907 | if(force == ForceOption.alwaysSend || reverseVideo != this.reverseVideo || foreground != _currentForeground || background != _currentBackground) { 908 | flush(); // if we don't do this now, the buffering can screw up the colors... 909 | if(reverseVideo) { 910 | if(background == Color.DEFAULT) 911 | setTof = Color.black; 912 | else 913 | setTof = cast(ushort) background | (foreground & Bright); 914 | 915 | if(background == Color.DEFAULT) 916 | setTob = Color.white; 917 | else 918 | setTob = cast(ushort) (foreground & ~Bright); 919 | } 920 | SetConsoleTextAttribute( 921 | hConsole, 922 | cast(ushort)((setTob << 4) | setTof)); 923 | } 924 | } else { 925 | import std.process; 926 | // I started using this envvar for my text editor, but now use it elsewhere too 927 | // if we aren't set to dark, assume light 928 | /* 929 | if(getenv("ELVISBG") == "dark") { 930 | // LowContrast on dark bg menas 931 | } else { 932 | foreground ^= LowContrast; 933 | background ^= LowContrast; 934 | } 935 | */ 936 | 937 | ushort setTof = cast(ushort) foreground & ~Bright; 938 | ushort setTob = cast(ushort) background & ~Bright; 939 | 940 | if(foreground & Color.DEFAULT) 941 | setTof = 9; // ansi sequence for reset 942 | if(background == Color.DEFAULT) 943 | setTob = 9; 944 | 945 | import std.string; 946 | 947 | if(force == ForceOption.alwaysSend || reverseVideo != this.reverseVideo || foreground != _currentForeground || background != _currentBackground) { 948 | writeStringRaw(format("\033[%dm\033[3%dm\033[4%dm\033[%dm", 949 | (foreground != Color.DEFAULT && (foreground & Bright)) ? 1 : 0, 950 | cast(int) setTof, 951 | cast(int) setTob, 952 | reverseVideo ? 7 : 27 953 | )); 954 | } 955 | } 956 | } 957 | 958 | _currentForeground = foreground; 959 | _currentBackground = background; 960 | this.reverseVideo = reverseVideo; 961 | } 962 | 963 | private bool _underlined = false; 964 | 965 | /// Note: the Windows console does not support underlining 966 | void underline(bool set, ForceOption force = ForceOption.automatic) { 967 | if(set == _underlined && force != ForceOption.alwaysSend) 968 | return; 969 | version(Posix) { 970 | if(set) 971 | writeStringRaw("\033[4m"); 972 | else 973 | writeStringRaw("\033[24m"); 974 | } 975 | _underlined = set; 976 | } 977 | // FIXME: do I want to do bold and italic? 978 | 979 | /// Returns the terminal to normal output colors 980 | void reset() { 981 | version(Windows) 982 | SetConsoleTextAttribute( 983 | hConsole, 984 | originalSbi.wAttributes); 985 | else 986 | writeStringRaw("\033[0m"); 987 | 988 | _underlined = false; 989 | _currentForeground = Color.DEFAULT; 990 | _currentBackground = Color.DEFAULT; 991 | reverseVideo = false; 992 | } 993 | 994 | // FIXME: add moveRelative 995 | 996 | /// The current x position of the output cursor. 0 == leftmost column 997 | @property int cursorX() { 998 | return _cursorX; 999 | } 1000 | 1001 | /// The current y position of the output cursor. 0 == topmost row 1002 | @property int cursorY() { 1003 | return _cursorY; 1004 | } 1005 | 1006 | private int _cursorX; 1007 | private int _cursorY; 1008 | 1009 | /// Moves the output cursor to the given position. (0, 0) is the upper left corner of the screen. The force parameter can be used to force an update, even if Terminal doesn't think it is necessary 1010 | void moveTo(int x, int y, ForceOption force = ForceOption.automatic) { 1011 | if(force != ForceOption.neverSend && (force == ForceOption.alwaysSend || x != _cursorX || y != _cursorY)) { 1012 | executeAutoHideCursor(); 1013 | version(Posix) { 1014 | doTermcap("cm", y, x); 1015 | } else version(Windows) { 1016 | 1017 | flush(); // if we don't do this now, the buffering can screw up the position 1018 | COORD coord = {cast(short) x, cast(short) y}; 1019 | SetConsoleCursorPosition(hConsole, coord); 1020 | } else static assert(0); 1021 | } 1022 | 1023 | _cursorX = x; 1024 | _cursorY = y; 1025 | } 1026 | 1027 | /// shows the cursor 1028 | void showCursor() { 1029 | version(Posix) 1030 | doTermcap("ve"); 1031 | else { 1032 | CONSOLE_CURSOR_INFO info; 1033 | GetConsoleCursorInfo(hConsole, &info); 1034 | info.bVisible = true; 1035 | SetConsoleCursorInfo(hConsole, &info); 1036 | } 1037 | } 1038 | 1039 | /// hides the cursor 1040 | void hideCursor() { 1041 | version(Posix) { 1042 | doTermcap("vi"); 1043 | } else { 1044 | CONSOLE_CURSOR_INFO info; 1045 | GetConsoleCursorInfo(hConsole, &info); 1046 | info.bVisible = false; 1047 | SetConsoleCursorInfo(hConsole, &info); 1048 | } 1049 | 1050 | } 1051 | 1052 | private bool autoHidingCursor; 1053 | private bool autoHiddenCursor; 1054 | // explicitly not publicly documented 1055 | // Sets the cursor to automatically insert a hide command at the front of the output buffer iff it is moved. 1056 | // Call autoShowCursor when you are done with the batch update. 1057 | void autoHideCursor() { 1058 | autoHidingCursor = true; 1059 | } 1060 | 1061 | private void executeAutoHideCursor() { 1062 | if(autoHidingCursor) { 1063 | version(Windows) 1064 | hideCursor(); 1065 | else version(Posix) { 1066 | // prepend the hide cursor command so it is the first thing flushed 1067 | writeBuffer = "\033[?25l" ~ writeBuffer; 1068 | } 1069 | 1070 | autoHiddenCursor = true; 1071 | autoHidingCursor = false; // already been done, don't insert the command again 1072 | } 1073 | } 1074 | 1075 | // explicitly not publicly documented 1076 | // Shows the cursor if it was automatically hidden by autoHideCursor and resets the internal auto hide state. 1077 | void autoShowCursor() { 1078 | if(autoHiddenCursor) 1079 | showCursor(); 1080 | 1081 | autoHidingCursor = false; 1082 | autoHiddenCursor = false; 1083 | } 1084 | 1085 | /* 1086 | // alas this doesn't work due to a bunch of delegate context pointer and postblit problems 1087 | // instead of using: auto input = terminal.captureInput(flags) 1088 | // use: auto input = RealTimeConsoleInput(&terminal, flags); 1089 | /// Gets real time input, disabling line buffering 1090 | RealTimeConsoleInput captureInput(ConsoleInputFlags flags) { 1091 | return RealTimeConsoleInput(&this, flags); 1092 | } 1093 | */ 1094 | 1095 | /// Changes the terminal's title 1096 | void setTitle(string t) { 1097 | version(Windows) { 1098 | SetConsoleTitleA(toStringz(t)); 1099 | } else { 1100 | import std.string; 1101 | if(terminalInFamily("xterm", "rxvt", "screen")) 1102 | writeStringRaw(format("\033]0;%s\007", t)); 1103 | } 1104 | } 1105 | 1106 | /// Flushes your updates to the terminal. 1107 | /// It is important to call this when you are finished writing for now if you are using the version=with_eventloop 1108 | void flush() { 1109 | if(writeBuffer.length == 0) 1110 | return; 1111 | 1112 | version(Posix) { 1113 | if(_writeDelegate !is null) { 1114 | _writeDelegate(writeBuffer); 1115 | } else { 1116 | ssize_t written; 1117 | 1118 | while(writeBuffer.length) { 1119 | written = unix.write(this.fdOut, writeBuffer.ptr, writeBuffer.length); 1120 | if(written < 0) 1121 | throw new Exception("write failed for some reason"); 1122 | writeBuffer = writeBuffer[written .. $]; 1123 | } 1124 | } 1125 | } else version(Windows) { 1126 | import std.conv; 1127 | // FIXME: I'm not sure I'm actually happy with this allocation but 1128 | // it probably isn't a big deal. At least it has unicode support now. 1129 | wstring writeBufferw = to!wstring(writeBuffer); 1130 | while(writeBufferw.length) { 1131 | DWORD written; 1132 | WriteConsoleW(hConsole, writeBufferw.ptr, cast(DWORD)writeBufferw.length, &written, null); 1133 | writeBufferw = writeBufferw[written .. $]; 1134 | } 1135 | 1136 | writeBuffer = null; 1137 | } 1138 | } 1139 | 1140 | int[] getSize() { 1141 | version(Windows) { 1142 | CONSOLE_SCREEN_BUFFER_INFO info; 1143 | GetConsoleScreenBufferInfo( hConsole, &info ); 1144 | 1145 | int cols, rows; 1146 | 1147 | cols = (info.srWindow.Right - info.srWindow.Left + 1); 1148 | rows = (info.srWindow.Bottom - info.srWindow.Top + 1); 1149 | 1150 | return [cols, rows]; 1151 | } else { 1152 | if(getSizeOverride is null) { 1153 | winsize w; 1154 | ioctl(0, TIOCGWINSZ, &w); 1155 | return [w.ws_col, w.ws_row]; 1156 | } else return getSizeOverride(); 1157 | } 1158 | } 1159 | 1160 | void updateSize() { 1161 | auto size = getSize(); 1162 | _width = size[0]; 1163 | _height = size[1]; 1164 | } 1165 | 1166 | private int _width; 1167 | private int _height; 1168 | 1169 | /// The current width of the terminal (the number of columns) 1170 | @property int width() { 1171 | if(_width == 0 || _height == 0) 1172 | updateSize(); 1173 | return _width; 1174 | } 1175 | 1176 | /// The current height of the terminal (the number of rows) 1177 | @property int height() { 1178 | if(_width == 0 || _height == 0) 1179 | updateSize(); 1180 | return _height; 1181 | } 1182 | 1183 | /* 1184 | void write(T...)(T t) { 1185 | foreach(arg; t) { 1186 | writeStringRaw(to!string(arg)); 1187 | } 1188 | } 1189 | */ 1190 | 1191 | /// Writes to the terminal at the current cursor position. 1192 | void writef(T...)(string f, T t) { 1193 | import std.string; 1194 | writePrintableString(format(f, t)); 1195 | } 1196 | 1197 | /// ditto 1198 | void writefln(T...)(string f, T t) { 1199 | writef(f ~ "\n", t); 1200 | } 1201 | 1202 | /// ditto 1203 | void write(T...)(T t) { 1204 | import std.conv; 1205 | string data; 1206 | foreach(arg; t) { 1207 | data ~= to!string(arg); 1208 | } 1209 | 1210 | writePrintableString(data); 1211 | } 1212 | 1213 | /// ditto 1214 | void writeln(T...)(T t) { 1215 | write(t, "\n"); 1216 | } 1217 | 1218 | /+ 1219 | /// A combined moveTo and writef that puts the cursor back where it was before when it finishes the write. 1220 | /// Only works in cellular mode. 1221 | /// Might give better performance than moveTo/writef because if the data to write matches the internal buffer, it skips sending anything (to override the buffer check, you can use moveTo and writePrintableString with ForceOption.alwaysSend) 1222 | void writefAt(T...)(int x, int y, string f, T t) { 1223 | import std.string; 1224 | auto toWrite = format(f, t); 1225 | 1226 | auto oldX = _cursorX; 1227 | auto oldY = _cursorY; 1228 | 1229 | writeAtWithoutReturn(x, y, toWrite); 1230 | 1231 | moveTo(oldX, oldY); 1232 | } 1233 | 1234 | void writeAtWithoutReturn(int x, int y, in char[] data) { 1235 | moveTo(x, y); 1236 | writeStringRaw(toWrite, ForceOption.alwaysSend); 1237 | } 1238 | +/ 1239 | 1240 | void writePrintableString(in char[] s, ForceOption force = ForceOption.automatic) { 1241 | // an escape character is going to mess things up. Actually any non-printable character could, but meh 1242 | // assert(s.indexOf("\033") == -1); 1243 | 1244 | // tracking cursor position 1245 | foreach(ch; s) { 1246 | switch(ch) { 1247 | case '\n': 1248 | _cursorX = 0; 1249 | _cursorY++; 1250 | break; 1251 | case '\r': 1252 | _cursorX = 0; 1253 | break; 1254 | case '\t': 1255 | _cursorX ++; 1256 | _cursorX += _cursorX % 8; // FIXME: get the actual tabstop, if possible 1257 | break; 1258 | default: 1259 | if(ch <= 127) // way of only advancing once per dchar instead of per code unit 1260 | _cursorX++; 1261 | } 1262 | 1263 | if(_wrapAround && _cursorX > width) { 1264 | _cursorX = 0; 1265 | _cursorY++; 1266 | } 1267 | 1268 | if(_cursorY == height) 1269 | _cursorY--; 1270 | 1271 | /+ 1272 | auto index = getIndex(_cursorX, _cursorY); 1273 | if(data[index] != ch) { 1274 | data[index] = ch; 1275 | } 1276 | +/ 1277 | } 1278 | 1279 | writeStringRaw(s); 1280 | } 1281 | 1282 | /* private */ bool _wrapAround = true; 1283 | 1284 | deprecated alias writePrintableString writeString; /// use write() or writePrintableString instead 1285 | 1286 | private string writeBuffer; 1287 | 1288 | // you really, really shouldn't use this unless you know what you are doing 1289 | /*private*/ void writeStringRaw(in char[] s) { 1290 | // FIXME: make sure all the data is sent, check for errors 1291 | version(Posix) { 1292 | writeBuffer ~= s; // buffer it to do everything at once in flush() calls 1293 | } else version(Windows) { 1294 | writeBuffer ~= s; 1295 | } else static assert(0); 1296 | } 1297 | 1298 | /// Clears the screen. 1299 | void clear() { 1300 | version(Posix) { 1301 | doTermcap("cl"); 1302 | } else version(Windows) { 1303 | // http://support.microsoft.com/kb/99261 1304 | flush(); 1305 | 1306 | DWORD c; 1307 | CONSOLE_SCREEN_BUFFER_INFO csbi; 1308 | DWORD conSize; 1309 | GetConsoleScreenBufferInfo(hConsole, &csbi); 1310 | conSize = csbi.dwSize.X * csbi.dwSize.Y; 1311 | COORD coordScreen; 1312 | FillConsoleOutputCharacterA(hConsole, ' ', conSize, coordScreen, &c); 1313 | FillConsoleOutputAttribute(hConsole, csbi.wAttributes, conSize, coordScreen, &c); 1314 | moveTo(0, 0, ForceOption.alwaysSend); 1315 | } 1316 | 1317 | _cursorX = 0; 1318 | _cursorY = 0; 1319 | } 1320 | 1321 | /// gets a line, including user editing. Convenience method around the LineGetter class and RealTimeConsoleInput facilities - use them if you need more control. 1322 | /// You really shouldn't call this if stdin isn't actually a user-interactive terminal! So if you expect people to pipe data to your app, check for that or use something else. 1323 | // FIXME: add a method to make it easy to check if stdin is actually a tty and use other methods there. 1324 | string getline(string prompt = null) { 1325 | if(lineGetter is null) 1326 | lineGetter = new LineGetter(&this); 1327 | // since the struct might move (it shouldn't, this should be unmovable!) but since 1328 | // it technically might, I'm updating the pointer before using it just in case. 1329 | lineGetter.terminal = &this; 1330 | 1331 | if(prompt !is null) 1332 | lineGetter.prompt = prompt; 1333 | 1334 | auto input = RealTimeConsoleInput(&this, ConsoleInputFlags.raw); 1335 | auto line = lineGetter.getline(&input); 1336 | 1337 | // lineGetter leaves us exactly where it was when the user hit enter, giving best 1338 | // flexibility to real-time input and cellular programs. The convenience function, 1339 | // however, wants to do what is right in most the simple cases, which is to actually 1340 | // print the line (echo would be enabled without RealTimeConsoleInput anyway and they 1341 | // did hit enter), so we'll do that here too. 1342 | writePrintableString("\n"); 1343 | 1344 | return line; 1345 | } 1346 | 1347 | } 1348 | 1349 | /+ 1350 | struct ConsoleBuffer { 1351 | int cursorX; 1352 | int cursorY; 1353 | int width; 1354 | int height; 1355 | dchar[] data; 1356 | 1357 | void actualize(Terminal* t) { 1358 | auto writer = t.getBufferedWriter(); 1359 | 1360 | this.copyTo(&(t.onScreen)); 1361 | } 1362 | 1363 | void copyTo(ConsoleBuffer* buffer) { 1364 | buffer.cursorX = this.cursorX; 1365 | buffer.cursorY = this.cursorY; 1366 | buffer.width = this.width; 1367 | buffer.height = this.height; 1368 | buffer.data[] = this.data[]; 1369 | } 1370 | } 1371 | +/ 1372 | 1373 | /** 1374 | * Encapsulates the stream of input events received from the terminal input. 1375 | */ 1376 | struct RealTimeConsoleInput { 1377 | @disable this(); 1378 | @disable this(this); 1379 | 1380 | version(Posix) { 1381 | private int fdOut; 1382 | private int fdIn; 1383 | private sigaction_t oldSigWinch; 1384 | private sigaction_t oldSigIntr; 1385 | private sigaction_t oldHupIntr; 1386 | private termios old; 1387 | ubyte[128] hack; 1388 | // apparently termios isn't the size druntime thinks it is (at least on 32 bit, sometimes).... 1389 | // tcgetattr smashed other variables in here too that could create random problems 1390 | // so this hack is just to give some room for that to happen without destroying the rest of the world 1391 | } 1392 | 1393 | version(Windows) { 1394 | private DWORD oldInput; 1395 | private DWORD oldOutput; 1396 | HANDLE inputHandle; 1397 | } 1398 | 1399 | private ConsoleInputFlags flags; 1400 | private Terminal* terminal; 1401 | private void delegate()[] destructor; 1402 | 1403 | /// To capture input, you need to provide a terminal and some flags. 1404 | public this(Terminal* terminal, ConsoleInputFlags flags) { 1405 | this.flags = flags; 1406 | this.terminal = terminal; 1407 | 1408 | version(Windows) { 1409 | inputHandle = GetStdHandle(STD_INPUT_HANDLE); 1410 | 1411 | GetConsoleMode(inputHandle, &oldInput); 1412 | 1413 | DWORD mode = 0; 1414 | mode |= ENABLE_PROCESSED_INPUT /* 0x01 */; // this gives Ctrl+C which we probably want to be similar to linux 1415 | //if(flags & ConsoleInputFlags.size) 1416 | mode |= ENABLE_WINDOW_INPUT /* 0208 */; // gives size etc 1417 | if(flags & ConsoleInputFlags.echo) 1418 | mode |= ENABLE_ECHO_INPUT; // 0x4 1419 | if(flags & ConsoleInputFlags.mouse) 1420 | mode |= ENABLE_MOUSE_INPUT; // 0x10 1421 | // if(flags & ConsoleInputFlags.raw) // FIXME: maybe that should be a separate flag for ENABLE_LINE_INPUT 1422 | 1423 | SetConsoleMode(inputHandle, mode); 1424 | destructor ~= { SetConsoleMode(inputHandle, oldInput); }; 1425 | 1426 | 1427 | GetConsoleMode(terminal.hConsole, &oldOutput); 1428 | mode = 0; 1429 | // we want this to match linux too 1430 | mode |= ENABLE_PROCESSED_OUTPUT; /* 0x01 */ 1431 | mode |= ENABLE_WRAP_AT_EOL_OUTPUT; /* 0x02 */ 1432 | SetConsoleMode(terminal.hConsole, mode); 1433 | destructor ~= { SetConsoleMode(terminal.hConsole, oldOutput); }; 1434 | 1435 | // FIXME: change to UTF8 as well 1436 | } 1437 | 1438 | version(Posix) { 1439 | this.fdIn = terminal.fdIn; 1440 | this.fdOut = terminal.fdOut; 1441 | 1442 | if(fdIn != -1) { 1443 | tcgetattr(fdIn, &old); 1444 | auto n = old; 1445 | 1446 | auto f = ICANON; 1447 | if(!(flags & ConsoleInputFlags.echo)) 1448 | f |= ECHO; 1449 | 1450 | n.c_lflag &= ~f; 1451 | tcsetattr(fdIn, TCSANOW, &n); 1452 | } 1453 | 1454 | // some weird bug breaks this, https://github.com/robik/ConsoleD/issues/3 1455 | //destructor ~= { tcsetattr(fdIn, TCSANOW, &old); }; 1456 | 1457 | if(flags & ConsoleInputFlags.size) { 1458 | import core.sys.posix.signal; 1459 | sigaction_t n; 1460 | n.sa_handler = &sizeSignalHandler; 1461 | n.sa_mask = cast(sigset_t) 0; 1462 | n.sa_flags = 0; 1463 | sigaction(SIGWINCH, &n, &oldSigWinch); 1464 | } 1465 | 1466 | { 1467 | import core.sys.posix.signal; 1468 | sigaction_t n; 1469 | n.sa_handler = &interruptSignalHandler; 1470 | n.sa_mask = cast(sigset_t) 0; 1471 | n.sa_flags = 0; 1472 | sigaction(SIGINT, &n, &oldSigIntr); 1473 | } 1474 | 1475 | { 1476 | import core.sys.posix.signal; 1477 | sigaction_t n; 1478 | n.sa_handler = &hangupSignalHandler; 1479 | n.sa_mask = cast(sigset_t) 0; 1480 | n.sa_flags = 0; 1481 | sigaction(SIGHUP, &n, &oldHupIntr); 1482 | } 1483 | 1484 | 1485 | 1486 | if(flags & ConsoleInputFlags.mouse) { 1487 | // basic button press+release notification 1488 | 1489 | // FIXME: try to get maximum capabilities from all terminals 1490 | // right now this works well on xterm but rxvt isn't sending movements... 1491 | 1492 | terminal.writeStringRaw("\033[?1000h"); 1493 | destructor ~= { terminal.writeStringRaw("\033[?1000l"); }; 1494 | // the MOUSE_HACK env var is for the case where I run screen 1495 | // but set TERM=xterm (which I do from putty). The 1003 mouse mode 1496 | // doesn't work there, breaking mouse support entirely. So by setting 1497 | // MOUSE_HACK=1002 it tells us to use the other mode for a fallback. 1498 | import std.process : environment; 1499 | if(terminal.terminalInFamily("xterm") && environment.get("MOUSE_HACK") != "1002") { 1500 | // this is vt200 mouse with full motion tracking, supported by xterm 1501 | terminal.writeStringRaw("\033[?1003h"); 1502 | destructor ~= { terminal.writeStringRaw("\033[?1003l"); }; 1503 | } else if(terminal.terminalInFamily("rxvt", "screen") || environment.get("MOUSE_HACK") == "1002") { 1504 | terminal.writeStringRaw("\033[?1002h"); // this is vt200 mouse with press/release and motion notification iff buttons are pressed 1505 | destructor ~= { terminal.writeStringRaw("\033[?1002l"); }; 1506 | } 1507 | } 1508 | if(flags & ConsoleInputFlags.paste) { 1509 | if(terminal.terminalInFamily("xterm", "rxvt", "screen")) { 1510 | terminal.writeStringRaw("\033[?2004h"); // bracketed paste mode 1511 | destructor ~= { terminal.writeStringRaw("\033[?2004l"); }; 1512 | } 1513 | } 1514 | 1515 | // try to ensure the terminal is in UTF-8 mode 1516 | if(terminal.terminalInFamily("xterm", "screen", "linux") && !terminal.isMacTerminal()) { 1517 | terminal.writeStringRaw("\033%G"); 1518 | } 1519 | 1520 | terminal.flush(); 1521 | } 1522 | 1523 | 1524 | version(with_eventloop) { 1525 | import arsd.eventloop; 1526 | version(Windows) 1527 | auto listenTo = inputHandle; 1528 | else version(Posix) 1529 | auto listenTo = this.fdIn; 1530 | else static assert(0, "idk about this OS"); 1531 | 1532 | version(Posix) 1533 | addListener(&signalFired); 1534 | 1535 | if(listenTo != -1) { 1536 | addFileEventListeners(listenTo, &eventListener, null, null); 1537 | destructor ~= { removeFileEventListeners(listenTo); }; 1538 | } 1539 | addOnIdle(&terminal.flush); 1540 | destructor ~= { removeOnIdle(&terminal.flush); }; 1541 | } 1542 | } 1543 | 1544 | version(with_eventloop) { 1545 | version(Posix) 1546 | void signalFired(SignalFired) { 1547 | if(interrupted) { 1548 | interrupted = false; 1549 | send(InputEvent(UserInterruptionEvent(), terminal)); 1550 | } 1551 | if(windowSizeChanged) 1552 | send(checkWindowSizeChanged()); 1553 | if(hangedUp) { 1554 | hangedUp = false; 1555 | send(InputEvent(HangupEvent(), terminal)); 1556 | } 1557 | } 1558 | 1559 | import arsd.eventloop; 1560 | void eventListener(OsFileHandle fd) { 1561 | auto queue = readNextEvents(); 1562 | foreach(event; queue) 1563 | send(event); 1564 | } 1565 | } 1566 | 1567 | ~this() { 1568 | // the delegate thing doesn't actually work for this... for some reason 1569 | version(Posix) 1570 | if(fdIn != -1) 1571 | tcsetattr(fdIn, TCSANOW, &old); 1572 | 1573 | version(Posix) { 1574 | if(flags & ConsoleInputFlags.size) { 1575 | // restoration 1576 | sigaction(SIGWINCH, &oldSigWinch, null); 1577 | } 1578 | sigaction(SIGINT, &oldSigIntr, null); 1579 | sigaction(SIGHUP, &oldHupIntr, null); 1580 | } 1581 | 1582 | // we're just undoing everything the constructor did, in reverse order, same criteria 1583 | foreach_reverse(d; destructor) 1584 | d(); 1585 | } 1586 | 1587 | /** 1588 | Returns true if there iff getch() would not block. 1589 | 1590 | WARNING: kbhit might consume input that would be ignored by getch. This 1591 | function is really only meant to be used in conjunction with getch. Typically, 1592 | you should use a full-fledged event loop if you want all kinds of input. kbhit+getch 1593 | are just for simple keyboard driven applications. 1594 | */ 1595 | bool kbhit() { 1596 | auto got = getch(true); 1597 | 1598 | if(got == dchar.init) 1599 | return false; 1600 | 1601 | getchBuffer = got; 1602 | return true; 1603 | } 1604 | 1605 | /// Check for input, waiting no longer than the number of milliseconds 1606 | bool timedCheckForInput(int milliseconds) { 1607 | version(Windows) { 1608 | auto response = WaitForSingleObject(terminal.hConsole, milliseconds); 1609 | if(response == 0) 1610 | return true; // the object is ready 1611 | return false; 1612 | } else version(Posix) { 1613 | if(fdIn == -1) 1614 | return false; 1615 | 1616 | timeval tv; 1617 | tv.tv_sec = 0; 1618 | tv.tv_usec = milliseconds * 1000; 1619 | 1620 | fd_set fs; 1621 | FD_ZERO(&fs); 1622 | 1623 | FD_SET(fdIn, &fs); 1624 | if(select(fdIn + 1, &fs, null, null, &tv) == -1) { 1625 | return false; 1626 | } 1627 | 1628 | return FD_ISSET(fdIn, &fs); 1629 | } 1630 | } 1631 | 1632 | /* private */ bool anyInput_internal() { 1633 | if(inputQueue.length || timedCheckForInput(0)) 1634 | return true; 1635 | version(Posix) 1636 | if(interrupted || windowSizeChanged || hangedUp) 1637 | return true; 1638 | return false; 1639 | } 1640 | 1641 | private dchar getchBuffer; 1642 | 1643 | /// Get one key press from the terminal, discarding other 1644 | /// events in the process. Returns dchar.init upon receiving end-of-file. 1645 | /// 1646 | /// Be aware that this may return non-character key events, like F1, F2, arrow keys, etc., as private use Unicode characters. Check them against KeyboardEvent.Key if you like. 1647 | dchar getch(bool nonblocking = false) { 1648 | if(getchBuffer != dchar.init) { 1649 | auto a = getchBuffer; 1650 | getchBuffer = dchar.init; 1651 | return a; 1652 | } 1653 | 1654 | if(nonblocking && !anyInput_internal()) 1655 | return dchar.init; 1656 | 1657 | auto event = nextEvent(); 1658 | while(event.type != InputEvent.Type.KeyboardEvent || event.keyboardEvent.pressed == false) { 1659 | if(event.type == InputEvent.Type.UserInterruptionEvent) 1660 | throw new UserInterruptionException(); 1661 | if(event.type == InputEvent.Type.HangupEvent) 1662 | throw new HangupException(); 1663 | if(event.type == InputEvent.Type.EndOfFileEvent) 1664 | return dchar.init; 1665 | 1666 | if(nonblocking && !anyInput_internal()) 1667 | return dchar.init; 1668 | 1669 | event = nextEvent(); 1670 | } 1671 | return event.keyboardEvent.which; 1672 | } 1673 | 1674 | //char[128] inputBuffer; 1675 | //int inputBufferPosition; 1676 | version(Posix) 1677 | int nextRaw(bool interruptable = false) { 1678 | if(fdIn == -1) 1679 | return 0; 1680 | 1681 | char[1] buf; 1682 | try_again: 1683 | auto ret = read(fdIn, buf.ptr, buf.length); 1684 | if(ret == 0) 1685 | return 0; // input closed 1686 | if(ret == -1) { 1687 | import core.stdc.errno; 1688 | if(errno == EINTR) 1689 | // interrupted by signal call, quite possibly resize or ctrl+c which we want to check for in the event loop 1690 | if(interruptable) 1691 | return -1; 1692 | else 1693 | goto try_again; 1694 | else 1695 | throw new Exception("read failed"); 1696 | } 1697 | 1698 | //terminal.writef("RAW READ: %d\n", buf[0]); 1699 | 1700 | if(ret == 1) 1701 | return inputPrefilter ? inputPrefilter(buf[0]) : buf[0]; 1702 | else 1703 | assert(0); // read too much, should be impossible 1704 | } 1705 | 1706 | version(Posix) 1707 | int delegate(char) inputPrefilter; 1708 | 1709 | version(Posix) 1710 | dchar nextChar(int starting) { 1711 | if(starting <= 127) 1712 | return cast(dchar) starting; 1713 | char[6] buffer; 1714 | int pos = 0; 1715 | buffer[pos++] = cast(char) starting; 1716 | 1717 | // see the utf-8 encoding for details 1718 | int remaining = 0; 1719 | ubyte magic = starting & 0xff; 1720 | while(magic & 0b1000_000) { 1721 | remaining++; 1722 | magic <<= 1; 1723 | } 1724 | 1725 | while(remaining && pos < buffer.length) { 1726 | buffer[pos++] = cast(char) nextRaw(); 1727 | remaining--; 1728 | } 1729 | 1730 | import std.utf; 1731 | size_t throwAway; // it insists on the index but we don't care 1732 | return decode(buffer[], throwAway); 1733 | } 1734 | 1735 | InputEvent checkWindowSizeChanged() { 1736 | auto oldWidth = terminal.width; 1737 | auto oldHeight = terminal.height; 1738 | terminal.updateSize(); 1739 | version(Posix) 1740 | windowSizeChanged = false; 1741 | return InputEvent(SizeChangedEvent(oldWidth, oldHeight, terminal.width, terminal.height), terminal); 1742 | } 1743 | 1744 | 1745 | // character event 1746 | // non-character key event 1747 | // paste event 1748 | // mouse event 1749 | // size event maybe, and if appropriate focus events 1750 | 1751 | /// Returns the next event. 1752 | /// 1753 | /// Experimental: It is also possible to integrate this into 1754 | /// a generic event loop, currently under -version=with_eventloop and it will 1755 | /// require the module arsd.eventloop (Linux only at this point) 1756 | InputEvent nextEvent() { 1757 | terminal.flush(); 1758 | if(inputQueue.length) { 1759 | auto e = inputQueue[0]; 1760 | inputQueue = inputQueue[1 .. $]; 1761 | return e; 1762 | } 1763 | 1764 | wait_for_more: 1765 | version(Posix) 1766 | if(interrupted) { 1767 | interrupted = false; 1768 | return InputEvent(UserInterruptionEvent(), terminal); 1769 | } 1770 | 1771 | version(Posix) 1772 | if(hangedUp) { 1773 | hangedUp = false; 1774 | return InputEvent(HangupEvent(), terminal); 1775 | } 1776 | 1777 | version(Posix) 1778 | if(windowSizeChanged) { 1779 | return checkWindowSizeChanged(); 1780 | } 1781 | 1782 | auto more = readNextEvents(); 1783 | if(!more.length) 1784 | goto wait_for_more; // i used to do a loop (readNextEvents can read something, but it might be discarded by the input filter) but now it goto's above because readNextEvents might be interrupted by a SIGWINCH aka size event so we want to check that at least 1785 | 1786 | assert(more.length); 1787 | 1788 | auto e = more[0]; 1789 | inputQueue = more[1 .. $]; 1790 | return e; 1791 | } 1792 | 1793 | InputEvent* peekNextEvent() { 1794 | if(inputQueue.length) 1795 | return &(inputQueue[0]); 1796 | return null; 1797 | } 1798 | 1799 | enum InjectionPosition { head, tail } 1800 | void injectEvent(InputEvent ev, InjectionPosition where) { 1801 | final switch(where) { 1802 | case InjectionPosition.head: 1803 | inputQueue = ev ~ inputQueue; 1804 | break; 1805 | case InjectionPosition.tail: 1806 | inputQueue ~= ev; 1807 | break; 1808 | } 1809 | } 1810 | 1811 | InputEvent[] inputQueue; 1812 | 1813 | version(Windows) 1814 | InputEvent[] readNextEvents() { 1815 | terminal.flush(); // make sure all output is sent out before waiting for anything 1816 | 1817 | INPUT_RECORD[32] buffer; 1818 | DWORD actuallyRead; 1819 | // FIXME: ReadConsoleInputW 1820 | auto success = ReadConsoleInputA(inputHandle, buffer.ptr, buffer.length, &actuallyRead); 1821 | if(success == 0) 1822 | throw new Exception("ReadConsoleInput"); 1823 | 1824 | InputEvent[] newEvents; 1825 | input_loop: foreach(record; buffer[0 .. actuallyRead]) { 1826 | switch(record.EventType) { 1827 | case KEY_EVENT: 1828 | auto ev = record.KeyEvent; 1829 | KeyboardEvent ke; 1830 | CharacterEvent e; 1831 | NonCharacterKeyEvent ne; 1832 | 1833 | e.eventType = ev.bKeyDown ? CharacterEvent.Type.Pressed : CharacterEvent.Type.Released; 1834 | ne.eventType = ev.bKeyDown ? NonCharacterKeyEvent.Type.Pressed : NonCharacterKeyEvent.Type.Released; 1835 | 1836 | ke.pressed = ev.bKeyDown ? true : false; 1837 | 1838 | // only send released events when specifically requested 1839 | if(!(flags & ConsoleInputFlags.releasedKeys) && !ev.bKeyDown) 1840 | break; 1841 | 1842 | e.modifierState = ev.dwControlKeyState; 1843 | ne.modifierState = ev.dwControlKeyState; 1844 | ke.modifierState = ev.dwControlKeyState; 1845 | 1846 | if(ev.UnicodeChar) { 1847 | // new style event goes first 1848 | ke.which = cast(dchar) cast(wchar) ev.UnicodeChar; 1849 | newEvents ~= InputEvent(ke, terminal); 1850 | 1851 | // old style event then follows as the fallback 1852 | e.character = cast(dchar) cast(wchar) ev.UnicodeChar; 1853 | newEvents ~= InputEvent(e, terminal); 1854 | } else { 1855 | // old style event 1856 | ne.key = cast(NonCharacterKeyEvent.Key) ev.wVirtualKeyCode; 1857 | 1858 | // new style event. See comment on KeyboardEvent.Key 1859 | ke.which = cast(KeyboardEvent.Key) (ev.wVirtualKeyCode + 0xF0000); 1860 | 1861 | // FIXME: make this better. the goal is to make sure the key code is a valid enum member 1862 | // Windows sends more keys than Unix and we're doing lowest common denominator here 1863 | foreach(member; __traits(allMembers, NonCharacterKeyEvent.Key)) 1864 | if(__traits(getMember, NonCharacterKeyEvent.Key, member) == ne.key) { 1865 | newEvents ~= InputEvent(ke, terminal); 1866 | newEvents ~= InputEvent(ne, terminal); 1867 | break; 1868 | } 1869 | } 1870 | break; 1871 | case MOUSE_EVENT: 1872 | auto ev = record.MouseEvent; 1873 | MouseEvent e; 1874 | 1875 | e.modifierState = ev.dwControlKeyState; 1876 | e.x = ev.dwMousePosition.X; 1877 | e.y = ev.dwMousePosition.Y; 1878 | 1879 | switch(ev.dwEventFlags) { 1880 | case 0: 1881 | //press or release 1882 | e.eventType = MouseEvent.Type.Pressed; 1883 | static DWORD lastButtonState; 1884 | auto lastButtonState2 = lastButtonState; 1885 | e.buttons = ev.dwButtonState; 1886 | lastButtonState = e.buttons; 1887 | 1888 | // this is sent on state change. if fewer buttons are pressed, it must mean released 1889 | if(cast(DWORD) e.buttons < lastButtonState2) { 1890 | e.eventType = MouseEvent.Type.Released; 1891 | // if last was 101 and now it is 100, then button far right was released 1892 | // so we flip the bits, ~100 == 011, then and them: 101 & 011 == 001, the 1893 | // button that was released 1894 | e.buttons = lastButtonState2 & ~e.buttons; 1895 | } 1896 | break; 1897 | case MOUSE_MOVED: 1898 | e.eventType = MouseEvent.Type.Moved; 1899 | e.buttons = ev.dwButtonState; 1900 | break; 1901 | case 0x0004/*MOUSE_WHEELED*/: 1902 | e.eventType = MouseEvent.Type.Pressed; 1903 | if(ev.dwButtonState > 0) 1904 | e.buttons = MouseEvent.Button.ScrollDown; 1905 | else 1906 | e.buttons = MouseEvent.Button.ScrollUp; 1907 | break; 1908 | default: 1909 | continue input_loop; 1910 | } 1911 | 1912 | newEvents ~= InputEvent(e, terminal); 1913 | break; 1914 | case WINDOW_BUFFER_SIZE_EVENT: 1915 | auto ev = record.WindowBufferSizeEvent; 1916 | auto oldWidth = terminal.width; 1917 | auto oldHeight = terminal.height; 1918 | terminal._width = ev.dwSize.X; 1919 | terminal._height = ev.dwSize.Y; 1920 | newEvents ~= InputEvent(SizeChangedEvent(oldWidth, oldHeight, terminal.width, terminal.height), terminal); 1921 | break; 1922 | // FIXME: can we catch ctrl+c here too? 1923 | default: 1924 | // ignore 1925 | } 1926 | } 1927 | 1928 | return newEvents; 1929 | } 1930 | 1931 | version(Posix) 1932 | InputEvent[] readNextEvents() { 1933 | terminal.flush(); // make sure all output is sent out before we try to get input 1934 | 1935 | // we want to starve the read, especially if we're called from an edge-triggered 1936 | // epoll (which might happen in version=with_eventloop.. impl detail there subject 1937 | // to change). 1938 | auto initial = readNextEventsHelper(); 1939 | 1940 | // lol this calls select() inside a function prolly called from epoll but meh, 1941 | // it is the simplest thing that can possibly work. The alternative would be 1942 | // doing non-blocking reads and buffering in the nextRaw function (not a bad idea 1943 | // btw, just a bit more of a hassle). 1944 | while(timedCheckForInput(0)) { 1945 | auto ne = readNextEventsHelper(); 1946 | initial ~= ne; 1947 | foreach(n; ne) 1948 | if(n.type == InputEvent.Type.EndOfFileEvent) 1949 | return initial; // hit end of file, get out of here lest we infinite loop 1950 | // (select still returns info available even after we read end of file) 1951 | } 1952 | return initial; 1953 | } 1954 | 1955 | // The helper reads just one actual event from the pipe... 1956 | version(Posix) 1957 | InputEvent[] readNextEventsHelper() { 1958 | InputEvent[] charPressAndRelease(dchar character) { 1959 | if((flags & ConsoleInputFlags.releasedKeys)) 1960 | return [ 1961 | // new style event 1962 | InputEvent(KeyboardEvent(true, character, 0), terminal), 1963 | InputEvent(KeyboardEvent(false, character, 0), terminal), 1964 | // old style event 1965 | InputEvent(CharacterEvent(CharacterEvent.Type.Pressed, character, 0), terminal), 1966 | InputEvent(CharacterEvent(CharacterEvent.Type.Released, character, 0), terminal), 1967 | ]; 1968 | else return [ 1969 | // new style event 1970 | InputEvent(KeyboardEvent(true, character, 0), terminal), 1971 | // old style event 1972 | InputEvent(CharacterEvent(CharacterEvent.Type.Pressed, character, 0), terminal) 1973 | ]; 1974 | } 1975 | InputEvent[] keyPressAndRelease(NonCharacterKeyEvent.Key key, uint modifiers = 0) { 1976 | if((flags & ConsoleInputFlags.releasedKeys)) 1977 | return [ 1978 | // new style event FIXME: when the old events are removed, kill the +0xF0000 from here! 1979 | InputEvent(KeyboardEvent(true, cast(dchar)(key) + 0xF0000, modifiers), terminal), 1980 | InputEvent(KeyboardEvent(false, cast(dchar)(key) + 0xF0000, modifiers), terminal), 1981 | // old style event 1982 | InputEvent(NonCharacterKeyEvent(NonCharacterKeyEvent.Type.Pressed, key, modifiers), terminal), 1983 | InputEvent(NonCharacterKeyEvent(NonCharacterKeyEvent.Type.Released, key, modifiers), terminal), 1984 | ]; 1985 | else return [ 1986 | // new style event FIXME: when the old events are removed, kill the +0xF0000 from here! 1987 | InputEvent(KeyboardEvent(true, cast(dchar)(key) + 0xF0000, modifiers), terminal), 1988 | // old style event 1989 | InputEvent(NonCharacterKeyEvent(NonCharacterKeyEvent.Type.Pressed, key, modifiers), terminal) 1990 | ]; 1991 | } 1992 | 1993 | char[30] sequenceBuffer; 1994 | 1995 | // this assumes you just read "\033[" 1996 | char[] readEscapeSequence(char[] sequence) { 1997 | int sequenceLength = 2; 1998 | sequence[0] = '\033'; 1999 | sequence[1] = '['; 2000 | 2001 | while(sequenceLength < sequence.length) { 2002 | auto n = nextRaw(); 2003 | sequence[sequenceLength++] = cast(char) n; 2004 | // I think a [ is supposed to termiate a CSI sequence 2005 | // but the Linux console sends CSI[A for F1, so I'm 2006 | // hacking it to accept that too 2007 | if(n >= 0x40 && !(sequenceLength == 3 && n == '[')) 2008 | break; 2009 | } 2010 | 2011 | return sequence[0 .. sequenceLength]; 2012 | } 2013 | 2014 | InputEvent[] translateTermcapName(string cap) { 2015 | switch(cap) { 2016 | //case "k0": 2017 | //return keyPressAndRelease(NonCharacterKeyEvent.Key.F1); 2018 | case "k1": 2019 | return keyPressAndRelease(NonCharacterKeyEvent.Key.F1); 2020 | case "k2": 2021 | return keyPressAndRelease(NonCharacterKeyEvent.Key.F2); 2022 | case "k3": 2023 | return keyPressAndRelease(NonCharacterKeyEvent.Key.F3); 2024 | case "k4": 2025 | return keyPressAndRelease(NonCharacterKeyEvent.Key.F4); 2026 | case "k5": 2027 | return keyPressAndRelease(NonCharacterKeyEvent.Key.F5); 2028 | case "k6": 2029 | return keyPressAndRelease(NonCharacterKeyEvent.Key.F6); 2030 | case "k7": 2031 | return keyPressAndRelease(NonCharacterKeyEvent.Key.F7); 2032 | case "k8": 2033 | return keyPressAndRelease(NonCharacterKeyEvent.Key.F8); 2034 | case "k9": 2035 | return keyPressAndRelease(NonCharacterKeyEvent.Key.F9); 2036 | case "k;": 2037 | case "k0": 2038 | return keyPressAndRelease(NonCharacterKeyEvent.Key.F10); 2039 | case "F1": 2040 | return keyPressAndRelease(NonCharacterKeyEvent.Key.F11); 2041 | case "F2": 2042 | return keyPressAndRelease(NonCharacterKeyEvent.Key.F12); 2043 | 2044 | 2045 | case "kb": 2046 | return charPressAndRelease('\b'); 2047 | case "kD": 2048 | return keyPressAndRelease(NonCharacterKeyEvent.Key.Delete); 2049 | 2050 | case "kd": 2051 | case "do": 2052 | return keyPressAndRelease(NonCharacterKeyEvent.Key.DownArrow); 2053 | case "ku": 2054 | case "up": 2055 | return keyPressAndRelease(NonCharacterKeyEvent.Key.UpArrow); 2056 | case "kl": 2057 | return keyPressAndRelease(NonCharacterKeyEvent.Key.LeftArrow); 2058 | case "kr": 2059 | case "nd": 2060 | return keyPressAndRelease(NonCharacterKeyEvent.Key.RightArrow); 2061 | 2062 | case "kN": 2063 | case "K5": 2064 | return keyPressAndRelease(NonCharacterKeyEvent.Key.PageDown); 2065 | case "kP": 2066 | case "K2": 2067 | return keyPressAndRelease(NonCharacterKeyEvent.Key.PageUp); 2068 | 2069 | case "ho": // this might not be a key but my thing sometimes returns it... weird... 2070 | case "kh": 2071 | case "K1": 2072 | return keyPressAndRelease(NonCharacterKeyEvent.Key.Home); 2073 | case "kH": 2074 | return keyPressAndRelease(NonCharacterKeyEvent.Key.End); 2075 | case "kI": 2076 | return keyPressAndRelease(NonCharacterKeyEvent.Key.Insert); 2077 | default: 2078 | // don't know it, just ignore 2079 | //import std.stdio; 2080 | //writeln(cap); 2081 | } 2082 | 2083 | return null; 2084 | } 2085 | 2086 | 2087 | InputEvent[] doEscapeSequence(in char[] sequence) { 2088 | switch(sequence) { 2089 | case "\033[200~": 2090 | // bracketed paste begin 2091 | // we want to keep reading until 2092 | // "\033[201~": 2093 | // and build a paste event out of it 2094 | 2095 | 2096 | string data; 2097 | for(;;) { 2098 | auto n = nextRaw(); 2099 | if(n == '\033') { 2100 | n = nextRaw(); 2101 | if(n == '[') { 2102 | auto esc = readEscapeSequence(sequenceBuffer); 2103 | if(esc == "\033[201~") { 2104 | // complete! 2105 | break; 2106 | } else { 2107 | // was something else apparently, but it is pasted, so keep it 2108 | data ~= esc; 2109 | } 2110 | } else { 2111 | data ~= '\033'; 2112 | data ~= cast(char) n; 2113 | } 2114 | } else { 2115 | data ~= cast(char) n; 2116 | } 2117 | } 2118 | return [InputEvent(PasteEvent(data), terminal)]; 2119 | case "\033[M": 2120 | // mouse event 2121 | auto buttonCode = nextRaw() - 32; 2122 | // nextChar is commented because i'm not using UTF-8 mouse mode 2123 | // cuz i don't think it is as widely supported 2124 | auto x = cast(int) (/*nextChar*/(nextRaw())) - 33; /* they encode value + 32, but make upper left 1,1. I want it to be 0,0 */ 2125 | auto y = cast(int) (/*nextChar*/(nextRaw())) - 33; /* ditto */ 2126 | 2127 | 2128 | bool isRelease = (buttonCode & 0b11) == 3; 2129 | int buttonNumber; 2130 | if(!isRelease) { 2131 | buttonNumber = (buttonCode & 0b11); 2132 | if(buttonCode & 64) 2133 | buttonNumber += 3; // button 4 and 5 are sent as like button 1 and 2, but code | 64 2134 | // so button 1 == button 4 here 2135 | 2136 | // note: buttonNumber == 0 means button 1 at this point 2137 | buttonNumber++; // hence this 2138 | 2139 | 2140 | // apparently this considers middle to be button 2. but i want middle to be button 3. 2141 | if(buttonNumber == 2) 2142 | buttonNumber = 3; 2143 | else if(buttonNumber == 3) 2144 | buttonNumber = 2; 2145 | } 2146 | 2147 | auto modifiers = buttonCode & (0b0001_1100); 2148 | // 4 == shift 2149 | // 8 == meta 2150 | // 16 == control 2151 | 2152 | MouseEvent m; 2153 | 2154 | if(buttonCode & 32) 2155 | m.eventType = MouseEvent.Type.Moved; 2156 | else 2157 | m.eventType = isRelease ? MouseEvent.Type.Released : MouseEvent.Type.Pressed; 2158 | 2159 | // ugh, if no buttons are pressed, released and moved are indistinguishable... 2160 | // so we'll count the buttons down, and if we get a release 2161 | static int buttonsDown = 0; 2162 | if(!isRelease && buttonNumber <= 3) // exclude wheel "presses"... 2163 | buttonsDown++; 2164 | 2165 | if(isRelease && m.eventType != MouseEvent.Type.Moved) { 2166 | if(buttonsDown) 2167 | buttonsDown--; 2168 | else // no buttons down, so this should be a motion instead.. 2169 | m.eventType = MouseEvent.Type.Moved; 2170 | } 2171 | 2172 | 2173 | if(buttonNumber == 0) 2174 | m.buttons = 0; // we don't actually know :( 2175 | else 2176 | m.buttons = 1 << (buttonNumber - 1); // I prefer flags so that's how we do it 2177 | m.x = x; 2178 | m.y = y; 2179 | m.modifierState = modifiers; 2180 | 2181 | return [InputEvent(m, terminal)]; 2182 | default: 2183 | // look it up in the termcap key database 2184 | auto cap = terminal.findSequenceInTermcap(sequence); 2185 | if(cap !is null) { 2186 | return translateTermcapName(cap); 2187 | } else { 2188 | if(terminal.terminalInFamily("xterm")) { 2189 | import std.conv, std.string; 2190 | auto terminator = sequence[$ - 1]; 2191 | auto parts = sequence[2 .. $ - 1].split(";"); 2192 | // parts[0] and terminator tells us the key 2193 | // parts[1] tells us the modifierState 2194 | 2195 | uint modifierState; 2196 | 2197 | int modGot; 2198 | if(parts.length > 1) 2199 | modGot = to!int(parts[1]); 2200 | mod_switch: switch(modGot) { 2201 | case 2: modifierState |= ModifierState.shift; break; 2202 | case 3: modifierState |= ModifierState.alt; break; 2203 | case 4: modifierState |= ModifierState.shift | ModifierState.alt; break; 2204 | case 5: modifierState |= ModifierState.control; break; 2205 | case 6: modifierState |= ModifierState.shift | ModifierState.control; break; 2206 | case 7: modifierState |= ModifierState.alt | ModifierState.control; break; 2207 | case 8: modifierState |= ModifierState.shift | ModifierState.alt | ModifierState.control; break; 2208 | case 9: 2209 | .. 2210 | case 16: 2211 | modifierState |= ModifierState.meta; 2212 | if(modGot != 9) { 2213 | modGot -= 8; 2214 | goto mod_switch; 2215 | } 2216 | break; 2217 | 2218 | // this is an extension in my own terminal emulator 2219 | case 20: 2220 | .. 2221 | case 36: 2222 | modifierState |= ModifierState.windows; 2223 | modGot -= 20; 2224 | goto mod_switch; 2225 | default: 2226 | } 2227 | 2228 | switch(terminator) { 2229 | case 'A': return keyPressAndRelease(NonCharacterKeyEvent.Key.UpArrow, modifierState); 2230 | case 'B': return keyPressAndRelease(NonCharacterKeyEvent.Key.DownArrow, modifierState); 2231 | case 'C': return keyPressAndRelease(NonCharacterKeyEvent.Key.RightArrow, modifierState); 2232 | case 'D': return keyPressAndRelease(NonCharacterKeyEvent.Key.LeftArrow, modifierState); 2233 | 2234 | case 'H': return keyPressAndRelease(NonCharacterKeyEvent.Key.Home, modifierState); 2235 | case 'F': return keyPressAndRelease(NonCharacterKeyEvent.Key.End, modifierState); 2236 | 2237 | case 'P': return keyPressAndRelease(NonCharacterKeyEvent.Key.F1, modifierState); 2238 | case 'Q': return keyPressAndRelease(NonCharacterKeyEvent.Key.F2, modifierState); 2239 | case 'R': return keyPressAndRelease(NonCharacterKeyEvent.Key.F3, modifierState); 2240 | case 'S': return keyPressAndRelease(NonCharacterKeyEvent.Key.F4, modifierState); 2241 | 2242 | case '~': // others 2243 | switch(parts[0]) { 2244 | case "5": return keyPressAndRelease(NonCharacterKeyEvent.Key.PageUp, modifierState); 2245 | case "6": return keyPressAndRelease(NonCharacterKeyEvent.Key.PageDown, modifierState); 2246 | case "2": return keyPressAndRelease(NonCharacterKeyEvent.Key.Insert, modifierState); 2247 | case "3": return keyPressAndRelease(NonCharacterKeyEvent.Key.Delete, modifierState); 2248 | 2249 | case "15": return keyPressAndRelease(NonCharacterKeyEvent.Key.F5, modifierState); 2250 | case "17": return keyPressAndRelease(NonCharacterKeyEvent.Key.F6, modifierState); 2251 | case "18": return keyPressAndRelease(NonCharacterKeyEvent.Key.F7, modifierState); 2252 | case "19": return keyPressAndRelease(NonCharacterKeyEvent.Key.F8, modifierState); 2253 | case "20": return keyPressAndRelease(NonCharacterKeyEvent.Key.F9, modifierState); 2254 | case "21": return keyPressAndRelease(NonCharacterKeyEvent.Key.F10, modifierState); 2255 | case "23": return keyPressAndRelease(NonCharacterKeyEvent.Key.F11, modifierState); 2256 | case "24": return keyPressAndRelease(NonCharacterKeyEvent.Key.F12, modifierState); 2257 | default: 2258 | } 2259 | break; 2260 | 2261 | default: 2262 | } 2263 | } else if(terminal.terminalInFamily("rxvt")) { 2264 | // FIXME: figure these out. rxvt seems to just change the terminator while keeping the rest the same 2265 | // though it isn't consistent. ugh. 2266 | } else { 2267 | // maybe we could do more terminals, but linux doesn't even send it and screen just seems to pass through, so i don't think so; xterm prolly covers most them anyway 2268 | // so this space is semi-intentionally left blank 2269 | } 2270 | } 2271 | } 2272 | 2273 | return null; 2274 | } 2275 | 2276 | auto c = nextRaw(true); 2277 | if(c == -1) 2278 | return null; // interrupted; give back nothing so the other level can recheck signal flags 2279 | if(c == 0) 2280 | return [InputEvent(EndOfFileEvent(), terminal)]; 2281 | if(c == '\033') { 2282 | if(timedCheckForInput(50)) { 2283 | // escape sequence 2284 | c = nextRaw(); 2285 | if(c == '[') { // CSI, ends on anything >= 'A' 2286 | return doEscapeSequence(readEscapeSequence(sequenceBuffer)); 2287 | } else if(c == 'O') { 2288 | // could be xterm function key 2289 | auto n = nextRaw(); 2290 | 2291 | char[3] thing; 2292 | thing[0] = '\033'; 2293 | thing[1] = 'O'; 2294 | thing[2] = cast(char) n; 2295 | 2296 | auto cap = terminal.findSequenceInTermcap(thing); 2297 | if(cap is null) { 2298 | return charPressAndRelease('\033') ~ 2299 | charPressAndRelease('O') ~ 2300 | charPressAndRelease(thing[2]); 2301 | } else { 2302 | return translateTermcapName(cap); 2303 | } 2304 | } else { 2305 | // I don't know, probably unsupported terminal or just quick user input or something 2306 | return charPressAndRelease('\033') ~ charPressAndRelease(nextChar(c)); 2307 | } 2308 | } else { 2309 | // user hit escape (or super slow escape sequence, but meh) 2310 | return keyPressAndRelease(NonCharacterKeyEvent.Key.escape); 2311 | } 2312 | } else { 2313 | // FIXME: what if it is neither? we should check the termcap 2314 | auto next = nextChar(c); 2315 | if(next == 127) // some terminals send 127 on the backspace. Let's normalize that. 2316 | next = '\b'; 2317 | return charPressAndRelease(next); 2318 | } 2319 | } 2320 | } 2321 | 2322 | /// The new style of keyboard event 2323 | struct KeyboardEvent { 2324 | bool pressed; /// 2325 | dchar which; /// 2326 | uint modifierState; /// 2327 | 2328 | /// 2329 | bool isCharacter() { 2330 | return !(which >= Key.min && which <= Key.max); 2331 | } 2332 | 2333 | // these match Windows virtual key codes numerically for simplicity of translation there 2334 | // but are plus a unicode private use area offset so i can cram them in the dchar 2335 | // http://msdn.microsoft.com/en-us/library/windows/desktop/dd375731%28v=vs.85%29.aspx 2336 | /// . 2337 | enum Key : dchar { 2338 | escape = 0x1b + 0xF0000, /// . 2339 | F1 = 0x70 + 0xF0000, /// . 2340 | F2 = 0x71 + 0xF0000, /// . 2341 | F3 = 0x72 + 0xF0000, /// . 2342 | F4 = 0x73 + 0xF0000, /// . 2343 | F5 = 0x74 + 0xF0000, /// . 2344 | F6 = 0x75 + 0xF0000, /// . 2345 | F7 = 0x76 + 0xF0000, /// . 2346 | F8 = 0x77 + 0xF0000, /// . 2347 | F9 = 0x78 + 0xF0000, /// . 2348 | F10 = 0x79 + 0xF0000, /// . 2349 | F11 = 0x7A + 0xF0000, /// . 2350 | F12 = 0x7B + 0xF0000, /// . 2351 | LeftArrow = 0x25 + 0xF0000, /// . 2352 | RightArrow = 0x27 + 0xF0000, /// . 2353 | UpArrow = 0x26 + 0xF0000, /// . 2354 | DownArrow = 0x28 + 0xF0000, /// . 2355 | Insert = 0x2d + 0xF0000, /// . 2356 | Delete = 0x2e + 0xF0000, /// . 2357 | Home = 0x24 + 0xF0000, /// . 2358 | End = 0x23 + 0xF0000, /// . 2359 | PageUp = 0x21 + 0xF0000, /// . 2360 | PageDown = 0x22 + 0xF0000, /// . 2361 | } 2362 | 2363 | 2364 | } 2365 | 2366 | /// Deprecated: use KeyboardEvent instead in new programs 2367 | /// Input event for characters 2368 | struct CharacterEvent { 2369 | /// . 2370 | enum Type { 2371 | Released, /// . 2372 | Pressed /// . 2373 | } 2374 | 2375 | Type eventType; /// . 2376 | dchar character; /// . 2377 | uint modifierState; /// Don't depend on this to be available for character events 2378 | } 2379 | 2380 | /// Deprecated: use KeyboardEvent instead in new programs 2381 | struct NonCharacterKeyEvent { 2382 | /// . 2383 | enum Type { 2384 | Released, /// . 2385 | Pressed /// . 2386 | } 2387 | Type eventType; /// . 2388 | 2389 | // these match Windows virtual key codes numerically for simplicity of translation there 2390 | //http://msdn.microsoft.com/en-us/library/windows/desktop/dd375731%28v=vs.85%29.aspx 2391 | /// . 2392 | enum Key : int { 2393 | escape = 0x1b, /// . 2394 | F1 = 0x70, /// . 2395 | F2 = 0x71, /// . 2396 | F3 = 0x72, /// . 2397 | F4 = 0x73, /// . 2398 | F5 = 0x74, /// . 2399 | F6 = 0x75, /// . 2400 | F7 = 0x76, /// . 2401 | F8 = 0x77, /// . 2402 | F9 = 0x78, /// . 2403 | F10 = 0x79, /// . 2404 | F11 = 0x7A, /// . 2405 | F12 = 0x7B, /// . 2406 | LeftArrow = 0x25, /// . 2407 | RightArrow = 0x27, /// . 2408 | UpArrow = 0x26, /// . 2409 | DownArrow = 0x28, /// . 2410 | Insert = 0x2d, /// . 2411 | Delete = 0x2e, /// . 2412 | Home = 0x24, /// . 2413 | End = 0x23, /// . 2414 | PageUp = 0x21, /// . 2415 | PageDown = 0x22, /// . 2416 | } 2417 | Key key; /// . 2418 | 2419 | uint modifierState; /// A mask of ModifierState. Always use by checking modifierState & ModifierState.something, the actual value differs across platforms 2420 | 2421 | } 2422 | 2423 | /// . 2424 | struct PasteEvent { 2425 | string pastedText; /// . 2426 | } 2427 | 2428 | /// . 2429 | struct MouseEvent { 2430 | // these match simpledisplay.d numerically as well 2431 | /// . 2432 | enum Type { 2433 | Moved = 0, /// . 2434 | Pressed = 1, /// . 2435 | Released = 2, /// . 2436 | Clicked, /// . 2437 | } 2438 | 2439 | Type eventType; /// . 2440 | 2441 | // note: these should numerically match simpledisplay.d for maximum beauty in my other code 2442 | /// . 2443 | enum Button : uint { 2444 | None = 0, /// . 2445 | Left = 1, /// . 2446 | Middle = 4, /// . 2447 | Right = 2, /// . 2448 | ScrollUp = 8, /// . 2449 | ScrollDown = 16 /// . 2450 | } 2451 | uint buttons; /// A mask of Button 2452 | int x; /// 0 == left side 2453 | int y; /// 0 == top 2454 | uint modifierState; /// shift, ctrl, alt, meta, altgr. Not always available. Always check by using modifierState & ModifierState.something 2455 | } 2456 | 2457 | /// When you get this, check terminal.width and terminal.height to see the new size and react accordingly. 2458 | struct SizeChangedEvent { 2459 | int oldWidth; 2460 | int oldHeight; 2461 | int newWidth; 2462 | int newHeight; 2463 | } 2464 | 2465 | /// the user hitting ctrl+c will send this 2466 | /// You should drop what you're doing and perhaps exit when this happens. 2467 | struct UserInterruptionEvent {} 2468 | 2469 | /// If the user hangs up (for example, closes the terminal emulator without exiting the app), this is sent. 2470 | /// If you receive it, you should generally cleanly exit. 2471 | struct HangupEvent {} 2472 | 2473 | /// Sent upon receiving end-of-file from stdin. 2474 | struct EndOfFileEvent {} 2475 | 2476 | interface CustomEvent {} 2477 | 2478 | version(Windows) 2479 | enum ModifierState : uint { 2480 | shift = 0x10, 2481 | control = 0x8 | 0x4, // 8 == left ctrl, 4 == right ctrl 2482 | 2483 | // i'm not sure if the next two are available 2484 | alt = 2 | 1, //2 ==left alt, 1 == right alt 2485 | 2486 | // FIXME: I don't think these are actually available 2487 | windows = 512, 2488 | meta = 4096, // FIXME sanity 2489 | 2490 | // I don't think this is available on Linux.... 2491 | scrollLock = 0x40, 2492 | } 2493 | else 2494 | enum ModifierState : uint { 2495 | shift = 4, 2496 | alt = 2, 2497 | control = 16, 2498 | meta = 8, 2499 | 2500 | windows = 512 // only available if you are using my terminal emulator; it isn't actually offered on standard linux ones 2501 | } 2502 | 2503 | version(DDoc) 2504 | /// 2505 | enum ModifierState : uint { 2506 | /// 2507 | shift = 4, 2508 | /// 2509 | alt = 2, 2510 | /// 2511 | control = 16, 2512 | 2513 | } 2514 | 2515 | /++ 2516 | [RealTimeConsoleInput.nextEvent] returns one of these. Check the type, then use the [InputEvent.get|get] method to get the more detailed information about the event. 2517 | ++/ 2518 | struct InputEvent { 2519 | /// . 2520 | enum Type { 2521 | KeyboardEvent, /// Keyboard key pressed (or released, where supported) 2522 | CharacterEvent, /// Do not use this in new programs, use KeyboardEvent instead 2523 | NonCharacterKeyEvent, /// Do not use this in new programs, use KeyboardEvent instead 2524 | PasteEvent, /// The user pasted some text. Not always available, the pasted text might come as a series of character events instead. 2525 | MouseEvent, /// only sent if you subscribed to mouse events 2526 | SizeChangedEvent, /// only sent if you subscribed to size events 2527 | UserInterruptionEvent, /// the user hit ctrl+c 2528 | EndOfFileEvent, /// stdin has received an end of file 2529 | HangupEvent, /// the terminal hanged up - for example, if the user closed a terminal emulator 2530 | CustomEvent /// . 2531 | } 2532 | 2533 | /// . 2534 | @property Type type() { return t; } 2535 | 2536 | /// Returns a pointer to the terminal associated with this event. 2537 | /// (You can usually just ignore this as there's only one terminal typically.) 2538 | /// 2539 | /// It may be null in the case of program-generated events; 2540 | @property Terminal* terminal() { return term; } 2541 | 2542 | /++ 2543 | Gets the specific event instance. First, check the type (such as in a `switch` statement, then extract the correct one from here. Note that the template argument is a $(B value type of the enum above), not a type argument. So to use it, do $(D event.get!(InputEvent.Type.KeyboardEvent)), for example. 2544 | 2545 | See_Also: 2546 | 2547 | The event types: 2548 | [KeyboardEvent], [MouseEvent], [SizeChangedEvent], 2549 | [PasteEvent], [UserInterruptionEvent], 2550 | [EndOfFileEvent], [HangupEvent], [CustomEvent] 2551 | 2552 | And associated functions: 2553 | [RealTimeConsoleInput], [ConsoleInputFlags] 2554 | ++/ 2555 | @property auto get(Type T)() { 2556 | if(type != T) 2557 | throw new Exception("Wrong event type"); 2558 | static if(T == Type.CharacterEvent) 2559 | return characterEvent; 2560 | else static if(T == Type.KeyboardEvent) 2561 | return keyboardEvent; 2562 | else static if(T == Type.NonCharacterKeyEvent) 2563 | return nonCharacterKeyEvent; 2564 | else static if(T == Type.PasteEvent) 2565 | return pasteEvent; 2566 | else static if(T == Type.MouseEvent) 2567 | return mouseEvent; 2568 | else static if(T == Type.SizeChangedEvent) 2569 | return sizeChangedEvent; 2570 | else static if(T == Type.UserInterruptionEvent) 2571 | return userInterruptionEvent; 2572 | else static if(T == Type.EndOfFileEvent) 2573 | return endOfFileEvent; 2574 | else static if(T == Type.HangupEvent) 2575 | return hangupEvent; 2576 | else static if(T == Type.CustomEvent) 2577 | return customEvent; 2578 | else static assert(0, "Type " ~ T.stringof ~ " not added to the get function"); 2579 | } 2580 | 2581 | /// custom event is public because otherwise there's no point at all 2582 | this(CustomEvent c, Terminal* p = null) { 2583 | t = Type.CustomEvent; 2584 | customEvent = c; 2585 | } 2586 | 2587 | private { 2588 | this(CharacterEvent c, Terminal* p) { 2589 | t = Type.CharacterEvent; 2590 | characterEvent = c; 2591 | } 2592 | this(KeyboardEvent c, Terminal* p) { 2593 | t = Type.KeyboardEvent; 2594 | keyboardEvent = c; 2595 | } 2596 | this(NonCharacterKeyEvent c, Terminal* p) { 2597 | t = Type.NonCharacterKeyEvent; 2598 | nonCharacterKeyEvent = c; 2599 | } 2600 | this(PasteEvent c, Terminal* p) { 2601 | t = Type.PasteEvent; 2602 | pasteEvent = c; 2603 | } 2604 | this(MouseEvent c, Terminal* p) { 2605 | t = Type.MouseEvent; 2606 | mouseEvent = c; 2607 | } 2608 | this(SizeChangedEvent c, Terminal* p) { 2609 | t = Type.SizeChangedEvent; 2610 | sizeChangedEvent = c; 2611 | } 2612 | this(UserInterruptionEvent c, Terminal* p) { 2613 | t = Type.UserInterruptionEvent; 2614 | userInterruptionEvent = c; 2615 | } 2616 | this(HangupEvent c, Terminal* p) { 2617 | t = Type.HangupEvent; 2618 | hangupEvent = c; 2619 | } 2620 | this(EndOfFileEvent c, Terminal* p) { 2621 | t = Type.EndOfFileEvent; 2622 | endOfFileEvent = c; 2623 | } 2624 | 2625 | Type t; 2626 | Terminal* term; 2627 | 2628 | union { 2629 | KeyboardEvent keyboardEvent; 2630 | CharacterEvent characterEvent; 2631 | NonCharacterKeyEvent nonCharacterKeyEvent; 2632 | PasteEvent pasteEvent; 2633 | MouseEvent mouseEvent; 2634 | SizeChangedEvent sizeChangedEvent; 2635 | UserInterruptionEvent userInterruptionEvent; 2636 | HangupEvent hangupEvent; 2637 | EndOfFileEvent endOfFileEvent; 2638 | CustomEvent customEvent; 2639 | } 2640 | } 2641 | } 2642 | 2643 | version(Demo) 2644 | /// View the source of this! 2645 | void main() { 2646 | auto terminal = Terminal(ConsoleOutputType.cellular); 2647 | 2648 | //terminal.color(Color.DEFAULT, Color.DEFAULT); 2649 | 2650 | // 2651 | ///* 2652 | auto getter = new FileLineGetter(&terminal, "test"); 2653 | getter.prompt = "> "; 2654 | getter.history = ["abcdefghijklmnopqrstuvwzyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ"]; 2655 | terminal.writeln("\n" ~ getter.getline()); 2656 | terminal.writeln("\n" ~ getter.getline()); 2657 | terminal.writeln("\n" ~ getter.getline()); 2658 | getter.dispose(); 2659 | //*/ 2660 | 2661 | terminal.writeln(terminal.getline()); 2662 | terminal.writeln(terminal.getline()); 2663 | terminal.writeln(terminal.getline()); 2664 | 2665 | //input.getch(); 2666 | 2667 | // return; 2668 | // 2669 | 2670 | terminal.setTitle("Basic I/O"); 2671 | auto input = RealTimeConsoleInput(&terminal, ConsoleInputFlags.raw | ConsoleInputFlags.allInputEvents); 2672 | terminal.color(Color.green | Bright, Color.black); 2673 | 2674 | terminal.write("test some long string to see if it wraps or what because i dont really know what it is going to do so i just want to test i think it will wrap but gotta be sure lolololololololol"); 2675 | terminal.writefln("%d %d", terminal.cursorX, terminal.cursorY); 2676 | 2677 | int centerX = terminal.width / 2; 2678 | int centerY = terminal.height / 2; 2679 | 2680 | bool timeToBreak = false; 2681 | 2682 | void handleEvent(InputEvent event) { 2683 | terminal.writef("%s\n", event.type); 2684 | final switch(event.type) { 2685 | case InputEvent.Type.UserInterruptionEvent: 2686 | case InputEvent.Type.HangupEvent: 2687 | case InputEvent.Type.EndOfFileEvent: 2688 | timeToBreak = true; 2689 | version(with_eventloop) { 2690 | import arsd.eventloop; 2691 | exit(); 2692 | } 2693 | break; 2694 | case InputEvent.Type.SizeChangedEvent: 2695 | auto ev = event.get!(InputEvent.Type.SizeChangedEvent); 2696 | terminal.writeln(ev); 2697 | break; 2698 | case InputEvent.Type.KeyboardEvent: 2699 | auto ev = event.get!(InputEvent.Type.KeyboardEvent); 2700 | terminal.writef("\t%s", ev); 2701 | terminal.writef(" (%s)", cast(KeyboardEvent.Key) ev.which); 2702 | terminal.writeln(); 2703 | if(ev.which == 'Q') { 2704 | timeToBreak = true; 2705 | version(with_eventloop) { 2706 | import arsd.eventloop; 2707 | exit(); 2708 | } 2709 | } 2710 | 2711 | if(ev.which == 'C') 2712 | terminal.clear(); 2713 | break; 2714 | case InputEvent.Type.CharacterEvent: // obsolete 2715 | auto ev = event.get!(InputEvent.Type.CharacterEvent); 2716 | terminal.writef("\t%s\n", ev); 2717 | break; 2718 | case InputEvent.Type.NonCharacterKeyEvent: // obsolete 2719 | terminal.writef("\t%s\n", event.get!(InputEvent.Type.NonCharacterKeyEvent)); 2720 | break; 2721 | case InputEvent.Type.PasteEvent: 2722 | terminal.writef("\t%s\n", event.get!(InputEvent.Type.PasteEvent)); 2723 | break; 2724 | case InputEvent.Type.MouseEvent: 2725 | terminal.writef("\t%s\n", event.get!(InputEvent.Type.MouseEvent)); 2726 | break; 2727 | case InputEvent.Type.CustomEvent: 2728 | break; 2729 | } 2730 | 2731 | terminal.writefln("%d %d", terminal.cursorX, terminal.cursorY); 2732 | 2733 | /* 2734 | if(input.kbhit()) { 2735 | auto c = input.getch(); 2736 | if(c == 'q' || c == 'Q') 2737 | break; 2738 | terminal.moveTo(centerX, centerY); 2739 | terminal.writef("%c", c); 2740 | terminal.flush(); 2741 | } 2742 | usleep(10000); 2743 | */ 2744 | } 2745 | 2746 | version(with_eventloop) { 2747 | import arsd.eventloop; 2748 | addListener(&handleEvent); 2749 | loop(); 2750 | } else { 2751 | loop: while(true) { 2752 | auto event = input.nextEvent(); 2753 | handleEvent(event); 2754 | if(timeToBreak) 2755 | break loop; 2756 | } 2757 | } 2758 | } 2759 | 2760 | /** 2761 | FIXME: support lines that wrap 2762 | FIXME: better controls maybe 2763 | 2764 | FIXME: support multi-line "lines" and some form of line continuation, both 2765 | from the user (if permitted) and from the application, so like the user 2766 | hits "class foo { \n" and the app says "that line needs continuation" automatically. 2767 | 2768 | FIXME: fix lengths on prompt and suggestion 2769 | 2770 | A note on history: 2771 | 2772 | To save history, you must call LineGetter.dispose() when you're done with it. 2773 | History will not be automatically saved without that call! 2774 | 2775 | The history saving and loading as a trivially encountered race condition: if you 2776 | open two programs that use the same one at the same time, the one that closes second 2777 | will overwrite any history changes the first closer saved. 2778 | 2779 | GNU Getline does this too... and it actually kinda drives me nuts. But I don't know 2780 | what a good fix is except for doing a transactional commit straight to the file every 2781 | time and that seems like hitting the disk way too often. 2782 | 2783 | We could also do like a history server like a database daemon that keeps the order 2784 | correct but I don't actually like that either because I kinda like different bashes 2785 | to have different history, I just don't like it all to get lost. 2786 | 2787 | Regardless though, this isn't even used in bash anyway, so I don't think I care enough 2788 | to put that much effort into it. Just using separate files for separate tasks is good 2789 | enough I think. 2790 | */ 2791 | class LineGetter { 2792 | /* A note on the assumeSafeAppends in here: since these buffers are private, we can be 2793 | pretty sure that stomping isn't an issue, so I'm using this liberally to keep the 2794 | append/realloc code simple and hopefully reasonably fast. */ 2795 | 2796 | // saved to file 2797 | string[] history; 2798 | 2799 | // not saved 2800 | Terminal* terminal; 2801 | string historyFilename; 2802 | 2803 | /// Make sure that the parent terminal struct remains in scope for the duration 2804 | /// of LineGetter's lifetime, as it does hold on to and use the passed pointer 2805 | /// throughout. 2806 | /// 2807 | /// historyFilename will load and save an input history log to a particular folder. 2808 | /// Leaving it null will mean no file will be used and history will not be saved across sessions. 2809 | this(Terminal* tty, string historyFilename = null) { 2810 | this.terminal = tty; 2811 | this.historyFilename = historyFilename; 2812 | 2813 | line.reserve(128); 2814 | 2815 | if(historyFilename.length) 2816 | loadSettingsAndHistoryFromFile(); 2817 | 2818 | regularForeground = cast(Color) terminal._currentForeground; 2819 | background = cast(Color) terminal._currentBackground; 2820 | suggestionForeground = Color.blue; 2821 | } 2822 | 2823 | /// Call this before letting LineGetter die so it can do any necessary 2824 | /// cleanup and save the updated history to a file. 2825 | void dispose() { 2826 | if(historyFilename.length) 2827 | saveSettingsAndHistoryToFile(); 2828 | } 2829 | 2830 | /// Override this to change the directory where history files are stored 2831 | /// 2832 | /// Default is $HOME/.arsd-getline on linux and %APPDATA%/arsd-getline/ on Windows. 2833 | /* virtual */ string historyFileDirectory() { 2834 | version(Windows) { 2835 | char[1024] path; 2836 | // FIXME: this doesn't link because the crappy dmd lib doesn't have it 2837 | if(0) { // SHGetFolderPathA(null, CSIDL_APPDATA, null, 0, path.ptr) >= 0) { 2838 | import core.stdc.string; 2839 | return cast(string) path[0 .. strlen(path.ptr)] ~ "\\arsd-getline"; 2840 | } else { 2841 | import std.process; 2842 | return environment["APPDATA"] ~ "\\arsd-getline"; 2843 | } 2844 | } else version(Posix) { 2845 | import std.process; 2846 | return environment["HOME"] ~ "/.arsd-getline"; 2847 | } 2848 | } 2849 | 2850 | /// You can customize the colors here. You should set these after construction, but before 2851 | /// calling startGettingLine or getline. 2852 | Color suggestionForeground; 2853 | Color regularForeground; /// . 2854 | Color background; /// . 2855 | //bool reverseVideo; 2856 | 2857 | /// Set this if you want a prompt to be drawn with the line. It does NOT support color in string. 2858 | string prompt; 2859 | 2860 | /// Turn on auto suggest if you want a greyed thing of what tab 2861 | /// would be able to fill in as you type. 2862 | /// 2863 | /// You might want to turn it off if generating a completion list is slow. 2864 | bool autoSuggest = true; 2865 | 2866 | 2867 | /// Override this if you don't want all lines added to the history. 2868 | /// You can return null to not add it at all, or you can transform it. 2869 | /* virtual */ string historyFilter(string candidate) { 2870 | return candidate; 2871 | } 2872 | 2873 | /// You may override this to do nothing 2874 | /* virtual */ void saveSettingsAndHistoryToFile() { 2875 | import std.file; 2876 | if(!exists(historyFileDirectory)) 2877 | mkdir(historyFileDirectory); 2878 | auto fn = historyPath(); 2879 | import std.stdio; 2880 | auto file = File(fn, "wt"); 2881 | foreach(item; history) 2882 | file.writeln(item); 2883 | } 2884 | 2885 | private string historyPath() { 2886 | import std.path; 2887 | auto filename = historyFileDirectory() ~ dirSeparator ~ historyFilename ~ ".history"; 2888 | return filename; 2889 | } 2890 | 2891 | /// You may override this to do nothing 2892 | /* virtual */ void loadSettingsAndHistoryFromFile() { 2893 | import std.file; 2894 | history = null; 2895 | auto fn = historyPath(); 2896 | if(exists(fn)) { 2897 | import std.stdio; 2898 | foreach(line; File(fn, "rt").byLine) 2899 | history ~= line.idup; 2900 | 2901 | } 2902 | } 2903 | 2904 | /** 2905 | Override this to provide tab completion. You may use the candidate 2906 | argument to filter the list, but you don't have to (LineGetter will 2907 | do it for you on the values you return). 2908 | 2909 | Ideally, you wouldn't return more than about ten items since the list 2910 | gets difficult to use if it is too long. 2911 | 2912 | Default is to provide recent command history as autocomplete. 2913 | */ 2914 | /* virtual */ protected string[] tabComplete(in dchar[] candidate) { 2915 | return history.length > 20 ? history[0 .. 20] : history; 2916 | } 2917 | 2918 | private string[] filterTabCompleteList(string[] list) { 2919 | if(list.length == 0) 2920 | return list; 2921 | 2922 | string[] f; 2923 | f.reserve(list.length); 2924 | 2925 | foreach(item; list) { 2926 | import std.algorithm; 2927 | if(startsWith(item, line[0 .. cursorPosition])) 2928 | f ~= item; 2929 | } 2930 | 2931 | return f; 2932 | } 2933 | 2934 | /// Override this to provide a custom display of the tab completion list 2935 | protected void showTabCompleteList(string[] list) { 2936 | if(list.length) { 2937 | // FIXME: allow mouse clicking of an item, that would be cool 2938 | 2939 | // FIXME: scroll 2940 | //if(terminal.type == ConsoleOutputType.linear) { 2941 | terminal.writeln(); 2942 | foreach(item; list) { 2943 | terminal.color(suggestionForeground, background); 2944 | import std.utf; 2945 | auto idx = codeLength!char(line[0 .. cursorPosition]); 2946 | terminal.write(" ", item[0 .. idx]); 2947 | terminal.color(regularForeground, background); 2948 | terminal.writeln(item[idx .. $]); 2949 | } 2950 | updateCursorPosition(); 2951 | redraw(); 2952 | //} 2953 | } 2954 | } 2955 | 2956 | /// One-call shop for the main workhorse 2957 | /// If you already have a RealTimeConsoleInput ready to go, you 2958 | /// should pass a pointer to yours here. Otherwise, LineGetter will 2959 | /// make its own. 2960 | public string getline(RealTimeConsoleInput* input = null) { 2961 | startGettingLine(); 2962 | if(input is null) { 2963 | auto i = RealTimeConsoleInput(terminal, ConsoleInputFlags.raw | ConsoleInputFlags.allInputEvents); 2964 | while(workOnLine(i.nextEvent())) {} 2965 | } else 2966 | while(workOnLine(input.nextEvent())) {} 2967 | return finishGettingLine(); 2968 | } 2969 | 2970 | private int currentHistoryViewPosition = 0; 2971 | private dchar[] uncommittedHistoryCandidate; 2972 | void loadFromHistory(int howFarBack) { 2973 | if(howFarBack < 0) 2974 | howFarBack = 0; 2975 | if(howFarBack > history.length) // lol signed/unsigned comparison here means if i did this first, before howFarBack < 0, it would totally cycle around. 2976 | howFarBack = cast(int) history.length; 2977 | if(howFarBack == currentHistoryViewPosition) 2978 | return; 2979 | if(currentHistoryViewPosition == 0) { 2980 | // save the current line so we can down arrow back to it later 2981 | if(uncommittedHistoryCandidate.length < line.length) { 2982 | uncommittedHistoryCandidate.length = line.length; 2983 | } 2984 | 2985 | uncommittedHistoryCandidate[0 .. line.length] = line[]; 2986 | uncommittedHistoryCandidate = uncommittedHistoryCandidate[0 .. line.length]; 2987 | uncommittedHistoryCandidate.assumeSafeAppend(); 2988 | } 2989 | 2990 | currentHistoryViewPosition = howFarBack; 2991 | 2992 | if(howFarBack == 0) { 2993 | line.length = uncommittedHistoryCandidate.length; 2994 | line.assumeSafeAppend(); 2995 | line[] = uncommittedHistoryCandidate[]; 2996 | } else { 2997 | line = line[0 .. 0]; 2998 | line.assumeSafeAppend(); 2999 | foreach(dchar ch; history[$ - howFarBack]) 3000 | line ~= ch; 3001 | } 3002 | 3003 | cursorPosition = cast(int) line.length; 3004 | scrollToEnd(); 3005 | } 3006 | 3007 | bool insertMode = true; 3008 | bool multiLineMode = false; 3009 | 3010 | private dchar[] line; 3011 | private int cursorPosition = 0; 3012 | private int horizontalScrollPosition = 0; 3013 | 3014 | private void scrollToEnd() { 3015 | horizontalScrollPosition = (cast(int) line.length); 3016 | horizontalScrollPosition -= availableLineLength(); 3017 | if(horizontalScrollPosition < 0) 3018 | horizontalScrollPosition = 0; 3019 | } 3020 | 3021 | // used for redrawing the line in the right place 3022 | // and detecting mouse events on our line. 3023 | private int startOfLineX; 3024 | private int startOfLineY; 3025 | 3026 | // private string[] cachedCompletionList; 3027 | 3028 | // FIXME 3029 | // /// Note that this assumes the tab complete list won't change between actual 3030 | // /// presses of tab by the user. If you pass it a list, it will use it, but 3031 | // /// otherwise it will keep track of the last one to avoid calls to tabComplete. 3032 | private string suggestion(string[] list = null) { 3033 | import std.algorithm, std.utf; 3034 | auto relevantLineSection = line[0 .. cursorPosition]; 3035 | // FIXME: see about caching the list if we easily can 3036 | if(list is null) 3037 | list = filterTabCompleteList(tabComplete(relevantLineSection)); 3038 | 3039 | if(list.length) { 3040 | string commonality = list[0]; 3041 | foreach(item; list[1 .. $]) { 3042 | commonality = commonPrefix(commonality, item); 3043 | } 3044 | 3045 | if(commonality.length) { 3046 | return commonality[codeLength!char(relevantLineSection) .. $]; 3047 | } 3048 | } 3049 | 3050 | return null; 3051 | } 3052 | 3053 | /// Adds a character at the current position in the line. You can call this too if you hook events for hotkeys or something. 3054 | /// You'll probably want to call redraw() after adding chars. 3055 | void addChar(dchar ch) { 3056 | assert(cursorPosition >= 0 && cursorPosition <= line.length); 3057 | if(cursorPosition == line.length) 3058 | line ~= ch; 3059 | else { 3060 | assert(line.length); 3061 | if(insertMode) { 3062 | line ~= ' '; 3063 | for(int i = cast(int) line.length - 2; i >= cursorPosition; i --) 3064 | line[i + 1] = line[i]; 3065 | } 3066 | line[cursorPosition] = ch; 3067 | } 3068 | cursorPosition++; 3069 | 3070 | if(cursorPosition >= horizontalScrollPosition + availableLineLength()) 3071 | horizontalScrollPosition++; 3072 | } 3073 | 3074 | /// . 3075 | void addString(string s) { 3076 | // FIXME: this could be more efficient 3077 | // but does it matter? these lines aren't super long anyway. But then again a paste could be excessively long (prolly accidental, but still) 3078 | foreach(dchar ch; s) 3079 | addChar(ch); 3080 | } 3081 | 3082 | /// Deletes the character at the current position in the line. 3083 | /// You'll probably want to call redraw() after deleting chars. 3084 | void deleteChar() { 3085 | if(cursorPosition == line.length) 3086 | return; 3087 | for(int i = cursorPosition; i < line.length - 1; i++) 3088 | line[i] = line[i + 1]; 3089 | line = line[0 .. $-1]; 3090 | line.assumeSafeAppend(); 3091 | } 3092 | 3093 | /// 3094 | void deleteToEndOfLine() { 3095 | while(cursorPosition < line.length) 3096 | deleteChar(); 3097 | } 3098 | 3099 | int availableLineLength() { 3100 | return terminal.width - startOfLineX - cast(int) prompt.length - 1; 3101 | } 3102 | 3103 | private int lastDrawLength = 0; 3104 | void redraw() { 3105 | terminal.moveTo(startOfLineX, startOfLineY); 3106 | 3107 | auto lineLength = availableLineLength(); 3108 | if(lineLength < 0) 3109 | throw new Exception("too narrow terminal to draw"); 3110 | 3111 | terminal.write(prompt); 3112 | 3113 | auto towrite = line[horizontalScrollPosition .. $]; 3114 | auto cursorPositionToDrawX = cursorPosition - horizontalScrollPosition; 3115 | auto cursorPositionToDrawY = 0; 3116 | 3117 | if(towrite.length > lineLength) { 3118 | towrite = towrite[0 .. lineLength]; 3119 | } 3120 | 3121 | terminal.write(towrite); 3122 | 3123 | lineLength -= towrite.length; 3124 | 3125 | string suggestion; 3126 | 3127 | if(lineLength >= 0) { 3128 | suggestion = ((cursorPosition == towrite.length) && autoSuggest) ? this.suggestion() : null; 3129 | if(suggestion.length) { 3130 | terminal.color(suggestionForeground, background); 3131 | terminal.write(suggestion); 3132 | terminal.color(regularForeground, background); 3133 | } 3134 | } 3135 | 3136 | // FIXME: graphemes and utf-8 on suggestion/prompt 3137 | auto written = cast(int) (towrite.length + suggestion.length + prompt.length); 3138 | 3139 | if(written < lastDrawLength) 3140 | foreach(i; written .. lastDrawLength) 3141 | terminal.write(" "); 3142 | lastDrawLength = written; 3143 | 3144 | terminal.moveTo(startOfLineX + cursorPositionToDrawX + cast(int) prompt.length, startOfLineY + cursorPositionToDrawY); 3145 | } 3146 | 3147 | /// Starts getting a new line. Call workOnLine and finishGettingLine afterward. 3148 | /// 3149 | /// Make sure that you've flushed your input and output before calling this 3150 | /// function or else you might lose events or get exceptions from this. 3151 | void startGettingLine() { 3152 | // reset from any previous call first 3153 | cursorPosition = 0; 3154 | horizontalScrollPosition = 0; 3155 | justHitTab = false; 3156 | currentHistoryViewPosition = 0; 3157 | if(line.length) { 3158 | line = line[0 .. 0]; 3159 | line.assumeSafeAppend(); 3160 | } 3161 | 3162 | updateCursorPosition(); 3163 | terminal.showCursor(); 3164 | 3165 | lastDrawLength = availableLineLength(); 3166 | redraw(); 3167 | } 3168 | 3169 | private void updateCursorPosition() { 3170 | terminal.flush(); 3171 | 3172 | // then get the current cursor position to start fresh 3173 | version(Windows) { 3174 | CONSOLE_SCREEN_BUFFER_INFO info; 3175 | GetConsoleScreenBufferInfo(terminal.hConsole, &info); 3176 | startOfLineX = info.dwCursorPosition.X; 3177 | startOfLineY = info.dwCursorPosition.Y; 3178 | } else { 3179 | // request current cursor position 3180 | 3181 | // we have to turn off cooked mode to get this answer, otherwise it will all 3182 | // be messed up. (I hate unix terminals, the Windows way is so much easer.) 3183 | 3184 | // We also can't use RealTimeConsoleInput here because it also does event loop stuff 3185 | // which would be broken by the child destructor :( (maybe that should be a FIXME) 3186 | 3187 | ubyte[128] hack2; 3188 | termios old; 3189 | ubyte[128] hack; 3190 | tcgetattr(terminal.fdIn, &old); 3191 | auto n = old; 3192 | n.c_lflag &= ~(ICANON | ECHO); 3193 | tcsetattr(terminal.fdIn, TCSANOW, &n); 3194 | scope(exit) 3195 | tcsetattr(terminal.fdIn, TCSANOW, &old); 3196 | 3197 | 3198 | terminal.writeStringRaw("\033[6n"); 3199 | terminal.flush(); 3200 | 3201 | import core.sys.posix.unistd; 3202 | // reading directly to bypass any buffering 3203 | ubyte[16] buffer; 3204 | auto len = read(terminal.fdIn, buffer.ptr, buffer.length); 3205 | if(len <= 0) 3206 | throw new Exception("Couldn't get cursor position to initialize get line"); 3207 | auto got = buffer[0 .. len]; 3208 | if(got.length < 6) 3209 | throw new Exception("not enough cursor reply answer"); 3210 | if(got[0] != '\033' || got[1] != '[' || got[$-1] != 'R') 3211 | throw new Exception("wrong answer for cursor position"); 3212 | auto gots = cast(char[]) got[2 .. $-1]; 3213 | 3214 | import std.conv; 3215 | import std.string; 3216 | 3217 | auto pieces = split(gots, ";"); 3218 | if(pieces.length != 2) throw new Exception("wtf wrong answer on cursor position"); 3219 | 3220 | startOfLineX = to!int(pieces[1]) - 1; 3221 | startOfLineY = to!int(pieces[0]) - 1; 3222 | } 3223 | 3224 | // updating these too because I can with the more accurate info from above 3225 | terminal._cursorX = startOfLineX; 3226 | terminal._cursorY = startOfLineY; 3227 | } 3228 | 3229 | private bool justHitTab; 3230 | 3231 | /// for integrating into another event loop 3232 | /// you can pass individual events to this and 3233 | /// the line getter will work on it 3234 | /// 3235 | /// returns false when there's nothing more to do 3236 | bool workOnLine(InputEvent e) { 3237 | switch(e.type) { 3238 | case InputEvent.Type.EndOfFileEvent: 3239 | justHitTab = false; 3240 | // FIXME: this should be distinct from an empty line when hit at the beginning 3241 | return false; 3242 | //break; 3243 | case InputEvent.Type.KeyboardEvent: 3244 | auto ev = e.keyboardEvent; 3245 | if(ev.pressed == false) 3246 | return true; 3247 | /* Insert the character (unless it is backspace, tab, or some other control char) */ 3248 | auto ch = ev.which; 3249 | switch(ch) { 3250 | case 4: // ctrl+d will also send a newline-equivalent 3251 | case '\r': 3252 | case '\n': 3253 | justHitTab = false; 3254 | return false; 3255 | case '\t': 3256 | auto relevantLineSection = line[0 .. cursorPosition]; 3257 | auto possibilities = filterTabCompleteList(tabComplete(relevantLineSection)); 3258 | import std.utf; 3259 | 3260 | if(possibilities.length == 1) { 3261 | auto toFill = possibilities[0][codeLength!char(relevantLineSection) .. $]; 3262 | if(toFill.length) { 3263 | addString(toFill); 3264 | redraw(); 3265 | } 3266 | justHitTab = false; 3267 | } else { 3268 | if(justHitTab) { 3269 | justHitTab = false; 3270 | showTabCompleteList(possibilities); 3271 | } else { 3272 | justHitTab = true; 3273 | /* fill it in with as much commonality as there is amongst all the suggestions */ 3274 | auto suggestion = this.suggestion(possibilities); 3275 | if(suggestion.length) { 3276 | addString(suggestion); 3277 | redraw(); 3278 | } 3279 | } 3280 | } 3281 | break; 3282 | case '\b': 3283 | justHitTab = false; 3284 | if(cursorPosition) { 3285 | cursorPosition--; 3286 | for(int i = cursorPosition; i < line.length - 1; i++) 3287 | line[i] = line[i + 1]; 3288 | line = line[0 .. $ - 1]; 3289 | line.assumeSafeAppend(); 3290 | 3291 | if(!multiLineMode) { 3292 | if(horizontalScrollPosition > cursorPosition - 1) 3293 | horizontalScrollPosition = cursorPosition - 1 - availableLineLength(); 3294 | if(horizontalScrollPosition < 0) 3295 | horizontalScrollPosition = 0; 3296 | } 3297 | 3298 | redraw(); 3299 | } 3300 | break; 3301 | case KeyboardEvent.Key.LeftArrow: 3302 | justHitTab = false; 3303 | if(cursorPosition) 3304 | cursorPosition--; 3305 | if(!multiLineMode) { 3306 | if(cursorPosition < horizontalScrollPosition) 3307 | horizontalScrollPosition--; 3308 | } 3309 | 3310 | redraw(); 3311 | break; 3312 | case KeyboardEvent.Key.RightArrow: 3313 | justHitTab = false; 3314 | if(cursorPosition < line.length) 3315 | cursorPosition++; 3316 | if(!multiLineMode) { 3317 | if(cursorPosition >= horizontalScrollPosition + availableLineLength()) 3318 | horizontalScrollPosition++; 3319 | } 3320 | 3321 | redraw(); 3322 | break; 3323 | case KeyboardEvent.Key.UpArrow: 3324 | justHitTab = false; 3325 | loadFromHistory(currentHistoryViewPosition + 1); 3326 | redraw(); 3327 | break; 3328 | case KeyboardEvent.Key.DownArrow: 3329 | justHitTab = false; 3330 | loadFromHistory(currentHistoryViewPosition - 1); 3331 | redraw(); 3332 | break; 3333 | case KeyboardEvent.Key.PageUp: 3334 | justHitTab = false; 3335 | loadFromHistory(cast(int) history.length); 3336 | redraw(); 3337 | break; 3338 | case KeyboardEvent.Key.PageDown: 3339 | justHitTab = false; 3340 | loadFromHistory(0); 3341 | redraw(); 3342 | break; 3343 | case 1: // ctrl+a does home too in the emacs keybindings 3344 | case KeyboardEvent.Key.Home: 3345 | justHitTab = false; 3346 | cursorPosition = 0; 3347 | horizontalScrollPosition = 0; 3348 | redraw(); 3349 | break; 3350 | case 5: // ctrl+e from emacs 3351 | case KeyboardEvent.Key.End: 3352 | justHitTab = false; 3353 | cursorPosition = cast(int) line.length; 3354 | scrollToEnd(); 3355 | redraw(); 3356 | break; 3357 | case KeyboardEvent.Key.Insert: 3358 | justHitTab = false; 3359 | insertMode = !insertMode; 3360 | // FIXME: indicate this on the UI somehow 3361 | // like change the cursor or something 3362 | break; 3363 | case KeyboardEvent.Key.Delete: 3364 | justHitTab = false; 3365 | if(ev.modifierState & ModifierState.control) 3366 | deleteToEndOfLine(); 3367 | else 3368 | deleteChar(); 3369 | redraw(); 3370 | break; 3371 | case 11: // ctrl+k is delete to end of line from emacs 3372 | justHitTab = false; 3373 | deleteToEndOfLine(); 3374 | redraw(); 3375 | break; 3376 | default: 3377 | justHitTab = false; 3378 | if(e.keyboardEvent.isCharacter) 3379 | addChar(ch); 3380 | redraw(); 3381 | } 3382 | break; 3383 | case InputEvent.Type.PasteEvent: 3384 | justHitTab = false; 3385 | addString(e.pasteEvent.pastedText); 3386 | redraw(); 3387 | break; 3388 | case InputEvent.Type.MouseEvent: 3389 | /* Clicking with the mouse to move the cursor is so much easier than arrowing 3390 | or even emacs/vi style movements much of the time, so I'ma support it. */ 3391 | 3392 | auto me = e.mouseEvent; 3393 | if(me.eventType == MouseEvent.Type.Pressed) { 3394 | if(me.buttons & MouseEvent.Button.Left) { 3395 | if(me.y == startOfLineY) { 3396 | // FIXME: prompt.length should be graphemes or at least code poitns 3397 | int p = me.x - startOfLineX - cast(int) prompt.length + horizontalScrollPosition; 3398 | if(p >= 0 && p < line.length) { 3399 | justHitTab = false; 3400 | cursorPosition = p; 3401 | redraw(); 3402 | } 3403 | } 3404 | } 3405 | } 3406 | break; 3407 | case InputEvent.Type.SizeChangedEvent: 3408 | /* We'll adjust the bounding box. If you don't like this, handle SizeChangedEvent 3409 | yourself and then don't pass it to this function. */ 3410 | // FIXME 3411 | break; 3412 | case InputEvent.Type.UserInterruptionEvent: 3413 | /* I'll take this as canceling the line. */ 3414 | throw new UserInterruptionException(); 3415 | //break; 3416 | case InputEvent.Type.HangupEvent: 3417 | /* I'll take this as canceling the line. */ 3418 | throw new HangupException(); 3419 | //break; 3420 | default: 3421 | /* ignore. ideally it wouldn't be passed to us anyway! */ 3422 | } 3423 | 3424 | return true; 3425 | } 3426 | 3427 | string finishGettingLine() { 3428 | import std.conv; 3429 | auto f = to!string(line); 3430 | auto history = historyFilter(f); 3431 | if(history !is null) 3432 | this.history ~= history; 3433 | 3434 | // FIXME: we should hide the cursor if it was hidden in the call to startGettingLine 3435 | return f; 3436 | } 3437 | } 3438 | 3439 | /// Adds default constructors that just forward to the superclass 3440 | mixin template LineGetterConstructors() { 3441 | this(Terminal* tty, string historyFilename = null) { 3442 | super(tty, historyFilename); 3443 | } 3444 | } 3445 | 3446 | /// This is a line getter that customizes the tab completion to 3447 | /// fill in file names separated by spaces, like a command line thing. 3448 | class FileLineGetter : LineGetter { 3449 | mixin LineGetterConstructors; 3450 | 3451 | /// You can set this property to tell it where to search for the files 3452 | /// to complete. 3453 | string searchDirectory = "."; 3454 | 3455 | override protected string[] tabComplete(in dchar[] candidate) { 3456 | import std.file, std.conv, std.algorithm, std.string; 3457 | const(dchar)[] soFar = candidate; 3458 | auto idx = candidate.lastIndexOf(" "); 3459 | if(idx != -1) 3460 | soFar = candidate[idx + 1 .. $]; 3461 | 3462 | string[] list; 3463 | foreach(string name; dirEntries(searchDirectory, SpanMode.breadth)) { 3464 | // try without the ./ 3465 | if(startsWith(name[2..$], soFar)) 3466 | list ~= text(candidate, name[searchDirectory.length + 1 + soFar.length .. $]); 3467 | else // and with 3468 | if(startsWith(name, soFar)) 3469 | list ~= text(candidate, name[soFar.length .. $]); 3470 | } 3471 | 3472 | return list; 3473 | } 3474 | } 3475 | 3476 | version(Windows) { 3477 | // to get the directory for saving history in the line things 3478 | enum CSIDL_APPDATA = 26; 3479 | extern(Windows) HRESULT SHGetFolderPathA(HWND, int, HANDLE, DWORD, LPSTR); 3480 | } 3481 | 3482 | 3483 | 3484 | 3485 | 3486 | /* Like getting a line, printing a lot of lines is kinda important too, so I'm including 3487 | that widget here too. */ 3488 | 3489 | 3490 | struct ScrollbackBuffer { 3491 | 3492 | bool demandsAttention; 3493 | 3494 | this(string name) { 3495 | this.name = name; 3496 | } 3497 | 3498 | void write(T...)(T t) { 3499 | import std.conv : text; 3500 | addComponent(text(t), foreground_, background_, null); 3501 | } 3502 | 3503 | void writeln(T...)(T t) { 3504 | write(t, "\n"); 3505 | } 3506 | 3507 | void writef(T...)(string fmt, T t) { 3508 | import std.format: format; 3509 | write(format(fmt, t)); 3510 | } 3511 | 3512 | void writefln(T...)(string fmt, T t) { 3513 | writef(fmt, t, "\n"); 3514 | } 3515 | 3516 | void clear() { 3517 | lines = null; 3518 | clickRegions = null; 3519 | scrollbackPosition = 0; 3520 | } 3521 | 3522 | int foreground_ = Color.DEFAULT, background_ = Color.DEFAULT; 3523 | void color(int foreground, int background) { 3524 | this.foreground_ = foreground; 3525 | this.background_ = background; 3526 | } 3527 | 3528 | void addComponent(string text, int foreground, int background, bool delegate() onclick) { 3529 | if(lines.length == 0) { 3530 | addLine(); 3531 | } 3532 | bool first = true; 3533 | import std.algorithm; 3534 | foreach(t; splitter(text, "\n")) { 3535 | if(!first) addLine(); 3536 | first = false; 3537 | lines[$-1].components ~= LineComponent(t, foreground, background, onclick); 3538 | } 3539 | } 3540 | 3541 | void addLine() { 3542 | lines ~= Line(); 3543 | if(scrollbackPosition) // if the user is scrolling back, we want to keep them basically centered where they are 3544 | scrollbackPosition++; 3545 | } 3546 | 3547 | void addLine(string line) { 3548 | lines ~= Line([LineComponent(line)]); 3549 | if(scrollbackPosition) // if the user is scrolling back, we want to keep them basically centered where they are 3550 | scrollbackPosition++; 3551 | } 3552 | 3553 | void scrollUp(int lines = 1) { 3554 | scrollbackPosition += lines; 3555 | //if(scrollbackPosition >= this.lines.length) 3556 | // scrollbackPosition = cast(int) this.lines.length - 1; 3557 | } 3558 | 3559 | void scrollDown(int lines = 1) { 3560 | scrollbackPosition -= lines; 3561 | if(scrollbackPosition < 0) 3562 | scrollbackPosition = 0; 3563 | } 3564 | 3565 | void scrollToBottom() { 3566 | scrollbackPosition = 0; 3567 | } 3568 | 3569 | // this needs width and height to know how to word wrap it 3570 | void scrollToTop(int width, int height) { 3571 | scrollbackPosition = scrollTopPosition(width, height); 3572 | } 3573 | 3574 | 3575 | 3576 | 3577 | struct LineComponent { 3578 | string text; 3579 | bool isRgb; 3580 | union { 3581 | int color; 3582 | RGB colorRgb; 3583 | } 3584 | union { 3585 | int background; 3586 | RGB backgroundRgb; 3587 | } 3588 | bool delegate() onclick; // return true if you need to redraw 3589 | 3590 | // 16 color ctor 3591 | this(string text, int color = Color.DEFAULT, int background = Color.DEFAULT, bool delegate() onclick = null) { 3592 | this.text = text; 3593 | this.color = color; 3594 | this.background = background; 3595 | this.onclick = onclick; 3596 | this.isRgb = false; 3597 | } 3598 | 3599 | // true color ctor 3600 | this(string text, RGB colorRgb, RGB backgroundRgb = RGB(0, 0, 0), bool delegate() onclick = null) { 3601 | this.text = text; 3602 | this.colorRgb = colorRgb; 3603 | this.backgroundRgb = backgroundRgb; 3604 | this.onclick = onclick; 3605 | this.isRgb = true; 3606 | } 3607 | } 3608 | 3609 | struct Line { 3610 | LineComponent[] components; 3611 | int length() { 3612 | int l = 0; 3613 | foreach(c; components) 3614 | l += c.text.length; 3615 | return l; 3616 | } 3617 | } 3618 | 3619 | // FIXME: limit scrollback lines.length 3620 | 3621 | Line[] lines; 3622 | string name; 3623 | 3624 | int x, y, width, height; 3625 | 3626 | int scrollbackPosition; 3627 | 3628 | 3629 | int scrollTopPosition(int width, int height) { 3630 | int lineCount; 3631 | 3632 | foreach_reverse(line; lines) { 3633 | int written = 0; 3634 | comp_loop: foreach(cidx, component; line.components) { 3635 | auto towrite = component.text; 3636 | foreach(idx, dchar ch; towrite) { 3637 | if(written >= width) { 3638 | lineCount++; 3639 | written = 0; 3640 | } 3641 | 3642 | if(ch == '\t') 3643 | written += 8; // FIXME 3644 | else 3645 | written++; 3646 | } 3647 | } 3648 | lineCount++; 3649 | } 3650 | 3651 | //if(lineCount > height) 3652 | return lineCount - height; 3653 | //return 0; 3654 | } 3655 | 3656 | void drawInto(Terminal* terminal, in int x = 0, in int y = 0, int width = 0, int height = 0) { 3657 | if(lines.length == 0) 3658 | return; 3659 | 3660 | if(width == 0) 3661 | width = terminal.width; 3662 | if(height == 0) 3663 | height = terminal.height; 3664 | 3665 | this.x = x; 3666 | this.y = y; 3667 | this.width = width; 3668 | this.height = height; 3669 | 3670 | /* We need to figure out how much is going to fit 3671 | in a first pass, so we can figure out where to 3672 | start drawing */ 3673 | 3674 | int remaining = height + scrollbackPosition; 3675 | int start = cast(int) lines.length; 3676 | int howMany = 0; 3677 | 3678 | bool firstPartial = false; 3679 | 3680 | static struct Idx { 3681 | size_t cidx; 3682 | size_t idx; 3683 | } 3684 | 3685 | Idx firstPartialStartIndex; 3686 | 3687 | // this is private so I know we can safe append 3688 | clickRegions.length = 0; 3689 | clickRegions.assumeSafeAppend(); 3690 | 3691 | // FIXME: should prolly handle \n and \r in here too. 3692 | 3693 | // we'll work backwards to figure out how much will fit... 3694 | // this will give accurate per-line things even with changing width and wrapping 3695 | // while being generally efficient - we usually want to show the end of the list 3696 | // anyway; actually using the scrollback is a bit of an exceptional case. 3697 | 3698 | // It could probably do this instead of on each redraw, on each resize or insertion. 3699 | // or at least cache between redraws until one of those invalidates it. 3700 | foreach_reverse(line; lines) { 3701 | int written = 0; 3702 | int brokenLineCount; 3703 | Idx[16] lineBreaksBuffer; 3704 | Idx[] lineBreaks = lineBreaksBuffer[]; 3705 | comp_loop: foreach(cidx, component; line.components) { 3706 | auto towrite = component.text; 3707 | foreach(idx, dchar ch; towrite) { 3708 | if(written >= width) { 3709 | if(brokenLineCount == lineBreaks.length) 3710 | lineBreaks ~= Idx(cidx, idx); 3711 | else 3712 | lineBreaks[brokenLineCount] = Idx(cidx, idx); 3713 | 3714 | brokenLineCount++; 3715 | 3716 | written = 0; 3717 | } 3718 | 3719 | if(ch == '\t') 3720 | written += 8; // FIXME 3721 | else 3722 | written++; 3723 | } 3724 | } 3725 | 3726 | lineBreaks = lineBreaks[0 .. brokenLineCount]; 3727 | 3728 | foreach_reverse(lineBreak; lineBreaks) { 3729 | if(remaining == 1) { 3730 | firstPartial = true; 3731 | firstPartialStartIndex = lineBreak; 3732 | break; 3733 | } else { 3734 | remaining--; 3735 | } 3736 | if(remaining <= 0) 3737 | break; 3738 | } 3739 | 3740 | remaining--; 3741 | 3742 | start--; 3743 | howMany++; 3744 | if(remaining <= 0) 3745 | break; 3746 | } 3747 | 3748 | // second pass: actually draw it 3749 | int linePos = remaining; 3750 | 3751 | foreach(idx, line; lines[start .. start + howMany]) { 3752 | int written = 0; 3753 | 3754 | if(linePos < 0) { 3755 | linePos++; 3756 | continue; 3757 | } 3758 | 3759 | terminal.moveTo(x, y + ((linePos >= 0) ? linePos : 0)); 3760 | 3761 | auto todo = line.components; 3762 | 3763 | if(firstPartial) { 3764 | todo = todo[firstPartialStartIndex.cidx .. $]; 3765 | } 3766 | 3767 | foreach(ref component; todo) { 3768 | if(component.isRgb) 3769 | terminal.setTrueColor(component.colorRgb, component.backgroundRgb); 3770 | else 3771 | terminal.color(component.color, component.background); 3772 | auto towrite = component.text; 3773 | 3774 | again: 3775 | 3776 | if(linePos >= height) 3777 | break; 3778 | 3779 | if(firstPartial) { 3780 | towrite = towrite[firstPartialStartIndex.idx .. $]; 3781 | firstPartial = false; 3782 | } 3783 | 3784 | foreach(idx, dchar ch; towrite) { 3785 | if(written >= width) { 3786 | clickRegions ~= ClickRegion(&component, terminal.cursorX, terminal.cursorY, written); 3787 | terminal.write(towrite[0 .. idx]); 3788 | towrite = towrite[idx .. $]; 3789 | linePos++; 3790 | written = 0; 3791 | terminal.moveTo(x, y + linePos); 3792 | goto again; 3793 | } 3794 | 3795 | if(ch == '\t') 3796 | written += 8; // FIXME 3797 | else 3798 | written++; 3799 | } 3800 | 3801 | if(towrite.length) { 3802 | clickRegions ~= ClickRegion(&component, terminal.cursorX, terminal.cursorY, written); 3803 | terminal.write(towrite); 3804 | } 3805 | } 3806 | 3807 | if(written < width) { 3808 | terminal.color(Color.DEFAULT, Color.DEFAULT); 3809 | foreach(i; written .. width) 3810 | terminal.write(" "); 3811 | } 3812 | 3813 | linePos++; 3814 | 3815 | if(linePos >= height) 3816 | break; 3817 | } 3818 | 3819 | if(linePos < height) { 3820 | terminal.color(Color.DEFAULT, Color.DEFAULT); 3821 | foreach(i; linePos .. height) { 3822 | if(i >= 0 && i < height) { 3823 | terminal.moveTo(x, y + i); 3824 | foreach(w; 0 .. width) 3825 | terminal.write(" "); 3826 | } 3827 | } 3828 | } 3829 | } 3830 | 3831 | private struct ClickRegion { 3832 | LineComponent* component; 3833 | int xStart; 3834 | int yStart; 3835 | int length; 3836 | } 3837 | private ClickRegion[] clickRegions; 3838 | 3839 | /// Default event handling for this widget. Call this only after drawing it into a rectangle 3840 | /// and only if the event ought to be dispatched to it (which you determine however you want; 3841 | /// you could dispatch all events to it, or perhaps filter some out too) 3842 | /// 3843 | /// Returns true if it should be redrawn 3844 | bool handleEvent(InputEvent e) { 3845 | final switch(e.type) { 3846 | case InputEvent.Type.KeyboardEvent: 3847 | auto ev = e.keyboardEvent; 3848 | 3849 | demandsAttention = false; 3850 | 3851 | switch(ev.which) { 3852 | case KeyboardEvent.Key.UpArrow: 3853 | scrollUp(); 3854 | return true; 3855 | case KeyboardEvent.Key.DownArrow: 3856 | scrollDown(); 3857 | return true; 3858 | case KeyboardEvent.Key.PageUp: 3859 | scrollUp(height); 3860 | return true; 3861 | case KeyboardEvent.Key.PageDown: 3862 | scrollDown(height); 3863 | return true; 3864 | default: 3865 | // ignore 3866 | } 3867 | break; 3868 | case InputEvent.Type.MouseEvent: 3869 | auto ev = e.mouseEvent; 3870 | if(ev.x >= x && ev.x < x + width && ev.y >= y && ev.y < y + height) { 3871 | demandsAttention = false; 3872 | // it is inside our box, so do something with it 3873 | auto mx = ev.x - x; 3874 | auto my = ev.y - y; 3875 | 3876 | if(ev.eventType == MouseEvent.Type.Pressed) { 3877 | if(ev.buttons & MouseEvent.Button.Left) { 3878 | foreach(region; clickRegions) 3879 | if(ev.x >= region.xStart && ev.x < region.xStart + region.length && ev.y == region.yStart) 3880 | if(region.component.onclick !is null) 3881 | return region.component.onclick(); 3882 | } 3883 | if(ev.buttons & MouseEvent.Button.ScrollUp) { 3884 | scrollUp(); 3885 | return true; 3886 | } 3887 | if(ev.buttons & MouseEvent.Button.ScrollDown) { 3888 | scrollDown(); 3889 | return true; 3890 | } 3891 | } 3892 | } else { 3893 | // outside our area, free to ignore 3894 | } 3895 | break; 3896 | case InputEvent.Type.SizeChangedEvent: 3897 | // (size changed might be but it needs to be handled at a higher level really anyway) 3898 | // though it will return true because it probably needs redrawing anyway. 3899 | return true; 3900 | case InputEvent.Type.UserInterruptionEvent: 3901 | throw new UserInterruptionException(); 3902 | case InputEvent.Type.HangupEvent: 3903 | throw new HangupException(); 3904 | case InputEvent.Type.EndOfFileEvent: 3905 | // ignore, not relevant to this 3906 | break; 3907 | case InputEvent.Type.CharacterEvent: 3908 | case InputEvent.Type.NonCharacterKeyEvent: 3909 | // obsolete, ignore them until they are removed 3910 | break; 3911 | case InputEvent.Type.CustomEvent: 3912 | case InputEvent.Type.PasteEvent: 3913 | // ignored, not relevant to us 3914 | break; 3915 | } 3916 | 3917 | return false; 3918 | } 3919 | } 3920 | 3921 | 3922 | class UserInterruptionException : Exception { 3923 | this() { super("Ctrl+C"); } 3924 | } 3925 | class HangupException : Exception { 3926 | this() { super("Hup"); } 3927 | } 3928 | 3929 | 3930 | 3931 | /* 3932 | 3933 | // more efficient scrolling 3934 | http://msdn.microsoft.com/en-us/library/windows/desktop/ms685113%28v=vs.85%29.aspx 3935 | // and the unix sequences 3936 | 3937 | 3938 | rxvt documentation: 3939 | use this to finish the input magic for that 3940 | 3941 | 3942 | For the keypad, use Shift to temporarily override Application-Keypad 3943 | setting use Num_Lock to toggle Application-Keypad setting if Num_Lock 3944 | is off, toggle Application-Keypad setting. Also note that values of 3945 | Home, End, Delete may have been compiled differently on your system. 3946 | 3947 | Normal Shift Control Ctrl+Shift 3948 | Tab ^I ESC [ Z ^I ESC [ Z 3949 | BackSpace ^H ^? ^? ^? 3950 | Find ESC [ 1 ~ ESC [ 1 $ ESC [ 1 ^ ESC [ 1 @ 3951 | Insert ESC [ 2 ~ paste ESC [ 2 ^ ESC [ 2 @ 3952 | Execute ESC [ 3 ~ ESC [ 3 $ ESC [ 3 ^ ESC [ 3 @ 3953 | Select ESC [ 4 ~ ESC [ 4 $ ESC [ 4 ^ ESC [ 4 @ 3954 | Prior ESC [ 5 ~ scroll-up ESC [ 5 ^ ESC [ 5 @ 3955 | Next ESC [ 6 ~ scroll-down ESC [ 6 ^ ESC [ 6 @ 3956 | Home ESC [ 7 ~ ESC [ 7 $ ESC [ 7 ^ ESC [ 7 @ 3957 | End ESC [ 8 ~ ESC [ 8 $ ESC [ 8 ^ ESC [ 8 @ 3958 | Delete ESC [ 3 ~ ESC [ 3 $ ESC [ 3 ^ ESC [ 3 @ 3959 | F1 ESC [ 11 ~ ESC [ 23 ~ ESC [ 11 ^ ESC [ 23 ^ 3960 | F2 ESC [ 12 ~ ESC [ 24 ~ ESC [ 12 ^ ESC [ 24 ^ 3961 | F3 ESC [ 13 ~ ESC [ 25 ~ ESC [ 13 ^ ESC [ 25 ^ 3962 | F4 ESC [ 14 ~ ESC [ 26 ~ ESC [ 14 ^ ESC [ 26 ^ 3963 | F5 ESC [ 15 ~ ESC [ 28 ~ ESC [ 15 ^ ESC [ 28 ^ 3964 | F6 ESC [ 17 ~ ESC [ 29 ~ ESC [ 17 ^ ESC [ 29 ^ 3965 | F7 ESC [ 18 ~ ESC [ 31 ~ ESC [ 18 ^ ESC [ 31 ^ 3966 | F8 ESC [ 19 ~ ESC [ 32 ~ ESC [ 19 ^ ESC [ 32 ^ 3967 | F9 ESC [ 20 ~ ESC [ 33 ~ ESC [ 20 ^ ESC [ 33 ^ 3968 | F10 ESC [ 21 ~ ESC [ 34 ~ ESC [ 21 ^ ESC [ 34 ^ 3969 | F11 ESC [ 23 ~ ESC [ 23 $ ESC [ 23 ^ ESC [ 23 @ 3970 | F12 ESC [ 24 ~ ESC [ 24 $ ESC [ 24 ^ ESC [ 24 @ 3971 | F13 ESC [ 25 ~ ESC [ 25 $ ESC [ 25 ^ ESC [ 25 @ 3972 | F14 ESC [ 26 ~ ESC [ 26 $ ESC [ 26 ^ ESC [ 26 @ 3973 | F15 (Help) ESC [ 28 ~ ESC [ 28 $ ESC [ 28 ^ ESC [ 28 @ 3974 | F16 (Menu) ESC [ 29 ~ ESC [ 29 $ ESC [ 29 ^ ESC [ 29 @ 3975 | 3976 | F17 ESC [ 31 ~ ESC [ 31 $ ESC [ 31 ^ ESC [ 31 @ 3977 | F18 ESC [ 32 ~ ESC [ 32 $ ESC [ 32 ^ ESC [ 32 @ 3978 | F19 ESC [ 33 ~ ESC [ 33 $ ESC [ 33 ^ ESC [ 33 @ 3979 | F20 ESC [ 34 ~ ESC [ 34 $ ESC [ 34 ^ ESC [ 34 @ 3980 | Application 3981 | Up ESC [ A ESC [ a ESC O a ESC O A 3982 | Down ESC [ B ESC [ b ESC O b ESC O B 3983 | Right ESC [ C ESC [ c ESC O c ESC O C 3984 | Left ESC [ D ESC [ d ESC O d ESC O D 3985 | KP_Enter ^M ESC O M 3986 | KP_F1 ESC O P ESC O P 3987 | KP_F2 ESC O Q ESC O Q 3988 | KP_F3 ESC O R ESC O R 3989 | KP_F4 ESC O S ESC O S 3990 | XK_KP_Multiply * ESC O j 3991 | XK_KP_Add + ESC O k 3992 | XK_KP_Separator , ESC O l 3993 | XK_KP_Subtract - ESC O m 3994 | XK_KP_Decimal . ESC O n 3995 | XK_KP_Divide / ESC O o 3996 | XK_KP_0 0 ESC O p 3997 | XK_KP_1 1 ESC O q 3998 | XK_KP_2 2 ESC O r 3999 | XK_KP_3 3 ESC O s 4000 | XK_KP_4 4 ESC O t 4001 | XK_KP_5 5 ESC O u 4002 | XK_KP_6 6 ESC O v 4003 | XK_KP_7 7 ESC O w 4004 | XK_KP_8 8 ESC O x 4005 | XK_KP_9 9 ESC O y 4006 | */ 4007 | 4008 | version(Demo_kbhit) 4009 | void main() { 4010 | auto terminal = Terminal(ConsoleOutputType.linear); 4011 | auto input = RealTimeConsoleInput(&terminal, ConsoleInputFlags.raw); 4012 | 4013 | int a; 4014 | char ch = '.'; 4015 | while(a < 1000) { 4016 | a++; 4017 | if(a % terminal.width == 0) { 4018 | terminal.write("\r"); 4019 | if(ch == '.') 4020 | ch = ' '; 4021 | else 4022 | ch = '.'; 4023 | } 4024 | 4025 | if(input.kbhit()) 4026 | terminal.write(input.getch()); 4027 | else 4028 | terminal.write(ch); 4029 | 4030 | terminal.flush(); 4031 | 4032 | import core.thread; 4033 | Thread.sleep(50.msecs); 4034 | } 4035 | } 4036 | 4037 | /* 4038 | The Xterm palette progression is: 4039 | [0, 95, 135, 175, 215, 255] 4040 | 4041 | So if I take the color and subtract 55, then div 40, I get 4042 | it into one of these areas. If I add 20, I get a reasonable 4043 | rounding. 4044 | */ 4045 | 4046 | ubyte colorToXTermPaletteIndex(RGB color) { 4047 | /* 4048 | Here, I will round off to the color ramp or the 4049 | greyscale. I will NOT use the bottom 16 colors because 4050 | there's duplicates (or very close enough) to them in here 4051 | */ 4052 | 4053 | if(color.r == color.g && color.g == color.b) { 4054 | // grey - find one of them: 4055 | if(color.r == 0) return 0; 4056 | // meh don't need those two, let's simplify branche 4057 | //if(color.r == 0xc0) return 7; 4058 | //if(color.r == 0x80) return 8; 4059 | // it isn't == 255 because it wants to catch anything 4060 | // that would wrap the simple algorithm below back to 0. 4061 | if(color.r >= 248) return 15; 4062 | 4063 | // there's greys in the color ramp too, but these 4064 | // are all close enough as-is, no need to complicate 4065 | // algorithm for approximation anyway 4066 | 4067 | return cast(ubyte) (232 + ((color.r - 8) / 10)); 4068 | } 4069 | 4070 | // if it isn't grey, it is color 4071 | 4072 | // the ramp goes blue, green, red, with 6 of each, 4073 | // so just multiplying will give something good enough 4074 | 4075 | // will give something between 0 and 5, with some rounding 4076 | auto r = (cast(int) color.r - 35) / 40; 4077 | auto g = (cast(int) color.g - 35) / 40; 4078 | auto b = (cast(int) color.b - 35) / 40; 4079 | 4080 | return cast(ubyte) (16 + b + g*6 + r*36); 4081 | } 4082 | 4083 | /++ 4084 | Represents a 24-bit color. 4085 | 4086 | 4087 | $(TIP You can convert these to and from [arsd.color.Color] using 4088 | `.tupleof`: 4089 | 4090 | --- 4091 | RGB rgb; 4092 | Color c = Color(rgb.tupleof); 4093 | --- 4094 | ) 4095 | +/ 4096 | struct RGB { 4097 | ubyte r; /// 4098 | ubyte g; /// 4099 | ubyte b; /// 4100 | // terminal can't actually use this but I want the value 4101 | // there for assignment to an arsd.color.Color 4102 | private ubyte a = 255; 4103 | } 4104 | 4105 | // This is an approximation too for a few entries, but a very close one. 4106 | RGB xtermPaletteIndexToColor(int paletteIdx) { 4107 | RGB color; 4108 | 4109 | if(paletteIdx < 16) { 4110 | if(paletteIdx == 7) 4111 | return RGB(0xc0, 0xc0, 0xc0); 4112 | else if(paletteIdx == 8) 4113 | return RGB(0x80, 0x80, 0x80); 4114 | 4115 | color.r = (paletteIdx & 0b001) ? ((paletteIdx & 0b1000) ? 0xff : 0x80) : 0x00; 4116 | color.g = (paletteIdx & 0b010) ? ((paletteIdx & 0b1000) ? 0xff : 0x80) : 0x00; 4117 | color.b = (paletteIdx & 0b100) ? ((paletteIdx & 0b1000) ? 0xff : 0x80) : 0x00; 4118 | 4119 | } else if(paletteIdx < 232) { 4120 | // color ramp, 6x6x6 cube 4121 | color.r = cast(ubyte) ((paletteIdx - 16) / 36 * 40 + 55); 4122 | color.g = cast(ubyte) (((paletteIdx - 16) % 36) / 6 * 40 + 55); 4123 | color.b = cast(ubyte) ((paletteIdx - 16) % 6 * 40 + 55); 4124 | 4125 | if(color.r == 55) color.r = 0; 4126 | if(color.g == 55) color.g = 0; 4127 | if(color.b == 55) color.b = 0; 4128 | } else { 4129 | // greyscale ramp, from 0x8 to 0xee 4130 | color.r = cast(ubyte) (8 + (paletteIdx - 232) * 10); 4131 | color.g = color.r; 4132 | color.b = color.g; 4133 | } 4134 | 4135 | return color; 4136 | } 4137 | 4138 | int approximate16Color(RGB color) { 4139 | int c; 4140 | c |= color.r > 64 ? RED_BIT : 0; 4141 | c |= color.g > 64 ? GREEN_BIT : 0; 4142 | c |= color.b > 64 ? BLUE_BIT : 0; 4143 | 4144 | c |= (((color.r + color.g + color.b) / 3) > 80) ? Bright : 0; 4145 | 4146 | return c; 4147 | } 4148 | 4149 | /* 4150 | void main() { 4151 | auto terminal = Terminal(ConsoleOutputType.linear); 4152 | terminal.setTrueColor(RGB(255, 0, 255), RGB(255, 255, 255)); 4153 | terminal.writeln("Hello, world!"); 4154 | } 4155 | */ 4156 | --------------------------------------------------------------------------------