├── .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 |
--------------------------------------------------------------------------------