├── .github └── workflows │ └── build+test.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── benchmark └── benchmark.d ├── dub.sdl ├── misc ├── log-watch.ps1 └── test.d └── source └── scone ├── core ├── dummy.d ├── flags.d ├── init.d └── package.d ├── input ├── input.d ├── keyboard_event.d ├── os │ ├── posix │ │ ├── background_thread.d │ │ ├── keyboard_event_tree.d │ │ ├── locale │ │ │ ├── input_map.d │ │ │ ├── locale.d │ │ │ └── locale_data │ │ │ │ ├── osx.sv_se.tsv │ │ │ │ └── ubuntu.sv_se.tsv │ │ └── posix_input.d │ ├── standard_input.d │ └── windows │ │ ├── key_event_record_converter.d │ │ └── windows_input.d ├── package.d ├── scone_control_key.d └── scone_key.d ├── output ├── buffer.d ├── frame.d ├── helpers │ ├── ansi_color_helper.d │ ├── arguments_to_cells_converter.d │ └── cli.d ├── os │ ├── posix │ │ ├── partial_row_output_handler.d │ │ └── posix_output.d │ ├── standard_output.d │ └── windows │ │ ├── cell_converter.d │ │ └── windows_output.d ├── package.d ├── text_style.d └── types │ ├── cell.d │ ├── color.d │ ├── coordinate.d │ └── size.d └── package.d /.github/workflows/build+test.yml: -------------------------------------------------------------------------------- 1 | name: build+test 2 | 3 | on: 4 | push: 5 | branches: [ develop ] 6 | pull_request: 7 | branches: [ develop ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | os: [ubuntu-latest, macos-latest, windows-latest] 15 | include: 16 | - os: ubuntu-latest 17 | dub: dub 18 | code-coverage: | 19 | tlc=$(echo "$(cat -- *.lst)" | grep -E '^ *[0-9]+' | wc -l | xargs) 20 | elc=$(echo "$(cat -- *.lst)" | grep -E '^0000000' | wc -l | xargs) 21 | echo code coverage: $(echo "scale=2;1-($elc/$tlc)" | bc) 22 | 23 | - os: macos-latest 24 | dub: dub 25 | code-coverage: | 26 | tlc=$(echo "$(cat -- *.lst)" | grep -E '^ *[0-9]+' | wc -l | xargs) 27 | elc=$(echo "$(cat -- *.lst)" | grep -E '^0000000' | wc -l | xargs) 28 | echo code coverage: $(echo "scale=2;1-($elc/$tlc)" | bc) 29 | 30 | - os: windows-latest 31 | dub: dub.exe 32 | code-coverage: echo "code coverage not implemented for windows" 33 | 34 | 35 | steps: 36 | - uses: actions/checkout@v2 37 | - uses: dlang-community/setup-dlang@v1.1.1 38 | 39 | - name: Build 40 | run: ${{matrix.dub}} build --config=ci 41 | 42 | - name: Test 43 | run: ${{matrix.dub}} test --config=ci --coverage 44 | 45 | - name: Simple code coverage 46 | run: ${{matrix.code-coverage}} 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .dub/ 2 | *.DS_Store 3 | *.exe 4 | *.lib 5 | *.o 6 | *.obj 7 | *.sublime-workspace 8 | scone-test-release 9 | libscone.a 10 | bin/ 11 | dub.selections.json 12 | main.d 13 | app.d 14 | scone.log 15 | *.lst 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dscanner.ignoredKeys": [ 3 | "dscanner.suspicious.unmodified", 4 | "dscanner.style.undocumented_declaration", 5 | "dscanner.style.long_line" 6 | ], 7 | "editor.formatOnSave": true, 8 | "editor.tabSize": 4, 9 | "editor.insertSpaces": true, 10 | "files.eol": "\n", 11 | "files.trimFinalNewlines": true, 12 | "files.insertFinalNewline": true, 13 | } 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Vladimirs Nordholm 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # scone · [![build status](https://github.com/vladdeSV/scone/workflows/build+test/badge.svg)](https://github.com/vladdeSV/scone/actions?query=workflow:build+test) [![license](https://img.shields.io/github/license/vladdeSV/scone?color=black&labelColor=24292E)](https://github.com/vladdeSV/scone/blob/develop/LICENSE) 2 | Create cross-platform terminal applications. 3 | 4 | ## Example 5 | 6 | ```d 7 | import scone; 8 | 9 | void main() { 10 | frame.title("example"); 11 | frame.size(33, 20); 12 | 13 | bool run = true; 14 | while(run) { 15 | foreach(input; input.keyboard) { 16 | // if CTRL+C is pressed 17 | if(input.key == SK.c && input.hasControlKey(SCK.ctrl)) { 18 | run = false; 19 | } 20 | } 21 | 22 | frame.write( 23 | 12, 9, 24 | TextStyle().fg(Color.yellow), "Hello ", // white foreground text (chainable pattern) 25 | TextStyle().fg(Color.red).bg(Color.white), "World" // red foreground, white background 26 | ); 27 | frame.print(); 28 | } 29 | } 30 | ``` 31 | 32 | ![win](https://public.vladde.net/scone-example-mac-434.png) 33 | ![mac](https://public.vladde.net/scone-example-windows-434.png) 34 | 35 | ### Features 36 | * Display text and colors 37 | * Receive keyboard input 38 | * Cross-platform 39 | * Some restrictions apply, please see [OS Limitations](https://github.com/vladdeSV/scone/wiki/OS-Limitations) 40 | 41 | #### Simple cross-platform chart 42 | |output|Windows|POSIX| 43 | |:---|:---:|:---:| 44 | |text|✓|✓| 45 | |emoji||✓| 46 | |ansi-color|✓|✓| 47 | |rgb-color||✓| 48 | |high performance output|✓|| 49 | 50 | |input|Windows|POSIX| 51 | |:---|:---:|:---:| 52 | |input detection|✓|✓| 53 | |reliable|✓|*| 54 | |control keys|✓|**| 55 | |key release detection|✓|| 56 | 57 | \* Input is converted from arbitrary number sequences (may differ from system to system) to an input event. Basic ASCII should work no matter what system, however special keys like the up-arrow or function keys can vary drastically. 58 | 59 | \*\* Only registers the last pressed control key. 60 | 61 | ### Install with [dub](https://code.dlang.org/download) 62 | 63 | **Note**: `3.0.0` is not yet available. 64 | 65 | ```js 66 | /// dub.json 67 | "dependencies": { 68 | "scone": "~>3.0.0", 69 | ... 70 | } 71 | ``` 72 | 73 | ```js 74 | /// dub.sdl 75 | dependency "scone" version="~>3.0.0" 76 | ``` 77 | -------------------------------------------------------------------------------- /benchmark/benchmark.d: -------------------------------------------------------------------------------- 1 | module benchmark.benchmark; 2 | 3 | import std.stdio : writef, writeln, stdout; 4 | import std.conv : to, text; 5 | 6 | void main() 7 | { 8 | writef("\033[2J"); 9 | writef("\033[1;1H"); 10 | stdout.flush(); 11 | 12 | immutable height = 60; 13 | immutable repeatMultiplier = 10_000; 14 | immutable repeat = (height * repeatMultiplier); 15 | 16 | void delegate()[] benchmarks; 17 | 18 | string[][] results; 19 | 20 | benchmarks = [ 21 | () { 22 | foreach (i; 0 .. repeat) 23 | { 24 | auto pos = (i % 2) + 1; 25 | writef("\033[%d;%dH", pos, pos); 26 | } 27 | 28 | stdout.flush(); 29 | }, () { 30 | foreach (i; 0 .. repeat) 31 | { 32 | auto pos = (i % 2) + 1; 33 | writef("\033[%d;%dH", pos, pos); 34 | stdout.flush(); 35 | } 36 | }, () { 37 | foreach (i; 0 .. repeatMultiplier) 38 | { 39 | string data = ""; 40 | 41 | foreach (j; 0 .. height) 42 | { 43 | auto pos = ((j * i) % 2) + 1; 44 | data ~= text("\033[", pos, ";", pos, "H"); 45 | } 46 | 47 | writef(data); 48 | } 49 | }, () { 50 | foreach (i; 0 .. repeatMultiplier) 51 | { 52 | string data = ""; 53 | 54 | foreach (j; 0 .. height) 55 | { 56 | auto pos = ((j * i) % 2) + 1; 57 | data ~= text("\033[", pos, ";", pos, "H"); 58 | } 59 | 60 | writef(data); 61 | stdout.flush(); 62 | } 63 | } 64 | ]; 65 | 66 | results ~= benchmark(benchmarks); 67 | /+ sample output on github codespaces 68 | 1: 327 ms, 110 μs, and 7 hnsecs 69 | 2: 5 secs, 931 ms, 412 μs, and 9 hnsecs 70 | 3: 734 ms, 360 μs, and 3 hnsecs 71 | 4: 597 ms, 116 μs, and 7 hnsecs 72 | +/ 73 | 74 | benchmarks = [ 75 | () { 76 | foreach (i; 0 .. repeat) 77 | { 78 | auto character = ['1', '2'][i % 2]; 79 | writef("\033[1;1H%s %s", 80 | character, character); 81 | } 82 | }, () { 83 | foreach (i; 0 .. repeat) 84 | { 85 | auto character = ['1', '2'][i % 2]; 86 | writef("\033[1;1H%s\033[1;80H%s", character, character); 87 | } 88 | }, () { 89 | foreach (i; 0 .. repeat) 90 | { 91 | auto character = ['1', '2'][i % 2]; 92 | writef("\033[1;1H%s %s", character, character); 93 | } 94 | }, () { 95 | foreach (i; 0 .. repeat) 96 | { 97 | auto character = ['1', '2'][i % 2]; 98 | writef("\033[1;1H%s\033[1;3H%s", character, character); 99 | } 100 | }, () { 101 | foreach (i; 0 .. repeat) 102 | { 103 | // 10 changed characters 104 | auto character = ['1', '2'][i % 2]; 105 | writef("\033[1;1H%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s", 106 | character, " ", character, " ", character, " ", 107 | character, " ", character, " ", character, " ", character, 108 | " ", character, " ", character, " ", character); 109 | } 110 | }, () { 111 | foreach (i; 0 .. repeat) 112 | { 113 | // 10 changed characters 114 | auto character = ['1', '2'][i % 2]; 115 | writef("\033[1;1H%s\033[1;3H%s\033[1;5H%s\033[1;7H%s\033[1;9H%s\033[1;11H%s\033[1;13H%s\033[1;15H%s\033[1;17H%s\033[1;19H%s", 116 | character, character, character, character, character, 117 | character, character, character, character, character); 118 | } 119 | }, 120 | ]; 121 | 122 | results ~= benchmark(benchmarks); 123 | 124 | printResults(results); 125 | } 126 | 127 | auto benchmark(void delegate()[] benchmarks) 128 | { 129 | import std.datetime.stopwatch : StopWatch; 130 | 131 | auto sw = StopWatch(); 132 | sw.stop(); 133 | sw.reset(); 134 | 135 | string[] results; 136 | foreach (benchmark; benchmarks) 137 | { 138 | sw.reset(); 139 | sw.start(); 140 | benchmark(); 141 | sw.stop(); 142 | stdout.flush(); 143 | results ~= to!string(sw.peek); 144 | } 145 | 146 | return results; 147 | } 148 | 149 | void printResults(string[][] results) 150 | { 151 | writef("\033[2J"); 152 | writef("\033[1;1H"); 153 | stdout.flush(); 154 | 155 | foreach (r; results) 156 | { 157 | writeln; 158 | foreach (n, string result; r) 159 | { 160 | writeln(n + 1, ": ", result); 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /dub.sdl: -------------------------------------------------------------------------------- 1 | name "scone" 2 | description "Cross-platform terminal/console input/output (text, colors)" 3 | copyright "Copyright © 2015, Vladimirs Nordholm" 4 | license "MIT" 5 | authors "vladdeSV" 6 | stringImportPaths "./source/scone/input/os/posix/locale/locale_data" 7 | 8 | configuration "debug" { 9 | buildType "debug" 10 | buildRequirements "allowWarnings" 11 | targetType "executable" 12 | targetPath "bin/" 13 | } 14 | 15 | configuration "release" { 16 | buildType "release" 17 | targetType "library" 18 | } 19 | 20 | configuration "ci" { 21 | buildType "debug" 22 | targetType "library" 23 | } 24 | -------------------------------------------------------------------------------- /misc/log-watch.ps1: -------------------------------------------------------------------------------- 1 | Get-Content -Path $args[0] -Wait 2 | -------------------------------------------------------------------------------- /misc/test.d: -------------------------------------------------------------------------------- 1 | import scone; 2 | 3 | void main() 4 | { 5 | frame.title("👮🏿‍♀️"); 6 | frame.size(40, 12); 7 | 8 | bool run = true; 9 | auto lastInput = KeyboardEvent(); 10 | 11 | while (run) 12 | { 13 | foreach (input; input.keyboard) 14 | { 15 | // if CTRL+C is pressed 16 | if (input.key == SK.c && input.hasControlKey(SCK.ctrl)) 17 | { 18 | run = false; 19 | } 20 | 21 | lastInput = input; 22 | } 23 | 24 | 25 | // light and dark color stripes 26 | frame.write(0, 0, Color.blue.background, " ", Color.initial.background, Color.red.foreground, " light"); 27 | frame.write(0, 1, Color.blueDark.background, " ", Color.initial.background, Color.redDark.foreground, " dark"); 28 | frame.write(4, 0, Color.initial.background, "1", Color.same.background, "1", Color.blackDark.background, "1"); 29 | 30 | // corners 31 | frame.write(39, 0, Color.red.background, " "); 32 | frame.write(39, 11, Color.red.background, " "); 33 | frame.write(0, 11, Color.red.background, " "); 34 | 35 | // emoji 36 | frame.write(0, 3, "1", Color.black.background, "234"); 37 | frame.write(0, 4, "🍞", Color.black.background, "234"); 38 | frame.write(0, 5, Color.redDark.background, "abcdefghijklmn_åäö"); 39 | frame.write(0, 5, Color.redDark.background, "🇸🇪"); 40 | frame.write(0, 6, Color.black.background, Color.blue.foreground, "👮🏿‍♀️"); 41 | frame.write(0, 7, Color.black.background, "👮🏿‍♀️"); 42 | frame.write(1, 7, "."); 43 | 44 | // last key 45 | if (lastInput != KeyboardEvent()) 46 | { 47 | frame.write(0, 9, lastInput.key, ", ", lastInput.controlKey, " ", lastInput.pressed ? "pressed" : "released"); 48 | } 49 | 50 | frame.print(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /source/scone/core/dummy.d: -------------------------------------------------------------------------------- 1 | module scone.core.dummy; 2 | 3 | import scone.output.buffer : Buffer; 4 | import scone.output.types.size : Size; 5 | import scone.input.keyboard_event : KeyboardEvent; 6 | import scone.output.os.standard_output : StandardOutput; 7 | import scone.input.os.standard_input : StandardInput; 8 | 9 | class DummyOutput : StandardOutput 10 | { 11 | void initialize() 12 | { 13 | } 14 | 15 | void deinitialize() 16 | { 17 | } 18 | 19 | Size size() 20 | { 21 | return this.currentSize; 22 | } 23 | 24 | void size(in Size size) 25 | { 26 | this.currentSize = size; 27 | } 28 | 29 | void title(in string title) 30 | { 31 | } 32 | 33 | void cursorVisible(in bool visible) 34 | { 35 | } 36 | 37 | void renderBuffer(Buffer buffer) 38 | { 39 | } 40 | 41 | private Size currentSize = Size(80, 24); 42 | } 43 | 44 | unittest 45 | { 46 | auto output = new DummyOutput(); 47 | assert(output.size == Size(80, 24)); 48 | 49 | output.size = Size(10, 10); 50 | assert(output.size == Size(10, 10)); 51 | } 52 | 53 | class DummyInput : StandardInput 54 | { 55 | void initialize() 56 | { 57 | } 58 | 59 | void deinitialize() 60 | { 61 | } 62 | 63 | KeyboardEvent[] latestKeyboardEvents() 64 | { 65 | scope (exit) 66 | { 67 | this.latestDummyEvents = []; 68 | } 69 | 70 | return this.latestDummyEvents; 71 | } 72 | 73 | void appendDummyKeyboardEvent(KeyboardEvent event) 74 | { 75 | this.latestDummyEvents ~= event; 76 | } 77 | 78 | private KeyboardEvent[] latestDummyEvents; 79 | } 80 | 81 | unittest 82 | { 83 | import scone.input.scone_key : SK; 84 | import scone.input.scone_control_key : SCK; 85 | 86 | auto input = new DummyInput(); 87 | assert(input.latestKeyboardEvents == []); 88 | 89 | // dfmt off 90 | version (Windows) 91 | { 92 | input.appendDummyKeyboardEvent(KeyboardEvent(SK.a, SCK.none, true)); 93 | input.appendDummyKeyboardEvent(KeyboardEvent(SK.b, SCK.none, true)); 94 | assert(input.latestKeyboardEvents == [KeyboardEvent(SK.a, SCK.none, true), KeyboardEvent(SK.b, SCK.none, true)]); 95 | assert(input.latestKeyboardEvents == []); 96 | } 97 | else 98 | { 99 | input.appendDummyKeyboardEvent(KeyboardEvent(SK.a, SCK.none)); 100 | input.appendDummyKeyboardEvent(KeyboardEvent(SK.b, SCK.none)); 101 | assert(input.latestKeyboardEvents == [KeyboardEvent(SK.a, SCK.none), KeyboardEvent(SK.b, SCK.none)]); 102 | assert(input.latestKeyboardEvents == []); 103 | } 104 | // dfmt on 105 | } 106 | -------------------------------------------------------------------------------- /source/scone/core/flags.d: -------------------------------------------------------------------------------- 1 | module scone.core.flags; 2 | 3 | import std.traits : isIntegral; 4 | 5 | bool hasFlag(T)(T value, T flag) if (isIntegral!(T)) 6 | { 7 | return ((value & flag) == flag); 8 | } 9 | 10 | T withFlag(T)(T value, T flag) if (isIntegral!(T)) 11 | { 12 | return value | flag; 13 | } 14 | 15 | T withoutFlag(T)(T value, T flag) if (isIntegral!(T)) 16 | { 17 | return value & ~flag; 18 | } 19 | 20 | unittest 21 | { 22 | assert(0b1111.hasFlag(0b0001)); 23 | assert(!0b1110.hasFlag(0b0001)); 24 | 25 | assert(0b1000.withFlag(0b0001) == 0b1001); 26 | assert(0b1001.withFlag(0b0001) == 0b1001); 27 | 28 | assert(0b1111.withoutFlag(0b0001) == 0b1110); 29 | assert(0b1110.withoutFlag(0b0001) == 0b1110); 30 | } 31 | -------------------------------------------------------------------------------- /source/scone/core/init.d: -------------------------------------------------------------------------------- 1 | module scone.core.init; 2 | 3 | import scone.output.frame : Frame; 4 | import scone.input.input : Input; 5 | import scone.input.os.standard_input : StandardInput; 6 | import scone.output.os.standard_output : StandardOutput; 7 | import std.experimental.logger; 8 | 9 | Frame frame; 10 | Input input; 11 | 12 | /// can be overidden 13 | void delegate() sconeSetup = { 14 | auto standardOutput = createApplicationOutput(); 15 | frame = new Frame(standardOutput); 16 | 17 | auto standardInput = createApplicationInput(); 18 | input = new Input(standardInput); 19 | }; 20 | 21 | private shared initialized = false; 22 | 23 | static this() 24 | { 25 | if (initialized) 26 | { 27 | return; 28 | } 29 | 30 | initialized = true; 31 | sharedLog = new FileLogger("scone.log"); 32 | 33 | sconeSetup(); 34 | } 35 | 36 | StandardOutput createApplicationOutput() 37 | { 38 | version (unittest) 39 | { 40 | import scone.core.dummy : DummyOutput; 41 | 42 | return new DummyOutput(); 43 | } 44 | else version (Posix) 45 | { 46 | import scone.output.os.posix.posix_output : PosixOutput; 47 | 48 | return new PosixOutput(); 49 | } 50 | else version (Windows) 51 | { 52 | import scone.output.os.windows.windows_output : WindowsOutput; 53 | 54 | return new WindowsOutput(); 55 | } 56 | } 57 | 58 | StandardInput createApplicationInput() 59 | { 60 | version (unittest) 61 | { 62 | import scone.core.dummy : DummyInput; 63 | 64 | return new DummyInput(); 65 | } 66 | else version (Posix) 67 | { 68 | import scone.input.os.posix.posix_input : PosixInput; 69 | 70 | return new PosixInput(); 71 | } 72 | else version (Windows) 73 | { 74 | import scone.os.windows.input.windows_input : WindowsInput; 75 | 76 | return new WindowsInput(); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /source/scone/core/package.d: -------------------------------------------------------------------------------- 1 | module scone.core; 2 | 3 | public import scone.core.init; 4 | -------------------------------------------------------------------------------- /source/scone/input/input.d: -------------------------------------------------------------------------------- 1 | module scone.input.input; 2 | 3 | import scone.input.os.standard_input : StandardInput; 4 | import scone.input.keyboard_event : KeyboardEvent; 5 | 6 | class Input 7 | { 8 | this(StandardInput input) 9 | { 10 | this.input = input; 11 | input.initialize(); 12 | } 13 | 14 | ~this() 15 | { 16 | this.input.deinitialize(); 17 | } 18 | 19 | KeyboardEvent[] keyboard() 20 | { 21 | return this.input.latestKeyboardEvents(); 22 | } 23 | 24 | private StandardInput input; 25 | } 26 | 27 | unittest 28 | { 29 | import scone.core.dummy : DummyInput; 30 | import scone.input.keyboard_event : KeyboardEvent; 31 | import scone.input.scone_control_key : SCK; 32 | import scone.input.scone_key : SK; 33 | 34 | auto stdin = new DummyInput(); 35 | 36 | auto input = new Input(stdin); 37 | assert(input.keyboard() == []); 38 | 39 | stdin.appendDummyKeyboardEvent(KeyboardEvent(SK.a, SCK.none)); 40 | stdin.appendDummyKeyboardEvent(KeyboardEvent(SK.b, SCK.none)); 41 | assert(input.keyboard() == [KeyboardEvent(SK.a, SCK.none), KeyboardEvent(SK.b, SCK.none)]); 42 | assert(input.keyboard() == []); 43 | 44 | destroy(input); 45 | } 46 | -------------------------------------------------------------------------------- /source/scone/input/keyboard_event.d: -------------------------------------------------------------------------------- 1 | module scone.input.keyboard_event; 2 | 3 | import scone.input.scone_key : SK; 4 | import scone.input.scone_control_key : SCK; 5 | import scone.core.flags : hasFlag; 6 | 7 | struct KeyboardEvent 8 | { 9 | auto hasControlKey(SCK ck) 10 | { 11 | return controlKey.hasFlag(ck); 12 | } 13 | 14 | public SK key; 15 | public SCK controlKey; 16 | version (Windows) public bool pressed = true; 17 | } 18 | 19 | unittest 20 | { 21 | assert(KeyboardEvent(SK.a, SCK.none).key == SK.a); 22 | assert(KeyboardEvent(SK.a, SCK.ctrl).controlKey == SCK.ctrl); 23 | 24 | version (Windows) 25 | { 26 | assert(KeyboardEvent(SK.a, SCK.none, false).pressed == false); 27 | assert(KeyboardEvent(SK.a, SCK.none, true).pressed == true); 28 | } 29 | 30 | auto keyboardEvent = KeyboardEvent(SK.a, SCK.ctrl | SCK.alt); 31 | assert(keyboardEvent.hasControlKey(SCK.ctrl)); 32 | assert(keyboardEvent.hasControlKey(SCK.alt)); 33 | assert(!keyboardEvent.hasControlKey(SCK.shift)); 34 | } 35 | -------------------------------------------------------------------------------- /source/scone/input/os/posix/background_thread.d: -------------------------------------------------------------------------------- 1 | module scone.input.os.posix.background_thread; 2 | 3 | version (Posix) 4 | { 5 | import core.sys.posix.fcntl; 6 | import core.sys.posix.poll; 7 | import core.sys.posix.sys.ioctl : ioctl, winsize, TIOCGWINSZ; 8 | import core.sys.posix.unistd : read, STDOUT_FILENO; 9 | import core.thread : Thread; 10 | import scone.output.types.size : Size; 11 | import std.concurrency : thisTid, send, ownerTid; 12 | import std.datetime : Duration, msecs; 13 | 14 | static void pollKeyboardEvent() 15 | { 16 | Thread.getThis.isDaemon = true; 17 | 18 | while (true) 19 | { 20 | pollfd ufds; 21 | ufds.fd = STDOUT_FILENO; 22 | ufds.events = POLLIN; 23 | 24 | uint input; 25 | auto bytesRead = poll(&ufds, 1, -1); 26 | 27 | if (bytesRead == -1) 28 | { 29 | // error 30 | } 31 | else if (bytesRead == 0) 32 | { 33 | // no key was pressed within `timeout`. this is normal 34 | } 35 | else if (ufds.revents & POLLIN) 36 | { 37 | // Read input from keyboard 38 | read(STDOUT_FILENO, &input, 1); 39 | 40 | // Send key code to main thread (where it will be handled). 41 | send(ownerTid, input); 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /source/scone/input/os/posix/keyboard_event_tree.d: -------------------------------------------------------------------------------- 1 | module scone.input.os.posix.keyboard_event_tree; 2 | 3 | version (Posix) 4 | { 5 | import scone.input.scone_control_key : SCK; 6 | import scone.input.scone_key : SK; 7 | import scone.input.keyboard_event : KeyboardEvent; 8 | import std.typecons : Nullable; 9 | 10 | // todo this logic for inserting values has some problems 11 | // i believe it does not safeguard a node from having both children and a value 12 | // also, getting multiple inputs from a sequence is not 100% reliable. it should work for known sequences, but i would not consider this reliable yet. 13 | class KeyboardEventTree 14 | { 15 | public KeyboardEvent[] find(uint[] sequence) 16 | { 17 | assert(sequence.length); 18 | 19 | // special case for escape key until i figure out this logic 20 | if (sequence == [27]) 21 | { 22 | return [KeyboardEvent(SK.escape, SCK.none)]; 23 | } 24 | 25 | KeyboardEvent[] keypresses = []; 26 | auto node = this.root; 27 | 28 | foreach (n, number; sequence) 29 | { 30 | const bool hasNodeChild = (number in node.children) !is null; 31 | 32 | if (node.value.isNull() && !hasNodeChild) 33 | { 34 | if (!(keypresses.length && keypresses[$ - 1].key == SK.unknown)) 35 | { 36 | keypresses ~= KeyboardEvent(SK.unknown, SCK.none); 37 | } 38 | 39 | node = this.root; 40 | continue; 41 | } 42 | 43 | if (hasNodeChild) 44 | { 45 | node = node.children[number]; 46 | } 47 | 48 | if (!node.value.isNull()) 49 | { 50 | keypresses ~= node.value.get(); 51 | node = this.root; 52 | continue; 53 | } 54 | } 55 | 56 | return keypresses; 57 | } 58 | 59 | public bool insert(in uint[] sequence, KeyboardEvent data) 60 | { 61 | // special case for escape key until i figure out this logic 62 | if (sequence == [27]) 63 | { 64 | return true; 65 | } 66 | 67 | auto node = this.root; 68 | 69 | foreach (number; sequence) 70 | { 71 | if ((number in node.children) is null) 72 | { 73 | if (!node.value.isNull()) 74 | { 75 | return false; 76 | } 77 | 78 | node.children[number] = new KeyboardEventNode(); 79 | } 80 | 81 | node = node.children[number]; 82 | } 83 | 84 | node.value = data; 85 | 86 | return true; 87 | } 88 | 89 | private KeyboardEventNode root = new KeyboardEventNode(); 90 | } 91 | 92 | unittest 93 | { 94 | auto tree = new KeyboardEventTree(); 95 | tree.insert([27], KeyboardEvent(SK.escape, SCK.none)); 96 | tree.insert([27, 91, 67], KeyboardEvent(SK.right, SCK.none)); 97 | tree.insert([27, 91, 66], KeyboardEvent(SK.down, SCK.none)); 98 | tree.insert([48], KeyboardEvent(SK.key_0, SCK.none)); 99 | tree.insert([49], KeyboardEvent(SK.key_1, SCK.none)); 100 | 101 | KeyboardEvent[] find; 102 | 103 | find = tree.find([1]); 104 | assert(find.length == 1); 105 | assert(SK.unknown == find[0].key); 106 | 107 | find = tree.find([48]); 108 | assert(find.length == 1); 109 | assert(SK.key_0 == find[0].key); 110 | 111 | find = tree.find([27]); 112 | assert(find.length == 1); 113 | assert(SK.escape == find[0].key); 114 | 115 | find = tree.find([27, 91, 67]); 116 | assert(find.length == 1); 117 | assert(SK.right == find[0].key); 118 | 119 | find = tree.find([27, 91, 66]); 120 | assert(find.length == 1); 121 | assert(SK.down == find[0].key); 122 | 123 | find = tree.find([48, 49]); 124 | assert(find.length == 2); 125 | assert(SK.key_0 == find[0].key); 126 | assert(SK.key_1 == find[1].key); 127 | 128 | find = tree.find([27, 91, 67, 27, 91, 66]); 129 | assert(find.length == 2); 130 | assert(SK.right == find[0].key); 131 | assert(SK.down == find[1].key); 132 | 133 | find = tree.find([27, 6, 67, 27, 91, 66]); 134 | assert(find.length == 2); 135 | assert(SK.unknown == find[0].key); 136 | assert(SK.down == find[1].key); 137 | } 138 | 139 | private class KeyboardEventNode 140 | { 141 | KeyboardEventNode[uint] children; 142 | Nullable!KeyboardEvent value; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /source/scone/input/os/posix/locale/input_map.d: -------------------------------------------------------------------------------- 1 | module scone.input.os.posix.locale.input_map; 2 | 3 | version (Posix) 4 | { 5 | import scone.input.keyboard_event : KeyboardEvent; 6 | import scone.input.scone_control_key : SCK; 7 | import scone.input.scone_key : SK; 8 | import scone.input.os.posix.keyboard_event_tree : KeyboardEventTree; 9 | import std.experimental.logger : sharedLog; 10 | 11 | /** 12 | * Wrapper for an input sequence sent by the POSIX terminal. 13 | * 14 | * An input from the terminal is given by numbers in a sequence. 15 | * 16 | * For example, the right arrow key might send the sequence "27 91 67", 17 | * and will be stored as [27, 91, 67] 18 | */ 19 | alias InputSequence = uint[]; 20 | 21 | class InputMap 22 | { 23 | this(string tsv) 24 | { 25 | this.keypressTree = new KeyboardEventTree(); 26 | loadKeyboardEventTree(this.keypressTree, tsv); 27 | } 28 | 29 | KeyboardEvent[] keyboardEventsFromSequence(uint[] sequence) 30 | { 31 | return this.keypressTree.find(sequence); 32 | } 33 | 34 | private void loadKeyboardEventTree(KeyboardEventTree tree, string tsv) 35 | { 36 | import std.file : exists, readText; 37 | import std.string : chomp; 38 | import std.array : split; 39 | import std.conv : parse; 40 | 41 | string[] ies = tsv.split('\n'); 42 | 43 | // Loop all input sequences, and store them 44 | foreach (s; ies) 45 | { 46 | s = s.chomp; 47 | // if line is empty or begins with # 48 | if (s == "" || s[0] == '#') 49 | { 50 | continue; 51 | } 52 | 53 | string[] arguments = split(s, '\t'); 54 | if (arguments.length != 3) 55 | { 56 | sharedLog.warning("Reading input sequences CSV found %i arguments, exprected 3", arguments 57 | .length); 58 | 59 | continue; 60 | } 61 | 62 | auto key = parse!(SK)(arguments[0]); 63 | auto controlKey = parse!(SCK)(arguments[1]); 64 | auto seq = arguments[2]; 65 | 66 | if (seq == "-") 67 | { 68 | continue; 69 | } 70 | 71 | bool inserted = tree.insert(sequenceFromString(seq), KeyboardEvent(key, controlKey)); 72 | if (!inserted) 73 | { 74 | sharedLog.error("Could not map sequence ", seq, " to keypress ", key, "+", controlKey); 75 | } 76 | } 77 | } 78 | 79 | /// get uint[], from string in the format of "num1,num2,...,numX" 80 | private uint[] sequenceFromString(string input) pure 81 | { 82 | import std.array : split; 83 | import std.conv : parse; 84 | 85 | string[] numbers = split(input, ","); 86 | uint[] sequence; 87 | foreach (number_as_string; numbers) 88 | { 89 | sequence ~= parse!uint(number_as_string); 90 | } 91 | 92 | return sequence; 93 | } 94 | 95 | private KeyboardEventTree keypressTree; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /source/scone/input/os/posix/locale/locale.d: -------------------------------------------------------------------------------- 1 | module scone.input.os.posix.locale.locale; 2 | 3 | version (Posix) 4 | { 5 | class Locale 6 | { 7 | //todo: figure out if can get information about locale from terminal session? 8 | 9 | string systemLocaleSequences() 10 | { 11 | version (OSX) 12 | { 13 | return import("osx.sv_se.tsv"); 14 | } 15 | else 16 | { 17 | //fallback 18 | return import("ubuntu.sv_se.tsv"); 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /source/scone/input/os/posix/locale/locale_data/osx.sv_se.tsv: -------------------------------------------------------------------------------- 1 | a none 97 2 | b none 98 3 | c none 99 4 | d none 100 5 | e none 101 6 | f none 102 7 | g none 103 8 | h none 104 9 | i none 105 10 | j none 106 11 | k none 107 12 | l none 108 13 | m none 109 14 | n none 110 15 | o none 111 16 | p none 112 17 | q none 113 18 | r none 114 19 | s none 115 20 | t none 116 21 | u none 117 22 | v none 118 23 | w none 119 24 | x none 120 25 | y none 121 26 | z none 122 27 | tab none 9 28 | page_up none 27,91,53,126 29 | page_down none 27,91,54,126 30 | end none 27,91,70 31 | home none 27,91,72 32 | left none 27,91,68 33 | up none 27,91,65 34 | right none 27,91,67 35 | down none 27,91,66 36 | key_0 none 48 37 | key_1 none 49 38 | key_2 none 50 39 | key_3 none 51 40 | key_4 none 52 41 | key_5 none 53 42 | key_6 none 54 43 | key_7 none 55 44 | key_8 none 56 45 | key_9 none 57 46 | numpad_0 none - 47 | numpad_1 none - 48 | numpad_2 none - 49 | numpad_3 none - 50 | numpad_4 none - 51 | numpad_5 none - 52 | numpad_6 none - 53 | numpad_7 none - 54 | numpad_8 none - 55 | numpad_9 none - 56 | plus none 43 57 | minus none 45 58 | period none 46 59 | comma none 44 60 | asterisk none 42 61 | divide none 47 62 | f1 none 27,79,80 63 | f2 none 27,79,81 64 | f3 none 27,79,82 65 | f4 none 27,79,83 66 | f5 none 27,91,49,53,126 67 | f6 none 27,91,49,55,126 68 | f7 none 27,91,49,56,126 69 | f8 none 27,91,49,57,126 70 | f9 none 27,91,50,48,126 71 | f10 none 27,91,50,49,126 72 | f11 none 27,91,50,51,126 73 | f12 none 27,91,50,52,126 74 | f13 none - 75 | f14 none - 76 | f15 none - 77 | f16 none - 78 | f17 none - 79 | f18 none - 80 | f19 none - 81 | f20 none - 82 | f21 none - 83 | f22 none - 84 | f23 none - 85 | f24 none - 86 | oem_1 none 195,165 87 | oem_2 none 195,164 88 | oem_3 none 195,182 89 | oem_4 none - 90 | oem_5 none - 91 | oem_6 none - 92 | oem_7 none - 93 | oem_8 none - 94 | oem_102 none - 95 | a shift 65 96 | b shift 66 97 | c shift 67 98 | d shift 68 99 | e shift 69 100 | f shift 70 101 | g shift 71 102 | h shift 72 103 | i shift 73 104 | j shift 74 105 | k shift 75 106 | l shift 76 107 | m shift 77 108 | n shift 78 109 | o shift 79 110 | p shift 80 111 | q shift 81 112 | r shift 82 113 | s shift 83 114 | t shift 84 115 | u shift 85 116 | v shift 86 117 | w shift 87 118 | x shift 88 119 | y shift 89 120 | z shift 90 121 | tab shift 27,91,90 122 | page_up shift 27,91,53,126 123 | page_down shift 27,91,54,126 124 | end shift 27,91,70 125 | home shift 27,91,72 126 | left shift 27,91,49,59,50,68 127 | up shift 27,91,49,59,50,65 128 | right shift 27,91,49,59,50,67 129 | down shift 27,91,49,59,50,66 130 | key_0 shift 61 131 | key_1 shift 33 132 | key_2 shift 34 133 | key_3 shift 35 134 | key_4 shift 226,130,172 135 | key_4 shift 194,164 136 | key_5 shift 37 137 | key_6 shift 38 138 | key_7 shift 47 139 | key_8 shift 40 140 | key_9 shift 41 141 | numpad_0 shift - 142 | numpad_1 shift - 143 | numpad_2 shift - 144 | numpad_3 shift - 145 | numpad_4 shift - 146 | numpad_5 shift - 147 | numpad_6 shift - 148 | numpad_7 shift - 149 | numpad_8 shift - 150 | numpad_9 shift - 151 | plus shift 63 152 | minus shift 95 153 | period shift 58 154 | comma shift 59 155 | asterisk shift - 156 | divide shift - 157 | f1 shift 27,91,49,59,50,80 158 | f2 shift 27,91,49,59,50,81 159 | f3 shift 27,91,49,59,50,82 160 | f4 shift 27,91,49,59,50,83 161 | f5 shift 27,91,49,53,59,50,126 162 | f6 shift 27,91,49,55,59,50,126 163 | f7 shift 27,91,49,56,59,50,126 164 | f8 shift 27,91,49,57,59,50,126 165 | f9 shift 27,91,50,48,59,50,126 166 | f10 shift - 167 | f11 shift 27,91,50,51,59,50,126 168 | f12 shift 27,91,50,52,59,50,126 169 | f13 shift - 170 | f14 shift - 171 | f15 shift - 172 | f16 shift - 173 | f17 shift - 174 | f18 shift - 175 | f19 shift - 176 | f20 shift - 177 | f21 shift - 178 | f22 shift - 179 | f23 shift - 180 | f24 shift - 181 | oem_1 shift 195,133 182 | oem_1 shift 1953 183 | oem_2 shift 195,150 184 | oem_2 shift 195,132 185 | oem_2 shift 1952 186 | oem_3 shift 195,150 187 | oem_4 shift - 188 | oem_5 shift - 189 | oem_6 shift - 190 | oem_7 shift - 191 | oem_8 shift - 192 | oem_102 shift - 193 | a ctrl 1 194 | b ctrl 2 195 | c ctrl 3 196 | d ctrl 4 197 | e ctrl 5 198 | f ctrl 6 199 | g ctrl 7 200 | h ctrl 8 201 | i ctrl 9 202 | j ctrl 10 203 | k ctrl 11 204 | l ctrl 12 205 | m ctrl - 206 | n ctrl 14 207 | o ctrl 15 208 | p ctrl 16 209 | q ctrl 17 210 | r ctrl 18 211 | s ctrl 19 212 | t ctrl 20 213 | u ctrl 21 214 | v ctrl 22 215 | w ctrl 23 216 | x ctrl 24 217 | y ctrl 25 218 | z ctrl 26 219 | tab ctrl - 220 | page_up ctrl 27,91,53,59,53,126 221 | page_down ctrl 27,91,54,59,53,126 222 | end ctrl 27,91,49,59,53,70 223 | home ctrl 27,91,49,59,53,72 224 | left ctrl 27,91,49,59,53,68 225 | up ctrl 27,91,49,59,53,65 226 | right ctrl 27,91,49,59,53,67 227 | down ctrl 27,91,49,59,53,66 228 | key_0 ctrl - 229 | key_1 ctrl - 230 | key_2 ctrl - 231 | key_3 ctrl - 232 | key_4 ctrl 28 233 | key_5 ctrl 29 234 | key_6 ctrl 30 235 | key_7 ctrl 31 236 | key_8 ctrl - 237 | key_9 ctrl - 238 | numpad_0 ctrl - 239 | numpad_1 ctrl - 240 | numpad_2 ctrl - 241 | numpad_3 ctrl - 242 | numpad_4 ctrl - 243 | numpad_5 ctrl - 244 | numpad_6 ctrl - 245 | numpad_7 ctrl - 246 | numpad_8 ctrl - 247 | numpad_9 ctrl - 248 | plus ctrl - 249 | minus ctrl 31 250 | period ctrl - 251 | comma ctrl - 252 | asterisk ctrl - 253 | divide ctrl - 254 | f1 ctrl 27,91,49,59,53,80 255 | f2 ctrl 27,91,49,59,53,81 256 | f3 ctrl 27,91,49,59,53,82 257 | f4 ctrl 27,91,49,59,53,83 258 | f5 ctrl 27,91,49,53,59,53,126 259 | f6 ctrl 27,91,49,55,59,53,126 260 | f7 ctrl 27,91,49,56,59,53,126 261 | f8 ctrl 27,91,49,57,59,53,126 262 | f9 ctrl 27,91,50,48,59,53,126 263 | f10 ctrl 27,91,50,49,59,53,126 264 | f11 ctrl 27,91,50,51,59,53,126 265 | f12 ctrl 27,91,50,52,59,53,126 266 | f13 ctrl - 267 | f14 ctrl - 268 | f15 ctrl - 269 | f16 ctrl - 270 | f17 ctrl - 271 | f18 ctrl - 272 | f19 ctrl - 273 | f20 ctrl - 274 | f21 ctrl - 275 | f22 ctrl - 276 | f23 ctrl - 277 | f24 ctrl - 278 | oem_1 ctrl 29 279 | oem_2 ctrl - 280 | oem_3 ctrl 28 281 | oem_4 ctrl - 282 | oem_5 ctrl - 283 | oem_6 ctrl - 284 | oem_7 ctrl - 285 | oem_8 ctrl - 286 | oem_102 ctrl - 287 | a alt 239,163,191 288 | a alt 27,97 289 | b alt 226,128,186 290 | b alt 27,98 291 | c alt 195,167 292 | c alt 27,99 293 | d alt 226,136,130 294 | d alt 27,100 295 | e alt 195,169 296 | e alt 27,101 297 | f alt 198,146 298 | f alt 27,102 299 | g alt 194,184 300 | g alt 27,103 301 | h alt 203,155 302 | h alt 27,104 303 | i alt 196,177 304 | i alt 27,105 305 | j alt 226,136,154 306 | j alt 27,106 307 | k alt 194,170 308 | k alt 27,107 309 | l alt 239,172,129 310 | l alt 27,108 311 | m alt 226,128,153 312 | m alt 27,109 313 | n alt 226,128,152 314 | n alt 27,110 315 | o alt 197,147 316 | o alt 27,111 317 | p alt 207,128 318 | p alt 27,112 319 | q alt 226,128,162 320 | q alt 27,113 321 | r alt 194,174 322 | r alt 27,114 323 | s alt 195,159 324 | s alt 27,115 325 | t alt 226,128,160 326 | t alt 27,116 327 | u alt 195,188 328 | u alt 27,117 329 | v alt 226,128,185 330 | v alt 27,118 331 | w alt 206,169 332 | w alt 27,119 333 | x alt 226,137,136 334 | x alt 27,120 335 | y alt 194,181 336 | y alt 27,121 337 | z alt 195,183 338 | z alt 27,122 339 | tab alt - 340 | page_up alt 27,27,91,53,126 341 | page_up alt 27,91,53,59,51,126 342 | page_down alt 27,27,91,54,126 343 | page_down alt 27,91,54,59,51,126 344 | end alt 27,91,49,59,57,70 345 | end alt 27,91,49,59,51,70 346 | home alt 27,91,49,59,57,72 347 | home alt 27,91,49,59,51,72 348 | left alt 27,98 349 | left alt 27,91,49,59,51,68 350 | up alt 27,91,49,59,51,65 351 | right alt 27,102 352 | right alt 27,91,49,59,51,67 353 | down alt 27,91,49,59,51,66 354 | key_0 alt 226,137,136 355 | key_0 alt 27,48 356 | key_1 alt 194,169 357 | key_1 alt 27,49 358 | key_2 alt 64 359 | key_2 alt 27,50 360 | key_3 alt 194,163 361 | key_3 alt 27,51 362 | key_4 alt 36 363 | key_4 alt 27,52 364 | key_5 alt 226,136,158 365 | key_5 alt 27,53 366 | key_6 alt 194,167 367 | key_6 alt 27,54 368 | key_7 alt 124 369 | key_7 alt 27,55 370 | key_8 alt 91 371 | key_8 alt 27,56 372 | key_9 alt 93 373 | key_9 alt 27,57 374 | numpad_0 alt - 375 | numpad_1 alt - 376 | numpad_2 alt - 377 | numpad_3 alt - 378 | numpad_4 alt - 379 | numpad_5 alt - 380 | numpad_6 alt - 381 | numpad_7 alt - 382 | numpad_8 alt - 383 | numpad_9 alt - 384 | plus alt 194,177 385 | minus alt 226,128,147 386 | period alt 226,128,166 387 | period alt 27,46 388 | comma alt 226,128,154 389 | comma alt 27,44 390 | asterisk alt 226,128,153 391 | divide alt 92 392 | f1 alt 27,91,49,55,126 393 | f2 alt 27,91,49,56,126 394 | f3 alt 27,91,49,57,126 395 | f4 alt 27,91,50,48,126 396 | f4 alt 39 397 | f5 alt 27,91,50,49,126 398 | f5 alt 27,91,49,53,59,51,126 399 | f6 alt 27,91,50,51,126 400 | f6 alt 27,91,49,55,59,51,126 401 | f7 alt 27,91,50,52,126 402 | f8 alt 27,91,50,53,126 403 | f9 alt 27,91,50,54,126 404 | f9 alt 27,91,50,48,59,51,126 405 | f10 alt 27,91,50,56,126 406 | f11 alt 27,91,50,57,126 407 | f11 alt 27,91,50,51,59,51,126 408 | f12 alt 27,91,51,49,126 409 | f12 alt 27,91,50,52,59,51,126 410 | f13 alt - 411 | f14 alt - 412 | f15 alt - 413 | f16 alt - 414 | f17 alt - 415 | f18 alt - 416 | f19 alt - 417 | f20 alt - 418 | f21 alt - 419 | f22 alt - 420 | f23 alt - 421 | f24 alt - 422 | oem_1 alt 203,153 423 | oem_1 alt 27,195,165 424 | oem_2 alt 195,166 425 | oem_2 alt 27,195,164 426 | oem_3 alt 195,184 427 | oem_3 alt 27,195,182 428 | oem_4 alt - 429 | oem_5 alt - 430 | oem_6 alt - 431 | oem_7 alt - 432 | oem_8 alt - 433 | oem_102 alt - 434 | escape none 27 435 | del none 127 436 | enter none 13 437 | space none 32 438 | space ctrl 0 439 | space alt 194,160 440 | -------------------------------------------------------------------------------- /source/scone/input/os/posix/locale/locale_data/ubuntu.sv_se.tsv: -------------------------------------------------------------------------------- 1 | a none 97 2 | b none 98 3 | c none 99 4 | d none 100 5 | e none 101 6 | f none 102 7 | g none 103 8 | h none 104 9 | i none 105 10 | j none 106 11 | k none 107 12 | l none 108 13 | m none 109 14 | n none 110 15 | o none 111 16 | p none 112 17 | q none 113 18 | r none 114 19 | s none 115 20 | t none 116 21 | u none 117 22 | v none 118 23 | w none 119 24 | x none 120 25 | y none 121 26 | z none 122 27 | tab none 9 28 | page_up none 27,91,53,126 29 | page_down none 27,91,54,126 30 | end none 27,91,70 31 | home none 27,91,72 32 | left none 27,91,68 33 | up none 27,91,65 34 | right none 27,91,67 35 | down none 27,91,66 36 | key_0 none 48 37 | key_1 none 49 38 | key_2 none 50 39 | key_3 none 51 40 | key_4 none 52 41 | key_5 none 53 42 | key_6 none 54 43 | key_7 none 55 44 | key_8 none 56 45 | key_9 none 57 46 | numpad_0 none - 47 | numpad_1 none - 48 | numpad_2 none - 49 | numpad_3 none - 50 | numpad_4 none - 51 | numpad_5 none - 52 | numpad_6 none - 53 | numpad_7 none - 54 | numpad_8 none - 55 | numpad_9 none - 56 | plus none 43 57 | minus none 45 58 | period none 46 59 | comma none 44 60 | asterisk none 42 61 | divide none 47 62 | f1 none - 63 | f2 none 27,79,81 64 | f3 none 27,79,82 65 | f4 none 27,79,83 66 | f5 none 27,91,49,53,126 67 | f6 none 27,91,49,55,126 68 | f7 none 27,91,49,56,126 69 | f8 none 27,91,49,57,126 70 | f9 none 27,91,50,48,126 71 | f10 none 27,91,50,49,126 72 | f11 none - 73 | f12 none 27,91,50,52,126 74 | f13 none - 75 | f14 none - 76 | f15 none - 77 | f16 none - 78 | f17 none - 79 | f18 none - 80 | f19 none - 81 | f20 none - 82 | f21 none - 83 | f22 none - 84 | f23 none - 85 | f24 none - 86 | oem_1 none 195,165 87 | oem_2 none 195,164 88 | oem_3 none 195,182 89 | oem_4 none - 90 | oem_5 none - 91 | oem_6 none - 92 | oem_7 none - 93 | oem_8 none - 94 | oem_102 none - 95 | a shift 65 96 | b shift 66 97 | c shift 67 98 | d shift 68 99 | e shift 69 100 | f shift 70 101 | g shift 71 102 | h shift 72 103 | i shift 73 104 | j shift 74 105 | k shift 75 106 | l shift 76 107 | m shift 77 108 | n shift 78 109 | o shift 79 110 | p shift 80 111 | q shift 81 112 | r shift 82 113 | s shift 83 114 | t shift 84 115 | u shift 85 116 | v shift 86 117 | w shift 87 118 | x shift 88 119 | y shift 89 120 | z shift 90 121 | tab shift 27,91,90 122 | page_up shift - 123 | page_down shift - 124 | end shift - 125 | home shift - 126 | left shift 27,91,49,59,50,68 127 | up shift 27,91,49,59,50,65 128 | right shift 27,91,49,59,50,67 129 | down shift 27,91,49,59,50,66 130 | key_0 shift 61 131 | key_1 shift 33 132 | key_2 shift 34 133 | key_3 shift 35 134 | key_4 shift 194,164 135 | key_5 shift 37 136 | key_6 shift 38 137 | key_7 shift 47 138 | key_8 shift 40 139 | key_9 shift 41 140 | numpad_0 shift - 141 | numpad_1 shift - 142 | numpad_2 shift - 143 | numpad_3 shift - 144 | numpad_4 shift - 145 | numpad_5 shift - 146 | numpad_6 shift - 147 | numpad_7 shift - 148 | numpad_8 shift - 149 | numpad_9 shift - 150 | plus shift - 151 | minus shift - 152 | period shift 58 153 | comma shift 59 154 | asterisk shift - 155 | divide shift - 156 | f1 shift 27,91,49,59,50,80 157 | f2 shift 27,91,49,59,50,81 158 | f3 shift 27,91,49,59,50,82 159 | f4 shift 27,91,49,59,50,83 160 | f5 shift 27,91,49,53,59,50,126 161 | f6 shift 27,91,49,55,59,50,126 162 | f7 shift 27,91,49,56,59,50,126 163 | f8 shift 27,91,49,57,59,50,126 164 | f9 shift 27,91,50,48,59,50,126 165 | f10 shift - 166 | f11 shift 27,91,50,51,59,50,126 167 | f12 shift 27,91,50,52,59,50,126 168 | f13 shift - 169 | f14 shift - 170 | f15 shift - 171 | f16 shift - 172 | f17 shift - 173 | f18 shift - 174 | f19 shift - 175 | f20 shift - 176 | f21 shift - 177 | f22 shift - 178 | f23 shift - 179 | f24 shift - 180 | oem_1 shift 195,133 181 | oem_2 shift 195,132 182 | oem_3 shift 195,150 183 | oem_4 shift - 184 | oem_5 shift - 185 | oem_6 shift - 186 | oem_7 shift - 187 | oem_8 shift - 188 | oem_102 shift - 189 | a ctrl 1 190 | b ctrl 2 191 | c ctrl 3 192 | d ctrl 4 193 | e ctrl 5 194 | f ctrl 6 195 | g ctrl 7 196 | h ctrl 8 197 | i ctrl 9 198 | j ctrl 10 199 | k ctrl 11 200 | l ctrl 12 201 | m ctrl - 202 | n ctrl 14 203 | o ctrl 15 204 | p ctrl 16 205 | q ctrl 17 206 | r ctrl 18 207 | s ctrl 19 208 | t ctrl 20 209 | u ctrl 21 210 | v ctrl 22 211 | w ctrl 23 212 | x ctrl 24 213 | y ctrl 25 214 | z ctrl 26 215 | tab ctrl - 216 | page_up ctrl 27,91,53,59,53,126 217 | page_down ctrl 27,91,54,59,53,126 218 | end ctrl 27,91,49,59,53,70 219 | home ctrl 27,91,49,59,53,72 220 | left ctrl 27,91,49,59,53,68 221 | up ctrl 27,91,49,59,53,65 222 | right ctrl 27,91,49,59,53,67 223 | down ctrl 27,91,49,59,53,66 224 | key_0 ctrl - 225 | key_1 ctrl - 226 | key_2 ctrl - 227 | key_3 ctrl - 228 | key_4 ctrl 28 229 | key_5 ctrl 29 230 | key_6 ctrl 30 231 | key_7 ctrl 31 232 | key_8 ctrl - 233 | key_9 ctrl - 234 | numpad_0 ctrl - 235 | numpad_1 ctrl - 236 | numpad_2 ctrl - 237 | numpad_3 ctrl - 238 | numpad_4 ctrl - 239 | numpad_5 ctrl - 240 | numpad_6 ctrl - 241 | numpad_7 ctrl - 242 | numpad_8 ctrl - 243 | numpad_9 ctrl - 244 | plus ctrl - 245 | minus ctrl - 246 | period ctrl - 247 | comma ctrl - 248 | asterisk ctrl - 249 | divide ctrl - 250 | f1 ctrl 27,91,49,59,53,80 251 | f2 ctrl 27,91,49,59,53,81 252 | f3 ctrl 27,91,49,59,53,82 253 | f4 ctrl 27,91,49,59,53,83 254 | f5 ctrl 27,91,49,53,59,53,126 255 | f6 ctrl 27,91,49,55,59,53,126 256 | f7 ctrl 27,91,49,56,59,53,126 257 | f8 ctrl 27,91,49,57,59,53,126 258 | f9 ctrl 27,91,50,48,59,53,126 259 | f10 ctrl 27,91,50,49,59,53,126 260 | f11 ctrl 27,91,50,51,59,53,126 261 | f12 ctrl 27,91,50,52,59,53,126 262 | f13 ctrl - 263 | f14 ctrl - 264 | f15 ctrl - 265 | f16 ctrl - 266 | f17 ctrl - 267 | f18 ctrl - 268 | f19 ctrl - 269 | f20 ctrl - 270 | f21 ctrl - 271 | f22 ctrl - 272 | f23 ctrl - 273 | f24 ctrl - 274 | oem_1 ctrl - 275 | oem_2 ctrl - 276 | oem_3 ctrl - 277 | oem_4 ctrl - 278 | oem_5 ctrl - 279 | oem_6 ctrl - 280 | oem_7 ctrl - 281 | oem_8 ctrl - 282 | oem_102 ctrl - 283 | a alt 27,97 284 | b alt 27,98 285 | c alt 27,99 286 | d alt 27,100 287 | e alt 27,101 288 | f alt 27,102 289 | g alt 27,103 290 | h alt 27,104 291 | i alt 27,105 292 | j alt 27,106 293 | k alt 27,107 294 | l alt 27,108 295 | m alt 27,109 296 | n alt 27,110 297 | o alt 27,111 298 | p alt 27,112 299 | q alt 27,113 300 | r alt 27,114 301 | s alt 27,115 302 | t alt 27,116 303 | u alt 27,117 304 | v alt 27,118 305 | w alt 27,119 306 | x alt 27,120 307 | y alt 27,121 308 | z alt 27,122 309 | tab alt - 310 | page_up alt 27,91,53,59,51,126 311 | page_down alt 27,91,54,59,51,126 312 | end alt 27,91,49,59,51,70 313 | home alt 27,91,49,59,51,72 314 | left alt 27,91,49,59,51,68 315 | up alt 27,91,49,59,51,65 316 | right alt 27,91,49,59,51,67 317 | down alt 27,91,49,59,51,66 318 | key_0 alt 27,48 319 | key_1 alt 27,49 320 | key_2 alt 27,50 321 | key_3 alt 27,51 322 | key_4 alt 27,52 323 | key_5 alt 27,53 324 | key_6 alt 27,54 325 | key_7 alt 27,55 326 | key_8 alt 27,56 327 | key_9 alt 27,57 328 | numpad_0 alt - 329 | numpad_1 alt - 330 | numpad_2 alt - 331 | numpad_3 alt - 332 | numpad_4 alt - 333 | numpad_5 alt - 334 | numpad_6 alt - 335 | numpad_7 alt - 336 | numpad_8 alt - 337 | numpad_9 alt - 338 | plus alt - 339 | minus alt - 340 | period alt 27,46 341 | comma alt 27,44 342 | asterisk alt - 343 | divide alt - 344 | f1 alt - 345 | f2 alt - 346 | f3 alt - 347 | f4 alt 39 348 | f5 alt 27,91,49,53,59,51,126 349 | f6 alt 27,91,49,55,59,51,126 350 | f7 alt - 351 | f8 alt - 352 | f9 alt 27,91,50,48,59,51,126 353 | f10 alt - 354 | f11 alt 27,91,50,51,59,51,126 355 | f12 alt 27,91,50,52,59,51,126 356 | f13 alt - 357 | f14 alt - 358 | f15 alt - 359 | f16 alt - 360 | f17 alt - 361 | f18 alt - 362 | f19 alt - 363 | f20 alt - 364 | f21 alt - 365 | f22 alt - 366 | f23 alt - 367 | f24 alt - 368 | oem_1 alt 27,195,165 369 | oem_2 alt 27,195,164 370 | oem_3 alt 27,195,182 371 | oem_4 alt - 372 | oem_5 alt - 373 | oem_6 alt - 374 | oem_7 alt - 375 | oem_8 alt - 376 | oem_102 alt - 377 | escape none 27 378 | del none 127 379 | enter none 13 380 | space none 32 381 | space ctrl 0 382 | space alt 194,160 383 | -------------------------------------------------------------------------------- /source/scone/input/os/posix/posix_input.d: -------------------------------------------------------------------------------- 1 | module scone.input.os.posix.posix_input; 2 | 3 | version (Posix) 4 | { 5 | import core.sys.posix.termios; 6 | import core.sys.posix.unistd : STDOUT_FILENO; 7 | import scone.input.keyboard_event : KeyboardEvent; 8 | import scone.input.scone_control_key : SCK; 9 | import scone.input.scone_key : SK; 10 | import scone.input.os.standard_input : StandardInput; 11 | import scone.input.os.posix.background_thread; 12 | import scone.input.os.posix.locale.input_map : InputMap; 13 | import scone.input.os.posix.locale.locale : Locale; 14 | import std.concurrency : spawn, Tid, thisTid, send, receiveTimeout, ownerTid; 15 | import std.conv : text; 16 | import std.datetime : msecs; 17 | 18 | extern (C) 19 | { 20 | void cfmakeraw(termios* termios_p); 21 | } 22 | 23 | class PosixInput : StandardInput 24 | { 25 | void initialize() 26 | { 27 | this.setInputMapping(); 28 | this.enableRawInput(); 29 | this.startPollingInput(); 30 | } 31 | 32 | void deinitialize() 33 | { 34 | this.resetTerminalState(); 35 | } 36 | 37 | uint[] retreiveInputSequence() 38 | { 39 | uint[] sequence; 40 | 41 | while (receiveTimeout(5.msecs, (uint code) { sequence ~= code; })) 42 | { 43 | // continiously repeat until no code is recieved within 5 milliseconds 44 | } 45 | 46 | return sequence; 47 | } 48 | 49 | KeyboardEvent[] latestKeyboardEvents() 50 | { 51 | auto sequence = this.retreiveInputSequence(); 52 | 53 | //todo: returns null here. should this logic be here or in `inputMap.keyboardEventsFromSequence(sequence)` instead? 54 | if (sequence.length == 0) 55 | { 56 | return null; 57 | } 58 | 59 | // conversion to input events. refactor whole (winodws+posix) code to use keypresses instead of input events? 60 | KeyboardEvent[] keyboardEvents = []; 61 | foreach (KeyboardEvent keypress; inputMap.keyboardEventsFromSequence(sequence)) 62 | { 63 | keyboardEvents ~= KeyboardEvent(keypress.key, keypress.controlKey); 64 | } 65 | 66 | return keyboardEvents; 67 | } 68 | 69 | private void startPollingInput() 70 | { 71 | // begin polling 72 | spawn(&pollKeyboardEvent); 73 | } 74 | 75 | // unsure when to use this. 76 | // on mac this shows a small key icon, used when entering passwords 77 | private void setInputEcho(in bool echo) 78 | { 79 | termios termInfo; 80 | tcgetattr(STDOUT_FILENO, &termInfo); 81 | 82 | if (echo) 83 | { 84 | termInfo.c_lflag |= ECHO; 85 | } 86 | else 87 | { 88 | termInfo.c_lflag &= ~ECHO; 89 | } 90 | 91 | tcsetattr(STDOUT_FILENO, TCSADRAIN, &termInfo); 92 | } 93 | 94 | private void setInputMapping() 95 | { 96 | Locale locale = new Locale(); 97 | this.inputMap = new InputMap(locale.systemLocaleSequences); 98 | } 99 | 100 | private void enableRawInput() 101 | { 102 | // store the state of the terminal 103 | tcgetattr(1, &originalTerminalState); 104 | 105 | // enable raw input 106 | termios newTerminalState = originalTerminalState; 107 | cfmakeraw(&newTerminalState); 108 | tcsetattr(STDOUT_FILENO, TCSADRAIN, &newTerminalState); 109 | } 110 | 111 | private void resetTerminalState() 112 | { 113 | tcsetattr(STDOUT_FILENO, TCSADRAIN, &originalTerminalState); 114 | } 115 | 116 | private InputMap inputMap; 117 | private termios originalTerminalState; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /source/scone/input/os/standard_input.d: -------------------------------------------------------------------------------- 1 | module scone.input.os.standard_input; 2 | 3 | import scone.input.keyboard_event : KeyboardEvent; 4 | 5 | interface StandardInput 6 | { 7 | public void initialize(); 8 | public void deinitialize(); 9 | public KeyboardEvent[] latestKeyboardEvents(); 10 | } 11 | -------------------------------------------------------------------------------- /source/scone/input/os/windows/key_event_record_converter.d: -------------------------------------------------------------------------------- 1 | module scone.input.os.windows.input.key_event_record_converter; 2 | 3 | version (Windows) 4 | { 5 | import core.sys.windows.windows; 6 | import scone.input.scone_control_key : SCK; 7 | import scone.input.scone_key : SK; 8 | import scone.core.flags : hasFlag, withFlag; 9 | 10 | final class KeyEventRecordConverter 11 | { 12 | private KEY_EVENT_RECORD keyEventRecord; 13 | 14 | this(KEY_EVENT_RECORD keyEventRecord) 15 | { 16 | this.keyEventRecord = keyEventRecord; 17 | } 18 | 19 | SK sconeKey() 20 | { 21 | switch (this.keyEventRecord.wVirtualKeyCode) 22 | { 23 | default: 24 | return SK.unknown; 25 | case WindowsKeyCode.K_0: 26 | return SK.key_0; 27 | case WindowsKeyCode.K_1: 28 | return SK.key_1; 29 | case WindowsKeyCode.K_2: 30 | return SK.key_2; 31 | case WindowsKeyCode.K_3: 32 | return SK.key_3; 33 | case WindowsKeyCode.K_4: 34 | return SK.key_4; 35 | case WindowsKeyCode.K_5: 36 | return SK.key_5; 37 | case WindowsKeyCode.K_6: 38 | return SK.key_6; 39 | case WindowsKeyCode.K_7: 40 | return SK.key_7; 41 | case WindowsKeyCode.K_8: 42 | return SK.key_8; 43 | case WindowsKeyCode.K_9: 44 | return SK.key_9; 45 | case WindowsKeyCode.K_A: 46 | return SK.a; 47 | case WindowsKeyCode.K_B: 48 | return SK.b; 49 | case WindowsKeyCode.K_C: 50 | return SK.c; 51 | case WindowsKeyCode.K_D: 52 | return SK.d; 53 | case WindowsKeyCode.K_E: 54 | return SK.e; 55 | case WindowsKeyCode.K_F: 56 | return SK.f; 57 | case WindowsKeyCode.K_G: 58 | return SK.g; 59 | case WindowsKeyCode.K_H: 60 | return SK.h; 61 | case WindowsKeyCode.K_I: 62 | return SK.i; 63 | case WindowsKeyCode.K_J: 64 | return SK.j; 65 | case WindowsKeyCode.K_K: 66 | return SK.k; 67 | case WindowsKeyCode.K_L: 68 | return SK.l; 69 | case WindowsKeyCode.K_M: 70 | return SK.m; 71 | case WindowsKeyCode.K_N: 72 | return SK.n; 73 | case WindowsKeyCode.K_O: 74 | return SK.o; 75 | case WindowsKeyCode.K_P: 76 | return SK.p; 77 | case WindowsKeyCode.K_Q: 78 | return SK.q; 79 | case WindowsKeyCode.K_R: 80 | return SK.r; 81 | case WindowsKeyCode.K_S: 82 | return SK.s; 83 | case WindowsKeyCode.K_T: 84 | return SK.t; 85 | case WindowsKeyCode.K_U: 86 | return SK.u; 87 | case WindowsKeyCode.K_V: 88 | return SK.v; 89 | case WindowsKeyCode.K_W: 90 | return SK.w; 91 | case WindowsKeyCode.K_X: 92 | return SK.x; 93 | case WindowsKeyCode.K_Y: 94 | return SK.y; 95 | case WindowsKeyCode.K_Z: 96 | return SK.z; 97 | case VK_F1: 98 | return SK.f1; 99 | case VK_F2: 100 | return SK.f2; 101 | case VK_F3: 102 | return SK.f3; 103 | case VK_F4: 104 | return SK.f4; 105 | case VK_F5: 106 | return SK.f5; 107 | case VK_F6: 108 | return SK.f6; 109 | case VK_F7: 110 | return SK.f7; 111 | case VK_F8: 112 | return SK.f8; 113 | case VK_F9: 114 | return SK.f9; 115 | case VK_F10: 116 | return SK.f10; 117 | case VK_F11: 118 | return SK.f11; 119 | case VK_F12: 120 | return SK.f12; 121 | case VK_F13: 122 | return SK.f13; 123 | case VK_F14: 124 | return SK.f14; 125 | case VK_F15: 126 | return SK.f15; 127 | case VK_F16: 128 | return SK.f16; 129 | case VK_F17: 130 | return SK.f17; 131 | case VK_F18: 132 | return SK.f18; 133 | case VK_F19: 134 | return SK.f19; 135 | case VK_F20: 136 | return SK.f20; 137 | case VK_F21: 138 | return SK.f21; 139 | case VK_F22: 140 | return SK.f22; 141 | case VK_F23: 142 | return SK.f23; 143 | case VK_F24: 144 | return SK.f24; 145 | case VK_NUMPAD0: 146 | return SK.numpad_0; 147 | case VK_NUMPAD1: 148 | return SK.numpad_1; 149 | case VK_NUMPAD2: 150 | return SK.numpad_2; 151 | case VK_NUMPAD3: 152 | return SK.numpad_3; 153 | case VK_NUMPAD4: 154 | return SK.numpad_4; 155 | case VK_NUMPAD5: 156 | return SK.numpad_5; 157 | case VK_NUMPAD6: 158 | return SK.numpad_6; 159 | case VK_NUMPAD7: 160 | return SK.numpad_7; 161 | case VK_NUMPAD8: 162 | return SK.numpad_8; 163 | case VK_NUMPAD9: 164 | return SK.numpad_9; 165 | case VK_BACK: 166 | return SK.backspace; 167 | case VK_TAB: 168 | return SK.tab; 169 | case VK_ESCAPE: 170 | return SK.escape; 171 | case VK_SPACE: 172 | return SK.space; 173 | case VK_PRIOR: 174 | return SK.page_up; 175 | case VK_NEXT: 176 | return SK.page_down; 177 | case VK_END: 178 | return SK.end; 179 | case VK_HOME: 180 | return SK.home; 181 | case VK_LEFT: 182 | return SK.left; 183 | case VK_RIGHT: 184 | return SK.right; 185 | case VK_UP: 186 | return SK.up; 187 | case VK_DOWN: 188 | return SK.down; 189 | case VK_DELETE: 190 | return SK.del; 191 | case VK_SEPARATOR: 192 | return SK.enter; 193 | case VK_ADD: 194 | return SK.plus; 195 | case VK_OEM_PLUS: 196 | return SK.plus; 197 | case VK_SUBTRACT: 198 | return SK.minus; 199 | case VK_OEM_MINUS: 200 | return SK.minus; 201 | case VK_OEM_PERIOD: 202 | return SK.period; 203 | case VK_OEM_COMMA: 204 | return SK.comma; 205 | case VK_DECIMAL: 206 | return SK.comma; 207 | case VK_MULTIPLY: 208 | return SK.asterisk; 209 | case VK_DIVIDE: 210 | return SK.divide; 211 | case VK_OEM_1: 212 | return SK.oem_1; 213 | case VK_OEM_2: 214 | return SK.oem_2; 215 | case VK_OEM_3: 216 | return SK.oem_3; 217 | case VK_OEM_4: 218 | return SK.oem_4; 219 | case VK_OEM_5: 220 | return SK.oem_5; 221 | case VK_OEM_6: 222 | return SK.oem_6; 223 | case VK_OEM_7: 224 | return SK.oem_7; 225 | case VK_OEM_8: 226 | return SK.oem_8; 227 | case VK_OEM_102: 228 | return SK.oem_102; 229 | case VK_RETURN: 230 | return SK.enter; 231 | 232 | /+ 233 | case VK_CLEAR: return SK.clear; 234 | case VK_SHIFT: return SK.shift; 235 | case VK_CONTROL: return SK.control; 236 | case VK_MENU: return SK.alt; 237 | case VK_CAPITAL: return SK.capslock; 238 | case VK_SELECT: return SK.select; 239 | case VK_PRINT: return SK.print; 240 | case VK_EXECUTE: return SK.execute; 241 | case VK_SNAPSHOT: return SK.print_screen; 242 | case VK_INSERT: return SK.insert; 243 | case VK_HELP: return SK.help; 244 | case VK_LWIN: return SK.windows_left; 245 | case VK_RWIN: return SK.windows_right; 246 | case VK_APPS: return SK.apps; 247 | case VK_SLEEP: return SK.sleep; 248 | case VK_NUMLOCK: return SK.numlock; 249 | case VK_SCROLL: return SK.scroll_lock; 250 | case VK_LSHIFT: return SK.shift_left; 251 | case VK_RSHIFT: return SK.shift_right; 252 | case VK_LCONTROL: return SK.control_left; 253 | case VK_RCONTROL: return SK.control_right; 254 | case VK_LMENU: return SK.menu_left; 255 | case VK_RMENU: return SK.menu_right; 256 | case VK_BROWSER_BACK: return SK.browser_back; 257 | case VK_BROWSER_FORWARD: return SK.browser_forward; 258 | case VK_BROWSER_REFRESH: return SK.browser_refresh; 259 | case VK_BROWSER_STOP: return SK.browser_stop; 260 | case VK_BROWSER_SEARCH: return SK.browser_search; 261 | case VK_BROWSER_FAVORITES: return SK.browser_favorites; 262 | case VK_BROWSER_HOME: return SK.browser_home; 263 | case VK_VOLUME_MUTE: return SK.volume_mute; 264 | case VK_VOLUME_DOWN: return SK.volume_down; 265 | case VK_VOLUME_UP: return SK.volume_up; 266 | case VK_MEDIA_NEXT_TRACK: return SK.media_next; 267 | case VK_MEDIA_PREV_TRACK: return SK.media_prev; 268 | case VK_MEDIA_STOP: return SK.media_stop; 269 | case VK_MEDIA_PLAY_PAUSE: return SK.media_play_pause; 270 | case VK_LAUNCH_MAIL: return SK.launch_mail; 271 | case VK_LAUNCH_MEDIA_SELECT: return SK.launch_media_select; 272 | case VK_LAUNCH_APP1: return SK.launch_app_1; 273 | case VK_LAUNCH_APP2: return SK.launch_app_2;case VK_PACKET: return SK.packet; 274 | case VK_ATTN: return SK.attn; 275 | case VK_CRSEL: return SK.crsel; 276 | case VK_EXSEL: return SK.exsel; 277 | case VK_EREOF: return SK.ereof; 278 | case VK_PLAY: return SK.play; 279 | case VK_ZOOM: return SK.zoom; 280 | case VK_OEM_CLEAR: return SK.oem_clear; 281 | case VK_PAUSE: return SK.pause; 282 | case VK_CANCEL: return SK.cancel; 283 | +/ 284 | } 285 | } 286 | 287 | SCK sconeControlKey() 288 | { 289 | SCK sck; 290 | 291 | DWORD controlKeyState = this.keyEventRecord.dwControlKeyState; 292 | 293 | if (controlKeyState.hasFlag(CAPSLOCK_ON)) 294 | { 295 | sck = sck.withFlag(SCK.capslock); 296 | } 297 | if (controlKeyState.hasFlag(SCROLLLOCK_ON)) 298 | { 299 | sck = sck.withFlag(SCK.scrolllock); 300 | } 301 | if (controlKeyState.hasFlag(SHIFT_PRESSED)) 302 | { 303 | sck = sck.withFlag(SCK.shift); 304 | } 305 | if (controlKeyState.hasFlag(ENHANCED_KEY)) 306 | { 307 | sck = sck.withFlag(SCK.enhanced); 308 | } 309 | if (controlKeyState.hasFlag(LEFT_ALT_PRESSED)) 310 | { 311 | sck = sck.withFlag(SCK.alt); 312 | } 313 | if (controlKeyState.hasFlag(RIGHT_ALT_PRESSED)) 314 | { 315 | sck = sck.withFlag(SCK.alt); 316 | } 317 | if (controlKeyState.hasFlag(LEFT_CTRL_PRESSED)) 318 | { 319 | sck = sck.withFlag(SCK.ctrl); 320 | } 321 | if (controlKeyState.hasFlag(RIGHT_CTRL_PRESSED)) 322 | { 323 | sck = sck.withFlag(SCK.ctrl); 324 | } 325 | if (controlKeyState.hasFlag(NUMLOCK_ON)) 326 | { 327 | sck = sck.withFlag(SCK.numlock); 328 | } 329 | 330 | return sck; 331 | } 332 | 333 | bool pressed() 334 | { 335 | return cast(bool) keyEventRecord.bKeyDown; 336 | } 337 | 338 | private enum WindowsKeyCode 339 | { 340 | K_0 = 0x30, 341 | K_1 = 0x31, 342 | K_2 = 0x32, 343 | K_3 = 0x33, 344 | K_4 = 0x34, 345 | K_5 = 0x35, 346 | K_6 = 0x36, 347 | K_7 = 0x37, 348 | K_8 = 0x38, 349 | K_9 = 0x39, 350 | K_A = 0x41, 351 | K_B = 0x42, 352 | K_C = 0x43, 353 | K_D = 0x44, 354 | K_E = 0x45, 355 | K_F = 0x46, 356 | K_G = 0x47, 357 | K_H = 0x48, 358 | K_I = 0x49, 359 | K_J = 0x4A, 360 | K_K = 0x4B, 361 | K_L = 0x4C, 362 | K_M = 0x4D, 363 | K_N = 0x4E, 364 | K_O = 0x4F, 365 | K_P = 0x50, 366 | K_Q = 0x51, 367 | K_R = 0x52, 368 | K_S = 0x53, 369 | K_T = 0x54, 370 | K_U = 0x55, 371 | K_V = 0x56, 372 | K_W = 0x57, 373 | K_X = 0x58, 374 | K_Y = 0x59, 375 | K_Z = 0x5A, 376 | } 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /source/scone/input/os/windows/windows_input.d: -------------------------------------------------------------------------------- 1 | module scone.os.windows.input.windows_input; 2 | 3 | version (Windows) 4 | { 5 | import scone.input.os.standard_input : StandardInput; 6 | import core.sys.windows.windows; 7 | import scone.output.types.size : Size; 8 | import scone.input.input : Input; 9 | import scone.input.keyboard_event : KeyboardEvent; 10 | import scone.input.scone_control_key : SCK; 11 | import scone.input.scone_key : SK; 12 | import scone.core.flags : hasFlag, withFlag; 13 | import scone.input.os.windows.input.key_event_record_converter : KeyEventRecordConverter; 14 | import std.conv : to; 15 | import std.experimental.logger; 16 | 17 | class WindowsInput : StandardInput 18 | { 19 | void initialize() 20 | { 21 | consoleInputHandle = GetStdHandle(STD_INPUT_HANDLE); 22 | if (consoleInputHandle == INVALID_HANDLE_VALUE) 23 | { 24 | throw new Exception("Cannot initialize input. Got INVALID_HANDLE_VALUE."); 25 | } 26 | } 27 | 28 | void deinitialize() 29 | { 30 | } 31 | 32 | KeyboardEvent[] latestKeyboardEvents() 33 | { 34 | INPUT_RECORD[16] inputRecordBuffer; 35 | DWORD read = 0; 36 | ReadConsoleInput(consoleInputHandle, inputRecordBuffer.ptr, 16, &read); 37 | 38 | KeyboardEvent[] keyboardEvents; 39 | 40 | for (size_t e = 0; e < read; ++e) 41 | { 42 | switch (inputRecordBuffer[e].EventType) 43 | { 44 | default: 45 | break; 46 | case /* 0x0002 */ MOUSE_EVENT: 47 | // mouse has been clicked/moved 48 | break; 49 | case /* 0x0004 */ WINDOW_BUFFER_SIZE_EVENT: 50 | /+ console has been resized 51 | COORD foo = inputRecordBuffer[e].WindowBufferSizeEvent.dwSize; 52 | Size newSize = Size(foo.X, foo.Y); 53 | +/ 54 | break; 55 | case /* 0x0001 */ KEY_EVENT: 56 | auto keyEventRecordConverter = new KeyEventRecordConverter( 57 | inputRecordBuffer[e].KeyEvent); 58 | keyboardEvents ~= KeyboardEvent( 59 | keyEventRecordConverter.sconeKey, 60 | keyEventRecordConverter.sconeControlKey, 61 | keyEventRecordConverter.pressed 62 | ); 63 | break; 64 | } 65 | } 66 | 67 | return keyboardEvents; 68 | } 69 | 70 | private HANDLE consoleInputHandle; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /source/scone/input/package.d: -------------------------------------------------------------------------------- 1 | module scone.input; 2 | 3 | public import scone.input.keyboard_event; 4 | public import scone.input.input; 5 | public import scone.input.scone_control_key; 6 | public import scone.input.scone_key; 7 | -------------------------------------------------------------------------------- /source/scone/input/scone_control_key.d: -------------------------------------------------------------------------------- 1 | module scone.input.scone_control_key; 2 | 3 | enum SCK 4 | { 5 | /// No control key is being pressed 6 | none = 0, 7 | 8 | /// CAPS LOCK light is activated 9 | capslock = 1, 10 | 11 | /// NUM LOCK is activated 12 | numlock = 2, 13 | 14 | /// SCROLL LOCK is activated 15 | scrolllock = 4, 16 | 17 | /// SHIFT key is pressed 18 | shift = 8, 19 | 20 | /// The key is enhanced (?) 21 | enhanced = 16, 22 | 23 | /// Left or right ALT key is pressed 24 | alt = 32, 25 | 26 | /// Left or right CTRL key is pressed 27 | ctrl = 64, 28 | } 29 | -------------------------------------------------------------------------------- /source/scone/input/scone_key.d: -------------------------------------------------------------------------------- 1 | module scone.input.scone_key; 2 | 3 | enum SK 4 | { 5 | /// Unknown key (Should never appear. If it does, please report bug) 6 | unknown, 7 | 8 | /// Control-break processing 9 | cancel, 10 | 11 | /// BACKSPACE key 12 | backspace, 13 | 14 | /// DEL key 15 | del, 16 | 17 | /// TAB key 18 | tab, 19 | 20 | /// ENTER key 21 | enter, 22 | 23 | /// ESC key 24 | escape, 25 | 26 | /// SPACEBAR 27 | space, 28 | 29 | /// PAGE UP key 30 | page_up, 31 | 32 | /// PAGE DOWN key 33 | page_down, 34 | 35 | /// END key 36 | end, 37 | 38 | /// HOME key 39 | home, 40 | 41 | /// LEFT ARROW key 42 | left, 43 | 44 | /// UP ARROW key 45 | up, 46 | 47 | /// RIGHT ARROW key 48 | right, 49 | 50 | /// DOWN ARROW key 51 | down, 52 | 53 | /// 0 key 54 | key_0, 55 | 56 | /// 1 key 57 | key_1, 58 | 59 | /// 2 key 60 | key_2, 61 | 62 | /// 3 key 63 | key_3, 64 | 65 | /// 4 key 66 | key_4, 67 | 68 | /// 5 key 69 | key_5, 70 | 71 | /// 6 key 72 | key_6, 73 | 74 | /// 7 key 75 | key_7, 76 | 77 | /// 8 key 78 | key_8, 79 | 80 | /// 9 key 81 | key_9, 82 | 83 | /// A key 84 | a, 85 | 86 | /// B key 87 | b, 88 | 89 | /// C key 90 | c, 91 | 92 | /// D key 93 | d, 94 | 95 | /// E key 96 | e, 97 | 98 | /// F key 99 | f, 100 | 101 | /// G key 102 | g, 103 | 104 | /// H key 105 | h, 106 | 107 | /// I key 108 | i, 109 | 110 | /// J key 111 | j, 112 | 113 | /// K key 114 | k, 115 | 116 | /// L key 117 | l, 118 | 119 | /// M key 120 | m, 121 | 122 | /// N key 123 | n, 124 | 125 | /// O key 126 | o, 127 | 128 | /// P key 129 | p, 130 | 131 | /// Q key 132 | q, 133 | 134 | /// R key 135 | r, 136 | 137 | /// S key 138 | s, 139 | 140 | /// T key 141 | t, 142 | 143 | /// U key 144 | u, 145 | 146 | /// V key 147 | v, 148 | 149 | /// W key 150 | w, 151 | 152 | /// X key 153 | x, 154 | 155 | /// Y key 156 | y, 157 | 158 | /// Z key 159 | z, 160 | 161 | /// Numeric keypad 0 key 162 | numpad_0, 163 | 164 | /// Numeric keypad 1 key 165 | numpad_1, 166 | 167 | /// Numeric keypad 2 key 168 | numpad_2, 169 | 170 | /// Numeric keypad 3 key 171 | numpad_3, 172 | 173 | /// Numeric keypad 4 key 174 | numpad_4, 175 | 176 | /// Numeric keypad 5 key 177 | numpad_5, 178 | 179 | /// Numeric keypad 6 key 180 | numpad_6, 181 | 182 | /// Numeric keypad 7 key 183 | numpad_7, 184 | 185 | /// Numeric keypad 8 key 186 | numpad_8, 187 | 188 | /// Numeric keypad 9 key 189 | numpad_9, 190 | 191 | /// For any country/region, the '+' key 192 | plus, 193 | 194 | /// For any country/region, the '-' key 195 | minus, 196 | 197 | /// For any country/region, the '.' key 198 | period, 199 | 200 | /// For any country/region, the ',' key 201 | comma, 202 | 203 | /// Asterisk key 204 | asterisk, 205 | 206 | /// Divide key 207 | divide, 208 | 209 | /// F1 key 210 | f1, 211 | 212 | /// F2 key 213 | f2, 214 | 215 | /// F3 key 216 | f3, 217 | 218 | /// F4 key 219 | f4, 220 | 221 | /// F5 key 222 | f5, 223 | 224 | /// F6 key 225 | f6, 226 | 227 | /// F7 key 228 | f7, 229 | 230 | /// F8 key 231 | f8, 232 | 233 | /// F9 key 234 | f9, 235 | 236 | /// F10 key 237 | f10, 238 | 239 | /// F11 key 240 | f11, 241 | 242 | /// F12 key 243 | f12, 244 | 245 | /// F13 key 246 | f13, 247 | 248 | /// F14 key 249 | f14, 250 | 251 | /// F15 key 252 | f15, 253 | 254 | /// F16 key 255 | f16, 256 | 257 | /// F17 key 258 | f17, 259 | 260 | /// F18 key 261 | f18, 262 | 263 | /// F19 key 264 | f19, 265 | 266 | /// F20 key 267 | f20, 268 | 269 | /// F21 key 270 | f21, 271 | 272 | /// F22 key 273 | f22, 274 | 275 | /// F23 key 276 | f23, 277 | 278 | /// F24 key 279 | f24, 280 | 281 | /// Used for miscellaneous characters; it can vary by keyboard. 282 | oem_1, 283 | 284 | /// ditto 285 | oem_2, 286 | 287 | /// ditto 288 | oem_3, 289 | 290 | /// ditto 291 | oem_4, 292 | 293 | /// ditto 294 | oem_5, 295 | 296 | /// ditto 297 | oem_6, 298 | 299 | /// ditto 300 | oem_7, 301 | 302 | /// ditto 303 | oem_8, 304 | 305 | /// Either the angle bracket key or the backslash key on the RT 102-key keyboard 306 | oem_102, 307 | } 308 | -------------------------------------------------------------------------------- /source/scone/output/buffer.d: -------------------------------------------------------------------------------- 1 | module scone.output.buffer; 2 | 3 | import scone.output.types.cell : Cell; 4 | import scone.output.types.color; 5 | import scone.output.types.coordinate : Coordinate; 6 | import scone.output.types.size : Size; 7 | import scone.output.text_style : TextStyle; 8 | import std.range : chunks; 9 | 10 | class Buffer 11 | { 12 | this(Size size) 13 | { 14 | this.size = size; 15 | } 16 | 17 | Size size() 18 | { 19 | return this.bufferSize; 20 | } 21 | 22 | void size(Size size) 23 | { 24 | this.buffer = new Cell[](size.width * size.height); 25 | this.staging = this.buffer.dup; 26 | this.bufferSize = size; 27 | } 28 | 29 | void stage(Coordinate coordinate, Cell cell) 30 | { 31 | assert(coordinate.x < this.bufferSize.width); 32 | assert(coordinate.y < this.bufferSize.height); 33 | assert(coordinate.x >= 0); 34 | assert(coordinate.y >= 0); 35 | 36 | const Cell bufferCell = this.get(coordinate); 37 | 38 | if (cell.style.foreground == Color.same) 39 | { 40 | cell.style.foreground = bufferCell.style.foreground; 41 | } 42 | 43 | if (cell.style.background == Color.same) 44 | { 45 | cell.style.background = bufferCell.style.background; 46 | } 47 | 48 | auto view = this.staging.chunks(this.bufferSize.width); 49 | view[coordinate.y][coordinate.x] = cell; 50 | } 51 | 52 | Coordinate[] diffs() 53 | { 54 | Coordinate[] coordinates; 55 | foreach (i; 0 .. this.buffer.length) 56 | { 57 | if (this.buffer[i] == this.staging[i] && !this.willFlush) 58 | { 59 | continue; 60 | } 61 | 62 | size_t y = cast(size_t)(i / this.bufferSize.width); 63 | size_t x = i % this.bufferSize.width; 64 | 65 | coordinates ~= Coordinate(x, y); 66 | } 67 | 68 | return coordinates; 69 | } 70 | 71 | Cell get(Coordinate coordinate) 72 | { 73 | assert(coordinate.x < this.bufferSize.width); 74 | assert(coordinate.y < this.bufferSize.height); 75 | assert(coordinate.x >= 0); 76 | assert(coordinate.y >= 0); 77 | 78 | auto view = this.staging.chunks(this.bufferSize.width); 79 | return view[coordinate.y][coordinate.x]; 80 | } 81 | 82 | void commit() 83 | { 84 | this.buffer = this.staging.dup; 85 | this.willFlush = false; 86 | } 87 | 88 | void clear() 89 | { 90 | this.staging[] = Cell.init; 91 | } 92 | 93 | void flush() 94 | { 95 | this.willFlush = true; 96 | } 97 | 98 | private Size bufferSize; 99 | private Cell[] buffer, staging; 100 | private bool willFlush = true; 101 | 102 | invariant (this.buffer.length == this.staging.length); 103 | invariant (this.buffer.length == (this.bufferSize.width * this.bufferSize.height)); 104 | } 105 | 106 | unittest 107 | { 108 | auto buffer = new Buffer(Size(4, 2)); 109 | assert(buffer.size == Size(4, 2)); 110 | assert(buffer.diffs.length == 8); 111 | 112 | buffer.commit(); 113 | assert(buffer.diffs.length == 0); 114 | 115 | assert(buffer.get(Coordinate(1, 1)) == Cell()); 116 | buffer.stage(Coordinate(1, 1), Cell('1')); 117 | assert(buffer.get(Coordinate(1, 1)) == Cell('1')); 118 | assert(buffer.diffs == [Coordinate(1, 1)]); 119 | 120 | buffer.commit(); 121 | assert(buffer.get(Coordinate(1, 1)) == Cell('1')); 122 | assert(buffer.diffs.length == 0); 123 | 124 | buffer.flush(); 125 | assert(buffer.diffs.length == 8); 126 | buffer.commit(); 127 | assert(buffer.diffs.length == 0); 128 | 129 | buffer.clear(); 130 | assert(buffer.diffs == [Coordinate(1, 1)]); 131 | buffer.commit(); 132 | 133 | buffer.stage(Coordinate(0, 0), Cell('2', TextStyle(Color.red, Color.green))); 134 | assert(buffer.diffs.length == 1); 135 | buffer.commit(); 136 | buffer.stage(Coordinate(0, 0), Cell('2', TextStyle(Color.same, Color.same))); 137 | assert(buffer.diffs.length == 0); 138 | } 139 | -------------------------------------------------------------------------------- /source/scone/output/frame.d: -------------------------------------------------------------------------------- 1 | module scone.output.frame; 2 | 3 | import scone.output.buffer : Buffer; 4 | import scone.output.types.cell : Cell; 5 | import scone.output.types.color; 6 | import scone.output.types.coordinate : Coordinate; 7 | import scone.output.types.size : Size; 8 | import scone.output.helpers.arguments_to_cells_converter; 9 | import scone.output.os.standard_output : StandardOutput; 10 | import std.concurrency : receiveTimeout; 11 | import std.conv : to; 12 | import std.datetime : Duration; 13 | import std.traits : isNumeric; 14 | 15 | class Frame 16 | { 17 | this(StandardOutput output) 18 | { 19 | output.initialize(); 20 | 21 | this.output = output; 22 | this.output.cursorVisible(false); 23 | this.buffer = new Buffer(output.size()); 24 | } 25 | 26 | ~this() 27 | { 28 | this.output.cursorVisible(true); 29 | output.deinitialize(); 30 | } 31 | 32 | void write(X, Y, Args...)(X tx, Y ty, Args args) 33 | if (isNumeric!X && isNumeric!Y && args.length) 34 | { 35 | Coordinate origin = Coordinate(to!size_t(tx), to!size_t(ty)); 36 | 37 | this.write(origin, args); 38 | } 39 | 40 | void write(Args...)(Coordinate origin, Args args) if (args.length) 41 | { 42 | auto cellConverter = new ArgumentsToCellsConverter!Args(args); 43 | Cell[] cells = cellConverter.cells; 44 | 45 | uint dx, dy; 46 | 47 | foreach (Cell cell; cells) 48 | { 49 | if (cell.character == '\n') 50 | { 51 | dx = 0; 52 | ++dy; 53 | continue; 54 | } 55 | 56 | if (cell.character == '\r') 57 | { 58 | dx = 0; 59 | continue; 60 | } 61 | 62 | if (cell.character == '\t') 63 | { 64 | //todo does not handle coloring until tabstop 65 | enum tabWidth = 4; 66 | dx += tabWidth - ((origin.x + dx) % tabWidth); 67 | continue; 68 | } 69 | 70 | Coordinate coordinate = Coordinate(origin.x + dx, origin.y + dy); 71 | 72 | if (coordinate.x < 0 || coordinate.x >= buffer.size.width) 73 | { 74 | continue; 75 | } 76 | 77 | if (coordinate.y < 0 || coordinate.y >= buffer.size.height) 78 | { 79 | return; 80 | } 81 | 82 | this.buffer.stage(coordinate, cell); 83 | 84 | ++dx; 85 | } 86 | } 87 | 88 | void print() 89 | { 90 | this.output.renderBuffer(this.buffer); 91 | this.buffer.commit(); 92 | this.buffer.clear(); 93 | } 94 | 95 | Size size() 96 | { 97 | return this.buffer.size; 98 | } 99 | 100 | void size(in Size size) 101 | { 102 | this.buffer = new Buffer(size); 103 | this.output.size(size); 104 | } 105 | 106 | void size(in size_t width, in size_t height) 107 | { 108 | this.size(Size(width, height)); 109 | } 110 | 111 | void title(in string title) 112 | { 113 | output.title(title); 114 | } 115 | 116 | private StandardOutput output; 117 | private Buffer buffer; 118 | } 119 | 120 | /// basic 121 | unittest 122 | { 123 | import scone.core.dummy : DummyOutput; 124 | 125 | auto output = new DummyOutput(); 126 | output.size = Size(5, 3); 127 | 128 | auto frame = new Frame(output); 129 | assert(frame.size == Size(5, 3)); 130 | 131 | frame.size(5, 4); 132 | assert(frame.size == Size(5, 4)); 133 | 134 | frame.title("test"); 135 | // todo: assert(frame.title == "test"); 136 | 137 | destroy(frame); 138 | } 139 | /// special control characters 140 | unittest 141 | { 142 | import scone.core.dummy : DummyOutput; 143 | import scone.output.text_style : TextStyle; 144 | 145 | auto output = new DummyOutput(); 146 | output.size = Size(5, 3); 147 | auto frame = new Frame(output); 148 | 149 | frame.write(0, 0, "1"); 150 | assert(frame.buffer.get(Coordinate(0, 0)) == Cell('1')); 151 | 152 | frame.print(); 153 | frame.write(0, 0, "\n2"); 154 | assert(frame.buffer.get(Coordinate(0, 1)) == Cell('2')); 155 | 156 | frame.print(); 157 | frame.write(0, 0, "11\r3"); 158 | assert(frame.buffer.get(Coordinate(0, 0)) == Cell('3')); 159 | assert(frame.buffer.get(Coordinate(1, 0)) == Cell('1')); 160 | 161 | frame.print(); 162 | frame.buffer.commit(); 163 | frame.write(4, 2, "11\n1"); 164 | assert(frame.buffer.diffs == [Coordinate(4, 2)]); 165 | } 166 | /// tab special character. should be changed in the future? 167 | unittest 168 | { 169 | import scone.core.dummy : DummyOutput; 170 | import scone.output.text_style : TextStyle; 171 | 172 | auto output = new DummyOutput(); 173 | output.size = Size(5, 3); 174 | auto frame = new Frame(output); 175 | 176 | frame.print(); 177 | frame.write(2, 0, TextStyle(Color.green), "X"); 178 | frame.write(0, 0, TextStyle(Color.red), "0\t4"); 179 | assert(frame.buffer.get(Coordinate(0, 0)) == Cell('0', TextStyle(Color.red, Color.initial))); 180 | assert(frame.buffer.get(Coordinate(1, 0)) == Cell(' ', TextStyle(Color.initial, Color.initial))); 181 | assert(frame.buffer.get(Coordinate(2, 0)) == Cell('X', TextStyle(Color.green, Color.initial))); 182 | assert(frame.buffer.get(Coordinate(3, 0)) == Cell(' ', TextStyle(Color.initial, Color.initial))); 183 | assert(frame.buffer.get(Coordinate(4, 0)) == Cell('4', TextStyle(Color.red, Color.initial))); 184 | 185 | frame.print(); 186 | frame.write(0, 1, TextStyle().fg(Color.red), TextStyle().bg(Color.green), "X"); 187 | assert(frame.buffer.get(Coordinate(0, 1)) == Cell('X', TextStyle(Color.red, Color.green))); 188 | } 189 | /// cells use its own style, independen of previous text style 190 | unittest 191 | { 192 | import scone.core.dummy : DummyOutput; 193 | import scone.output.text_style : TextStyle, StyledText; 194 | 195 | auto output = new DummyOutput(); 196 | output.size = Size(5, 3); 197 | auto frame = new Frame(output); 198 | 199 | frame.print(); 200 | frame.write(0, 0, TextStyle(Color.blue, Color.yellow), Cell('1', 201 | TextStyle(Color.red, Color.green)), '2'); 202 | assert(frame.buffer.get(Coordinate(0, 0)) == Cell('1', TextStyle(Color.red, Color.green))); 203 | assert(frame.buffer.get(Coordinate(1, 0)) == Cell('2', TextStyle(Color.blue, Color.yellow))); 204 | 205 | frame.print(); 206 | frame.write(0, 0, TextStyle(Color.green, Color.black), "a", StyledText("bc", TextStyle(Color.red, Color 207 | .black)), "d"); 208 | assert(frame.buffer.get(Coordinate(0, 0)) == Cell('a', TextStyle(Color.green, Color.black))); 209 | assert(frame.buffer.get(Coordinate(1, 0)) == Cell('b', TextStyle(Color.red, Color.black))); 210 | assert(frame.buffer.get(Coordinate(2, 0)) == Cell('c', TextStyle(Color.red, Color.black))); 211 | assert(frame.buffer.get(Coordinate(3, 0)) == Cell('d', TextStyle(Color.green, Color.black))); 212 | } 213 | -------------------------------------------------------------------------------- /source/scone/output/helpers/ansi_color_helper.d: -------------------------------------------------------------------------------- 1 | module scone.output.helpers.ansi_color_helper; 2 | 3 | import scone.output.types.color : Color, AnsiColor, ColorState; 4 | 5 | enum AnsiColorType 6 | { 7 | foreground, 8 | background, 9 | } 10 | 11 | auto ansi16ColorNumber(AnsiColor ansi, AnsiColorType type) 12 | { 13 | int number = 0; 14 | 15 | switch (ansi) 16 | { 17 | default: 18 | assert(0); 19 | case AnsiColor.initial: 20 | number = 39; 21 | break; 22 | case AnsiColor.black: 23 | number = 90; 24 | break; 25 | case AnsiColor.red: 26 | number = 91; 27 | break; 28 | case AnsiColor.green: 29 | number = 92; 30 | break; 31 | case AnsiColor.yellow: 32 | number = 93; 33 | break; 34 | case AnsiColor.blue: 35 | number = 94; 36 | break; 37 | case AnsiColor.magenta: 38 | number = 95; 39 | break; 40 | case AnsiColor.cyan: 41 | number = 96; 42 | break; 43 | case AnsiColor.white: 44 | number = 97; 45 | break; 46 | case AnsiColor.blackDark: 47 | number = 30; 48 | break; 49 | case AnsiColor.redDark: 50 | number = 31; 51 | break; 52 | case AnsiColor.greenDark: 53 | number = 32; 54 | break; 55 | case AnsiColor.yellowDark: 56 | number = 33; 57 | break; 58 | case AnsiColor.blueDark: 59 | number = 34; 60 | break; 61 | case AnsiColor.magentaDark: 62 | number = 35; 63 | break; 64 | case AnsiColor.cyanDark: 65 | number = 36; 66 | break; 67 | case AnsiColor.whiteDark: 68 | number = 37; 69 | break; 70 | } 71 | 72 | if (type == AnsiColorType.background) 73 | { 74 | number += 10; 75 | } 76 | 77 | return number; 78 | } 79 | /// 80 | unittest 81 | { 82 | assert(ansi16ColorNumber(AnsiColor.initial, AnsiColorType.foreground) == 39); 83 | 84 | assert(ansi16ColorNumber(AnsiColor.blackDark, AnsiColorType.foreground) == 30); 85 | assert(ansi16ColorNumber(AnsiColor.redDark, AnsiColorType.foreground) == 31); 86 | assert(ansi16ColorNumber(AnsiColor.greenDark, AnsiColorType.foreground) == 32); 87 | assert(ansi16ColorNumber(AnsiColor.yellowDark, AnsiColorType.foreground) == 33); 88 | assert(ansi16ColorNumber(AnsiColor.blueDark, AnsiColorType.foreground) == 34); 89 | assert(ansi16ColorNumber(AnsiColor.magentaDark, AnsiColorType.foreground) == 35); 90 | assert(ansi16ColorNumber(AnsiColor.cyanDark, AnsiColorType.foreground) == 36); 91 | assert(ansi16ColorNumber(AnsiColor.whiteDark, AnsiColorType.foreground) == 37); 92 | 93 | assert(ansi16ColorNumber(AnsiColor.black, AnsiColorType.foreground) == 90); 94 | assert(ansi16ColorNumber(AnsiColor.red, AnsiColorType.foreground) == 91); 95 | assert(ansi16ColorNumber(AnsiColor.green, AnsiColorType.foreground) == 92); 96 | assert(ansi16ColorNumber(AnsiColor.yellow, AnsiColorType.foreground) == 93); 97 | assert(ansi16ColorNumber(AnsiColor.blue, AnsiColorType.foreground) == 94); 98 | assert(ansi16ColorNumber(AnsiColor.magenta, AnsiColorType.foreground) == 95); 99 | assert(ansi16ColorNumber(AnsiColor.cyan, AnsiColorType.foreground) == 96); 100 | assert(ansi16ColorNumber(AnsiColor.white, AnsiColorType.foreground) == 97); 101 | } 102 | /// 103 | unittest 104 | { 105 | assert(ansi16ColorNumber(AnsiColor.initial, AnsiColorType.background) == 49); 106 | 107 | assert(ansi16ColorNumber(AnsiColor.blackDark, AnsiColorType.background) == 40); 108 | assert(ansi16ColorNumber(AnsiColor.redDark, AnsiColorType.background) == 41); 109 | assert(ansi16ColorNumber(AnsiColor.greenDark, AnsiColorType.background) == 42); 110 | assert(ansi16ColorNumber(AnsiColor.yellowDark, AnsiColorType.background) == 43); 111 | assert(ansi16ColorNumber(AnsiColor.blueDark, AnsiColorType.background) == 44); 112 | assert(ansi16ColorNumber(AnsiColor.magentaDark, AnsiColorType.background) == 45); 113 | assert(ansi16ColorNumber(AnsiColor.cyanDark, AnsiColorType.background) == 46); 114 | assert(ansi16ColorNumber(AnsiColor.whiteDark, AnsiColorType.background) == 47); 115 | 116 | assert(ansi16ColorNumber(AnsiColor.black, AnsiColorType.background) == 100); 117 | assert(ansi16ColorNumber(AnsiColor.red, AnsiColorType.background) == 101); 118 | assert(ansi16ColorNumber(AnsiColor.green, AnsiColorType.background) == 102); 119 | assert(ansi16ColorNumber(AnsiColor.yellow, AnsiColorType.background) == 103); 120 | assert(ansi16ColorNumber(AnsiColor.blue, AnsiColorType.background) == 104); 121 | assert(ansi16ColorNumber(AnsiColor.magenta, AnsiColorType.background) == 105); 122 | assert(ansi16ColorNumber(AnsiColor.cyan, AnsiColorType.background) == 106); 123 | assert(ansi16ColorNumber(AnsiColor.white, AnsiColorType.background) == 107); 124 | } 125 | 126 | string ansiColorString(Color foreground, Color background) 127 | { 128 | //dfmt off 129 | import std.conv : text; 130 | 131 | assert(!foreground.state != ColorState.same); 132 | assert(!background.state != ColorState.same); 133 | 134 | if (foreground.state == ColorState.ansi && background.state == ColorState.ansi) 135 | { 136 | auto foregroundNumber = ansi16ColorNumber(foreground.ansi, AnsiColorType.foreground); 137 | auto backgroundNumber = ansi16ColorNumber(background.ansi, AnsiColorType.background); 138 | return text("\033[0;", foregroundNumber, ";", backgroundNumber, "m",); 139 | } 140 | 141 | string ret; 142 | if (foreground.state == ColorState.ansi) 143 | { 144 | ret ~= text("\033[", ansi16ColorNumber(foreground.ansi, AnsiColorType.foreground), "m"); 145 | } 146 | else if (foreground.state == ColorState.rgb) 147 | { 148 | version(Posix) { 149 | ret ~= text("\033[38;2;", foreground.rgb.r, ";", foreground.rgb.g, ";", foreground.rgb.b, "m"); 150 | } else { 151 | // warning: rgb not available on windows 152 | } 153 | } 154 | 155 | if (background.state == ColorState.ansi) 156 | { 157 | ret ~= text("\033[", ansi16ColorNumber(background.ansi, AnsiColorType.background), "m"); 158 | } 159 | else if (background.state == ColorState.rgb) 160 | { 161 | version(Posix) { 162 | ret ~= text("\033[48;2;", background.rgb.r, ";", background.rgb.g, ";", background.rgb.b, "m"); 163 | } else { 164 | // warning: rgb not available on windows 165 | } 166 | } 167 | 168 | return ret; 169 | //dfmt on 170 | } 171 | /// 172 | unittest 173 | { 174 | //dfmt off 175 | assert(ansiColorString(Color.initial, Color.initial) == "\033[0;39;49m"); 176 | assert(ansiColorString(Color.red, Color.red) == "\033[0;91;101m"); 177 | assert(ansiColorString(Color.red, Color.green) == "\033[0;91;102m"); 178 | 179 | version(Posix) 180 | { 181 | assert(ansiColorString(Color.red, Color.rgb(10, 20, 30)) == "\033[91m\033[48;2;10;20;30m"); 182 | assert(ansiColorString(Color.rgb(10, 20, 30), Color.green) == "\033[38;2;10;20;30m\033[102m"); 183 | assert(ansiColorString(Color.rgb(10, 20, 30), Color.rgb(40, 50, 60)) == "\033[38;2;10;20;30m\033[48;2;40;50;60m"); 184 | } 185 | //dfmt on 186 | } 187 | -------------------------------------------------------------------------------- /source/scone/output/helpers/arguments_to_cells_converter.d: -------------------------------------------------------------------------------- 1 | module scone.output.helpers.arguments_to_cells_converter; 2 | 3 | import scone.output.types.cell : Cell; 4 | import scone.output.types.color; 5 | import scone.output.text_style : TextStyle, StyledText; 6 | import std.conv : to; 7 | import std.traits : isImplicitlyConvertible; 8 | 9 | /// convert arguments to Cell[] 10 | class ArgumentsToCellsConverter(Args...) 11 | { 12 | this(Args args) 13 | { 14 | this.args = args; 15 | } 16 | 17 | public Cell[] cells() 18 | { 19 | auto length = this.length(); 20 | if (length == 0) 21 | { 22 | return []; 23 | } 24 | 25 | auto cells = new Cell[](length); 26 | TextStyle textStyle = TextStyle(); 27 | 28 | int i = 0; 29 | foreach (arg; args) 30 | { 31 | alias Type = typeof(arg); 32 | static if (isImplicitlyConvertible!(Type, TextStyle)) 33 | { 34 | if (arg.foreground != Color.same) 35 | { 36 | textStyle.foreground = arg.foreground; 37 | } 38 | 39 | if (arg.background != Color.same) 40 | { 41 | textStyle.background = arg.background; 42 | } 43 | } 44 | else static if (isImplicitlyConvertible!(Type, Cell)) 45 | { 46 | cells[i] = arg; 47 | ++i; 48 | } 49 | else static if (isImplicitlyConvertible!(Type, Cell[])) 50 | { 51 | foreach (cell; arg) 52 | { 53 | cells[i] = cell; 54 | ++i; 55 | } 56 | } 57 | else static if (isImplicitlyConvertible!(Type, StyledText)) 58 | { 59 | foreach (cell; arg.cells) 60 | { 61 | cells[i] = cell; 62 | ++i; 63 | } 64 | } 65 | else static if (isImplicitlyConvertible!(Type, Color)) 66 | { 67 | //logger.warning("`write(x, y, ...)`: Type `Color` passed in, which has no effect"); 68 | } 69 | else 70 | { 71 | foreach (c; to!dstring(arg)) 72 | { 73 | cells[i] = Cell(c, textStyle); 74 | ++i; 75 | } 76 | } 77 | } 78 | 79 | return cells; 80 | } 81 | 82 | /// calculate the length of arguments if converted to Cell[] 83 | private size_t length() 84 | { 85 | int length = 0; 86 | foreach (arg; this.args) 87 | { 88 | alias Type = typeof(arg); 89 | static if (isImplicitlyConvertible!(Type, TextStyle)) 90 | { 91 | continue; 92 | } 93 | else static if (isImplicitlyConvertible!(Type, Color)) 94 | { 95 | continue; 96 | } 97 | else static if (isImplicitlyConvertible!(Type, Cell)) 98 | { 99 | ++length; 100 | continue; 101 | } 102 | else static if (isImplicitlyConvertible!(Type, Cell[])) 103 | { 104 | length += arg.length; 105 | continue; 106 | } 107 | else static if (isImplicitlyConvertible!(Type, StyledText)) 108 | { 109 | length += arg.cells.length; 110 | continue; 111 | } 112 | else 113 | { 114 | length += to!string(arg).length; 115 | } 116 | } 117 | 118 | return length; 119 | } 120 | 121 | private Args args; 122 | } 123 | 124 | unittest 125 | { 126 | auto converter1 = new ArgumentsToCellsConverter!(); 127 | assert(converter1.cells == []); 128 | assert(converter1.length == 0); 129 | 130 | auto converter2 = new ArgumentsToCellsConverter!(int, int, int)(0, 0, 0); 131 | assert(converter2.cells == [Cell('0', TextStyle(Color.same, Color.same)), Cell('0', TextStyle(Color.same, Color 132 | .same)), Cell('0', TextStyle(Color.same, Color.same))]); 133 | assert(converter2.length == 3); 134 | 135 | auto converter3 = new ArgumentsToCellsConverter!(string)("foo"); 136 | assert(converter3.cells == [Cell('f', TextStyle(Color.same, Color.same)), Cell('o', TextStyle(Color.same, Color 137 | .same)), Cell('o', TextStyle(Color.same, Color.same))]); 138 | assert(converter3.length == 3); 139 | 140 | auto converter4 = new ArgumentsToCellsConverter!(Cell, Cell[])(Cell('1'), [Cell('2'), Cell('3')]); 141 | assert(converter4.cells == [Cell('1'), Cell('2'), Cell('3')]); 142 | assert(converter4.length == 3); 143 | } 144 | /// text style 145 | unittest 146 | { 147 | import scone.output.text_style : TextStyle; 148 | 149 | auto converter1 = new ArgumentsToCellsConverter!(TextStyle)(TextStyle() 150 | .fg(Color.red).bg(Color.green)); 151 | assert(converter1.cells == []); 152 | assert(converter1.length == 0); 153 | 154 | auto converter2 = new ArgumentsToCellsConverter!(TextStyle)(TextStyle().fg(Color.red)); 155 | assert(converter2.cells == []); 156 | assert(converter2.length == 0); 157 | 158 | auto converter3 = new ArgumentsToCellsConverter!(TextStyle, string)(TextStyle() 159 | .fg(Color.red), "1"); 160 | assert(converter3.cells == [Cell('1', TextStyle().fg(Color.red))]); 161 | assert(converter3.length == 1); 162 | 163 | auto converter4 = new ArgumentsToCellsConverter!(TextStyle, Cell)(TextStyle() 164 | .fg(Color.red), Cell('1')); 165 | assert(converter4.cells == [Cell('1', TextStyle(Color.initial, Color.initial))]); 166 | assert(converter4.length == 1); 167 | 168 | auto converter5 = new ArgumentsToCellsConverter!(StyledText)(StyledText("abc", TextStyle().fg( 169 | Color.red))); 170 | assert(converter5.cells == [Cell('a', TextStyle(Color.red, Color.same)), Cell('b', TextStyle(Color.red, Color 171 | .same)), Cell('c', TextStyle(Color.red, Color.same))]); 172 | assert(converter5.length == 3); 173 | } 174 | /// only color 175 | unittest 176 | { 177 | auto converter1 = new ArgumentsToCellsConverter!(Color)(Color.green); 178 | assert(converter1.cells == []); 179 | assert(converter1.length == 0); 180 | } 181 | /// immutable and const 182 | unittest 183 | { 184 | immutable(TextStyle) immutableStyle = TextStyle().fg(Color.yellow); 185 | immutable(TextStyle) constStyle = TextStyle().fg(Color.red); 186 | auto converter1 = new ArgumentsToCellsConverter!(immutable(TextStyle), const(TextStyle), string)( 187 | immutableStyle, constStyle, "1"); 188 | assert(converter1.length == 1); 189 | assert(converter1.cells == [Cell('1', TextStyle().fg(Color.red))]); 190 | } 191 | -------------------------------------------------------------------------------- /source/scone/output/helpers/cli.d: -------------------------------------------------------------------------------- 1 | module scone.output.helpers.cli; 2 | 3 | import scone.output.text_style : TextStyle; 4 | import scone.output.types.color : Color; 5 | import scone.output.helpers.ansi_color_helper : ansiColorString; 6 | 7 | /// 8 | string cli(TextStyle style) 9 | { 10 | return ansiColorString(style.foreground, style.background); 11 | } 12 | /// 13 | unittest 14 | { 15 | assert(TextStyle().cli() == ansiColorString(Color.same, Color.same)); 16 | } 17 | -------------------------------------------------------------------------------- /source/scone/output/os/posix/partial_row_output_handler.d: -------------------------------------------------------------------------------- 1 | module scone.output.os.posix.partial_row_output_handler; 2 | 3 | version (Posix) 4 | { 5 | import scone.output.buffer : Buffer; 6 | import scone.output.helpers.ansi_color_helper : ansiColorString, AnsiColorType; 7 | import scone.output.types.cell : Cell; 8 | import scone.output.types.color; 9 | import scone.output.types.coordinate : Coordinate; 10 | import std.algorithm.searching : minElement, maxElement; 11 | import std.conv : text; 12 | 13 | string printDataFromPartialRowOutput(PartialRowOutput[] pros) 14 | { 15 | import std.array : join; 16 | import std.algorithm.iteration : map; 17 | import std.conv : text; 18 | 19 | return pros.map!(pro => text("\033[", pro.coordinate.y + 1, ";", 20 | pro.coordinate.x + 1, "H", pro.output)).join(); 21 | } 22 | /// 23 | unittest 24 | { 25 | PartialRowOutput[] pros; 26 | 27 | pros = [PartialRowOutput(Coordinate(0, 2), "foo")]; 28 | assert(pros.printDataFromPartialRowOutput() == "\033[3;1Hfoo"); 29 | 30 | pros = [ 31 | PartialRowOutput(Coordinate(0, 0), "foo"), 32 | PartialRowOutput(Coordinate(2, 3), "bar"), 33 | ]; 34 | assert(pros.printDataFromPartialRowOutput() == "\033[1;1Hfoo\033[4;3Hbar"); 35 | } 36 | 37 | struct PartialRowOutput 38 | { 39 | Coordinate coordinate; 40 | string output; 41 | } 42 | 43 | struct PartialRowOutputHandler 44 | { 45 | private struct ModifiedRowSection 46 | { 47 | size_t row, firstChangedIndex, lastChangedIndex; 48 | } 49 | 50 | this(Buffer buffer) 51 | { 52 | this.buffer = buffer; 53 | } 54 | 55 | PartialRowOutput[] partialRows() 56 | { 57 | PartialRowOutput[] foos; 58 | 59 | foreach (ModifiedRowSection row; this.modifiedRowSections(buffer)) 60 | { 61 | size_t y = row.row; 62 | string print; 63 | 64 | foreach (x; row.firstChangedIndex .. (row.lastChangedIndex + 1)) 65 | { 66 | Cell currentCell = this.buffer.get(Coordinate(x, y)); 67 | 68 | bool updateColors = false; 69 | 70 | if (x == row.firstChangedIndex) 71 | { 72 | updateColors = true; 73 | } 74 | else 75 | { 76 | assert(x > 0); 77 | immutable Cell previousCell = this.buffer.get(Coordinate(x - 1, y)); 78 | 79 | //todo revisit and see if i though of this correctly 80 | bool a = previousCell.style.foreground != currentCell.style.foreground 81 | || previousCell.style.background != currentCell.style.background; 82 | bool b = currentCell.character == ' ' 83 | && previousCell.style.background == currentCell.style.background; 84 | 85 | if (a && !b) 86 | { 87 | updateColors = true; 88 | } 89 | } 90 | 91 | if (updateColors) 92 | { 93 | print ~= ansiColorString(currentCell.style.foreground, 94 | currentCell.style.background); 95 | } 96 | 97 | print ~= currentCell.character; 98 | } 99 | 100 | PartialRowOutput partialRowOutput; 101 | partialRowOutput.coordinate = Coordinate(row.firstChangedIndex, y); 102 | partialRowOutput.output = print; 103 | 104 | foos ~= partialRowOutput; 105 | } 106 | 107 | return foos; 108 | } 109 | 110 | private ModifiedRowSection[] modifiedRowSections(Buffer buffer) 111 | { 112 | size_t[][size_t] changedCellsMap; 113 | foreach (Coordinate coordinate; buffer.diffs()) 114 | { 115 | if ((coordinate.y in changedCellsMap) is null) 116 | { 117 | changedCellsMap[coordinate.y] = []; 118 | } 119 | 120 | changedCellsMap[coordinate.y] ~= coordinate.x; 121 | } 122 | 123 | ModifiedRowSection[] affectedRows = []; 124 | foreach (y, size_t[] row; changedCellsMap) 125 | { 126 | size_t firstChangedIndex = row.minElement; 127 | size_t lastChangedIndex = row.maxElement; 128 | 129 | ModifiedRowSection dirtyRow; 130 | dirtyRow.row = y; 131 | dirtyRow.firstChangedIndex = firstChangedIndex; 132 | dirtyRow.lastChangedIndex = lastChangedIndex; 133 | 134 | affectedRows ~= dirtyRow; //todo isn't this inefficient 135 | } 136 | 137 | return affectedRows; 138 | } 139 | 140 | private Buffer buffer; 141 | } 142 | 143 | unittest 144 | { 145 | import scone.output.buffer : Buffer; 146 | import scone.output.types.size : Size; 147 | import scone.output.text_style : TextStyle; 148 | 149 | auto buffer = new Buffer(Size(5, 3)); 150 | auto proh = PartialRowOutputHandler(buffer); 151 | 152 | buffer.commit(); 153 | assert(proh.partialRows.length == 0); 154 | assert(proh.partialRows == []); 155 | 156 | buffer.stage(Coordinate(1, 1), Cell('A', TextStyle(Color.red, Color.green))); 157 | assert(proh.partialRows.length == 1); 158 | assert(proh.partialRows == [PartialRowOutput(Coordinate(1, 1), "\033[0;91;102mA")]); 159 | 160 | buffer.commit(); 161 | assert(proh.partialRows.length == 0); 162 | assert(proh.partialRows == []); 163 | 164 | buffer.stage(Coordinate(2, 1), Cell('B', TextStyle(Color.green, Color.red))); 165 | buffer.stage(Coordinate(3, 1), Cell('B', TextStyle(Color.green, Color.red))); 166 | assert(proh.partialRows.length == 1); 167 | assert(proh.partialRows == [PartialRowOutput(Coordinate(2, 1), "\033[0;92;101mBB")]); 168 | 169 | buffer.commit(); 170 | buffer.stage(Coordinate(2, 1), Cell('C', TextStyle(Color.green, Color.red))); 171 | buffer.stage(Coordinate(3, 2), Cell('C', TextStyle(Color.green, Color.red))); 172 | assert(proh.partialRows.length == 2); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /source/scone/output/os/posix/posix_output.d: -------------------------------------------------------------------------------- 1 | module scone.output.os.posix.posix_output; 2 | 3 | version (Posix) 4 | { 5 | import scone.output.os.standard_output : StandardOutput; 6 | import scone.output.types.size : Size; 7 | import scone.output.types.coordinate : Coordinate; 8 | import scone.output.buffer : Buffer; 9 | import scone.output.os.posix.partial_row_output_handler : PartialRowOutputHandler, 10 | PartialRowOutput, printDataFromPartialRowOutput; 11 | 12 | import core.sys.posix.sys.ioctl : ioctl, winsize, TIOCGWINSZ; 13 | import std.stdio : writef, stdout; 14 | import core.sys.posix.unistd : STDOUT_FILENO; 15 | 16 | class PosixOutput : StandardOutput 17 | { 18 | void initialize() 19 | { 20 | this.cursorVisible(false); 21 | this.clear(); 22 | this.lastSize = this.size(); 23 | } 24 | 25 | void deinitialize() 26 | { 27 | this.cursorVisible(true); 28 | this.clear(); 29 | this.cursorPosition(Coordinate(0, 0)); 30 | } 31 | 32 | void renderBuffer(Buffer buffer) 33 | { 34 | auto currentSize = this.size(); 35 | if (currentSize != lastSize) 36 | { 37 | this.clear(); 38 | buffer.size = currentSize; 39 | lastSize = currentSize; 40 | } 41 | 42 | auto proh = PartialRowOutputHandler(buffer); 43 | 44 | .writef(proh.partialRows.printDataFromPartialRowOutput()); 45 | .writef("\033[0m"); 46 | 47 | stdout.flush(); 48 | } 49 | 50 | Size size() 51 | { 52 | winsize w; 53 | ioctl(STDOUT_FILENO, TIOCGWINSZ, &w); 54 | return Size(w.ws_col, w.ws_row); 55 | } 56 | 57 | void size(in Size size) 58 | { 59 | writef("\033[8;%s;%st", size.height, size.width); 60 | stdout.flush(); 61 | } 62 | 63 | void title(in string title) 64 | { 65 | writef("\033]0;%s\007", title); 66 | stdout.flush(); 67 | } 68 | 69 | void cursorVisible(in bool visible) 70 | { 71 | writef("\033[?25%s", visible ? "h" : "l"); 72 | stdout.flush(); 73 | } 74 | 75 | private void cursorPosition(in Coordinate coordinate) 76 | { 77 | writef("\033[%d;%dH", coordinate.y + 1, coordinate.x + 1); 78 | stdout.flush(); 79 | } 80 | 81 | private void clear() 82 | { 83 | writef("\033[2J"); 84 | stdout.flush(); 85 | } 86 | 87 | private Size lastSize; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /source/scone/output/os/standard_output.d: -------------------------------------------------------------------------------- 1 | module scone.output.os.standard_output; 2 | 3 | import scone.output.buffer : Buffer; 4 | import scone.output.types.coordinate : Coordinate; 5 | import scone.output.types.size : Size; 6 | import scone.input.keyboard_event : KeyboardEvent; 7 | 8 | interface StandardOutput 9 | { 10 | void initialize(); 11 | void deinitialize(); 12 | 13 | Size size(); 14 | void size(in Size size); 15 | void title(in string title); 16 | void cursorVisible(in bool visible); 17 | 18 | void renderBuffer(Buffer buffer); 19 | } 20 | -------------------------------------------------------------------------------- /source/scone/output/os/windows/cell_converter.d: -------------------------------------------------------------------------------- 1 | module scone.output.os.windows.cell_converter; 2 | 3 | version (Windows) 4 | { 5 | import core.sys.windows.windows; 6 | import scone.output.types.cell : Cell; 7 | import scone.output.types.color; 8 | import std.conv : ConvOverflowException; 9 | import std.conv : to; 10 | 11 | abstract final class CellConverter 12 | { 13 | public static CHAR_INFO toCharInfo(Cell cell, WORD initialAttributes) 14 | { 15 | wchar unicodeCharacter; 16 | 17 | try 18 | { 19 | unicodeCharacter = to!wchar(cell.character); 20 | } 21 | catch (ConvOverflowException e) 22 | { 23 | unicodeCharacter = '?'; 24 | } 25 | 26 | CHAR_INFO character; 27 | character.UnicodeChar = unicodeCharacter; 28 | character.Attributes = typeof(this).attributesFromCell(cell, initialAttributes); 29 | 30 | return character; 31 | } 32 | 33 | private static WORD attributesFromCell(Cell cell, WORD initialAttributes) 34 | { 35 | WORD attributes; 36 | 37 | switch (cell.style.foreground.ansi) 38 | { 39 | case AnsiColor.initial: 40 | // take the inital colors, and filter out all flags except the foreground ones 41 | attributes |= (initialAttributes & ( 42 | FOREGROUND_INTENSITY | FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE)); 43 | break; 44 | case AnsiColor.blue: 45 | attributes |= FOREGROUND_INTENSITY | FOREGROUND_BLUE; 46 | break; 47 | case AnsiColor.blueDark: 48 | attributes |= FOREGROUND_BLUE; 49 | break; 50 | case AnsiColor.cyan: 51 | attributes |= FOREGROUND_INTENSITY | FOREGROUND_GREEN | FOREGROUND_BLUE; 52 | break; 53 | case AnsiColor.cyanDark: 54 | attributes |= FOREGROUND_GREEN | FOREGROUND_BLUE; 55 | break; 56 | case AnsiColor.white: 57 | attributes |= FOREGROUND_INTENSITY | FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE; 58 | break; 59 | case AnsiColor.whiteDark: 60 | attributes |= FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE; 61 | break; 62 | case AnsiColor.black: 63 | attributes |= FOREGROUND_INTENSITY; 64 | break; 65 | case AnsiColor.blackDark: 66 | attributes |= 0; 67 | break; 68 | case AnsiColor.green: 69 | attributes |= FOREGROUND_INTENSITY | FOREGROUND_GREEN; 70 | break; 71 | case AnsiColor.greenDark: 72 | attributes |= FOREGROUND_GREEN; 73 | break; 74 | case AnsiColor.magenta: 75 | attributes |= FOREGROUND_INTENSITY | FOREGROUND_RED | FOREGROUND_BLUE; 76 | break; 77 | case AnsiColor.magentaDark: 78 | attributes |= FOREGROUND_RED | FOREGROUND_BLUE; 79 | break; 80 | case AnsiColor.red: 81 | attributes |= FOREGROUND_INTENSITY | FOREGROUND_RED; 82 | break; 83 | case AnsiColor.redDark: 84 | attributes |= FOREGROUND_RED; 85 | break; 86 | case AnsiColor.yellow: 87 | attributes |= FOREGROUND_INTENSITY | FOREGROUND_RED | FOREGROUND_GREEN; 88 | break; 89 | case AnsiColor.yellowDark: 90 | attributes |= FOREGROUND_RED | FOREGROUND_GREEN; 91 | break; 92 | default: 93 | break; 94 | } 95 | 96 | switch (cell.style.background.ansi) 97 | { 98 | case AnsiColor.initial: 99 | // take the inital colors, and filter out all flags except the background ones 100 | attributes |= (initialAttributes & ( 101 | BACKGROUND_INTENSITY | BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE)); 102 | break; 103 | case AnsiColor.blue: 104 | attributes |= BACKGROUND_INTENSITY | BACKGROUND_BLUE; 105 | break; 106 | case AnsiColor.blueDark: 107 | attributes |= BACKGROUND_BLUE; 108 | break; 109 | case AnsiColor.cyan: 110 | attributes |= BACKGROUND_INTENSITY | BACKGROUND_GREEN | BACKGROUND_BLUE; 111 | break; 112 | case AnsiColor.cyanDark: 113 | attributes |= BACKGROUND_GREEN | BACKGROUND_BLUE; 114 | break; 115 | case AnsiColor.white: 116 | attributes |= BACKGROUND_INTENSITY | BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE; 117 | break; 118 | case AnsiColor.whiteDark: 119 | attributes |= BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE; 120 | break; 121 | case AnsiColor.black: 122 | attributes |= BACKGROUND_INTENSITY; 123 | break; 124 | case AnsiColor.blackDark: 125 | attributes |= 0; 126 | break; 127 | case AnsiColor.green: 128 | attributes |= BACKGROUND_INTENSITY | BACKGROUND_GREEN; 129 | break; 130 | case AnsiColor.greenDark: 131 | attributes |= BACKGROUND_GREEN; 132 | break; 133 | case AnsiColor.magenta: 134 | attributes |= BACKGROUND_INTENSITY | BACKGROUND_RED | BACKGROUND_BLUE; 135 | break; 136 | case AnsiColor.magentaDark: 137 | attributes |= BACKGROUND_RED | BACKGROUND_BLUE; 138 | break; 139 | case AnsiColor.red: 140 | attributes |= BACKGROUND_INTENSITY | BACKGROUND_RED; 141 | break; 142 | case AnsiColor.redDark: 143 | attributes |= BACKGROUND_RED; 144 | break; 145 | case AnsiColor.yellow: 146 | attributes |= BACKGROUND_INTENSITY | BACKGROUND_RED | BACKGROUND_GREEN; 147 | break; 148 | case AnsiColor.yellowDark: 149 | attributes |= BACKGROUND_RED | BACKGROUND_GREEN; 150 | break; 151 | default: 152 | break; 153 | } 154 | 155 | return attributes; 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /source/scone/output/os/windows/windows_output.d: -------------------------------------------------------------------------------- 1 | module scone.output.os.windows.windows_output; 2 | 3 | version (Windows) 4 | { 5 | import core.sys.windows.windows; 6 | import scone.output.buffer : Buffer; 7 | import scone.output.types.cell : Cell; 8 | import scone.output.types.color; 9 | import scone.output.types.coordinate : Coordinate; 10 | import scone.output.types.size : Size; 11 | import scone.output.os.standard_output : StandardOutput; 12 | import scone.output.os.windows.cell_converter : CellConverter; 13 | import std.conv : ConvOverflowException; 14 | import std.conv : to; 15 | import std.experimental.logger; 16 | 17 | pragma(lib, "User32.lib"); 18 | 19 | extern (Windows) 20 | { 21 | BOOL GetCurrentConsoleFont(HANDLE hConsoleOutput, BOOL bMaximumWindow, 22 | PCONSOLE_FONT_INFO lpConsoleCurrentFont); 23 | COORD GetConsoleFontSize(HANDLE hConsoleOutput, DWORD nFont); 24 | } 25 | 26 | class WindowsOutput : StandardOutput 27 | { 28 | void initialize() 29 | { 30 | oldConsoleOutputHandle = GetStdHandle(STD_OUTPUT_HANDLE); 31 | 32 | consoleOutputHandle = CreateConsoleScreenBuffer(GENERIC_READ | GENERIC_WRITE, 33 | FILE_SHARE_WRITE | FILE_SHARE_READ, null, CONSOLE_TEXTMODE_BUFFER, null); 34 | 35 | if (consoleOutputHandle == INVALID_HANDLE_VALUE) 36 | { 37 | throw new Exception("Cannot initialize output. Got INVALID_HANDLE_VALUE."); 38 | } 39 | 40 | CONSOLE_SCREEN_BUFFER_INFO csbi; 41 | GetConsoleScreenBufferInfo(consoleOutputHandle, &csbi); 42 | this.initialAttributes = csbi.wAttributes; 43 | 44 | SetConsoleActiveScreenBuffer(consoleOutputHandle); 45 | 46 | this.lastSize = this.size(); 47 | } 48 | 49 | void deinitialize() 50 | { 51 | SetConsoleActiveScreenBuffer(oldConsoleOutputHandle); 52 | } 53 | 54 | void size(in Size size) 55 | { 56 | // todo this was copied from my old codebase. this should really be reworked 57 | // todo does not work with windows terminal v1.0.1401.0, see https://github.com/microsoft/terminal/issues/5094 58 | 59 | auto width = size.width; 60 | auto height = size.height; 61 | 62 | // here comes a workaround of windows stange behaviour (in my honest opinion) 63 | // it sets the WINDOW size to 1x1, then sets the BUFFER size (crashes if window size is larger than buffer size), finally setting the correct window size 64 | 65 | //dfmt off 66 | CONSOLE_FONT_INFO consoleFontInfo; 67 | GetCurrentConsoleFont(consoleOutputHandle, FALSE, &consoleFontInfo); 68 | immutable COORD fontSize = GetConsoleFontSize(consoleOutputHandle, consoleFontInfo.nFont); 69 | immutable consoleMinimumPixelWidth = GetSystemMetrics(SM_CXMIN); 70 | immutable consoleMinimumPixelHeight = GetSystemMetrics(SM_CYMIN); 71 | 72 | if (width * fontSize.X < consoleMinimumPixelWidth || height * fontSize.Y < consoleMinimumPixelHeight) 73 | { 74 | sharedLog.warning("Tried to set the window size smaller than allowed. Ignored resize."); 75 | return; 76 | } 77 | 78 | // set window size to 1x1 79 | SMALL_RECT onebyone = {0, 0, 1, 1}; 80 | if (!SetConsoleWindowInfo(consoleOutputHandle, 1, &onebyone)) 81 | { 82 | sharedLog.error("1. Unable to resize window to 1x1: ERROR " ~ to!string(GetLastError())); 83 | return; 84 | } 85 | 86 | // set the buffer size to desired size 87 | COORD bufferSize = {to!short(width), to!short(height)}; 88 | if (!SetConsoleScreenBufferSize(consoleOutputHandle, bufferSize)) 89 | { 90 | sharedLog.error("2. Unable to resize screen buffer: ERROR " ~ to!string(GetLastError())); 91 | return; 92 | } 93 | 94 | // resize back the window to correct size 95 | SMALL_RECT info = {0, 0, to!short(width - 1), to!short(height - 1)}; 96 | if (!SetConsoleWindowInfo(consoleOutputHandle, 1, &info)) 97 | { 98 | sharedLog.error("3. Unable to resize window the second time: ERROR " ~ to!string(GetLastError())); 99 | return; 100 | } 101 | //dfmt on 102 | } 103 | 104 | Size size() 105 | { 106 | CONSOLE_SCREEN_BUFFER_INFO csbi; 107 | GetConsoleScreenBufferInfo(consoleOutputHandle, &csbi); 108 | 109 | return Size(cast(size_t) csbi.srWindow.Right - csbi.srWindow.Left + 1, 110 | cast(size_t) csbi.srWindow.Bottom - csbi.srWindow.Top + 1); 111 | } 112 | 113 | void title(in string title) 114 | { 115 | wstring a = to!wstring(title) ~ "\0"; 116 | SetConsoleTitleW(a.ptr); 117 | } 118 | 119 | void cursorVisible(in bool visible) 120 | { 121 | CONSOLE_CURSOR_INFO cci; 122 | GetConsoleCursorInfo(consoleOutputHandle, &cci); 123 | cci.bVisible = visible; 124 | SetConsoleCursorInfo(consoleOutputHandle, &cci); 125 | } 126 | 127 | void renderBuffer(Buffer buffer) 128 | { 129 | auto currentSize = this.size(); 130 | if (currentSize != lastSize) 131 | { 132 | buffer.size = currentSize; 133 | lastSize = currentSize; 134 | } 135 | 136 | foreach (Coordinate coordinate; buffer.diffs) 137 | { 138 | Cell cell = buffer.get(coordinate); 139 | this.writeCellAt(cell, coordinate); 140 | } 141 | } 142 | 143 | private void writeCellAt(Cell cell, Coordinate coordinate) 144 | { 145 | CHAR_INFO[] charBuffer = [CellConverter.toCharInfo(cell, this.initialAttributes)]; 146 | COORD bufferSize = {1, 1}; 147 | COORD bufferCoord = {0, 0}; 148 | SMALL_RECT writeRegion = { 149 | cast(SHORT) coordinate.x, cast(SHORT) coordinate.y, 150 | cast(SHORT)(coordinate.x + 1), cast(SHORT)(coordinate.y + 1) 151 | }; 152 | 153 | WriteConsoleOutput(consoleOutputHandle, charBuffer.ptr, bufferSize, 154 | bufferCoord, &writeRegion); 155 | } 156 | 157 | private HANDLE windowHandle; 158 | private HANDLE oldConsoleOutputHandle; 159 | private HANDLE consoleOutputHandle; 160 | private Size lastSize; 161 | private WORD initialAttributes; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /source/scone/output/package.d: -------------------------------------------------------------------------------- 1 | module scone.output; 2 | 3 | public import scone.output.frame; 4 | public import scone.output.text_style; 5 | public import scone.output.types.cell; 6 | public import scone.output.types.color; 7 | public import scone.output.types.coordinate; 8 | public import scone.output.types.size; 9 | -------------------------------------------------------------------------------- /source/scone/output/text_style.d: -------------------------------------------------------------------------------- 1 | module scone.output.text_style; 2 | 3 | import scone.output.types.cell : Cell; 4 | import scone.output.types.color : Color; 5 | import std.conv : to; 6 | 7 | struct TextStyle 8 | { 9 | typeof(this) fg(Color color) 10 | { 11 | this.foreground = color; 12 | 13 | return this; 14 | } 15 | 16 | typeof(this) bg(Color color) 17 | { 18 | this.background = color; 19 | 20 | return this; 21 | } 22 | 23 | Color foreground = Color.same; 24 | Color background = Color.same; 25 | } 26 | 27 | unittest 28 | { 29 | auto style1 = TextStyle(); 30 | assert(style1.foreground == Color.same); 31 | assert(style1.background == Color.same); 32 | 33 | auto style2 = TextStyle().bg(Color.green); 34 | assert(style2.foreground == Color.same); 35 | assert(style2.background == Color.green); 36 | 37 | auto style3 = TextStyle(Color.red); 38 | assert(style3.foreground == Color.red); 39 | assert(style3.background == Color.same); 40 | } 41 | 42 | unittest 43 | { 44 | auto style1 = TextStyle().fg(Color.red).bg(Color.green); 45 | assert(style1.foreground == Color.red); 46 | assert(style1.background == Color.green); 47 | 48 | auto style2 = TextStyle(); 49 | style2.foreground = Color.red; 50 | style2.background = Color.green; 51 | 52 | assert(style1 == style2); 53 | } 54 | 55 | struct StyledText 56 | { 57 | this(string text, TextStyle style = TextStyle()) 58 | { 59 | Cell[] ret = new Cell[](text.length); 60 | 61 | foreach (i, c; to!dstring(text)) 62 | { 63 | ret[i] = Cell(c, style); 64 | } 65 | 66 | this.cells = ret; 67 | } 68 | 69 | Cell[] cells; 70 | } 71 | 72 | unittest 73 | { 74 | //dfmt off 75 | auto st1 = StyledText("foo"); 76 | assert 77 | ( 78 | st1.cells == [ 79 | Cell('f', TextStyle(Color.same, Color.same)), 80 | Cell('o', TextStyle(Color.same, Color.same)), 81 | Cell('o', TextStyle(Color.same, Color.same)) 82 | ] 83 | ); 84 | 85 | auto st2 = StyledText("bar", TextStyle().bg(Color.red)); 86 | assert 87 | ( 88 | st2.cells == [ 89 | Cell('b', TextStyle(Color.same, Color.red)), 90 | Cell('a', TextStyle(Color.same, Color.red)), 91 | Cell('r', TextStyle(Color.same, Color.red)), 92 | ] 93 | ); 94 | ///dfmt on 95 | } 96 | -------------------------------------------------------------------------------- /source/scone/output/types/cell.d: -------------------------------------------------------------------------------- 1 | module scone.output.types.cell; 2 | 3 | import scone.output.types.color : Color; 4 | import scone.output.text_style : TextStyle; 5 | 6 | struct Cell 7 | { 8 | dchar character = ' '; 9 | TextStyle style = TextStyle(Color.initial, Color.initial); 10 | } 11 | -------------------------------------------------------------------------------- /source/scone/output/types/color.d: -------------------------------------------------------------------------------- 1 | module scone.output.types.color; 2 | 3 | import scone.output.text_style : TextStyle; 4 | import std.typecons : Nullable; 5 | 6 | enum AnsiColor 7 | { 8 | initial = 16, 9 | 10 | black = 0, 11 | red = 1, 12 | green = 2, 13 | yellow = 3, 14 | blue = 4, 15 | magenta = 5, 16 | cyan = 6, 17 | white = 7, 18 | 19 | blackDark = 8, 20 | redDark = 9, 21 | greenDark = 10, 22 | yellowDark = 11, 23 | blueDark = 12, 24 | magentaDark = 13, 25 | cyanDark = 14, 26 | whiteDark = 15, 27 | } 28 | 29 | struct RGB 30 | { 31 | ubyte r, g, b; 32 | } 33 | 34 | enum ColorState 35 | { 36 | ansi, 37 | rgb, 38 | same, 39 | } 40 | 41 | struct Color 42 | { 43 | static Color same() 44 | { 45 | Color color = Color(); 46 | color.colorState = ColorState.same; 47 | color.ansiColor.nullify(); 48 | color.rgbColor.nullify(); 49 | 50 | return color; 51 | } 52 | 53 | // generate creation methods from AnsiColor 54 | import std.traits : EnumMembers; 55 | import std.conv : to; 56 | 57 | static foreach (member; EnumMembers!AnsiColor) 58 | { 59 | mixin(" 60 | static Color " 61 | ~ to!string(member) ~ "() 62 | { 63 | Color color = Color(); 64 | color.colorState = ColorState.ansi; 65 | color.ansiColor = AnsiColor." 66 | ~ to!string( 67 | member) ~ "; 68 | color.rgbColor.nullify(); 69 | 70 | return color; 71 | } 72 | "); 73 | } 74 | 75 | version (Posix) static Color rgb(ubyte r, ubyte g, ubyte b) 76 | { 77 | Color color = Color(); 78 | color.colorState = ColorState.rgb; 79 | color.ansiColor.nullify(); 80 | color.rgbColor = RGB(r, g, b); 81 | 82 | return color; 83 | } 84 | 85 | ColorState state() 86 | { 87 | return this.colorState; 88 | } 89 | 90 | AnsiColor ansi() 91 | { 92 | assert(!this.ansiColor.isNull); 93 | return this.ansiColor.get(); 94 | } 95 | 96 | version (Posix) RGB rgb() 97 | { 98 | assert(!this.rgbColor.isNull); 99 | return this.rgbColor.get(); 100 | } 101 | 102 | private ColorState colorState = ColorState.ansi; 103 | private Nullable!AnsiColor ansiColor = AnsiColor.initial; 104 | private Nullable!RGB rgbColor; 105 | } 106 | /// 107 | unittest 108 | { 109 | Color a; 110 | assert(a.colorState == ColorState.ansi); 111 | assert(a.ansiColor == AnsiColor.initial); 112 | assert(a.state() == ColorState.ansi); 113 | assert(a.ansi() == AnsiColor.initial); 114 | assert(a.rgbColor.isNull()); 115 | 116 | Color b = Color.red; 117 | assert(b.colorState == ColorState.ansi); 118 | assert(b.ansiColor == AnsiColor.red); 119 | assert(b.state() == ColorState.ansi); 120 | assert(b.ansi() == AnsiColor.red); 121 | assert(b.rgbColor.isNull()); 122 | 123 | version (Posix) 124 | { 125 | Color c = Color.rgb(123, 232, 123); 126 | assert(c.colorState == ColorState.rgb); 127 | assert(c.state() == ColorState.rgb); 128 | assert(c.ansiColor.isNull()); 129 | assert(c.rgbColor == RGB(123, 232, 123)); 130 | assert(c.rgb() == RGB(123, 232, 123)); 131 | } 132 | 133 | Color d = Color.same; 134 | assert(d.colorState == ColorState.same); 135 | assert(d.state() == ColorState.same); 136 | assert(d.ansiColor.isNull()); 137 | assert(d.rgbColor.isNull()); 138 | } 139 | 140 | /// 141 | TextStyle foreground(Color color) 142 | { 143 | return TextStyle().fg(color); 144 | } 145 | /// 146 | unittest 147 | { 148 | assert(TextStyle(Color.red, Color.same) == Color.red.foreground); 149 | assert(TextStyle().fg(Color.red) == foreground(Color.red)); 150 | } 151 | 152 | /// 153 | TextStyle background(Color color) 154 | { 155 | return TextStyle().bg(color); 156 | } 157 | /// 158 | unittest 159 | { 160 | assert(TextStyle(Color.same, Color.green) == Color.green.background); 161 | assert(TextStyle().bg(Color.green) == background(Color.green)); 162 | } 163 | -------------------------------------------------------------------------------- /source/scone/output/types/coordinate.d: -------------------------------------------------------------------------------- 1 | module scone.output.types.coordinate; 2 | 3 | struct Coordinate 4 | { 5 | size_t x, y; 6 | } 7 | -------------------------------------------------------------------------------- /source/scone/output/types/size.d: -------------------------------------------------------------------------------- 1 | module scone.output.types.size; 2 | 3 | struct Size 4 | { 5 | size_t width, height; 6 | } 7 | -------------------------------------------------------------------------------- /source/scone/package.d: -------------------------------------------------------------------------------- 1 | module scone; 2 | 3 | public import scone.core; 4 | public import scone.output; 5 | public import scone.input; 6 | --------------------------------------------------------------------------------