b3f83919d7ebb6e6483dac8893a4c0ac0b83545c
[nikiroo-utils.git] / src / jexer / TTerminalWindow.java
1 /*
2 * Jexer - Java Text User Interface
3 *
4 * License: LGPLv3 or later
5 *
6 * This module is licensed under the GNU Lesser General Public License
7 * Version 3. Please see the file "COPYING" in this directory for more
8 * information about the GNU Lesser General Public License Version 3.
9 *
10 * Copyright (C) 2015 Kevin Lamonte
11 *
12 * This program is free software; you can redistribute it and/or
13 * modify it under the terms of the GNU Lesser General Public License
14 * as published by the Free Software Foundation; either version 3 of
15 * the License, or (at your option) any later version.
16 *
17 * This program is distributed in the hope that it will be useful, but
18 * WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20 * General Public License for more details.
21 *
22 * You should have received a copy of the GNU Lesser General Public
23 * License along with this program; if not, see
24 * http://www.gnu.org/licenses/, or write to the Free Software
25 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
26 * 02110-1301 USA
27 *
28 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
29 * @version 1
30 */
31 package jexer;
32
33 import java.io.InputStream;
34 import java.io.IOException;
35 import java.io.OutputStream;
36 import java.io.UnsupportedEncodingException;
37 import java.util.LinkedList;
38 import java.util.List;
39 import java.util.Map;
40
41 import jexer.bits.Cell;
42 import jexer.bits.CellAttributes;
43 import jexer.event.TKeypressEvent;
44 import jexer.event.TMouseEvent;
45 import jexer.event.TResizeEvent;
46 import jexer.tterminal.DisplayLine;
47 import jexer.tterminal.ECMA48;
48 import static jexer.TKeypress.*;
49
50 /**
51 * TTerminalWindow exposes a ECMA-48 / ANSI X3.64 style terminal in a window.
52 */
53 public class TTerminalWindow extends TWindow {
54
55 /**
56 * The emulator.
57 */
58 private ECMA48 emulator;
59
60 /**
61 * The Process created by the shell spawning constructor.
62 */
63 private Process shell;
64
65 /**
66 * Vertical scrollbar.
67 */
68 private TVScroller vScroller;
69
70 /**
71 * Public constructor spawns a shell.
72 *
73 * @param application TApplication that manages this window
74 * @param x column relative to parent
75 * @param y row relative to parent
76 * @param flags mask of CENTERED, MODAL, or RESIZABLE
77 */
78 public TTerminalWindow(final TApplication application, final int x,
79 final int y, final int flags) {
80
81 super(application, "Terminal", x, y, 80 + 2, 24 + 2, flags);
82
83 // Assume XTERM
84 ECMA48.DeviceType deviceType = ECMA48.DeviceType.XTERM;
85
86 try {
87 String [] cmdShellWindows = {
88 "cmd.exe"
89 };
90
91 // You cannot run a login shell in a bare Process interactively,
92 // due to libc's behavior of buffering when stdin/stdout aren't a
93 // tty. Use 'script' instead to run a shell in a pty.
94 String [] cmdShell = {
95 "script", "-fqe", "/dev/null"
96 };
97 // Spawn a shell and pass its I/O to the other constructor.
98
99 ProcessBuilder pb;
100 if (System.getProperty("os.name").startsWith("Windows")) {
101 pb = new ProcessBuilder(cmdShellWindows);
102 } else {
103 pb = new ProcessBuilder(cmdShell);
104 }
105 Map<String, String> env = pb.environment();
106 env.put("TERM", ECMA48.deviceTypeTerm(deviceType));
107 env.put("LANG", ECMA48.deviceTypeLang(deviceType, "en"));
108 env.put("COLUMNS", "80");
109 env.put("LINES", "24");
110 pb.redirectErrorStream(true);
111 shell = pb.start();
112 emulator = new ECMA48(deviceType, shell.getInputStream(),
113 shell.getOutputStream());
114 } catch (IOException e) {
115 e.printStackTrace();
116 }
117
118 // Setup the scroll bars
119 onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
120 getHeight()));
121 }
122
123 /**
124 * Public constructor.
125 *
126 * @param application TApplication that manages this window
127 * @param x column relative to parent
128 * @param y row relative to parent
129 * @param flags mask of CENTERED, MODAL, or RESIZABLE
130 * @param input an InputStream connected to the remote side. For type ==
131 * XTERM, input is converted to a Reader with UTF-8 encoding.
132 * @param output an OutputStream connected to the remote user. For type
133 * == XTERM, output is converted to a Writer with UTF-8 encoding.
134 * @throws UnsupportedEncodingException if an exception is thrown when
135 * creating the InputStreamReader
136 */
137 public TTerminalWindow(final TApplication application, final int x,
138 final int y, final int flags, final InputStream input,
139 final OutputStream output) throws UnsupportedEncodingException {
140
141 super(application, "Terminal", x, y, 80 + 2, 24 + 2, flags);
142
143 emulator = new ECMA48(ECMA48.DeviceType.XTERM, input, output);
144
145 // Setup the scroll bars
146 onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
147 getHeight()));
148
149 }
150
151 /**
152 * Draw the display buffer.
153 */
154 @Override
155 public void draw() {
156 // Synchronize against the emulator so we don't stomp on its reader
157 // thread.
158 synchronized (emulator) {
159
160 // Update the scroll bars
161 reflow();
162
163 // Draw the box using my superclass
164 super.draw();
165
166 List<DisplayLine> scrollback = emulator.getScrollbackBuffer();
167 List<DisplayLine> display = emulator.getDisplayBuffer();
168
169 // Put together the visible rows
170 int visibleHeight = getHeight() - 2;
171 int visibleBottom = scrollback.size() + display.size()
172 + vScroller.getValue();
173 assert (visibleBottom >= 0);
174
175 List<DisplayLine> preceedingBlankLines = new LinkedList<DisplayLine>();
176 int visibleTop = visibleBottom - visibleHeight;
177 if (visibleTop < 0) {
178 for (int i = visibleTop; i < 0; i++) {
179 preceedingBlankLines.add(emulator.getBlankDisplayLine());
180 }
181 visibleTop = 0;
182 }
183 assert (visibleTop >= 0);
184
185 List<DisplayLine> displayLines = new LinkedList<DisplayLine>();
186 displayLines.addAll(scrollback);
187 displayLines.addAll(display);
188
189 List<DisplayLine> visibleLines = new LinkedList<DisplayLine>();
190 visibleLines.addAll(preceedingBlankLines);
191 visibleLines.addAll(displayLines.subList(visibleTop,
192 visibleBottom));
193
194 visibleHeight -= visibleLines.size();
195 assert (visibleHeight >= 0);
196
197 // Now draw the emulator screen
198 int row = 1;
199 for (DisplayLine line: visibleLines) {
200 int widthMax = emulator.getWidth();
201 if (line.isDoubleWidth()) {
202 widthMax /= 2;
203 }
204 if (widthMax > getWidth() - 2) {
205 widthMax = getWidth() - 2;
206 }
207 for (int i = 0; i < widthMax; i++) {
208 Cell ch = line.charAt(i);
209 Cell newCell = new Cell();
210 newCell.setTo(ch);
211 boolean reverse = line.isReverseColor() ^ ch.isReverse();
212 newCell.setReverse(false);
213 if (reverse) {
214 newCell.setBackColor(ch.getForeColor());
215 newCell.setForeColor(ch.getBackColor());
216 }
217 if (line.isDoubleWidth()) {
218 getScreen().putCharXY((i * 2) + 1, row, newCell);
219 getScreen().putCharXY((i * 2) + 2, row, ' ', newCell);
220 } else {
221 getScreen().putCharXY(i + 1, row, newCell);
222 }
223 }
224 row++;
225 if (row == getHeight() - 1) {
226 // Don't overwrite the box edge
227 break;
228 }
229 }
230 CellAttributes background = new CellAttributes();
231 // Fill in the blank lines on bottom
232 for (int i = 0; i < visibleHeight; i++) {
233 getScreen().hLineXY(1, i + row, getWidth() - 2, ' ',
234 background);
235 }
236
237 } // synchronized (emulator)
238
239 }
240
241 /**
242 * Handle window close.
243 */
244 @Override public void onClose() {
245 if (shell != null) {
246 shell.destroy();
247 shell = null;
248 } else {
249 emulator.close();
250 }
251 }
252
253 /**
254 * Copy out variables from the emulator that TTerminal has to expose on
255 * screen.
256 */
257 private void readEmulatorState() {
258 // Synchronize against the emulator so we don't stomp on its reader
259 // thread.
260 synchronized (emulator) {
261
262 setCursorX(emulator.getCursorX() + 1);
263 setCursorY(emulator.getCursorY() + 1
264 + (getHeight() - 2 - emulator.getHeight()));
265 if (vScroller != null) {
266 setCursorY(getCursorY() - vScroller.getValue());
267 }
268 setCursorVisible(emulator.isCursorVisible());
269 if (getCursorX() > getWidth() - 2) {
270 setCursorVisible(false);
271 }
272 if ((getCursorY() > getHeight() - 2) || (getCursorY() < 0)) {
273 setCursorVisible(false);
274 }
275 if (emulator.getScreenTitle().length() > 0) {
276 // Only update the title if the shell is still alive
277 if (shell != null) {
278 setTitle(emulator.getScreenTitle());
279 }
280 }
281
282 // Check to see if the shell has died.
283 if (!emulator.isReading() && (shell != null)) {
284 try {
285 int rc = shell.exitValue();
286 // The emulator exited on its own, all is fine
287 setTitle(String.format("%s [Completed - %d]",
288 getTitle(), rc));
289 shell = null;
290 emulator.close();
291 } catch (IllegalThreadStateException e) {
292 // The emulator thread has exited, but the shell Process
293 // hasn't figured that out yet. Do nothing, we will see
294 // this in a future tick.
295 }
296 } else if (emulator.isReading() && (shell != null)) {
297 // The shell might be dead, let's check
298 try {
299 int rc = shell.exitValue();
300 // If we got here, the shell died.
301 setTitle(String.format("%s [Completed - %d]",
302 getTitle(), rc));
303 shell = null;
304 emulator.close();
305 } catch (IllegalThreadStateException e) {
306 // The shell is still running, do nothing.
307 }
308 }
309
310 } // synchronized (emulator)
311 }
312
313 /**
314 * Handle window/screen resize events.
315 *
316 * @param resize resize event
317 */
318 @Override
319 public void onResize(final TResizeEvent resize) {
320
321 // Synchronize against the emulator so we don't stomp on its reader
322 // thread.
323 synchronized (emulator) {
324
325 if (resize.getType() == TResizeEvent.Type.WIDGET) {
326 // Resize the scroll bars
327 reflow();
328
329 // Get out of scrollback
330 vScroller.setValue(0);
331 }
332 return;
333
334 } // synchronized (emulator)
335 }
336
337 /**
338 * Resize scrollbars for a new width/height.
339 */
340 private void reflow() {
341
342 // Synchronize against the emulator so we don't stomp on its reader
343 // thread.
344 synchronized (emulator) {
345
346 // Pull cursor information
347 readEmulatorState();
348
349 // Vertical scrollbar
350 if (vScroller == null) {
351 vScroller = new TVScroller(this, getWidth() - 2, 0,
352 getHeight() - 2);
353 vScroller.setBottomValue(0);
354 vScroller.setValue(0);
355 } else {
356 vScroller.setX(getWidth() - 2);
357 vScroller.setHeight(getHeight() - 2);
358 }
359 vScroller.setTopValue(getHeight() - 2
360 - (emulator.getScrollbackBuffer().size()
361 + emulator.getDisplayBuffer().size()));
362 vScroller.setBigChange(getHeight() - 2);
363
364 } // synchronized (emulator)
365 }
366
367 /**
368 * Check if a mouse press/release/motion event coordinate is over the
369 * emulator.
370 *
371 * @param mouse a mouse-based event
372 * @return whether or not the mouse is on the emulator
373 */
374 private final boolean mouseOnEmulator(final TMouseEvent mouse) {
375
376 synchronized (emulator) {
377 if (!emulator.isReading()) {
378 return false;
379 }
380 }
381
382 if ((mouse.getAbsoluteX() >= getAbsoluteX() + 1)
383 && (mouse.getAbsoluteX() < getAbsoluteX() + getWidth() - 1)
384 && (mouse.getAbsoluteY() >= getAbsoluteY() + 1)
385 && (mouse.getAbsoluteY() < getAbsoluteY() + getHeight() - 1)
386 ) {
387 return true;
388 }
389 return false;
390 }
391
392 /**
393 * Handle keystrokes.
394 *
395 * @param keypress keystroke event
396 */
397 @Override
398 public void onKeypress(final TKeypressEvent keypress) {
399
400 // Scrollback up/down
401 if (keypress.equals(kbShiftPgUp)
402 || keypress.equals(kbCtrlPgUp)
403 || keypress.equals(kbAltPgUp)
404 ) {
405 vScroller.bigDecrement();
406 return;
407 }
408 if (keypress.equals(kbShiftPgDn)
409 || keypress.equals(kbCtrlPgDn)
410 || keypress.equals(kbAltPgDn)
411 ) {
412 vScroller.bigIncrement();
413 return;
414 }
415
416 // Synchronize against the emulator so we don't stomp on its reader
417 // thread.
418 synchronized (emulator) {
419 if (emulator.isReading()) {
420 // Get out of scrollback
421 vScroller.setValue(0);
422 emulator.keypress(keypress.getKey());
423
424 // UGLY HACK TIME! cmd.exe needs CRLF, not just CR, so if
425 // this is kBEnter then also send kbCtrlJ.
426 if (System.getProperty("os.name").startsWith("Windows")) {
427 if (keypress.equals(kbEnter)) {
428 emulator.keypress(kbCtrlJ);
429 }
430 }
431
432 readEmulatorState();
433 return;
434 }
435 }
436
437 // Process is closed, honor "normal" TUI keystrokes
438 super.onKeypress(keypress);
439 }
440
441 /**
442 * Handle mouse press events.
443 *
444 * @param mouse mouse button press event
445 */
446 @Override
447 public void onMouseDown(final TMouseEvent mouse) {
448 if (inWindowMove || inWindowResize) {
449 // TWindow needs to deal with this.
450 super.onMouseDown(mouse);
451 return;
452 }
453
454 if (mouse.isMouseWheelUp()) {
455 vScroller.decrement();
456 return;
457 }
458 if (mouse.isMouseWheelDown()) {
459 vScroller.increment();
460 return;
461 }
462 if (mouseOnEmulator(mouse)) {
463 synchronized (emulator) {
464 mouse.setX(mouse.getX() - 1);
465 mouse.setY(mouse.getY() - 1);
466 emulator.mouse(mouse);
467 readEmulatorState();
468 return;
469 }
470 }
471
472 // Emulator didn't consume it, pass it on
473 super.onMouseDown(mouse);
474 }
475
476 /**
477 * Handle mouse release events.
478 *
479 * @param mouse mouse button release event
480 */
481 @Override
482 public void onMouseUp(final TMouseEvent mouse) {
483 if (inWindowMove || inWindowResize) {
484 // TWindow needs to deal with this.
485 super.onMouseUp(mouse);
486 return;
487 }
488
489 if (mouseOnEmulator(mouse)) {
490 synchronized (emulator) {
491 mouse.setX(mouse.getX() - 1);
492 mouse.setY(mouse.getY() - 1);
493 emulator.mouse(mouse);
494 readEmulatorState();
495 return;
496 }
497 }
498
499 // Emulator didn't consume it, pass it on
500 super.onMouseUp(mouse);
501 }
502
503 /**
504 * Handle mouse motion events.
505 *
506 * @param mouse mouse motion event
507 */
508 @Override
509 public void onMouseMotion(final TMouseEvent mouse) {
510 if (inWindowMove || inWindowResize) {
511 // TWindow needs to deal with this.
512 super.onMouseMotion(mouse);
513 return;
514 }
515
516 if (mouseOnEmulator(mouse)) {
517 synchronized (emulator) {
518 mouse.setX(mouse.getX() - 1);
519 mouse.setY(mouse.getY() - 1);
520 emulator.mouse(mouse);
521 readEmulatorState();
522 return;
523 }
524 }
525
526 // Emulator didn't consume it, pass it on
527 super.onMouseMotion(mouse);
528 }
529
530 }