Commit a87e8d9d authored by Viet-Trung Luu's avatar Viet-Trung Luu

Add a simple prototype "terminal" example.

(Together with an app that echoes stuff from the terminal.)

R=erg@chromium.org

Review URL: https://codereview.chromium.org/999193002
parent 6d7725b3
Terminal
========
This is a prototype "terminal" application that can connect to any Mojo
application (providing the |terminal.TerminalClient| interface) and provide
interactive terminal facilities via an implementation of |mojo.files.File|.
I.e., once connected, the application can write to/read from the terminal by
performing the corresponding operations on a "file" (thus replicating
decades-old technology, poorly).
#!mojo mojo:sky_viewer
<!--
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-->
<import src="terminal.sky" />
<terminal url="mojo:echo_terminal" />
<!--
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-->
<import src="/sky/framework/sky-element.sky" />
<import src="/sky/framework/sky-scrollable.sky" />
<sky-element>
<template>
<style>
#control {
height: -webkit-fill-available;
background-color: black;
color: rgb(255, 191, 0);
font-family: 'Courier', 'monospace';
}
span {
white-space: nowrap;
}
</style>
<sky-scrollable id="control" contenteditable />
</template>
<script>
import 'dart:async';
import 'dart:core';
import 'dart:sky';
import 'package:examples/echo_terminal/terminal_client.mojom.dart' as terminal;
import '/sky/framework/embedder.dart';
import 'terminal_display.dart';
import 'terminal_file_impl.dart';
// Implements the <terminal> element, which implements a "terminal display". Has
// an |url| attribute, whose value should be a Mojo app that provides the
// |terminal.TerminalClient| service.
@Tagname('terminal')
class TerminalDisplayImpl extends SkyElement implements TerminalDisplay {
Element _control;
// Queue of unconsumed input (keystrokes), with the head at index 0.
// Keystrokes end up here if there's no reader (i.e., |getChar()|) pending,
// i.e., if |_readerQueue| is empty. Invariant: At most one of |_inputQueue|
// and |_readerQueue| may be nonempty at any given time.
List<int> _inputQueue;
// Queue of things waiting for input, with the head at index 0. If a keystroke
// is received and this is nonempty, the head is given that keystroke (and
// dequeued).
List<Completer<int>> _readerQueue;
TerminalDisplayImpl()
: _inputQueue = new List<int>(),
_readerQueue = new List<Completer<int>>() {
}
void shadowRootReady() {
_control = shadowRoot.getElementById('control');
_control.addEventListener('keypress', _handleKeyPress);
// Initialize with the first line.
_newLine();
_connect(getAttribute('url'));
}
void _handleKeyPress(KeyboardEvent event) {
// TODO(vtl): Add "echo" mode; do |putChar(event.charCode);| if echo is on.
if (_readerQueue.isEmpty) {
_inputQueue.add(event.charCode);
} else {
_readerQueue.removeAt(0).complete(event.charCode);
}
event.preventDefault();
}
void _newLine() {
_control.appendChild(document.createElement('span'));
}
// TODO(vtl): Should we always auto-connect? Should there be facilities for
// programmatically connecting? (What if the |url| attribute isn't set?)
void _connect(String url) {
var terminalClient = new terminal.TerminalClientProxy.unbound();
embedder.connectToService(url, terminalClient);
terminalClient.ptr.connectToTerminal(new TerminalFileImpl(this).stub);
terminalClient.close();
}
// |TerminalDisplay| implementation:
@override
void putChar(int byte) {
if (byte == 10 || byte == 13) {
_newLine();
return;
}
_control.lastChild.textContent += new String.fromCharCode(byte);
}
@override
Future<int> getChar() async {
if (_inputQueue.isNotEmpty) {
return new Future.value(_inputQueue.removeAt(0));
}
var completer = new Completer<int>();
_readerQueue.add(completer);
return completer.future;
}
}
_init(script) => register(script, TerminalDisplayImpl);
</script>
</sky-element>
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
// Interface for a terminal display, able to accept bytes (from the computer)
// and typically displaying them (or possibly handling them as escape codes,
// etc.) and able to get bytes from the "user".
abstract class TerminalDisplay {
void putChar(int byte);
Future<int> getChar();
// TODO(vtl): Should probably also have facilities for putting many bytes at a
// time or getting as many bytes as available.
}
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:core';
import 'mojo:core';
import 'package:services/files/file.mojom.dart' as files;
import 'package:services/files/types.mojom.dart' as files;
import 'terminal_display.dart';
// This implements a |mojo.files.File| that acts like a (pseudo)terminal. Bytes
// written to the |File| will be read by this implementation and passed on to
// the (Dart) |TerminalDisplay| (using |putChar()|). A read from the |File| will
// be completed if/when |TerminalDisplay| makes a byte available (via
// |getChar()|).
// TODO(vtl): This implementation is very incomplete.
class TerminalFileImpl implements files.File {
final files.FileStub stub;
final TerminalDisplay _display;
TerminalFileImpl(this._display) : stub = new files.FileStub.unbound() {
stub.impl = this;
}
// |files.File| implementation:
@override
Future close(Function responseFactory) async {
// TODO(vtl): We should probably do more than just say OK.
return responseFactory(files.Error_OK);
}
@override
Future read(int numBytesToRead, int offset, int whence,
Function responseFactory) async {
if (numBytesToRead < 0) {
return responseFactory(files.Error_INVALID_ARGUMENT, null);
}
// TODO(vtl): Error if |offset|/|whence| not appropriate.
if (numBytesToRead == 0) {
return responseFactory(files.Error_OK, []);
}
return responseFactory(files.Error_OK, [await _display.getChar()]);
}
@override
Future write(List<int> bytesToWrite, int offset, int whence,
Function responseFactory) async {
// TODO(vtl): Error if |offset|/|whence| not appropriate.
for (var c in bytesToWrite) {
_display.putChar(c);
}
return responseFactory(files.Error_OK, bytesToWrite.length);
}
@override
Future readToStream(MojoDataPipeProducer source, int offset, int whence,
int numBytesToRead, Function responseFactory) async {
// TODO(vtl)
return responseFactory(files.Error_UNIMPLEMENTED);
}
@override
Future writeFromStream(MojoDataPipeConsumer sink, int offset, int whence,
Function responseFactory) async {
// TODO(vtl)
return responseFactory(files.Error_UNIMPLEMENTED);
}
@override
Future tell(Function responseFactory) async {
// TODO(vtl)
return responseFactory(files.Error_UNIMPLEMENTED, 0);
}
@override
Future seek(int offset, int whence, Function responseFactory) async {
// TODO(vtl)
return responseFactory(files.Error_UNIMPLEMENTED, 0);
}
@override
Future stat(Function responseFactory) async {
// TODO(vtl)
return responseFactory(files.Error_UNIMPLEMENTED, null);
}
@override
Future truncate(int size, Function responseFactory) async {
// TODO(vtl)
return responseFactory(files.Error_UNIMPLEMENTED);
}
@override
Future touch(files.TimespecOrNow atime, files.TimespecOrNow mtime,
Function responseFactory) async {
// TODO(vtl)
return responseFactory(files.Error_UNIMPLEMENTED);
}
@override
Future dup(Object file, Function responseFactory) async {
// TODO(vtl)
return responseFactory(files.Error_UNIMPLEMENTED);
}
@override
Future reopen(Object file, int openFlags, Function responseFactory) async {
// TODO(vtl)
return responseFactory(files.Error_UNIMPLEMENTED);
}
@override
Future asBuffer(Function responseFactory) async {
// TODO(vtl)
return responseFactory(files.Error_UNIMPLEMENTED, null);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment