├── .editorconfig ├── .github ├── funding.yml └── workflows │ ├── ci.yaml │ └── publish.yaml ├── .gitignore ├── LICENSE.md ├── NuGet.Config ├── README.md ├── build.cake ├── dotnet-tools.json ├── examples ├── Ansi │ ├── Ansi.csproj │ └── Program.cs ├── Info │ ├── Info.csproj │ └── Program.cs ├── Input │ ├── Input.csproj │ └── Program.cs └── Signals │ ├── Program.cs │ └── Signals.csproj ├── global.json ├── resources └── gfx │ ├── large-logo.png │ ├── medium-logo.png │ └── small-logo.png └── src ├── .editorconfig ├── Directory.Build.props ├── Directory.Build.targets ├── Spectre.Terminals.Tests ├── .editorconfig ├── AnsiSequenceTests.cs ├── Properties │ └── Usings.cs ├── Spectre.Terminals.Tests.csproj └── Utilities │ └── AnsiSequenceVisitor.cs ├── Spectre.Terminals.sln ├── Spectre.Terminals.v3.ncrunchsolution ├── Spectre.Terminals ├── ClearDisplay.cs ├── ClearLine.cs ├── CursorDirection.cs ├── Drivers │ ├── Linux │ │ ├── LinuxDriver.cs │ │ ├── LinuxInterop.Constants.cs │ │ └── LinuxInterop.cs │ ├── MacOS │ │ ├── MacOSDriver.cs │ │ ├── MacOSInterop.Constants.cs │ │ └── MacOSInterop.cs │ ├── Unix │ │ ├── UnixConstants.cs │ │ ├── UnixDriver.cs │ │ ├── UnixTerminalReader.cs │ │ └── UnixTerminalWriter.cs │ └── Windows │ │ ├── Emulation │ │ ├── WindowsColors.cs │ │ ├── WindowsTerminalEmulator.cs │ │ ├── WindowsTerminalEmulatorAdapter.cs │ │ └── WindowsTerminalState.cs │ │ ├── IWindowsTerminalWriter.cs │ │ ├── WindowsConsoleStream.cs │ │ ├── WindowsConstants.cs │ │ ├── WindowsDriver.cs │ │ ├── WindowsKeyReader.cs │ │ ├── WindowsSignals.cs │ │ ├── WindowsTerminalHandle.cs │ │ ├── WindowsTerminalReader.cs │ │ └── WindowsTerminalWriter.cs ├── Emulation │ ├── AnsiInstruction.cs │ ├── AnsiInterpreter.cs │ ├── IAnsiSequenceVisitor.cs │ ├── Instructions │ │ ├── CursorBack.cs │ │ ├── CursorDown.cs │ │ ├── CursorForward.cs │ │ ├── CursorHorizontalAbsolute.cs │ │ ├── CursorNextLine.cs │ │ ├── CursorPosition.cs │ │ ├── CursorPreviousLine.cs │ │ ├── CursorUp.cs │ │ ├── DisableAlternativeBuffer.cs │ │ ├── EnableAlternativeBuffer.cs │ │ ├── EraseInDisplay.cs │ │ ├── EraseInLine.cs │ │ ├── HideCursor.cs │ │ ├── PrintText.cs │ │ ├── RestoreCursor.cs │ │ ├── SelectGraphicRendition.cs │ │ ├── ShowCursor.cs │ │ └── StoreCursor.cs │ └── Parsing │ │ ├── AnsiParser.cs │ │ ├── AnsiToken.cs │ │ └── Tokenization │ │ ├── AnsiSequenceSplitter.cs │ │ ├── AnsiSequenceToken.cs │ │ ├── AnsiSequenceTokenType.cs │ │ └── AnsiSequenceTokenizer.cs ├── Extensions │ ├── EncodingExtensions.cs │ ├── EnumerableExtensions.cs │ ├── ITerminalExtensions.Ansi.cs │ ├── ITerminalExtensions.cs │ └── TerminalOutputExtensions.cs ├── ITerminal.cs ├── ITerminalDriver.cs ├── ITerminalReader.cs ├── ITerminalWriter.cs ├── NativeMethods.txt ├── Polyfill │ └── Nullable.cs ├── Properties │ ├── AssemblyInfo.cs │ └── Usings.cs ├── Spectre.Terminals.csproj ├── Terminal.cs ├── TerminalFactory.cs ├── TerminalInput.cs ├── TerminalOutput.cs ├── TerminalSignal.cs ├── TerminalSignalEventArgs.cs ├── TerminalSize.cs └── Utilities │ ├── EncodingHelper.Unix.cs │ ├── EncodingHelper.cs │ ├── EncodingWithoutPreamble.cs │ ├── MemoryCursor.cs │ └── SynchronizedTextReader.cs └── stylecop.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = CRLF 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = false 9 | trim_trailing_whitespace = true 10 | 11 | [*.sln] 12 | indent_style = tab 13 | 14 | [*.{csproj,vbproj,vcxproj,vcxproj.filters}] 15 | indent_size = 2 16 | 17 | [*.{xml,config,props,targets,nuspec,ruleset}] 18 | indent_size = 2 19 | 20 | [*.{yml,yaml}] 21 | indent_size = 2 22 | 23 | [*.json] 24 | indent_size = 2 25 | 26 | [*.md] 27 | trim_trailing_whitespace = false 28 | 29 | [*.sh] 30 | end_of_line = lf 31 | 32 | [*.cs] 33 | # Prefer file scoped namespace declarations 34 | csharp_style_namespace_declarations = file_scoped:warning 35 | 36 | # Sort using and Import directives with System.* appearing first 37 | dotnet_sort_system_directives_first = true 38 | dotnet_separate_import_directive_groups = false 39 | 40 | # Avoid "this." and "Me." if not necessary 41 | dotnet_style_qualification_for_field = false:refactoring 42 | dotnet_style_qualification_for_property = false:refactoring 43 | dotnet_style_qualification_for_method = false:refactoring 44 | dotnet_style_qualification_for_event = false:refactoring 45 | 46 | # Use language keywords instead of framework type names for type references 47 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion 48 | dotnet_style_predefined_type_for_member_access = true:suggestion 49 | 50 | # Suggest more modern language features when available 51 | dotnet_style_object_initializer = true:suggestion 52 | dotnet_style_collection_initializer = true:suggestion 53 | dotnet_style_coalesce_expression = true:suggestion 54 | dotnet_style_null_propagation = true:suggestion 55 | dotnet_style_explicit_tuple_names = true:suggestion 56 | 57 | # Non-private static fields are PascalCase 58 | dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = suggestion 59 | dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields 60 | dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style 61 | dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field 62 | dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected 63 | dotnet_naming_symbols.non_private_static_fields.required_modifiers = static 64 | dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case 65 | 66 | # Non-private readonly fields are PascalCase 67 | dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.severity = suggestion 68 | dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.symbols = non_private_readonly_fields 69 | dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.style = non_private_readonly_field_style 70 | dotnet_naming_symbols.non_private_readonly_fields.applicable_kinds = field 71 | dotnet_naming_symbols.non_private_readonly_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected 72 | dotnet_naming_symbols.non_private_readonly_fields.required_modifiers = readonly 73 | dotnet_naming_style.non_private_readonly_field_style.capitalization = pascal_case 74 | 75 | # Constants are PascalCase 76 | dotnet_naming_rule.constants_should_be_pascal_case.severity = suggestion 77 | dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants 78 | dotnet_naming_rule.constants_should_be_pascal_case.style = constant_style 79 | dotnet_naming_symbols.constants.applicable_kinds = field, local 80 | dotnet_naming_symbols.constants.required_modifiers = const 81 | dotnet_naming_style.constant_style.capitalization = pascal_case 82 | 83 | # Instance fields are camelCase and start with _ 84 | dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion 85 | dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields 86 | dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style 87 | dotnet_naming_symbols.instance_fields.applicable_kinds = field 88 | dotnet_naming_style.instance_field_style.capitalization = camel_case 89 | dotnet_naming_style.instance_field_style.required_prefix = _ 90 | 91 | # Locals and parameters are camelCase 92 | dotnet_naming_rule.locals_should_be_camel_case.severity = suggestion 93 | dotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters 94 | dotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style 95 | dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local 96 | dotnet_naming_style.camel_case_style.capitalization = camel_case 97 | 98 | # Local functions are PascalCase 99 | dotnet_naming_rule.local_functions_should_be_pascal_case.severity = suggestion 100 | dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions 101 | dotnet_naming_rule.local_functions_should_be_pascal_case.style = local_function_style 102 | dotnet_naming_symbols.local_functions.applicable_kinds = local_function 103 | dotnet_naming_style.local_function_style.capitalization = pascal_case 104 | 105 | # By default, name items with PascalCase 106 | dotnet_naming_rule.members_should_be_pascal_case.severity = suggestion 107 | dotnet_naming_rule.members_should_be_pascal_case.symbols = all_members 108 | dotnet_naming_rule.members_should_be_pascal_case.style = pascal_case_style 109 | dotnet_naming_symbols.all_members.applicable_kinds = * 110 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 111 | 112 | # Newline settings 113 | csharp_new_line_before_open_brace = all 114 | csharp_new_line_before_else = true 115 | csharp_new_line_before_catch = true 116 | csharp_new_line_before_finally = true 117 | csharp_new_line_before_members_in_object_initializers = true 118 | csharp_new_line_before_members_in_anonymous_types = true 119 | csharp_new_line_between_query_expression_clauses = true 120 | 121 | # Indentation preferences 122 | csharp_indent_block_contents = true 123 | csharp_indent_braces = false 124 | csharp_indent_case_contents = true 125 | csharp_indent_case_contents_when_block = true 126 | csharp_indent_switch_labels = true 127 | csharp_indent_labels = flush_left 128 | 129 | # Prefer "var" everywhere 130 | csharp_style_var_for_built_in_types = true:suggestion 131 | csharp_style_var_when_type_is_apparent = true:suggestion 132 | csharp_style_var_elsewhere = true:suggestion 133 | 134 | # Prefer method-like constructs to have a block body 135 | csharp_style_expression_bodied_methods = false:none 136 | csharp_style_expression_bodied_constructors = false:none 137 | csharp_style_expression_bodied_operators = false:none 138 | 139 | # Prefer property-like constructs to have an expression-body 140 | csharp_style_expression_bodied_properties = true:none 141 | csharp_style_expression_bodied_indexers = true:none 142 | csharp_style_expression_bodied_accessors = true:none 143 | 144 | # Suggest more modern language features when available 145 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 146 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 147 | csharp_style_inlined_variable_declaration = true:suggestion 148 | csharp_style_throw_expression = true:suggestion 149 | csharp_style_conditional_delegate_call = true:suggestion 150 | 151 | # Space preferences 152 | csharp_space_after_cast = false 153 | csharp_space_after_colon_in_inheritance_clause = true 154 | csharp_space_after_comma = true 155 | csharp_space_after_dot = false 156 | csharp_space_after_keywords_in_control_flow_statements = true 157 | csharp_space_after_semicolon_in_for_statement = true 158 | csharp_space_around_binary_operators = before_and_after 159 | csharp_space_around_declaration_statements = do_not_ignore 160 | csharp_space_before_colon_in_inheritance_clause = true 161 | csharp_space_before_comma = false 162 | csharp_space_before_dot = false 163 | csharp_space_before_open_square_brackets = false 164 | csharp_space_before_semicolon_in_for_statement = false 165 | csharp_space_between_empty_square_brackets = false 166 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 167 | csharp_space_between_method_call_name_and_opening_parenthesis = false 168 | csharp_space_between_method_call_parameter_list_parentheses = false 169 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 170 | csharp_space_between_method_declaration_name_and_open_parenthesis = false 171 | csharp_space_between_method_declaration_parameter_list_parentheses = false 172 | csharp_space_between_parentheses = false 173 | csharp_space_between_square_brackets = false 174 | 175 | # Blocks are allowed 176 | csharp_prefer_braces = true:silent 177 | csharp_preserve_single_line_blocks = true 178 | csharp_preserve_single_line_statements = true 179 | 180 | # warning RS0037: PublicAPI.txt is missing '#nullable enable' 181 | dotnet_diagnostic.RS0037.severity = none -------------------------------------------------------------------------------- /.github/funding.yml: -------------------------------------------------------------------------------- 1 | github: patriksvensson -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | on: pull_request 3 | 4 | env: 5 | # Set the DOTNET_SKIP_FIRST_TIME_EXPERIENCE environment variable to stop wasting time caching packages 6 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 7 | # Disable sending usage data to Microsoft 8 | DOTNET_CLI_TELEMETRY_OPTOUT: true 9 | 10 | jobs: 11 | 12 | ################################################### 13 | # BUILD 14 | ################################################### 15 | 16 | build: 17 | name: Build 18 | if: "!contains(github.event.head_commit.message, 'skip-ci')" 19 | strategy: 20 | matrix: 21 | kind: ['linux', 'windows', 'macOS'] 22 | include: 23 | - kind: linux 24 | os: ubuntu-latest 25 | - kind: windows 26 | os: windows-latest 27 | - kind: macOS 28 | os: macos-latest 29 | runs-on: ${{ matrix.os }} 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v2 33 | with: 34 | fetch-depth: 0 35 | 36 | - name: 'Get Git tags' 37 | run: git fetch --tags 38 | shell: bash 39 | 40 | - name: Setup dotnet 41 | uses: actions/setup-dotnet@v1 42 | 43 | - name: Build 44 | shell: bash 45 | run: | 46 | dotnet tool restore 47 | dotnet cake -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | branches: 8 | - main 9 | paths: 10 | - 'src/**' 11 | - '.github/**' 12 | - 'examples/**' 13 | 14 | env: 15 | # Set the DOTNET_SKIP_FIRST_TIME_EXPERIENCE environment variable to stop wasting time caching packages 16 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 17 | # Disable sending usage data to Microsoft 18 | DOTNET_CLI_TELEMETRY_OPTOUT: true 19 | 20 | jobs: 21 | 22 | ################################################### 23 | # BUILD 24 | ################################################### 25 | 26 | build: 27 | name: Build 28 | if: "!contains(github.event.head_commit.message, 'skip-ci')" 29 | strategy: 30 | matrix: 31 | kind: ['linux', 'windows', 'macOS'] 32 | include: 33 | - kind: linux 34 | os: ubuntu-latest 35 | - kind: windows 36 | os: windows-latest 37 | - kind: macOS 38 | os: macos-latest 39 | runs-on: ${{ matrix.os }} 40 | steps: 41 | - name: Checkout 42 | uses: actions/checkout@v2 43 | with: 44 | fetch-depth: 0 45 | 46 | - name: 'Get Git tags' 47 | run: git fetch --tags 48 | shell: bash 49 | 50 | - name: Setup dotnet 51 | uses: actions/setup-dotnet@v1 52 | 53 | - name: Build 54 | shell: bash 55 | run: | 56 | dotnet tool restore 57 | dotnet cake 58 | 59 | ################################################### 60 | # PUBLISH 61 | ################################################### 62 | 63 | publish: 64 | name: Publish 65 | needs: [build] 66 | if: "!contains(github.event.head_commit.message, 'skip-ci')" 67 | runs-on: ubuntu-latest 68 | steps: 69 | - name: Checkout 70 | uses: actions/checkout@v2 71 | with: 72 | fetch-depth: 0 73 | 74 | - name: 'Get Git tags' 75 | run: git fetch --tags 76 | shell: bash 77 | 78 | - name: Setup dotnet 79 | uses: actions/setup-dotnet@v1 80 | 81 | - name: Publish 82 | shell: bash 83 | run: | 84 | dotnet tool restore 85 | dotnet cake --target="publish" \ 86 | --nuget-key="${{secrets.NUGET_API_KEY}}" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Misc folders 2 | [Bb]in/ 3 | [Oo]bj/ 4 | [Tt]emp/ 5 | [Pp]ackages/ 6 | /.artifacts/ 7 | /[Tt]ools/ 8 | .idea 9 | .DS_Store 10 | 11 | # Cakeup 12 | cakeup-x86_64-latest.exe 13 | 14 | # .NET Core CLI 15 | /.dotnet/ 16 | /.packages/ 17 | dotnet-install.sh* 18 | *.lock.json 19 | 20 | # Visual Studio 21 | .vs/ 22 | .vscode/ 23 | launchSettings.json 24 | *.sln.ide/ 25 | 26 | # Rider 27 | src/.idea/**/workspace.xml 28 | src/.idea/**/tasks.xml 29 | src/.idea/dictionaries 30 | src/.idea/**/dataSources/ 31 | src/.idea/**/dataSources.ids 32 | src/.idea/**/dataSources.xml 33 | src/.idea/**/dataSources.local.xml 34 | src/.idea/**/sqlDataSources.xml 35 | src/.idea/**/dynamic.xml 36 | src/.idea/**/uiDesigner.xml 37 | 38 | ## Ignore Visual Studio temporary files, build results, and 39 | ## files generated by popular Visual Studio add-ons. 40 | 41 | # User-specific files 42 | *.suo 43 | *.user 44 | *.sln.docstates 45 | *.userprefs 46 | *.GhostDoc.xml 47 | *StyleCop.Cache 48 | 49 | # Build results 50 | [Dd]ebug/ 51 | [Rr]elease/ 52 | x64/ 53 | *_i.c 54 | *_p.c 55 | *.ilk 56 | *.meta 57 | *.obj 58 | *.pch 59 | *.pdb 60 | *.pgc 61 | *.pgd 62 | *.rsp 63 | *.sbr 64 | *.tlb 65 | *.tli 66 | *.tlh 67 | *.tmp 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | 73 | # Visual Studio profiler 74 | *.psess 75 | *.vsp 76 | *.vspx 77 | 78 | # ReSharper is a .NET coding add-in 79 | _ReSharper* 80 | 81 | # NCrunch 82 | .*crunch*.local.xml 83 | _NCrunch_* 84 | 85 | # NuGet Packages Directory 86 | packages 87 | 88 | # Windows 89 | Thumbs.db 90 | 91 | *.received.* 92 | 93 | node_modules -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Patrik Svensson, Phil Scott 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 | -------------------------------------------------------------------------------- /NuGet.Config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spectre.Terminals 2 | 3 | A terminal abstraction with platform specific drivers. 4 | 5 | ## Disclaimer 6 | This is a work in progress, and usage is not yet recommended. 7 | Things will change, move around and break. 8 | 9 | ## Acknowledgement 10 | 11 | Inspired by [system-terminal](https://github.com/alexrp/system-terminal) written by Alex Rønne Petersen. 12 | 13 | ## Features 14 | 15 | - [x] **Windows** 16 | - [x] STDIN 17 | - [x] Read Key 18 | - [x] Read Single Character (if raw mode is enabled) 19 | - [x] Read Line 20 | - [x] Get encoding 21 | - [x] Set encoding 22 | - [x] Redirect to custom reader 23 | - [x] Is handle redirected? 24 | - [x] STDOUT/STDERR 25 | - [x] Write 26 | - [x] Get encoding 27 | - [x] Set encoding 28 | - [x] Redirect to custom writer 29 | - [x] Is handle redirected? 30 | - [x] Raw mode (enable/disable) 31 | - [x] Signals 32 | - [x] SIGINT (CTRL+C) 33 | - [x] SIGQUIT (CTRL+BREAK) 34 | - [x] Window 35 | - [x] Get width 36 | - [x] Get height 37 | - [x] VT/ANSI emulation (Windows only) 38 | - [x] CUU (Cursor up) 39 | - [x] CUD (Cursor down) 40 | - [x] CUF (Cursor forward) 41 | - [x] CUB (Cursor back) 42 | - [x] CNL (Cursor next line) 43 | - [x] CPL (Cursor previous line) 44 | - [x] CHA (Cursor horizontal absolute) 45 | - [X] CUP (Cursor position) 46 | - [X] ED (Erase in display) 47 | - [X] EL (Erase in line) 48 | - [x] SGR (Selection graphic rendition) 49 | - [x] SCP (Save current cursor position) 50 | - [x] RCP (Restore saved cursor position) 51 | - [x] DECTCEM 25 (Hide cursor) 52 | - [x] DECTCEM 25 (Show cursor) 53 | - [x] DECSET 1049 (Enable alternative buffer) 54 | - [x] DECSET 1049 (Disable alternative buffer) 55 | 56 | - [x] **Linux** 57 | - [ ] STDIN 58 | - [ ] Read Key 59 | - [ ] Read Single Character 60 | - [ ] Read Line 61 | - [x] Get encoding 62 | - [x] Set encoding (NOT SUPPORTED) 63 | - [x] Redirect to custom reader 64 | - [x] Is handle redirected? 65 | - [x] STDOUT/STDERR 66 | - [x] Write 67 | - [x] Get encoding 68 | - [x] Set encoding (NOT SUPPORTED) 69 | - [x] Redirect to custom writer 70 | - [x] Is handle redirected? 71 | - [x] Raw mode (enable/disable) 72 | - [x] Signals 73 | - [x] SIGINT 74 | - [x] SIGQUIT 75 | - [x] Window 76 | - [x] Get width 77 | - [x] Get height 78 | 79 | - [x] **macOS** 80 | - [ ] STDIN 81 | - [ ] Read Key 82 | - [ ] Read Single Character 83 | - [ ] Read Line 84 | - [x] Get encoding 85 | - [x] Set encoding (NOT SUPPORTED) 86 | - [x] Redirect to custom reader 87 | - [x] Is handle redirected? 88 | - [x] STDOUT/STDERR 89 | - [x] Write 90 | - [x] Get encoding 91 | - [x] Set encoding (NOT SUPPORTED) 92 | - [x] Redirect to custom writer 93 | - [x] Is handle redirected? 94 | - [x] Raw mode (enable/disable) 95 | - [x] Signals 96 | - [x] SIGINT 97 | - [x] SIGQUIT 98 | - [x] Window 99 | - [x] Get width 100 | - [x] Get height -------------------------------------------------------------------------------- /build.cake: -------------------------------------------------------------------------------- 1 | var target = Argument("target", "Default"); 2 | var configuration = Argument("configuration", "Release"); 3 | 4 | //////////////////////////////////////////////////////////////// 5 | // Tasks 6 | 7 | Task("Build") 8 | .Does(context => 9 | { 10 | DotNetBuild("./src/Spectre.Terminals.sln", new DotNetBuildSettings { 11 | Configuration = configuration, 12 | NoIncremental = context.HasArgument("rebuild"), 13 | MSBuildSettings = new DotNetCoreMSBuildSettings() 14 | .TreatAllWarningsAs(MSBuildTreatAllWarningsAs.Error) 15 | }); 16 | }); 17 | 18 | Task("Test") 19 | .IsDependentOn("Build") 20 | .Does(context => 21 | { 22 | DotNetTest("./src/Spectre.Terminals.Tests/Spectre.Terminals.Tests.csproj", new DotNetTestSettings { 23 | Configuration = configuration, 24 | NoRestore = true, 25 | NoBuild = true, 26 | }); 27 | }); 28 | 29 | Task("Package") 30 | .IsDependentOn("Test") 31 | .Does(context => 32 | { 33 | context.CleanDirectory("./.artifacts"); 34 | 35 | context.DotNetPack($"./src/Spectre.Terminals.sln", new DotNetPackSettings { 36 | Configuration = configuration, 37 | NoRestore = true, 38 | NoBuild = true, 39 | OutputDirectory = "./.artifacts", 40 | MSBuildSettings = new DotNetMSBuildSettings() 41 | .TreatAllWarningsAs(MSBuildTreatAllWarningsAs.Error) 42 | }); 43 | }); 44 | 45 | Task("Publish-NuGet") 46 | .WithCriteria(ctx => BuildSystem.IsRunningOnGitHubActions, "Not running on GitHub Actions") 47 | .IsDependentOn("Package") 48 | .Does(context => 49 | { 50 | var apiKey = Argument("nuget-key", null); 51 | if(string.IsNullOrWhiteSpace(apiKey)) { 52 | throw new CakeException("No NuGet API key was provided."); 53 | } 54 | 55 | // Publish to GitHub Packages 56 | foreach(var file in context.GetFiles("./.artifacts/*.nupkg")) 57 | { 58 | context.Information("Publishing {0}...", file.GetFilename().FullPath); 59 | DotNetNuGetPush(file.FullPath, new DotNetNuGetPushSettings 60 | { 61 | Source = "https://api.nuget.org/v3/index.json", 62 | ApiKey = apiKey, 63 | }); 64 | } 65 | }); 66 | 67 | //////////////////////////////////////////////////////////////// 68 | // Targets 69 | 70 | Task("Publish") 71 | .IsDependentOn("Publish-NuGet"); 72 | 73 | Task("Default") 74 | .IsDependentOn("Package"); 75 | 76 | //////////////////////////////////////////////////////////////// 77 | // Execution 78 | 79 | RunTarget(target) -------------------------------------------------------------------------------- /dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "cake.tool": { 6 | "version": "2.2.0", 7 | "commands": [ 8 | "dotnet-cake" 9 | ] 10 | }, 11 | "dotnet-example": { 12 | "version": "1.6.0", 13 | "commands": [ 14 | "dotnet-example" 15 | ] 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /examples/Ansi/Ansi.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | false 7 | Ansi 8 | Demonstrates how to write VT/ANSI codes to the terminal 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/Ansi/Program.cs: -------------------------------------------------------------------------------- 1 | using Spectre.Terminals; 2 | 3 | namespace Examples; 4 | 5 | public static class Program 6 | { 7 | public static void Main(string[] args) 8 | { 9 | var terminal = Terminal.Shared; 10 | 11 | // Information 12 | terminal.WriteLine("\u001b[2J\u001b[1;1HSpectre.Terminal"); 13 | terminal.WriteLine(); 14 | terminal.WriteLine($" Terminal driver = {terminal.Name}"); 15 | terminal.WriteLine($" Window size = {terminal.Size}"); 16 | terminal.WriteLine($"Output redirected = {terminal.Output.IsRedirected}"); 17 | terminal.WriteLine($" Output encoding = {terminal.Output.Encoding.EncodingName}"); 18 | terminal.WriteLine($" Error redirected = {terminal.Error.IsRedirected}"); 19 | terminal.WriteLine($" Error encoding = {terminal.Error.Encoding.EncodingName}"); 20 | terminal.WriteLine($" Input redirected = {terminal.Input.IsRedirected}"); 21 | terminal.WriteLine($" Input encoding = {terminal.Input.Encoding.EncodingName}"); 22 | terminal.WriteLine(); 23 | terminal.WriteLine("Press ANY key"); 24 | terminal.WriteLine(); 25 | 26 | // Do some line manipulation 27 | terminal.Write("\u001b[6;8H[Delete after]\u001b[0K"); 28 | terminal.Write("\u001b[5;15H\u001b[1K[Delete before]"); 29 | terminal.Write("\u001b[4;15H\u001b[2K[Delete line]"); 30 | 31 | // Write some text in an alternate buffer 32 | terminal.Write("\u001b[?1049h"); 33 | terminal.WriteLine("HELLO WORLD!"); 34 | terminal.Write("\u001b[?1049l"); 35 | terminal.Write(""); 36 | 37 | terminal.Output.WriteLine("Goodbye!"); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /examples/Info/Info.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | false 7 | Info 8 | Shows terminal capabilities 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/Info/Program.cs: -------------------------------------------------------------------------------- 1 | using Spectre.Terminals; 2 | 3 | namespace Examples; 4 | 5 | public static class Program 6 | { 7 | public static void Main(string[] args) 8 | { 9 | var terminal = Terminal.Shared; 10 | 11 | terminal.WriteLine(); 12 | terminal.WriteLine("\u001b[38;5;15m\u001b[48;5;9mSpectre.Terminals\u001b[0m"); 13 | terminal.WriteLine(); 14 | terminal.WriteLine($" Terminal driver = {terminal.Name}"); 15 | terminal.WriteLine($" Window size = {terminal.Size}"); 16 | terminal.WriteLine($"Output redirected = {terminal.Output.IsRedirected}"); 17 | terminal.WriteLine($" Output encoding = {terminal.Output.Encoding.EncodingName}"); 18 | terminal.WriteLine($" Error redirected = {terminal.Error.IsRedirected}"); 19 | terminal.WriteLine($" Error encoding = {terminal.Error.Encoding.EncodingName}"); 20 | terminal.WriteLine($" Input redirected = {terminal.Input.IsRedirected}"); 21 | terminal.WriteLine($" Input encoding = {terminal.Input.Encoding.EncodingName}"); 22 | terminal.WriteLine(); 23 | 24 | WriteColors(terminal); 25 | } 26 | 27 | private static void WriteColors(ITerminal terminal) 28 | { 29 | terminal.WriteLine("Spectre.Terminals"); 30 | for (var i = 0; i < 16; i++) 31 | { 32 | terminal.Write($"\u001b[38;5;{i}m\u001b[48;5;{i}m \u001b[0m"); 33 | } 34 | 35 | terminal.WriteLine(); 36 | terminal.WriteLine("System.Console"); 37 | for (var i = 0; i < 16; i++) 38 | { 39 | System.Console.BackgroundColor = GetColor(i); 40 | System.Console.Write(" "); 41 | } 42 | System.Console.ResetColor(); 43 | } 44 | 45 | private static System.ConsoleColor GetColor(int number) 46 | { 47 | return number switch 48 | { 49 | 0 => System.ConsoleColor.Black, // 0 50 | 1 => System.ConsoleColor.DarkRed, // 4 51 | 2 => System.ConsoleColor.DarkGreen, // 2 52 | 3 => System.ConsoleColor.DarkYellow, // 6 53 | 4 => System.ConsoleColor.DarkBlue, // 1 54 | 5 => System.ConsoleColor.DarkMagenta, // 5 55 | 6 => System.ConsoleColor.DarkCyan, // 3 56 | 7 => System.ConsoleColor.Gray, // 7 57 | 8 => System.ConsoleColor.DarkGray, // 8 58 | 9 => System.ConsoleColor.Red, // 12 59 | 10 => System.ConsoleColor.Green, // 10 60 | 11 => System.ConsoleColor.Yellow, // 14 61 | 12 => System.ConsoleColor.Blue, // 9 62 | 13 => System.ConsoleColor.Magenta, // 13 63 | 14 => System.ConsoleColor.Cyan, // 11 64 | 15 => System.ConsoleColor.White, // 15 65 | _ => throw new System.InvalidOperationException("Cannot convert color to console color."), 66 | }; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /examples/Input/Input.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | false 7 | Input 8 | Demonstrates how to receive input from STDIN 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/Input/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Spectre.Terminals; 3 | 4 | namespace Examples; 5 | 6 | public static class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | ReadLine(); 11 | ReadKeys(); 12 | } 13 | 14 | private static void ReadLine() 15 | { 16 | Terminal.Shared.Write("Write something> "); 17 | var line = Terminal.Shared.Input.ReadLine(); 18 | Terminal.Shared.WriteLine($"Read = {line}"); 19 | } 20 | 21 | private static void ReadKeys() 22 | { 23 | Terminal.Shared.WriteLine(); 24 | Terminal.Shared.WriteLine("[Press any keys]"); 25 | 26 | while (true) 27 | { 28 | // Read a key from the keyboard 29 | var key = Terminal.Shared.Input.ReadKey(); 30 | if (key.Key == ConsoleKey.Escape) 31 | { 32 | break; 33 | } 34 | 35 | // Get the character representation 36 | var character = !char.IsWhiteSpace(key.KeyChar) 37 | ? key.KeyChar : '*'; 38 | 39 | // Write to terminal 40 | Terminal.Shared.WriteLine( 41 | $"{character} [KEY={key.Key} MOD={key.Modifiers}]"); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/Signals/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using Spectre.Terminals; 3 | 4 | namespace Examples; 5 | 6 | public static class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | var terminal = Terminal.Shared; 11 | var cancel = new ManualResetEvent(false); 12 | 13 | // Hook up signal handling 14 | terminal.Signalled += (s, e) => 15 | { 16 | if (e.Signal == TerminalSignal.SIGINT) 17 | { 18 | terminal.WriteLine("Received \u001b[38;5;14mSIGINT\u001b[0m"); 19 | e.Cancel = true; 20 | cancel.Set(); 21 | } 22 | else if(e.Signal == TerminalSignal.SIGQUIT) 23 | { 24 | terminal.WriteLine("Received \u001b[38;5;14mSIGQUIT\u001b[0m"); 25 | e.Cancel = true; 26 | cancel.Set(); 27 | } 28 | }; 29 | 30 | // Wait for a signal 31 | terminal.WriteLine("Press CTRL+C or CTRL+BREAK to quit"); 32 | cancel.WaitOne(); 33 | terminal.WriteLine("Bye!"); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/Signals/Signals.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | false 7 | Signals 8 | Demonstrates how to listen for signals 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "projects": [ "src" ], 3 | "sdk": { 4 | "version": "6.0.300", 5 | "rollForward": "latestFeature" 6 | } 7 | } -------------------------------------------------------------------------------- /resources/gfx/large-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spectreconsole/terminal/10cef58def869b3e5141121a05e80271e951dfee/resources/gfx/large-logo.png -------------------------------------------------------------------------------- /resources/gfx/medium-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spectreconsole/terminal/10cef58def869b3e5141121a05e80271e951dfee/resources/gfx/medium-logo.png -------------------------------------------------------------------------------- /resources/gfx/small-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spectreconsole/terminal/10cef58def869b3e5141121a05e80271e951dfee/resources/gfx/small-logo.png -------------------------------------------------------------------------------- /src/.editorconfig: -------------------------------------------------------------------------------- 1 | root = false 2 | 3 | [*.cs] 4 | # IDE0055: Fix formatting 5 | dotnet_diagnostic.IDE0055.severity = warning 6 | 7 | # SA1101: Prefix local calls with this 8 | dotnet_diagnostic.SA1101.severity = none 9 | 10 | # SA1633: File should have header 11 | dotnet_diagnostic.SA1633.severity = none 12 | 13 | # SA1201: Elements should appear in the correct order 14 | dotnet_diagnostic.SA1201.severity = none 15 | 16 | # SA1202: Public members should come before private members 17 | dotnet_diagnostic.SA1202.severity = none 18 | 19 | # SA1309: Field names should not begin with underscore 20 | dotnet_diagnostic.SA1309.severity = none 21 | 22 | # SA1404: Code analysis suppressions should have justification 23 | dotnet_diagnostic.SA1404.severity = none 24 | 25 | # SA1516: Elements should be separated by a blank line 26 | dotnet_diagnostic.SA1516.severity = none 27 | 28 | # CA1303: Do not pass literals as localized parameters 29 | dotnet_diagnostic.CA1303.severity = none 30 | 31 | # CSA1204: Static members should appear before non-static members 32 | dotnet_diagnostic.SA1204.severity = none 33 | 34 | # IDE0052: Remove unread private members 35 | dotnet_diagnostic.IDE0052.severity = warning 36 | 37 | # IDE0063: Use simple 'using' statement 38 | csharp_prefer_simple_using_statement = false:suggestion 39 | 40 | # IDE0018: Variable declaration can be inlined 41 | dotnet_diagnostic.IDE0018.severity = warning 42 | 43 | # SA1625: Element documenation should not be copied and pasted 44 | dotnet_diagnostic.SA1625.severity = none 45 | 46 | # IDE0005: Using directive is unnecessary 47 | dotnet_diagnostic.IDE0005.severity = warning 48 | 49 | # SA1117: Parameters should be on same line or separate lines 50 | dotnet_diagnostic.SA1117.severity = none 51 | 52 | # SA1404: Code analysis suppression should have justification 53 | dotnet_diagnostic.SA1404.severity = none 54 | 55 | # SA1101: Prefix local calls with this 56 | dotnet_diagnostic.SA1101.severity = none 57 | 58 | # SA1633: File should have header 59 | dotnet_diagnostic.SA1633.severity = none 60 | 61 | # SA1649: File name should match first type name 62 | dotnet_diagnostic.SA1649.severity = none 63 | 64 | # SA1402: File may only contain a single type 65 | dotnet_diagnostic.SA1402.severity = none 66 | 67 | # CA1814: Prefer jagged arrays over multidimensional 68 | dotnet_diagnostic.CA1814.severity = none 69 | 70 | # RCS1194: Implement exception constructors. 71 | dotnet_diagnostic.RCS1194.severity = none 72 | 73 | # CA1032: Implement standard exception constructors 74 | dotnet_diagnostic.CA1032.severity = none 75 | 76 | # CA1826: Do not use Enumerable methods on indexable collections. Instead use the collection directly 77 | dotnet_diagnostic.CA1826.severity = none 78 | 79 | # RCS1079: Throwing of new NotImplementedException. 80 | dotnet_diagnostic.RCS1079.severity = warning 81 | 82 | # RCS1057: Add empty line between declarations. 83 | dotnet_diagnostic.RCS1057.severity = none 84 | 85 | # RCS1057: Validate arguments correctly 86 | dotnet_diagnostic.RCS1227.severity = none 87 | 88 | # IDE0004: Remove Unnecessary Cast 89 | dotnet_diagnostic.IDE0004.severity = warning 90 | 91 | # CA1810: Initialize reference type static fields inline 92 | dotnet_diagnostic.CA1810.severity = none 93 | 94 | # IDE0044: Add readonly modifier 95 | dotnet_diagnostic.IDE0044.severity = warning -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | 10.0 5 | true 6 | embedded 7 | true 8 | true 9 | false 10 | enable 11 | 12 | 13 | 14 | true 15 | 16 | 17 | 18 | A terminal abstraction with platform specific drivers. 19 | Patrik Svensson, Phil Scott 20 | Patrik Svensson, Phil Scott 21 | git 22 | https://github.com/spectreconsole/terminal 23 | small-logo.png 24 | True 25 | https://github.com/spectreconsole/terminal 26 | MIT 27 | 28 | 29 | 30 | true 31 | true 32 | 33 | 34 | 35 | 36 | 37 | 38 | all 39 | runtime; build; native; contentfiles; analyzers; buildtransitive 40 | 41 | 42 | All 43 | 44 | 45 | All 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | preview 5 | normal 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/Spectre.Terminals.Tests/.editorconfig: -------------------------------------------------------------------------------- 1 | root = false 2 | [*.cs] 3 | 4 | # Default severity for analyzer diagnostics with category 'StyleCop.CSharp.DocumentationRules' 5 | dotnet_analyzer_diagnostic.category-StyleCop.CSharp.DocumentationRules.severity = none 6 | 7 | # CA1707: Identifiers should not contain underscores 8 | dotnet_diagnostic.CA1707.severity = none 9 | 10 | # SA1600: Elements should be documented 11 | dotnet_diagnostic.SA1600.severity = none 12 | 13 | # SA1601: Partial elements should be documented 14 | dotnet_diagnostic.SA1601.severity = none 15 | 16 | # SA1200: Using directives should be placed correctly 17 | dotnet_diagnostic.SA1200.severity = none 18 | 19 | # CS1591: Missing XML comment for publicly visible type or member 20 | dotnet_diagnostic.CS1591.severity = none 21 | 22 | # SA1210: Using directives should be ordered alphabetically by namespace 23 | dotnet_diagnostic.SA1210.severity = none 24 | 25 | # SA1516: Elements should be separated by blank line 26 | dotnet_diagnostic.SA1516.severity = none 27 | 28 | # CA1034: Nested types should not be visible 29 | dotnet_diagnostic.CA1034.severity = none 30 | 31 | # CA2000: Dispose objects before losing scope 32 | dotnet_diagnostic.CA2000.severity = none 33 | 34 | # SA1118: Parameter should not span multiple lines 35 | dotnet_diagnostic.SA1118.severity = none 36 | 37 | # CA1031: Do not catch general exception types 38 | dotnet_diagnostic.CA1031.severity = none -------------------------------------------------------------------------------- /src/Spectre.Terminals.Tests/AnsiSequenceTests.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals.Tests; 2 | 3 | public sealed class AnsiSequenceTests 4 | { 5 | public sealed class TheInterpretMethod 6 | { 7 | [Fact] 8 | public void Should_Interpret_Codes_As_Expected() 9 | { 10 | // Given 11 | var printer = new AnsiPrinter(); 12 | var state = new StringBuilder(); 13 | 14 | // When 15 | AnsiInterpreter.Interpret( 16 | printer, state, 17 | "\u001b[?1049h " + 18 | "\u001b[?25l\u001b[2KHello World!\u001b[?25h " + 19 | "\u001b[3A \u001b[4B \u001b[5C \u001b[6D " + 20 | "\u001b[7E \u001b[8F \u001b[9G \u001b[10;11H " + 21 | "\u001b[0J \u001b[1J \u001b[2J \u001b[3J " + 22 | "\u001b[0K \u001b[1K \u001b[2K " + 23 | "\u001b[s \u001b[u " + 24 | "\u001b[?1049l"); 25 | 26 | // Then 27 | state.ToString() 28 | .ShouldBe( 29 | "[EnableAltBuffer] " + 30 | "[HideCursor][EL2]Hello World![ShowCursor] " + 31 | "[CUU3] [CUD4] [CUF5] [CUB6] " + 32 | "[CNL7] [CPL8] [CHA9] [CUP10,11] " + 33 | "[ED0] [ED1] [ED2] [ED3] " + 34 | "[EL0] [EL1] [EL2] " + 35 | "[SaveCursor] [RestoreCursor] " + 36 | "[DisableAltBuffer]"); 37 | } 38 | 39 | [Theory] 40 | [InlineData("\u001b[35m", "[SGR-FG=5]")] 41 | [InlineData("\u001b[38;5;29m", "[SGR-FG=29]")] 42 | [InlineData("\u001b[38;2;92;128;255m", "[SGR-FG=92,128,255]")] 43 | [InlineData("\u001b[42m", "[SGR-BG=2]")] 44 | [InlineData("\u001b[48;5;29m", "[SGR-BG=29]")] 45 | [InlineData("\u001b[48;2;92;128;255m", "[SGR-BG=92,128,255]")] 46 | [InlineData("\u001b[0m", "[SGR-RESET]")] 47 | public void Should_Interpret_SGR_Attributes_Correctly(string input, string expected) 48 | { 49 | // Given 50 | var printer = new AnsiPrinter(); 51 | var state = new StringBuilder(); 52 | 53 | // When 54 | AnsiInterpreter.Interpret(printer, state, input); 55 | 56 | // Then 57 | state.ToString().ShouldBe(expected); 58 | } 59 | 60 | [Theory] 61 | [InlineData("\u001b[38;5m")] 62 | [InlineData("\u001b[38;2;92;128m")] 63 | [InlineData("\u001b[38;2;92m")] 64 | [InlineData("\u001b[38;2m")] 65 | [InlineData("\u001b[48;5m")] 66 | [InlineData("\u001b[48;2;92;128m")] 67 | [InlineData("\u001b[48;2;92m")] 68 | [InlineData("\u001b[48;2m")] 69 | public void Should_Not_Parse_Malformed_SGR_Attributes(string input) 70 | { 71 | // Given 72 | var printer = new AnsiPrinter(); 73 | var state = new StringBuilder(); 74 | 75 | // When 76 | AnsiInterpreter.Interpret(printer, state, input); 77 | 78 | // Then 79 | state.Length.ShouldBe(0); 80 | } 81 | 82 | private sealed class AnsiPrinter : AnsiSequenceVisitor 83 | { 84 | protected override void PrintText(PrintText instruction, StringBuilder state) 85 | { 86 | state.Append(instruction.Text); 87 | } 88 | 89 | protected override void ShowCursor(ShowCursor instruction, StringBuilder state) 90 | { 91 | state.Append("[ShowCursor]"); 92 | } 93 | 94 | protected override void HideCursor(HideCursor instruction, StringBuilder state) 95 | { 96 | state.Append("[HideCursor]"); 97 | } 98 | 99 | protected override void CursorUp(CursorUp instruction, StringBuilder state) 100 | { 101 | state.Append("[CUU").Append(instruction.Count).Append(']'); 102 | } 103 | 104 | protected override void CursorDown(CursorDown instruction, StringBuilder state) 105 | { 106 | state.Append("[CUD").Append(instruction.Count).Append(']'); 107 | } 108 | 109 | protected override void CursorBack(CursorBack instruction, StringBuilder state) 110 | { 111 | state.Append("[CUB").Append(instruction.Count).Append(']'); 112 | } 113 | 114 | protected override void CursorForward(CursorForward instruction, StringBuilder state) 115 | { 116 | state.Append("[CUF").Append(instruction.Count).Append(']'); 117 | } 118 | 119 | protected override void CursorHorizontalAbsolute(CursorHorizontalAbsolute instruction, StringBuilder state) 120 | { 121 | state.Append("[CHA").Append(instruction.Column).Append(']'); 122 | } 123 | 124 | protected override void CursorNextLine(CursorNextLine instruction, StringBuilder state) 125 | { 126 | state.Append("[CNL").Append(instruction.Count).Append(']'); 127 | } 128 | 129 | protected override void CursorPosition(CursorPosition instruction, StringBuilder state) 130 | { 131 | state.Append("[CUP").Append(instruction.Row).Append(',').Append(instruction.Column).Append(']'); 132 | } 133 | 134 | protected override void CursorPreviousLine(CursorPreviousLine instruction, StringBuilder state) 135 | { 136 | state.Append("[CPL").Append(instruction.Count).Append(']'); 137 | } 138 | 139 | protected override void DisableAlternativeBuffer(DisableAlternativeBuffer instruction, StringBuilder state) 140 | { 141 | state.Append("[DisableAltBuffer]"); 142 | } 143 | 144 | protected override void EnableAlternativeBuffer(EnableAlternativeBuffer instruction, StringBuilder state) 145 | { 146 | state.Append("[EnableAltBuffer]"); 147 | } 148 | 149 | protected override void EraseInDisplay(EraseInDisplay instruction, StringBuilder state) 150 | { 151 | state.Append("[ED").Append(instruction.Mode).Append(']'); 152 | } 153 | 154 | protected override void EraseInLine(EraseInLine instruction, StringBuilder state) 155 | { 156 | state.Append("[EL").Append(instruction.Mode).Append(']'); 157 | } 158 | 159 | protected override void RestoreCursor(RestoreCursor instruction, StringBuilder state) 160 | { 161 | state.Append("[RestoreCursor]"); 162 | } 163 | 164 | protected override void StoreCursor(StoreCursor instruction, StringBuilder state) 165 | { 166 | state.Append("[SaveCursor]"); 167 | } 168 | 169 | protected override void SelectGraphicRendition(SelectGraphicRendition instruction, StringBuilder state) 170 | { 171 | foreach (var operation in instruction.Operations) 172 | { 173 | if (operation.Reset) 174 | { 175 | state.Append("[SGR-RESET]"); 176 | } 177 | else if (operation.Foreground != null) 178 | { 179 | state.Append($"[SGR-FG={operation.Foreground}]"); 180 | } 181 | else if (operation.Background != null) 182 | { 183 | state.Append($"[SGR-BG={operation.Background}]"); 184 | } 185 | } 186 | } 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/Spectre.Terminals.Tests/Properties/Usings.cs: -------------------------------------------------------------------------------- 1 | global using System.Text; 2 | global using Shouldly; 3 | global using Spectre.Terminals.Emulation; 4 | global using Xunit; -------------------------------------------------------------------------------- /src/Spectre.Terminals.Tests/Spectre.Terminals.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | all 19 | 20 | 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | all 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/Spectre.Terminals.Tests/Utilities/AnsiSequenceVisitor.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals.Tests; 2 | 3 | internal abstract class AnsiSequenceVisitor : IAnsiSequenceVisitor 4 | { 5 | void IAnsiSequenceVisitor.CursorBack(CursorBack instruction, TState state) => CursorBack(instruction, state); 6 | void IAnsiSequenceVisitor.CursorDown(CursorDown instruction, TState state) => CursorDown(instruction, state); 7 | void IAnsiSequenceVisitor.CursorForward(CursorForward instruction, TState state) => CursorForward(instruction, state); 8 | void IAnsiSequenceVisitor.CursorHorizontalAbsolute(CursorHorizontalAbsolute instruction, TState state) => CursorHorizontalAbsolute(instruction, state); 9 | void IAnsiSequenceVisitor.CursorNextLine(CursorNextLine instruction, TState state) => CursorNextLine(instruction, state); 10 | void IAnsiSequenceVisitor.CursorPosition(CursorPosition instruction, TState state) => CursorPosition(instruction, state); 11 | void IAnsiSequenceVisitor.CursorPreviousLine(CursorPreviousLine instruction, TState state) => CursorPreviousLine(instruction, state); 12 | void IAnsiSequenceVisitor.CursorUp(CursorUp instruction, TState state) => CursorUp(instruction, state); 13 | void IAnsiSequenceVisitor.EraseInDisplay(EraseInDisplay instruction, TState state) => EraseInDisplay(instruction, state); 14 | void IAnsiSequenceVisitor.EraseInLine(EraseInLine instruction, TState state) => EraseInLine(instruction, state); 15 | void IAnsiSequenceVisitor.PrintText(PrintText instruction, TState state) => PrintText(instruction, state); 16 | void IAnsiSequenceVisitor.RestoreCursor(RestoreCursor instruction, TState state) => RestoreCursor(instruction, state); 17 | void IAnsiSequenceVisitor.StoreCursor(StoreCursor instruction, TState state) => StoreCursor(instruction, state); 18 | void IAnsiSequenceVisitor.HideCursor(HideCursor instruction, TState state) => HideCursor(instruction, state); 19 | void IAnsiSequenceVisitor.ShowCursor(ShowCursor instruction, TState state) => ShowCursor(instruction, state); 20 | void IAnsiSequenceVisitor.EnableAlternativeBuffer(EnableAlternativeBuffer instruction, TState state) => EnableAlternativeBuffer(instruction, state); 21 | void IAnsiSequenceVisitor.DisableAlternativeBuffer(DisableAlternativeBuffer instruction, TState state) => DisableAlternativeBuffer(instruction, state); 22 | void IAnsiSequenceVisitor.SelectGraphicRendition(SelectGraphicRendition instruction, TState state) => SelectGraphicRendition(instruction, state); 23 | 24 | protected virtual void CursorBack(CursorBack instruction, TState state) 25 | { 26 | } 27 | 28 | protected virtual void CursorDown(CursorDown instruction, TState state) 29 | { 30 | } 31 | 32 | protected virtual void CursorForward(CursorForward instruction, TState state) 33 | { 34 | } 35 | 36 | protected virtual void CursorHorizontalAbsolute(CursorHorizontalAbsolute instruction, TState state) 37 | { 38 | } 39 | 40 | protected virtual void CursorNextLine(CursorNextLine instruction, TState state) 41 | { 42 | } 43 | 44 | protected virtual void CursorPosition(CursorPosition instruction, TState state) 45 | { 46 | } 47 | 48 | protected virtual void CursorPreviousLine(CursorPreviousLine instruction, TState state) 49 | { 50 | } 51 | 52 | protected virtual void CursorUp(CursorUp instruction, TState state) 53 | { 54 | } 55 | 56 | protected virtual void EraseInDisplay(EraseInDisplay instruction, TState state) 57 | { 58 | } 59 | 60 | protected virtual void EraseInLine(EraseInLine instruction, TState state) 61 | { 62 | } 63 | 64 | protected virtual void PrintText(PrintText instruction, TState state) 65 | { 66 | } 67 | 68 | protected virtual void RestoreCursor(RestoreCursor instruction, TState state) 69 | { 70 | } 71 | 72 | protected virtual void StoreCursor(StoreCursor instruction, TState state) 73 | { 74 | } 75 | 76 | protected virtual void HideCursor(HideCursor instruction, TState state) 77 | { 78 | } 79 | 80 | protected virtual void ShowCursor(ShowCursor instruction, TState state) 81 | { 82 | } 83 | 84 | protected virtual void EnableAlternativeBuffer(EnableAlternativeBuffer instruction, TState state) 85 | { 86 | } 87 | 88 | protected virtual void DisableAlternativeBuffer(DisableAlternativeBuffer instruction, TState state) 89 | { 90 | } 91 | 92 | protected virtual void SelectGraphicRendition(SelectGraphicRendition instruction, TState state) 93 | { 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Spectre.Terminals.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.6.30114.105 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spectre.Terminals", "Spectre.Terminals\Spectre.Terminals.csproj", "{11FF129D-7BA1-4016-A52B-6AAE6C3F7703}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spectre.Terminals.Tests", "Spectre.Terminals.Tests\Spectre.Terminals.Tests.csproj", "{B9AA6477-1C5F-44CE-87C1-219948DB8772}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{02C78B96-9010-48EC-ADB5-4F48884F8937}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ansi", "..\examples\Ansi\Ansi.csproj", "{198EB3A4-39C6-4E46-A2B7-3F7B52F7763E}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Info", "..\examples\Info\Info.csproj", "{DEE537E6-84FE-4314-81B3-FEBA5021A2A2}" 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Signals", "..\examples\Signals\Signals.csproj", "{731549D8-F3E9-4B1C-89B3-455875FDAB30}" 17 | EndProject 18 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Input", "..\examples\Input\Input.csproj", "{67A01D2A-8B52-4B3C-9B49-54625C6DE1AF}" 19 | EndProject 20 | Global 21 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 22 | Debug|Any CPU = Debug|Any CPU 23 | Debug|x64 = Debug|x64 24 | Debug|x86 = Debug|x86 25 | Release|Any CPU = Release|Any CPU 26 | Release|x64 = Release|x64 27 | Release|x86 = Release|x86 28 | EndGlobalSection 29 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 30 | {11FF129D-7BA1-4016-A52B-6AAE6C3F7703}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {11FF129D-7BA1-4016-A52B-6AAE6C3F7703}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {11FF129D-7BA1-4016-A52B-6AAE6C3F7703}.Debug|x64.ActiveCfg = Debug|Any CPU 33 | {11FF129D-7BA1-4016-A52B-6AAE6C3F7703}.Debug|x64.Build.0 = Debug|Any CPU 34 | {11FF129D-7BA1-4016-A52B-6AAE6C3F7703}.Debug|x86.ActiveCfg = Debug|Any CPU 35 | {11FF129D-7BA1-4016-A52B-6AAE6C3F7703}.Debug|x86.Build.0 = Debug|Any CPU 36 | {11FF129D-7BA1-4016-A52B-6AAE6C3F7703}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {11FF129D-7BA1-4016-A52B-6AAE6C3F7703}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {11FF129D-7BA1-4016-A52B-6AAE6C3F7703}.Release|x64.ActiveCfg = Release|Any CPU 39 | {11FF129D-7BA1-4016-A52B-6AAE6C3F7703}.Release|x64.Build.0 = Release|Any CPU 40 | {11FF129D-7BA1-4016-A52B-6AAE6C3F7703}.Release|x86.ActiveCfg = Release|Any CPU 41 | {11FF129D-7BA1-4016-A52B-6AAE6C3F7703}.Release|x86.Build.0 = Release|Any CPU 42 | {B9AA6477-1C5F-44CE-87C1-219948DB8772}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {B9AA6477-1C5F-44CE-87C1-219948DB8772}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {B9AA6477-1C5F-44CE-87C1-219948DB8772}.Debug|x64.ActiveCfg = Debug|Any CPU 45 | {B9AA6477-1C5F-44CE-87C1-219948DB8772}.Debug|x64.Build.0 = Debug|Any CPU 46 | {B9AA6477-1C5F-44CE-87C1-219948DB8772}.Debug|x86.ActiveCfg = Debug|Any CPU 47 | {B9AA6477-1C5F-44CE-87C1-219948DB8772}.Debug|x86.Build.0 = Debug|Any CPU 48 | {B9AA6477-1C5F-44CE-87C1-219948DB8772}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {B9AA6477-1C5F-44CE-87C1-219948DB8772}.Release|Any CPU.Build.0 = Release|Any CPU 50 | {B9AA6477-1C5F-44CE-87C1-219948DB8772}.Release|x64.ActiveCfg = Release|Any CPU 51 | {B9AA6477-1C5F-44CE-87C1-219948DB8772}.Release|x64.Build.0 = Release|Any CPU 52 | {B9AA6477-1C5F-44CE-87C1-219948DB8772}.Release|x86.ActiveCfg = Release|Any CPU 53 | {B9AA6477-1C5F-44CE-87C1-219948DB8772}.Release|x86.Build.0 = Release|Any CPU 54 | {198EB3A4-39C6-4E46-A2B7-3F7B52F7763E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 55 | {198EB3A4-39C6-4E46-A2B7-3F7B52F7763E}.Debug|Any CPU.Build.0 = Debug|Any CPU 56 | {198EB3A4-39C6-4E46-A2B7-3F7B52F7763E}.Debug|x64.ActiveCfg = Debug|Any CPU 57 | {198EB3A4-39C6-4E46-A2B7-3F7B52F7763E}.Debug|x64.Build.0 = Debug|Any CPU 58 | {198EB3A4-39C6-4E46-A2B7-3F7B52F7763E}.Debug|x86.ActiveCfg = Debug|Any CPU 59 | {198EB3A4-39C6-4E46-A2B7-3F7B52F7763E}.Debug|x86.Build.0 = Debug|Any CPU 60 | {198EB3A4-39C6-4E46-A2B7-3F7B52F7763E}.Release|Any CPU.ActiveCfg = Release|Any CPU 61 | {198EB3A4-39C6-4E46-A2B7-3F7B52F7763E}.Release|Any CPU.Build.0 = Release|Any CPU 62 | {198EB3A4-39C6-4E46-A2B7-3F7B52F7763E}.Release|x64.ActiveCfg = Release|Any CPU 63 | {198EB3A4-39C6-4E46-A2B7-3F7B52F7763E}.Release|x64.Build.0 = Release|Any CPU 64 | {198EB3A4-39C6-4E46-A2B7-3F7B52F7763E}.Release|x86.ActiveCfg = Release|Any CPU 65 | {198EB3A4-39C6-4E46-A2B7-3F7B52F7763E}.Release|x86.Build.0 = Release|Any CPU 66 | {DEE537E6-84FE-4314-81B3-FEBA5021A2A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 67 | {DEE537E6-84FE-4314-81B3-FEBA5021A2A2}.Debug|Any CPU.Build.0 = Debug|Any CPU 68 | {DEE537E6-84FE-4314-81B3-FEBA5021A2A2}.Debug|x64.ActiveCfg = Debug|Any CPU 69 | {DEE537E6-84FE-4314-81B3-FEBA5021A2A2}.Debug|x64.Build.0 = Debug|Any CPU 70 | {DEE537E6-84FE-4314-81B3-FEBA5021A2A2}.Debug|x86.ActiveCfg = Debug|Any CPU 71 | {DEE537E6-84FE-4314-81B3-FEBA5021A2A2}.Debug|x86.Build.0 = Debug|Any CPU 72 | {DEE537E6-84FE-4314-81B3-FEBA5021A2A2}.Release|Any CPU.ActiveCfg = Release|Any CPU 73 | {DEE537E6-84FE-4314-81B3-FEBA5021A2A2}.Release|Any CPU.Build.0 = Release|Any CPU 74 | {DEE537E6-84FE-4314-81B3-FEBA5021A2A2}.Release|x64.ActiveCfg = Release|Any CPU 75 | {DEE537E6-84FE-4314-81B3-FEBA5021A2A2}.Release|x64.Build.0 = Release|Any CPU 76 | {DEE537E6-84FE-4314-81B3-FEBA5021A2A2}.Release|x86.ActiveCfg = Release|Any CPU 77 | {DEE537E6-84FE-4314-81B3-FEBA5021A2A2}.Release|x86.Build.0 = Release|Any CPU 78 | {731549D8-F3E9-4B1C-89B3-455875FDAB30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 79 | {731549D8-F3E9-4B1C-89B3-455875FDAB30}.Debug|Any CPU.Build.0 = Debug|Any CPU 80 | {731549D8-F3E9-4B1C-89B3-455875FDAB30}.Debug|x64.ActiveCfg = Debug|Any CPU 81 | {731549D8-F3E9-4B1C-89B3-455875FDAB30}.Debug|x64.Build.0 = Debug|Any CPU 82 | {731549D8-F3E9-4B1C-89B3-455875FDAB30}.Debug|x86.ActiveCfg = Debug|Any CPU 83 | {731549D8-F3E9-4B1C-89B3-455875FDAB30}.Debug|x86.Build.0 = Debug|Any CPU 84 | {731549D8-F3E9-4B1C-89B3-455875FDAB30}.Release|Any CPU.ActiveCfg = Release|Any CPU 85 | {731549D8-F3E9-4B1C-89B3-455875FDAB30}.Release|Any CPU.Build.0 = Release|Any CPU 86 | {731549D8-F3E9-4B1C-89B3-455875FDAB30}.Release|x64.ActiveCfg = Release|Any CPU 87 | {731549D8-F3E9-4B1C-89B3-455875FDAB30}.Release|x64.Build.0 = Release|Any CPU 88 | {731549D8-F3E9-4B1C-89B3-455875FDAB30}.Release|x86.ActiveCfg = Release|Any CPU 89 | {731549D8-F3E9-4B1C-89B3-455875FDAB30}.Release|x86.Build.0 = Release|Any CPU 90 | {67A01D2A-8B52-4B3C-9B49-54625C6DE1AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 91 | {67A01D2A-8B52-4B3C-9B49-54625C6DE1AF}.Debug|Any CPU.Build.0 = Debug|Any CPU 92 | {67A01D2A-8B52-4B3C-9B49-54625C6DE1AF}.Debug|x64.ActiveCfg = Debug|Any CPU 93 | {67A01D2A-8B52-4B3C-9B49-54625C6DE1AF}.Debug|x64.Build.0 = Debug|Any CPU 94 | {67A01D2A-8B52-4B3C-9B49-54625C6DE1AF}.Debug|x86.ActiveCfg = Debug|Any CPU 95 | {67A01D2A-8B52-4B3C-9B49-54625C6DE1AF}.Debug|x86.Build.0 = Debug|Any CPU 96 | {67A01D2A-8B52-4B3C-9B49-54625C6DE1AF}.Release|Any CPU.ActiveCfg = Release|Any CPU 97 | {67A01D2A-8B52-4B3C-9B49-54625C6DE1AF}.Release|Any CPU.Build.0 = Release|Any CPU 98 | {67A01D2A-8B52-4B3C-9B49-54625C6DE1AF}.Release|x64.ActiveCfg = Release|Any CPU 99 | {67A01D2A-8B52-4B3C-9B49-54625C6DE1AF}.Release|x64.Build.0 = Release|Any CPU 100 | {67A01D2A-8B52-4B3C-9B49-54625C6DE1AF}.Release|x86.ActiveCfg = Release|Any CPU 101 | {67A01D2A-8B52-4B3C-9B49-54625C6DE1AF}.Release|x86.Build.0 = Release|Any CPU 102 | EndGlobalSection 103 | GlobalSection(SolutionProperties) = preSolution 104 | HideSolutionNode = FALSE 105 | EndGlobalSection 106 | GlobalSection(NestedProjects) = preSolution 107 | {198EB3A4-39C6-4E46-A2B7-3F7B52F7763E} = {02C78B96-9010-48EC-ADB5-4F48884F8937} 108 | {DEE537E6-84FE-4314-81B3-FEBA5021A2A2} = {02C78B96-9010-48EC-ADB5-4F48884F8937} 109 | {731549D8-F3E9-4B1C-89B3-455875FDAB30} = {02C78B96-9010-48EC-ADB5-4F48884F8937} 110 | {67A01D2A-8B52-4B3C-9B49-54625C6DE1AF} = {02C78B96-9010-48EC-ADB5-4F48884F8937} 111 | EndGlobalSection 112 | GlobalSection(ExtensibilityGlobals) = postSolution 113 | SolutionGuid = {C3CAB9C8-0317-476E-AEA8-66EF76BA8661} 114 | EndGlobalSection 115 | EndGlobal 116 | -------------------------------------------------------------------------------- /src/Spectre.Terminals.v3.ncrunchsolution: -------------------------------------------------------------------------------- 1 |  2 | 3 | True 4 | True 5 | 6 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/ClearDisplay.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals; 2 | 3 | /// 4 | /// Represents different ways of clearing a display. 5 | /// 6 | public enum ClearDisplay 7 | { 8 | /// 9 | /// Clears the whole display. 10 | /// 11 | Everything = 0, 12 | 13 | /// 14 | /// Clears the whole display, including the 15 | /// scrollback buffer. 16 | /// 17 | EverythingAndScrollbackBuffer = 1, 18 | 19 | /// 20 | /// Clears everything before the cursor. 21 | /// 22 | BeforeCursor = 2, 23 | 24 | /// 25 | /// Clears everything after the cursor. 26 | /// 27 | AfterCursor = 3, 28 | } 29 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/ClearLine.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals; 2 | 3 | /// 4 | /// Represents different ways of clearing a line. 5 | /// 6 | public enum ClearLine 7 | { 8 | /// 9 | /// Clears the whole line. 10 | /// 11 | WholeLine = 0, 12 | 13 | /// 14 | /// Clears everything before the cursor. 15 | /// 16 | BeforeCursor = 1, 17 | 18 | /// 19 | /// Clears everything after the cursor. 20 | /// 21 | AfterCursor = 2, 22 | } 23 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/CursorDirection.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals; 2 | 3 | /// 4 | /// Represents a cursor direction. 5 | /// 6 | public enum CursorDirection 7 | { 8 | /// 9 | /// Moves the cursor forward. 10 | /// 11 | Forward = 0, 12 | 13 | /// 14 | /// Moves the cursor backwards. 15 | /// 16 | Back = 1, 17 | 18 | /// 19 | /// Moves the cursor up. 20 | /// 21 | Up = 2, 22 | 23 | /// 24 | /// Moves the cursor down. 25 | /// 26 | Down = 3, 27 | } 28 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Drivers/Linux/LinuxDriver.cs: -------------------------------------------------------------------------------- 1 | using static Spectre.Terminals.Drivers.LinuxInterop; 2 | 3 | namespace Spectre.Terminals.Drivers; 4 | 5 | internal sealed class LinuxDriver : UnixDriver 6 | { 7 | private termios? _original; 8 | private termios? _current; 9 | 10 | public override string Name { get; } = "Linux"; 11 | 12 | public LinuxDriver() 13 | { 14 | if (tcgetattr(UnixConstants.STDIN, out var settings) == 0) 15 | { 16 | // These values are usually the default, but we set them just to be safe. 17 | settings.c_cc[VTIME] = 0; 18 | settings.c_cc[VMIN] = 1; 19 | 20 | _original = settings; 21 | 22 | // We might get really unlucky and fail to apply the settings right after the call 23 | // above. We should still assign _current so we can apply it later. 24 | if (!UpdateSettings(TCSANOW, settings)) 25 | { 26 | _current = settings; 27 | } 28 | } 29 | } 30 | 31 | public override bool EnableRawMode() 32 | { 33 | return SetRawMode(true); 34 | } 35 | 36 | public override bool DisableRawMode() 37 | { 38 | return SetRawMode(false); 39 | } 40 | 41 | public override void RefreshSettings() 42 | { 43 | if (_current is termios settings) 44 | { 45 | // This call can fail if the terminal is detached, but that is OK. 46 | UpdateSettings(TCSANOW, settings); 47 | } 48 | } 49 | 50 | public override TerminalSize? GetTerminalSize() 51 | { 52 | var result = ioctl(UnixConstants.STDOUT, (UIntPtr)TIOCGWINSZ, out var w); 53 | if (result == 0) 54 | { 55 | return new TerminalSize(w.ws_col, w.ws_row); 56 | } 57 | 58 | return null; 59 | } 60 | 61 | private bool SetRawMode(bool raw) 62 | { 63 | if (_original is not termios settings) 64 | { 65 | return false; 66 | } 67 | 68 | if (raw) 69 | { 70 | settings.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON); 71 | settings.c_oflag &= ~OPOST; 72 | settings.c_cflag &= ~(CSIZE | PARENB); 73 | settings.c_cflag |= CS8; 74 | settings.c_lflag &= ~(ISIG | ICANON | ECHO | ECHONL | IEXTEN); 75 | } 76 | 77 | return UpdateSettings(TCSAFLUSH, settings) ? true : 78 | throw new InvalidOperationException( 79 | $"Could not change raw mode setting: {Stdlib.strerror(Stdlib.GetLastError())}"); 80 | } 81 | 82 | private bool UpdateSettings(int mode, termios settings) 83 | { 84 | int result; 85 | while ((result = tcsetattr(UnixConstants.STDIN, mode, settings)) == -1 86 | && Stdlib.GetLastError() == Errno.EINTR) 87 | { 88 | // Retry in case we get interrupted by a signal. 89 | } 90 | 91 | if (result == 0) 92 | { 93 | _current = settings; 94 | return true; 95 | } 96 | 97 | return false; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Drivers/Linux/LinuxInterop.Constants.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals.Drivers; 2 | 3 | internal sealed partial class LinuxInterop 4 | { 5 | /// 6 | /// winsize: Fill in the winsize structure pointed to by the 7 | /// third argument with the screen width and height. 8 | /// 9 | public const uint TIOCGWINSZ = 0x5413; 10 | 11 | /// 12 | /// termio: Size of the control character array. 13 | /// 14 | public const int NCCS = 32; 15 | 16 | /// 17 | /// termio: Apply changes immediately. 18 | /// 19 | public const int TCSANOW = 0; 20 | 21 | /// 22 | /// termio: the change occurs after all output written to the object 23 | /// referred by fd has been transmitted, and all input that 24 | /// has been received but not read will be discarded before 25 | /// the change is made. 26 | /// 27 | public const int TCSAFLUSH = 2; 28 | 29 | /// 30 | /// c_iflag: Ignore BREAK condition on input. 31 | /// 32 | public const uint IGNBRK = 0x1; 33 | 34 | /// 35 | /// c_iflag: If IGNBRK is set, a BREAK is ignored. If it is not set 36 | /// but BRKINT is set, then a BREAK causes the input and 37 | /// output queues to be flushed, and if the terminal is the 38 | /// controlling terminal of a foreground process group, it 39 | /// will cause a SIGINT to be sent to this foreground process 40 | /// group.When neither IGNBRK nor BRKINT are set, a BREAK 41 | /// reads as a null byte ('\0'), except when PARMRK is set, in 42 | /// which case it reads as the sequence \377 \0 \0. 43 | /// 44 | public const uint BRKINT = 0x2; 45 | 46 | /// 47 | /// c_iflag: If this bit is set, input bytes with parity or framing 48 | /// errors are marked when passed to the program. This bit is 49 | /// meaningful only when INPCK is set and IGNPAR is not set. 50 | /// The way erroneous bytes are marked is with two preceding 51 | /// bytes, \377 and \0. Thus, the program actually reads 52 | /// three bytes for one erroneous byte received from the 53 | /// terminal. If a valid byte has the value \377, and ISTRIP 54 | /// (see below) is not set, the program might confuse it with 55 | /// the prefix that marks a parity error. Therefore, a valid 56 | /// byte \377 is passed to the program as two bytes, \377 57 | /// \377, in this case. 58 | /// 59 | /// If neither IGNPAR nor PARMRK is set, read a character with 60 | /// a parity error or framing error as \0. 61 | /// 62 | public const uint PARMRK = 0x8; 63 | 64 | /// 65 | /// c_iflag: Strip off eighth bit. 66 | /// 67 | public const uint ISTRIP = 0x20; 68 | 69 | /// 70 | /// c_iflag: Translate NL to CR on input. 71 | /// 72 | public const uint INLCR = 0x40; 73 | 74 | /// 75 | /// c_iflag: Ignore carriage return on input. 76 | /// 77 | public const uint IGNCR = 0x80; 78 | 79 | /// 80 | /// c_iflag: Translate carriage return to newline on input (unless IGNCR is set). 81 | /// 82 | public const uint ICRNL = 0x100; 83 | 84 | /// 85 | /// c_iflag: (not in POSIX) Map uppercase characters to lowercase on input. 86 | /// 87 | public const uint IXON = 0x400; 88 | 89 | /// 90 | /// c_oflag: Enable implementation-defined output processing. 91 | /// 92 | public const uint OPOST = 0x1; 93 | 94 | /// 95 | /// c_cflag: Character size mask. Values are CS5, CS6, CS7, or CS8. 96 | /// 97 | public const uint CSIZE = 0x30; 98 | 99 | /// 100 | /// c_cflag: Character size mask: 8 bit. 101 | /// 102 | public const uint CS8 = 0x30; 103 | 104 | /// 105 | /// c_cflag: Enable parity generation on output and parity checking for input. 106 | /// 107 | public const uint PARENB = 0x100; 108 | 109 | /// 110 | /// c_lflag: When any of the characters INTR, QUIT, SUSP, or DSUSP are 111 | /// received, generate the corresponding signal. 112 | /// 113 | public const uint ISIG = 0x1; 114 | 115 | /// 116 | /// c_lflag: Enable canonical mode. 117 | /// Input is made available line by line. 118 | /// An input line is available when one of the line delimiters is typed 119 | /// (NL, EOL, EOL2; or EOF at the start of line). 120 | /// Line editing is enabled. 121 | /// The maximum line length is 4096 chars (including the terminating newline character). 122 | /// 123 | public const uint ICANON = 0x2; 124 | 125 | /// 126 | /// c_lflag: Echo input characters. 127 | /// 128 | public const uint ECHO = 0x8; 129 | 130 | /// 131 | /// c_lflag: If ICANON is also set, echo the NL character even if ECHO is not set. 132 | /// 133 | public const uint ECHONL = 0x40; 134 | 135 | /// 136 | /// c_lflag: Enable implementation-defined input processing. 137 | /// This flag, as well as ICANON must be enabled for the special 138 | /// characters EOL2, LNEXT, REPRINT, WERASE to be interpreted, 139 | /// and for the IUCLC flag to be effective. 140 | /// 141 | public const uint IEXTEN = 0x8000; 142 | 143 | /// 144 | /// c_cc: Timeout in deciseconds for noncanonical read (TIME). 145 | /// 146 | public const int VTIME = 5; 147 | 148 | /// 149 | /// c_cc: Minimum number of characters for noncanonical read (MIN). 150 | /// 151 | public const int VMIN = 6; 152 | } 153 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Drivers/Linux/LinuxInterop.cs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // 3 | // Not really auto-generated, but easier than supressing rules manually... 4 | // 5 | // ------------------------------------------------------------------------------ 6 | 7 | namespace Spectre.Terminals.Drivers 8 | { 9 | internal sealed partial class LinuxInterop 10 | { 11 | public struct termios 12 | { 13 | public uint c_iflag; 14 | public uint c_oflag; 15 | public uint c_cflag; 16 | public uint c_lflag; 17 | public byte c_line; 18 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = NCCS)] 19 | public byte[] c_cc; 20 | public uint c_ispeed; 21 | public uint c_ospeed; 22 | } 23 | 24 | public struct winsize 25 | { 26 | public ushort ws_row; 27 | public ushort ws_col; 28 | public ushort ws_xpixel; 29 | public ushort ws_ypixel; 30 | } 31 | 32 | [DllImport("libc")] 33 | public static extern int tcgetattr(int fd, out termios termios_p); 34 | 35 | [DllImport("libc")] 36 | public static extern int tcsetattr(int fd, int optional_actions, in termios termios_p); 37 | 38 | [DllImport("libc")] 39 | public static extern int ioctl(int fd, UIntPtr request, out winsize argp); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Drivers/MacOS/MacOSDriver.cs: -------------------------------------------------------------------------------- 1 | using static Spectre.Terminals.Drivers.MacOSInterop; 2 | 3 | namespace Spectre.Terminals.Drivers; 4 | 5 | internal sealed class MacOSDriver : UnixDriver 6 | { 7 | private termios? _original; 8 | private termios? _current; 9 | 10 | public override string Name => "macOS"; 11 | 12 | public MacOSDriver() 13 | { 14 | if (tcgetattr(UnixConstants.STDIN, out var settings) == 0) 15 | { 16 | // These values are usually the default, but we set them just to be safe. 17 | settings.c_cc[VTIME] = 0; 18 | settings.c_cc[VMIN] = 1; 19 | 20 | _original = settings; 21 | 22 | // We might get really unlucky and fail to apply the settings right after the call 23 | // above. We should still assign _current so we can apply it later. 24 | if (!UpdateSettings(TCSANOW, settings)) 25 | { 26 | _current = settings; 27 | } 28 | } 29 | } 30 | 31 | public override bool EnableRawMode() 32 | { 33 | return SetRawMode(true); 34 | } 35 | 36 | public override bool DisableRawMode() 37 | { 38 | return SetRawMode(false); 39 | } 40 | 41 | private bool SetRawMode(bool raw) 42 | { 43 | if (_original is not termios settings) 44 | { 45 | return false; 46 | } 47 | 48 | if (raw) 49 | { 50 | var iflag = (uint)settings.c_iflag; 51 | var oflag = (uint)settings.c_oflag; 52 | var cflag = (uint)settings.c_cflag; 53 | var lflag = (uint)settings.c_lflag; 54 | 55 | iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON); 56 | oflag &= ~OPOST; 57 | cflag &= ~(CSIZE | PARENB); 58 | cflag |= CS8; 59 | lflag &= ~(ISIG | ICANON | ECHO | ECHONL | IEXTEN); 60 | 61 | settings.c_iflag = (UIntPtr)iflag; 62 | settings.c_oflag = (UIntPtr)oflag; 63 | settings.c_cflag = (UIntPtr)cflag; 64 | settings.c_lflag = (UIntPtr)lflag; 65 | } 66 | 67 | return UpdateSettings(TCSAFLUSH, settings) ? true : 68 | throw new InvalidOperationException( 69 | $"Could not change raw mode setting: {Stdlib.strerror(Stdlib.GetLastError())}"); 70 | } 71 | 72 | public override TerminalSize? GetTerminalSize() 73 | { 74 | var result = ioctl(UnixConstants.STDOUT, (UIntPtr)TIOCGWINSZ, out var w); 75 | if (result == 0) 76 | { 77 | return new TerminalSize(w.ws_col, w.ws_row); 78 | } 79 | 80 | return null; 81 | } 82 | 83 | public override void RefreshSettings() 84 | { 85 | if (_current is termios settings) 86 | { 87 | // This call can fail if the terminal is detached, but that is OK. 88 | UpdateSettings(TCSANOW, settings); 89 | } 90 | } 91 | 92 | private bool UpdateSettings(int mode, termios settings) 93 | { 94 | int result; 95 | while ((result = tcsetattr(UnixConstants.STDIN, mode, settings)) == -1 96 | && Stdlib.GetLastError() == Errno.EINTR) 97 | { 98 | // Retry in case we get interrupted by a signal. 99 | } 100 | 101 | if (result == 0) 102 | { 103 | _current = settings; 104 | return true; 105 | } 106 | 107 | return false; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Drivers/MacOS/MacOSInterop.Constants.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals.Drivers; 2 | 3 | internal sealed partial class MacOSInterop 4 | { 5 | /// 6 | /// winsize: Fill in the winsize structure pointed to by the 7 | /// third argument with the screen width and height. 8 | /// 9 | public const uint TIOCGWINSZ = 0x40087468; 10 | 11 | /// 12 | /// termio: Size of the control character array. 13 | /// 14 | public const int NCCS = 20; 15 | 16 | /// 17 | /// termio: Apply changes immediately. 18 | /// 19 | public const int TCSANOW = 0; 20 | 21 | /// 22 | /// termio: the change occurs after all output written to the object 23 | /// referred by fd has been transmitted, and all input that 24 | /// has been received but not read will be discarded before 25 | /// the change is made. 26 | /// 27 | public const int TCSAFLUSH = 2; 28 | 29 | /// 30 | /// c_iflag: Ignore BREAK condition on input. 31 | /// 32 | public const uint IGNBRK = 0x1; 33 | 34 | /// 35 | /// c_iflag: If IGNBRK is set, a BREAK is ignored. If it is not set 36 | /// but BRKINT is set, then a BREAK causes the input and 37 | /// output queues to be flushed, and if the terminal is the 38 | /// controlling terminal of a foreground process group, it 39 | /// will cause a SIGINT to be sent to this foreground process 40 | /// group.When neither IGNBRK nor BRKINT are set, a BREAK 41 | /// reads as a null byte ('\0'), except when PARMRK is set, in 42 | /// which case it reads as the sequence \377 \0 \0. 43 | /// 44 | public const uint BRKINT = 0x2; 45 | 46 | /// 47 | /// c_iflag: If this bit is set, input bytes with parity or framing 48 | /// errors are marked when passed to the program. This bit is 49 | /// meaningful only when INPCK is set and IGNPAR is not set. 50 | /// The way erroneous bytes are marked is with two preceding 51 | /// bytes, \377 and \0. Thus, the program actually reads 52 | /// three bytes for one erroneous byte received from the 53 | /// terminal. If a valid byte has the value \377, and ISTRIP 54 | /// (see below) is not set, the program might confuse it with 55 | /// the prefix that marks a parity error. Therefore, a valid 56 | /// byte \377 is passed to the program as two bytes, \377 57 | /// \377, in this case. 58 | /// 59 | /// If neither IGNPAR nor PARMRK is set, read a character with 60 | /// a parity error or framing error as \0. 61 | /// 62 | public const uint PARMRK = 0x8; 63 | 64 | /// 65 | /// c_iflag: Strip off eighth bit. 66 | /// 67 | public const uint ISTRIP = 0x20; 68 | 69 | /// 70 | /// c_iflag: Translate NL to CR on input. 71 | /// 72 | public const uint INLCR = 0x40; 73 | 74 | /// 75 | /// c_iflag: Ignore carriage return on input. 76 | /// 77 | public const uint IGNCR = 0x80; 78 | 79 | /// 80 | /// c_iflag: Translate carriage return to newline on input (unless IGNCR is set). 81 | /// 82 | public const uint ICRNL = 0x100; 83 | 84 | /// 85 | /// c_iflag: (not in POSIX) Map uppercase characters to lowercase on input. 86 | /// 87 | public const uint IXON = 0x200; 88 | 89 | /// 90 | /// c_oflag: Enable implementation-defined output processing. 91 | /// 92 | public const uint OPOST = 0x1; 93 | 94 | /// 95 | /// c_cflag: Character size mask. Values are CS5, CS6, CS7, or CS8. 96 | /// 97 | public const uint CSIZE = 0x300; 98 | 99 | /// 100 | /// c_cflag: Character size mask: 8 bit. 101 | /// 102 | public const uint CS8 = 0x300; 103 | 104 | /// 105 | /// c_cflag: Enable parity generation on output and parity checking for input. 106 | /// 107 | public const uint PARENB = 0x1000; 108 | 109 | /// 110 | /// c_lflag: When any of the characters INTR, QUIT, SUSP, or DSUSP are 111 | /// received, generate the corresponding signal. 112 | /// 113 | public const uint ISIG = 0x80; 114 | 115 | /// 116 | /// c_lflag: Enable canonical mode. 117 | /// Input is made available line by line. 118 | /// An input line is available when one of the line delimiters is typed 119 | /// (NL, EOL, EOL2; or EOF at the start of line). 120 | /// Line editing is enabled. 121 | /// The maximum line length is 4096 chars (including the terminating newline character). 122 | /// 123 | public const uint ICANON = 0x100; 124 | 125 | /// 126 | /// c_lflag: Echo input characters. 127 | /// 128 | public const uint ECHO = 0x8; 129 | 130 | /// 131 | /// c_lflag: If ICANON is also set, echo the NL character even if ECHO is not set. 132 | /// 133 | public const uint ECHONL = 0x10; 134 | 135 | /// 136 | /// c_lflag: Enable implementation-defined input processing. 137 | /// This flag, as well as ICANON must be enabled for the special 138 | /// characters EOL2, LNEXT, REPRINT, WERASE to be interpreted, 139 | /// and for the IUCLC flag to be effective. 140 | /// 141 | public const uint IEXTEN = 0x400; 142 | 143 | /// 144 | /// c_cc: Timeout in deciseconds for noncanonical read (TIME). 145 | /// 146 | public const int VTIME = 17; 147 | 148 | /// 149 | /// c_cc: Minimum number of characters for noncanonical read (MIN). 150 | /// 151 | public const int VMIN = 16; 152 | } 153 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Drivers/MacOS/MacOSInterop.cs: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------ 2 | // 3 | // Not really auto-generated, but easier than supressing rules manually... 4 | // 5 | // ------------------------------------------------------------------------------ 6 | 7 | namespace Spectre.Terminals.Drivers 8 | { 9 | internal sealed partial class MacOSInterop 10 | { 11 | public struct termios 12 | { 13 | public UIntPtr c_iflag; 14 | public UIntPtr c_oflag; 15 | public UIntPtr c_cflag; 16 | public UIntPtr c_lflag; 17 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = NCCS)] 18 | public byte[] c_cc; 19 | public UIntPtr c_ispeed; 20 | public UIntPtr c_ospeed; 21 | } 22 | 23 | public struct winsize 24 | { 25 | public ushort ws_row; 26 | public ushort ws_col; 27 | public ushort ws_xpixel; 28 | public ushort ws_ypixel; 29 | } 30 | 31 | [DllImport("libc")] 32 | public static extern int tcgetattr(int fd, out termios termios_p); 33 | 34 | [DllImport("libc")] 35 | public static extern int tcsetattr(int fd, int optional_actions, in termios termios_p); 36 | 37 | [DllImport("libc")] 38 | public static extern int ioctl(int fildes, UIntPtr request, out winsize argp); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Drivers/Unix/UnixConstants.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals.Drivers; 2 | 3 | internal sealed class UnixConstants 4 | { 5 | public const int STDIN = 0; 6 | public const int STDOUT = 1; 7 | public const int STDERR = 2; 8 | } 9 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Drivers/Unix/UnixDriver.cs: -------------------------------------------------------------------------------- 1 | using Syscall = Mono.Unix.Native.Syscall; 2 | 3 | namespace Spectre.Terminals.Drivers; 4 | 5 | internal abstract class UnixDriver : ITerminalDriver 6 | { 7 | private readonly Thread _signalListenerThread; 8 | private readonly ManualResetEvent _stopEvent; 9 | private readonly ManualResetEvent _stoppedEvent; 10 | 11 | public abstract string Name { get; } 12 | public bool IsRawMode { get; } 13 | public TerminalSize? Size { get; private set; } 14 | 15 | public event EventHandler? Signalled; 16 | 17 | public ITerminalReader Input { get; } 18 | public ITerminalWriter Output { get; } 19 | public ITerminalWriter Error { get; } 20 | 21 | protected UnixDriver() 22 | { 23 | Input = new UnixTerminalReader(); 24 | Output = new UnixTerminalWriter(UnixConstants.STDIN); 25 | Error = new UnixTerminalWriter(UnixConstants.STDERR); 26 | 27 | Size = GetTerminalSize(); 28 | 29 | _stopEvent = new ManualResetEvent(false); 30 | _stoppedEvent = new ManualResetEvent(false); 31 | _signalListenerThread = CreateSignalListener(_stopEvent); 32 | _signalListenerThread.Start(); 33 | } 34 | 35 | public void Dispose() 36 | { 37 | if (!_stoppedEvent.WaitOne(0)) 38 | { 39 | _stopEvent.Set(); 40 | _signalListenerThread.Join(); 41 | _stoppedEvent.WaitOne(); 42 | } 43 | } 44 | 45 | public abstract bool EnableRawMode(); 46 | public abstract bool DisableRawMode(); 47 | public abstract void RefreshSettings(); 48 | public abstract TerminalSize? GetTerminalSize(); 49 | 50 | public bool EmitSignal(TerminalSignal signal) 51 | { 52 | switch (signal) 53 | { 54 | case TerminalSignal.SIGINT: 55 | _ = Syscall.kill(0, Signum.SIGINT); 56 | return true; 57 | case TerminalSignal.SIGQUIT: 58 | _ = Syscall.kill(0, Signum.SIGQUIT); 59 | return true; 60 | } 61 | 62 | return false; 63 | } 64 | 65 | private Thread CreateSignalListener(WaitHandle stop) 66 | { 67 | return new Thread(() => 68 | { 69 | using var sigWinch = new UnixSignal(Signum.SIGWINCH); 70 | using var sigCont = new UnixSignal(Signum.SIGCONT); 71 | using var sigInt = new UnixSignal(Signum.SIGINT); 72 | using var sigQuit = new UnixSignal(Signum.SIGQUIT); 73 | 74 | var signals = new[] { sigWinch, sigCont, sigInt, sigQuit }; 75 | while (!stop.WaitOne(0)) 76 | { 77 | var index = UnixSignal.WaitAny(signals, TimeSpan.FromMilliseconds(250)); 78 | if (index == 250) 79 | { 80 | continue; 81 | } 82 | 83 | if (index == -1) 84 | { 85 | break; 86 | } 87 | 88 | var signal = signals[index]; 89 | if (signal == stop) 90 | { 91 | break; 92 | } 93 | 94 | // If we are being restored from the background (SIGCONT), it is possible that 95 | // terminal settings have been mangled, so restore them. 96 | if (signal == sigCont) 97 | { 98 | RefreshSettings(); 99 | } 100 | 101 | // Terminal width/height might have changed for SIGCONT, and will definitely 102 | // have changed for SIGWINCH. 103 | if (signal == sigCont || signal == sigWinch) 104 | { 105 | Size = GetTerminalSize(); 106 | } 107 | 108 | if (signal == sigQuit || signal == sigInt) 109 | { 110 | if (Signalled != null) 111 | { 112 | // Propaate the signal 113 | var received = signal == sigQuit ? TerminalSignal.SIGQUIT : TerminalSignal.SIGINT; 114 | var signalEventArguments = new TerminalSignalEventArgs(received); 115 | Signalled(null, new TerminalSignalEventArgs(received)); 116 | 117 | // Not cancelled? 118 | if (!signalEventArguments.Cancel) 119 | { 120 | //// Get the value early to avoid ObjectDisposedException. 121 | var num = signal.Signum; 122 | 123 | //// Remove our signal handler and send the signal again. Since we 124 | //// have overwritten the signal handlers in CoreCLR and 125 | //// System.Native, this gives those handlers an opportunity to run. 126 | signal.Dispose(); 127 | Syscall.kill(Syscall.getpid(), num); 128 | } 129 | } 130 | } 131 | } 132 | 133 | _stoppedEvent.Set(); 134 | }) 135 | { 136 | IsBackground = true, 137 | Name = "Signal Listener", 138 | }; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Drivers/Unix/UnixTerminalReader.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals.Drivers; 2 | 3 | internal sealed class UnixTerminalReader : ITerminalReader 4 | { 5 | private readonly Encoding _encoding; 6 | 7 | public Encoding Encoding 8 | { 9 | get => _encoding; 10 | set { /* Do nothing for now */ } 11 | } 12 | 13 | public bool IsKeyAvailable => throw new NotSupportedException("Not yet supported"); 14 | 15 | public bool IsRedirected => !Syscall.isatty(UnixConstants.STDIN); 16 | 17 | public UnixTerminalReader() 18 | { 19 | _encoding = EncodingHelper.GetEncodingFromCharset() ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); 20 | } 21 | 22 | public int Read() 23 | { 24 | throw new NotSupportedException("Not yet supported"); 25 | } 26 | 27 | public string? ReadLine() 28 | { 29 | throw new NotSupportedException("Not yet supported"); 30 | } 31 | 32 | public ConsoleKeyInfo ReadKey() 33 | { 34 | throw new NotSupportedException("Not yet supported"); 35 | } 36 | 37 | private static unsafe int Read(Span buffer) 38 | { 39 | if (buffer.IsEmpty) 40 | { 41 | return 0; 42 | } 43 | 44 | long ret; 45 | 46 | while (true) 47 | { 48 | fixed (byte* p = buffer) 49 | { 50 | while ((ret = Syscall.read(UnixConstants.STDIN, p, (ulong)buffer.Length)) == -1 && 51 | Stdlib.GetLastError() == Errno.EINTR) 52 | { 53 | // Retry in case we get interrupted by a signal. 54 | } 55 | 56 | if (ret != -1) 57 | { 58 | break; 59 | } 60 | 61 | var err = Stdlib.GetLastError(); 62 | 63 | // The descriptor was probably redirected to a program that ended. Just 64 | // silently ignore this situation. 65 | // 66 | // The strange condition where errno is zero happens e.g. on Linux if 67 | // the process is killed while blocking in the read system call. 68 | if (err == 0 || err == Errno.EPIPE) 69 | { 70 | ret = 0; 71 | 72 | break; 73 | } 74 | 75 | // The file descriptor has been configured as non-blocking. Instead of 76 | // busily trying to read over and over, poll until we can write and then 77 | // try again. 78 | if (err == Errno.EAGAIN) 79 | { 80 | _ = Syscall.poll( 81 | new[] 82 | { 83 | new Pollfd 84 | { 85 | fd = UnixConstants.STDIN, 86 | events = PollEvents.POLLIN, 87 | }, 88 | }, 1, Timeout.Infinite); 89 | 90 | continue; 91 | } 92 | 93 | if (err == 0) 94 | { 95 | err = Errno.EBADF; 96 | } 97 | 98 | throw new InvalidOperationException( 99 | $"Could not read from STDIN: {Stdlib.strerror(err)}"); 100 | } 101 | } 102 | 103 | return (int)ret; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Drivers/Unix/UnixTerminalWriter.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals.Drivers; 2 | 3 | internal sealed class UnixTerminalWriter : ITerminalWriter 4 | { 5 | private readonly int _handle; 6 | private readonly string _name; 7 | private readonly Encoding _encoding; 8 | 9 | public Encoding Encoding 10 | { 11 | get => _encoding; 12 | set { /* Do nothing for now */ } 13 | } 14 | 15 | public bool IsRedirected => !Syscall.isatty(_handle); 16 | 17 | public UnixTerminalWriter(int handle) 18 | { 19 | _handle = handle; 20 | _name = handle == UnixConstants.STDIN ? "STDIN" : "STDERR"; 21 | _encoding = EncodingHelper.GetEncodingFromCharset() ?? new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); 22 | } 23 | 24 | // From https://github.com/alexrp/system-terminal/blob/819090b722e3198b6b932fdd67641371be99e844/src/core/Drivers/UnixTerminalDriver.cs#L111 25 | public unsafe void Write(ReadOnlySpan buffer) 26 | { 27 | if (buffer.IsEmpty) 28 | { 29 | return; 30 | } 31 | 32 | var progress = 0; 33 | 34 | fixed (byte* p = buffer) 35 | { 36 | var len = buffer.Length; 37 | 38 | while (progress < len) 39 | { 40 | long result; 41 | while ((result = Syscall.write(_handle, p + progress, (ulong)(len - progress))) == -1 && 42 | Stdlib.GetLastError() == Errno.EINTR) 43 | { 44 | } 45 | 46 | // The descriptor has been closed by someone else. Just silently ignore 47 | // this situation. 48 | if (result == 0) 49 | { 50 | break; 51 | } 52 | 53 | if (result != -1) 54 | { 55 | progress += (int)result; 56 | continue; 57 | } 58 | 59 | var err = Stdlib.GetLastError(); 60 | if (err == Errno.EPIPE) 61 | { 62 | // The descriptor was probably redirected to a program that ended. Just 63 | // silently ignore this situation. 64 | break; 65 | } 66 | else if (err == Errno.EAGAIN) 67 | { 68 | // The file descriptor has been configured as non-blocking. Instead of 69 | // busily trying to write over and over, poll until we can write and 70 | // then try again. 71 | Syscall.poll( 72 | new[] 73 | { 74 | new Pollfd 75 | { 76 | fd = _handle, 77 | events = PollEvents.POLLOUT, 78 | }, 79 | }, 1, Timeout.Infinite); 80 | 81 | continue; 82 | } 83 | 84 | throw new InvalidOperationException($"Could not write to standard {_name}: {Stdlib.strerror(err)}"); 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Drivers/Windows/Emulation/WindowsColors.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals.Drivers; 2 | 3 | internal sealed class WindowsColors 4 | { 5 | private readonly SafeHandle _stdout; 6 | private readonly SafeHandle _stderr; 7 | private readonly Color[]? _colorTable; 8 | private readonly short? _defaultColors; 9 | 10 | public WindowsColors(SafeHandle stdout, SafeHandle stderr) 11 | { 12 | _stdout = stdout ?? throw new ArgumentNullException(nameof(stdout)); 13 | _stderr = stderr ?? throw new ArgumentNullException(nameof(stderr)); 14 | 15 | if (TryGetConsoleBuffer(out var buffer)) 16 | { 17 | // Build the color table 18 | // We will use this to find the nearest color. 19 | _colorTable = new Color[16]; 20 | for (var index = 0; index < 16; index++) 21 | { 22 | _colorTable[index] = new Color( 23 | (int)((GetValue(ref buffer.ColorTable, index) >> 16) & 0xff), 24 | (int)((GetValue(ref buffer.ColorTable, index) >> 8) & 0xff), 25 | (int)(GetValue(ref buffer.ColorTable, index) & 0xff)); 26 | } 27 | 28 | // Get the default colors 29 | _defaultColors = (byte)(buffer.wAttributes & WindowsConstants.Colors.COLOR_MASK); 30 | } 31 | } 32 | 33 | public void SetForeground(Color color) 34 | { 35 | SetColor(color, foreground: true); 36 | } 37 | 38 | public void SetBackground(Color color) 39 | { 40 | SetColor(color, foreground: false); 41 | } 42 | 43 | public void Reset() 44 | { 45 | if (_defaultColors != null) 46 | { 47 | PInvoke.SetConsoleTextAttribute(_stdout, (ushort)_defaultColors.Value); 48 | } 49 | } 50 | 51 | private uint GetValue(ref CONSOLE_SCREEN_BUFFER_INFOEX.__uint_16 value, int index) 52 | { 53 | #if NET5_0_OR_GREATER 54 | return value[index]; 55 | #else 56 | return index switch 57 | { 58 | 0 => value._0, 59 | 1 => value._1, 60 | 2 => value._2, 61 | 3 => value._3, 62 | 4 => value._4, 63 | 5 => value._5, 64 | 6 => value._6, 65 | 7 => value._7, 66 | 8 => value._8, 67 | 9 => value._9, 68 | 10 => value._10, 69 | 11 => value._11, 70 | 12 => value._12, 71 | 13 => value._13, 72 | 14 => value._14, 73 | 15 => value._15, 74 | _ => throw new InvalidOperationException("Invalid __uint_16 index"), 75 | }; 76 | #endif 77 | } 78 | 79 | private unsafe bool TryGetConsoleBuffer(out CONSOLE_SCREEN_BUFFER_INFOEX info) 80 | { 81 | info = default(CONSOLE_SCREEN_BUFFER_INFOEX); 82 | info.cbSize = (uint)sizeof(CONSOLE_SCREEN_BUFFER_INFOEX); 83 | 84 | if (PInvoke.GetConsoleScreenBufferInfoEx(_stdout, ref info) || 85 | PInvoke.GetConsoleScreenBufferInfoEx(_stderr, ref info)) 86 | { 87 | return true; 88 | } 89 | 90 | return false; 91 | } 92 | 93 | private void SetColor(Color color, bool foreground) 94 | { 95 | var number = color.Number; 96 | 97 | if (color.IsRgb && number == null) 98 | { 99 | // TODO: Find the closest color 100 | return; 101 | } 102 | 103 | if (number != null) 104 | { 105 | var colorNumber = number.Value; 106 | if (colorNumber >= 0 && colorNumber < 16) 107 | { 108 | // Map the number to 4-bit color 109 | colorNumber = Map4BitColor(colorNumber); 110 | } 111 | else if (colorNumber < 256) 112 | { 113 | // TODO: Support 256-bit colors 114 | return; 115 | } 116 | 117 | var c = GetColorAttribute(colorNumber, !foreground); 118 | if (c == null) 119 | { 120 | return; 121 | } 122 | 123 | if (TryGetConsoleBuffer(out var buffer)) 124 | { 125 | var attrs = (short)buffer.wAttributes; 126 | if (foreground) 127 | { 128 | attrs &= ~WindowsConstants.Colors.FOREGROUND_MASK; 129 | } 130 | else 131 | { 132 | attrs &= ~WindowsConstants.Colors.BACKGROUND_MASK; 133 | } 134 | 135 | attrs = (short)(((uint)(ushort)attrs) | (ushort)c.Value); 136 | PInvoke.SetConsoleTextAttribute(_stdout, (ushort)attrs); 137 | } 138 | } 139 | } 140 | 141 | private static int? GetColorAttribute(int color, bool isBackground) 142 | { 143 | if ((color & ~0xf) != 0) 144 | { 145 | return null; 146 | } 147 | 148 | if (isBackground) 149 | { 150 | color <<= 4; 151 | } 152 | 153 | return color; 154 | } 155 | 156 | private static int Map4BitColor(int number) 157 | { 158 | return number switch 159 | { 160 | 0 => 0, 161 | 1 => 4, 162 | 2 => 2, 163 | 3 => 6, 164 | 4 => 1, 165 | 5 => 5, 166 | 6 => 3, 167 | 7 => 7, 168 | 8 => 8, 169 | 9 => 12, 170 | 10 => 10, 171 | 11 => 14, 172 | 12 => 9, 173 | 13 => 13, 174 | 14 => 11, 175 | 15 => 15, 176 | _ => number, 177 | }; 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Drivers/Windows/Emulation/WindowsTerminalEmulatorAdapter.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals.Drivers; 2 | 3 | internal sealed class WindowsTerminalEmulatorAdapter : IWindowsTerminalWriter 4 | { 5 | private readonly IWindowsTerminalWriter _writer; 6 | private readonly WindowsTerminalEmulator _emulator; 7 | private readonly WindowsTerminalState _state; 8 | 9 | public SafeHandle Handle => _writer.Handle; 10 | 11 | public Encoding Encoding 12 | { 13 | get => _writer.Encoding; 14 | set => _writer.Encoding = value; 15 | } 16 | 17 | public bool IsRedirected => _writer.IsRedirected; 18 | 19 | public WindowsTerminalEmulatorAdapter(IWindowsTerminalWriter writer, WindowsColors colors) 20 | { 21 | _writer = writer ?? throw new ArgumentNullException(nameof(writer)); 22 | _emulator = new WindowsTerminalEmulator(); 23 | _state = new WindowsTerminalState(writer, colors); 24 | } 25 | 26 | public void Dispose() 27 | { 28 | _writer.Dispose(); 29 | } 30 | 31 | public bool GetMode([NotNullWhen(true)] out CONSOLE_MODE? mode) 32 | { 33 | return _writer.GetMode(out mode); 34 | } 35 | 36 | public bool AddMode(CONSOLE_MODE mode) 37 | { 38 | return _writer.AddMode(mode); 39 | } 40 | 41 | public bool RemoveMode(CONSOLE_MODE mode) 42 | { 43 | return _writer.RemoveMode(mode); 44 | } 45 | 46 | public void Write(ReadOnlySpan buffer) 47 | { 48 | if (_writer.IsRedirected) 49 | { 50 | _writer.Write(Handle, buffer); 51 | } 52 | else 53 | { 54 | _emulator.Write(_state, buffer); 55 | } 56 | } 57 | 58 | public void Write(SafeHandle handle, ReadOnlySpan buffer) 59 | { 60 | throw new NotSupportedException(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Drivers/Windows/Emulation/WindowsTerminalState.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals.Drivers; 2 | 3 | internal class WindowsTerminalState 4 | { 5 | public IWindowsTerminalWriter Writer { get; } 6 | public SafeHandle Handle => AlternativeBuffer ?? MainBuffer; 7 | public Encoding Encoding => Writer.Encoding; 8 | 9 | public COORD? StoredCursorPosition { get; set; } 10 | 11 | public SafeHandle MainBuffer { get; set; } 12 | public SafeHandle? AlternativeBuffer { get; set; } 13 | 14 | public WindowsColors Colors { get; } 15 | 16 | public WindowsTerminalState(IWindowsTerminalWriter writer, WindowsColors colors) 17 | { 18 | MainBuffer = writer.Handle; 19 | Writer = writer ?? throw new ArgumentNullException(nameof(writer)); 20 | Colors = colors ?? throw new ArgumentNullException(nameof(colors)); 21 | } 22 | } -------------------------------------------------------------------------------- /src/Spectre.Terminals/Drivers/Windows/IWindowsTerminalWriter.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals.Drivers; 2 | 3 | /// 4 | /// Represents a Windows writer. 5 | /// 6 | internal interface IWindowsTerminalWriter : ITerminalWriter, IDisposable 7 | { 8 | /// 9 | /// Gets the handle. 10 | /// 11 | SafeHandle Handle { get; } 12 | 13 | /// 14 | /// Writes the specified data to the specified handle. 15 | /// 16 | /// The handle to write to. 17 | /// The buffer to write. 18 | void Write(SafeHandle handle, ReadOnlySpan buffer); 19 | 20 | /// 21 | /// Gets the for the writer. 22 | /// 23 | /// The resulting , or null if the operation failed. 24 | /// true if the operation succeeded, otherwise false. 25 | bool GetMode([NotNullWhen(true)] out CONSOLE_MODE? mode); 26 | 27 | /// 28 | /// Adds the specified . 29 | /// 30 | /// The to add. 31 | /// true if the operation succeeded, otherwise false. 32 | bool AddMode(CONSOLE_MODE mode); 33 | 34 | /// 35 | /// Removes the specified . 36 | /// 37 | /// The to remove. 38 | /// true if the operation succeeded, otherwise false. 39 | bool RemoveMode(CONSOLE_MODE mode); 40 | } 41 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Drivers/Windows/WindowsConsoleStream.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals.Drivers; 2 | 3 | internal sealed class WindowsConsoleStream : Stream 4 | { 5 | private const int BytesPerWChar = 2; 6 | 7 | private readonly SafeHandle _handle; 8 | private readonly bool _useFileAPIs; 9 | 10 | public override bool CanRead => true; 11 | public override bool CanSeek => false; 12 | public override bool CanWrite => false; 13 | public override long Length => throw new NotSupportedException(); 14 | public override long Position 15 | { 16 | get { throw new NotSupportedException(); } 17 | set { throw new NotSupportedException(); } 18 | } 19 | 20 | public WindowsConsoleStream(SafeHandle handle, bool useFileApis) 21 | { 22 | _handle = handle ?? throw new ArgumentNullException(nameof(handle)); 23 | _useFileAPIs = useFileApis; 24 | } 25 | 26 | public override void Flush() 27 | { 28 | } 29 | 30 | public override int Read(byte[] buffer, int offset, int count) 31 | { 32 | return ReadFromHandle(new Span(buffer, offset, count)); 33 | } 34 | 35 | public override long Seek(long offset, SeekOrigin origin) 36 | { 37 | throw new NotSupportedException(); 38 | } 39 | 40 | public override void SetLength(long value) 41 | { 42 | throw new NotSupportedException(); 43 | } 44 | 45 | public override void Write(byte[] buffer, int offset, int count) 46 | { 47 | throw new NotSupportedException(); 48 | } 49 | 50 | private unsafe int ReadFromHandle(Span buffer) 51 | { 52 | if (buffer.IsEmpty) 53 | { 54 | return 0; 55 | } 56 | 57 | bool readSuccess; 58 | var bytesRead = 0; 59 | 60 | fixed (byte* p = buffer) 61 | { 62 | if (_useFileAPIs) 63 | { 64 | uint result; 65 | var ptrResult = &result; 66 | readSuccess = PInvoke.ReadFile(_handle, p, (uint)buffer.Length, ptrResult, null); 67 | bytesRead = (int)result; 68 | } 69 | else 70 | { 71 | uint result; 72 | var ptrResult = &result; 73 | readSuccess = PInvoke.ReadConsole(_handle, p, (uint)buffer.Length, out var charsRead, null); 74 | bytesRead = (int)(charsRead * BytesPerWChar); 75 | } 76 | } 77 | 78 | if (readSuccess) 79 | { 80 | return bytesRead; 81 | } 82 | 83 | throw new InvalidOperationException("Could not read from console input"); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Drivers/Windows/WindowsConstants.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals.Drivers; 2 | 3 | [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore")] 4 | internal static class WindowsConstants 5 | { 6 | public const int ERROR_HANDLE_EOF = 38; 7 | public const int ERROR_BROKEN_PIPE = 109; 8 | public const int ERROR_NO_DATA = 232; 9 | 10 | public const int KEY_EVENT = 0x0001; 11 | 12 | public const uint GENERIC_READ = 0x80000000; 13 | public const uint GENERIC_WRITE = 0x40000000; 14 | 15 | public const int FILE_SHARE_READ = 1; 16 | public const int FILE_SHARE_WRITE = 2; 17 | 18 | public const uint CONSOLE_TEXTMODE_BUFFER = 1; 19 | 20 | public static class Signals 21 | { 22 | public const uint CTRL_C_EVENT = 0; 23 | public const uint CTRL_BREAK_EVENT = 1; 24 | } 25 | 26 | public static class Colors 27 | { 28 | public const short FOREGROUND_MASK = 0xf; 29 | public const short BACKGROUND_MASK = 0xf0; 30 | public const short COLOR_MASK = 0xff; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Drivers/Windows/WindowsDriver.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals.Drivers; 2 | 3 | internal sealed class WindowsDriver : ITerminalDriver 4 | { 5 | [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore")] 6 | private const CONSOLE_MODE IN_MODE = CONSOLE_MODE.ENABLE_PROCESSED_INPUT | CONSOLE_MODE.ENABLE_LINE_INPUT | CONSOLE_MODE.ENABLE_ECHO_INPUT; 7 | [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore")] 8 | private const CONSOLE_MODE OUT_MODE = CONSOLE_MODE.DISABLE_NEWLINE_AUTO_RETURN; 9 | 10 | private readonly WindowsTerminalReader _input; 11 | private readonly IWindowsTerminalWriter _output; 12 | private readonly IWindowsTerminalWriter _error; 13 | private readonly WindowsSignals _signals; 14 | 15 | public string Name { get; } = "Windows"; 16 | public bool IsRawMode { get; private set; } 17 | public TerminalSize? Size => GetTerminalSize(); 18 | 19 | public event EventHandler? Signalled 20 | { 21 | add => _signals.Signalled += value; 22 | remove => _signals.Signalled += value; 23 | } 24 | 25 | ITerminalReader ITerminalDriver.Input => _input; 26 | ITerminalWriter ITerminalDriver.Output => _output; 27 | ITerminalWriter ITerminalDriver.Error => _error; 28 | 29 | public WindowsDriver(bool emulate = false) 30 | { 31 | _signals = new WindowsSignals(); 32 | 33 | _input = new WindowsTerminalReader(this); 34 | _input.AddMode( 35 | CONSOLE_MODE.ENABLE_LINE_INPUT | 36 | CONSOLE_MODE.ENABLE_ECHO_INPUT | 37 | CONSOLE_MODE.ENABLE_PROCESSED_INPUT | 38 | CONSOLE_MODE.ENABLE_INSERT_MODE); 39 | 40 | _output = new WindowsTerminalWriter(STD_HANDLE_TYPE.STD_OUTPUT_HANDLE); 41 | _output.AddMode( 42 | CONSOLE_MODE.ENABLE_PROCESSED_OUTPUT | 43 | CONSOLE_MODE.ENABLE_WRAP_AT_EOL_OUTPUT | 44 | CONSOLE_MODE.DISABLE_NEWLINE_AUTO_RETURN | 45 | CONSOLE_MODE.ENABLE_VIRTUAL_TERMINAL_PROCESSING); 46 | 47 | _error = new WindowsTerminalWriter(STD_HANDLE_TYPE.STD_ERROR_HANDLE); 48 | _error.AddMode( 49 | CONSOLE_MODE.ENABLE_PROCESSED_OUTPUT | 50 | CONSOLE_MODE.ENABLE_WRAP_AT_EOL_OUTPUT | 51 | CONSOLE_MODE.DISABLE_NEWLINE_AUTO_RETURN | 52 | CONSOLE_MODE.ENABLE_VIRTUAL_TERMINAL_PROCESSING); 53 | 54 | // ENABLE_VIRTUAL_TERMINAL_PROCESSING not supported? 55 | if (emulate || (!(_output.GetMode(out var mode) && (mode & CONSOLE_MODE.ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0) 56 | && !(_error.GetMode(out mode) && (mode & CONSOLE_MODE.ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0))) 57 | { 58 | var colors = new WindowsColors(_output.Handle, _error.Handle); 59 | 60 | // Wrap STDOUT and STDERR in an emulator 61 | _output = new WindowsTerminalEmulatorAdapter(_output, colors); 62 | _error = new WindowsTerminalEmulatorAdapter(_error, colors); 63 | 64 | // Update the name to reflect the changes 65 | Name = "Windows (emulated)"; 66 | } 67 | } 68 | 69 | public void Dispose() 70 | { 71 | _input.Dispose(); 72 | _output.Dispose(); 73 | _error.Dispose(); 74 | _signals.Dispose(); 75 | } 76 | 77 | public bool EmitSignal(TerminalSignal signal) 78 | { 79 | return WindowsSignals.Emit(signal); 80 | } 81 | 82 | public bool EnableRawMode() 83 | { 84 | // TODO: Restoration of mode when failed 85 | if (!(_input.RemoveMode(IN_MODE) && (_output.RemoveMode(OUT_MODE) || _error.RemoveMode(OUT_MODE)))) 86 | { 87 | return false; 88 | } 89 | 90 | if (!PInvoke.FlushConsoleInputBuffer(_input.Handle)) 91 | { 92 | throw new InvalidOperationException("Could not flush input buffer"); 93 | } 94 | 95 | IsRawMode = true; 96 | return true; 97 | } 98 | 99 | public bool DisableRawMode() 100 | { 101 | // TODO: Restoration of mode when failed 102 | if (!(_input.AddMode(IN_MODE) && (_output.AddMode(OUT_MODE) || _error.AddMode(OUT_MODE)))) 103 | { 104 | return false; 105 | } 106 | 107 | if (!PInvoke.FlushConsoleInputBuffer(_input.Handle)) 108 | { 109 | throw new InvalidOperationException("Could not flush input buffer"); 110 | } 111 | 112 | IsRawMode = false; 113 | return true; 114 | } 115 | 116 | private TerminalSize? GetTerminalSize() 117 | { 118 | if (!PInvoke.GetConsoleScreenBufferInfo(_output.Handle, out var info) && 119 | !PInvoke.GetConsoleScreenBufferInfo(_error.Handle, out info) && 120 | !PInvoke.GetConsoleScreenBufferInfo(_input.Handle, out info)) 121 | { 122 | return null; 123 | } 124 | 125 | return new TerminalSize(info.srWindow.Right, info.srWindow.Bottom); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Drivers/Windows/WindowsKeyReader.cs: -------------------------------------------------------------------------------- 1 | // Parts of this code used from: https://github.com/dotnet/runtime 2 | // Licensed to the .NET Foundation under one or more agreements. 3 | 4 | using static Spectre.Terminals.Drivers.WindowsConstants; 5 | 6 | namespace Spectre.Terminals.Drivers; 7 | 8 | internal sealed class WindowsKeyReader 9 | { 10 | private const short AltVKCode = 0x12; 11 | 12 | private readonly object _lock; 13 | private readonly SafeHandle _handle; 14 | private INPUT_RECORD _cachedInputRecord; 15 | 16 | [Flags] 17 | private enum ControlKeyState 18 | { 19 | Unknown = 0, 20 | RightAltPressed = 0x0001, 21 | LeftAltPressed = 0x0002, 22 | RightCtrlPressed = 0x0004, 23 | LeftCtrlPressed = 0x0008, 24 | ShiftPressed = 0x0010, 25 | NumLockOn = 0x0020, 26 | ScrollLockOn = 0x0040, 27 | CapsLockOn = 0x0080, 28 | EnhancedKey = 0x0100, 29 | } 30 | 31 | public WindowsKeyReader(SafeHandle handle) 32 | { 33 | _handle = handle ?? throw new ArgumentNullException(nameof(handle)); 34 | _lock = new object(); 35 | } 36 | 37 | public unsafe bool IsKeyAvailable() 38 | { 39 | if (_cachedInputRecord.EventType == KEY_EVENT) 40 | { 41 | return true; 42 | } 43 | 44 | INPUT_RECORD ir; 45 | var buffer = new Span(new INPUT_RECORD[1]); 46 | 47 | while (true) 48 | { 49 | var r = PInvoke.PeekConsoleInput(_handle, buffer, out var numEventsRead); 50 | if (!r) 51 | { 52 | throw new InvalidOperationException(); 53 | } 54 | 55 | if (numEventsRead == 0) 56 | { 57 | return false; 58 | } 59 | 60 | ir = buffer[0]; 61 | 62 | // Skip non key-down && mod key events. 63 | if (!IsKeyDownEvent(ir) || IsModKey(ir)) 64 | { 65 | r = PInvoke.ReadConsoleInput(_handle, buffer, out _); 66 | if (!r) 67 | { 68 | throw new InvalidOperationException(); 69 | } 70 | } 71 | else 72 | { 73 | return true; 74 | } 75 | } 76 | } 77 | 78 | public ConsoleKeyInfo ReadKey() 79 | { 80 | INPUT_RECORD ir; 81 | var buffer = new Span(new INPUT_RECORD[1]); 82 | 83 | lock (_lock) 84 | { 85 | if (_cachedInputRecord.EventType == KEY_EVENT) 86 | { 87 | // We had a previous keystroke with repeated characters. 88 | ir = _cachedInputRecord; 89 | if (_cachedInputRecord.Event.KeyEvent.wRepeatCount == 0) 90 | { 91 | _cachedInputRecord.EventType = ushort.MaxValue; 92 | } 93 | else 94 | { 95 | _cachedInputRecord.Event.KeyEvent.wRepeatCount--; 96 | } 97 | 98 | // We will return one key from this method, so we decrement the 99 | // repeatCount here, leaving the cachedInputRecord in the "queue". 100 | } 101 | else 102 | { 103 | // We did NOT have a previous keystroke with repeated characters: 104 | while (true) 105 | { 106 | var r = PInvoke.ReadConsoleInput(_handle, buffer, out var numEventsRead); 107 | if (!r || numEventsRead != 1) 108 | { 109 | // This will fail when stdin is redirected from a file or pipe. 110 | // We could theoretically call Console.Read here, but I 111 | // think we might do some things incorrectly then. 112 | throw new InvalidOperationException("Could not read from STDIN. Has it been redirected?"); 113 | } 114 | 115 | ir = buffer[0]; 116 | var keyCode = ir.Event.KeyEvent.wVirtualKeyCode; 117 | 118 | // First check for non-keyboard events & discard them. Generally we tap into only KeyDown events and ignore the KeyUp events 119 | // but it is possible that we are dealing with a Alt+NumPad unicode key sequence, the final unicode char is revealed only when 120 | // the Alt key is released (i.e when the sequence is complete). To avoid noise, when the Alt key is down, we should eat up 121 | // any intermediate key strokes (from NumPad) that collectively forms the Unicode character. 122 | if (!IsKeyDownEvent(ir)) 123 | { 124 | // REVIEW: Unicode IME input comes through as KeyUp event with no accompanying KeyDown. 125 | if (keyCode != AltVKCode) 126 | { 127 | continue; 128 | } 129 | } 130 | 131 | var ch = (char)ir.Event.KeyEvent.uChar.UnicodeChar; 132 | 133 | // In a Alt+NumPad unicode sequence, when the alt key is released uChar will represent the final unicode character, we need to 134 | // surface this. VirtualKeyCode for this event will be Alt from the Alt-Up key event. This is probably not the right code, 135 | // especially when we don't expose ConsoleKey.Alt, so this will end up being the hex value (0x12). VK_PACKET comes very 136 | // close to being useful and something that we could look into using for this purpose... 137 | if (ch == 0) 138 | { 139 | // Skip mod keys. 140 | if (IsModKey(ir)) 141 | { 142 | continue; 143 | } 144 | } 145 | 146 | // When Alt is down, it is possible that we are in the middle of a Alt+NumPad unicode sequence. 147 | // Escape any intermediate NumPad keys whether NumLock is on or not (notepad behavior) 148 | var key = (ConsoleKey)keyCode; 149 | if (IsAltKeyDown(ir) && ((key >= ConsoleKey.NumPad0 && key <= ConsoleKey.NumPad9) 150 | || (key == ConsoleKey.Clear) || (key == ConsoleKey.Insert) 151 | || (key >= ConsoleKey.PageUp && key <= ConsoleKey.DownArrow))) 152 | { 153 | continue; 154 | } 155 | 156 | if (ir.Event.KeyEvent.wRepeatCount > 1) 157 | { 158 | ir.Event.KeyEvent.wRepeatCount--; 159 | _cachedInputRecord = ir; 160 | } 161 | 162 | break; 163 | } 164 | } 165 | 166 | // we did NOT have a previous keystroke with repeated characters. 167 | } 168 | 169 | var state = (ControlKeyState)ir.Event.KeyEvent.dwControlKeyState; 170 | var shift = (state & ControlKeyState.ShiftPressed) != 0; 171 | var alt = (state & (ControlKeyState.LeftAltPressed | ControlKeyState.RightAltPressed)) != 0; 172 | var control = (state & (ControlKeyState.LeftCtrlPressed | ControlKeyState.RightCtrlPressed)) != 0; 173 | 174 | return new ConsoleKeyInfo( 175 | (char)ir.Event.KeyEvent.uChar.UnicodeChar, 176 | (ConsoleKey)ir.Event.KeyEvent.wVirtualKeyCode, 177 | shift, alt, control); 178 | } 179 | 180 | private static bool IsKeyDownEvent(INPUT_RECORD ir) 181 | { 182 | return ir.EventType == KEY_EVENT && ir.Event.KeyEvent.bKeyDown.Value != 0; 183 | } 184 | 185 | private static bool IsModKey(INPUT_RECORD ir) 186 | { 187 | // We should also skip over Shift, Control, and Alt, as well as caps lock. 188 | // Apparently we don't need to check for 0xA0 through 0xA5, which are keys like 189 | // Left Control & Right Control. See the ConsoleKey enum for these values. 190 | var keyCode = ir.Event.KeyEvent.wVirtualKeyCode; 191 | return (keyCode >= 0x10 && keyCode <= 0x12) || keyCode == 0x14 || keyCode == 0x90 || keyCode == 0x91; 192 | } 193 | 194 | // For tracking Alt+NumPad unicode key sequence. When you press Alt key down 195 | // and press a numpad unicode decimal sequence and then release Alt key, the 196 | // desired effect is to translate the sequence into one Unicode KeyPress. 197 | // We need to keep track of the Alt+NumPad sequence and surface the final 198 | // unicode char alone when the Alt key is released. 199 | private static bool IsAltKeyDown(INPUT_RECORD ir) 200 | { 201 | return (((ControlKeyState)ir.Event.KeyEvent.dwControlKeyState) 202 | & (ControlKeyState.LeftAltPressed | ControlKeyState.RightAltPressed)) != 0; 203 | } 204 | } -------------------------------------------------------------------------------- /src/Spectre.Terminals/Drivers/Windows/WindowsSignals.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals.Drivers; 2 | 3 | internal sealed class WindowsSignals : IDisposable 4 | { 5 | private readonly object _lock; 6 | private bool _installed; 7 | 8 | private EventHandler? _event; 9 | 10 | public event EventHandler? Signalled 11 | { 12 | add 13 | { 14 | _event = value; 15 | InstallHandler(); 16 | } 17 | remove 18 | { 19 | UninstallHandler(); 20 | _event = null; 21 | } 22 | } 23 | 24 | public WindowsSignals() 25 | { 26 | _lock = new object(); 27 | } 28 | 29 | public void Dispose() 30 | { 31 | UninstallHandler(); 32 | } 33 | 34 | public static bool Emit(TerminalSignal signal) 35 | { 36 | uint? ctrlEvent = signal switch 37 | { 38 | TerminalSignal.SIGINT => WindowsConstants.Signals.CTRL_C_EVENT, 39 | TerminalSignal.SIGQUIT => WindowsConstants.Signals.CTRL_C_EVENT, 40 | _ => null, 41 | }; 42 | 43 | if (ctrlEvent == null) 44 | { 45 | return false; 46 | } 47 | 48 | return PInvoke.GenerateConsoleCtrlEvent(ctrlEvent.Value, 0); 49 | } 50 | 51 | private BOOL Callback(uint ctrlType) 52 | { 53 | var @event = _event; 54 | 55 | TerminalSignal? signal = null; 56 | switch (ctrlType) 57 | { 58 | case WindowsConstants.Signals.CTRL_C_EVENT: 59 | signal = TerminalSignal.SIGINT; 60 | break; 61 | case WindowsConstants.Signals.CTRL_BREAK_EVENT: 62 | signal = TerminalSignal.SIGQUIT; 63 | break; 64 | } 65 | 66 | if (@event != null && signal != null) 67 | { 68 | var args = new TerminalSignalEventArgs(signal.Value); 69 | @event(null, args); 70 | return args.Cancel; 71 | } 72 | 73 | return false; 74 | } 75 | 76 | private void InstallHandler() 77 | { 78 | lock (_lock) 79 | { 80 | if (!_installed) 81 | { 82 | if (!PInvoke.SetConsoleCtrlHandler(Callback, true)) 83 | { 84 | var errorCode = Marshal.GetLastWin32Error(); 85 | throw new InvalidOperationException( 86 | $"An error occured when installing Windows signal handler. Error code: {errorCode}"); 87 | } 88 | 89 | _installed = true; 90 | } 91 | } 92 | } 93 | 94 | private void UninstallHandler() 95 | { 96 | lock (_lock) 97 | { 98 | if (_installed) 99 | { 100 | if (!PInvoke.SetConsoleCtrlHandler(Callback, false)) 101 | { 102 | var errorCode = Marshal.GetLastWin32Error(); 103 | throw new InvalidOperationException( 104 | $"An error occured when uninstalling Windows signal handler. Error code: {errorCode}"); 105 | } 106 | 107 | _installed = false; 108 | } 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Drivers/Windows/WindowsTerminalHandle.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals.Drivers; 2 | 3 | internal abstract class WindowsTerminalHandle : IDisposable 4 | { 5 | private readonly object _lock; 6 | 7 | public SafeHandle Handle { get; set; } 8 | public bool IsRedirected { get; } 9 | 10 | protected WindowsTerminalHandle(STD_HANDLE_TYPE handle) 11 | { 12 | _lock = new object(); 13 | 14 | Handle = PInvoke.GetStdHandle_SafeHandle(handle); 15 | IsRedirected = !GetMode(out _) || (PInvoke.GetFileType(Handle) & 2) == 0; 16 | } 17 | 18 | public void Dispose() 19 | { 20 | Handle.Dispose(); 21 | } 22 | 23 | public unsafe bool GetMode([NotNullWhen(true)] out CONSOLE_MODE? mode) 24 | { 25 | if (PInvoke.GetConsoleMode(Handle, out var result)) 26 | { 27 | mode = result; 28 | return true; 29 | } 30 | 31 | mode = null; 32 | return false; 33 | } 34 | 35 | public unsafe bool AddMode(CONSOLE_MODE mode) 36 | { 37 | lock (_lock) 38 | { 39 | if (GetMode(out var currentMode)) 40 | { 41 | return PInvoke.SetConsoleMode(Handle, currentMode.Value | mode); 42 | } 43 | 44 | return false; 45 | } 46 | } 47 | 48 | public unsafe bool RemoveMode(CONSOLE_MODE mode) 49 | { 50 | lock (_lock) 51 | { 52 | if (GetMode(out var currentMode)) 53 | { 54 | return PInvoke.SetConsoleMode(Handle, currentMode.Value & ~mode); 55 | } 56 | 57 | return false; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Drivers/Windows/WindowsTerminalReader.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals.Drivers; 2 | 3 | internal sealed class WindowsTerminalReader : WindowsTerminalHandle, ITerminalReader 4 | { 5 | private readonly WindowsDriver _driver; 6 | private readonly WindowsKeyReader _keyReader; 7 | private readonly SynchronizedTextReader _reader; 8 | private Encoding _encoding; 9 | 10 | public Encoding Encoding 11 | { 12 | get => _encoding; 13 | set => SetEncoding(value); 14 | } 15 | 16 | public bool IsRawMode => _driver.IsRawMode; 17 | public bool IsKeyAvailable => _keyReader.IsKeyAvailable(); 18 | 19 | public WindowsTerminalReader(WindowsDriver driver) 20 | : base(STD_HANDLE_TYPE.STD_INPUT_HANDLE) 21 | { 22 | _encoding = EncodingHelper.GetEncodingFromCodePage((int)PInvoke.GetConsoleCP()); 23 | _driver = driver; 24 | _keyReader = new WindowsKeyReader(Handle); 25 | _reader = CreateReader(Handle, _encoding, IsRedirected); 26 | } 27 | 28 | public int Read() 29 | { 30 | return _reader.Read(); 31 | } 32 | 33 | public string? ReadLine() 34 | { 35 | return _reader.ReadLine(); 36 | } 37 | 38 | public ConsoleKeyInfo ReadKey() 39 | { 40 | return _keyReader.ReadKey(); 41 | } 42 | 43 | private static SynchronizedTextReader CreateReader(SafeHandle handle, Encoding encoding, bool isRedirected) 44 | { 45 | static Stream Create(SafeHandle handle, bool useFileApis) 46 | { 47 | if (handle.IsInvalid || handle.IsClosed) 48 | { 49 | return Stream.Null; 50 | } 51 | else 52 | { 53 | return new WindowsConsoleStream(handle, useFileApis); 54 | } 55 | } 56 | 57 | var useFileApis = !encoding.IsUnicode() || isRedirected; 58 | 59 | var stream = Create(handle, useFileApis); 60 | if (stream == null || stream == Stream.Null) 61 | { 62 | return new SynchronizedTextReader(StreamReader.Null); 63 | } 64 | 65 | return new SynchronizedTextReader( 66 | new StreamReader( 67 | stream: stream, 68 | encoding: new EncodingWithoutPreamble(encoding), 69 | detectEncodingFromByteOrderMarks: false, 70 | bufferSize: 4096, 71 | leaveOpen: true)); 72 | } 73 | 74 | private void SetEncoding(Encoding encoding) 75 | { 76 | if (PInvoke.SetConsoleCP((uint)encoding.CodePage)) 77 | { 78 | // TODO 2021-07-31: Recreate text reader 79 | _encoding = encoding; 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Drivers/Windows/WindowsTerminalWriter.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals.Drivers; 2 | 3 | internal sealed class WindowsTerminalWriter : WindowsTerminalHandle, IWindowsTerminalWriter 4 | { 5 | private readonly string _name; 6 | private Encoding _encoding; 7 | 8 | public Encoding Encoding 9 | { 10 | get => _encoding; 11 | set => SetEncoding(value); 12 | } 13 | 14 | public WindowsTerminalWriter(STD_HANDLE_TYPE handle) 15 | : base(handle) 16 | { 17 | _name = handle == STD_HANDLE_TYPE.STD_OUTPUT_HANDLE ? "STDOUT" : "STDERR"; 18 | _encoding = EncodingHelper.GetEncodingFromCodePage((int)PInvoke.GetConsoleOutputCP()); 19 | } 20 | 21 | public unsafe void Write(ReadOnlySpan buffer) 22 | { 23 | Write(Handle, buffer); 24 | } 25 | 26 | public unsafe void Write(SafeHandle handle, ReadOnlySpan buffer) 27 | { 28 | if (buffer.IsEmpty) 29 | { 30 | return; 31 | } 32 | 33 | uint written; 34 | uint* ptrWritten = &written; 35 | 36 | fixed (byte* ptrData = buffer) 37 | { 38 | if (PInvoke.WriteFile(handle, ptrData, (uint)buffer.Length, ptrWritten, null)) 39 | { 40 | return; 41 | } 42 | } 43 | 44 | switch (Marshal.GetLastWin32Error()) 45 | { 46 | case WindowsConstants.ERROR_HANDLE_EOF: 47 | case WindowsConstants.ERROR_BROKEN_PIPE: 48 | case WindowsConstants.ERROR_NO_DATA: 49 | break; 50 | default: 51 | throw new InvalidOperationException($"Could not write to {_name}"); 52 | } 53 | } 54 | 55 | private void SetEncoding(Encoding encoding) 56 | { 57 | if (encoding is null) 58 | { 59 | throw new ArgumentNullException(nameof(encoding)); 60 | } 61 | 62 | if (PInvoke.SetConsoleOutputCP((uint)encoding.CodePage)) 63 | { 64 | _encoding = encoding; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Emulation/AnsiInstruction.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals.Emulation; 2 | 3 | internal abstract class AnsiInstruction 4 | { 5 | public abstract void Accept(IAnsiSequenceVisitor visitor, TState state); 6 | } 7 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Emulation/AnsiInterpreter.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals.Emulation; 2 | 3 | internal static class AnsiInterpreter 4 | { 5 | public static void Interpret(IAnsiSequenceVisitor visitor, TContext context, string text) 6 | { 7 | Interpret(visitor, context, text.AsMemory()); 8 | } 9 | 10 | public static void Interpret(IAnsiSequenceVisitor visitor, TContext context, ReadOnlyMemory buffer) 11 | { 12 | if (visitor is null) 13 | { 14 | throw new ArgumentNullException(nameof(visitor)); 15 | } 16 | 17 | var instructions = AnsiParser.Parse(buffer); 18 | foreach (var instruction in instructions) 19 | { 20 | instruction.Accept(visitor, context); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Emulation/IAnsiSequenceVisitor.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals.Emulation; 2 | 3 | /// 4 | /// Represents a VT/ANSI sequence visitor. 5 | /// 6 | /// The state. 7 | internal interface IAnsiSequenceVisitor 8 | { 9 | /// 10 | /// Handles a request to move the cursor backwards. 11 | /// 12 | /// The instruction. 13 | /// The state. 14 | void CursorBack(CursorBack instruction, TState state); 15 | 16 | /// 17 | /// Handles a request to move the cursor down. 18 | /// 19 | /// The instruction. 20 | /// The state. 21 | void CursorDown(CursorDown instruction, TState state); 22 | 23 | /// 24 | /// Handles a request to move the cursor forward. 25 | /// 26 | /// The instruction. 27 | /// The state. 28 | void CursorForward(CursorForward instruction, TState state); 29 | 30 | /// 31 | /// Handles a request to set the cursor's horizontal position. 32 | /// 33 | /// The instruction. 34 | /// The state. 35 | void CursorHorizontalAbsolute(CursorHorizontalAbsolute instruction, TState state); 36 | 37 | /// 38 | /// Handles a request to move the cursor to the next line 39 | /// and to set the cursor column to 0 (zero) position once done. 40 | /// 41 | /// The instruction. 42 | /// The state. 43 | void CursorNextLine(CursorNextLine instruction, TState state); 44 | 45 | /// 46 | /// Handles a request to set the cursor position. 47 | /// 48 | /// The instruction. 49 | /// The state. 50 | void CursorPosition(CursorPosition instruction, TState state); 51 | 52 | /// 53 | /// Handles a request to move the cursor to the previous line 54 | /// and to set the cursor column to 0 (zero) position once done. 55 | /// 56 | /// The instruction. 57 | /// The state. 58 | void CursorPreviousLine(CursorPreviousLine instruction, TState state); 59 | 60 | /// 61 | /// Handles a request to move the cursor up. 62 | /// 63 | /// The instruction. 64 | /// The state. 65 | void CursorUp(CursorUp instruction, TState state); 66 | 67 | /// 68 | /// Handles a request to clear the whole, or part of, the display. 69 | /// 70 | /// The instruction. 71 | /// The state. 72 | void EraseInDisplay(EraseInDisplay instruction, TState state); 73 | 74 | /// 75 | /// Handles a request to clear the whole, or part of, the current line. 76 | /// 77 | /// The instruction. 78 | /// The state. 79 | void EraseInLine(EraseInLine instruction, TState state); 80 | 81 | /// 82 | /// Handles a request to print text. 83 | /// 84 | /// The instruction. 85 | /// The state. 86 | void PrintText(PrintText instruction, TState state); 87 | 88 | /// 89 | /// Handles a request to restore the cursor position to 90 | /// it's previously stored value. 91 | /// 92 | /// The instruction. 93 | /// The state. 94 | void RestoreCursor(RestoreCursor instruction, TState state); 95 | 96 | /// 97 | /// Handles a request to store the cursor position. 98 | /// 99 | /// The instruction. 100 | /// The state. 101 | void StoreCursor(StoreCursor instruction, TState state); 102 | 103 | /// 104 | /// Handles a request to hide the cursor. 105 | /// 106 | /// The instruction. 107 | /// The state. 108 | void HideCursor(HideCursor instruction, TState state); 109 | 110 | /// 111 | /// Handles a request to show the cursor. 112 | /// 113 | /// The instruction. 114 | /// The state. 115 | void ShowCursor(ShowCursor instruction, TState state); 116 | 117 | /// 118 | /// Handles a request to enable the alternate buffer. 119 | /// 120 | /// The instruction. 121 | /// The state. 122 | void EnableAlternativeBuffer(EnableAlternativeBuffer instruction, TState state); 123 | 124 | /// 125 | /// Handles a request to disable the alternate buffer. 126 | /// 127 | /// The instruction. 128 | /// The state. 129 | void DisableAlternativeBuffer(DisableAlternativeBuffer instruction, TState state); 130 | 131 | /// 132 | /// Handles a request to set the appearance of subsequent characters. 133 | /// 134 | /// The instruction. 135 | /// The state. 136 | void SelectGraphicRendition(SelectGraphicRendition instruction, TState state); 137 | } 138 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Emulation/Instructions/CursorBack.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals.Emulation; 2 | 3 | internal sealed class CursorBack : AnsiInstruction 4 | { 5 | public int Count { get; } 6 | 7 | public CursorBack(int count) 8 | { 9 | Count = count; 10 | } 11 | 12 | public override void Accept(IAnsiSequenceVisitor visitor, TState context) 13 | { 14 | visitor.CursorBack(this, context); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Emulation/Instructions/CursorDown.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals.Emulation; 2 | 3 | internal sealed class CursorDown : AnsiInstruction 4 | { 5 | public int Count { get; } 6 | 7 | public CursorDown(int count) 8 | { 9 | Count = count; 10 | } 11 | 12 | public override void Accept(IAnsiSequenceVisitor visitor, TState context) 13 | { 14 | visitor.CursorDown(this, context); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Emulation/Instructions/CursorForward.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals.Emulation; 2 | 3 | internal sealed class CursorForward : AnsiInstruction 4 | { 5 | public int Count { get; } 6 | 7 | public CursorForward(int count) 8 | { 9 | Count = count; 10 | } 11 | 12 | public override void Accept(IAnsiSequenceVisitor visitor, TState context) 13 | { 14 | visitor.CursorForward(this, context); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Emulation/Instructions/CursorHorizontalAbsolute.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals.Emulation; 2 | 3 | internal sealed class CursorHorizontalAbsolute : AnsiInstruction 4 | { 5 | public int Column { get; } 6 | 7 | public CursorHorizontalAbsolute(int count) 8 | { 9 | Column = count; 10 | } 11 | 12 | public override void Accept(IAnsiSequenceVisitor visitor, TState context) 13 | { 14 | visitor.CursorHorizontalAbsolute(this, context); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Emulation/Instructions/CursorNextLine.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals.Emulation; 2 | 3 | internal sealed class CursorNextLine : AnsiInstruction 4 | { 5 | public int Count { get; } 6 | 7 | public CursorNextLine(int count) 8 | { 9 | Count = count; 10 | } 11 | 12 | public override void Accept(IAnsiSequenceVisitor visitor, TState context) 13 | { 14 | visitor.CursorNextLine(this, context); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Emulation/Instructions/CursorPosition.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals.Emulation; 2 | 3 | internal sealed class CursorPosition : AnsiInstruction 4 | { 5 | public int Column { get; } 6 | public int Row { get; } 7 | 8 | public CursorPosition(int column, int row) 9 | { 10 | Column = column; 11 | Row = row; 12 | } 13 | 14 | public override void Accept(IAnsiSequenceVisitor visitor, TState context) 15 | { 16 | visitor.CursorPosition(this, context); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Emulation/Instructions/CursorPreviousLine.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals.Emulation; 2 | 3 | internal sealed class CursorPreviousLine : AnsiInstruction 4 | { 5 | public int Count { get; } 6 | 7 | public CursorPreviousLine(int count) 8 | { 9 | Count = count; 10 | } 11 | 12 | public override void Accept(IAnsiSequenceVisitor visitor, TState context) 13 | { 14 | visitor.CursorPreviousLine(this, context); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Emulation/Instructions/CursorUp.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals.Emulation; 2 | 3 | internal sealed class CursorUp : AnsiInstruction 4 | { 5 | public int Count { get; } 6 | 7 | public CursorUp(int count) 8 | { 9 | Count = count; 10 | } 11 | 12 | public override void Accept(IAnsiSequenceVisitor visitor, TState context) 13 | { 14 | visitor.CursorUp(this, context); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Emulation/Instructions/DisableAlternativeBuffer.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals.Emulation; 2 | 3 | internal sealed class DisableAlternativeBuffer : AnsiInstruction 4 | { 5 | public override void Accept(IAnsiSequenceVisitor visitor, TState state) 6 | { 7 | visitor.DisableAlternativeBuffer(this, state); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Emulation/Instructions/EnableAlternativeBuffer.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals.Emulation; 2 | 3 | internal sealed class EnableAlternativeBuffer : AnsiInstruction 4 | { 5 | public override void Accept(IAnsiSequenceVisitor visitor, TState state) 6 | { 7 | visitor.EnableAlternativeBuffer(this, state); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Emulation/Instructions/EraseInDisplay.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals.Emulation; 2 | 3 | internal sealed class EraseInDisplay : AnsiInstruction 4 | { 5 | public int Mode { get; } 6 | 7 | public EraseInDisplay(int mode) 8 | { 9 | Mode = mode; 10 | } 11 | 12 | public override void Accept(IAnsiSequenceVisitor visitor, TState context) 13 | { 14 | visitor.EraseInDisplay(this, context); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Emulation/Instructions/EraseInLine.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals.Emulation; 2 | 3 | internal sealed class EraseInLine : AnsiInstruction 4 | { 5 | public int Mode { get; } 6 | 7 | public EraseInLine(int mode) 8 | { 9 | Mode = mode; 10 | } 11 | 12 | public override void Accept(IAnsiSequenceVisitor visitor, TState context) 13 | { 14 | visitor.EraseInLine(this, context); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Emulation/Instructions/HideCursor.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals.Emulation; 2 | 3 | internal sealed class HideCursor : AnsiInstruction 4 | { 5 | public override void Accept(IAnsiSequenceVisitor visitor, TState state) 6 | { 7 | visitor.HideCursor(this, state); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Emulation/Instructions/PrintText.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals.Emulation; 2 | 3 | internal sealed class PrintText : AnsiInstruction 4 | { 5 | public ReadOnlyMemory Text { get; } 6 | 7 | public PrintText(ReadOnlyMemory text) 8 | { 9 | Text = text; 10 | } 11 | 12 | public override void Accept(IAnsiSequenceVisitor visitor, TState context) 13 | { 14 | visitor.PrintText(this, context); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Emulation/Instructions/RestoreCursor.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals.Emulation; 2 | 3 | internal sealed class RestoreCursor : AnsiInstruction 4 | { 5 | public override void Accept(IAnsiSequenceVisitor visitor, TState context) 6 | { 7 | visitor.RestoreCursor(this, context); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Emulation/Instructions/SelectGraphicRendition.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals.Emulation; 2 | 3 | internal sealed class SelectGraphicRendition : AnsiInstruction 4 | { 5 | public IReadOnlyList Operations { get; } 6 | 7 | internal sealed class Operation 8 | { 9 | public bool Reset { get; set; } 10 | public Color? Foreground { get; set; } 11 | public Color? Background { get; set; } 12 | } 13 | 14 | public SelectGraphicRendition(IEnumerable ops) 15 | { 16 | Operations = ops as IReadOnlyList 17 | ?? new List(ops); 18 | } 19 | 20 | public override void Accept(IAnsiSequenceVisitor visitor, TState state) 21 | { 22 | visitor.SelectGraphicRendition(this, state); 23 | } 24 | } 25 | 26 | internal readonly struct Color 27 | { 28 | public int? Number { get; } 29 | public int? Red { get; } 30 | public int? Green { get; } 31 | public int? Blue { get; } 32 | 33 | public bool IsNumber => Number != null; 34 | public bool IsRgb => Red != null && Green != null && Blue != null; 35 | 36 | public Color(int number) 37 | { 38 | Number = number; 39 | Red = null; 40 | Green = null; 41 | Blue = null; 42 | } 43 | 44 | public Color(int red, int green, int blue) 45 | { 46 | Number = null; 47 | Red = red; 48 | Green = green; 49 | Blue = blue; 50 | } 51 | 52 | public override string ToString() 53 | { 54 | if (Number != null) 55 | { 56 | return Number.Value.ToString(); 57 | } 58 | else if (Red != null && Green != null && Blue != null) 59 | { 60 | return $"{Red},{Green},{Blue}"; 61 | } 62 | 63 | return "?"; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Emulation/Instructions/ShowCursor.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals.Emulation; 2 | 3 | internal sealed class ShowCursor : AnsiInstruction 4 | { 5 | public override void Accept(IAnsiSequenceVisitor visitor, TState state) 6 | { 7 | visitor.ShowCursor(this, state); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Emulation/Instructions/StoreCursor.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals.Emulation; 2 | 3 | internal sealed class StoreCursor : AnsiInstruction 4 | { 5 | public override void Accept(IAnsiSequenceVisitor visitor, TState context) 6 | { 7 | visitor.StoreCursor(this, context); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Emulation/Parsing/AnsiToken.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals.Emulation; 2 | 3 | internal sealed class AnsiToken 4 | { 5 | private readonly ReadOnlyMemory? _span; 6 | private readonly IReadOnlyList? _tokens; 7 | 8 | public ReadOnlyMemory Text => _span ?? ReadOnlyMemory.Empty; 9 | public IReadOnlyList Sequence => _tokens ?? Array.Empty(); 10 | 11 | public bool IsText => _span != null; 12 | public bool IsSequence => _tokens != null; 13 | 14 | private AnsiToken(ReadOnlyMemory? span, IReadOnlyList? tokens) 15 | { 16 | _span = span; 17 | _tokens = tokens; 18 | } 19 | 20 | public static AnsiToken CreateText(ReadOnlyMemory span) 21 | { 22 | return new AnsiToken(span, null); 23 | } 24 | 25 | public static AnsiToken CreateSequence(IReadOnlyList? tokens) 26 | { 27 | return new AnsiToken(null, tokens); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Emulation/Parsing/Tokenization/AnsiSequenceSplitter.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals.Emulation; 2 | 3 | internal static class AnsiSequenceSplitter 4 | { 5 | public static List<(ReadOnlyMemory Text, bool IsSequence)> Split(ReadOnlyMemory buffer) 6 | { 7 | var index = 0; 8 | var end = 0; 9 | 10 | var result = new List<(ReadOnlyMemory, bool)>(); 11 | while (index < buffer.Length) 12 | { 13 | // Encounter ESC? 14 | if (buffer.Span[index] == 0x1b) 15 | { 16 | var start = index; 17 | index++; 18 | 19 | if (index > buffer.Length - 1) 20 | { 21 | break; 22 | } 23 | 24 | // Not CSI? 25 | if (buffer.Span[index] != '[') 26 | { 27 | continue; 28 | } 29 | 30 | index++; 31 | 32 | if (index >= buffer.Length) 33 | { 34 | break; 35 | } 36 | 37 | // Any number (including none) of "parameter bytes" in the range 0x30–0x3F 38 | if (!EatOptionalRange(buffer, 0x30, 0x3f, ref index)) 39 | { 40 | break; 41 | } 42 | 43 | // Any number of "intermediate bytes" in the range 0x20–0x2F 44 | if (!EatOptionalRange(buffer, 0x20, 0x2f, ref index)) 45 | { 46 | break; 47 | } 48 | 49 | // A single "final byte" in the range 0x40–0x7E 50 | var terminal = buffer.Span[index]; 51 | if (terminal < 0x40 || terminal > 0x7e) 52 | { 53 | throw new InvalidOperationException("Malformed ANSI escape code"); 54 | } 55 | 56 | index++; 57 | 58 | // Need to flush? 59 | if (end < start) 60 | { 61 | result.Add((buffer[end..start], false)); 62 | } 63 | 64 | // Add the escape code to the result 65 | end = index; 66 | result.Add((buffer[start..end], true)); 67 | 68 | continue; 69 | } 70 | 71 | index++; 72 | } 73 | 74 | // More to flush? 75 | if (end < buffer.Length) 76 | { 77 | result.Add((buffer[end..buffer.Length], false)); 78 | } 79 | 80 | return result; 81 | } 82 | 83 | private static bool EatOptionalRange(ReadOnlyMemory buffer, int start, int stop, ref int index) 84 | { 85 | while (true) 86 | { 87 | if (index >= buffer.Length) 88 | { 89 | return false; 90 | } 91 | 92 | var current1 = buffer.Span[index]; 93 | if (current1 < start || current1 > stop) 94 | { 95 | return true; 96 | } 97 | 98 | index++; 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Emulation/Parsing/Tokenization/AnsiSequenceToken.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals.Emulation; 2 | 3 | internal sealed class AnsiSequenceToken 4 | { 5 | public AnsiSequenceTokenType Type { get; } 6 | public ReadOnlyMemory Content { get; set; } 7 | 8 | public char? AsCharacter() 9 | { 10 | return Content.Span[Content.Length - 1]; 11 | } 12 | 13 | public AnsiSequenceToken(AnsiSequenceTokenType type, ReadOnlyMemory value) 14 | { 15 | Type = type; 16 | Content = value; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Emulation/Parsing/Tokenization/AnsiSequenceTokenType.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals.Emulation; 2 | 3 | internal enum AnsiSequenceTokenType 4 | { 5 | Unknown, 6 | Csi, 7 | Character, 8 | Integer, 9 | Delimiter, 10 | Bang, 11 | Query, 12 | Equals, 13 | Send, 14 | } 15 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Emulation/Parsing/Tokenization/AnsiSequenceTokenizer.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals.Emulation; 2 | 3 | internal static class AnsiSequenceTokenizer 4 | { 5 | public static IReadOnlyList Tokenize(MemoryCursor buffer) 6 | { 7 | var result = new List(); 8 | while (buffer.CanRead) 9 | { 10 | if (!ReadSequenceToken(buffer, out var token)) 11 | { 12 | // Could not parse, so return an empty result 13 | return Array.Empty(); 14 | } 15 | 16 | result.Add(token); 17 | } 18 | 19 | return result; 20 | } 21 | 22 | private static bool ReadSequenceToken(MemoryCursor buffer, [NotNullWhen(true)] out AnsiSequenceToken? token) 23 | { 24 | var current = buffer.PeekChar(); 25 | 26 | // ESC? 27 | if (current == 0x1b) 28 | { 29 | // CSI? 30 | var start = buffer.Position; 31 | buffer.Discard(); 32 | if (buffer.CanRead && buffer.PeekChar() == '[') 33 | { 34 | buffer.Discard(); 35 | token = new AnsiSequenceToken(AnsiSequenceTokenType.Csi, buffer.Slice(start, buffer.Position)); 36 | return true; 37 | } 38 | 39 | // Unknown escape sequence 40 | token = null; 41 | return false; 42 | } 43 | else if (char.IsNumber(current)) 44 | { 45 | // Number 46 | var start = buffer.Position; 47 | while (buffer.CanRead) 48 | { 49 | current = buffer.PeekChar(); 50 | if (!char.IsNumber(current)) 51 | { 52 | break; 53 | } 54 | 55 | buffer.Discard(); 56 | } 57 | 58 | var end = buffer.Position; 59 | 60 | token = new AnsiSequenceToken( 61 | AnsiSequenceTokenType.Integer, 62 | buffer.Slice(start, end)); 63 | 64 | return true; 65 | } 66 | else if (char.IsLetter(current)) 67 | { 68 | // Letter 69 | var start = buffer.Position; 70 | buffer.Discard(); 71 | token = new AnsiSequenceToken( 72 | AnsiSequenceTokenType.Character, 73 | buffer.Slice(start, start + 1)); 74 | 75 | return true; 76 | } 77 | else if (current == ';') 78 | { 79 | // Separator 80 | var start = buffer.Position; 81 | buffer.Discard(); 82 | token = new AnsiSequenceToken( 83 | AnsiSequenceTokenType.Delimiter, 84 | buffer.Slice(start, start + 1)); 85 | 86 | return true; 87 | } 88 | else if (current == '?') 89 | { 90 | // Query 91 | var start = buffer.Position; 92 | buffer.Discard(); 93 | token = new AnsiSequenceToken( 94 | AnsiSequenceTokenType.Query, 95 | buffer.Slice(start, start + 1)); 96 | 97 | return true; 98 | } 99 | 100 | token = null; 101 | return false; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Extensions/EncodingExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals; 2 | 3 | internal static class EncodingExtensions 4 | { 5 | private const int UnicodeCodePage = 1200; 6 | 7 | public static bool IsUnicode(this Encoding encoding) 8 | { 9 | return encoding.CodePage == UnicodeCodePage; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Extensions/EnumerableExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals; 2 | 3 | internal static class EnumerableExtensions 4 | { 5 | public static IEnumerable<(bool First, bool Last, T Item)> Enumerate(this IEnumerator source) 6 | { 7 | if (source is null) 8 | { 9 | throw new ArgumentNullException(nameof(source)); 10 | } 11 | 12 | var first = true; 13 | var last = !source.MoveNext(); 14 | T current; 15 | 16 | for (var index = 0; !last; index++) 17 | { 18 | current = source.Current; 19 | last = !source.MoveNext(); 20 | yield return (first, last, current); 21 | first = false; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Extensions/ITerminalExtensions.Ansi.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals; 2 | 3 | /// 4 | /// Contains extension methods for . 5 | /// 6 | public static partial class ITerminalExtensions 7 | { 8 | /// 9 | /// Moves the cursor. 10 | /// 11 | /// The terminal. 12 | /// The direction to move the cursor. 13 | /// The number of steps to move the cursor. 14 | public static void MoveCursor(this ITerminal terminal, CursorDirection direction, int count = 1) 15 | { 16 | if (count <= 0) 17 | { 18 | return; 19 | } 20 | 21 | switch (direction) 22 | { 23 | case CursorDirection.Up: 24 | terminal.Write($"\u001b[{count}A"); 25 | break; 26 | case CursorDirection.Down: 27 | terminal.Write($"\u001b[{count}B"); 28 | break; 29 | case CursorDirection.Forward: 30 | terminal.Write($"\u001b[{count}C"); 31 | break; 32 | case CursorDirection.Back: 33 | terminal.Write($"\u001b[{count}D"); 34 | break; 35 | } 36 | } 37 | 38 | /// 39 | /// Sets the cursor position. 40 | /// 41 | /// The terminal. 42 | /// The row. 43 | /// The column. 44 | public static void SetCursorProsition(this ITerminal terminal, int row, int column) 45 | { 46 | row = Math.Max(0, row); 47 | column = Math.Max(0, column); 48 | 49 | terminal.Write($"\u001b[{row};{column}H"); 50 | } 51 | 52 | /// 53 | /// Moves the cursor down and resets the column position. 54 | /// 55 | /// The terminal. 56 | /// The number of lines to move down. 57 | public static void MoveCursorToNextLine(this ITerminal terminal, int count) 58 | { 59 | if (count <= 0) 60 | { 61 | return; 62 | } 63 | 64 | terminal.Write($"\u001b[{count}E"); 65 | } 66 | 67 | /// 68 | /// Moves the cursor up and resets the column position. 69 | /// 70 | /// The terminal. 71 | /// The number of lines to move up. 72 | public static void MoveCursorToPreviousLine(this ITerminal terminal, int count) 73 | { 74 | if (count <= 0) 75 | { 76 | return; 77 | } 78 | 79 | terminal.Write($"\u001b[{count}F"); 80 | } 81 | 82 | /// 83 | /// Moves the cursor to the specified column. 84 | /// 85 | /// The terminal. 86 | /// The column to move to. 87 | public static void MoveCursorToColumn(this ITerminal terminal, int column) 88 | { 89 | if (column <= 0) 90 | { 91 | return; 92 | } 93 | 94 | terminal.Write($"\u001b[{column}G"); 95 | } 96 | 97 | /// 98 | /// Clears the display. 99 | /// 100 | /// The terminal. 101 | /// The clear options. 102 | public static void Clear(this ITerminal terminal, ClearDisplay option = ClearDisplay.Everything) 103 | { 104 | switch (option) 105 | { 106 | case ClearDisplay.AfterCursor: 107 | terminal.Write("\u001b[0J"); 108 | break; 109 | case ClearDisplay.BeforeCursor: 110 | terminal.Write("\u001b[1J"); 111 | break; 112 | case ClearDisplay.Everything: 113 | terminal.Write("\u001b[2J"); 114 | break; 115 | case ClearDisplay.EverythingAndScrollbackBuffer: 116 | terminal.Write("\u001b[3J"); 117 | break; 118 | } 119 | } 120 | 121 | /// 122 | /// Clears the current line. 123 | /// 124 | /// The terminal. 125 | /// The clear options. 126 | public static void Clear(this ITerminal terminal, ClearLine option) 127 | { 128 | switch (option) 129 | { 130 | case ClearLine.AfterCursor: 131 | terminal.Write("\u001b[0K"); 132 | break; 133 | case ClearLine.BeforeCursor: 134 | terminal.Write("\u001b[1K"); 135 | break; 136 | case ClearLine.WholeLine: 137 | terminal.Write("\u001b[2K"); 138 | break; 139 | } 140 | } 141 | 142 | /// 143 | /// Saves the cursor position. 144 | /// 145 | /// The terminal. 146 | public static void SaveCursorPosition(this ITerminal terminal) 147 | { 148 | terminal.Write("\u001b[s"); 149 | } 150 | 151 | /// 152 | /// Restores the cursor position. 153 | /// 154 | /// The terminal. 155 | public static void RestoreCursorPosition(this ITerminal terminal) 156 | { 157 | terminal.Write("\u001b[u"); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Extensions/ITerminalExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals; 2 | 3 | /// 4 | /// Contains extension methods for . 5 | /// 6 | public static partial class ITerminalExtensions 7 | { 8 | /// 9 | /// Writes the specified buffer to the terminal's output handle. 10 | /// 11 | /// The terminal. 12 | /// The value to write. 13 | public static void Write(this ITerminal terminal, ReadOnlySpan value) 14 | { 15 | terminal.Output.Write(value); 16 | } 17 | 18 | /// 19 | /// Writes the specified text to the terminal's output handle. 20 | /// 21 | /// The terminal. 22 | /// The value to write. 23 | public static void Write(this ITerminal terminal, string? value) 24 | { 25 | terminal.Output.Write(value); 26 | } 27 | 28 | /// 29 | /// Writes an empty line to the terminal's output handle. 30 | /// 31 | /// The terminal. 32 | public static void WriteLine(this ITerminal terminal) 33 | { 34 | terminal.Output.WriteLine(); 35 | } 36 | 37 | /// 38 | /// Writes the specified text followed by a line break to the terminal's output handle. 39 | /// 40 | /// The terminal. 41 | /// The value to write. 42 | public static void WriteLine(this ITerminal terminal, string? value) 43 | { 44 | terminal.Output.WriteLine(value); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Extensions/TerminalOutputExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals; 2 | 3 | /// 4 | /// Contains extension methods for . 5 | /// 6 | public static class TerminalOutputExtensions 7 | { 8 | /// 9 | /// Writes the specified text. 10 | /// 11 | /// The writer. 12 | /// The value to write. 13 | public static void Write(this TerminalOutput writer, string? value) 14 | { 15 | if (writer is null) 16 | { 17 | throw new ArgumentNullException(nameof(writer)); 18 | } 19 | 20 | if (string.IsNullOrEmpty(value)) 21 | { 22 | return; 23 | } 24 | 25 | writer.Write(value.AsSpan()); 26 | } 27 | 28 | /// 29 | /// Writes an empty line. 30 | /// 31 | /// The writer. 32 | public static void WriteLine(this TerminalOutput writer) 33 | { 34 | if (writer is null) 35 | { 36 | throw new ArgumentNullException(nameof(writer)); 37 | } 38 | 39 | Write(writer, Environment.NewLine); 40 | } 41 | 42 | /// 43 | /// Writes the specified text followed by a line break. 44 | /// 45 | /// The writer. 46 | /// The value to write. 47 | public static void WriteLine(this TerminalOutput writer, string? value) 48 | { 49 | if (writer is null) 50 | { 51 | throw new ArgumentNullException(nameof(writer)); 52 | } 53 | 54 | Write(writer, value); 55 | Write(writer, Environment.NewLine); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/ITerminal.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals; 2 | 3 | /// 4 | /// Represents a terminal. 5 | /// 6 | public interface ITerminal : IDisposable 7 | { 8 | /// 9 | /// Gets the name of the terminal driver. 10 | /// 11 | string Name { get; } 12 | 13 | /// 14 | /// Gets a value indicating whether or not the terminal is in raw mode. 15 | /// 16 | bool IsRawMode { get; } 17 | 18 | /// 19 | /// Occurs when a signal is received. 20 | /// 21 | event EventHandler? Signalled; 22 | 23 | /// 24 | /// Gets the terminal size. 25 | /// 26 | TerminalSize? Size { get; } 27 | 28 | /// 29 | /// Gets a for STDIN. 30 | /// 31 | TerminalInput Input { get; } 32 | 33 | /// 34 | /// Gets a for STDOUT. 35 | /// 36 | TerminalOutput Output { get; } 37 | 38 | /// 39 | /// Gets a for STDERR. 40 | /// 41 | TerminalOutput Error { get; } 42 | 43 | /// 44 | /// Emits a signal. 45 | /// 46 | /// The signal to emit. 47 | /// true if successful, otherwise false. 48 | bool EmitSignal(TerminalSignal signal); 49 | 50 | /// 51 | /// Enables raw mode. 52 | /// 53 | /// true if the operation succeeded, otherwise false. 54 | bool EnableRawMode(); 55 | 56 | /// 57 | /// Disables raw mode. 58 | /// 59 | /// true if the operation succeeded, otherwise false. 60 | bool DisableRawMode(); 61 | } 62 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/ITerminalDriver.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals; 2 | 3 | /// 4 | /// Represent a terminal driver. 5 | /// 6 | public interface ITerminalDriver : IDisposable 7 | { 8 | /// 9 | /// Gets the name of the terminal driver. 10 | /// 11 | string Name { get; } 12 | 13 | /// 14 | /// Gets a value indicating whether or not the terminal is in raw mode. 15 | /// 16 | bool IsRawMode { get; } 17 | 18 | /// 19 | /// Occurs when a signal is received. 20 | /// 21 | event EventHandler? Signalled; 22 | 23 | /// 24 | /// Gets the terminal size. 25 | /// 26 | TerminalSize? Size { get; } 27 | 28 | /// 29 | /// Gets a for STDIN. 30 | /// 31 | ITerminalReader Input { get; } 32 | 33 | /// 34 | /// Gets a for STDOUT. 35 | /// 36 | ITerminalWriter Output { get; } 37 | 38 | /// 39 | /// Gets a for STDERR. 40 | /// 41 | ITerminalWriter Error { get; } 42 | 43 | /// 44 | /// Emits a signal. 45 | /// 46 | /// The signal to emit. 47 | /// true if successful, otherwise false. 48 | bool EmitSignal(TerminalSignal signal); 49 | 50 | /// 51 | /// Enables raw mode. 52 | /// 53 | /// true if the operation succeeded, otherwise false. 54 | bool EnableRawMode(); 55 | 56 | /// 57 | /// Disables raw mode. 58 | /// 59 | /// true if the operation succeeded, otherwise false. 60 | bool DisableRawMode(); 61 | } 62 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/ITerminalReader.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals; 2 | 3 | /// 4 | /// Represents a reader. 5 | /// 6 | public interface ITerminalReader 7 | { 8 | /// 9 | /// Gets or sets the encoding. 10 | /// 11 | Encoding Encoding { get; set; } 12 | 13 | /// 14 | /// Gets a value indicating whether a key press is available in the input stream. 15 | /// 16 | bool IsKeyAvailable { get; } 17 | 18 | /// 19 | /// Gets a value indicating whether or not the reader has been redirected. 20 | /// 21 | bool IsRedirected { get; } 22 | 23 | /// 24 | /// Reads the next character from the standard input stream. 25 | /// 26 | /// 27 | /// The next character from the input stream, or negative one (-1) 28 | /// if there are currently no more characters to be read. 29 | /// 30 | int Read(); 31 | 32 | /// 33 | /// Reads the next line of characters from the standard input stream. 34 | /// 35 | /// 36 | /// The next line of characters from the input stream, or null if 37 | /// no more lines are available. 38 | /// 39 | string? ReadLine(); 40 | 41 | /// 42 | /// Obtains the next character or function key pressed by the user. 43 | /// 44 | /// 45 | /// An object that describes the System.ConsoleKey constant and Unicode character, 46 | /// if any, that correspond to the pressed console key. The System.ConsoleKeyInfo 47 | /// object also describes, in a bitwise combination of System.ConsoleModifiers values, 48 | /// whether one or more Shift, Alt, or Ctrl modifier keys was pressed simultaneously 49 | /// with the console key. 50 | /// 51 | ConsoleKeyInfo ReadKey(); 52 | } 53 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/ITerminalWriter.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals; 2 | 3 | /// 4 | /// Represents a writer. 5 | /// 6 | public interface ITerminalWriter 7 | { 8 | /// 9 | /// Gets or sets the encoding. 10 | /// 11 | Encoding Encoding { get; set; } 12 | 13 | /// 14 | /// Gets a value indicating whether or not the writer has been redirected. 15 | /// 16 | bool IsRedirected { get; } 17 | 18 | /// 19 | /// Writes a sequence of bytes to the current writer. 20 | /// 21 | /// A region of memory. This method copies the contents of this region to the writer. 22 | void Write(ReadOnlySpan buffer); 23 | } 24 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/NativeMethods.txt: -------------------------------------------------------------------------------- 1 | SetConsoleCP 2 | GetConsoleCP 3 | GetConsoleOutputCP 4 | SetConsoleOutputCP 5 | GetStdHandle 6 | WriteFile 7 | ReadFile 8 | SetConsoleMode 9 | GetConsoleMode 10 | GetFileType 11 | FlushConsoleInputBuffer 12 | GetConsoleScreenBufferInfo 13 | GetConsoleScreenBufferInfoEx 14 | SetConsoleCursorPosition 15 | FillConsoleOutputCharacter 16 | GetConsoleCursorInfo 17 | SetConsoleCursorInfo 18 | CreateConsoleScreenBuffer 19 | SetConsoleActiveScreenBuffer 20 | CloseHandle 21 | GetLargestConsoleWindowSize 22 | SetConsoleTextAttribute 23 | SetConsoleCtrlHandler 24 | GenerateConsoleCtrlEvent 25 | ReadConsole 26 | ReadConsoleInput 27 | PeekConsoleInput -------------------------------------------------------------------------------- /src/Spectre.Terminals/Polyfill/Nullable.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | #if NETSTANDARD2_0 6 | #pragma warning disable SA1642 // Constructor summary documentation should begin with standard text 7 | #pragma warning disable SA1623 // Property summary documentation should match accessors 8 | namespace System.Diagnostics.CodeAnalysis; 9 | 10 | /// Specifies that null is allowed as an input even if the corresponding type disallows it. 11 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] 12 | #if INTERNAL_NULLABLE_ATTRIBUTES 13 | internal 14 | #else 15 | public 16 | #endif 17 | sealed class AllowNullAttribute : Attribute 18 | { 19 | } 20 | 21 | /// Specifies that null is disallowed as an input even if the corresponding type allows it. 22 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] 23 | #if INTERNAL_NULLABLE_ATTRIBUTES 24 | internal 25 | #else 26 | public 27 | #endif 28 | sealed class DisallowNullAttribute : Attribute 29 | { 30 | } 31 | 32 | /// Specifies that an output may be null even if the corresponding type disallows it. 33 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] 34 | #if INTERNAL_NULLABLE_ATTRIBUTES 35 | internal 36 | #else 37 | public 38 | #endif 39 | sealed class MaybeNullAttribute : Attribute 40 | { 41 | } 42 | 43 | /// Specifies that an output will not be null even if the corresponding type allows it. Specifies that an input argument was not null when the call returns. 44 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] 45 | #if INTERNAL_NULLABLE_ATTRIBUTES 46 | internal 47 | #else 48 | public 49 | #endif 50 | sealed class NotNullAttribute : Attribute 51 | { 52 | } 53 | 54 | /// Specifies that when a method returns , the parameter may be null even if the corresponding type disallows it. 55 | [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] 56 | #if INTERNAL_NULLABLE_ATTRIBUTES 57 | internal 58 | #else 59 | public 60 | #endif 61 | sealed class MaybeNullWhenAttribute : Attribute 62 | { 63 | /// Initializes the attribute with the specified return value condition. 64 | /// 65 | /// The return value condition. If the method returns this value, the associated parameter may be null. 66 | /// 67 | public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; 68 | 69 | /// Gets the return value condition. 70 | public bool ReturnValue { get; } 71 | } 72 | 73 | /// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. 74 | [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] 75 | #if INTERNAL_NULLABLE_ATTRIBUTES 76 | internal 77 | #else 78 | public 79 | #endif 80 | sealed class NotNullWhenAttribute : Attribute 81 | { 82 | /// Initializes the attribute with the specified return value condition. 83 | /// 84 | /// The return value condition. If the method returns this value, the associated parameter will not be null. 85 | /// 86 | public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; 87 | 88 | /// Gets the return value condition. 89 | public bool ReturnValue { get; } 90 | } 91 | 92 | /// Specifies that the output will be non-null if the named parameter is non-null. 93 | [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)] 94 | #if INTERNAL_NULLABLE_ATTRIBUTES 95 | internal 96 | #else 97 | public 98 | #endif 99 | sealed class NotNullIfNotNullAttribute : Attribute 100 | { 101 | /// Initializes the attribute with the associated parameter name. 102 | /// 103 | /// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null. 104 | /// 105 | public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName; 106 | 107 | /// Gets the associated parameter name. 108 | public string ParameterName { get; } 109 | } 110 | 111 | /// Applied to a method that will never return under any circumstance. 112 | [AttributeUsage(AttributeTargets.Method, Inherited = false)] 113 | #if INTERNAL_NULLABLE_ATTRIBUTES 114 | internal 115 | #else 116 | public 117 | #endif 118 | sealed class DoesNotReturnAttribute : Attribute 119 | { 120 | } 121 | 122 | /// Specifies that the method will not return if the associated Boolean parameter is passed the specified value. 123 | [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] 124 | #if INTERNAL_NULLABLE_ATTRIBUTES 125 | internal 126 | #else 127 | public 128 | #endif 129 | sealed class DoesNotReturnIfAttribute : Attribute 130 | { 131 | /// Initializes the attribute with the specified parameter value. 132 | /// 133 | /// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to 134 | /// the associated parameter matches this value. 135 | /// 136 | public DoesNotReturnIfAttribute(bool parameterValue) => ParameterValue = parameterValue; 137 | 138 | /// Gets the condition parameter value. 139 | public bool ParameterValue { get; } 140 | } 141 | 142 | /// Specifies that the method or property will ensure that the listed field and property members have not-null values. 143 | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] 144 | #if INTERNAL_NULLABLE_ATTRIBUTES 145 | internal 146 | #else 147 | public 148 | #endif 149 | sealed class MemberNotNullAttribute : Attribute 150 | { 151 | /// Initializes the attribute with a field or property member. 152 | /// 153 | /// The field or property member that is promised to be not-null. 154 | /// 155 | public MemberNotNullAttribute(string member) => Members = new[] { member }; 156 | 157 | /// Initializes the attribute with the list of field and property members. 158 | /// 159 | /// The list of field and property members that are promised to be not-null. 160 | /// 161 | public MemberNotNullAttribute(params string[] members) => Members = members; 162 | 163 | /// Gets field or property member names. 164 | public string[] Members { get; } 165 | } 166 | 167 | /// Specifies that the method or property will ensure that the listed field and property members have not-null values when returning with the specified return value condition. 168 | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] 169 | #if INTERNAL_NULLABLE_ATTRIBUTES 170 | internal 171 | #else 172 | public 173 | #endif 174 | sealed class MemberNotNullWhenAttribute : Attribute 175 | { 176 | /// Initializes the attribute with the specified return value condition and a field or property member. 177 | /// 178 | /// The return value condition. If the method returns this value, the associated parameter will not be null. 179 | /// 180 | /// 181 | /// The field or property member that is promised to be not-null. 182 | /// 183 | public MemberNotNullWhenAttribute(bool returnValue, string member) 184 | { 185 | ReturnValue = returnValue; 186 | Members = new[] { member }; 187 | } 188 | 189 | /// Initializes the attribute with the specified return value condition and list of field and property members. 190 | /// 191 | /// The return value condition. If the method returns this value, the associated parameter will not be null. 192 | /// 193 | /// 194 | /// The list of field and property members that are promised to be not-null. 195 | /// 196 | public MemberNotNullWhenAttribute(bool returnValue, params string[] members) 197 | { 198 | ReturnValue = returnValue; 199 | Members = members; 200 | } 201 | 202 | /// Gets the return value condition. 203 | public bool ReturnValue { get; } 204 | 205 | /// Gets field or property member names. 206 | public string[] Members { get; } 207 | } 208 | #pragma warning restore SA1623 // Property summary documentation should match accessors 209 | #pragma warning restore SA1642 // Constructor summary documentation should begin with standard text 210 | #endif -------------------------------------------------------------------------------- /src/Spectre.Terminals/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | // Not a fan, but I really, really want to test this. 4 | [assembly: InternalsVisibleTo("Spectre.Terminals.Tests")] 5 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Properties/Usings.cs: -------------------------------------------------------------------------------- 1 | global using System; 2 | global using System.Collections.Generic; 3 | global using System.Diagnostics; 4 | global using System.Diagnostics.CodeAnalysis; 5 | global using System.Globalization; 6 | global using System.IO; 7 | global using System.Linq; 8 | global using System.Runtime.InteropServices; 9 | global using System.Text; 10 | global using System.Threading; 11 | global using System.Threading.Tasks; 12 | global using Microsoft.Windows.Sdk; 13 | global using Mono.Unix; 14 | global using Mono.Unix.Native; 15 | global using Spectre.Terminals.Drivers; 16 | global using Spectre.Terminals.Emulation; 17 | 18 | #if NET6_0_OR_GREATER 19 | global using System.Buffers; 20 | #endif -------------------------------------------------------------------------------- /src/Spectre.Terminals/Spectre.Terminals.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0;netstandard2.0 5 | true 6 | $(NoWarn);NU5104;CS1685 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | all 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | 23 | 24 | 25 | 26 | 27 | $(DefineConstants)INTERNAL_NULLABLE_ATTRIBUTES 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Terminal.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals; 2 | 3 | /// 4 | /// Represents a terminal. 5 | /// 6 | public sealed class Terminal : ITerminal 7 | { 8 | private static readonly Lazy _instance; 9 | 10 | /// 11 | /// Gets a lazily constructed, shared instance. 12 | /// 13 | public static ITerminal Shared => _instance.Value; 14 | 15 | private readonly ITerminalDriver _driver; 16 | private readonly object _lock; 17 | 18 | /// 19 | public string Name => _driver.Name; 20 | 21 | /// 22 | public bool IsRawMode { get; private set; } 23 | 24 | /// 25 | public event EventHandler? Signalled 26 | { 27 | add => _driver.Signalled += value; 28 | remove => _driver.Signalled += value; 29 | } 30 | 31 | /// 32 | public TerminalSize? Size => _driver.Size; 33 | 34 | /// 35 | public TerminalInput Input { get; } 36 | 37 | /// 38 | public TerminalOutput Output { get; } 39 | 40 | /// 41 | public TerminalOutput Error { get; } 42 | 43 | static Terminal() 44 | { 45 | _instance = new Lazy(() => TerminalFactory.Create()); 46 | } 47 | 48 | /// 49 | /// Initializes a new instance of the class. 50 | /// 51 | /// The terminal driver. 52 | public Terminal(ITerminalDriver driver) 53 | { 54 | _driver = driver ?? throw new ArgumentNullException(nameof(driver)); 55 | _lock = new object(); 56 | 57 | Input = new TerminalInput(_driver.Input); 58 | Output = new TerminalOutput(_driver.Output); 59 | Error = new TerminalOutput(_driver.Error); 60 | } 61 | 62 | /// 63 | /// Finalizes an instance of the class. 64 | /// 65 | ~Terminal() 66 | { 67 | Dispose(); 68 | } 69 | 70 | /// 71 | public void Dispose() 72 | { 73 | GC.SuppressFinalize(this); 74 | 75 | DisableRawMode(); 76 | _driver.Dispose(); 77 | } 78 | 79 | /// 80 | public bool EmitSignal(TerminalSignal signal) 81 | { 82 | return _driver.EmitSignal(signal); 83 | } 84 | 85 | /// 86 | public bool EnableRawMode() 87 | { 88 | lock (_lock) 89 | { 90 | if (IsRawMode) 91 | { 92 | return true; 93 | } 94 | 95 | IsRawMode = _driver.EnableRawMode(); 96 | return IsRawMode; 97 | } 98 | } 99 | 100 | /// 101 | public bool DisableRawMode() 102 | { 103 | lock (_lock) 104 | { 105 | if (!IsRawMode) 106 | { 107 | return true; 108 | } 109 | 110 | IsRawMode = !_driver.DisableRawMode(); 111 | return !IsRawMode; 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/TerminalFactory.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals; 2 | 3 | internal static class TerminalFactory 4 | { 5 | public static ITerminal Create() 6 | { 7 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 8 | { 9 | return new Terminal(new WindowsDriver()); 10 | } 11 | else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) 12 | { 13 | return new Terminal(new LinuxDriver()); 14 | } 15 | else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) 16 | { 17 | return new Terminal(new MacOSDriver()); 18 | } 19 | 20 | throw new PlatformNotSupportedException(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/TerminalInput.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals; 2 | 3 | /// 4 | /// Represents a mechanism to read input from the terminal. 5 | /// 6 | public sealed class TerminalInput 7 | { 8 | private readonly ITerminalReader _reader; 9 | private readonly object _lock; 10 | private ITerminalReader? _redirected; 11 | 12 | /// 13 | /// Gets or sets the encoding. 14 | /// 15 | public Encoding Encoding 16 | { 17 | get => GetEncoding(); 18 | set => SetEncoding(value); 19 | } 20 | 21 | /// 22 | /// Gets a value indicating whether or not input has been redirected. 23 | /// 24 | public bool IsRedirected => GetIsRedirected(); 25 | 26 | /// 27 | /// Gets a value indicating whether a key press is available in the input stream. 28 | /// 29 | public bool IsKeyAvailable => throw new NotSupportedException(); 30 | 31 | /// 32 | /// Initializes a new instance of the class. 33 | /// 34 | /// The terminal reader. 35 | public TerminalInput(ITerminalReader reader) 36 | { 37 | _reader = reader ?? throw new ArgumentNullException(nameof(reader)); 38 | _lock = new object(); 39 | } 40 | 41 | /// 42 | /// Redirects input to the specified . 43 | /// 44 | /// 45 | /// The reader to redirect to, 46 | /// or null, if the current redirected reader should be removed. 47 | /// 48 | public void Redirect(ITerminalReader? reader) 49 | { 50 | lock (_lock) 51 | { 52 | _redirected = reader; 53 | } 54 | } 55 | 56 | /// 57 | /// Reads the next character from the standard input stream. 58 | /// 59 | /// 60 | /// The next character from the input stream, or negative one (-1) 61 | /// if there are currently no more characters to be read. 62 | /// 63 | public int Read() 64 | { 65 | return _reader.Read(); 66 | } 67 | 68 | /// 69 | /// Reads the next line of characters from the standard input stream. 70 | /// 71 | /// 72 | /// The next line of characters from the input stream, or null if 73 | /// no more lines are available. 74 | /// 75 | public string? ReadLine() 76 | { 77 | return _reader.ReadLine(); 78 | } 79 | 80 | /// 81 | /// Obtains the next character or function key pressed by the user. 82 | /// 83 | /// 84 | /// An object that describes the System.ConsoleKey constant and Unicode character, 85 | /// if any, that correspond to the pressed console key. The System.ConsoleKeyInfo 86 | /// object also describes, in a bitwise combination of System.ConsoleModifiers values, 87 | /// whether one or more Shift, Alt, or Ctrl modifier keys was pressed simultaneously 88 | /// with the console key. 89 | /// 90 | public ConsoleKeyInfo ReadKey() 91 | { 92 | return _reader.ReadKey(); 93 | } 94 | 95 | private bool GetIsRedirected() 96 | { 97 | lock (_lock) 98 | { 99 | return GetReader().IsRedirected; 100 | } 101 | } 102 | 103 | private Encoding GetEncoding() 104 | { 105 | lock (_lock) 106 | { 107 | return GetReader().Encoding; 108 | } 109 | } 110 | 111 | private void SetEncoding(Encoding encoding) 112 | { 113 | if (encoding is null) 114 | { 115 | throw new ArgumentNullException(nameof(encoding)); 116 | } 117 | 118 | lock (_lock) 119 | { 120 | GetReader().Encoding = encoding; 121 | } 122 | } 123 | 124 | private ITerminalReader GetReader() 125 | { 126 | return _redirected ?? _reader; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/TerminalOutput.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals; 2 | 3 | /// 4 | /// Represents a mechanism to write to a terminal output handle. 5 | /// 6 | public sealed class TerminalOutput 7 | { 8 | private readonly ITerminalWriter _writer; 9 | private readonly object _lock; 10 | private ITerminalWriter? _redirected; 11 | 12 | /// 13 | /// Gets or sets the encoding. 14 | /// 15 | public Encoding Encoding 16 | { 17 | get => GetEncoding(); 18 | set => SetEncoding(value); 19 | } 20 | 21 | /// 22 | /// Gets a value indicating whether or not output has been redirected. 23 | /// 24 | public bool IsRedirected => GetIsRedirected(); 25 | 26 | /// 27 | /// Initializes a new instance of the class. 28 | /// 29 | /// The terminal writer. 30 | public TerminalOutput(ITerminalWriter writer) 31 | { 32 | _writer = writer ?? throw new ArgumentNullException(nameof(writer)); 33 | _lock = new object(); 34 | } 35 | 36 | /// 37 | /// Redirects input to the specified . 38 | /// 39 | /// 40 | /// The writer to redirect to, 41 | /// or null, if the current redirected writer should be removed. 42 | /// 43 | public void Redirect(ITerminalWriter? writer) 44 | { 45 | lock (_lock) 46 | { 47 | _redirected = writer; 48 | } 49 | } 50 | 51 | /// 52 | /// Writes the specified buffer. 53 | /// 54 | /// The value to write. 55 | public void Write(ReadOnlySpan value) 56 | { 57 | lock (_lock) 58 | { 59 | #if NET5_0_OR_GREATER 60 | var len = Encoding.GetByteCount(value); 61 | var array = ArrayPool.Shared.Rent(len); 62 | 63 | try 64 | { 65 | var span = array.AsSpan(0, len); 66 | Encoding.GetBytes(value, span); 67 | GetWriter().Write(span); 68 | } 69 | finally 70 | { 71 | ArrayPool.Shared.Return(array); 72 | } 73 | #else 74 | var chars = value.ToArray(); 75 | var bytes = Encoding.GetBytes(chars); 76 | GetWriter().Write(new Span(bytes)); 77 | #endif 78 | } 79 | } 80 | 81 | private Encoding GetEncoding() 82 | { 83 | lock (_lock) 84 | { 85 | return GetWriter().Encoding; 86 | } 87 | } 88 | 89 | private void SetEncoding(Encoding encoding) 90 | { 91 | if (encoding is null) 92 | { 93 | throw new ArgumentNullException(nameof(encoding)); 94 | } 95 | 96 | lock (_lock) 97 | { 98 | GetWriter().Encoding = encoding; 99 | } 100 | } 101 | 102 | private bool GetIsRedirected() 103 | { 104 | lock (_lock) 105 | { 106 | return GetWriter().IsRedirected; 107 | } 108 | } 109 | 110 | private ITerminalWriter GetWriter() 111 | { 112 | return _redirected ?? _writer; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/TerminalSignal.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals; 2 | 3 | /// 4 | /// Represents a terminal signal. 5 | /// 6 | public enum TerminalSignal 7 | { 8 | /// 9 | /// The SIGINT signal is sent to a process by its controlling terminal 10 | /// when a user wishes to interrupt the process. 11 | /// This is typically initiated by pressing Ctrl+C. 12 | /// 13 | SIGINT, 14 | 15 | /// 16 | /// The SIGQUIT signal is sent to a process by its controlling terminal 17 | /// when the user requests that the process quit. 18 | /// 19 | SIGQUIT, 20 | } 21 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/TerminalSignalEventArgs.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals; 2 | 3 | /// 4 | /// Provides data for the Signalled event. 5 | /// 6 | public sealed class TerminalSignalEventArgs : EventArgs 7 | { 8 | /// 9 | /// Gets the signal. 10 | /// 11 | public TerminalSignal Signal { get; } 12 | 13 | /// 14 | /// Gets or sets a value indicating whether or not 15 | /// the event was handled or not. 16 | /// 17 | public bool Cancel { get; set; } 18 | 19 | /// 20 | /// Initializes a new instance of the class. 21 | /// 22 | /// The signal. 23 | public TerminalSignalEventArgs(TerminalSignal signal) 24 | { 25 | Signal = signal; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/TerminalSize.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals; 2 | 3 | /// 4 | /// Represents terminal size. 5 | /// 6 | [DebuggerDisplay("{Width}x{Height}")] 7 | public readonly struct TerminalSize : IEquatable, IEqualityComparer 8 | { 9 | /// 10 | /// Gets the terminal width in cells. 11 | /// 12 | public int Width { get; } 13 | 14 | /// 15 | /// Gets the terminal height in cells. 16 | /// 17 | public int Height { get; } 18 | 19 | /// 20 | /// Initializes a new instance of the struct. 21 | /// 22 | /// The width. 23 | /// The height. 24 | public TerminalSize(int width, int height) 25 | { 26 | Width = width; 27 | Height = height; 28 | } 29 | 30 | /// 31 | public bool Equals(TerminalSize other) 32 | { 33 | return Width == other.Width 34 | && Height == other.Height; 35 | } 36 | 37 | /// 38 | public bool Equals(TerminalSize x, TerminalSize y) 39 | { 40 | return x.Width == y.Width 41 | && x.Height == y.Height; 42 | } 43 | 44 | /// 45 | public int GetHashCode([DisallowNull] TerminalSize obj) 46 | { 47 | return HashCode.Combine(obj.Width, obj.Height); 48 | } 49 | 50 | /// 51 | public override string ToString() 52 | { 53 | return $"{Width}x{Height}"; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Utilities/EncodingHelper.Unix.cs: -------------------------------------------------------------------------------- 1 | // Parts of this code used from: https://github.com/dotnet/runtime 2 | // Licensed to the .NET Foundation under one or more agreements. 3 | 4 | namespace Spectre.Terminals; 5 | 6 | internal static partial class EncodingHelper 7 | { 8 | /// Environment variables that should be checked, in order, for locale. 9 | /// 10 | /// One of these environment variables should contain a string of a form consistent with 11 | /// the X/Open Portability Guide syntax: 12 | /// language[territory][.charset][@modifier] 13 | /// We're interested in the charset, as it specifies the encoding used 14 | /// for the console. 15 | /// 16 | private static readonly string[] _localeEnvVars = { "LC_ALL", "LC_MESSAGES", "LANG" }; // this ordering codifies the lookup rules prescribed by POSIX 17 | 18 | internal static Encoding RemovePreamble(Encoding encoding) 19 | { 20 | if (encoding.GetPreamble().Length == 0) 21 | { 22 | return encoding; 23 | } 24 | 25 | return new EncodingWithoutPreamble(encoding); 26 | } 27 | 28 | /// Creates an encoding from the current environment. 29 | /// The encoding, or null if it could not be determined. 30 | internal static Encoding? GetEncodingFromCharset() 31 | { 32 | var charset = GetCharset(); 33 | if (charset != null) 34 | { 35 | try 36 | { 37 | var encoding = Encoding.GetEncoding(charset); 38 | if (encoding != null) 39 | { 40 | return RemovePreamble(encoding); 41 | } 42 | } 43 | catch 44 | { 45 | } 46 | } 47 | 48 | return null; 49 | } 50 | 51 | /// Gets the current charset name from the environment. 52 | /// The charset name if found; otherwise, null. 53 | private static string? GetCharset() 54 | { 55 | // Find the first of the locale environment variables that's set. 56 | string? locale = null; 57 | foreach (var envVar in _localeEnvVars) 58 | { 59 | locale = Environment.GetEnvironmentVariable(envVar); 60 | if (!string.IsNullOrWhiteSpace(locale)) 61 | { 62 | break; 63 | } 64 | } 65 | 66 | // If we found one, try to parse it. 67 | // The locale string is expected to be of a form that matches the 68 | // X/Open Portability Guide syntax: language[_territory][.charset][@modifier] 69 | if (locale != null) 70 | { 71 | // Does it contain the optional charset? 72 | var dotPos = locale.IndexOf('.'); 73 | if (dotPos >= 0) 74 | { 75 | dotPos++; 76 | var atPos = locale.IndexOf('@', dotPos + 1); 77 | 78 | // return the charset from the locale, stripping off everything else 79 | var charset = atPos < dotPos ? 80 | locale.Substring(dotPos) : // no modifier 81 | locale.Substring(dotPos, atPos - dotPos); // has modifier 82 | return charset.ToLowerInvariant(); 83 | } 84 | } 85 | 86 | // no charset found; the default will be used 87 | return null; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Utilities/EncodingHelper.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals; 2 | 3 | internal static partial class EncodingHelper 4 | { 5 | static EncodingHelper() 6 | { 7 | Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); 8 | } 9 | 10 | internal static Encoding GetEncodingFromCodePage(int codePage) 11 | { 12 | return Encoding.GetEncoding(codePage) ?? Encoding.UTF8; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Utilities/EncodingWithoutPreamble.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals; 2 | 3 | internal sealed class EncodingWithoutPreamble : Encoding 4 | { 5 | private readonly Encoding _encoding; 6 | 7 | public override string BodyName => _encoding.BodyName; 8 | 9 | public override int CodePage => _encoding.CodePage; 10 | 11 | public override bool IsSingleByte => _encoding.IsSingleByte; 12 | 13 | public override string EncodingName => _encoding.EncodingName; 14 | 15 | public override string WebName 16 | { 17 | get { return _encoding.WebName; } 18 | } 19 | 20 | public EncodingWithoutPreamble(Encoding encoding) 21 | { 22 | _encoding = encoding ?? throw new ArgumentNullException(nameof(encoding)); 23 | } 24 | 25 | public override byte[] GetPreamble() 26 | { 27 | return Array.Empty(); 28 | } 29 | 30 | public override unsafe int GetByteCount(char* chars, int count) 31 | { 32 | return _encoding.GetByteCount(chars, count); 33 | } 34 | 35 | public override int GetByteCount(char[] chars) 36 | { 37 | return _encoding.GetByteCount(chars); 38 | } 39 | 40 | public override int GetByteCount(string s) 41 | { 42 | return _encoding.GetByteCount(s); 43 | } 44 | 45 | public override int GetByteCount(char[] chars, int index, int count) 46 | { 47 | return _encoding.GetByteCount(chars, index, count); 48 | } 49 | 50 | public override unsafe int GetBytes(char* chars, int charCount, byte* bytes, int byteCount) 51 | { 52 | return _encoding.GetBytes(chars, charCount, bytes, byteCount); 53 | } 54 | 55 | public override byte[] GetBytes(char[] chars) 56 | { 57 | return _encoding.GetBytes(chars); 58 | } 59 | 60 | public override byte[] GetBytes(char[] chars, int index, int count) 61 | { 62 | return _encoding.GetBytes(chars, index, count); 63 | } 64 | 65 | public override byte[] GetBytes(string s) 66 | { 67 | return _encoding.GetBytes(s); 68 | } 69 | 70 | public override int GetBytes(string s, int charIndex, int charCount, byte[] bytes, int byteIndex) 71 | { 72 | return _encoding.GetBytes(s, charIndex, charCount, bytes, byteIndex); 73 | } 74 | 75 | public override unsafe int GetCharCount(byte* bytes, int count) 76 | { 77 | return _encoding.GetCharCount(bytes, count); 78 | } 79 | 80 | public override int GetCharCount(byte[] bytes) 81 | { 82 | return _encoding.GetCharCount(bytes); 83 | } 84 | 85 | public override unsafe int GetChars(byte* bytes, int byteCount, char* chars, int charCount) 86 | { 87 | return _encoding.GetChars(bytes, byteCount, chars, charCount); 88 | } 89 | 90 | public override char[] GetChars(byte[] bytes) 91 | { 92 | return _encoding.GetChars(bytes); 93 | } 94 | 95 | public override char[] GetChars(byte[] bytes, int index, int count) 96 | { 97 | return _encoding.GetChars(bytes, index, count); 98 | } 99 | 100 | public override Decoder GetDecoder() 101 | { 102 | return _encoding.GetDecoder(); 103 | } 104 | 105 | public override Encoder GetEncoder() 106 | { 107 | return _encoding.GetEncoder(); 108 | } 109 | 110 | public override string GetString(byte[] bytes) 111 | { 112 | return _encoding.GetString(bytes); 113 | } 114 | 115 | public override bool IsAlwaysNormalized(NormalizationForm form) 116 | { 117 | return _encoding.IsAlwaysNormalized(form); 118 | } 119 | 120 | public override string GetString(byte[] bytes, int index, int count) 121 | { 122 | return _encoding.GetString(bytes, index, count); 123 | } 124 | 125 | public override int GetBytes(char[] chars, int charIndex, int charCount, byte[] bytes, int byteIndex) 126 | { 127 | return _encoding.GetBytes(chars, charIndex, charCount, bytes, byteIndex); 128 | } 129 | 130 | public override int GetCharCount(byte[] bytes, int index, int count) 131 | { 132 | return _encoding.GetCharCount(bytes, index, count); 133 | } 134 | 135 | public override int GetChars(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex) 136 | { 137 | return _encoding.GetChars(bytes, byteIndex, byteCount, chars, charIndex); 138 | } 139 | 140 | public override int GetMaxByteCount(int charCount) 141 | { 142 | return _encoding.GetMaxByteCount(charCount); 143 | } 144 | 145 | public override int GetMaxCharCount(int byteCount) 146 | { 147 | return _encoding.GetMaxByteCount(byteCount); 148 | } 149 | 150 | #if NET5_0_OR_GREATER 151 | public override ReadOnlySpan Preamble => ReadOnlySpan.Empty; 152 | 153 | public override int GetByteCount(ReadOnlySpan chars) 154 | { 155 | return _encoding.GetByteCount(chars); 156 | } 157 | 158 | public override int GetBytes(ReadOnlySpan chars, Span bytes) 159 | { 160 | return _encoding.GetBytes(chars, bytes); 161 | } 162 | 163 | public override int GetCharCount(ReadOnlySpan bytes) 164 | { 165 | return _encoding.GetCharCount(bytes); 166 | } 167 | 168 | public override int GetChars(ReadOnlySpan bytes, Span chars) 169 | { 170 | return _encoding.GetChars(bytes, chars); 171 | } 172 | #endif 173 | } 174 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Utilities/MemoryCursor.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals; 2 | 3 | internal sealed class MemoryCursor 4 | { 5 | private readonly ReadOnlyMemory _buffer; 6 | private int _position; 7 | 8 | public bool CanRead => _position < _buffer.Length; 9 | public int Position => _position; 10 | 11 | public MemoryCursor(ReadOnlyMemory buffer) 12 | { 13 | _buffer = buffer; 14 | _position = 0; 15 | } 16 | 17 | public int Peek() 18 | { 19 | if (_position >= _buffer.Length) 20 | { 21 | return -1; 22 | } 23 | 24 | return _buffer.Span[_position]; 25 | } 26 | 27 | public char PeekChar() 28 | { 29 | return (char)Peek(); 30 | } 31 | 32 | public char ReadChar() 33 | { 34 | return (char)Read(); 35 | } 36 | 37 | public void Discard() 38 | { 39 | Read(); 40 | } 41 | 42 | public void Discard(char expected) 43 | { 44 | var read = ReadChar(); 45 | if (read != expected) 46 | { 47 | throw new InvalidOperationException($"Expected '{expected}' but got '{read}'."); 48 | } 49 | } 50 | 51 | public int Read() 52 | { 53 | var result = Peek(); 54 | if (result != -1) 55 | { 56 | _position++; 57 | } 58 | 59 | return result; 60 | } 61 | 62 | public ReadOnlyMemory Slice(int start, int stop) 63 | { 64 | return _buffer.Slice(start, stop - start); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Spectre.Terminals/Utilities/SynchronizedTextReader.cs: -------------------------------------------------------------------------------- 1 | namespace Spectre.Terminals; 2 | 3 | internal sealed class SynchronizedTextReader : TextReader 4 | { 5 | private readonly TextReader _inner; 6 | private readonly object _lock; 7 | 8 | public SynchronizedTextReader(TextReader reader) 9 | { 10 | _inner = reader ?? throw new ArgumentNullException(nameof(reader)); 11 | _lock = new object(); 12 | } 13 | 14 | protected override void Dispose(bool disposing) 15 | { 16 | if (disposing) 17 | { 18 | _inner.Dispose(); 19 | } 20 | } 21 | 22 | public override int Peek() 23 | { 24 | lock (_lock) 25 | { 26 | return _inner.Peek(); 27 | } 28 | } 29 | 30 | public override int Read() 31 | { 32 | lock (_lock) 33 | { 34 | return _inner.Peek(); 35 | } 36 | } 37 | 38 | public override int Read(char[] buffer, int index, int count) 39 | { 40 | lock (_lock) 41 | { 42 | // TODO 2021-07-31: Validate input 43 | return _inner.Read(buffer, index, count); 44 | } 45 | } 46 | 47 | public override int ReadBlock(char[] buffer, int index, int count) 48 | { 49 | lock (_lock) 50 | { 51 | // TODO 2021-07-31: Validate input 52 | return _inner.ReadBlock(buffer, index, count); 53 | } 54 | } 55 | 56 | public override string? ReadLine() 57 | { 58 | lock (_lock) 59 | { 60 | return _inner.ReadLine(); 61 | } 62 | } 63 | 64 | public override string ReadToEnd() 65 | { 66 | lock (_lock) 67 | { 68 | return _inner.ReadToEnd(); 69 | } 70 | } 71 | 72 | public override Task ReadLineAsync() 73 | { 74 | return Task.FromResult(ReadLine()); 75 | } 76 | 77 | public override Task ReadToEndAsync() 78 | { 79 | return Task.FromResult(ReadToEnd()); 80 | } 81 | 82 | public override Task ReadBlockAsync(char[] buffer, int index, int count) 83 | { 84 | // TODO 2021-07-31: Validate input 85 | return Task.FromResult(ReadBlock(buffer, index, count)); 86 | } 87 | 88 | public override Task ReadAsync(char[] buffer, int index, int count) 89 | { 90 | // TODO 2021-07-31: Validate input 91 | return Task.FromResult(Read(buffer, index, count)); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", 3 | "settings": { 4 | "documentationRules": { 5 | "documentExposedElements": true, 6 | "documentInternalElements": false, 7 | "documentPrivateElements": false, 8 | "documentPrivateFields": false 9 | }, 10 | "layoutRules": { 11 | "newlineAtEndOfFile": "allow", 12 | "allowConsecutiveUsings": true 13 | }, 14 | "orderingRules": { 15 | "usingDirectivesPlacement": "outsideNamespace", 16 | "systemUsingDirectivesFirst": true, 17 | "elementOrder": [ 18 | "kind", 19 | "accessibility", 20 | "constant", 21 | "static", 22 | "readonly" 23 | ] 24 | } 25 | } 26 | } --------------------------------------------------------------------------------