├── .gitignore ├── LICENSE ├── LICENSE.Apachev2 ├── LICENSE.BSD2 ├── LICENSE.BSD3 ├── LICENSE.CC0 ├── LICENSE.MIT ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── linux console.png ├── settings.gradle.kts ├── src └── dorkbox │ └── console │ ├── Console.kt │ ├── input │ ├── Input.kt │ ├── MacOsTerminal.kt │ ├── PosixTerminal.kt │ ├── SupportedTerminal.kt │ ├── Terminal.kt │ ├── UnsupportedTerminal.kt │ ├── WindowsTerminal.kt │ └── package-info.java │ ├── output │ ├── Ansi.kt │ ├── AnsiCodeMap.kt │ ├── AnsiOutputStream.kt │ ├── AnsiRenderWriter.kt │ ├── AnsiRenderer.kt │ ├── AnsiString.kt │ ├── Attribute.kt │ ├── Color.kt │ ├── Erase.kt │ ├── HtmlAnsiOutputStream.kt │ ├── WindowsAnsiOutputStream.kt │ └── package-info.java │ ├── package-info.java │ └── util │ ├── CharHolder.kt │ ├── TerminalDetection.kt │ └── package-info.java ├── src9 ├── dorkbox │ ├── EmptyClass.java │ ├── input │ │ └── EmptyClass.java │ ├── output │ │ └── EmptyClass.java │ └── util │ │ └── EmptyClass.java └── module-info.java ├── test └── dorkbox │ └── console │ ├── AnsiConsoleExample.kt │ ├── AnsiRenderWriterTest.kt │ ├── AnsiRendererTest.kt │ ├── AnsiStringTest.kt │ ├── AnsiTest.kt │ └── HtmlAnsiOutputStreamTest.kt └── windows console.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 2 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 3 | 4 | # User-specific stuff: 5 | .idea/**/workspace.xml 6 | .idea/**/tasks.xml 7 | .idea/dictionaries 8 | .idea/**/codeStyles/ 9 | .idea/**/codeStyleSettings.xml 10 | 11 | # Sensitive or high-churn files: 12 | .idea/**/dataSources/ 13 | .idea/**/dataSources.ids 14 | .idea/**/dataSources.xml 15 | .idea/**/dataSources.local.xml 16 | .idea/**/sqlDataSources.xml 17 | .idea/**/dynamic.xml 18 | .idea/**/uiDesigner.xml 19 | .idea/**/shelf/ 20 | 21 | 22 | # Gradle: 23 | .idea/**/gradle.xml 24 | .idea/**/libraries 25 | 26 | # CMake 27 | cmake-build-debug/ 28 | 29 | # Mongo Explorer plugin: 30 | .idea/**/mongoSettings.xml 31 | 32 | ## File-based project format: 33 | *.iws 34 | 35 | ## Plugin-specific files: 36 | 37 | 38 | # IntelliJ 39 | out/ 40 | 41 | # mpeltonen/sbt-idea plugin 42 | .idea_modules/ 43 | 44 | # JIRA plugin 45 | atlassian-ide-plugin.xml 46 | 47 | # Cursive Clojure plugin 48 | .idea/replstate.xml 49 | 50 | # Crashlytics plugin (for Android Studio and IntelliJ) 51 | com_crashlytics_export_strings.xml 52 | crashlytics.properties 53 | crashlytics-build.properties 54 | fabric.properties 55 | 56 | ###################### 57 | # End JetBrains IDEs # 58 | ###################### 59 | 60 | 61 | # From https://github.com/github/gitignore/blob/master/Gradle.gitignore 62 | .gradle 63 | /build/ 64 | 65 | # Ignore Gradle GUI config 66 | gradle-app.setting 67 | 68 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 69 | !gradle-wrapper.jar 70 | !gradle-wrapper.properties 71 | 72 | # Cache of project 73 | .gradletasknamecache 74 | 75 | 76 | 77 | 78 | # From https://github.com/github/gitignore/blob/master/Java.gitignore 79 | *.class 80 | 81 | # Mobile Tools for Java (J2ME) 82 | .mtj.tmp/ 83 | 84 | 85 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 86 | hs_err_pid* 87 | 88 | *.DS_Store 89 | .AppleDouble 90 | .LSOverride 91 | 92 | # Icon must end with two \r 93 | Icon 94 | 95 | 96 | # Thumbnails 97 | ._* 98 | 99 | # Files that might appear in the root of a volume 100 | .DocumentRevisions-V100 101 | .fseventsd 102 | .Spotlight-V100 103 | .TemporaryItems 104 | .Trashes 105 | .VolumeIcon.icns 106 | .com.apple.timemachine.donotpresent 107 | 108 | # Directories potentially created on remote AFP share 109 | .AppleDB 110 | .AppleDesktop 111 | Network Trash Folder 112 | Temporary Items 113 | .apdisk 114 | 115 | 116 | 117 | ########################################################## 118 | # Specific to this module 119 | 120 | # iml files are generated by intellij/gradle now 121 | **/*.iml 122 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | - Console - Unbuffered input and ANSI output support for Linux, MacOS, or Windows for Java 8+ 2 | [The Apache Software License, Version 2.0] 3 | https://git.dorkbox.com/dorkbox/Console 4 | Copyright 2023 5 | Dorkbox LLC 6 | 7 | Extra license information 8 | - Mordant - 9 | [The Apache Software License, Version 2.0] 10 | https://github.com/ajalt/mordant 11 | Copyright 2018 12 | AJ Alt 13 | 14 | - JAnsi - 15 | [The Apache Software License, Version 2.0] 16 | https://github.com/fusesource/jansi 17 | Copyright 2009 18 | Progress Software Corporation 19 | Joris Kuipers 20 | Jason Dillon 21 | Hiram Chirino 22 | 23 | - SLF4J - Simple facade or abstraction for various logging frameworks 24 | [MIT License] 25 | https://www.slf4j.org 26 | Copyright 2023 27 | QOS.ch 28 | 29 | - JNA - Simplified native library access for Java. 30 | [The Apache Software License, Version 2.0] 31 | https://github.com/twall/jna 32 | Copyright 2023 33 | Timothy Wall 34 | 35 | - JNA-Platform - Mappings for a number of commonly used platform functions 36 | [The Apache Software License, Version 2.0] 37 | https://github.com/twall/jna 38 | Copyright 2023 39 | Timothy Wall 40 | 41 | - Kotlin - 42 | [The Apache Software License, Version 2.0] 43 | https://github.com/JetBrains/kotlin 44 | Copyright 2020 45 | JetBrains s.r.o. and Kotlin Programming Language contributors 46 | Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply 47 | See: https://github.com/JetBrains/kotlin/blob/master/license/README.md 48 | 49 | - ByteUtilities - Byte manipulation and SHA/xxHash utilities 50 | [The Apache Software License, Version 2.0] 51 | https://git.dorkbox.com/dorkbox/ByteUtilities 52 | Copyright 2023 53 | Dorkbox LLC 54 | 55 | Extra license information 56 | - Kryo Serialization - 57 | [BSD 3-Clause License] 58 | https://github.com/EsotericSoftware/kryo 59 | Copyright 2020 60 | Nathan Sweet 61 | 62 | - Base58 - 63 | [The Apache Software License, Version 2.0] 64 | https://bitcoinj.github.io 65 | https://github.com/komputing/KBase58 66 | Copyright 2018 67 | Google Inc 68 | Andreas Schildbach 69 | ligi 70 | 71 | - Kotlin - 72 | [The Apache Software License, Version 2.0] 73 | https://github.com/JetBrains/kotlin 74 | Copyright 2020 75 | JetBrains s.r.o. and Kotlin Programming Language contributors 76 | Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply 77 | See: https://github.com/JetBrains/kotlin/blob/master/license/README.md 78 | 79 | - Netty - An event-driven asynchronous network application framework 80 | [The Apache Software License, Version 2.0] 81 | https://netty.io 82 | Copyright 2023 83 | The Netty Project 84 | Contributors. See source NOTICE 85 | 86 | - Kryo - Fast and efficient binary object graph serialization framework for Java 87 | [BSD 3-Clause License] 88 | https://github.com/EsotericSoftware/kryo 89 | Copyright 2023 90 | Nathan Sweet 91 | 92 | Extra license information 93 | - ReflectASM - 94 | [BSD 3-Clause License] 95 | https://github.com/EsotericSoftware/reflectasm 96 | Nathan Sweet 97 | 98 | - Objenesis - 99 | [The Apache Software License, Version 2.0] 100 | https://github.com/easymock/objenesis 101 | Objenesis Team and all contributors 102 | 103 | - MinLog-SLF4J - 104 | [BSD 3-Clause License] 105 | https://github.com/EsotericSoftware/minlog 106 | Nathan Sweet 107 | 108 | - LZ4 and xxHash - LZ4 compression for Java, based on Yann Collet's work 109 | [The Apache Software License, Version 2.0] 110 | https://github.com/lz4/lz4 111 | Copyright 2023 112 | Yann Collet 113 | Adrien Grand 114 | 115 | - XZ for Java - Complete implementation of XZ data compression in pure Java 116 | [Public Domain, per Creative Commons CC0] 117 | https://tukaani.org/xz/java.html 118 | Copyright 2023 119 | Lasse Collin 120 | Igor Pavlov 121 | 122 | - Updates - Software Update Management 123 | [The Apache Software License, Version 2.0] 124 | https://git.dorkbox.com/dorkbox/Updates 125 | Copyright 2021 126 | Dorkbox LLC 127 | 128 | Extra license information 129 | - Kotlin - 130 | [The Apache Software License, Version 2.0] 131 | https://github.com/JetBrains/kotlin 132 | Copyright 2020 133 | JetBrains s.r.o. and Kotlin Programming Language contributors 134 | Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply 135 | See: https://github.com/JetBrains/kotlin/blob/master/license/README.md 136 | 137 | - PropertyLoader - Property annotation and loader for fields 138 | [The Apache Software License, Version 2.0] 139 | https://git.dorkbox.com/dorkbox/PropertyLoader 140 | Copyright 2023 141 | Dorkbox LLC 142 | 143 | - Updates - Software Update Management 144 | [The Apache Software License, Version 2.0] 145 | https://git.dorkbox.com/dorkbox/Updates 146 | Copyright 2021 147 | Dorkbox LLC 148 | 149 | Extra license information 150 | - Kotlin - 151 | [The Apache Software License, Version 2.0] 152 | https://github.com/JetBrains/kotlin 153 | Copyright 2020 154 | JetBrains s.r.o. and Kotlin Programming Language contributors 155 | Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply 156 | See: https://github.com/JetBrains/kotlin/blob/master/license/README.md 157 | 158 | - JNA - Native JNA extensions for Linux, MacOS, and Windows, Java 1.8+ 159 | [The Apache Software License, Version 2.0] 160 | https://git.dorkbox.com/dorkbox/JNA 161 | Copyright 2023 162 | Dorkbox LLC 163 | 164 | Extra license information 165 | - Kotlin - 166 | [The Apache Software License, Version 2.0] 167 | https://github.com/JetBrains/kotlin 168 | Copyright 2020 169 | JetBrains s.r.o. and Kotlin Programming Language contributors 170 | Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply 171 | See: https://github.com/JetBrains/kotlin/blob/master/license/README.md 172 | 173 | - JNA - Simplified native library access for Java. 174 | [The Apache Software License, Version 2.0] 175 | https://github.com/twall/jna 176 | Copyright 2023 177 | Timothy Wall 178 | 179 | - JNA-Platform - Mappings for a number of commonly used platform functions 180 | [The Apache Software License, Version 2.0] 181 | https://github.com/twall/jna 182 | Copyright 2023 183 | Timothy Wall 184 | 185 | - SLF4J - Simple facade or abstraction for various logging frameworks 186 | [MIT License] 187 | https://www.slf4j.org 188 | Copyright 2023 189 | QOS.ch 190 | 191 | - OS - Information about the system, Java runtime, OS, Window Manager, and Desktop Environment. 192 | [The Apache Software License, Version 2.0] 193 | https://git.dorkbox.com/dorkbox/OS 194 | Copyright 2023 195 | Dorkbox LLC 196 | 197 | Extra license information 198 | - Kotlin - 199 | [The Apache Software License, Version 2.0] 200 | https://github.com/JetBrains/kotlin 201 | Copyright 2020 202 | JetBrains s.r.o. and Kotlin Programming Language contributors 203 | Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply 204 | See: https://github.com/JetBrains/kotlin/blob/master/license/README.md 205 | 206 | - Updates - Software Update Management 207 | [The Apache Software License, Version 2.0] 208 | https://git.dorkbox.com/dorkbox/Updates 209 | Copyright 2021 210 | Dorkbox LLC 211 | 212 | Extra license information 213 | - Kotlin - 214 | [The Apache Software License, Version 2.0] 215 | https://github.com/JetBrains/kotlin 216 | Copyright 2020 217 | JetBrains s.r.o. and Kotlin Programming Language contributors 218 | Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply 219 | See: https://github.com/JetBrains/kotlin/blob/master/license/README.md 220 | 221 | - Updates - Software Update Management 222 | [The Apache Software License, Version 2.0] 223 | https://git.dorkbox.com/dorkbox/Updates 224 | Copyright 2021 225 | Dorkbox LLC 226 | 227 | Extra license information 228 | - Kotlin - 229 | [The Apache Software License, Version 2.0] 230 | https://github.com/JetBrains/kotlin 231 | Copyright 2020 232 | JetBrains s.r.o. and Kotlin Programming Language contributors 233 | Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply 234 | See: https://github.com/JetBrains/kotlin/blob/master/license/README.md 235 | 236 | - OS - Information about the system, Java runtime, OS, Window Manager, and Desktop Environment. 237 | [The Apache Software License, Version 2.0] 238 | https://git.dorkbox.com/dorkbox/OS 239 | Copyright 2023 240 | Dorkbox LLC 241 | 242 | Extra license information 243 | - Kotlin - 244 | [The Apache Software License, Version 2.0] 245 | https://github.com/JetBrains/kotlin 246 | Copyright 2020 247 | JetBrains s.r.o. and Kotlin Programming Language contributors 248 | Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply 249 | See: https://github.com/JetBrains/kotlin/blob/master/license/README.md 250 | 251 | - Updates - Software Update Management 252 | [The Apache Software License, Version 2.0] 253 | https://git.dorkbox.com/dorkbox/Updates 254 | Copyright 2021 255 | Dorkbox LLC 256 | 257 | Extra license information 258 | - Kotlin - 259 | [The Apache Software License, Version 2.0] 260 | https://github.com/JetBrains/kotlin 261 | Copyright 2020 262 | JetBrains s.r.o. and Kotlin Programming Language contributors 263 | Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply 264 | See: https://github.com/JetBrains/kotlin/blob/master/license/README.md 265 | 266 | - Utilities - Utilities for use within Java projects 267 | [The Apache Software License, Version 2.0] 268 | https://git.dorkbox.com/dorkbox/Utilities 269 | Copyright 2023 270 | Dorkbox LLC 271 | 272 | Extra license information 273 | - MersenneTwisterFast - 274 | [BSD 3-Clause License] 275 | https://git.dorkbox.com/dorkbox/Utilities 276 | Copyright 2003 277 | Sean Luke 278 | Michael Lecuyer (portions Copyright 1993 279 | 280 | - FastThreadLocal - 281 | [BSD 3-Clause License] 282 | https://git.dorkbox.com/dorkbox/Utilities 283 | https://github.com/LWJGL/lwjgl3/blob/5819c9123222f6ce51f208e022cb907091dd8023/modules/core/src/main/java/org/lwjgl/system/FastThreadLocal.java 284 | https://github.com/riven8192/LibStruct/blob/master/src/net/indiespot/struct/runtime/FastThreadLocal.java 285 | Copyright 2014 286 | Lightweight Java Game Library Project 287 | Riven 288 | 289 | - Retrofit - A type-safe HTTP client for Android and Java 290 | [The Apache Software License, Version 2.0] 291 | https://github.com/square/retrofit 292 | Copyright 2020 293 | Square, Inc 294 | 295 | - Resource Listing - Listing the contents of a resource directory 296 | [The Apache Software License, Version 2.0] 297 | https://www.uofr.net/~greg/java/get-resource-listing.html 298 | Copyright 2017 299 | Greg Briggs 300 | 301 | - CommonUtils - Common utility extension functions for kotlin 302 | [The Apache Software License, Version 2.0] 303 | https://www.pronghorn.tech 304 | Copyright 2017 305 | Pronghorn Technology LLC 306 | Dorkbox LLC 307 | 308 | - Kotlin Coroutine CountDownLatch - 309 | [The Apache Software License, Version 2.0] 310 | https://github.com/Kotlin/kotlinx.coroutines/issues/59 311 | https://github.com/venkatperi/kotlin-coroutines-lib 312 | Copyright 2018 313 | Venkat Peri 314 | 315 | - kotlinx.coroutines - Library support for Kotlin coroutines with multiplatform support 316 | [The Apache Software License, Version 2.0] 317 | https://github.com/Kotlin/kotlinx.coroutines 318 | Copyright 2023 319 | JetBrains s.r.o. 320 | 321 | - Java Uuid Generator - A set of Java classes for working with UUIDs 322 | [The Apache Software License, Version 2.0] 323 | https://github.com/cowtowncoder/java-uuid-generator 324 | Copyright 2023 325 | Tatu Saloranta (tatu.saloranta@iki.fi) 326 | Contributors. See source release-notes/CREDITS 327 | 328 | - Kotlin - 329 | [The Apache Software License, Version 2.0] 330 | https://github.com/JetBrains/kotlin 331 | Copyright 2020 332 | JetBrains s.r.o. and Kotlin Programming Language contributors 333 | Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply 334 | See: https://github.com/JetBrains/kotlin/blob/master/license/README.md 335 | 336 | - OS - Information about the system, Java runtime, OS, Window Manager, and Desktop Environment. 337 | [The Apache Software License, Version 2.0] 338 | https://git.dorkbox.com/dorkbox/OS 339 | Copyright 2023 340 | Dorkbox LLC 341 | 342 | Extra license information 343 | - Kotlin - 344 | [The Apache Software License, Version 2.0] 345 | https://github.com/JetBrains/kotlin 346 | Copyright 2020 347 | JetBrains s.r.o. and Kotlin Programming Language contributors 348 | Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply 349 | See: https://github.com/JetBrains/kotlin/blob/master/license/README.md 350 | 351 | - Updates - Software Update Management 352 | [The Apache Software License, Version 2.0] 353 | https://git.dorkbox.com/dorkbox/Updates 354 | Copyright 2021 355 | Dorkbox LLC 356 | 357 | Extra license information 358 | - Kotlin - 359 | [The Apache Software License, Version 2.0] 360 | https://github.com/JetBrains/kotlin 361 | Copyright 2020 362 | JetBrains s.r.o. and Kotlin Programming Language contributors 363 | Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply 364 | See: https://github.com/JetBrains/kotlin/blob/master/license/README.md 365 | 366 | - Updates - Software Update Management 367 | [The Apache Software License, Version 2.0] 368 | https://git.dorkbox.com/dorkbox/Updates 369 | Copyright 2021 370 | Dorkbox LLC 371 | 372 | Extra license information 373 | - Kotlin - 374 | [The Apache Software License, Version 2.0] 375 | https://github.com/JetBrains/kotlin 376 | Copyright 2020 377 | JetBrains s.r.o. and Kotlin Programming Language contributors 378 | Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply 379 | See: https://github.com/JetBrains/kotlin/blob/master/license/README.md 380 | -------------------------------------------------------------------------------- /LICENSE.Apachev2: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a 16 | copy of this software and associated documentation files (the "Software"), 17 | to deal in the Software without restriction, including without limitation 18 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 19 | and/or sell copies of the Software, and to permit persons to whom the 20 | Software is furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included 23 | in all copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 26 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 28 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 30 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 31 | DEALINGS IN THE SOFTWARE. 32 | "Legal Entity" shall mean the union of the acting entity and all 33 | other entities that control, are controlled by, or are under common 34 | control with that entity. For the purposes of this definition, 35 | "control" means (i) the power, direct or indirect, to cause the 36 | direction or management of such entity, whether by contract or 37 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 38 | outstanding shares, or (iii) beneficial ownership of such entity. 39 | 40 | "You" (or "Your") shall mean an individual or Legal Entity 41 | exercising permissions granted by this License. 42 | 43 | "Source" form shall mean the preferred form for making modifications, 44 | including but not limited to software source code, documentation 45 | source, and configuration files. 46 | 47 | "Object" form shall mean any form resulting from mechanical 48 | transformation or translation of a Source form, including but 49 | not limited to compiled object code, generated documentation, 50 | and conversions to other media types. 51 | 52 | "Work" shall mean the work of authorship, whether in Source or 53 | Object form, made available under the License, as indicated by a 54 | copyright notice that is included in or attached to the work 55 | (an example is provided in the Appendix below). 56 | 57 | "Derivative Works" shall mean any work, whether in Source or Object 58 | form, that is based on (or derived from) the Work and for which the 59 | editorial revisions, annotations, elaborations, or other modifications 60 | represent, as a whole, an original work of authorship. For the purposes 61 | of this License, Derivative Works shall not include works that remain 62 | separable from, or merely link (or bind by name) to the interfaces of, 63 | the Work and Derivative Works thereof. 64 | 65 | "Contribution" shall mean any work of authorship, including 66 | the original version of the Work and any modifications or additions 67 | to that Work or Derivative Works thereof, that is intentionally 68 | submitted to Licensor for inclusion in the Work by the copyright owner 69 | or by an individual or Legal Entity authorized to submit on behalf of 70 | the copyright owner. For the purposes of this definition, "submitted" 71 | means any form of electronic, verbal, or written communication sent 72 | to the Licensor or its representatives, including but not limited to 73 | communication on electronic mailing lists, source code control systems, 74 | and issue tracking systems that are managed by, or on behalf of, the 75 | Licensor for the purpose of discussing and improving the Work, but 76 | excluding communication that is conspicuously marked or otherwise 77 | designated in writing by the copyright owner as "Not a Contribution." 78 | 79 | "Contributor" shall mean Licensor and any individual or Legal Entity 80 | on behalf of whom a Contribution has been received by Licensor and 81 | subsequently incorporated within the Work. 82 | 83 | 2. Grant of Copyright License. Subject to the terms and conditions of 84 | this License, each Contributor hereby grants to You a perpetual, 85 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 86 | copyright license to reproduce, prepare Derivative Works of, 87 | publicly display, publicly perform, sublicense, and distribute the 88 | Work and such Derivative Works in Source or Object form. 89 | 90 | 3. Grant of Patent License. Subject to the terms and conditions of 91 | this License, each Contributor hereby grants to You a perpetual, 92 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 93 | (except as stated in this section) patent license to make, have made, 94 | use, offer to sell, sell, import, and otherwise transfer the Work, 95 | where such license applies only to those patent claims licensable 96 | by such Contributor that are necessarily infringed by their 97 | Contribution(s) alone or by combination of their Contribution(s) 98 | with the Work to which such Contribution(s) was submitted. If You 99 | institute patent litigation against any entity (including a 100 | cross-claim or counterclaim in a lawsuit) alleging that the Work 101 | or a Contribution incorporated within the Work constitutes direct 102 | or contributory patent infringement, then any patent licenses 103 | granted to You under this License for that Work shall terminate 104 | as of the date such litigation is filed. 105 | 106 | 4. Redistribution. You may reproduce and distribute copies of the 107 | Work or Derivative Works thereof in any medium, with or without 108 | modifications, and in Source or Object form, provided that You 109 | meet the following conditions: 110 | 111 | (a) You must give any other recipients of the Work or 112 | Derivative Works a copy of this License; and 113 | 114 | (b) You must cause any modified files to carry prominent notices 115 | stating that You changed the files; and 116 | 117 | (c) You must retain, in the Source form of any Derivative Works 118 | that You distribute, all copyright, patent, trademark, and 119 | attribution notices from the Source form of the Work, 120 | excluding those notices that do not pertain to any part of 121 | the Derivative Works; and 122 | 123 | (d) If the Work includes a "NOTICE" text file as part of its 124 | distribution, then any Derivative Works that You distribute must 125 | include a readable copy of the attribution notices contained 126 | within such NOTICE file, excluding those notices that do not 127 | pertain to any part of the Derivative Works, in at least one 128 | of the following places: within a NOTICE text file distributed 129 | as part of the Derivative Works; within the Source form or 130 | documentation, if provided along with the Derivative Works; or, 131 | within a display generated by the Derivative Works, if and 132 | wherever such third-party notices normally appear. The contents 133 | of the NOTICE file are for informational purposes only and 134 | do not modify the License. You may add Your own attribution 135 | notices within Derivative Works that You distribute, alongside 136 | or as an addendum to the NOTICE text from the Work, provided 137 | that such additional attribution notices cannot be construed 138 | as modifying the License. 139 | 140 | You may add Your own copyright statement to Your modifications and 141 | may provide additional or different license terms and conditions 142 | for use, reproduction, or distribution of Your modifications, or 143 | for any such Derivative Works as a whole, provided Your use, 144 | reproduction, and distribution of the Work otherwise complies with 145 | the conditions stated in this License. 146 | 147 | 5. Submission of Contributions. Unless You explicitly state otherwise, 148 | any Contribution intentionally submitted for inclusion in the Work 149 | by You to the Licensor shall be under the terms and conditions of 150 | this License, without any additional terms or conditions. 151 | Notwithstanding the above, nothing herein shall supersede or modify 152 | the terms of any separate license agreement you may have executed 153 | with Licensor regarding such Contributions. 154 | 155 | 6. Trademarks. This License does not grant permission to use the trade 156 | names, trademarks, service marks, or product names of the Licensor, 157 | except as required for reasonable and customary use in describing the 158 | origin of the Work and reproducing the content of the NOTICE file. 159 | 160 | 7. Disclaimer of Warranty. Unless required by applicable law or 161 | agreed to in writing, Licensor provides the Work (and each 162 | Contributor provides its Contributions) on an "AS IS" BASIS, 163 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 164 | implied, including, without limitation, any warranties or conditions 165 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 166 | PARTICULAR PURPOSE. You are solely responsible for determining the 167 | appropriateness of using or redistributing the Work and assume any 168 | risks associated with Your exercise of permissions under this License. 169 | 170 | 8. Limitation of Liability. In no event and under no legal theory, 171 | whether in tort (including negligence), contract, or otherwise, 172 | unless required by applicable law (such as deliberate and grossly 173 | negligent acts) or agreed to in writing, shall any Contributor be 174 | liable to You for damages, including any direct, indirect, special, 175 | incidental, or consequential damages of any character arising as a 176 | result of this License or out of the use or inability to use the 177 | Work (including but not limited to damages for loss of goodwill, 178 | work stoppage, computer failure or malfunction, or any and all 179 | other commercial damages or losses), even if such Contributor 180 | has been advised of the possibility of such damages. 181 | 182 | 9. Accepting Warranty or Additional Liability. While redistributing 183 | the Work or Derivative Works thereof, You may choose to offer, 184 | and charge a fee for, acceptance of support, warranty, indemnity, 185 | or other liability obligations and/or rights consistent with this 186 | License. However, in accepting such obligations, You may act only 187 | on Your own behalf and on Your sole responsibility, not on behalf 188 | of any other Contributor, and only if You agree to indemnify, 189 | defend, and hold each Contributor harmless for any liability 190 | incurred by, or claims asserted against, such Contributor by reason 191 | of your accepting any such warranty or additional liability. 192 | 193 | END OF TERMS AND CONDITIONS 194 | 195 | APPENDIX: How to apply the Apache License to your work. 196 | 197 | To apply the Apache License to your work, attach the following 198 | boilerplate notice, with the fields enclosed by brackets "[]" 199 | replaced with your own identifying information. (Don't include 200 | the brackets!) The text should be enclosed in the appropriate 201 | comment syntax for the file format. We also recommend that a 202 | file or class name and description of purpose be included on the 203 | same "printed page" as the copyright notice for easier 204 | identification within third-party archives. 205 | 206 | Copyright [yyyy] [name of copyright owner] 207 | 208 | Licensed under the Apache License, Version 2.0 (the "License"); 209 | you may not use this file except in compliance with the License. 210 | You may obtain a copy of the License at 211 | 212 | http://www.apache.org/licenses/LICENSE-2.0 213 | 214 | Unless required by applicable law or agreed to in writing, software 215 | distributed under the License is distributed on an "AS IS" BASIS, 216 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 217 | See the License for the specific language governing permissions and 218 | limitations under the License. -------------------------------------------------------------------------------- /LICENSE.BSD2: -------------------------------------------------------------------------------- 1 | BSD License 2 | 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 17 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /LICENSE.BSD3: -------------------------------------------------------------------------------- 1 | BSD License 2 | 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | 13 | * Neither the name of the nor the names of its contributors 14 | may be used to endorse or promote products derived from this software 15 | without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 21 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 24 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /LICENSE.CC0: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. -------------------------------------------------------------------------------- /LICENSE.MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Console 2 | ======= 3 | 4 | ###### [![Dorkbox](https://badge.dorkbox.com/dorkbox.svg "Dorkbox")](https://git.dorkbox.com/dorkbox/Console) [![Github](https://badge.dorkbox.com/github.svg "Github")](https://github.com/dorkbox/Console) [![Gitlab](https://badge.dorkbox.com/gitlab.svg "Gitlab")](https://gitlab.com/dorkbox/Console) 5 | 6 | 7 | 8 | Unbuffered input and ANSI output support for linux, mac, windows. Java 8+ 9 | 10 | This library is the evolution of what [JLine](https://github.com/jline/jline2) should be, and the optimization of [JAnsi](https://github.com/fusesource/jansi). While it is very similar in functionality to what these libraries provide, there are several things that are significantly different. 11 | 12 | 1. JNA *direct-mapping* instead of custom JNI/shell execution which is [slightly slower than JNI](https://github.com/java-native-access/jna/blob/master/www/DirectMapping.md) but significantly easier to read, modify, debug, and provide support for non-intel architectures. 13 | 1. Complete implementation of common [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code) 14 | 1. Automatically hooks into `System.err/out` for seamless integration in Java environments 15 | 1. Automatically detects when an `IDE` is in use 16 | - Prevents issues with console corruption 17 | - Provides simulated single character input via `in.read()`, which still requires the enter key to flush the buffer, but feeds single characters at a time 18 | 1. Backspace functionality for line input is preserved (if ANSI is enabled, the console is updated as well). 19 | 1. Controls `ECHO` on/off in the console 20 | 1. Controls `Ctrl-C` (SIGINT) on/off in the console 21 | 1. Multi-threaded, intelligent buffering of command input for simultaneous input readers on different threads 22 | 1. Solves un-interruptable blocking reads from System.in when in an "unsupported" terminal (ie: anything other than a *nix/windows shell) so one can successfully stop reading from the input stream, 23 | 24 | 25 | - This is for cross-platform use, specifically - linux arm/32/64, mac 64, and windows 32/64. Java 8+ 26 | 27 | Windows 28 | ![Windows](https://git.dorkbox.com/dorkbox/Console/raw/branch/master/windows%20console.png) 29 | 30 | Linux/Mac 31 | ![*nix](https://git.dorkbox.com/dorkbox/Console/raw/branch/master/linux%20console.png) 32 | 33 | 34 | ``` 35 | Customization parameters: 36 | Console.ENABLE_ANSI (type boolean, default value 'true') 37 | - If true, allows an ANSI output stream to be created on System.out/err, otherwise it will provide an ANSI aware PrintStream which strips out the ANSI escape sequences. 38 | 39 | 40 | Console.FORCE_ENABLE_ANSI (type boolean, default value 'false') 41 | - If true, then we always force the raw ANSI output stream to be enabled (even if the output stream is not aware of ANSI commands). 42 | This can be used to obtain the raw ANSI escape codes for other color aware programs (ie: less -r) 43 | 44 | 45 | Console.ENABLE_ECHO (type boolean, default value 'true') 46 | - Enables or disables character echo to stdout in the console, should call Console.setEchoEnabled(boolean) after initialization. 47 | 48 | 49 | Console.ENABLE_INTERRUPT (type boolean, default value 'false') 50 | - Enables or disables CTRL-C behavior in the console, should call Console.setInterruptEnabled(boolean) after initialization. 51 | 52 | 53 | Console.ENABLE_BACKSPACE (type boolean, default value 'true') 54 | - Enables the backspace key to delete characters in the line buffer and (if ANSI is enabled) from the screen. 55 | 56 | 57 | Console.INPUT_CONSOLE_TYPE (type String, default value 'AUTO') 58 | - Used to determine what console to use/hook when AUTO is not correctly working. 59 | Valid options are: 60 | AUTO - automatically determine which OS/console type to use 61 | MACOS - try to control a MACOS console 62 | WINDOWS - try to control a WINDOWS console 63 | UNIX - try to control a UNIX console 64 | NONE - do not try to control anything, only line input is supported 65 | 66 | 67 | Console.AUTO_FLUSH (type boolean, default value 'true') 68 | - Enables the output printstream to automatically flush after every write. NOTE: This is DANGEROUS, as it removes the usefulness of 69 | * the backing BufferWriter! 70 | 71 | 72 | Ansi.restoreSystemStreams() 73 | - Restores System.err/out PrintStreams to their ORIGINAL configuration. Useful when using ANSI functionality but do not want to hook into the system. 74 | ``` 75 | 76 | 77 | 78 | ``` 79 | Note: This project was inspired by the excellent JLine and JAnsi libraries. Many thanks to their hard work. 80 | ``` 81 | 82 |   83 |   84 | 85 | Maven Info 86 | --------- 87 | ``` 88 | 89 | ... 90 | 91 | com.dorkbox 92 | Console 93 | 4.1 94 | 95 | 96 | ``` 97 | 98 | Gradle Info 99 | --------- 100 | ``` 101 | dependencies { 102 | ... 103 | implementation("com.dorkbox:Console:4.1") 104 | } 105 | ``` 106 | 107 | License 108 | --------- 109 | This project is © 2021 dorkbox llc, and is distributed under the terms of the Apache v2.0 License. See file "LICENSE" for further 110 | references. 111 | 112 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 dorkbox, llc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 18 | /////////////////////////////// 19 | ////// PUBLISH TO SONATYPE / MAVEN CENTRAL 20 | ////// TESTING : (to local maven repo) <'publish and release' - 'publishToMavenLocal'> 21 | ////// RELEASE : (to sonatype/maven central), <'publish and release' - 'publishToSonatypeAndRelease'> 22 | /////////////////////////////// 23 | 24 | gradle.startParameter.showStacktrace = ShowStacktrace.ALWAYS // always show the stacktrace! 25 | 26 | plugins { 27 | java 28 | 29 | id("com.dorkbox.GradleUtils") version "3.18" 30 | id("com.dorkbox.Licensing") version "2.28" 31 | id("com.dorkbox.VersionUpdate") version "2.8" 32 | id("com.dorkbox.GradlePublish") version "1.20" 33 | 34 | id("com.github.johnrengelman.shadow") version "8.1.1" 35 | 36 | kotlin("jvm") version "1.9.0" 37 | } 38 | 39 | object Extras { 40 | // set for the project 41 | const val description = "Unbuffered input and ANSI output support for Linux, MacOS, or Windows for Java 8+" 42 | const val group = "com.dorkbox" 43 | const val version = "4.1" 44 | 45 | // set as project.ext 46 | const val name = "Console" 47 | const val id = "Console" 48 | const val vendor = "Dorkbox LLC" 49 | const val vendorUrl = "https://dorkbox.com" 50 | const val url = "https://git.dorkbox.com/dorkbox/Console" 51 | } 52 | 53 | /////////////////////////////// 54 | ///// assign 'Extras' 55 | /////////////////////////////// 56 | GradleUtils.load("$projectDir/../../gradle.properties", Extras) 57 | GradleUtils.defaults() 58 | GradleUtils.compileConfiguration(JavaVersion.VERSION_1_8) 59 | GradleUtils.jpms(JavaVersion.VERSION_1_9) 60 | 61 | licensing { 62 | license(License.APACHE_2) { 63 | description(Extras.description) 64 | url(Extras.url) 65 | author(Extras.vendor) 66 | 67 | extra("Mordant", License.APACHE_2) { 68 | copyright(2018) 69 | author("AJ Alt") 70 | url("https://github.com/ajalt/mordant") 71 | } 72 | 73 | extra("JAnsi", License.APACHE_2) { 74 | copyright(2009) 75 | author("Progress Software Corporation") 76 | author("Joris Kuipers") 77 | author("Jason Dillon") 78 | author("Hiram Chirino") 79 | url("https://github.com/fusesource/jansi") 80 | } 81 | } 82 | } 83 | 84 | tasks.jar.get().apply { 85 | manifest { 86 | // https://docs.oracle.com/javase/tutorial/deployment/jar/packageman.html 87 | attributes["Name"] = Extras.name 88 | 89 | attributes["Specification-Title"] = Extras.name 90 | attributes["Specification-Version"] = Extras.version 91 | attributes["Specification-Vendor"] = Extras.vendor 92 | 93 | attributes["Implementation-Title"] = "${Extras.group}.${Extras.id}" 94 | attributes["Implementation-Version"] = GradleUtils.now() 95 | attributes["Implementation-Vendor"] = Extras.vendor 96 | 97 | attributes["Automatic-Module-Name"] = Extras.id 98 | } 99 | } 100 | 101 | val shadowJar: com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar by tasks 102 | shadowJar.apply { 103 | manifest.inheritFrom(tasks.jar.get().manifest) 104 | 105 | manifest.attributes.apply { 106 | put("Main-Class", "dorkbox.console.AnsiConsoleExample") 107 | } 108 | 109 | mergeServiceFiles() 110 | 111 | duplicatesStrategy = DuplicatesStrategy.INCLUDE 112 | 113 | from(sourceSets.test.get().output) 114 | configurations = listOf(project.configurations.testRuntimeClasspath.get()) 115 | 116 | archiveBaseName.set(project.name + "-all") 117 | } 118 | 119 | 120 | dependencies { 121 | api("com.dorkbox:ByteUtilities:2.0") 122 | api("com.dorkbox:PropertyLoader:1.4") 123 | api("com.dorkbox:Updates:1.1") 124 | api("com.dorkbox:JNA:1.2") 125 | api("com.dorkbox:OS:1.8") 126 | api("com.dorkbox:Utilities:1.46") 127 | 128 | api("org.slf4j:slf4j-api:2.0.7") 129 | 130 | val jnaVersion = "5.13.0" 131 | api("net.java.dev.jna:jna:$jnaVersion") 132 | api("net.java.dev.jna:jna-platform:$jnaVersion") 133 | 134 | testImplementation("junit:junit:4.13.2") 135 | testImplementation("ch.qos.logback:logback-classic:1.2.9") // can run on java 1.8 136 | } 137 | 138 | 139 | publishToSonatype { 140 | groupId = Extras.group 141 | artifactId = Extras.id 142 | version = Extras.version 143 | 144 | name = Extras.name 145 | description = Extras.description 146 | url = Extras.url 147 | 148 | vendor = Extras.vendor 149 | vendorUrl = Extras.vendorUrl 150 | 151 | issueManagement { 152 | url = "${Extras.url}/issues" 153 | nickname = "Gitea Issues" 154 | } 155 | 156 | developer { 157 | id = "dorkbox" 158 | name = Extras.vendor 159 | email = "email@dorkbox.com" 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties 2 | org.gradle.jvmargs=-Dfile.encoding=UTF-8 3 | 4 | #org.gradle.warning.mode=(all,fail,none,summary) 5 | org.gradle.warning.mode=all 6 | 7 | #org.gradle.daemon=false 8 | # default is 3 hours, this is 1 minute 9 | org.gradle.daemon.idletimeout=60000 10 | 11 | #org.gradle.console=(auto,plain,rich,verbose) 12 | org.gradle.console=auto 13 | 14 | #org.gradle.logging.level=(quiet,warn,lifecycle,info,debug) 15 | org.gradle.logging.level=lifecycle 16 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorkbox/Console/30b800de83d7cb06ecf33b0829120aea2c848e08/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 147 | # shellcheck disable=SC3045 148 | MAX_FD=$( ulimit -H -n ) || 149 | warn "Could not query maximum file descriptor limit" 150 | esac 151 | case $MAX_FD in #( 152 | '' | soft) :;; #( 153 | *) 154 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 155 | # shellcheck disable=SC3045 156 | ulimit -n "$MAX_FD" || 157 | warn "Could not set maximum file descriptor limit to $MAX_FD" 158 | esac 159 | fi 160 | 161 | # Collect all arguments for the java command, stacking in reverse order: 162 | # * args from the command line 163 | # * the main class name 164 | # * -classpath 165 | # * -D...appname settings 166 | # * --module-path (only if needed) 167 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 168 | 169 | # For Cygwin or MSYS, switch paths to Windows format before running java 170 | if "$cygwin" || "$msys" ; then 171 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 172 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 173 | 174 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 175 | 176 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 177 | for arg do 178 | if 179 | case $arg in #( 180 | -*) false ;; # don't mess with options #( 181 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 182 | [ -e "$t" ] ;; #( 183 | *) false ;; 184 | esac 185 | then 186 | arg=$( cygpath --path --ignore --mixed "$arg" ) 187 | fi 188 | # Roll the args list around exactly as many times as the number of 189 | # args, so each arg winds up back in the position where it started, but 190 | # possibly modified. 191 | # 192 | # NB: a `for` loop captures its iteration list before it begins, so 193 | # changing the positional parameters here affects neither the number of 194 | # iterations, nor the values presented in `arg`. 195 | shift # remove old arg 196 | set -- "$@" "$arg" # push replacement arg 197 | done 198 | fi 199 | 200 | # Collect all arguments for the java command; 201 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 202 | # shell script including quotes and variable substitutions, so put them in 203 | # double quotes to make sure that they get re-expanded; and 204 | # * put everything else in single quotes, so that it's not re-expanded. 205 | 206 | set -- \ 207 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 208 | -classpath "$CLASSPATH" \ 209 | org.gradle.wrapper.GradleWrapperMain \ 210 | "$@" 211 | 212 | # Stop when "xargs" is not available. 213 | if ! command -v xargs >/dev/null 2>&1 214 | then 215 | die "xargs is not available" 216 | fi 217 | 218 | # Use "xargs" to parse quoted args. 219 | # 220 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 221 | # 222 | # In Bash we could simply go: 223 | # 224 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 225 | # set -- "${ARGS[@]}" "$@" 226 | # 227 | # but POSIX shell has neither arrays nor command substitution, so instead we 228 | # post-process each arg (as a line of input to sed) to backslash-escape any 229 | # character that might be a shell metacharacter, then use eval to reverse 230 | # that process (while maintaining the separation between arguments), and wrap 231 | # the whole thing up as a single "set" statement. 232 | # 233 | # This will of course break if any of these variables contains a newline or 234 | # an unmatched quote. 235 | # 236 | 237 | eval "set -- $( 238 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 239 | xargs -n1 | 240 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 241 | tr '\n' ' ' 242 | )" '"$@"' 243 | 244 | exec "$JAVACMD" "$@" 245 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /linux console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorkbox/Console/30b800de83d7cb06ecf33b0829120aea2c848e08/linux console.png -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 dorkbox, llc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | rootProject.name = "Console" 17 | -------------------------------------------------------------------------------- /src/dorkbox/console/Console.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 dorkbox, llc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package dorkbox.console 17 | 18 | import dorkbox.console.input.Input 19 | import dorkbox.console.input.Terminal 20 | import dorkbox.console.output.Ansi 21 | import dorkbox.console.output.AnsiOutputStream 22 | import dorkbox.console.util.TerminalDetection 23 | import dorkbox.propertyLoader.Property 24 | import dorkbox.updates.Updates.add 25 | import java.io.IOException 26 | import java.io.InputStream 27 | import java.io.PrintStream 28 | 29 | /** 30 | * Provides access to single character input streams and ANSI capable output streams. 31 | * 32 | * @author dorkbox, llc 33 | */ 34 | @Suppress("unused") 35 | object Console { 36 | /** 37 | * If true, allows an ANSI output stream to be created on System.out/err, otherwise it will provide an ANSI aware PrintStream which 38 | * strips out the ANSI escape sequences. 39 | */ 40 | @Property(description = "If true, allows an ANSI output stream to be created on System.out/err, otherwise it will provide an ANSI aware PrintStream which strips out the ANSI escape sequences.") 41 | var ENABLE_ANSI = true 42 | 43 | /** 44 | * If true, then we always force the raw ANSI output stream to be enabled, even if the output stream is not aware of ANSI commands. 45 | * This can be used to obtain the raw ANSI escape codes for other color aware programs (ie: less -r) 46 | */ 47 | @Property(description = "If true, then we always force the raw ANSI output stream to be enabled, even if the output stream is not aware of ANSI commands.") 48 | var FORCE_ENABLE_ANSI = false 49 | 50 | /** 51 | * Enables or disables character echo to stdout in the console, should call [Terminal.setEchoEnabled] after initialization 52 | */ 53 | @Property(description = "Enables or disables character echo to stdout in the console, should call [Terminal.setEchoEnabled] after initialization") 54 | @Volatile 55 | var ENABLE_ECHO = true 56 | 57 | /** 58 | * Enables or disables CTRL-C behavior in the console, should call [Terminal.setInterruptEnabled] after initialization 59 | */ 60 | @Property(description = "Enables or disables CTRL-C behavior in the console, should call [Terminal.setInterruptEnabled] after initialization") 61 | @Volatile 62 | var ENABLE_INTERRUPT = false 63 | 64 | /** 65 | * Enables the backspace key to delete characters in the line buffer and (if ANSI is enabled) from the screen. 66 | */ 67 | @Property(description = "Enables the backspace key to delete characters in the line buffer and (if ANSI is enabled) from the screen.") 68 | val ENABLE_BACKSPACE = true 69 | 70 | 71 | 72 | 73 | /** 74 | * Used to determine what console to use/hook when AUTO is not correctly working. 75 | * Valid options are: 76 | * AUTO - automatically determine which OS/console type to use 77 | * MACOS - try to control a MACOS console 78 | * WINDOWS - try to control a WINDOWS console 79 | * UNIX - try to control a UNIX console 80 | * NONE - do not try to control anything, only line input is supported 81 | */ 82 | @Property(description = "Used to determine what console to use/hook when AUTO is not correctly working.") 83 | val INPUT_CONSOLE_TYPE = TerminalDetection.AUTO 84 | 85 | /** 86 | * Enables the output print-stream to automatically flush after every write. NOTE: This is DANGEROUS, as it removes the usefulness of 87 | * the streams Buffered Writer! 88 | */ 89 | @Property(description = "Enables the output print-stream to automatically flush after every write.") 90 | val AUTO_FLUSH = true 91 | 92 | /** 93 | * Gets the version number. 94 | */ 95 | const val version = "4.1" 96 | 97 | 98 | init { 99 | // Add this project to the updates system, which verifies this class + UUID + version information 100 | add(Console::class.java, "030fa739af4e4698ba99cf275a69d230", version) 101 | } 102 | 103 | /** 104 | * If the standard in supports single character input, then a terminal will be returned that supports it, otherwise a buffered (aka 105 | * 'normal') input will be returned 106 | * 107 | * @return a terminal that supports single character input or the default buffered input 108 | */ 109 | val `in`: Terminal 110 | get() { 111 | return Terminal.terminal 112 | } 113 | 114 | /** 115 | * If the standard in supports single character input, then an InputStream will be returned that supports it, otherwise a buffered (aka 116 | * 'normal') InputStream will be returned 117 | * 118 | * @return an InputStream that supports single character input or the default buffered input 119 | */ 120 | val inputStream: InputStream 121 | get() { 122 | return Input.wrappedInputStream 123 | } 124 | 125 | /** 126 | * Initializes and hooks output streams, necessary when using ANSI for the first time inside of an output stream (as it initializes 127 | * after assignment). 128 | * 129 | * 130 | * This is not needed for input streams, since they do not hook System.err/out. 131 | */ 132 | fun hookSystemOutputStreams() { 133 | out 134 | err 135 | } 136 | 137 | /** 138 | * If the standard out natively supports ANSI escape codes, then this just returns System.out (wrapped to reset ANSI stream on close), 139 | * otherwise it will provide an ANSI aware PrintStream which strips out the ANSI escape sequences. 140 | * 141 | * @return a PrintStream which is ANSI aware. 142 | */ 143 | val out: PrintStream 144 | get() { 145 | return Ansi.out 146 | } 147 | 148 | /** 149 | * If the standard out natively supports ANSI escape codes, then this just returns System.err (wrapped to reset ANSI stream on close), 150 | * otherwise it will provide an ANSI aware PrintStream which strips out the ANSI escape sequences. 151 | * 152 | * @return a PrintStream which is ANSI aware. 153 | */ 154 | val err: PrintStream 155 | get() { 156 | return Ansi.err 157 | } 158 | 159 | /** 160 | * If we are installed to the system (IE: System.err/out, then reset those streams, otherwise there is nothing to do from a static 161 | * perspective (since creating a NEW ANSI stream will automatically reset the output 162 | */ 163 | fun reset() { 164 | try { 165 | Ansi.out.write(AnsiOutputStream.RESET_CODE) 166 | Ansi.err.write(AnsiOutputStream.RESET_CODE) 167 | } 168 | catch (e: IOException) { 169 | e.printStackTrace() 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/dorkbox/console/input/Input.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 dorkbox, llc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package dorkbox.console.input 17 | 18 | import dorkbox.console.Console 19 | import dorkbox.console.input.Terminal.Companion.terminal 20 | import dorkbox.console.util.TerminalDetection 21 | import dorkbox.os.OS 22 | import dorkbox.os.OS.isWindows 23 | import org.slf4j.LoggerFactory 24 | import java.io.IOException 25 | import java.io.InputStream 26 | 27 | object Input { 28 | val wrappedInputStream: InputStream = object : InputStream() { 29 | @Throws(IOException::class) 30 | override fun read(): Int { 31 | return terminal.read() 32 | } 33 | 34 | @Throws(IOException::class) 35 | override fun close() { 36 | terminal.close() 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/dorkbox/console/input/MacOsTerminal.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 dorkbox, llc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package dorkbox.console.input 17 | 18 | import com.sun.jna.ptr.IntByReference 19 | import dorkbox.jna.linux.CLibraryPosix 20 | import dorkbox.jna.macos.CLibraryApple 21 | import dorkbox.jna.macos.CLibraryApple.TIOCGWINSZ 22 | import dorkbox.jna.macos.structs.Termios 23 | import dorkbox.jna.macos.structs.Termios.* 24 | import dorkbox.jna.macos.structs.Termios.Input 25 | import dorkbox.jna.macos.structs.WindowSize 26 | import dorkbox.os.OS 27 | import java.io.IOException 28 | import java.util.concurrent.* 29 | 30 | /** 31 | * Terminal that is used for unix platforms. Terminal initialization is handled via JNA and ioctl/tcgetattr/tcsetattr/cfmakeraw. 32 | * 33 | * This implementation should work for Apple osx. 34 | */ 35 | class MacOsTerminal : SupportedTerminal() { 36 | // stty size logic via Mordent: https://github.com/ajalt/mordant/blob/master/mordant/src/jvmMain/kotlin/com/github/ajalt/mordant/internal/JnaMppImplsMacos.kt 37 | // apache 2.0 38 | // Copyright 2018 AJ Alt 39 | companion object { 40 | private fun runCommand(vararg args: String): Process? { 41 | return try { 42 | ProcessBuilder(*args).redirectInput(ProcessBuilder.Redirect.INHERIT).start() 43 | } 44 | catch (e: IOException) { 45 | null 46 | } 47 | } 48 | 49 | private fun parseSttySize(output: String): Pair? { 50 | val dimens = output.split(" ").mapNotNull { it.toIntOrNull() } 51 | if (dimens.size != 2) return null 52 | return dimens[1] to dimens[0] 53 | } 54 | 55 | private fun getSttySize(timeoutMs: Long): Pair? { 56 | val process = when { 57 | // Try running stty both directly and via env, since neither one works on all systems 58 | else -> runCommand("stty", "size") ?: runCommand("/usr/bin/env", "stty", "size") 59 | } ?: return null 60 | 61 | try { 62 | if (!process.waitFor(timeoutMs, TimeUnit.MILLISECONDS)) { 63 | return null 64 | } 65 | } 66 | catch (e: InterruptedException) { 67 | return null 68 | } 69 | 70 | val output = process.inputStream.bufferedReader().readText().trim() 71 | return parseSttySize(output) 72 | } 73 | } 74 | 75 | private val original = Termios() 76 | private val termInfo = Termios() 77 | private val inputRef = IntByReference() 78 | 79 | init { 80 | // save off the defaults 81 | if (CLibraryApple.tcgetattr(0, original) != 0) { 82 | throw IOException(CONSOLE_ERROR_INIT) 83 | } 84 | 85 | original.read() 86 | 87 | // CTRL-I (tab), CTRL-M (enter) do not work 88 | if (CLibraryApple.tcgetattr(0, termInfo) != 0) { 89 | throw IOException(CONSOLE_ERROR_INIT) 90 | } 91 | 92 | termInfo.read() 93 | 94 | and(termInfo.inputFlags, Input.IXON.inv()) // DISABLE - output flow control mediated by ^S and ^Q 95 | and(termInfo.inputFlags, Input.IXOFF.inv()) // DISABLE - input flow control mediated by ^S and ^Q 96 | and(termInfo.inputFlags, Input.BRKINT.inv()) // DISABLE - map BREAK to SIGINTR 97 | and(termInfo.inputFlags, Input.INPCK.inv()) // DISABLE - enable checking of parity errors 98 | and(termInfo.inputFlags, Input.PARMRK.inv()) // DISABLE - mark parity and framing errors 99 | and(termInfo.inputFlags, Input.ISTRIP.inv()) // DISABLE - strip 8th bit off chars 100 | or(termInfo.inputFlags, Input.IGNBRK) // ignore BREAK condition 101 | 102 | and(termInfo.localFlags, Local.ICANON.inv()) // DISABLE - pass chars straight through to terminal instantly 103 | or(termInfo.localFlags, Local.ECHOCTL) // echo control chars as ^(Char) 104 | 105 | and(termInfo.controlFlags, Control.CSIZE.inv()) // REMOVE character size mask 106 | and(termInfo.controlFlags, Control.PARENB.inv()) // DISABLE - parity enable 107 | or(termInfo.controlFlags, Control.CS8) // set character size mask 8 bits 108 | or(termInfo.controlFlags, Control.CREAD) // enable receiver 109 | 110 | if (CLibraryApple.tcsetattr(0, TCSANOW, termInfo) != 0) { 111 | throw IOException("Can not set terminal flags") 112 | } 113 | } 114 | 115 | 116 | /** 117 | * Restore the original terminal configuration, which can be used when shutting down the console reader. The ConsoleReader cannot be 118 | * used after calling this method. 119 | */ 120 | @Throws(IOException::class) 121 | override fun restore() { 122 | if (CLibraryApple.tcsetattr(0, TCSANOW, original) != 0) { 123 | throw IOException("Can not reset terminal to defaults") 124 | } 125 | } 126 | 127 | /** 128 | * Returns number of columns in the terminal. 129 | */ 130 | override val width: Int 131 | get() { 132 | return if (OS.is64bit && OS.isArm) { 133 | //M1 doesn't work for whatever reason! 134 | // https://github.com/ajalt/mordant/issues/86 135 | // see https://github.com/search?q=repo%3Aajalt%2Fmordant%20detectTerminalSize&type=code 136 | return getSttySize(100)?.first ?: DEFAULT_WIDTH 137 | } else { 138 | val size = WindowSize() 139 | if (CLibraryApple.ioctl(0, TIOCGWINSZ, size) == -1) { 140 | DEFAULT_WIDTH 141 | } 142 | else { 143 | size.read() 144 | size.ws_row.toInt() 145 | } 146 | } 147 | } 148 | 149 | /** 150 | * Returns number of rows in the terminal. 151 | */ 152 | override val height: Int 153 | get() { 154 | return if (OS.is64bit && OS.isArm) { 155 | //M1 doesn't work for whatever reason! 156 | // https://github.com/ajalt/mordant/issues/86 157 | // see https://github.com/search?q=repo%3Aajalt%2Fmordant%20detectTerminalSize&type=code 158 | return getSttySize(100)?.second ?: DEFAULT_HEIGHT 159 | } else { 160 | val size = WindowSize() 161 | if (CLibraryApple.ioctl(0, TIOCGWINSZ, size) == -1) { 162 | DEFAULT_HEIGHT 163 | } 164 | else { 165 | size.read() 166 | size.ws_col.toInt() 167 | } 168 | } 169 | } 170 | 171 | override fun doSetEchoEnabled(enabled: Boolean) { 172 | // have to re-get them, since flags change everything 173 | if (CLibraryApple.tcgetattr(0, termInfo) != 0) { 174 | logger.error("Failed to get terminal info") 175 | } 176 | 177 | if (enabled) { 178 | or(termInfo.localFlags, Local.ECHO) // ENABLE Echo input characters. 179 | } 180 | else { 181 | and(termInfo.localFlags, Local.ECHO.inv()) // DISABLE Echo input characters. 182 | } 183 | 184 | if (CLibraryApple.tcsetattr(0, TCSANOW, termInfo) != 0) { 185 | logger.error("Can not set terminal flags") 186 | } 187 | } 188 | 189 | override fun doSetInterruptEnabled(enabled: Boolean) { 190 | // have to re-get them, since flags change everything 191 | if (CLibraryApple.tcgetattr(0, termInfo) != 0) { 192 | logger.error("Failed to get terminal info") 193 | } 194 | 195 | if (enabled) { 196 | or(termInfo.localFlags, Local.ISIG) // ENABLE ctrl-C 197 | } 198 | else { 199 | and(termInfo.localFlags, Local.ISIG.inv()) // DISABLE ctrl-C 200 | } 201 | if (CLibraryApple.tcsetattr(0, TCSANOW, termInfo) != 0) { 202 | logger.error("Can not set terminal flags") 203 | } 204 | } 205 | 206 | override fun doRead(): Int { 207 | CLibraryPosix.read(0, inputRef, 1) 208 | return inputRef.value 209 | } 210 | 211 | 212 | } 213 | -------------------------------------------------------------------------------- /src/dorkbox/console/input/PosixTerminal.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 dorkbox, llc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package dorkbox.console.input 17 | 18 | import com.sun.jna.ptr.IntByReference 19 | import dorkbox.jna.linux.CLibraryPosix 20 | import dorkbox.jna.linux.structs.Termios 21 | import java.io.IOException 22 | import java.nio.ByteBuffer 23 | 24 | /** 25 | * Terminal that is used for unix platforms. Terminal initialization is handled via JNA and ioctl/tcgetattr/tcsetattr/cfmakeraw. 26 | * 27 | * 28 | * This implementation should work for a reasonable POSIX system. 29 | */ 30 | class PosixTerminal : SupportedTerminal() { 31 | private val original = Termios() 32 | private val termInfo = Termios() 33 | private val windowSizeBuffer = ByteBuffer.allocate(8) 34 | private val inputRef = IntByReference() 35 | 36 | init { 37 | // save off the defaults 38 | if (CLibraryPosix.tcgetattr(0, original) != 0) { 39 | throw IOException(CONSOLE_ERROR_INIT) 40 | } 41 | 42 | original.read() 43 | 44 | // CTRL-I (tab), CTRL-M (enter) do not work 45 | if (CLibraryPosix.tcgetattr(0, termInfo) != 0) { 46 | throw IOException(CONSOLE_ERROR_INIT) 47 | } 48 | 49 | termInfo.read() 50 | 51 | termInfo.inputFlags = termInfo.inputFlags and Termios.Input.IXON.inv() // DISABLE - output flow control mediated by ^S and ^Q 52 | termInfo.inputFlags = termInfo.inputFlags and Termios.Input.IXOFF.inv() // DISABLE - input flow control mediated by ^S and ^Q 53 | termInfo.inputFlags = termInfo.inputFlags and Termios.Input.BRKINT.inv() // DISABLE - map BREAK to SIGINTR 54 | termInfo.inputFlags = termInfo.inputFlags and Termios.Input.INPCK.inv() // DISABLE - enable checking of parity errors 55 | termInfo.inputFlags = termInfo.inputFlags and Termios.Input.PARMRK.inv() // DISABLE - mark parity and framing errors 56 | termInfo.inputFlags = termInfo.inputFlags and Termios.Input.ISTRIP.inv() // DISABLE - strip 8th bit off chars 57 | termInfo.inputFlags = termInfo.inputFlags or Termios.Input.IGNBRK // ignore BREAK condition 58 | 59 | termInfo.localFlags = termInfo.localFlags and Termios.Local.ICANON.inv() // DISABLE - pass chars straight through to terminal instantly 60 | termInfo.localFlags = termInfo.localFlags or Termios.Local.ECHOCTL // echo control chars as ^(Char) 61 | 62 | termInfo.controlFlags = termInfo.controlFlags and Termios.Control.CSIZE.inv() // REMOVE character size mask 63 | termInfo.controlFlags = termInfo.controlFlags and Termios.Control.PARENB.inv() // DISABLE - parity enable 64 | termInfo.controlFlags = termInfo.controlFlags or Termios.Control.CS8 // set character size mask 8 bits 65 | termInfo.controlFlags = termInfo.controlFlags or Termios.Control.CREAD // enable receiver 66 | 67 | if (CLibraryPosix.tcsetattr(0, Termios.TCSANOW, termInfo) != 0) { 68 | throw IOException("Can not set terminal flags") 69 | } 70 | } 71 | 72 | /** 73 | * Restore the original terminal configuration, which can be used when shutting down the console reader. The ConsoleReader cannot be 74 | * used after calling this method. 75 | */ 76 | @Throws(IOException::class) 77 | override fun restore() { 78 | if (CLibraryPosix.tcsetattr(0, Termios.TCSANOW, original) != 0) { 79 | throw IOException("Can not reset terminal to defaults") 80 | } 81 | } 82 | 83 | /** 84 | * Returns number of columns in the terminal. 85 | */ 86 | override val width: Int 87 | get() { 88 | return if (CLibraryPosix.ioctl(0, CLibraryPosix.TIOCGWINSZ, windowSizeBuffer) != 0) { 89 | DEFAULT_WIDTH 90 | } 91 | else { 92 | (0x000000FF and windowSizeBuffer[2] + (0x000000FF and windowSizeBuffer[3].toInt()) * 256).toShort().toInt() 93 | } 94 | } 95 | 96 | /** 97 | * Returns number of rows in the terminal. 98 | */ 99 | override val height: Int 100 | get() { 101 | return if (CLibraryPosix.ioctl(0, CLibraryPosix.TIOCGWINSZ, windowSizeBuffer) != 0) { 102 | DEFAULT_HEIGHT 103 | } 104 | else { 105 | (0x000000FF and windowSizeBuffer[0] + (0x000000FF and windowSizeBuffer[1].toInt()) * 256).toShort().toInt() 106 | } 107 | } 108 | 109 | override fun doSetEchoEnabled(enabled: Boolean) { 110 | // have to re-get them, since flags change everything 111 | if (CLibraryPosix.tcgetattr(0, termInfo) != 0) { 112 | logger.error("Failed to get terminal info") 113 | } 114 | 115 | if (enabled) { 116 | termInfo.localFlags = termInfo.localFlags or Termios.Local.ECHO // ENABLE Echo input characters. 117 | } 118 | else { 119 | termInfo.localFlags = termInfo.localFlags and Termios.Local.ECHO.inv() // DISABLE Echo input characters. 120 | } 121 | 122 | if (CLibraryPosix.tcsetattr(0, Termios.TCSANOW, termInfo) != 0) { 123 | logger.error("Can not set terminal flags") 124 | } 125 | } 126 | 127 | override fun doSetInterruptEnabled(enabled: Boolean) { 128 | 129 | // have to re-get them, since flags change everything 130 | if (CLibraryPosix.tcgetattr(0, termInfo) != 0) { 131 | logger.error("Failed to get terminal info") 132 | } 133 | 134 | if (enabled) { 135 | termInfo.localFlags = termInfo.localFlags or Termios.Local.ISIG // ENABLE ctrl-C 136 | } 137 | else { 138 | termInfo.localFlags = termInfo.localFlags and Termios.Local.ISIG.inv() // DISABLE ctrl-C 139 | } 140 | if (CLibraryPosix.tcsetattr(0, Termios.TCSANOW, termInfo) != 0) { 141 | logger.error("Can not set terminal flags") 142 | } 143 | } 144 | 145 | override fun doRead(): Int { 146 | CLibraryPosix.read(0, inputRef, 1) 147 | return inputRef.value 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/dorkbox/console/input/SupportedTerminal.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 dorkbox, llc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package dorkbox.console.input 17 | 18 | import dorkbox.bytes.ByteArrayBuffer 19 | import dorkbox.console.Console 20 | import dorkbox.console.output.Ansi 21 | import dorkbox.console.util.CharHolder 22 | import java.util.concurrent.locks.* 23 | import kotlin.concurrent.withLock 24 | 25 | abstract class SupportedTerminal : Terminal(), Runnable { 26 | private val out = System.out 27 | 28 | private val inputLockLine = ReentrantLock() 29 | private val inputLockLineCondition = inputLockLine.newCondition() 30 | 31 | private val inputLockSingle = ReentrantLock() 32 | private val inputLockSingleCondition = inputLockSingle.newCondition() 33 | 34 | 35 | private val charInputBuffers: MutableList = ArrayList() 36 | private val charInput: ThreadLocal = object : ThreadLocal() { 37 | public override fun initialValue(): CharHolder { 38 | return CharHolder() 39 | } 40 | } 41 | 42 | private val lineInputBuffers: MutableList = ArrayList() 43 | private val lineInput: ThreadLocal = object : ThreadLocal() { 44 | public override fun initialValue(): ByteArrayBuffer { 45 | return ByteArrayBuffer(8, -1) 46 | } 47 | } 48 | 49 | /** 50 | * Reads single character input from the console. 51 | * 52 | * @return -1 if no data or problems 53 | */ 54 | override fun read(): Int { 55 | val holder = charInput.get() 56 | 57 | inputLockSingle.withLock { 58 | // don't want to register a read() WHILE we are still processing the current input. 59 | // also adds it to the global list of char inputs 60 | charInputBuffers.add(holder) 61 | 62 | try { 63 | inputLockSingleCondition.await() 64 | } 65 | catch (e: InterruptedException) { 66 | return -1 67 | } 68 | val c = holder.character 69 | 70 | // also clears and removes from the global list of char inputs 71 | charInputBuffers.remove(holder) 72 | return c.code 73 | } 74 | } 75 | 76 | /** 77 | * Reads a line of characters from the console as a character array, defined as everything before the 'ENTER' key is pressed 78 | * 79 | * @return empty char[] if no data 80 | */ 81 | override fun readLineChars(): CharArray { 82 | val buffer = lineInput.get() 83 | 84 | inputLockLine.withLock { 85 | // don't want to register a readLine() WHILE we are still processing the current line info. 86 | // also adds it to the global list of line inputs 87 | lineInputBuffers.add(buffer) 88 | 89 | try { 90 | inputLockLineCondition.await() 91 | } 92 | catch (e: InterruptedException) { 93 | return EMPTY_LINE 94 | } 95 | 96 | // removes from the global list of line inputs 97 | lineInputBuffers.remove(buffer) 98 | 99 | val len = buffer.position() 100 | if (len == 0) { 101 | return EMPTY_LINE 102 | } 103 | 104 | buffer.rewind() 105 | val readChars = buffer.readChars(len / 2) // java always stores chars in 2 bytes 106 | 107 | // dump the chars in the buffer (safer for passwords, etc) 108 | buffer.clearSecure() 109 | 110 | 111 | return readChars 112 | } 113 | } 114 | 115 | /** 116 | * releases any thread still waiting. 117 | */ 118 | override fun close() { 119 | inputLockSingle.withLock { 120 | inputLockSingleCondition.signalAll() 121 | } 122 | inputLockLine.withLock { 123 | inputLockLineCondition.signalAll() 124 | } 125 | } 126 | 127 | /** 128 | * Reads a single character from whatever underlying stream is available. 129 | */ 130 | protected abstract fun doRead(): Int 131 | 132 | override fun run() { 133 | val logger2 = logger 134 | val overWriteChar = ' ' 135 | var ansi: Ansi? = null 136 | var typedChar: Int 137 | var asChar: Char 138 | 139 | while (doRead().also { typedChar = it } != -1) { 140 | // don't let anyone add a new reader while we are still processing the current actions 141 | asChar = typedChar.toChar() 142 | if (logger2.isTraceEnabled) { 143 | logger2.trace("READ: {} ({})", asChar, typedChar) 144 | } 145 | 146 | // notify everyone waiting for a character. 147 | inputLockSingle.withLock { 148 | // have to do readChar first (readLine has to deal with \b and \n 149 | for (holder in charInputBuffers) { 150 | holder.character = asChar // copy by value 151 | } 152 | 153 | inputLockSingleCondition.signalAll() 154 | } 155 | 156 | // now to handle readLine stuff 157 | 158 | // if we type a backspace key, swallow it + previous in READLINE. READCHAR will have it passed anyways. 159 | if (Console.ENABLE_BACKSPACE && (asChar == '\b' || asChar == '\u007F')) { 160 | var position = 0 161 | var overwrite: CharArray? = null 162 | 163 | // clear ourself + one extra. 164 | inputLockLine.withLock { 165 | for (buffer in lineInputBuffers) { 166 | // size of the buffer BEFORE our backspace was typed 167 | var length = buffer.position() 168 | var amtToOverwrite = 4 // 2*2 backspace is always 2 chars (^?) * 2 because it's bytes 169 | if (length > 1) { 170 | var charAt = buffer.readChar(length - 2) 171 | amtToOverwrite += getPrintableCharacters(charAt.code) 172 | 173 | // delete last item in our buffer 174 | length -= 2 175 | buffer.setPosition(length) 176 | 177 | // now figure out where the cursor is really at. 178 | // this is more memory friendly than buf.toString.length 179 | var i = 0 180 | while (i < length) { 181 | charAt = buffer.readChar(i) 182 | position += getPrintableCharacters(charAt.code) 183 | i += 2 184 | } 185 | position++ 186 | } 187 | overwrite = CharArray(amtToOverwrite) 188 | 189 | for (i in 0 until amtToOverwrite) { 190 | overwrite!![i] = overWriteChar 191 | } 192 | } 193 | } 194 | 195 | 196 | if (Console.ENABLE_ANSI && overwrite != null) { 197 | if (ansi == null) { 198 | ansi = Ansi.ansi() 199 | } 200 | 201 | // move back however many, overwrite, then go back again 202 | out.print(ansi.cursorToColumn(position)) 203 | out.print(overwrite!!) 204 | out.print(ansi.cursorToColumn(position)) 205 | out.flush() 206 | } 207 | } 208 | else if (asChar == '\n') { 209 | // ignoring \r, because \n is ALWAYS the last character in a new line sequence. (even for windows, which we changed) 210 | inputLockLine.withLock { 211 | inputLockLineCondition.signalAll() 212 | } 213 | } 214 | else { 215 | // only append if we are not a new line. 216 | // our windows console PREVENTS us from returning '\r' (it truncates '\r\n', and returns just '\n') 217 | inputLockLine.withLock { 218 | for (buffer in lineInputBuffers) { 219 | buffer.writeChar(asChar) 220 | } 221 | } 222 | } 223 | } 224 | } 225 | 226 | companion object { 227 | private const val PLUS_TWO_MAYBE = 128 + 32 228 | private const val PLUS_ONE = 128 + 127 229 | 230 | /** 231 | * Return the number of characters that will be printed when the specified character is echoed to the screen 232 | * 233 | * 234 | * Adapted from cat by Torbjorn Granlund, as repeated in stty by David MacKenzie. 235 | */ 236 | private fun getPrintableCharacters(ch: Int): Int { 237 | // StringBuilder sbuff = new StringBuilder(); 238 | return if (ch >= 32) { 239 | if (ch < 127) { 240 | // sbuff.append((char) ch); 241 | 1 242 | } 243 | else if (ch == 127) { 244 | // sbuff.append('^'); 245 | // sbuff.append('?'); 246 | 2 247 | } 248 | else { 249 | // sbuff.append('M'); 250 | // sbuff.append('-'); 251 | var count = 2 252 | if (ch >= PLUS_TWO_MAYBE) { 253 | if (ch < PLUS_ONE) { 254 | // sbuff.append((char) (ch - 128)); 255 | count++ 256 | } 257 | else { 258 | // sbuff.append('^'); 259 | // sbuff.append('?'); 260 | count += 2 261 | } 262 | } 263 | else { 264 | // sbuff.append('^'); 265 | // sbuff.append((char) (ch - 128 + 64)); 266 | count += 2 267 | } 268 | count 269 | } 270 | } 271 | else { 272 | // sbuff.append('^'); 273 | // sbuff.append((char) (ch + 64)); 274 | 2 275 | } 276 | 277 | // return sbuff; 278 | } 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /src/dorkbox/console/input/Terminal.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 dorkbox, llc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package dorkbox.console.input 17 | 18 | import dorkbox.console.Console 19 | import dorkbox.console.util.TerminalDetection 20 | import dorkbox.os.OS 21 | import org.slf4j.LoggerFactory 22 | import java.io.IOException 23 | 24 | @Suppress("unused") 25 | abstract class Terminal internal constructor() { 26 | companion object { 27 | private val logger = LoggerFactory.getLogger(Console::class.java) 28 | val terminal: Terminal 29 | 30 | val EMPTY_LINE = CharArray(0) 31 | 32 | const val CONSOLE_ERROR_INIT = "Unable to initialize the input console." 33 | const val DEFAULT_WIDTH = 80 34 | const val DEFAULT_HEIGHT = 24 35 | 36 | 37 | init { 38 | var didFallbackE: Throwable? = null 39 | var term: Terminal 40 | 41 | try { 42 | term = when (Console.INPUT_CONSOLE_TYPE) { 43 | TerminalDetection.MACOS -> { 44 | MacOsTerminal() 45 | } 46 | TerminalDetection.UNIX -> { 47 | PosixTerminal() 48 | } 49 | TerminalDetection.WINDOWS -> { 50 | WindowsTerminal() 51 | } 52 | TerminalDetection.NONE -> { 53 | UnsupportedTerminal() 54 | } 55 | else -> { 56 | // AUTO type. 57 | 58 | // if these cannot be created, because we are in an IDE, an error will be thrown 59 | 60 | if (OS.isMacOsX) { 61 | MacOsTerminal() 62 | } else { 63 | val IS_CYGWIN = OS.isWindows && System.getenv("PWD") != null && System.getenv("PWD").startsWith("/") 64 | val IS_MSYSTEM = OS.isWindows && System.getenv("MSYSTEM") != null && (System.getenv("MSYSTEM").startsWith("MINGW") || System.getenv("MSYSTEM") == "MSYS") 65 | val IS_CONEMU = (OS.isWindows && System.getenv("ConEmuPID") != null) 66 | 67 | 68 | if (OS.isWindows && !IS_CYGWIN && !IS_MSYSTEM && !IS_CONEMU) { 69 | WindowsTerminal() 70 | } 71 | else { 72 | PosixTerminal() 73 | } 74 | } 75 | } 76 | } 77 | } 78 | catch (e: Exception) { 79 | didFallbackE = e 80 | term = UnsupportedTerminal() 81 | } 82 | 83 | terminal = term 84 | 85 | if (terminal is SupportedTerminal) { 86 | // enable echo and backspace 87 | terminal.setEchoEnabled(Console.ENABLE_ECHO) 88 | terminal.setInterruptEnabled(Console.ENABLE_INTERRUPT) 89 | 90 | val consoleThread = Thread(terminal) 91 | consoleThread.setDaemon(true) 92 | consoleThread.setName("Console Input Reader") 93 | consoleThread.start() 94 | 95 | // has to be NOT DAEMON thread, since it must run before the app closes. 96 | // alternatively, shut everything down when the JVM closes. 97 | val shutdownThread = object : Thread() { 98 | override fun run() { 99 | // called when the JVM is shutting down. 100 | terminal.close() 101 | 102 | try { 103 | terminal.restore() 104 | // this will 'hang' our shutdown, and honestly, who cares? We're shutting down anyways. 105 | // inputConsole.reader.close(); // hangs on shutdown 106 | } 107 | catch (ignored: IOException) { 108 | ignored.printStackTrace() 109 | } 110 | } 111 | } 112 | shutdownThread.setName("Console Input Shutdown") 113 | shutdownThread.setDaemon(true) 114 | Runtime.getRuntime().addShutdownHook(shutdownThread) 115 | } 116 | 117 | 118 | 119 | 120 | val debugEnabled = logger.isDebugEnabled 121 | 122 | if (didFallbackE != null && didFallbackE.message != CONSOLE_ERROR_INIT) { 123 | logger.error("Failed to construct terminal, falling back to unsupported.", didFallbackE) 124 | } 125 | else if (debugEnabled && term is UnsupportedTerminal) { 126 | logger.debug("Terminal is UNSUPPORTED (best guess). Unable to support single key input. Only line input available.") 127 | } 128 | else if (debugEnabled) { 129 | logger.debug("Created Terminal: ${terminal.javaClass.getSimpleName()} (${terminal.width}w x ${terminal.height}h)") 130 | } 131 | } 132 | } 133 | 134 | val logger = LoggerFactory.getLogger(javaClass) 135 | 136 | abstract fun doSetInterruptEnabled(enabled: Boolean) 137 | protected abstract fun doSetEchoEnabled(enabled: Boolean) 138 | 139 | @Throws(IOException::class) 140 | abstract fun restore() 141 | 142 | abstract val width: Int 143 | abstract val height: Int 144 | 145 | /** 146 | * Enables or disables CTRL-C behavior in the console 147 | */ 148 | fun setInterruptEnabled(enabled: Boolean) { 149 | Console.ENABLE_INTERRUPT = enabled 150 | doSetInterruptEnabled(enabled) 151 | } 152 | 153 | /** 154 | * Enables or disables character echo to stdout 155 | */ 156 | fun setEchoEnabled(enabled: Boolean) { 157 | Console.ENABLE_ECHO = enabled 158 | doSetEchoEnabled(enabled) 159 | } 160 | 161 | /** 162 | * Reads single character input from the console. 163 | * 164 | * @return -1 if no data or problems 165 | */ 166 | abstract fun read(): Int 167 | 168 | /** 169 | * Reads a line of characters from the console as a character array, defined as everything before the 'ENTER' key is pressed 170 | * 171 | * @return empty char[] if no data 172 | */ 173 | abstract fun readLineChars(): CharArray 174 | 175 | /** 176 | * Reads a single line of characters, defined as everything before the 'ENTER' key is pressed 177 | * 178 | * @return the string contents of a line (empty if there is no characters) 179 | */ 180 | fun readLine(): String { 181 | val line = readLineChars() 182 | return String(line) 183 | } 184 | 185 | /** 186 | * Reads a line of characters from the console as a character array, defined as everything before the 'ENTER' key is pressed 187 | * 188 | * @return empty char[] if no data 189 | */ 190 | fun readLinePassword(): CharArray { 191 | // don't bother in an IDE. it won't work. 192 | val echoEnabled = Console.ENABLE_ECHO 193 | Console.ENABLE_ECHO = false 194 | 195 | val readLine0 = readLineChars() 196 | Console.ENABLE_ECHO = echoEnabled 197 | return readLine0 198 | } 199 | 200 | /** 201 | * releases any thread still waiting. 202 | */ 203 | abstract fun close() 204 | } 205 | -------------------------------------------------------------------------------- /src/dorkbox/console/input/UnsupportedTerminal.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 dorkbox, llc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package dorkbox.console.input 17 | 18 | import dorkbox.bytes.ByteArrayBuffer 19 | import java.io.BufferedReader 20 | import java.io.IOException 21 | import java.io.InputStreamReader 22 | import java.util.concurrent.locks.* 23 | import kotlin.concurrent.withLock 24 | 25 | class UnsupportedTerminal : Terminal() { 26 | private val buffer: ThreadLocal = object : ThreadLocal() { 27 | public override fun initialValue(): ByteArrayBuffer { 28 | return ByteArrayBuffer(8, -1) 29 | } 30 | } 31 | private val readCount: ThreadLocal = object : ThreadLocal() { 32 | public override fun initialValue(): Int { 33 | return 0 34 | } 35 | } 36 | 37 | override fun doSetInterruptEnabled(enabled: Boolean) {} 38 | override fun doSetEchoEnabled(enabled: Boolean) {} 39 | override fun restore() {} 40 | 41 | override val width: Int 42 | get() { 43 | return 0 44 | } 45 | 46 | override val height: Int 47 | get () { 48 | return 0 49 | } 50 | 51 | /** 52 | * Reads single character input from the console. This is "faked" by reading a line 53 | * 54 | * @return -1 if no data or problems 55 | */ 56 | override fun read(): Int { 57 | val position: Int 58 | // so, 'readCount' is REALLY the index at which we return letters (until the whole string is returned) 59 | val buffer = buffer.get() 60 | buffer.clearSecure() 61 | 62 | // we have to wait for more data. 63 | if (readCount.get() == 0) { 64 | try { 65 | lock.withLock { 66 | condition.await() 67 | } 68 | } 69 | catch (ignored: Exception) { 70 | } 71 | 72 | if (currentConsoleInput == null) { 73 | return -1 74 | } 75 | 76 | val chars = currentConsoleInput!!.toCharArray() 77 | buffer.writeChars(chars) 78 | position = buffer.position() 79 | buffer.rewind() 80 | 81 | readCount.set(position) 82 | if (position == 0) { 83 | // only send a NEW LINE if it was the ONLY thing pressed (this is to MOST ACCURATELY simulate single char input 84 | return '\n'.code 85 | } 86 | buffer.rewind() 87 | } 88 | 89 | readCount.set(readCount.get() - 2) // 2 bytes per char in the stream 90 | return buffer.readChar().code 91 | } 92 | 93 | /** 94 | * Reads a line of characters from the console as a character array, defined as everything before the 'ENTER' key is pressed 95 | * 96 | * @return empty char[] if no data 97 | */ 98 | override fun readLineChars(): CharArray { 99 | // we have to wait for more data. 100 | try { 101 | lock.withLock { 102 | condition.await() 103 | } 104 | } 105 | catch (ignored: Exception) { 106 | } 107 | 108 | if (currentConsoleInput == null) { 109 | return EMPTY_LINE 110 | } 111 | 112 | val chars = currentConsoleInput!!.toCharArray() 113 | val length = chars.size 114 | return if (length == 0) { 115 | // only send a NEW LINE if it was the ONLY thing pressed (this is to MOST ACCURATELY simulate single char input 116 | NEW_LINE 117 | } 118 | else chars 119 | } 120 | 121 | override fun close() { 122 | lock.withLock { 123 | condition.signalAll() 124 | } 125 | } 126 | 127 | companion object { 128 | private val NEW_LINE: CharArray 129 | private val backgroundReaderThread: Thread 130 | 131 | private val lock = ReentrantLock() 132 | private val condition = lock.newCondition() 133 | 134 | private var currentConsoleInput: String? = null 135 | 136 | init { 137 | NEW_LINE = CharArray(1) 138 | NEW_LINE[0] = '\n' 139 | 140 | // this adopts a different thread + locking to enable reader threads to "unblock" after the blocking read. 141 | // http://bugs.java.com/bugdatabase/view_bug.do?bug_id=4514257 142 | // https://community.oracle.com/message/5318833#5318833 143 | backgroundReaderThread = Thread { 144 | val reader = BufferedReader(InputStreamReader(System.`in`)) 145 | try { 146 | while (!Thread.interrupted()) { 147 | currentConsoleInput = null 148 | val line = reader.readLine() ?: break 149 | 150 | lock.withLock { 151 | currentConsoleInput = line 152 | condition.signalAll() 153 | } 154 | } 155 | } 156 | catch (e: IOException) { 157 | throw RuntimeException(e) 158 | } 159 | } 160 | backgroundReaderThread.setDaemon(true) 161 | backgroundReaderThread.start() 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/dorkbox/console/input/WindowsTerminal.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 dorkbox, llc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dorkbox.console.input 18 | 19 | import com.sun.jna.platform.win32.WinBase 20 | import com.sun.jna.platform.win32.WinNT 21 | import com.sun.jna.platform.win32.Wincon 22 | import com.sun.jna.ptr.IntByReference 23 | import dorkbox.jna.windows.Kernel32 24 | import dorkbox.jna.windows.structs.CONSOLE_SCREEN_BUFFER_INFO 25 | import dorkbox.jna.windows.structs.INPUT_RECORD 26 | import java.io.IOException 27 | 28 | /** 29 | * Terminal implementation for Microsoft Windows. 30 | */ 31 | class WindowsTerminal : SupportedTerminal() { 32 | companion object { 33 | // output stream for "echo" to goto 34 | private val OUT = System.out 35 | } 36 | 37 | private val console = Kernel32.GetStdHandle(Wincon.STD_INPUT_HANDLE) 38 | private val outputConsole = Kernel32.GetStdHandle(Wincon.STD_OUTPUT_HANDLE) 39 | private val info = CONSOLE_SCREEN_BUFFER_INFO() 40 | private val inputRecords = INPUT_RECORD.ByReference() 41 | private val reference = IntByReference() 42 | 43 | private val originalMode: Int 44 | 45 | @Volatile 46 | private var echoEnabled = false 47 | 48 | init { 49 | if (console === WinBase.INVALID_HANDLE_VALUE) { 50 | throw IOException("Unable to get input console handle.") 51 | } 52 | 53 | if (outputConsole === WinBase.INVALID_HANDLE_VALUE) { 54 | throw IOException("Unable to get output console handle.") 55 | } 56 | 57 | val mode = IntByReference() 58 | if (Kernel32.GetConsoleMode(console, mode) == 0) { 59 | throw IOException(CONSOLE_ERROR_INIT) 60 | } 61 | 62 | originalMode = mode.value 63 | 64 | val newMode = 0 // this is raw everything, not ignoring ctrl-c 65 | Kernel32.ASSERT(Kernel32.SetConsoleMode(console, newMode), CONSOLE_ERROR_INIT) 66 | } 67 | 68 | /** 69 | * Restore the original terminal configuration, which can be used when shutting down the console reader. 70 | * The ConsoleReader cannot be used after calling this method. 71 | */ 72 | @Throws(IOException::class) 73 | override fun restore() { 74 | Kernel32.ASSERT(Kernel32.SetConsoleMode(console, originalMode), CONSOLE_ERROR_INIT) 75 | Kernel32.CloseHandle(console) 76 | Kernel32.CloseHandle(outputConsole) 77 | } 78 | 79 | override val width: Int 80 | get() { 81 | Kernel32.GetConsoleScreenBufferInfo(outputConsole, info) 82 | val w = info.window.width() + 1 83 | return if (w < 1) DEFAULT_WIDTH else w 84 | } 85 | 86 | override val height: Int 87 | get() { 88 | Kernel32.GetConsoleScreenBufferInfo(outputConsole, info) 89 | val h = info.window.height() + 1 90 | return if (h < 1) DEFAULT_HEIGHT else h 91 | } 92 | 93 | override fun doSetEchoEnabled(enabled: Boolean) { 94 | // only way to do this, console modes DO NOT work 95 | echoEnabled = enabled 96 | } 97 | 98 | override fun doSetInterruptEnabled(enabled: Boolean) { 99 | val mode = IntByReference() 100 | Kernel32.GetConsoleMode(console, mode) 101 | 102 | /** 103 | * CTRL+C is processed by the system and is not placed in the input buffer. If the input buffer is being read by ReadFile or 104 | * ReadConsole, other control keys are processed by the system and are not returned in the ReadFile or ReadConsole buffer. If the 105 | * ENABLE_LINE_INPUT mode is also enabled, backspace, carriage return, and linefeed characters are handled by the system. 106 | */ 107 | val newMode: Int = if (enabled) { 108 | // Enable Ctrl+C 109 | mode.value or 1 110 | } 111 | else { 112 | // Disable Ctrl+C 113 | mode.value and 1.inv() 114 | } 115 | Kernel32.ASSERT(Kernel32.SetConsoleMode(console, newMode), CONSOLE_ERROR_INIT) 116 | } 117 | 118 | override fun doRead(): Int { 119 | val input = readInput() 120 | 121 | if (echoEnabled) { 122 | val asChar = input.toChar() 123 | if (asChar == '\n') { 124 | OUT.println() 125 | } 126 | else { 127 | OUT.write(asChar.code) 128 | } 129 | // have to flush, otherwise we'll never see the chars on screen 130 | OUT.flush() 131 | } 132 | return input 133 | } 134 | 135 | private fun readInput(): Int { 136 | // keep reading input events until we find one that we are interested in (ie: keyboard input) 137 | while (true) { 138 | // blocks until there is (at least) 1 event on the buffer 139 | Kernel32.ReadConsoleInput(console, inputRecords, 1, reference) 140 | for (i in 0 until reference.value) { 141 | if (inputRecords.EventType == INPUT_RECORD.KEY_EVENT) { 142 | val keyEvent = inputRecords.Event.KeyEvent 143 | 144 | //logger.trace(keyEvent.bKeyDown ? "KEY_DOWN" : "KEY_UP", "key code:", keyEvent.wVirtualKeyCode, "char:", (long)keyEvent.uChar.unicodeChar); 145 | if (keyEvent.keyDown) { 146 | val uChar = keyEvent.uChar.unicodeChar 147 | if (uChar.code > 0) { 148 | if (uChar == '\r') { 149 | // we purposefully swallow input after \r, and substitute it with \n 150 | return '\n'.code 151 | } 152 | else if (uChar == '\n') { 153 | continue 154 | } 155 | return uChar.code 156 | } 157 | } 158 | } 159 | } 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/dorkbox/console/input/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 dorkbox, llc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dorkbox.console.input; 18 | -------------------------------------------------------------------------------- /src/dorkbox/console/output/AnsiCodeMap.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 dorkbox, llc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package dorkbox.console.output 17 | 18 | /** 19 | * Used for determining what ANSI attribute to use based on a formal name 20 | */ 21 | internal class AnsiCodeMap(private val anEnum: Enum<*>, val isBackgroundColor: Boolean) { 22 | 23 | val isColor: Boolean 24 | get() = anEnum is Color 25 | 26 | val color: Color 27 | get() = anEnum as Color 28 | 29 | val isAttribute: Boolean 30 | get() = anEnum is Attribute 31 | 32 | val attribute: Attribute 33 | get() = anEnum as Attribute 34 | } 35 | -------------------------------------------------------------------------------- /src/dorkbox/console/output/AnsiOutputStream.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 dorkbox, llc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package dorkbox.console.output 17 | 18 | import java.io.FilterOutputStream 19 | import java.io.IOException 20 | import java.io.OutputStream 21 | import java.nio.charset.Charset 22 | 23 | /** 24 | * A ANSI output stream extracts ANSI escape codes written to an output stream. 25 | * 26 | * For more information about ANSI escape codes, see: 27 | * http://en.wikipedia.org/wiki/ANSI_escape_code 28 | * 29 | * This class just filters out the escape codes so that they are not sent out to the underlying OutputStream. Subclasses should 30 | * actually perform the ANSI escape behaviors. 31 | * 32 | * @author dorkbox, llc 33 | * @author [Hiram Chirino](http://hiramchirino.com) 34 | * @author Joris Kuipers 35 | */ 36 | open class AnsiOutputStream(os: OutputStream?) : FilterOutputStream(os) { 37 | private var state = STATE.LOOKING_FOR_FIRST_ESC_CHAR 38 | private val buffer = ByteArray(MAX_ESCAPE_SEQUENCE_LENGTH) 39 | 40 | private var pos = 0 41 | private var startOfValue = 0 42 | 43 | private val options = ArrayList() 44 | 45 | @Throws(IOException::class) 46 | override fun write(data: Int) { 47 | when (state) { 48 | STATE.LOOKING_FOR_FIRST_ESC_CHAR -> { 49 | if (data == FIRST_ESC_CHAR) { 50 | buffer[pos++] = data.toByte() 51 | state = STATE.LOOKING_FOR_SECOND_ESC_CHAR 52 | } 53 | else { 54 | out.write(data) 55 | } 56 | } 57 | 58 | STATE.LOOKING_FOR_SECOND_ESC_CHAR -> { 59 | buffer[pos++] = data.toByte() 60 | if (data == SECOND_ESC_CHAR) { 61 | state = STATE.LOOKING_FOR_NEXT_ARG 62 | } 63 | else if (data == SECOND_OSC_CHAR) { 64 | state = STATE.LOOKING_FOR_OSC_COMMAND 65 | } 66 | else { 67 | reset(false) 68 | } 69 | } 70 | 71 | STATE.LOOKING_FOR_NEXT_ARG -> { 72 | buffer[pos++] = data.toByte() 73 | 74 | if ('"'.code == data) { 75 | startOfValue = pos - 1 76 | state = STATE.LOOKING_FOR_STR_ARG_END 77 | } 78 | else if ('0'.code <= data && data <= '9'.code) { 79 | startOfValue = pos - 1 80 | state = STATE.LOOKING_FOR_INT_ARG_END 81 | } 82 | else if (';'.code == data) { 83 | options.add(null) 84 | } 85 | else if ('?'.code == data) { 86 | options.add('?') 87 | } 88 | else if ('='.code == data) { 89 | options.add('=') 90 | } 91 | else { 92 | reset(processEscapeCommand(options, data.toChar())) 93 | } 94 | } 95 | 96 | STATE.LOOKING_FOR_INT_ARG_END -> { 97 | buffer[pos++] = data.toByte() 98 | 99 | if (!('0'.code <= data && data <= '9'.code)) { 100 | val strValue = String(buffer, startOfValue, pos - 1 - startOfValue, CHARSET) 101 | val value = strValue.toInt() 102 | options.add(value) 103 | 104 | if (data == ';'.code) { 105 | state = STATE.LOOKING_FOR_NEXT_ARG 106 | } 107 | else { 108 | reset(processEscapeCommand(options, data.toChar())) 109 | } 110 | } 111 | } 112 | 113 | STATE.LOOKING_FOR_STR_ARG_END -> { 114 | buffer[pos++] = data.toByte() 115 | 116 | if ('"'.code != data) { 117 | val value = String(buffer, startOfValue, pos - 1 - startOfValue, CHARSET) 118 | options.add(value) 119 | 120 | if (data == ';'.code) { 121 | state = STATE.LOOKING_FOR_NEXT_ARG 122 | } 123 | else { 124 | reset(processEscapeCommand(options, data.toChar())) 125 | } 126 | } 127 | } 128 | 129 | STATE.LOOKING_FOR_OSC_COMMAND -> { 130 | buffer[pos++] = data.toByte() 131 | 132 | if ('0'.code <= data && data <= '9'.code) { 133 | startOfValue = pos - 1 134 | state = STATE.LOOKING_FOR_OSC_COMMAND_END 135 | } 136 | else { 137 | reset(false) 138 | } 139 | } 140 | 141 | STATE.LOOKING_FOR_OSC_COMMAND_END -> { 142 | buffer[pos++] = data.toByte() 143 | 144 | if (';'.code == data) { 145 | val strValue = String(buffer, startOfValue, pos - 1 - startOfValue, CHARSET) 146 | val value = strValue.toInt() 147 | options.add(value) 148 | startOfValue = pos 149 | state = STATE.LOOKING_FOR_OSC_PARAM 150 | } 151 | else if ('0'.code <= data && data <= '9'.code) { 152 | // already pushed digit to buffer, just keep looking 153 | } 154 | else { 155 | // oops, did not expect this 156 | reset(false) 157 | } 158 | } 159 | 160 | STATE.LOOKING_FOR_OSC_PARAM -> { 161 | buffer[pos++] = data.toByte() 162 | 163 | if (BEL == data) { 164 | val value = String(buffer, startOfValue, pos - 1 - startOfValue, CHARSET) 165 | options.add(value) 166 | reset(processOperatingSystemCommand(options)) 167 | } 168 | else if (FIRST_ESC_CHAR == data) { 169 | state = STATE.LOOKING_FOR_ST 170 | } 171 | else { 172 | // just keep looking while adding text 173 | } 174 | } 175 | 176 | STATE.LOOKING_FOR_ST -> { 177 | buffer[pos++] = data.toByte() 178 | 179 | if (SECOND_ST_CHAR == data) { 180 | val value = String(buffer, startOfValue, pos - 2 - startOfValue, CHARSET) 181 | options.add(value) 182 | reset(processOperatingSystemCommand(options)) 183 | } 184 | else { 185 | state = STATE.LOOKING_FOR_OSC_PARAM 186 | } 187 | } 188 | } 189 | 190 | // Is it just too long? 191 | if (pos >= buffer.size) { 192 | reset(false) 193 | } 194 | } 195 | 196 | /** 197 | * Resets all state to continue with regular parsing 198 | * 199 | * @param skipBuffer if current buffer should be skipped or written to out 200 | * @throws IOException 201 | */ 202 | @Throws(IOException::class) 203 | private fun reset(skipBuffer: Boolean) { 204 | if (!skipBuffer) { 205 | out.write(buffer, 0, pos) 206 | } 207 | pos = 0 208 | startOfValue = 0 209 | options.clear() 210 | state = STATE.LOOKING_FOR_FIRST_ESC_CHAR 211 | } 212 | 213 | /** 214 | * @return true if the escape command was processed. 215 | */ 216 | @Throws(IOException::class) 217 | private fun processEscapeCommand(options: ArrayList, command: Char): Boolean { 218 | try { 219 | return when (command) { 220 | CURSOR_UP -> { 221 | processCursorUp(optionInt(options, 0, 1)) 222 | true 223 | } 224 | 225 | CURSOR_DOWN -> { 226 | processCursorDown(optionInt(options, 0, 1)) 227 | true 228 | } 229 | 230 | CURSOR_FORWARD -> { 231 | processCursorRight(optionInt(options, 0, 1)) 232 | true 233 | } 234 | 235 | CURSOR_BACK -> { 236 | processCursorLeft(optionInt(options, 0, 1)) 237 | true 238 | } 239 | 240 | CURSOR_DOWN_LINE -> { 241 | processCursorDownLine(optionInt(options, 0, 1)) 242 | true 243 | } 244 | 245 | CURSOR_UP_LINE -> { 246 | processCursorUpLine(optionInt(options, 0, 1)) 247 | true 248 | } 249 | 250 | CURSOR_TO_COL -> { 251 | processCursorToColumn(optionInt(options, 0)) 252 | true 253 | } 254 | 255 | CURSOR_POS, CURSOR_POS_ALT -> { 256 | processCursorTo(optionInt(options, 0, 1), optionInt(options, 1, 1)) 257 | true 258 | } 259 | 260 | CURSOR_ERASE_SCREEN -> { 261 | processEraseScreen(optionInt(options, 0, 0)) 262 | true 263 | } 264 | 265 | CURSOR_ERASE_LINE -> { 266 | processEraseLine(optionInt(options, 0, 0)) 267 | true 268 | } 269 | 270 | SCROLL_UP -> { 271 | processScrollUp(optionInt(options, 0, 1)) 272 | true 273 | } 274 | 275 | SCROLL_DOWN -> { 276 | processScrollDown(optionInt(options, 0, 1)) 277 | true 278 | } 279 | 280 | TEXT_ATTRIBUTE -> { 281 | var count = 0 282 | for (next in options) { 283 | if (next != null) { 284 | count++ 285 | 286 | // will throw a ClassCast exception IF NOT an int. 287 | val value = next as Int 288 | when (value) { 289 | in 30..37 -> { 290 | // foreground 291 | processSetForegroundColor(value - 30) 292 | } 293 | in 40..47 -> { 294 | // background 295 | processSetBackgroundColor(value - 40) 296 | } 297 | else -> { 298 | when (value) { 299 | ATTRIBUTE_DEFAULT_FG -> processDefaultTextColor() 300 | ATTRIBUTE_DEFAULT_BG -> processDefaultBackgroundColor() 301 | ATTRIBUTE_RESET -> processAttributeReset() 302 | else -> processSetAttribute(value) 303 | } 304 | } 305 | } 306 | } 307 | } 308 | if (count == 0) { 309 | processAttributeReset() 310 | } 311 | true 312 | } 313 | 314 | SAVE_CURSOR_POS -> { 315 | processSaveCursorPosition() 316 | true 317 | } 318 | 319 | RESTORE_CURSOR_POS -> { 320 | processRestoreCursorPosition() 321 | true 322 | } 323 | 324 | else -> { 325 | if (command in 'a'..'z') { 326 | processUnknownExtension(options, command) 327 | return true 328 | } 329 | if (command in 'A'..'Z') { 330 | processUnknownExtension(options, command) 331 | return true 332 | } 333 | false 334 | } 335 | } 336 | } 337 | catch (ignore: IllegalArgumentException) { 338 | } 339 | return false 340 | } 341 | 342 | /** 343 | * @return true if the operating system command was processed. 344 | */ 345 | @Throws(IOException::class) 346 | private fun processOperatingSystemCommand(options: ArrayList): Boolean { 347 | val command = optionInt(options, 0) 348 | val label = options[1] as String? 349 | 350 | // for command > 2 label could be composed (i.e. contain ';'), but we'll leave 351 | // it to processUnknownOperatingSystemCommand implementations to handle that 352 | try { 353 | return when (command) { 354 | else -> { 355 | // not exactly unknown, but not supported through dedicated process methods 356 | processUnknownOperatingSystemCommand(command, label) 357 | true 358 | } 359 | } 360 | } 361 | catch (ignore: IllegalArgumentException) { 362 | } 363 | 364 | return false 365 | } 366 | 367 | @Throws(IOException::class) 368 | protected open fun processRestoreCursorPosition() { 369 | } 370 | 371 | @Throws(IOException::class) 372 | protected open fun processSaveCursorPosition() { 373 | } 374 | 375 | @Throws(IOException::class) 376 | protected open fun processScrollDown(count: Int) { 377 | } 378 | 379 | @Throws(IOException::class) 380 | protected open fun processScrollUp(count: Int) { 381 | } 382 | 383 | @Throws(IOException::class) 384 | protected open fun processEraseScreen(eraseOption: Int) { 385 | } 386 | 387 | @Throws(IOException::class) 388 | protected open fun processEraseLine(eraseOption: Int) { 389 | } 390 | 391 | @Throws(IOException::class) 392 | protected open fun processSetAttribute(attribute: Int) { 393 | } 394 | 395 | @Throws(IOException::class) 396 | protected open fun processSetForegroundColor(color: Int) { 397 | } 398 | 399 | @Throws(IOException::class) 400 | protected open fun processSetBackgroundColor(color: Int) { 401 | } 402 | 403 | @Throws(IOException::class) 404 | protected open fun processDefaultTextColor() { 405 | } 406 | 407 | @Throws(IOException::class) 408 | protected open fun processDefaultBackgroundColor() { 409 | } 410 | 411 | @Throws(IOException::class) 412 | protected open fun processAttributeReset() { 413 | } 414 | 415 | @Throws(IOException::class) 416 | protected open fun processCursorTo(row: Int, col: Int) { 417 | } 418 | 419 | @Throws(IOException::class) 420 | protected open fun processCursorToColumn(x: Int) { 421 | } 422 | 423 | @Throws(IOException::class) 424 | protected open fun processCursorUpLine(count: Int) { 425 | } 426 | 427 | @Throws(IOException::class) 428 | protected open fun processCursorDownLine(count: Int) { 429 | } 430 | 431 | @Throws(IOException::class) 432 | protected open fun processCursorLeft(count: Int) { 433 | } 434 | 435 | @Throws(IOException::class) 436 | protected open fun processCursorRight(count: Int) { 437 | } 438 | 439 | @Throws(IOException::class) 440 | protected open fun processCursorDown(count: Int) { 441 | } 442 | 443 | @Throws(IOException::class) 444 | protected open fun processCursorUp(count: Int) { 445 | } 446 | 447 | protected fun processUnknownExtension(options: ArrayList, command: Char) {} 448 | protected fun processUnknownOperatingSystemCommand(command: Int, param: String?) {} 449 | 450 | private fun optionInt(options: ArrayList, index: Int): Int { 451 | require(options.size > index) 452 | val value = options[index] ?: throw IllegalArgumentException() 453 | 454 | require(value.javaClass == Int::class.java) 455 | return value as Int 456 | } 457 | 458 | private fun optionInt(options: ArrayList, index: Int, defaultValue: Int): Int { 459 | if (options.size > index) { 460 | val value = options[index] ?: return defaultValue 461 | return value as Int 462 | } 463 | return defaultValue 464 | } 465 | 466 | @Throws(IOException::class) 467 | override fun close() { 468 | flush() 469 | super.close() 470 | } 471 | 472 | companion object { 473 | private val CHARSET = Charset.forName("UTF-8") 474 | 475 | const val BLACK = 0 476 | const val RED = 1 477 | const val GREEN = 2 478 | const val YELLOW = 3 479 | const val BLUE = 4 480 | const val MAGENTA = 5 481 | const val CYAN = 6 482 | const val WHITE = 7 483 | 484 | // Moves the cursor n (default 1) cells in the given direction. If the cursor is already at the edge of the screen, this has no effect. 485 | const val CURSOR_UP = 'A' 486 | const val CURSOR_DOWN = 'B' 487 | const val CURSOR_FORWARD = 'C' 488 | const val CURSOR_BACK = 'D' 489 | const val CURSOR_DOWN_LINE = 'E' // Moves cursor to beginning of the line n (default 1) lines down. 490 | const val CURSOR_UP_LINE = 'F' // Moves cursor to beginning of the line n (default 1) lines up. 491 | const val CURSOR_TO_COL = 'G' // Moves the cursor to column n (default 1). 492 | 493 | // Moves the cursor to row n, column m. The values are 1-based, and default to 1 (top left corner) if omitted. 494 | const val CURSOR_POS = 'H' 495 | 496 | // Moves the cursor to row n, column m. Both default to 1 if omitted. Same as CUP 497 | const val CURSOR_POS_ALT = 'f' 498 | 499 | // Clears part of the screen. If n is 0 (or missing), clear from cursor to end of screen. If n is 1, clear from cursor to beginning of the screen. If n is 2, clear entire screen (and moves cursor to upper left on DOS ANSI.SYS). 500 | const val CURSOR_ERASE_SCREEN = 'J' 501 | 502 | // Erases part of the line. If n is zero (or missing), clear from cursor to the end of the line. If n is one, clear from cursor to beginning of the line. If n is two, clear entire line. Cursor position does not change. 503 | const val CURSOR_ERASE_LINE = 'K' 504 | 505 | const val SCROLL_UP = 'S' // Scroll whole page up by n (default 1) lines. New lines are added at the bottom. (not ANSI.SYS) 506 | const val SCROLL_DOWN = 'T' // Scroll whole page down by n (default 1) lines. New lines are added at the top. (not ANSI.SYS) 507 | const val SAVE_CURSOR_POS = 's' // Saves the cursor position. 508 | const val RESTORE_CURSOR_POS = 'u' // Restores the cursor position. 509 | 510 | // Sets SGR parameters, including text color. After CSI can be zero or more parameters separated with ;. With no parameters, CSI m is treated as CSI 0 m (reset / normal), which is typical of most of the ANSI escape sequences. 511 | const val TEXT_ATTRIBUTE = 'm' 512 | 513 | const val ATTRIBUTE_RESET = 0 // Reset / Normal - all attributes off 514 | const val ATTRIBUTE_BOLD = 1 // Intensity: Bold 515 | const val ATTRIBUTE_FAINT = 2 // Intensity; Faint (not widely supported) 516 | const val ATTRIBUTE_ITALIC = 3 // Italic; (on not widely supported. Sometimes treated as inverse) 517 | const val ATTRIBUTE_UNDERLINE = 4 // Underline; Single 518 | const val ATTRIBUTE_BLINK_SLOW = 5 // Blink; Slow less than 150 per minute 519 | const val ATTRIBUTE_BLINK_FAST = 6 // Blink; Rapid 150 per minute or more 520 | const val ATTRIBUTE_NEGATIVE_ON = 7 // Negative inverse or reverse; swap foreground and background 521 | const val ATTRIBUTE_CONCEAL_ON = 8 // Conceal on 522 | const val ATTRIBUTE_STRIKETHROUGH_ON = 9 // Crossed-out 523 | const val ATTRIBUTE_UNDERLINE_DOUBLE = 21 // Underline; Double not widely supported 524 | const val ATTRIBUTE_NORMAL = 22 // Intensity; Normal not bold and not faint 525 | const val ATTRIBUTE_ITALIC_OFF = 23 // Not italic 526 | const val ATTRIBUTE_UNDERLINE_OFF = 24 // Underline; None 527 | const val ATTRIBUTE_BLINK_OFF = 25 // Blink; off 528 | const val ATTRIBUTE_NEGATIVE_OFF = 27 // Image; Positive 529 | const val ATTRIBUTE_CONCEAL_OFF = 28 // Reveal conceal off 530 | const val ATTRIBUTE_STRIKETHROUGH_OFF = 29 // Not crossed out 531 | const val ATTRIBUTE_DEFAULT_FG = 39 // Default text color (foreground) 532 | const val ATTRIBUTE_DEFAULT_BG = 49 // Default background color 533 | 534 | // for Erase Screen/Line 535 | const val ERASE_TO_END = 0 536 | const val ERASE_TO_BEGINNING = 1 537 | const val ERASE_ALL = 2 538 | 539 | private const val MAX_ESCAPE_SEQUENCE_LENGTH = 100 540 | 541 | internal enum class STATE { 542 | LOOKING_FOR_FIRST_ESC_CHAR, 543 | LOOKING_FOR_SECOND_ESC_CHAR, 544 | LOOKING_FOR_NEXT_ARG, 545 | LOOKING_FOR_STR_ARG_END, 546 | LOOKING_FOR_INT_ARG_END, 547 | LOOKING_FOR_OSC_COMMAND, 548 | LOOKING_FOR_OSC_COMMAND_END, 549 | LOOKING_FOR_OSC_PARAM, 550 | LOOKING_FOR_ST 551 | } 552 | 553 | 554 | private const val FIRST_ESC_CHAR = 27 555 | private const val SECOND_ESC_CHAR = '['.code 556 | private const val SECOND_OSC_CHAR = ']'.code 557 | private const val BEL = 7 558 | private const val SECOND_ST_CHAR = '\\'.code 559 | 560 | val RESET_CODE = StringBuilder(3).append(FIRST_ESC_CHAR.toChar()).append(SECOND_ESC_CHAR.toChar()).append(TEXT_ATTRIBUTE).toString() 561 | .toByteArray(CHARSET) 562 | } 563 | } 564 | -------------------------------------------------------------------------------- /src/dorkbox/console/output/AnsiRenderWriter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 dorkbox, llc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /* 18 | * Copyright (C) 2009 the original author(s). 19 | * 20 | * Licensed under the Apache License, Version 2.0 (the "License"); 21 | * you may not use this file except in compliance with the License. 22 | * You may obtain a copy of the License at 23 | * 24 | * http://www.apache.org/licenses/LICENSE-2.0 25 | * 26 | * Unless required by applicable law or agreed to in writing, software 27 | * distributed under the License is distributed on an "AS IS" BASIS, 28 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 29 | * See the License for the specific language governing permissions and 30 | * limitations under the License. 31 | */ 32 | package dorkbox.console.output 33 | 34 | import java.io.OutputStream 35 | import java.io.PrintWriter 36 | import java.io.Writer 37 | import java.util.* 38 | 39 | /** 40 | * Print writer which supports automatic ANSI color rendering via [AnsiRenderer]. 41 | * 42 | * @author [Jason Dillon](mailto:jason@planet57.com) 43 | * @author [Hiram Chirino](http://hiramchirino.com) 44 | */ 45 | class AnsiRenderWriter : PrintWriter { 46 | constructor(out: OutputStream) : super(out) 47 | constructor(out: OutputStream, autoFlush: Boolean) : super(out, autoFlush) 48 | constructor(out: Writer) : super(out) 49 | constructor(out: Writer, autoFlush: Boolean) : super(out, autoFlush) 50 | 51 | override fun write(s: String) { 52 | if (s.contains(AnsiRenderer.BEGIN_TOKEN)) { 53 | super.write(AnsiRenderer.render(s)) 54 | } 55 | else { 56 | super.write(s) 57 | } 58 | } 59 | 60 | override fun format(format: String, vararg args: Any): PrintWriter { 61 | flush() // prevents partial output from being written while formatting or we will get rendering exceptions 62 | print(String.format(format, *args)) 63 | return this 64 | } 65 | 66 | override fun format(l: Locale, format: String, vararg args: Any): PrintWriter { 67 | flush() // prevents partial output from being written while formatting or we will get rendering exceptions 68 | print(String.format(l, format, *args)) 69 | return this 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/dorkbox/console/output/AnsiRenderer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 dorkbox, llc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package dorkbox.console.output 17 | 18 | import dorkbox.propertyLoader.Property 19 | 20 | /** 21 | * Renders ANSI color escape-codes in strings by parsing out some special syntax to pick up the correct fluff to use. 22 | * 23 | * 24 | * The syntax for embedded ANSI codes is: 25 | * 26 | * ``` 27 | * @|*code*(,*code*)* *text*|@ 28 | * 29 | * Examples: 30 | * 31 | * @|bold Hello|@ 32 | * @|bold,red Warning!|@ 33 | * ``` 34 | * For Colors, FG_x and BG_x are supported, as are BRIGHT_x (and consequently, FG_BRIGHT_x) 35 | * 36 | * @author dorkbox, llc 37 | * @author [Jason Dillon](mailto:jason@planet57.com) 38 | * @author [Hiram Chirino](http://hiramchirino.com) 39 | */ 40 | object AnsiRenderer { 41 | private val regex = "\\s".toRegex() 42 | 43 | @Property(description = "Sets the begin-token used for creating ANSI streams") 44 | var BEGIN_TOKEN = "@|" 45 | 46 | @Property(description = "Sets the end-token used for creating ANSI streams") 47 | var END_TOKEN = "|@" 48 | 49 | const val CODE_LIST_SEPARATOR = "," 50 | const val CODE_TEXT_SEPARATOR = " " 51 | 52 | private val regex1 = CODE_TEXT_SEPARATOR.toRegex() 53 | private val regex2 = CODE_LIST_SEPARATOR.toRegex() 54 | 55 | private const val BEGIN_TOKEN_LEN = 2 56 | private const val END_TOKEN_LEN = 2 57 | 58 | private val codeMap = mutableMapOf() 59 | 60 | fun reg(anEnum: Enum<*>, codeName: String, isBackgroundColor: Boolean = false) { 61 | codeMap[codeName] = AnsiCodeMap(anEnum, isBackgroundColor) 62 | } 63 | 64 | /** 65 | * Renders [AnsiCodeMap] names on the given Ansi. 66 | * 67 | * @param ansi The Ansi to render upon 68 | * @param codeNames The code names to render 69 | */ 70 | fun render(ansi: Ansi, vararg codeNames: String): Ansi { 71 | for (codeName in codeNames) { 72 | render(ansi, codeName) 73 | } 74 | return ansi 75 | } 76 | 77 | /** 78 | * Renders a [AnsiCodeMap] name on the given Ansi. 79 | * 80 | * @param ansi The Ansi to render upon 81 | * @param codeName The code name to render 82 | */ 83 | fun render(ansi: Ansi, codeName: String): Ansi { 84 | val ansiCodeMap = codeMap[codeName.uppercase()] ?: error("Invalid ANSI code name: '$codeName'") 85 | 86 | if (ansiCodeMap.isColor) { 87 | if (ansiCodeMap.isBackgroundColor) { 88 | ansi.bg(ansiCodeMap.color) 89 | } 90 | else { 91 | ansi.fg(ansiCodeMap.color) 92 | } 93 | } 94 | else if (ansiCodeMap.isAttribute) { 95 | ansi.a(ansiCodeMap.attribute) 96 | } 97 | 98 | return ansi 99 | } 100 | 101 | /** 102 | * Renders text using the [AnsiCodeMap] names. 103 | * 104 | * @param text The text to render 105 | * @param codeNames The code names to render 106 | */ 107 | fun render(text: String, vararg codeNames: String): String { 108 | val ansi: Ansi = render(Ansi.ansi(), *codeNames) 109 | return ansi.a(text).reset().toString() 110 | } 111 | 112 | @Throws(IllegalArgumentException::class) 113 | fun render(input: String): String { 114 | val buff = StringBuilder() 115 | var i = 0 116 | var j: Int 117 | var k: Int 118 | while (true) { 119 | j = input.indexOf(BEGIN_TOKEN, i) 120 | if (j == -1) { 121 | return if (i == 0) { 122 | input 123 | } 124 | else { 125 | buff.append(input.substring(i, input.length)) 126 | buff.toString() 127 | } 128 | } 129 | else { 130 | buff.append(input.substring(i, j)) 131 | k = input.indexOf(END_TOKEN, j) 132 | if (k == -1) { 133 | return input 134 | } 135 | else { 136 | j += BEGIN_TOKEN_LEN 137 | val spec = input.substring(j, k) 138 | 139 | val items = spec.split(regex1, limit = 2).toTypedArray() 140 | if (items.size == 1) { 141 | return input 142 | } 143 | 144 | val replacement = 145 | render(items[1], *items[0].split(regex2).dropLastWhile { it.isEmpty() }.toTypedArray()) 146 | buff.append(replacement) 147 | i = k + END_TOKEN_LEN 148 | } 149 | } 150 | } 151 | } 152 | 153 | /** 154 | * Renders [AnsiCodeMap] names as an ANSI escape string. 155 | * 156 | * @param codeNames The code names to render 157 | * 158 | * @return an ANSI escape string. 159 | */ 160 | fun renderCodeNames(codeNames: String): String { 161 | return render(Ansi(), *codeNames.split(regex).dropLastWhile { it.isEmpty() }.toTypedArray()).toString() 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/dorkbox/console/output/AnsiString.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 dorkbox, llc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /* 18 | * Copyright (C) 2009 the original author(s). 19 | * 20 | * Licensed under the Apache License, Version 2.0 (the "License"); 21 | * you may not use this file except in compliance with the License. 22 | * You may obtain a copy of the License at 23 | * 24 | * http://www.apache.org/licenses/LICENSE-2.0 25 | * 26 | * Unless required by applicable law or agreed to in writing, software 27 | * distributed under the License is distributed on an "AS IS" BASIS, 28 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 29 | * See the License for the specific language governing permissions and 30 | * limitations under the License. 31 | */ 32 | package dorkbox.console.output 33 | 34 | import java.io.ByteArrayOutputStream 35 | import java.io.IOException 36 | import java.nio.charset.Charset 37 | 38 | /** 39 | * An ANSI string which reports the size of rendered text correctly (ignoring any ANSI escapes). 40 | * 41 | * @author [Jason Dillon](mailto:jason@planet57.com) 42 | */ 43 | class AnsiString(str: CharSequence) : CharSequence { 44 | companion object { 45 | private val CHARSET = Charset.forName("UTF-8") 46 | } 47 | 48 | val encoded: CharSequence 49 | val plain: CharSequence 50 | 51 | @Volatile 52 | private var toStringCalled = false 53 | 54 | init { 55 | encoded = str 56 | plain = chew(str) 57 | } 58 | 59 | private fun chew(str: CharSequence): CharSequence { 60 | val buff = ByteArrayOutputStream() 61 | val out = AnsiOutputStream(buff) 62 | 63 | try { 64 | out.write( 65 | str.toString().toByteArray(CHARSET) 66 | ) 67 | out.flush() 68 | out.close() 69 | } 70 | catch (e: IOException) { 71 | throw RuntimeException(e) 72 | } 73 | 74 | return String(buff.toByteArray()) 75 | } 76 | 77 | override val length: Int 78 | get() { 79 | return plain.length 80 | } 81 | 82 | override fun get(index: Int): Char { 83 | // toString() must be called first to get expected results 84 | if (!toStringCalled) { 85 | toStringCalled = true 86 | encoded.toString() 87 | } 88 | return encoded[index] 89 | } 90 | 91 | override fun subSequence(startIndex: Int, endIndex: Int): CharSequence { 92 | // toString() must be called first to get expected results 93 | if (!toStringCalled) { 94 | toStringCalled = true 95 | encoded.toString() 96 | } 97 | return encoded.subSequence(startIndex, endIndex) 98 | } 99 | 100 | override fun hashCode(): Int { 101 | return encoded.hashCode() 102 | } 103 | 104 | override fun equals(other: Any?): Boolean { 105 | return encoded == other 106 | } 107 | 108 | override fun toString(): String { 109 | return encoded.toString() 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/dorkbox/console/output/Attribute.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 dorkbox, llc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package dorkbox.console.output 17 | 18 | enum class Attribute(val value: Int, 19 | private val namePriv: String) { 20 | 21 | RESET(AnsiOutputStream.ATTRIBUTE_RESET, "RESET"), 22 | 23 | BOLD(AnsiOutputStream.ATTRIBUTE_BOLD, "BOLD"), 24 | BOLD_OFF(AnsiOutputStream.ATTRIBUTE_NORMAL, "BOLD_OFF"), 25 | 26 | FAINT(AnsiOutputStream.ATTRIBUTE_FAINT, "FAINT"), 27 | FAINT_OFF(AnsiOutputStream.ATTRIBUTE_NORMAL, "FAINT_OFF"), 28 | 29 | ITALIC(AnsiOutputStream.ATTRIBUTE_ITALIC, "ITALIC"), 30 | ITALIC_OFF(AnsiOutputStream.ATTRIBUTE_ITALIC_OFF, "ITALIC_OFF"), 31 | 32 | UNDERLINE(AnsiOutputStream.ATTRIBUTE_UNDERLINE, "UNDERLINE"), 33 | UNDERLINE_DOUBLE(AnsiOutputStream.ATTRIBUTE_UNDERLINE_DOUBLE, "UNDERLINE_DOUBLE"), 34 | UNDERLINE_OFF(AnsiOutputStream.ATTRIBUTE_UNDERLINE_OFF, "UNDERLINE_OFF"), 35 | 36 | BLINK_SLOW(AnsiOutputStream.ATTRIBUTE_BLINK_SLOW, "BLINK_SLOW"), 37 | BLINK_FAST(AnsiOutputStream.ATTRIBUTE_BLINK_FAST, "BLINK_FAST"), 38 | BLINK_OFF(AnsiOutputStream.ATTRIBUTE_BLINK_OFF, "BLINK_OFF"), 39 | 40 | NEGATIVE(AnsiOutputStream.ATTRIBUTE_NEGATIVE_ON, "NEGATIVE"), 41 | NEGATIVE_OFF(AnsiOutputStream.ATTRIBUTE_NEGATIVE_OFF, "NEGATIVE_OFF"), 42 | 43 | CONCEAL(AnsiOutputStream.ATTRIBUTE_CONCEAL_ON, "CONCEAL"), 44 | CONCEAL_OFF(AnsiOutputStream.ATTRIBUTE_CONCEAL_OFF, "CONCEAL_OFF"), 45 | 46 | STRIKETHROUGH(AnsiOutputStream.ATTRIBUTE_STRIKETHROUGH_ON, "STRIKETHROUGH"), 47 | STRIKETHROUGH_OFF(AnsiOutputStream.ATTRIBUTE_STRIKETHROUGH_OFF, "STRIKETHROUGH_OFF"); 48 | 49 | init { 50 | // register code names with the ANSI renderer 51 | AnsiRenderer.reg(this, namePriv) 52 | } 53 | 54 | override fun toString(): String { 55 | return namePriv 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/dorkbox/console/output/Color.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 dorkbox, llc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package dorkbox.console.output 17 | 18 | enum class Color( 19 | private val value: Int, 20 | /** is this a BRIGHT color or NORMAL color? */ 21 | val isNormal: Boolean, 22 | private val namePriv: String 23 | ) { 24 | BLACK(AnsiOutputStream.BLACK, true, "BLACK"), 25 | RED(AnsiOutputStream.RED, true, "RED"), 26 | GREEN(AnsiOutputStream.GREEN, true, "GREEN"), 27 | YELLOW(AnsiOutputStream.YELLOW, true, "YELLOW"), 28 | BLUE(AnsiOutputStream.BLUE, true, "BLUE"), 29 | MAGENTA(AnsiOutputStream.MAGENTA, true, "MAGENTA"), 30 | CYAN(AnsiOutputStream.CYAN, true, "CYAN"), 31 | WHITE(AnsiOutputStream.WHITE, true, "WHITE"), 32 | 33 | // Brighter versions of those colors, ie: BRIGHT_BLACK is gray. 34 | BRIGHT_BLACK(AnsiOutputStream.BLACK, false, "BRIGHT_BLACK"), 35 | BRIGHT_RED(AnsiOutputStream.RED, false, "BRIGHT_RED"), 36 | BRIGHT_GREEN(AnsiOutputStream.GREEN, false, "BRIGHT_GREEN"), 37 | BRIGHT_YELLOW(AnsiOutputStream.YELLOW, false, "BRIGHT_YELLOW"), 38 | BRIGHT_BLUE(AnsiOutputStream.BLUE, false, "BRIGHT_BLUE"), 39 | BRIGHT_MAGENTA(AnsiOutputStream.MAGENTA, false, "BRIGHT_MAGENTA"), 40 | BRIGHT_CYAN(AnsiOutputStream.CYAN, false, "BRIGHT_CYAN"), 41 | BRIGHT_WHITE(AnsiOutputStream.WHITE, false, "BRIGHT_WHITE"), 42 | 43 | // SPECIAL use case here. This is intercepted (so the color doesn't matter) 44 | /** 45 | * DEFAULT is the color of console BEFORE any colors/settings are applied 46 | */ 47 | DEFAULT(AnsiOutputStream.WHITE, true, "DEFAULT"), 48 | 49 | /** 50 | * DEFAULT is the color of console BEFORE any colors/settings are applied 51 | */ 52 | BRIGHT_DEFAULT(AnsiOutputStream.WHITE, false, "BRIGHT_DEFAULT"); 53 | 54 | init { 55 | // register code names with the ANSI renderer 56 | AnsiRenderer.reg(this, namePriv, false) 57 | AnsiRenderer.reg(this, "FG_$namePriv", false) 58 | AnsiRenderer.reg(this, "BG_$namePriv", true) 59 | } 60 | 61 | override fun toString(): String { 62 | return namePriv 63 | } 64 | 65 | fun fg(): Int { 66 | return value + 30 67 | } 68 | 69 | fun bg(): Int { 70 | return value + 40 71 | } 72 | 73 | fun fgBright(): Int { 74 | return value + 90 75 | } 76 | 77 | fun bgBright(): Int { 78 | return value + 100 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/dorkbox/console/output/Erase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 dorkbox, llc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package dorkbox.console.output 17 | 18 | enum class Erase(private val value: Int, 19 | private val namePriv: String) { 20 | 21 | FORWARD(AnsiOutputStream.ERASE_TO_END, "FORWARD"), 22 | BACKWARD(AnsiOutputStream.ERASE_TO_BEGINNING, "BACKWARD"), 23 | ALL(AnsiOutputStream.ERASE_ALL, "ALL"); 24 | 25 | override fun toString(): String { 26 | return namePriv 27 | } 28 | 29 | fun value(): Int { 30 | return value 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/dorkbox/console/output/HtmlAnsiOutputStream.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 dorkbox, llc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package dorkbox.console.output 17 | 18 | import java.io.IOException 19 | import java.io.OutputStream 20 | 21 | /** 22 | * @author dorkbox, llc 23 | * @author [Daniel Doubrovkine](http://code.dblock.org) 24 | */ 25 | class HtmlAnsiOutputStream(os: OutputStream?) : AnsiOutputStream(os) { 26 | companion object { 27 | private val regex = " ".toRegex() 28 | 29 | private val ANSI_COLOR_MAP: Array 30 | 31 | init { 32 | @Suppress("UNCHECKED_CAST") 33 | ANSI_COLOR_MAP = arrayOfNulls(8) as Array 34 | ANSI_COLOR_MAP[BLACK] = "black" 35 | ANSI_COLOR_MAP[RED] = "red" 36 | ANSI_COLOR_MAP[GREEN] = "green" 37 | ANSI_COLOR_MAP[YELLOW] = "yellow" 38 | ANSI_COLOR_MAP[BLUE] = "blue" 39 | ANSI_COLOR_MAP[MAGENTA] = "magenta" 40 | ANSI_COLOR_MAP[CYAN] = "cyan" 41 | ANSI_COLOR_MAP[WHITE] = "white" 42 | } 43 | 44 | private val BYTES_QUOT = """.toByteArray() 45 | private val BYTES_AMP = "&".toByteArray() 46 | private val BYTES_LT = "<".toByteArray() 47 | private val BYTES_GT = ">".toByteArray() 48 | } 49 | 50 | private var concealOn = false 51 | private val closingAttributes: MutableList = ArrayList() 52 | 53 | @Throws(IOException::class) 54 | private fun write(s: String) { 55 | super.out.write(s.toByteArray()) 56 | } 57 | 58 | @Throws(IOException::class) 59 | private fun writeAttribute(s: String) { 60 | write("<$s>") 61 | 62 | closingAttributes.add(0, s.split(regex, limit = 2).toTypedArray()[0]) 63 | } 64 | 65 | @Throws(IOException::class) 66 | private fun closeAttributes() { 67 | for (attr in closingAttributes) { 68 | write("") 69 | } 70 | closingAttributes.clear() 71 | } 72 | 73 | @Throws(IOException::class) 74 | override fun write(data: Int) { 75 | when (data) { 76 | 34 -> out.write(BYTES_QUOT) 77 | 38 -> out.write(BYTES_AMP) 78 | 60 -> out.write(BYTES_LT) 79 | 62 -> out.write(BYTES_GT) 80 | else -> super.write(data) 81 | } 82 | } 83 | 84 | @Throws(IOException::class) 85 | override fun processSetAttribute(attribute: Int) { 86 | when (attribute) { 87 | ATTRIBUTE_CONCEAL_ON -> { 88 | write("\u001B[8m") 89 | concealOn = true 90 | } 91 | 92 | ATTRIBUTE_BOLD -> writeAttribute("b") 93 | ATTRIBUTE_NORMAL -> closeAttributes() 94 | ATTRIBUTE_UNDERLINE -> writeAttribute("u") 95 | ATTRIBUTE_UNDERLINE_OFF -> closeAttributes() 96 | ATTRIBUTE_NEGATIVE_ON -> {} 97 | ATTRIBUTE_NEGATIVE_OFF -> {} 98 | } 99 | } 100 | 101 | @Throws(IOException::class) 102 | override fun processSetForegroundColor(color: Int) { 103 | writeAttribute("span style=\"color: ${ANSI_COLOR_MAP[color]};\"") 104 | } 105 | 106 | @Throws(IOException::class) 107 | override fun processSetBackgroundColor(color: Int) { 108 | writeAttribute("span style=\"background-color: ${ANSI_COLOR_MAP[color]};\"") 109 | } 110 | 111 | @Throws(IOException::class) 112 | override fun processAttributeReset() { 113 | if (concealOn) { 114 | write("\u001B[0m") 115 | concealOn = false 116 | } 117 | closeAttributes() 118 | } 119 | 120 | @Throws(IOException::class) 121 | override fun close() { 122 | closeAttributes() 123 | super.close() 124 | } 125 | 126 | @Throws(IOException::class) 127 | fun writeLine(buf: ByteArray, offset: Int, len: Int) { 128 | write(buf, offset, len) 129 | closeAttributes() 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/dorkbox/console/output/WindowsAnsiOutputStream.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 dorkbox, llc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package dorkbox.console.output 17 | 18 | import com.sun.jna.platform.win32.WinBase 19 | import com.sun.jna.platform.win32.WinNT 20 | import com.sun.jna.platform.win32.Wincon 21 | import com.sun.jna.ptr.IntByReference 22 | import dorkbox.jna.windows.Kernel32 23 | import dorkbox.jna.windows.structs.CONSOLE_SCREEN_BUFFER_INFO 24 | import dorkbox.jna.windows.structs.COORD 25 | import dorkbox.jna.windows.structs.SMALL_RECT 26 | import java.io.IOException 27 | import java.io.OutputStream 28 | import kotlin.experimental.and 29 | import kotlin.experimental.inv 30 | import kotlin.experimental.or 31 | import kotlin.math.max 32 | import kotlin.math.min 33 | 34 | /** 35 | * A Windows ANSI escape processor, uses JNA direct-mapping to access native platform API's to change the console attributes. 36 | * 37 | * See: https://en.wikipedia.org/wiki/ANSI_escape_code 38 | * 39 | * @author dorkbox, llc 40 | * @author [Hiram Chirino](http://hiramchirino.com) 41 | * @author Joris Kuipers 42 | */ 43 | class WindowsAnsiOutputStream internal constructor(os: OutputStream?, fHandle: Int) : AnsiOutputStream(os) { 44 | private val console: WinNT.HANDLE? 45 | private val originalInfo = CONSOLE_SCREEN_BUFFER_INFO() 46 | private val fileHandle: Int 47 | 48 | private val info = CONSOLE_SCREEN_BUFFER_INFO() 49 | 50 | @Volatile 51 | private var negative = false 52 | 53 | @Volatile 54 | private var savedX = (-1).toShort() 55 | 56 | @Volatile 57 | private var savedY = (-1).toShort() 58 | 59 | // reused vars 60 | private val written = IntByReference() 61 | 62 | init { 63 | fileHandle = if (fHandle == 1) { // STDOUT_FILENO 64 | Wincon.STD_OUTPUT_HANDLE 65 | } 66 | else if (fHandle == 2) { // STDERR_FILENO 67 | Wincon.STD_ERROR_HANDLE 68 | } 69 | else { 70 | throw IllegalArgumentException("Invalid file handle $fHandle") 71 | } 72 | 73 | 74 | console = Kernel32.GetStdHandle(fileHandle) 75 | if (console === WinBase.INVALID_HANDLE_VALUE) { 76 | throw IOException("Unable to get input console handle.") 77 | } 78 | out.flush() 79 | if (Kernel32.GetConsoleScreenBufferInfo(console, originalInfo) == 0) { 80 | throw IOException("Could not get the screen info") 81 | } 82 | } 83 | 84 | @get:Throws(IOException::class) 85 | private val consoleInfo: Unit 86 | get() { 87 | out.flush() 88 | Kernel32.ASSERT(Kernel32.GetConsoleScreenBufferInfo(console, info), "Could not get the screen info:") 89 | } 90 | 91 | @Throws(IOException::class) 92 | private fun applyAttributes() { 93 | out.flush() 94 | 95 | var attributes = info.attributes 96 | if (negative) { 97 | // Swap the the Foreground and Background bits. 98 | var fg = 0x000F and attributes.toInt() 99 | fg = fg shl 8 100 | var bg = 0X00F0 * attributes 101 | bg = bg shr 8 102 | attributes = (attributes.toInt() and 0xFF00 or fg or bg).toShort() 103 | } 104 | Kernel32.ASSERT(Kernel32.SetConsoleTextAttribute(console, attributes), "Could not set text attributes") 105 | } 106 | 107 | @Throws(IOException::class) 108 | private fun applyCursorPosition() { 109 | Kernel32.ASSERT(Kernel32.SetConsoleCursorPosition(console, info.cursorPosition.asValue()), "Could not set cursor position") 110 | } 111 | 112 | @Throws(IOException::class) 113 | override fun processRestoreCursorPosition() { 114 | // restore only if there was a save operation first 115 | if (savedX.toInt() != -1 && savedY.toInt() != -1) { 116 | out.flush() 117 | info.cursorPosition.x = savedX 118 | info.cursorPosition.y = savedY 119 | applyCursorPosition() 120 | } 121 | } 122 | 123 | @Throws(IOException::class) 124 | override fun processSaveCursorPosition() { 125 | consoleInfo 126 | savedX = info.cursorPosition.x 127 | savedY = info.cursorPosition.y 128 | } 129 | 130 | @Throws(IOException::class) 131 | override fun processEraseScreen(eraseOption: Int) { 132 | consoleInfo 133 | when (eraseOption) { 134 | ERASE_ALL -> { 135 | val topLeft = COORD() 136 | topLeft.x = 0.toShort() 137 | topLeft.y = info.window.top 138 | 139 | val screenLength = info.window.height() * info.size.x 140 | Kernel32.ASSERT( 141 | Kernel32.FillConsoleOutputAttribute( 142 | console, 143 | originalInfo.attributes, 144 | screenLength, 145 | topLeft.asValue(), 146 | written 147 | ), "Could not fill console" 148 | ) 149 | Kernel32.ASSERT( 150 | Kernel32.FillConsoleOutputCharacter(console, ' ', screenLength, topLeft.asValue(), written), 151 | "Could not fill console" 152 | ) 153 | } 154 | 155 | ERASE_TO_BEGINNING -> { 156 | val topLeft2 = COORD() 157 | topLeft2.x = 0.toShort() 158 | topLeft2.y = info.window.top 159 | 160 | val lengthToCursor = (info.cursorPosition.y - info.window.top) * info.size.x + info.cursorPosition.x 161 | Kernel32.ASSERT( 162 | Kernel32.FillConsoleOutputAttribute( 163 | console, 164 | originalInfo.attributes, 165 | lengthToCursor, 166 | topLeft2.asValue(), 167 | written 168 | ), "Could not fill console" 169 | ) 170 | Kernel32.ASSERT( 171 | Kernel32.FillConsoleOutputCharacter(console, ' ', lengthToCursor, topLeft2.asValue(), written), 172 | "Could not fill console" 173 | ) 174 | } 175 | 176 | ERASE_TO_END -> { 177 | val lengthToEnd = (info.window.bottom - info.cursorPosition.y) * info.size.x + info.size.x - info.cursorPosition.x 178 | Kernel32.ASSERT( 179 | Kernel32.FillConsoleOutputAttribute( 180 | console, 181 | originalInfo.attributes, 182 | lengthToEnd, 183 | info.cursorPosition.asValue(), 184 | written 185 | ), "Could not fill console" 186 | ) 187 | Kernel32.ASSERT( 188 | Kernel32.FillConsoleOutputCharacter(console, ' ', lengthToEnd, info.cursorPosition.asValue(), written), 189 | "Could not fill console" 190 | ) 191 | } 192 | } 193 | } 194 | 195 | @Throws(IOException::class) 196 | override fun processEraseLine(eraseOption: Int) { 197 | consoleInfo 198 | when (eraseOption) { 199 | ERASE_ALL -> { 200 | val currentRow: COORD = info.cursorPosition.asValue() 201 | currentRow.x = 0.toShort() 202 | 203 | Kernel32.ASSERT( 204 | Kernel32.FillConsoleOutputAttribute( 205 | console, 206 | originalInfo.attributes, 207 | info.size.x.toInt(), 208 | currentRow.asValue(), 209 | written 210 | ), "Could not fill console" 211 | ) 212 | Kernel32.ASSERT( 213 | Kernel32.FillConsoleOutputCharacter(console, ' ', info.size.x.toInt(), currentRow.asValue(), written), 214 | "Could not fill console" 215 | ) 216 | } 217 | 218 | ERASE_TO_BEGINNING -> { 219 | val leftColCurrRow2: COORD = info.cursorPosition.asValue() 220 | leftColCurrRow2.x = 0.toShort() 221 | 222 | Kernel32.ASSERT( 223 | Kernel32.FillConsoleOutputAttribute( 224 | console, 225 | originalInfo.attributes, 226 | info.cursorPosition.x.toInt(), 227 | leftColCurrRow2.asValue(), 228 | written 229 | ), "Could not fill console" 230 | ) 231 | Kernel32.ASSERT( 232 | Kernel32.FillConsoleOutputCharacter( 233 | console, 234 | ' ', 235 | info.cursorPosition.x.toInt(), 236 | leftColCurrRow2.asValue(), 237 | written 238 | ), "Could not fill console" 239 | ) 240 | } 241 | 242 | ERASE_TO_END -> { 243 | val lengthToLastCol = info.size.x - info.cursorPosition.x 244 | 245 | Kernel32.ASSERT( 246 | Kernel32.FillConsoleOutputAttribute( 247 | console, 248 | originalInfo.attributes, 249 | lengthToLastCol, 250 | info.cursorPosition.asValue(), 251 | written 252 | ), "Could not fill console" 253 | ) 254 | Kernel32.ASSERT( 255 | Kernel32.FillConsoleOutputCharacter(console, ' ', lengthToLastCol, info.cursorPosition.asValue(), written), 256 | "Could not fill console" 257 | ) 258 | } 259 | } 260 | } 261 | 262 | @Throws(IOException::class) 263 | override fun processSetAttribute(attribute: Int) { 264 | if (90 <= attribute && attribute <= 97) { 265 | // foreground bright 266 | info.attributes = (info.attributes.toInt() and 0x000F.inv() or ANSI_FOREGROUND_COLOR_MAP[attribute - 90].toInt()).toShort() 267 | info.attributes = (info.attributes or Kernel32.FOREGROUND_INTENSITY) 268 | applyAttributes() 269 | return 270 | } 271 | else if (100 <= attribute && attribute <= 107) { 272 | // background bright 273 | info.attributes = (info.attributes.toInt() and 0x00F0.inv() or ANSI_BACKGROUND_COLOR_MAP[attribute - 100].toInt()).toShort() 274 | info.attributes = (info.attributes or Kernel32.BACKGROUND_INTENSITY) 275 | applyAttributes() 276 | return 277 | } 278 | 279 | when (attribute) { 280 | ATTRIBUTE_BOLD -> { 281 | info.attributes = (info.attributes or Kernel32.FOREGROUND_INTENSITY) 282 | applyAttributes() 283 | } 284 | 285 | ATTRIBUTE_NORMAL -> { 286 | info.attributes = (info.attributes and Kernel32.FOREGROUND_INTENSITY.inv()) 287 | applyAttributes() 288 | } 289 | 290 | ATTRIBUTE_UNDERLINE -> { 291 | info.attributes = (info.attributes or Kernel32.BACKGROUND_INTENSITY) 292 | applyAttributes() 293 | } 294 | 295 | ATTRIBUTE_UNDERLINE_OFF -> { 296 | info.attributes = (info.attributes and Kernel32.BACKGROUND_INTENSITY.inv()) 297 | applyAttributes() 298 | } 299 | 300 | ATTRIBUTE_NEGATIVE_ON -> { 301 | negative = true 302 | applyAttributes() 303 | } 304 | 305 | ATTRIBUTE_NEGATIVE_OFF -> { 306 | negative = false 307 | applyAttributes() 308 | } 309 | } 310 | } 311 | 312 | @Throws(IOException::class) 313 | override fun processSetForegroundColor(color: Int) { 314 | info.attributes = (info.attributes.toInt() and 0x000F.inv() or ANSI_FOREGROUND_COLOR_MAP[color].toInt()).toShort() 315 | applyAttributes() 316 | } 317 | 318 | @Throws(IOException::class) 319 | override fun processSetBackgroundColor(color: Int) { 320 | info.attributes = (info.attributes.toInt() and 0x00F0.inv() or ANSI_BACKGROUND_COLOR_MAP[color].toInt()).toShort() 321 | applyAttributes() 322 | } 323 | 324 | @Throws(IOException::class) 325 | override fun processDefaultTextColor() { 326 | info.attributes = (info.attributes.toInt() and 0x000F.inv() or (originalInfo.attributes.toInt() and 0x000F)).toShort() 327 | applyAttributes() 328 | } 329 | 330 | @Throws(IOException::class) 331 | override fun processDefaultBackgroundColor() { 332 | info.attributes = (info.attributes.toInt() and 0x00F0.inv() or (originalInfo.attributes.toInt() and 0x00F0)).toShort() 333 | applyAttributes() 334 | } 335 | 336 | @Throws(IOException::class) 337 | override fun processAttributeReset() { 338 | //info.attributes = originalInfo.attributes; 339 | info.attributes = (info.attributes.toInt() and 0x00FF.inv() or originalInfo.attributes.toInt()).toShort() 340 | negative = false 341 | applyAttributes() 342 | } 343 | 344 | @Throws(IOException::class) 345 | override fun processScrollDown(count: Int) { 346 | scroll((-count).toShort()) 347 | } 348 | 349 | @Throws(IOException::class) 350 | override fun processScrollUp(count: Int) { 351 | scroll(count.toShort()) 352 | } 353 | 354 | @Throws(IOException::class) 355 | override fun processCursorUpLine(count: Int) { 356 | consoleInfo 357 | info.cursorPosition.y = max(info.window.top.toDouble(), (info.cursorPosition.y - count).toDouble()).toInt().toShort() 358 | info.cursorPosition.x = 0.toShort() 359 | applyCursorPosition() 360 | } 361 | 362 | @Throws(IOException::class) 363 | override fun processCursorDownLine(count: Int) { 364 | consoleInfo 365 | info.cursorPosition.y = max(info.window.top.toDouble(), (info.cursorPosition.y + count).toDouble()).toInt().toShort() 366 | info.cursorPosition.x = 0.toShort() 367 | applyCursorPosition() 368 | } 369 | 370 | @Throws(IOException::class) 371 | override fun processCursorTo(row: Int, col: Int) { 372 | consoleInfo 373 | info.cursorPosition.y = 374 | max(info.window.top.toDouble(), min(info.size.y.toDouble(), (info.window.top + row - 1).toDouble())).toInt().toShort() 375 | info.cursorPosition.x = max(0.0, min(info.window.width().toDouble(), (col - 1).toDouble())).toInt().toShort() 376 | applyCursorPosition() 377 | } 378 | 379 | @Throws(IOException::class) 380 | override fun processCursorToColumn(x: Int) { 381 | consoleInfo 382 | info.cursorPosition.x = max(0.0, min(info.window.width().toDouble(), (x - 1).toDouble())).toInt().toShort() 383 | applyCursorPosition() 384 | } 385 | 386 | @Throws(IOException::class) 387 | override fun processCursorLeft(count: Int) { 388 | consoleInfo 389 | info.cursorPosition.x = max(0.0, (info.cursorPosition.x - count).toDouble()).toInt().toShort() 390 | applyCursorPosition() 391 | } 392 | 393 | @Throws(IOException::class) 394 | override fun processCursorRight(count: Int) { 395 | consoleInfo 396 | info.cursorPosition.x = min(info.window.width().toDouble(), (info.cursorPosition.x + count).toDouble()).toInt().toShort() 397 | applyCursorPosition() 398 | } 399 | 400 | @Throws(IOException::class) 401 | override fun processCursorDown(count: Int) { 402 | consoleInfo 403 | info.cursorPosition.y = min(info.size.y.toDouble(), (info.cursorPosition.y + count).toDouble()).toInt().toShort() 404 | applyCursorPosition() 405 | } 406 | 407 | @Throws(IOException::class) 408 | override fun processCursorUp(count: Int) { 409 | consoleInfo 410 | info.cursorPosition.y = max(info.window.top.toDouble(), (info.cursorPosition.y - count).toDouble()).toInt().toShort() 411 | applyCursorPosition() 412 | } 413 | 414 | /** 415 | * Scrolls the contents of the buffer either UP or DOWN 416 | * 417 | * @param rowsToScroll negative to go down, positive to go up. 418 | * 419 | * Scroll up and new lines are added at the bottom, scroll down and new lines are added at the 420 | * top (per the definition). 421 | * 422 | * Windows doesn't EXACTLY do this, since it will use whatever content is still on the buffer 423 | * and show THAT instead of blank lines. If the content is moved enough so that it runs OFF the 424 | * buffer, blank lines will be shown. 425 | */ 426 | @Throws(IOException::class) 427 | private fun scroll(rowsToScroll: Short) { 428 | if (rowsToScroll.toInt() == 0) { 429 | return 430 | } 431 | 432 | // Get the current screen buffer window position. 433 | consoleInfo 434 | val scrollRect = SMALL_RECT.ByReference() 435 | val coordDest = COORD.ByValue() 436 | 437 | // the content that will be scrolled (just what is visible in the window) 438 | scrollRect.top = (info.cursorPosition.y - info.window.height()).toShort() 439 | scrollRect.bottom = info.cursorPosition.y 440 | scrollRect.left = 0.toShort() 441 | scrollRect.right = (info.size.x - 1).toShort() 442 | 443 | // The destination for the scroll rectangle is xxx row up/down. 444 | coordDest.x = 0.toShort() 445 | coordDest.y = (scrollRect.top - rowsToScroll).toShort() 446 | 447 | // fill the space with whatever color was already there with spaces 448 | val attribs = written 449 | attribs.value = info.attributes.toInt() 450 | 451 | // The clipping rectangle is the same as the scrolling rectangle, so we pass NULL 452 | Kernel32.ASSERT(Kernel32.ScrollConsoleScreenBuffer(console, scrollRect, null, coordDest, attribs), "Could not scroll console") 453 | } 454 | 455 | @Throws(IOException::class) 456 | override fun close() { 457 | super.close() 458 | if (console != null) { 459 | Kernel32.CloseHandle(console) 460 | } 461 | } 462 | 463 | companion object { 464 | private val ANSI_FOREGROUND_COLOR_MAP: ShortArray 465 | private val ANSI_BACKGROUND_COLOR_MAP: ShortArray 466 | 467 | init { 468 | ANSI_FOREGROUND_COLOR_MAP = ShortArray(8) 469 | ANSI_FOREGROUND_COLOR_MAP[BLACK] = Kernel32.FOREGROUND_BLACK 470 | ANSI_FOREGROUND_COLOR_MAP[RED] = Kernel32.FOREGROUND_RED 471 | ANSI_FOREGROUND_COLOR_MAP[GREEN] = Kernel32.FOREGROUND_GREEN 472 | ANSI_FOREGROUND_COLOR_MAP[YELLOW] = Kernel32.FOREGROUND_YELLOW 473 | ANSI_FOREGROUND_COLOR_MAP[BLUE] = Kernel32.FOREGROUND_BLUE 474 | ANSI_FOREGROUND_COLOR_MAP[MAGENTA] = Kernel32.FOREGROUND_MAGENTA 475 | ANSI_FOREGROUND_COLOR_MAP[CYAN] = Kernel32.FOREGROUND_CYAN 476 | ANSI_FOREGROUND_COLOR_MAP[WHITE] = Kernel32.FOREGROUND_GREY 477 | ANSI_BACKGROUND_COLOR_MAP = ShortArray(8) 478 | ANSI_BACKGROUND_COLOR_MAP[BLACK] = Kernel32.BACKGROUND_BLACK 479 | ANSI_BACKGROUND_COLOR_MAP[RED] = Kernel32.BACKGROUND_RED 480 | ANSI_BACKGROUND_COLOR_MAP[GREEN] = Kernel32.BACKGROUND_GREEN 481 | ANSI_BACKGROUND_COLOR_MAP[YELLOW] = Kernel32.BACKGROUND_YELLOW 482 | ANSI_BACKGROUND_COLOR_MAP[BLUE] = Kernel32.BACKGROUND_BLUE 483 | ANSI_BACKGROUND_COLOR_MAP[MAGENTA] = Kernel32.BACKGROUND_MAGENTA 484 | ANSI_BACKGROUND_COLOR_MAP[CYAN] = Kernel32.BACKGROUND_CYAN 485 | ANSI_BACKGROUND_COLOR_MAP[WHITE] = Kernel32.BACKGROUND_GREY 486 | } 487 | } 488 | } 489 | -------------------------------------------------------------------------------- /src/dorkbox/console/output/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 dorkbox, llc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dorkbox.console.output; 18 | -------------------------------------------------------------------------------- /src/dorkbox/console/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 dorkbox, llc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dorkbox.console; 18 | -------------------------------------------------------------------------------- /src/dorkbox/console/util/CharHolder.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 dorkbox, llc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package dorkbox.console.util 17 | 18 | /** 19 | * Used for single char input 20 | */ 21 | class CharHolder { 22 | // default is nothing (0) 23 | @kotlin.jvm.JvmField 24 | var character = 0.toChar() 25 | } 26 | -------------------------------------------------------------------------------- /src/dorkbox/console/util/TerminalDetection.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 dorkbox, llc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dorkbox.console.util 18 | 19 | enum class TerminalDetection { 20 | AUTO, 21 | MACOS, 22 | WINDOWS, 23 | UNIX, 24 | NONE 25 | } 26 | -------------------------------------------------------------------------------- /src/dorkbox/console/util/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 dorkbox, llc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dorkbox.console.util; 18 | -------------------------------------------------------------------------------- /src9/dorkbox/EmptyClass.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 dorkbox, llc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dorkbox.jna; 18 | 19 | /** 20 | * Required for intellij to not complain regarding `module-info` for a multi-release jar. 21 | * This file is completely ignored by the gradle build process 22 | */ 23 | public 24 | class EmptyClass {} 25 | -------------------------------------------------------------------------------- /src9/dorkbox/input/EmptyClass.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 dorkbox, llc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dorkbox.jna; 18 | 19 | /** 20 | * Required for intellij to not complain regarding `module-info` for a multi-release jar. 21 | * This file is completely ignored by the gradle build process 22 | */ 23 | public 24 | class EmptyClass {} 25 | -------------------------------------------------------------------------------- /src9/dorkbox/output/EmptyClass.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 dorkbox, llc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dorkbox.jna; 18 | 19 | /** 20 | * Required for intellij to not complain regarding `module-info` for a multi-release jar. 21 | * This file is completely ignored by the gradle build process 22 | */ 23 | public 24 | class EmptyClass {} 25 | -------------------------------------------------------------------------------- /src9/dorkbox/util/EmptyClass.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 dorkbox, llc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dorkbox.jna; 18 | 19 | /** 20 | * Required for intellij to not complain regarding `module-info` for a multi-release jar. 21 | * This file is completely ignored by the gradle build process 22 | */ 23 | public 24 | class EmptyClass {} 25 | -------------------------------------------------------------------------------- /src9/module-info.java: -------------------------------------------------------------------------------- 1 | module dorkbox.console { 2 | exports dorkbox.console; 3 | exports dorkbox.console.input; 4 | exports dorkbox.console.output; 5 | 6 | requires transitive dorkbox.byteUtils; 7 | requires transitive dorkbox.propertyLoader; 8 | requires transitive dorkbox.jna; 9 | requires transitive dorkbox.updates; 10 | 11 | requires transitive kotlin.stdlib; 12 | 13 | requires transitive com.sun.jna; 14 | requires transitive com.sun.jna.platform; 15 | 16 | requires transitive org.slf4j; 17 | } 18 | -------------------------------------------------------------------------------- /test/dorkbox/console/AnsiConsoleExample.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 dorkbox, llc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dorkbox.console 18 | 19 | import dorkbox.console.input.Terminal 20 | import dorkbox.console.output.Ansi 21 | import dorkbox.console.output.AnsiRenderer 22 | import dorkbox.console.output.Color 23 | import dorkbox.console.output.Erase 24 | import java.io.IOException 25 | import java.lang.management.ManagementFactory 26 | 27 | /** 28 | * System output for visual confirmation of ANSI codes. 29 | * 30 | * 31 | * Must enable assertions to verify no errors!! (ie: java -ea -jar blah.jar) 32 | */ 33 | object AnsiConsoleExample { 34 | @Throws(IOException::class) 35 | @JvmStatic 36 | fun main(args: Array) { 37 | Console.ENABLE_ANSI = true 38 | Console.ENABLE_ECHO = true 39 | 40 | // needed to hook the output streams, so "normal" System.out/err work (rather than having to use Console.err/out 41 | Console.hookSystemOutputStreams() 42 | 43 | System.err.println("System Properties") 44 | val properties = System.getProperties() 45 | for ((key, value) in properties) { 46 | System.err.format("\t%s=%s%n", key, value) 47 | } 48 | 49 | System.err.println("") 50 | System.err.println("") 51 | System.err.println("") 52 | System.err.println("Runtime Arguments") 53 | val runtimeMxBean = ManagementFactory.getRuntimeMXBean() 54 | val arguments = runtimeMxBean.inputArguments 55 | System.err.println(arguments.toTypedArray().contentToString()) 56 | System.err.println("") 57 | System.err.println("") 58 | System.err.println("") 59 | 60 | println( 61 | Ansi.ansi().fg(Color.BLACK).a("black").bg(Color.BLACK).a("black") 62 | .reset() 63 | .fg(Color.BRIGHT_BLACK).a("b-black") 64 | .bg(Color.BRIGHT_BLACK).a("b-black") 65 | .reset() 66 | ) 67 | println( 68 | Ansi.ansi().fg(Color.BLUE).a("blue").bg(Color.BLUE).a("blue").reset().fg(Color.BRIGHT_BLUE).a("b-blue").bg(Color.BRIGHT_BLUE) 69 | .a("b-blue").reset() 70 | ) 71 | println( 72 | Ansi.ansi().fg(Color.CYAN).a("cyan").bg(Color.CYAN).a("cyan").reset().fg(Color.BRIGHT_CYAN).a("b-cyan").bg(Color.BRIGHT_CYAN) 73 | .a("b-cyan").reset() 74 | ) 75 | println( 76 | Ansi.ansi().fg(Color.GREEN).a("green").bg(Color.GREEN).a("green").reset().fg(Color.BRIGHT_GREEN).a("b-green") 77 | .bg(Color.BRIGHT_GREEN).a("b-green").reset() 78 | ) 79 | println( 80 | Ansi.ansi().fg(Color.MAGENTA).a("magenta").bg(Color.MAGENTA).a("magenta").reset().fg(Color.BRIGHT_MAGENTA).a("b-magenta") 81 | .bg(Color.BRIGHT_MAGENTA).a("b-magenta").reset() 82 | ) 83 | println( 84 | Ansi.ansi().fg(Color.RED).a("red").bg(Color.RED).a("red").reset().fg(Color.BRIGHT_RED).a("b-red").bg(Color.BRIGHT_RED) 85 | .a("b-red").reset() 86 | ) 87 | println( 88 | Ansi.ansi().fg(Color.YELLOW).a("yellow").bg(Color.YELLOW).a("yellow").reset().fg(Color.BRIGHT_YELLOW).a("b-yellow") 89 | .bg(Color.BRIGHT_YELLOW).a("b-yellow").reset() 90 | ) 91 | println( 92 | Ansi.ansi().fg(Color.WHITE).a("white").bg(Color.WHITE).a("white").reset().fg(Color.BRIGHT_WHITE).a("b-white") 93 | .bg(Color.BRIGHT_WHITE).a("b-white").reset() 94 | ) 95 | 96 | Console.reset() // reset the ansi stream. Can ALSO have ansi().reset(), but that would be redundant 97 | 98 | println("The following line should be blank except for the first '>'") 99 | println( 100 | Ansi.ansi().a(">THIS SHOULD BE BLANK").cursorToColumn(2).eraseLine() 101 | ) 102 | 103 | println("The following line should be blank") 104 | println( 105 | Ansi.ansi().a(">THIS SHOULD BE BLANK").eraseLine(Erase.ALL) 106 | ) 107 | 108 | println( 109 | Ansi.ansi().a(">THIS SHOULD BE BLANK").eraseLine(Erase.BACKWARD).a("Everything on this line before this should be blank") 110 | ) 111 | 112 | println( 113 | Ansi.ansi().a("Everything on this line after this should be blank").saveCursorPosition().a(">THIS SHOULD BE BLANK") 114 | .restoreCursorPosition().eraseLine() 115 | ) 116 | 117 | println("00000000000000000000000000") 118 | println("00000000000000000000000000") 119 | println("00000000000000000000000000") 120 | println("00000000000000000000000000") 121 | println("00000000000000000000000000") 122 | 123 | 124 | println( 125 | Ansi.ansi().a("Should have two blank spots in the above 0's") 126 | .saveCursorPosition() 127 | .cursorUp(4).cursorLeft(30).a(" ") 128 | .cursorDownLine().cursorRight(5).a(" ") 129 | .restoreCursorPosition() 130 | ) 131 | 132 | 133 | println("ver : " + AnsiRenderer.render("@|bold,green ${Console.version}|@") ) 134 | println("console type: " + AnsiRenderer.render("@|bold,red ${Console.`in`.javaClass}|@") + " (${Terminal.terminal.width}w x ${Terminal.terminal.height}h)") 135 | 136 | println() 137 | println("Now testing the input console. 'q' to quit") 138 | 139 | var read: Int 140 | while (Console.`in`.read().also { read = it } != 'q'.code) { 141 | if (Character.isDigit(read)) { 142 | val numericValue = Character.getNumericValue(read) 143 | // reverse if pressing 2 144 | if (numericValue == 2) { 145 | Console.out.print(Ansi.ansi().cursorLeft().scrollDown(1)) 146 | } 147 | else { 148 | Console.out.print(Ansi.ansi().cursorLeft().scrollUp(numericValue)) 149 | } 150 | } else { 151 | Console.out.println(" char : " + read + " (" + read.toChar() + ")") 152 | } 153 | 154 | // not needed because our ANSI stream is auto-flushing now. 155 | // Console.out.flush() // flush guarantees the terminal moves the way we want 156 | } 157 | 158 | println() 159 | println("Now testing the input console LINE input. 'q' to quit") 160 | 161 | var line: String? 162 | while (Console.`in`.readLine().also { line = it } != "q") { 163 | System.err.println("line: $line") 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /test/dorkbox/console/AnsiRenderWriterTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 dorkbox, llc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /* 18 | * Copyright (C) 2009 the original author or authors. 19 | * 20 | * Licensed under the Apache License, Version 2.0 (the "License"); 21 | * you may not use this file except in compliance with the License. 22 | * You may obtain a copy of the License at 23 | * 24 | * http://www.apache.org/licenses/LICENSE-2.0 25 | * 26 | * Unless required by applicable law or agreed to in writing, software 27 | * distributed under the License is distributed on an "AS IS" BASIS, 28 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 29 | * See the License for the specific language governing permissions and 30 | * limitations under the License. 31 | */ 32 | package dorkbox.console 33 | 34 | import dorkbox.console.output.AnsiRenderWriter 35 | import org.junit.After 36 | import org.junit.Assert 37 | import org.junit.Before 38 | import org.junit.Test 39 | import java.io.ByteArrayOutputStream 40 | 41 | /** 42 | * Tests for the [AnsiRenderWriter] class. 43 | * 44 | * @author [Jason Dillon](mailto:jason@planet57.com) 45 | */ 46 | class AnsiRenderWriterTest { 47 | private var baos: ByteArrayOutputStream? = null 48 | private var out: AnsiRenderWriter? = null 49 | 50 | @Before 51 | fun setUp() { 52 | baos = ByteArrayOutputStream() 53 | out = AnsiRenderWriter(baos!!) 54 | } 55 | 56 | @After 57 | fun tearDown() { 58 | out = null 59 | baos = null 60 | } 61 | 62 | @Test 63 | fun testRenderNothing() { 64 | out!!.print("foo") 65 | out!!.flush() 66 | 67 | val result = String(baos!!.toByteArray()) 68 | Assert.assertEquals("foo", result) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /test/dorkbox/console/AnsiRendererTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 dorkbox, llc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /* 18 | * Copyright (C) 2009 the original author or authors. 19 | * 20 | * Licensed under the Apache License, Version 2.0 (the "License"); 21 | * you may not use this file except in compliance with the License. 22 | * You may obtain a copy of the License at 23 | * 24 | * http://www.apache.org/licenses/LICENSE-2.0 25 | * 26 | * Unless required by applicable law or agreed to in writing, software 27 | * distributed under the License is distributed on an "AS IS" BASIS, 28 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 29 | * See the License for the specific language governing permissions and 30 | * limitations under the License. 31 | */ 32 | package dorkbox.console 33 | 34 | import dorkbox.console.Console 35 | import dorkbox.console.output.Ansi 36 | import dorkbox.console.output.AnsiRenderer 37 | import dorkbox.console.output.Attribute 38 | import dorkbox.console.output.Color 39 | import org.junit.Assert 40 | import org.junit.Assert.assertEquals 41 | import org.junit.Before 42 | import org.junit.Test 43 | 44 | /** 45 | * Tests for the [AnsiRenderer] class. 46 | * 47 | * @author [Jason Dillon](mailto:jason@planet57.com) 48 | */ 49 | class AnsiRendererTest { 50 | @Before 51 | fun setUp() { 52 | Console.ENABLE_ANSI = true 53 | } 54 | 55 | @Test 56 | @Throws(Exception::class) 57 | fun testTest() { 58 | Assert.assertFalse(test("foo")) 59 | Assert.assertTrue(test("@|foo|")) 60 | Assert.assertTrue(test("@|foo")) 61 | } 62 | 63 | @Test 64 | fun testRender() { 65 | val str: String = AnsiRenderer.render("@|bold foo|@") 66 | println(str) 67 | assertEquals(Ansi.ansi().a(Attribute.BOLD).a("foo").reset().toString(), str) 68 | assertEquals(Ansi.ansi().bold().a("foo").reset().toString(), str) 69 | } 70 | 71 | @Test 72 | fun testRender2() { 73 | val str: String = AnsiRenderer.render("@|bold,red foo|@") 74 | println(str) 75 | assertEquals(Ansi.ansi().a(Attribute.BOLD).fg(Color.RED).a("foo").reset().toString(), str) 76 | } 77 | 78 | @Test 79 | fun testRender3() { 80 | val str: String = AnsiRenderer.render("@|bold,red foo bar baz|@") 81 | println(str) 82 | assertEquals(Ansi.ansi().a(Attribute.BOLD).fg(Color.RED).a("foo bar baz").reset().toString(), str) 83 | } 84 | 85 | @Test 86 | fun testRender4() { 87 | val str: String = AnsiRenderer.render("@|bold,red foo bar baz|@ ick @|bold,red foo bar baz|@") 88 | println(str) 89 | assertEquals( 90 | Ansi.ansi().a(Attribute.BOLD).fg(Color.RED).a("foo bar baz").reset().a(" ick ").a(Attribute.BOLD).fg(Color.RED).a("foo bar baz") 91 | .reset().toString(), str 92 | ) 93 | } 94 | 95 | @Test 96 | fun testRender5() { 97 | // Check the ansi() render method. 98 | val str = Ansi.ansi().render("@|bold Hello|@").toString() 99 | println(str) 100 | assertEquals(Ansi.ansi().a(Attribute.BOLD).a("Hello").reset().toString(), str) 101 | } 102 | 103 | @Test 104 | fun testRenderNothing() { 105 | assertEquals("foo", AnsiRenderer.render("foo")) 106 | } 107 | 108 | @Test 109 | fun testRenderInvalidMissingEnd() { 110 | val str: String = AnsiRenderer.render("@|bold foo") 111 | Assert.assertEquals("@|bold foo", str) 112 | } 113 | 114 | @Test 115 | fun testRenderInvalidMissingText() { 116 | val str: String = AnsiRenderer.render("@|bold|@") 117 | Assert.assertEquals("@|bold|@", str) 118 | } 119 | 120 | companion object { 121 | fun test(text: String): Boolean { 122 | return text.contains(AnsiRenderer.BEGIN_TOKEN) 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /test/dorkbox/console/AnsiStringTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 dorkbox, llc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /* 18 | * Copyright (C) 2009 the original author or authors. 19 | * 20 | * Licensed under the Apache License, Version 2.0 (the "License"); 21 | * you may not use this file except in compliance with the License. 22 | * You may obtain a copy of the License at 23 | * 24 | * http://www.apache.org/licenses/LICENSE-2.0 25 | * 26 | * Unless required by applicable law or agreed to in writing, software 27 | * distributed under the License is distributed on an "AS IS" BASIS, 28 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 29 | * See the License for the specific language governing permissions and 30 | * limitations under the License. 31 | */ 32 | package dorkbox.console 33 | 34 | import dorkbox.console.output.Ansi 35 | import dorkbox.console.output.AnsiString 36 | import dorkbox.console.output.Attribute 37 | import org.junit.Assert 38 | import org.junit.Test 39 | 40 | /** 41 | * Tests for [AnsiString]. 42 | * 43 | * @author [Jason Dillon](mailto:jason@planet57.com) 44 | */ 45 | class AnsiStringTest { 46 | @Test 47 | @Throws(Exception::class) 48 | fun testNotEncoded() { 49 | val `as` = AnsiString("foo") 50 | Assert.assertEquals("foo", `as`.encoded) 51 | Assert.assertEquals("foo", `as`.plain) 52 | Assert.assertEquals(3, `as`.length.toLong()) 53 | } 54 | 55 | @Test 56 | @Throws(Exception::class) 57 | fun testEncoded() { 58 | val `as` = AnsiString(Ansi.ansi().a(Attribute.BOLD).a("foo").reset().toString()) 59 | 60 | Assert.assertEquals("foo", `as`.plain) 61 | Assert.assertEquals(3, `as`.length.toLong()) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /test/dorkbox/console/AnsiTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 dorkbox, llc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /* 18 | * Copyright (C) 2009 the original author(s). 19 | * 20 | * Licensed under the Apache License, Version 2.0 (the "License"); 21 | * you may not use this file except in compliance with the License. 22 | * You may obtain a copy of the License at 23 | * 24 | * http://www.apache.org/licenses/LICENSE-2.0 25 | * 26 | * Unless required by applicable law or agreed to in writing, software 27 | * distributed under the License is distributed on an "AS IS" BASIS, 28 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 29 | * See the License for the specific language governing permissions and 30 | * limitations under the License. 31 | */ 32 | package dorkbox.console 33 | 34 | import dorkbox.console.output.Ansi 35 | import dorkbox.console.output.Ansi.Companion.ansi 36 | import dorkbox.console.output.AnsiRenderer.render 37 | import dorkbox.console.output.Attribute 38 | import dorkbox.console.output.Color 39 | import org.junit.Assert 40 | import org.junit.Assert.assertEquals 41 | import org.junit.Test 42 | 43 | /** 44 | * Tests for the [Ansi] class. 45 | * 46 | * @author [Jason Dillon](mailto:jason@planet57.com) 47 | */ 48 | class AnsiTest { 49 | @Test 50 | @Throws(CloneNotSupportedException::class) 51 | fun testClone() { 52 | val ansi: Ansi = ansi().a("Some text").bg(Color.BLACK).fg(Color.WHITE) 53 | val clone: Ansi = ansi(ansi) 54 | 55 | assertEquals(ansi.a("test").reset().toString(), clone.a("test").reset().toString()) 56 | } 57 | 58 | @Test 59 | @Throws(CloneNotSupportedException::class) 60 | fun testOutput() { 61 | 62 | // verify the output renderer 63 | var str: String = render("@|bold foo|@foo") 64 | assertEquals(ansi().a(Attribute.BOLD).a("foo").reset().a("foo").toString(), str) 65 | assertEquals(ansi().bold().a("foo").reset().a("foo").toString(), str) 66 | 67 | str = render("@|bold,red foo|@") 68 | assertEquals(ansi().a(Attribute.BOLD).fg(Color.RED).a("foo").reset().toString(), str) 69 | assertEquals(ansi().bold().fg(Color.RED).a("foo").reset().toString(), str) 70 | 71 | str = render("@|bold,red foo bar baz|@") 72 | assertEquals(ansi().a(Attribute.BOLD).fg(Color.RED).a("foo bar baz").reset().toString(), str) 73 | assertEquals(ansi().bold().fg(Color.RED).a("foo bar baz").reset().toString(), str) 74 | 75 | str = render("@|bold,red foo bar baz|@ ick @|bold,red foo bar baz|@") 76 | val expected: String = 77 | ansi().a(Attribute.BOLD).fg(Color.RED).a("foo bar baz").reset().a(" ick ").a(Attribute.BOLD).fg(Color.RED).a("foo bar baz") 78 | .reset().toString() 79 | 80 | assertEquals(expected, str) 81 | 82 | str = render("@|bold foo") // shouldn't work 83 | System.err.println("$str <- shouldn't work") 84 | 85 | str = render("@|bold|@") // shouldn't work 86 | System.err.println("$str <- shouldn't work") 87 | 88 | str = render("@|bold foo|@foo") 89 | println("$str <- shouldn't work") 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /test/dorkbox/console/HtmlAnsiOutputStreamTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 dorkbox, llc 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /* 18 | * Copyright (C) 2009 the original author(s). 19 | * 20 | * Licensed under the Apache License, Version 2.0 (the "License"); 21 | * you may not use this file except in compliance with the License. 22 | * You may obtain a copy of the License at 23 | * 24 | * http://www.apache.org/licenses/LICENSE-2.0 25 | * 26 | * Unless required by applicable law or agreed to in writing, software 27 | * distributed under the License is distributed on an "AS IS" BASIS, 28 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 29 | * See the License for the specific language governing permissions and 30 | * limitations under the License. 31 | */ 32 | package dorkbox.console 33 | 34 | import dorkbox.console.output.HtmlAnsiOutputStream 35 | import org.junit.Assert 36 | import org.junit.Test 37 | import java.io.ByteArrayOutputStream 38 | import java.io.IOException 39 | import java.nio.charset.Charset 40 | 41 | /** 42 | * @author [Daniel Doubrovkine](http://code.dblock.org) 43 | */ 44 | class HtmlAnsiOutputStreamTest { 45 | @Test 46 | @Throws(IOException::class) 47 | fun testNoMarkup() { 48 | Assert.assertEquals("line", colorize("line")) 49 | } 50 | 51 | @Test 52 | @Throws(IOException::class) 53 | fun testClear() { 54 | Assert.assertEquals("", colorize("")) 55 | Assert.assertEquals("hello world", colorize("hello world")) 56 | } 57 | 58 | @Test 59 | @Throws(IOException::class) 60 | fun testBold() { 61 | Assert.assertEquals("hello world", colorize("hello world")) 62 | } 63 | 64 | @Test 65 | @Throws(IOException::class) 66 | fun testGreen() { 67 | Assert.assertEquals( 68 | "hello world", colorize("hello world") 69 | ) 70 | } 71 | 72 | @Test 73 | @Throws(IOException::class) 74 | fun testGreenOnWhite() { 75 | Assert.assertEquals( 76 | "hello world", 77 | colorize("hello world") 78 | ) 79 | } 80 | 81 | @Test 82 | @Throws(IOException::class) 83 | fun testEscapeHtml() { 84 | Assert.assertEquals(""", colorize("\"")) 85 | Assert.assertEquals("&", colorize("&")) 86 | Assert.assertEquals("<", colorize("<")) 87 | Assert.assertEquals(">", colorize(">")) 88 | Assert.assertEquals(""&<>", colorize("\"&<>")) 89 | } 90 | 91 | @Test 92 | @Throws(IOException::class) 93 | fun testResetOnOpen() { 94 | Assert.assertEquals( 95 | "red", colorize("red") 96 | ) 97 | } 98 | 99 | @Test 100 | @Throws(IOException::class) 101 | fun testUTF8Character() { 102 | Assert.assertEquals( 103 | "\u3053\u3093\u306b\u3061\u306f", colorize("\u3053\u3093\u306b\u3061\u306f") 104 | ) 105 | } 106 | 107 | @Throws(IOException::class) 108 | private fun colorize(text: String): String { 109 | val os = ByteArrayOutputStream() 110 | val hos = HtmlAnsiOutputStream(os) 111 | hos.write(text.toByteArray(charset)) 112 | hos.close() 113 | return String(os.toByteArray(), charset) 114 | } 115 | 116 | companion object { 117 | private val charset = Charset.forName("UTF-8") 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /windows console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dorkbox/Console/30b800de83d7cb06ecf33b0829120aea2c848e08/windows console.png --------------------------------------------------------------------------------