Interactive CLI in Dart
Example how to build a interactive CLI application in Dart
/// Copyright 2020, Pascal Welsch | |
/// | |
/// Licensed under the Apache License, Version 2.0 (the "License"); | |
/// you may not use this file except in compliance with the License. | |
/// You may obtain a copy of the License at | |
/// | |
/// http://www.apache.org/licenses/LICENSE-2.0 | |
/// | |
/// Unless required by applicable law or agreed to in writing, software | |
/// distributed under the License is distributed on an "AS IS" BASIS, | |
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
/// See the License for the specific language governing permissions and | |
/// limitations under the License. | |
import 'dart:io' as io; | |
Future<void> main() async { | |
try { | |
io.stdin.echoMode = false; | |
io.stdin.lineMode = false; | |
} catch (_) { | |
print("Running in non-interactive mode, exiting"); | |
return; | |
} | |
print("How would you rate writing CLIs with Dart? (Press <space> to select)"); | |
final answer = await selectAnswer(["bad", "good", "amazing 🎉"]); | |
print("selected $answer"); | |
io.exit(0); | |
} | |
const ESC = '\u001B['; | |
const eraseLine = ESC + '2K'; | |
const cursorUp = ESC + '1A'; | |
const cursorHide = ESC + '?25l'; | |
const cursorShow = ESC + '?25h'; | |
const arrowUp = ESC + 'A'; | |
const arrowDown = ESC + 'B'; | |
const space = " "; | |
Future<String> selectAnswer(List<String> options) async { | |
var selection = 0; | |
String _displayed = ""; | |
/// replaces all visible text in terminal with new one | |
void render(String text) { | |
final lines = _displayed.runes.where((c) => c == 10 /*\n*/).length; | |
io.stdout.write(eraseLine); | |
for (var i = 0; i < lines; i++) { | |
io.stdout.write(eraseLine); | |
io.stdout.write(cursorUp); | |
} | |
io.stdout.writeCharCode(13); | |
io.stdout.write(text); | |
_displayed = text; | |
} | |
/// print the options + selection to terminal | |
void showOptions() { | |
final sb = StringBuffer(); | |
for (var i = 0; i < options.length; i++) { | |
final icon = () { | |
if (selection == i) return "●"; | |
return "○"; | |
}(); | |
sb.write(" $icon ${options[i]}\n"); | |
} | |
render(sb.toString()); | |
} | |
void select() { | |
render(""); | |
io.stdout.write(cursorShow); | |
} | |
// hide cursor | |
io.stdout.write(cursorHide); | |
// show cursor on forced exit | |
io.ProcessSignal.sigint.watch().listen((event) { | |
io.stdout.write(cursorShow); | |
io.exit(1); | |
}); | |
showOptions(); | |
await for (final input in io.stdin) { | |
final String command = String.fromCharCodes(input); | |
switch (command) { | |
case arrowUp: | |
selection = (options.length + selection - 1) % options.length; | |
showOptions(); | |
continue; | |
case arrowDown: | |
selection = (selection + 1) % options.length; | |
showOptions(); | |
continue; | |
case space: | |
select(); | |
return options[selection]; | |
} | |
} | |
throw "nothing selected"; | |
} |