package bin; import kernel.binstore.BinStore; import kernel.ps.ProcessHandle; import kernel.ps.Process; import kernel.ps.ProcessManager; import lib.Color; import kernel.ui.WindowContext; using tink.CoreApi; class Terminal implements Process { private static inline final MAX_BACKLOG:Int = 100; private var handle:ProcessHandle; private var ctx:WindowContext; private var requestRender: () -> Void; private var input:String = ""; private var backlog:Array = []; private var history:Array = []; private var historyIndex:Int = 0; private var runningPID:PID = -1; public function new() {} public function run(handle: ProcessHandle): Void { this.handle = handle; var statelessContext = handle.createStatelessWindowContext(); this.ctx = statelessContext.ctx; this.requestRender = statelessContext.requestRender; statelessContext.setRenderFunc(this.render); // Add input event handlers handle.addCallbackLink(this.ctx.onChar.handle(char -> { if (this.runningPID > 0) return; this.input += char; this.requestRender(); })); // Add key event handlers handle.addCallbackLink(this.ctx.onKey.handle(e -> { switch (e.keyCode) { case 259: // Backspace if (this.runningPID > 0) return; this.input = this.input.substr(0, this.input.length - 1); this.requestRender(); case 257: // Enter if (this.runningPID > 0) return; this.backlog.push("> " + this.input); var command = this.input; this.input = ""; this.requestRender(); this.historyIndex = 0; this.invokeCommand(command); case 269: // END this.stopCurrentPS(); case 265: // UP if (this.historyIndex < this.history.length) { this.historyIndex++; this.input = this.history[this.history.length - this.historyIndex]; this.requestRender(); } } })); this.requestRender(); } private function stopCurrentPS() { if (this.runningPID < 0) { return; } ProcessManager.kill(this.runningPID); } private function render() { var size = this.ctx.getSize(); var linesAvailable = size.y - 1; var start:Int = this.backlog.length - linesAvailable; for (i in 0...linesAvailable) { var line = this.backlog[start + i]; this.ctx.setCursorPos(0, i); this.ctx.clearLine(); if (line != null) { this.ctx.write(line); } } this.ctx.setCursorPos(0, size.y - 1); this.ctx.clearLine(); this.ctx.setTextColor(Color.Blue); this.ctx.write("> "); this.ctx.setTextColor(Color.White); this.ctx.write(this.input); if (this.runningPID < 0) { this.ctx.setCursorBlink(true); } else { this.ctx.setCursorBlink(false); } } private function invokeCommand(command:String):Void { var args = this.parseArgs(command); if (args.length == 0) { return; } this.history.push(command); if (this.history.length > MAX_BACKLOG) { this.history.shift(); } var commandName = args[0]; // Handle built-in commands switch (commandName) { case "clear": this.clear(); return; } var commandArgs:Array = args.slice(1); var hadInput = false; var ps = getProgByName(commandName); if (ps == null) { this.backlog.push("Unknown command: " + commandName); this.requestRender(); return; } this.runningPID = ProcessManager.run(ps,{ args: commandArgs, onWrite: (s:String) -> { if (s == "") { return; } if (!hadInput) { // Add a new line, so that the input is not on the same line as the command this.backlog.push(""); hadInput = true; } for (line in s.split("\n")) { if (line == ""){ this.backlog.push(""); } else { this.backlog[this.backlog.length - 1] += s; } // Trim the backlog if it's too long if (this.backlog.length > MAX_BACKLOG) { this.backlog.shift(); } } this.requestRender(); }, onExit: (success:Bool) -> { this.runningPID = -1; if (this.backlog[this.backlog.length - 1] == "") { this.backlog.pop(); } this.requestRender(); } }); this.ctx.setCursorBlink(false); } /** Convter a command string into an array of arguments where the first element is the command name **/ private function parseArgs(command:String):Array { // TODO: tim and quote handling return command.split(" "); } private function clear() { this.backlog = []; this.requestRender(); } private function getProgByName(name:String):Process { var bin = BinStore.getBinByAlias(name); if (bin == null) { return null; } return Type.createInstance(bin.c,[]); } private function moveCursorToInput() { var size = this.ctx.getSize(); this.ctx.setCursorPos(this.input.length + 2, size.y - 1); } }