├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── example └── example.dart ├── lib ├── cli_repl.dart └── src │ ├── repl_adapter.dart │ └── repl_adapter │ ├── codes.dart │ ├── interface.dart │ ├── node.dart │ └── vm.dart ├── pubspec.yaml ├── test └── repl_test.dart └── tool └── grind.dart /.gitignore: -------------------------------------------------------------------------------- 1 | # Files and directories created by pub 2 | .packages 3 | .pub/ 4 | build/ 5 | .dart_tool/ 6 | # Remove the following pattern if you wish to check in your lock file 7 | pubspec.lock 8 | 9 | # Directory created by dartdoc 10 | doc/api/ 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.2.3 4 | 5 | - Code cleanup to change how some parameters are initialized. 6 | 7 | ## 0.2.2 8 | 9 | - Avoid triggering https://github.com/dart-lang/sdk/issue/34775. 10 | 11 | * Explicitly declare `Repl.exit()`'s type as `FutureOr`. 12 | 13 | ## 0.2.1 14 | 15 | - Migrate to null-safety 16 | - Require \>=Dart 2.12 17 | 18 | ## 0.2.0+2 19 | 20 | - Removed a `dart:async` import that isn't required for \>=Dart 2.1. 21 | - Require \>=Dart 2.1. 22 | 23 | ## 0.2.0+1 24 | 25 | - Support Dart 2 stable. 26 | 27 | ## 0.2.0 28 | 29 | - Removes option to use sharedStdIn, as well as the `io` package dependency. 30 | 31 | ## 0.1.3 32 | 33 | - Line editing should now work in environments like the Emacs terminal where 34 | `EscO` is used for ANSI-escaped input instead of the more typical `Esc[`. 35 | 36 | - Fixed issue with the prompt changing to the Node default when running on it. 37 | 38 | - Broadened dependency on the async package to support 2.x.x versions. 39 | 40 | ## 0.1.2 41 | 42 | - If compiled to JS and run with Node, `Repl.runAsync()` should now work. It 43 | uses the [Node readline][] library for line editing. 44 | 45 | - `Repl.runAsync()` now supports running with no terminal, and should operate 46 | similarly to how `Repl.run()` does, both on the Dart VM and on Node. 47 | 48 | [Node readline]: https://nodejs.org/api/readline.html 49 | 50 | ## 0.1.1 51 | 52 | - Fix issues on Windows 53 | 54 | ## 0.1.0 55 | 56 | - Makes `Repl.run()` synchronous, since that use case is probably more common. 57 | The asynchronous version can now be run with `Repl.runAsync()`. 58 | 59 | - When running with `Repl.run()` and no terminal, this will no longer crash, and 60 | instead print both prompts and the input, allowing you to test a REPL by piping 61 | input to it. 62 | 63 | - Adds support for limited cutting and pasting with Ctrl-U, Ctrl-K, and Ctrl-Y. 64 | 65 | ## 0.0.1 66 | 67 | - Initial release 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018, Jennifer Thakar. 2 | All rights reserved. 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 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the project nor the names of its contributors may be 12 | used to endorse or promote products derived from this software without 13 | specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 19 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cli_repl 2 | 3 | A simple library for creating CLI REPLs in Dart. 4 | 5 | ## Features 6 | 7 | Example Usage: 8 | 9 | ```dart 10 | /// Echoes all entered lines 11 | for (var line in new Repl().run()) { 12 | print(line); 13 | } 14 | ``` 15 | 16 | ### Statement Validation 17 | 18 | By passing a `validator` to the `Repl` constructor, you can tell the REPL 19 | whether some entered text is a complete statement or not. The REPL calls this 20 | whenever a newline is entered to determine whether to yield a complete statement 21 | or continue it on a new line. The default validator returns true for all text. 22 | 23 | ### Custom Prompts 24 | 25 | By default, the REPL gives no prompt to the user when asking for a statement. 26 | You can change this by passing a `prompt` to the `Repl` constructor. By default, 27 | statement continuations on a new line will start with whitespace equal to the 28 | length of the `prompt`. You can override this by passing in `continuation`. 29 | 30 | See [`example/example.dart`][example] for a demonstration of statement 31 | validation and custom prompts. 32 | 33 | [example]: https://github.com/jathak/cli_repl/tree/master/example/example.dart 34 | 35 | ### History 36 | 37 | A history of entered lines is stored. History entries are modified when edited. 38 | 39 | By default, a maximum of 50 entries are stored. You can change this by passing 40 | `maxHistory` into the `Repl` constructor. 41 | 42 | ### Navigation 43 | 44 | - `Left`/`Ctrl-B`: Move left one character 45 | - `Right`/`Ctrl-F`: Move right one character 46 | - `Home`/`Ctrl-A`: Move to start of line 47 | - `End`/`Ctrl-E`: Move to end of line 48 | - `Ctrl-L`: Clear the screen 49 | - `Ctrl-D`: If there is text, delete the character under the cursor. If there is 50 | no text, exit. 51 | - `Ctrl-F`: Moves forward one character 52 | - `Ctrl-B`: Moves backward one character 53 | - `Ctrl-U`: Kill (cut) to start of line 54 | - `Ctrl-K`: Kill (cut) to end of line 55 | - `Ctrl-Y`: Yank (paste) previously killed text, inserting at cursor 56 | - `Up`/`Down`: Navigate within history 57 | 58 | ### Testing REPLs 59 | 60 | If running without a terminal, the input will be printed along with the prompts, 61 | allowing you to test REPLs made with this library by comparing stdout to the 62 | expected log input and output together. 63 | 64 | See [test/repl_test.dart][repl_test] for an example of this. 65 | 66 | [repl_test]: https://github.com/jathak/cli_repl/tree/master/test/repl_test.dart 67 | 68 | ### Running on Node 69 | 70 | If you compile this to JS with Dart 2, you can run it on Node. 71 | 72 | There are a couple of behavior differences: 73 | 74 | - Node's built-in readline library is used, so the supported navigation and 75 | history commands may vary from the Dart version. 76 | - Likewise, line history is managed by Node, and you can't change the maximum 77 | number of entries or edit history manually from Dart. 78 | - Only `Repl.runAsync()` works. Calling `Repl.run()` will throw an error. 79 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # Lint rules and documentation, see http://dart-lang.github.io/linter/lints 2 | linter: 3 | rules: 4 | - cancel_subscriptions 5 | - hash_and_equals 6 | - iterable_contains_unrelated_type 7 | - list_remove_unrelated_type 8 | - test_types_in_equals 9 | - unrelated_type_equality_checks 10 | - valid_regexps 11 | - prefer_equal_for_default_values 12 | 13 | -------------------------------------------------------------------------------- /example/example.dart: -------------------------------------------------------------------------------- 1 | /// Example REPL that looks for a semicolon to complete a statement and then 2 | /// echoes all completed statements. 3 | 4 | import 'package:cli_repl/cli_repl.dart'; 5 | 6 | main(args) async { 7 | var v = (str) => str.trim().isEmpty || str.trim().endsWith(';'); 8 | var repl = new Repl(prompt: '>>> ', continuation: '... ', validator: v); 9 | await for (var x in repl.runAsync()) { 10 | if (x.trim().isEmpty) continue; 11 | if (x == 'throw;') throw "oh no!"; 12 | print(x); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/cli_repl.dart: -------------------------------------------------------------------------------- 1 | library cli_repl; 2 | 3 | import 'dart:async'; 4 | 5 | import 'src/repl_adapter.dart'; 6 | 7 | class Repl { 8 | /// Text displayed when prompting the user for a new statement. 9 | String prompt; 10 | 11 | /// Text displayed at start of continued statement line. 12 | String continuation; 13 | 14 | /// Called when a newline is entered to determine whether the queue a 15 | /// completed statement or allow for a continuation. 16 | StatementValidator validator; 17 | 18 | Repl( 19 | {this.prompt = '', 20 | String? continuation, 21 | StatementValidator? validator, 22 | this.maxHistory = 50}) 23 | : continuation = continuation ?? ' ' * prompt.length, 24 | validator = validator ?? alwaysValid { 25 | _adapter = new ReplAdapter(this); 26 | } 27 | 28 | late ReplAdapter _adapter; 29 | 30 | /// Run the REPL, yielding complete statements synchronously. 31 | Iterable run() => _adapter.run(); 32 | 33 | /// Run the REPL, yielding complete statements asynchronously. 34 | /// 35 | /// Note that the REPL will continue if you await in an "await for" loop. 36 | Stream runAsync() => _adapter.runAsync(); 37 | 38 | /// Kills and cleans up the REPL. 39 | FutureOr exit() => _adapter.exit(); 40 | 41 | /// History is by line, not by statement. 42 | /// 43 | /// The first item in the list is the most recent history item. 44 | List history = []; 45 | 46 | /// Maximum history that will be kept in the list. 47 | /// 48 | /// Defaults to 50. 49 | int maxHistory; 50 | } 51 | 52 | /// Returns true if [text] is a complete statement or false otherwise. 53 | typedef bool StatementValidator(String text); 54 | 55 | final StatementValidator alwaysValid = (text) => true; 56 | -------------------------------------------------------------------------------- /lib/src/repl_adapter.dart: -------------------------------------------------------------------------------- 1 | export 'repl_adapter/interface.dart' 2 | if (dart.library.io) 'repl_adapter/vm.dart' 3 | if (dart.library.js) 'repl_adapter/node.dart'; 4 | -------------------------------------------------------------------------------- /lib/src/repl_adapter/codes.dart: -------------------------------------------------------------------------------- 1 | library constants; 2 | 3 | const escape = 27; 4 | const arrowLeft = 68; 5 | const arrowRight = 67; 6 | const arrowUp = 65; 7 | const arrowDown = 66; 8 | const startOfLine = 1; 9 | const endOfLine = 5; 10 | const eof = 4; 11 | const backspace = 127; 12 | const clear = 12; 13 | const newLine = 10; 14 | const carriageReturn = 13; 15 | const forward = 6; 16 | const backward = 2; 17 | const killToStart = 21; 18 | const killToEnd = 11; 19 | const yank = 25; 20 | const home = 72; 21 | const end = 70; 22 | 23 | final String ansiEscape = new String.fromCharCode(escape); 24 | 25 | /// Returns the code of the first code unit. 26 | int c(String s) => s.codeUnitAt(0); 27 | -------------------------------------------------------------------------------- /lib/src/repl_adapter/interface.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import '../../cli_repl.dart'; 4 | 5 | class ReplAdapter { 6 | Repl repl; 7 | 8 | ReplAdapter(this.repl); 9 | 10 | Iterable run() sync* {} 11 | 12 | Stream runAsync() async* {} 13 | 14 | FutureOr exit() {} 15 | } 16 | -------------------------------------------------------------------------------- /lib/src/repl_adapter/node.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:async/async.dart'; 4 | import 'package:js/js.dart'; 5 | 6 | import '../../cli_repl.dart'; 7 | 8 | class ReplAdapter { 9 | Repl repl; 10 | 11 | ReplAdapter(this.repl); 12 | 13 | Iterable run() sync* { 14 | throw new UnsupportedError('Synchronous REPLs not supported in Node'); 15 | } 16 | 17 | ReadlineInterface? rl; 18 | 19 | Stream runAsync() { 20 | var output = stdinIsTTY ? stdout : null; 21 | var rl = this.rl = readline.createInterface( 22 | new ReadlineOptions(input: stdin, output: output, prompt: repl.prompt)); 23 | var statement = ""; 24 | var prompt = repl.prompt; 25 | 26 | late StreamController runController; 27 | runController = StreamController( 28 | onListen: () async { 29 | try { 30 | var lineController = StreamController(); 31 | var lineQueue = StreamQueue(lineController.stream); 32 | rl.on('line', 33 | allowInterop((value) => lineController.add(value as String))); 34 | 35 | while (true) { 36 | if (stdinIsTTY) stdout.write(prompt); 37 | var line = await lineQueue.next; 38 | if (!stdinIsTTY) print('$prompt$line'); 39 | statement += line; 40 | if (repl.validator(statement)) { 41 | runController.add(statement); 42 | statement = ""; 43 | prompt = repl.prompt; 44 | rl.setPrompt(repl.prompt); 45 | } else { 46 | statement += '\n'; 47 | prompt = repl.continuation; 48 | rl.setPrompt(repl.continuation); 49 | } 50 | } 51 | } catch (error, stackTrace) { 52 | runController.addError(error, stackTrace); 53 | await exit(); 54 | runController.close(); 55 | } 56 | }, 57 | onCancel: exit); 58 | 59 | return runController.stream; 60 | } 61 | 62 | FutureOr exit() { 63 | rl?.close(); 64 | rl = null; 65 | } 66 | } 67 | 68 | @JS('require') 69 | external ReadlineModule require(String name); 70 | 71 | final readline = require('readline'); 72 | 73 | bool get stdinIsTTY => stdin.isTTY ?? false; 74 | 75 | @JS('process.stdin') 76 | external Stdin get stdin; 77 | 78 | @JS() 79 | class Stdin { 80 | external bool? get isTTY; 81 | } 82 | 83 | @JS('process.stdout') 84 | external Stdout get stdout; 85 | 86 | @JS() 87 | class Stdout { 88 | external void write(String data); 89 | } 90 | 91 | @JS() 92 | class ReadlineModule { 93 | external ReadlineInterface createInterface(ReadlineOptions options); 94 | } 95 | 96 | @JS() 97 | @anonymous 98 | class ReadlineOptions { 99 | external get input; 100 | external get output; 101 | external String get prompt; 102 | external factory ReadlineOptions({input, output, String? prompt}); 103 | } 104 | 105 | @JS() 106 | class ReadlineInterface { 107 | external void on(String event, void callback(object)); 108 | external void question(String prompt, void callback(object)); 109 | external void close(); 110 | external void pause(); 111 | external void setPrompt(String prompt); 112 | } 113 | -------------------------------------------------------------------------------- /lib/src/repl_adapter/vm.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:async/async.dart'; 5 | 6 | import '../../cli_repl.dart'; 7 | import 'codes.dart'; 8 | 9 | class ReplAdapter { 10 | Repl repl; 11 | 12 | ReplAdapter(this.repl); 13 | 14 | Iterable run() sync* { 15 | try { 16 | // Try to set up for interactive session 17 | stdin.echoMode = false; 18 | stdin.lineMode = false; 19 | } on StdinException { 20 | // If it can't, print both input and prompts (useful for testing) 21 | yield* linesToStatements(inputLines()); 22 | return; 23 | } 24 | while (true) { 25 | try { 26 | var result = readStatement(); 27 | if (result == null) { 28 | print(""); 29 | break; 30 | } 31 | yield result; 32 | } on Exception catch (e) { 33 | print(e); 34 | } 35 | } 36 | exit(); 37 | } 38 | 39 | Iterable inputLines() sync* { 40 | while (true) { 41 | try { 42 | String? line = stdin.readLineSync(); 43 | if (line == null) break; 44 | yield line; 45 | } on StdinException { 46 | break; 47 | } 48 | } 49 | } 50 | 51 | Stream runAsync() { 52 | bool interactive = true; 53 | try { 54 | stdin.echoMode = false; 55 | stdin.lineMode = false; 56 | } on StdinException { 57 | interactive = false; 58 | } 59 | 60 | late StreamController controller; 61 | controller = StreamController( 62 | onListen: () async { 63 | try { 64 | var charQueue = 65 | this.charQueue = StreamQueue(stdin.expand((data) => data)); 66 | while (true) { 67 | if (!interactive && !(await charQueue.hasNext)) { 68 | this.charQueue = null; 69 | controller.close(); 70 | return; 71 | } 72 | 73 | var result = await _readStatementAsync(charQueue); 74 | if (result == null) { 75 | print(""); 76 | break; 77 | } 78 | controller.add(result); 79 | } 80 | } catch (error, stackTrace) { 81 | controller.addError(error, stackTrace); 82 | await exit(); 83 | controller.close(); 84 | } 85 | }, 86 | onCancel: exit, 87 | sync: true); 88 | 89 | return controller.stream; 90 | } 91 | 92 | FutureOr exit() { 93 | try { 94 | stdin.lineMode = true; 95 | stdin.echoMode = true; 96 | } on StdinException {} 97 | 98 | var future = charQueue?.cancel(immediate: true); 99 | charQueue = null; 100 | return future; 101 | } 102 | 103 | Iterable linesToStatements(Iterable lines) sync* { 104 | String previous = ""; 105 | for (var line in lines) { 106 | write(previous == "" ? repl.prompt : repl.continuation); 107 | previous += line; 108 | stdout.writeln(line); 109 | if (repl.validator(previous)) { 110 | yield previous; 111 | previous = ""; 112 | } else { 113 | previous += '\n'; 114 | } 115 | } 116 | } 117 | 118 | StreamQueue? charQueue; 119 | 120 | List buffer = []; 121 | int cursor = 0; 122 | 123 | setCursor(int c) { 124 | if (c < 0) { 125 | c = 0; 126 | } else if (c > buffer.length) { 127 | c = buffer.length; 128 | } 129 | moveCursor(c - cursor); 130 | cursor = c; 131 | } 132 | 133 | write(String text) { 134 | stdout.write(text); 135 | } 136 | 137 | writeChar(int char) { 138 | stdout.writeCharCode(char); 139 | } 140 | 141 | int historyIndex = -1; 142 | String currentSaved = ""; 143 | 144 | String previousLines = ""; 145 | bool inContinuation = false; 146 | 147 | String? readStatement() { 148 | startReadStatement(); 149 | while (true) { 150 | int char = stdin.readByteSync(); 151 | if (char == eof && buffer.isEmpty) return null; 152 | if (char == escape) { 153 | var char = stdin.readByteSync(); 154 | if (char == c('[') || char == c('O')) { 155 | var ansi = stdin.readByteSync(); 156 | if (!handleAnsi(ansi)) { 157 | write('^['); 158 | input(char); 159 | input(ansi); 160 | } 161 | continue; 162 | } 163 | write('^['); 164 | } 165 | var result = processCharacter(char); 166 | if (result != null) return result; 167 | } 168 | } 169 | 170 | Future _readStatementAsync(StreamQueue charQueue) async { 171 | startReadStatement(); 172 | while (true) { 173 | int char = await charQueue.next; 174 | if (char == eof && buffer.isEmpty) return null; 175 | if (char == escape) { 176 | char = await charQueue.next; 177 | if (char == c('[') || char == c('O')) { 178 | var ansi = await charQueue.next; 179 | if (!handleAnsi(ansi)) { 180 | write('^['); 181 | input(char); 182 | input(ansi); 183 | } 184 | continue; 185 | } 186 | write('^['); 187 | } 188 | var result = processCharacter(char); 189 | if (result != null) return result; 190 | } 191 | } 192 | 193 | void startReadStatement() { 194 | write(repl.prompt); 195 | buffer.clear(); 196 | cursor = 0; 197 | historyIndex = -1; 198 | currentSaved = ""; 199 | inContinuation = false; 200 | previousLines = ""; 201 | } 202 | 203 | List yanked = []; 204 | 205 | String? processCharacter(int char) { 206 | switch (char) { 207 | case eof: 208 | if (cursor != buffer.length) delete(1); 209 | break; 210 | case clear: 211 | clearScreen(); 212 | break; 213 | case backspace: 214 | if (cursor > 0) { 215 | setCursor(cursor - 1); 216 | delete(1); 217 | } 218 | break; 219 | case killToEnd: 220 | yanked = delete(buffer.length - cursor); 221 | break; 222 | case killToStart: 223 | int oldCursor = cursor; 224 | setCursor(0); 225 | yanked = delete(oldCursor); 226 | break; 227 | case yank: 228 | yanked.forEach(input); 229 | break; 230 | case startOfLine: 231 | setCursor(0); 232 | break; 233 | case endOfLine: 234 | setCursor(buffer.length); 235 | break; 236 | case forward: 237 | setCursor(cursor + 1); 238 | break; 239 | case backward: 240 | setCursor(cursor - 1); 241 | break; 242 | case carriageReturn: 243 | case newLine: 244 | String contents = new String.fromCharCodes(buffer); 245 | setCursor(buffer.length); 246 | input(char); 247 | if (repl.history.isEmpty || contents != repl.history.first) { 248 | repl.history.insert(0, contents); 249 | } 250 | while (repl.history.length > repl.maxHistory) { 251 | repl.history.removeLast(); 252 | } 253 | if (char == carriageReturn) { 254 | write('\n'); 255 | } 256 | if (repl.validator(previousLines + contents)) { 257 | return previousLines + contents; 258 | } 259 | previousLines += contents + '\n'; 260 | buffer.clear(); 261 | cursor = 0; 262 | inContinuation = true; 263 | write(repl.continuation); 264 | break; 265 | default: 266 | input(char); 267 | break; 268 | } 269 | return null; 270 | } 271 | 272 | input(int char) { 273 | buffer.insert(cursor++, char); 274 | write(new String.fromCharCodes(buffer.skip(cursor - 1))); 275 | moveCursor(-(buffer.length - cursor)); 276 | } 277 | 278 | List delete(int amount) { 279 | if (amount <= 0) return []; 280 | int wipeAmount = buffer.length - cursor; 281 | if (amount > wipeAmount) amount = wipeAmount; 282 | write(' ' * wipeAmount); 283 | moveCursor(-wipeAmount); 284 | var result = buffer.sublist(cursor, cursor + amount); 285 | for (int i = 0; i < amount; i++) { 286 | buffer.removeAt(cursor); 287 | } 288 | write(new String.fromCharCodes(buffer.skip(cursor))); 289 | moveCursor(-(buffer.length - cursor)); 290 | return result; 291 | } 292 | 293 | replaceWith(String text) { 294 | moveCursor(-cursor); 295 | write(' ' * buffer.length); 296 | moveCursor(-buffer.length); 297 | write(text); 298 | buffer.clear(); 299 | buffer.addAll(text.codeUnits); 300 | cursor = buffer.length; 301 | } 302 | 303 | bool handleAnsi(int char) { 304 | switch (char) { 305 | case arrowLeft: 306 | setCursor(cursor - 1); 307 | return true; 308 | case arrowRight: 309 | setCursor(cursor + 1); 310 | return true; 311 | case arrowUp: 312 | if (historyIndex + 1 < repl.history.length) { 313 | if (historyIndex == -1) { 314 | currentSaved = new String.fromCharCodes(buffer); 315 | } else { 316 | repl.history[historyIndex] = new String.fromCharCodes(buffer); 317 | } 318 | replaceWith(repl.history[++historyIndex]); 319 | } 320 | return true; 321 | case arrowDown: 322 | if (historyIndex > 0) { 323 | repl.history[historyIndex] = new String.fromCharCodes(buffer); 324 | replaceWith(repl.history[--historyIndex]); 325 | } else if (historyIndex == 0) { 326 | historyIndex--; 327 | replaceWith(currentSaved); 328 | } 329 | return true; 330 | case home: 331 | setCursor(0); 332 | return true; 333 | case end: 334 | setCursor(buffer.length); 335 | return true; 336 | default: 337 | return false; 338 | } 339 | } 340 | 341 | moveCursor(int amount) { 342 | if (amount == 0) return; 343 | int amt = amount < 0 ? -amount : amount; 344 | String dir = amount < 0 ? 'D' : 'C'; 345 | write('$ansiEscape[$amt$dir'); 346 | } 347 | 348 | clearScreen() { 349 | write('$ansiEscape[2J'); // clear 350 | write('$ansiEscape[H'); // return home 351 | write(inContinuation ? repl.continuation : repl.prompt); 352 | write(new String.fromCharCodes(buffer)); 353 | moveCursor(cursor - buffer.length); 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: cli_repl 2 | description: A simple library for creating CLI REPLs 3 | version: 0.2.3 4 | homepage: https://github.com/jathak/cli_repl 5 | # author: Jen Thakar 6 | 7 | dependencies: 8 | async: ^2.5.0 9 | js: ^0.6.3 10 | 11 | dev_dependencies: 12 | grinder: ^0.9.0-nullsafety.0 13 | node_preamble: ^2.0.0 14 | test: ^1.16.5 15 | test_process: ^2.0.0 16 | 17 | environment: 18 | sdk: '>=2.12.0 <3.0.0' 19 | -------------------------------------------------------------------------------- /test/repl_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:test/test.dart'; 4 | import 'package:test_process/test_process.dart'; 5 | 6 | main() { 7 | group("VM", () => testWith(Platform.executable, 'example/example.dart')); 8 | group("Node.js", () => testWith('node', 'build/example/example.js')); 9 | } 10 | 11 | testWith(String executable, String script) { 12 | test('example repl works', () async { 13 | if (!await new File(script).exists()) { 14 | fail("$script does not exist"); 15 | } 16 | var process = await TestProcess.start(executable, [script]); 17 | process.stdin.writeln('4;'); 18 | process.stdin.writeln('a b c'); 19 | process.stdin.writeln('d e f;'); 20 | process.stdin.writeln(''); 21 | process.stdin.writeln('test;'); 22 | process.stdin.close(); 23 | expect( 24 | process.stdout, 25 | emitsInOrder([ 26 | '>>> 4;', 27 | '4;', 28 | '>>> a b c', 29 | '... d e f;', 30 | 'a b c', 31 | 'd e f;', 32 | '>>> ', 33 | '>>> test;', 34 | 'test;' 35 | ])); 36 | expect(process.stdout, emitsDone); 37 | await process.shouldExit(0); 38 | }); 39 | 40 | // This is a regression test to ensure that the reply code doesn't fall prey 41 | // to dart-lang/sdk#34775. 42 | test('example repl throws an error', () async { 43 | if (!await new File(script).exists()) { 44 | fail("$script does not exist"); 45 | } 46 | 47 | var process = await TestProcess.start(executable, [script]); 48 | process.stdin.writeln('throw;'); 49 | expect(process.stdout, emits(">>> throw;")); 50 | expect(process.stdout, emitsDone); 51 | 52 | expect(process.stderr, emitsThrough(contains("oh no!"))); 53 | 54 | await process.shouldExit(greaterThan(0)); 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /tool/grind.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:grinder/grinder.dart'; 4 | import "package:node_preamble/preamble.dart" as preamble; 5 | 6 | main() => grind(); 7 | 8 | @DefaultTask('Builds example.js') 9 | grind() { 10 | var dir = new Directory('build/example'); 11 | if (dir.existsSync()) dir.deleteSync(recursive: true); 12 | dir.createSync(recursive: true); 13 | var out = new File('build/example/example.js'); 14 | Dart2js.compile(new File('example/example.dart'), outFile: out); 15 | var text = out.readAsStringSync(); 16 | print('Adding preamble...'); 17 | out.writeAsStringSync(preamble.getPreamble() + text); 18 | print('Done.'); 19 | } 20 | --------------------------------------------------------------------------------