OLD | NEW |
| (Empty) |
1 <!-- | |
2 // Copyright 2015 The Chromium Authors. All rights reserved. | |
3 // Use of this source code is governed by a BSD-style license that can be | |
4 // found in the LICENSE file. | |
5 --> | |
6 <import src="/sky/framework/elements/sky-element.sky" /> | |
7 <sky-element> | |
8 <template> | |
9 <style> | |
10 #container { | |
11 height: -webkit-fill-available; | |
12 } | |
13 #input-box { | |
14 position: absolute; | |
15 left: 0; | |
16 top: 0; | |
17 z-index: 10; | |
18 width: 100%; | |
19 height: 100%; | |
20 opacity: 0; | |
21 } | |
22 #output-box { | |
23 position: absolute; | |
24 left: 0; | |
25 top: 0; | |
26 z-index: 0; | |
27 width: 100%; | |
28 height: 100%; | |
29 background-color: black; | |
30 color: rgb(255, 191, 0); | |
31 font-family: 'Courier', 'monospace'; | |
32 font-size: small; | |
33 } | |
34 .line { | |
35 height: 1em; | |
36 white-space: nowrap; | |
37 } | |
38 </style> | |
39 <div id="container"> | |
40 <div id="input-box" contenteditable /> | |
41 <div id="output-box" /> | |
42 </div> | |
43 </div> | |
44 </template> | |
45 <script> | |
46 import 'dart:async'; | |
47 import 'dart:core'; | |
48 import 'dart:sky'; | |
49 import 'package:mojo/services/terminal/public/interfaces/terminal_client.mojom.d
art' as terminal; | |
50 import 'package:sky/framework/embedder.dart'; | |
51 import 'terminal_display.dart'; | |
52 import 'terminal_file_impl.dart'; | |
53 | |
54 // Implements the <terminal> element, which implements a "terminal display". Has | |
55 // an |url| attribute, whose value should be a Mojo app that provides the | |
56 // |terminal.TerminalClient| service. | |
57 @Tagname('terminal') | |
58 class TerminalDisplayImpl extends SkyElement implements TerminalDisplay { | |
59 Element _inputBox; | |
60 Element _outputBox; | |
61 int _maxLines; | |
62 | |
63 // Queue of unconsumed input (keystrokes), with the head at index 0. | |
64 // Keystrokes end up here if there's no reader (i.e., |getChar()|) pending, | |
65 // i.e., if |_readerQueue| is empty. Invariant: At most one of |_inputQueue| | |
66 // and |_readerQueue| may be nonempty at any given time. | |
67 List<int> _inputQueue; | |
68 | |
69 // Queue of things waiting for input, with the head at index 0. If a keystroke | |
70 // is received and this is nonempty, the head is given that keystroke (and | |
71 // dequeued). | |
72 List<Completer<int>> _readerQueue; | |
73 | |
74 TerminalDisplayImpl() | |
75 : _inputQueue = new List<int>(), | |
76 _readerQueue = new List<Completer<int>>() { | |
77 } | |
78 | |
79 void shadowRootReady() { | |
80 _inputBox = shadowRoot.getElementById('input-box'); | |
81 _inputBox.addEventListener('keydown', _handleKeyDown); | |
82 _inputBox.addEventListener('keypress', _handleKeyPress); | |
83 _inputBox.addEventListener('wheel', _handleWheel); | |
84 _outputBox = shadowRoot.getElementById('output-box'); | |
85 | |
86 // Hack to allow |_newLine()| to work. | |
87 _maxLines = 100; | |
88 | |
89 // Initialize with the first line. | |
90 _newLine(); | |
91 | |
92 // Actually compute the maximum number of lines. | |
93 // TODO(vtl): Recompute on resize. | |
94 _maxLines = _outputBox.clientHeight ~/ _outputBox.firstChild.offsetHeight; | |
95 | |
96 var url = getAttribute('url'); | |
97 if (url != null) { | |
98 connect(url); | |
99 } | |
100 } | |
101 | |
102 void _handleKeyDown(KeyboardEvent event) { | |
103 // TODO(vtl): In general, our key handling is a total hack (due in part to | |
104 // sky's keyboard support being incomplete) -- e.g., we shouldn't have to | |
105 // make our div contenteditable. We have to intercept backspace (^H) here, | |
106 // since we won't actually get a keypress event for it. (Possibly, we should | |
107 // only handle keydown instead of keypress, but then we'd have to handle | |
108 // shift, etc. ourselves.) | |
109 if (event.key == 8) { | |
110 _enqueueChar(8); | |
111 event.preventDefault(); | |
112 } | |
113 } | |
114 | |
115 void _handleKeyPress(KeyboardEvent event) { | |
116 if (event.charCode != 0) { | |
117 _enqueueChar(event.charCode); | |
118 } | |
119 event.preventDefault(); | |
120 } | |
121 | |
122 void _handleWheel(WheelEvent event) { | |
123 _outputBox.dispatchEvent(event); | |
124 } | |
125 | |
126 void _enqueueChar(int charCode) { | |
127 // TODO(vtl): Add "echo" mode; do |putChar(event.charCode);| if echo is on. | |
128 | |
129 if (_readerQueue.isEmpty) { | |
130 _inputQueue.add(charCode); | |
131 } else { | |
132 _readerQueue.removeAt(0).complete(charCode); | |
133 } | |
134 } | |
135 | |
136 void _backspace() { | |
137 var oldText = _outputBox.lastChild.textContent; | |
138 if (oldText.length > 0) { | |
139 _outputBox.lastChild.textContent = oldText.substring(0, | |
140 oldText.length - 1); | |
141 } | |
142 } | |
143 | |
144 void _newLine() { | |
145 var line = document.createElement('div'); | |
146 line.setAttribute('class', 'line'); | |
147 _outputBox.appendChild(line); | |
148 | |
149 // Scroll if necessary. | |
150 var children = _outputBox.getChildNodes(); | |
151 if (children.length > _maxLines) { | |
152 children = new List.from(children.skip(children.length - _maxLines)); | |
153 _outputBox.setChildren(children); | |
154 } | |
155 } | |
156 | |
157 void _clear() { | |
158 _outputBox.setChildren([]); | |
159 _newLine(); | |
160 } | |
161 | |
162 void connect(String url) { | |
163 var terminalClient = new terminal.TerminalClientProxy.unbound(); | |
164 embedder.connectToService(url, terminalClient); | |
165 terminalClient.ptr.connectToTerminal(new TerminalFileImpl(this).stub); | |
166 terminalClient.close(); | |
167 } | |
168 | |
169 void putString(String s) { | |
170 for (var i = 0; i < s.length; i++) { | |
171 putChar(s.codeUnitAt(i)); | |
172 } | |
173 } | |
174 | |
175 // |TerminalDisplay| implementation: | |
176 | |
177 @override | |
178 void putChar(int byte) { | |
179 // Fast-path for printable chars. | |
180 if (byte >= 32) { | |
181 _outputBox.lastChild.textContent += new String.fromCharCode(byte); | |
182 return; | |
183 } | |
184 | |
185 switch (byte) { | |
186 case 8: // BS (^H). | |
187 _backspace(); | |
188 break; | |
189 case 10: // LF ('\n'). | |
190 _newLine(); // TODO(vtl): LF and CR should be separated. | |
191 break; | |
192 case 12: // FF (^L). | |
193 _clear(); | |
194 break; | |
195 case 13: // CR ('\r'). | |
196 _newLine(); // TODO(vtl): LF and CR should be separated. | |
197 break; | |
198 default: | |
199 // Should beep or something. | |
200 break; | |
201 } | |
202 } | |
203 | |
204 @override | |
205 Future<int> getChar() async { | |
206 if (_inputQueue.isNotEmpty) { | |
207 return new Future.value(_inputQueue.removeAt(0)); | |
208 } | |
209 | |
210 var completer = new Completer<int>(); | |
211 _readerQueue.add(completer); | |
212 return completer.future; | |
213 } | |
214 } | |
215 | |
216 _init(script) => register(script, TerminalDisplayImpl); | |
217 </script> | |
218 </sky-element> | |
OLD | NEW |