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