Fix for ich() corrupting display
[fanfix.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
5dfd1c11
KL
68 /**
69 * Claim the keystrokes the emulator will need.
70 */
71 private void addShortcutKeys() {
72 addShortcutKeypress(kbCtrlA);
73 addShortcutKeypress(kbCtrlB);
74 addShortcutKeypress(kbCtrlC);
75 addShortcutKeypress(kbCtrlD);
76 addShortcutKeypress(kbCtrlE);
77 addShortcutKeypress(kbCtrlF);
78 addShortcutKeypress(kbCtrlG);
79 addShortcutKeypress(kbCtrlH);
80 addShortcutKeypress(kbCtrlU);
81 addShortcutKeypress(kbCtrlJ);
82 addShortcutKeypress(kbCtrlK);
83 addShortcutKeypress(kbCtrlL);
84 addShortcutKeypress(kbCtrlM);
85 addShortcutKeypress(kbCtrlN);
86 addShortcutKeypress(kbCtrlO);
87 addShortcutKeypress(kbCtrlP);
88 addShortcutKeypress(kbCtrlQ);
89 addShortcutKeypress(kbCtrlR);
90 addShortcutKeypress(kbCtrlS);
91 addShortcutKeypress(kbCtrlT);
92 addShortcutKeypress(kbCtrlU);
93 addShortcutKeypress(kbCtrlV);
94 addShortcutKeypress(kbCtrlW);
95 addShortcutKeypress(kbCtrlX);
96 addShortcutKeypress(kbCtrlY);
97 addShortcutKeypress(kbCtrlZ);
98 addShortcutKeypress(kbF1);
99 addShortcutKeypress(kbF2);
100 addShortcutKeypress(kbF3);
101 addShortcutKeypress(kbF4);
102 addShortcutKeypress(kbF5);
103 addShortcutKeypress(kbF6);
104 addShortcutKeypress(kbF7);
105 addShortcutKeypress(kbF8);
106 addShortcutKeypress(kbF9);
107 addShortcutKeypress(kbF10);
108 addShortcutKeypress(kbF11);
109 addShortcutKeypress(kbF12);
110 addShortcutKeypress(kbAltA);
111 addShortcutKeypress(kbAltB);
112 addShortcutKeypress(kbAltC);
113 addShortcutKeypress(kbAltD);
114 addShortcutKeypress(kbAltE);
115 addShortcutKeypress(kbAltF);
116 addShortcutKeypress(kbAltG);
117 addShortcutKeypress(kbAltH);
118 addShortcutKeypress(kbAltU);
119 addShortcutKeypress(kbAltJ);
120 addShortcutKeypress(kbAltK);
121 addShortcutKeypress(kbAltL);
122 addShortcutKeypress(kbAltM);
123 addShortcutKeypress(kbAltN);
124 addShortcutKeypress(kbAltO);
125 addShortcutKeypress(kbAltP);
126 addShortcutKeypress(kbAltQ);
127 addShortcutKeypress(kbAltR);
128 addShortcutKeypress(kbAltS);
129 addShortcutKeypress(kbAltT);
130 addShortcutKeypress(kbAltU);
131 addShortcutKeypress(kbAltV);
132 addShortcutKeypress(kbAltW);
133 addShortcutKeypress(kbAltX);
134 addShortcutKeypress(kbAltY);
135 addShortcutKeypress(kbAltZ);
136 }
137
34a42e78
KL
138 /**
139 * Public constructor spawns a shell.
140 *
141 * @param application TApplication that manages this window
142 * @param x column relative to parent
143 * @param y row relative to parent
144 * @param flags mask of CENTERED, MODAL, or RESIZABLE
145 */
146 public TTerminalWindow(final TApplication application, final int x,
147 final int y, final int flags) {
148
149 super(application, "Terminal", x, y, 80 + 2, 24 + 2, flags);
150
bd8d51fa
KL
151 // Assume XTERM
152 ECMA48.DeviceType deviceType = ECMA48.DeviceType.XTERM;
153
34a42e78
KL
154 try {
155 String [] cmdShellWindows = {
156 "cmd.exe"
157 };
158
159 // You cannot run a login shell in a bare Process interactively,
160 // due to libc's behavior of buffering when stdin/stdout aren't a
161 // tty. Use 'script' instead to run a shell in a pty.
162 String [] cmdShell = {
163 "script", "-fqe", "/dev/null"
164 };
165 // Spawn a shell and pass its I/O to the other constructor.
bd8d51fa 166
34a42e78
KL
167 ProcessBuilder pb;
168 if (System.getProperty("os.name").startsWith("Windows")) {
169 pb = new ProcessBuilder(cmdShellWindows);
170 } else {
171 pb = new ProcessBuilder(cmdShell);
172 }
bd8d51fa
KL
173 Map<String, String> env = pb.environment();
174 env.put("TERM", ECMA48.deviceTypeTerm(deviceType));
175 env.put("LANG", ECMA48.deviceTypeLang(deviceType, "en"));
176 env.put("COLUMNS", "80");
177 env.put("LINES", "24");
34a42e78
KL
178 pb.redirectErrorStream(true);
179 shell = pb.start();
bd8d51fa 180 emulator = new ECMA48(deviceType, shell.getInputStream(),
34a42e78
KL
181 shell.getOutputStream());
182 } catch (IOException e) {
183 e.printStackTrace();
184 }
185
186 // Setup the scroll bars
187 onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
188 getHeight()));
5dfd1c11
KL
189
190 // Claim the keystrokes the emulator will need.
191 addShortcutKeys();
34a42e78
KL
192 }
193
194 /**
195 * Public constructor.
196 *
197 * @param application TApplication that manages this window
198 * @param x column relative to parent
199 * @param y row relative to parent
200 * @param flags mask of CENTERED, MODAL, or RESIZABLE
201 * @param input an InputStream connected to the remote side. For type ==
202 * XTERM, input is converted to a Reader with UTF-8 encoding.
203 * @param output an OutputStream connected to the remote user. For type
204 * == XTERM, output is converted to a Writer with UTF-8 encoding.
205 * @throws UnsupportedEncodingException if an exception is thrown when
206 * creating the InputStreamReader
207 */
208 public TTerminalWindow(final TApplication application, final int x,
209 final int y, final int flags, final InputStream input,
210 final OutputStream output) throws UnsupportedEncodingException {
211
212 super(application, "Terminal", x, y, 80 + 2, 24 + 2, flags);
213
214 emulator = new ECMA48(ECMA48.DeviceType.XTERM, input, output);
215
216 // Setup the scroll bars
217 onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
218 getHeight()));
219
5dfd1c11
KL
220 // Claim the keystrokes the emulator will need.
221 addShortcutKeys();
34a42e78
KL
222 }
223
224 /**
225 * Draw the display buffer.
226 */
227 @Override
228 public void draw() {
229 // Synchronize against the emulator so we don't stomp on its reader
230 // thread.
231 synchronized (emulator) {
232
233 // Update the scroll bars
234 reflow();
235
236 // Draw the box using my superclass
237 super.draw();
238
239 List<DisplayLine> scrollback = emulator.getScrollbackBuffer();
240 List<DisplayLine> display = emulator.getDisplayBuffer();
241
242 // Put together the visible rows
34a42e78 243 int visibleHeight = getHeight() - 2;
34a42e78
KL
244 int visibleBottom = scrollback.size() + display.size()
245 + vScroller.getValue();
34a42e78
KL
246 assert (visibleBottom >= 0);
247
248 List<DisplayLine> preceedingBlankLines = new LinkedList<DisplayLine>();
249 int visibleTop = visibleBottom - visibleHeight;
34a42e78
KL
250 if (visibleTop < 0) {
251 for (int i = visibleTop; i < 0; i++) {
252 preceedingBlankLines.add(emulator.getBlankDisplayLine());
253 }
254 visibleTop = 0;
255 }
256 assert (visibleTop >= 0);
257
258 List<DisplayLine> displayLines = new LinkedList<DisplayLine>();
259 displayLines.addAll(scrollback);
260 displayLines.addAll(display);
34a42e78
KL
261
262 List<DisplayLine> visibleLines = new LinkedList<DisplayLine>();
263 visibleLines.addAll(preceedingBlankLines);
264 visibleLines.addAll(displayLines.subList(visibleTop,
265 visibleBottom));
34a42e78
KL
266
267 visibleHeight -= visibleLines.size();
34a42e78
KL
268 assert (visibleHeight >= 0);
269
270 // Now draw the emulator screen
271 int row = 1;
272 for (DisplayLine line: visibleLines) {
273 int widthMax = emulator.getWidth();
274 if (line.isDoubleWidth()) {
275 widthMax /= 2;
276 }
277 if (widthMax > getWidth() - 2) {
278 widthMax = getWidth() - 2;
279 }
280 for (int i = 0; i < widthMax; i++) {
281 Cell ch = line.charAt(i);
282 Cell newCell = new Cell();
283 newCell.setTo(ch);
7c870d89 284 boolean reverse = line.isReverseColor() ^ ch.isReverse();
34a42e78
KL
285 newCell.setReverse(false);
286 if (reverse) {
287 newCell.setBackColor(ch.getForeColor());
288 newCell.setForeColor(ch.getBackColor());
289 }
290 if (line.isDoubleWidth()) {
291 getScreen().putCharXY((i * 2) + 1, row, newCell);
292 getScreen().putCharXY((i * 2) + 2, row, ' ', newCell);
293 } else {
294 getScreen().putCharXY(i + 1, row, newCell);
295 }
296 }
297 row++;
298 if (row == getHeight() - 1) {
299 // Don't overwrite the box edge
300 break;
301 }
302 }
303 CellAttributes background = new CellAttributes();
304 // Fill in the blank lines on bottom
305 for (int i = 0; i < visibleHeight; i++) {
306 getScreen().hLineXY(1, i + row, getWidth() - 2, ' ',
307 background);
308 }
309
310 } // synchronized (emulator)
311
312 }
313
314 /**
315 * Handle window close.
316 */
317 @Override public void onClose() {
5dfd1c11 318 emulator.close();
69345248 319 if (shell != null) {
5dfd1c11 320 // System.err.println("shell.destroy()");
69345248
KL
321 shell.destroy();
322 shell = null;
69345248 323 }
34a42e78
KL
324 }
325
326 /**
327 * Copy out variables from the emulator that TTerminal has to expose on
328 * screen.
329 */
330 private void readEmulatorState() {
331 // Synchronize against the emulator so we don't stomp on its reader
332 // thread.
333 synchronized (emulator) {
334
335 setCursorX(emulator.getCursorX() + 1);
336 setCursorY(emulator.getCursorY() + 1
337 + (getHeight() - 2 - emulator.getHeight()));
338 if (vScroller != null) {
339 setCursorY(getCursorY() - vScroller.getValue());
340 }
7c870d89 341 setCursorVisible(emulator.isCursorVisible());
34a42e78 342 if (getCursorX() > getWidth() - 2) {
7c870d89 343 setCursorVisible(false);
34a42e78
KL
344 }
345 if ((getCursorY() > getHeight() - 2) || (getCursorY() < 0)) {
7c870d89 346 setCursorVisible(false);
34a42e78
KL
347 }
348 if (emulator.getScreenTitle().length() > 0) {
349 // Only update the title if the shell is still alive
350 if (shell != null) {
351 setTitle(emulator.getScreenTitle());
352 }
353 }
34a42e78
KL
354
355 // Check to see if the shell has died.
356 if (!emulator.isReading() && (shell != null)) {
55b4f29b
KL
357 try {
358 int rc = shell.exitValue();
359 // The emulator exited on its own, all is fine
360 setTitle(String.format("%s [Completed - %d]",
0d47c546 361 getTitle(), rc));
55b4f29b
KL
362 shell = null;
363 emulator.close();
5dfd1c11 364 clearShortcutKeypresses();
55b4f29b
KL
365 } catch (IllegalThreadStateException e) {
366 // The emulator thread has exited, but the shell Process
367 // hasn't figured that out yet. Do nothing, we will see
368 // this in a future tick.
369 }
34a42e78
KL
370 } else if (emulator.isReading() && (shell != null)) {
371 // The shell might be dead, let's check
372 try {
373 int rc = shell.exitValue();
374 // If we got here, the shell died.
375 setTitle(String.format("%s [Completed - %d]",
376 getTitle(), rc));
377 shell = null;
378 emulator.close();
5dfd1c11 379 clearShortcutKeypresses();
34a42e78
KL
380 } catch (IllegalThreadStateException e) {
381 // The shell is still running, do nothing.
382 }
383 }
92554d64 384
34a42e78
KL
385 } // synchronized (emulator)
386 }
387
388 /**
389 * Handle window/screen resize events.
390 *
391 * @param resize resize event
392 */
393 @Override
394 public void onResize(final TResizeEvent resize) {
395
396 // Synchronize against the emulator so we don't stomp on its reader
397 // thread.
398 synchronized (emulator) {
399
400 if (resize.getType() == TResizeEvent.Type.WIDGET) {
401 // Resize the scroll bars
402 reflow();
403
404 // Get out of scrollback
405 vScroller.setValue(0);
406 }
407 return;
408
409 } // synchronized (emulator)
410 }
411
412 /**
413 * Resize scrollbars for a new width/height.
414 */
415 private void reflow() {
416
417 // Synchronize against the emulator so we don't stomp on its reader
418 // thread.
419 synchronized (emulator) {
420
421 // Pull cursor information
422 readEmulatorState();
423
424 // Vertical scrollbar
425 if (vScroller == null) {
426 vScroller = new TVScroller(this, getWidth() - 2, 0,
427 getHeight() - 2);
428 vScroller.setBottomValue(0);
429 vScroller.setValue(0);
430 } else {
431 vScroller.setX(getWidth() - 2);
432 vScroller.setHeight(getHeight() - 2);
433 }
434 vScroller.setTopValue(getHeight() - 2
435 - (emulator.getScrollbackBuffer().size()
436 + emulator.getDisplayBuffer().size()));
437 vScroller.setBigChange(getHeight() - 2);
438
439 } // synchronized (emulator)
440 }
441
bd8d51fa
KL
442 /**
443 * Check if a mouse press/release/motion event coordinate is over the
444 * emulator.
445 *
446 * @param mouse a mouse-based event
447 * @return whether or not the mouse is on the emulator
448 */
449 private final boolean mouseOnEmulator(final TMouseEvent mouse) {
450
451 synchronized (emulator) {
452 if (!emulator.isReading()) {
453 return false;
454 }
455 }
456
457 if ((mouse.getAbsoluteX() >= getAbsoluteX() + 1)
458 && (mouse.getAbsoluteX() < getAbsoluteX() + getWidth() - 1)
459 && (mouse.getAbsoluteY() >= getAbsoluteY() + 1)
460 && (mouse.getAbsoluteY() < getAbsoluteY() + getHeight() - 1)
461 ) {
462 return true;
463 }
464 return false;
465 }
466
34a42e78
KL
467 /**
468 * Handle keystrokes.
469 *
470 * @param keypress keystroke event
471 */
472 @Override
473 public void onKeypress(final TKeypressEvent keypress) {
474
475 // Scrollback up/down
476 if (keypress.equals(kbShiftPgUp)
477 || keypress.equals(kbCtrlPgUp)
478 || keypress.equals(kbAltPgUp)
479 ) {
480 vScroller.bigDecrement();
481 return;
482 }
483 if (keypress.equals(kbShiftPgDn)
484 || keypress.equals(kbCtrlPgDn)
485 || keypress.equals(kbAltPgDn)
486 ) {
487 vScroller.bigIncrement();
488 return;
489 }
490
491 // Synchronize against the emulator so we don't stomp on its reader
492 // thread.
493 synchronized (emulator) {
494 if (emulator.isReading()) {
495 // Get out of scrollback
496 vScroller.setValue(0);
497 emulator.keypress(keypress.getKey());
92554d64
KL
498
499 // UGLY HACK TIME! cmd.exe needs CRLF, not just CR, so if
500 // this is kBEnter then also send kbCtrlJ.
501 if (System.getProperty("os.name").startsWith("Windows")) {
502 if (keypress.equals(kbEnter)) {
503 emulator.keypress(kbCtrlJ);
504 }
505 }
506
34a42e78
KL
507 readEmulatorState();
508 return;
509 }
510 }
511
512 // Process is closed, honor "normal" TUI keystrokes
513 super.onKeypress(keypress);
514 }
515
516 /**
517 * Handle mouse press events.
518 *
519 * @param mouse mouse button press event
520 */
521 @Override
522 public void onMouseDown(final TMouseEvent mouse) {
bd8d51fa
KL
523 if (inWindowMove || inWindowResize) {
524 // TWindow needs to deal with this.
525 super.onMouseDown(mouse);
526 return;
527 }
34a42e78 528
7c870d89 529 if (mouse.isMouseWheelUp()) {
34a42e78
KL
530 vScroller.decrement();
531 return;
532 }
7c870d89 533 if (mouse.isMouseWheelDown()) {
34a42e78
KL
534 vScroller.increment();
535 return;
536 }
bd8d51fa
KL
537 if (mouseOnEmulator(mouse)) {
538 synchronized (emulator) {
539 mouse.setX(mouse.getX() - 1);
540 mouse.setY(mouse.getY() - 1);
541 emulator.mouse(mouse);
542 readEmulatorState();
543 return;
544 }
545 }
34a42e78 546
bd8d51fa 547 // Emulator didn't consume it, pass it on
34a42e78
KL
548 super.onMouseDown(mouse);
549 }
550
bd8d51fa
KL
551 /**
552 * Handle mouse release events.
553 *
554 * @param mouse mouse button release event
555 */
556 @Override
557 public void onMouseUp(final TMouseEvent mouse) {
558 if (inWindowMove || inWindowResize) {
559 // TWindow needs to deal with this.
560 super.onMouseUp(mouse);
561 return;
562 }
563
564 if (mouseOnEmulator(mouse)) {
565 synchronized (emulator) {
566 mouse.setX(mouse.getX() - 1);
567 mouse.setY(mouse.getY() - 1);
568 emulator.mouse(mouse);
569 readEmulatorState();
570 return;
571 }
572 }
573
574 // Emulator didn't consume it, pass it on
575 super.onMouseUp(mouse);
576 }
577
578 /**
579 * Handle mouse motion events.
580 *
581 * @param mouse mouse motion event
582 */
583 @Override
584 public void onMouseMotion(final TMouseEvent mouse) {
585 if (inWindowMove || inWindowResize) {
586 // TWindow needs to deal with this.
587 super.onMouseMotion(mouse);
588 return;
589 }
590
591 if (mouseOnEmulator(mouse)) {
592 synchronized (emulator) {
593 mouse.setX(mouse.getX() - 1);
594 mouse.setY(mouse.getY() - 1);
595 emulator.mouse(mouse);
596 readEmulatorState();
597 return;
598 }
599 }
600
601 // Emulator didn't consume it, pass it on
602 super.onMouseMotion(mouse);
603 }
604
34a42e78 605}