#18 repaint after screen resize
[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 *
a2018e99 6 * Copyright (C) 2017 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
34a42e78 31import java.io.IOException;
55d2b2c2 32import java.lang.reflect.Field;
34a42e78
KL
33import java.util.LinkedList;
34import java.util.List;
bd8d51fa 35import java.util.Map;
34a42e78
KL
36
37import jexer.bits.Cell;
38import jexer.bits.CellAttributes;
39import jexer.event.TKeypressEvent;
40import jexer.event.TMouseEvent;
41import jexer.event.TResizeEvent;
42import jexer.tterminal.DisplayLine;
be72cb5c 43import jexer.tterminal.DisplayListener;
34a42e78
KL
44import jexer.tterminal.ECMA48;
45import static jexer.TKeypress.*;
46
47/**
48 * TTerminalWindow exposes a ECMA-48 / ANSI X3.64 style terminal in a window.
49 */
be72cb5c
KL
50public class TTerminalWindow extends TScrollableWindow
51 implements DisplayListener {
34a42e78
KL
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
1d99a38f
KL
63 /**
64 * If true, we are using the ptypipe utility to support dynamic window
65 * resizing. ptypipe is available at
66 * https://github.com/klamonte/ptypipe .
67 */
68 private boolean ptypipe = false;
69
5dfd1c11
KL
70 /**
71 * Claim the keystrokes the emulator will need.
72 */
73 private void addShortcutKeys() {
74 addShortcutKeypress(kbCtrlA);
75 addShortcutKeypress(kbCtrlB);
76 addShortcutKeypress(kbCtrlC);
77 addShortcutKeypress(kbCtrlD);
78 addShortcutKeypress(kbCtrlE);
79 addShortcutKeypress(kbCtrlF);
80 addShortcutKeypress(kbCtrlG);
81 addShortcutKeypress(kbCtrlH);
82 addShortcutKeypress(kbCtrlU);
83 addShortcutKeypress(kbCtrlJ);
84 addShortcutKeypress(kbCtrlK);
85 addShortcutKeypress(kbCtrlL);
86 addShortcutKeypress(kbCtrlM);
87 addShortcutKeypress(kbCtrlN);
88 addShortcutKeypress(kbCtrlO);
89 addShortcutKeypress(kbCtrlP);
90 addShortcutKeypress(kbCtrlQ);
91 addShortcutKeypress(kbCtrlR);
92 addShortcutKeypress(kbCtrlS);
93 addShortcutKeypress(kbCtrlT);
94 addShortcutKeypress(kbCtrlU);
95 addShortcutKeypress(kbCtrlV);
96 addShortcutKeypress(kbCtrlW);
97 addShortcutKeypress(kbCtrlX);
98 addShortcutKeypress(kbCtrlY);
99 addShortcutKeypress(kbCtrlZ);
100 addShortcutKeypress(kbF1);
101 addShortcutKeypress(kbF2);
102 addShortcutKeypress(kbF3);
103 addShortcutKeypress(kbF4);
104 addShortcutKeypress(kbF5);
105 addShortcutKeypress(kbF6);
106 addShortcutKeypress(kbF7);
107 addShortcutKeypress(kbF8);
108 addShortcutKeypress(kbF9);
109 addShortcutKeypress(kbF10);
110 addShortcutKeypress(kbF11);
111 addShortcutKeypress(kbF12);
112 addShortcutKeypress(kbAltA);
113 addShortcutKeypress(kbAltB);
114 addShortcutKeypress(kbAltC);
115 addShortcutKeypress(kbAltD);
116 addShortcutKeypress(kbAltE);
117 addShortcutKeypress(kbAltF);
118 addShortcutKeypress(kbAltG);
119 addShortcutKeypress(kbAltH);
120 addShortcutKeypress(kbAltU);
121 addShortcutKeypress(kbAltJ);
122 addShortcutKeypress(kbAltK);
123 addShortcutKeypress(kbAltL);
124 addShortcutKeypress(kbAltM);
125 addShortcutKeypress(kbAltN);
126 addShortcutKeypress(kbAltO);
127 addShortcutKeypress(kbAltP);
128 addShortcutKeypress(kbAltQ);
129 addShortcutKeypress(kbAltR);
130 addShortcutKeypress(kbAltS);
131 addShortcutKeypress(kbAltT);
132 addShortcutKeypress(kbAltU);
133 addShortcutKeypress(kbAltV);
134 addShortcutKeypress(kbAltW);
135 addShortcutKeypress(kbAltX);
136 addShortcutKeypress(kbAltY);
137 addShortcutKeypress(kbAltZ);
138 }
139
34a42e78
KL
140 /**
141 * Public constructor spawns a shell.
142 *
143 * @param application TApplication that manages this window
144 * @param x column relative to parent
145 * @param y row relative to parent
146 * @param flags mask of CENTERED, MODAL, or RESIZABLE
147 */
148 public TTerminalWindow(final TApplication application, final int x,
149 final int y, final int flags) {
150
151 super(application, "Terminal", x, y, 80 + 2, 24 + 2, flags);
152
56661844
KL
153 vScroller = new TVScroller(this, getWidth() - 2, 0, getHeight() - 2);
154 setBottomValue(0);
155
bd8d51fa
KL
156 // Assume XTERM
157 ECMA48.DeviceType deviceType = ECMA48.DeviceType.XTERM;
158
34a42e78
KL
159 try {
160 String [] cmdShellWindows = {
161 "cmd.exe"
162 };
163
164 // You cannot run a login shell in a bare Process interactively,
165 // due to libc's behavior of buffering when stdin/stdout aren't a
55d2b2c2
KL
166 // tty. Use 'script' instead to run a shell in a pty. And
167 // because BSD and GNU differ on the '-f' vs '-F' flags, we need
168 // two different commands. Lovely.
169 String [] cmdShellGNU = {
34a42e78
KL
170 "script", "-fqe", "/dev/null"
171 };
55d2b2c2 172 String [] cmdShellBSD = {
2ce6dab2 173 "script", "-q", "-F", "/dev/null"
55d2b2c2 174 };
1d99a38f
KL
175 String [] cmdShellPtypipe = {
176 "ptypipe", "/bin/bash", "--login"
177 };
34a42e78 178 // Spawn a shell and pass its I/O to the other constructor.
bd8d51fa 179
34a42e78 180 ProcessBuilder pb;
1d99a38f
KL
181 if ((System.getProperty("jexer.TTerminal.ptypipe") != null)
182 && (System.getProperty("jexer.TTerminal.ptypipe").
183 equals("true"))
184 ) {
185 pb = new ProcessBuilder(cmdShellPtypipe);
186 ptypipe = true;
187 } else if (System.getProperty("os.name").startsWith("Windows")) {
34a42e78 188 pb = new ProcessBuilder(cmdShellWindows);
55d2b2c2
KL
189 } else if (System.getProperty("os.name").startsWith("Mac")) {
190 pb = new ProcessBuilder(cmdShellBSD);
191 } else if (System.getProperty("os.name").startsWith("Linux")) {
192 pb = new ProcessBuilder(cmdShellGNU);
34a42e78 193 } else {
55d2b2c2
KL
194 // When all else fails, assume GNU.
195 pb = new ProcessBuilder(cmdShellGNU);
34a42e78 196 }
bd8d51fa
KL
197 Map<String, String> env = pb.environment();
198 env.put("TERM", ECMA48.deviceTypeTerm(deviceType));
199 env.put("LANG", ECMA48.deviceTypeLang(deviceType, "en"));
200 env.put("COLUMNS", "80");
201 env.put("LINES", "24");
34a42e78
KL
202 pb.redirectErrorStream(true);
203 shell = pb.start();
bd8d51fa 204 emulator = new ECMA48(deviceType, shell.getInputStream(),
34a42e78 205 shell.getOutputStream());
be72cb5c 206 emulator.setListener(this);
34a42e78 207 } catch (IOException e) {
fe0770f9 208 messageBox("Error", "Error launching shell: " + e.getMessage());
34a42e78
KL
209 }
210
211 // Setup the scroll bars
212 onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
213 getHeight()));
5dfd1c11
KL
214
215 // Claim the keystrokes the emulator will need.
216 addShortcutKeys();
2ce6dab2
KL
217
218 // Add shortcut text
219 newStatusBar("Terminal session executing...");
34a42e78
KL
220 }
221
55d2b2c2
KL
222 /**
223 * Terminate the child of the 'script' process used on POSIX. This may
224 * or may not work.
225 */
226 private void terminateShellChildProcess() {
227 int pid = -1;
228 if (shell.getClass().getName().equals("java.lang.UNIXProcess")) {
229 /* get the PID on unix/linux systems */
230 try {
231 Field field = shell.getClass().getDeclaredField("pid");
232 field.setAccessible(true);
233 pid = field.getInt(shell);
234 } catch (Throwable e) {
235 // SQUASH, this didn't work. Just bail out quietly.
236 return;
237 }
238 }
239 if (pid != -1) {
240 // shell.destroy() works successfully at killing this side of
241 // 'script'. But we need to make sure the other side (child
242 // process) is also killed.
243 String [] cmdKillIt = {
244 "pkill", "-P", Integer.toString(pid)
245 };
246 try {
247 Runtime.getRuntime().exec(cmdKillIt);
248 } catch (Throwable e) {
249 // SQUASH, this didn't work. Just bail out quietly.
250 return;
251 }
252 }
253 }
254
34a42e78
KL
255 /**
256 * Draw the display buffer.
257 */
258 @Override
259 public void draw() {
260 // Synchronize against the emulator so we don't stomp on its reader
261 // thread.
262 synchronized (emulator) {
263
264 // Update the scroll bars
56661844 265 reflowData();
34a42e78
KL
266
267 // Draw the box using my superclass
268 super.draw();
269
270 List<DisplayLine> scrollback = emulator.getScrollbackBuffer();
271 List<DisplayLine> display = emulator.getDisplayBuffer();
272
273 // Put together the visible rows
34a42e78 274 int visibleHeight = getHeight() - 2;
34a42e78 275 int visibleBottom = scrollback.size() + display.size()
56661844 276 + getVerticalValue();
34a42e78
KL
277 assert (visibleBottom >= 0);
278
279 List<DisplayLine> preceedingBlankLines = new LinkedList<DisplayLine>();
280 int visibleTop = visibleBottom - visibleHeight;
34a42e78
KL
281 if (visibleTop < 0) {
282 for (int i = visibleTop; i < 0; i++) {
283 preceedingBlankLines.add(emulator.getBlankDisplayLine());
284 }
285 visibleTop = 0;
286 }
287 assert (visibleTop >= 0);
288
289 List<DisplayLine> displayLines = new LinkedList<DisplayLine>();
290 displayLines.addAll(scrollback);
291 displayLines.addAll(display);
34a42e78
KL
292
293 List<DisplayLine> visibleLines = new LinkedList<DisplayLine>();
294 visibleLines.addAll(preceedingBlankLines);
295 visibleLines.addAll(displayLines.subList(visibleTop,
296 visibleBottom));
34a42e78
KL
297
298 visibleHeight -= visibleLines.size();
34a42e78
KL
299 assert (visibleHeight >= 0);
300
301 // Now draw the emulator screen
302 int row = 1;
303 for (DisplayLine line: visibleLines) {
304 int widthMax = emulator.getWidth();
305 if (line.isDoubleWidth()) {
306 widthMax /= 2;
307 }
308 if (widthMax > getWidth() - 2) {
309 widthMax = getWidth() - 2;
310 }
311 for (int i = 0; i < widthMax; i++) {
312 Cell ch = line.charAt(i);
313 Cell newCell = new Cell();
314 newCell.setTo(ch);
7c870d89 315 boolean reverse = line.isReverseColor() ^ ch.isReverse();
34a42e78
KL
316 newCell.setReverse(false);
317 if (reverse) {
318 newCell.setBackColor(ch.getForeColor());
319 newCell.setForeColor(ch.getBackColor());
320 }
321 if (line.isDoubleWidth()) {
322 getScreen().putCharXY((i * 2) + 1, row, newCell);
323 getScreen().putCharXY((i * 2) + 2, row, ' ', newCell);
324 } else {
325 getScreen().putCharXY(i + 1, row, newCell);
326 }
327 }
328 row++;
329 if (row == getHeight() - 1) {
330 // Don't overwrite the box edge
331 break;
332 }
333 }
334 CellAttributes background = new CellAttributes();
335 // Fill in the blank lines on bottom
336 for (int i = 0; i < visibleHeight; i++) {
337 getScreen().hLineXY(1, i + row, getWidth() - 2, ' ',
338 background);
339 }
340
341 } // synchronized (emulator)
342
343 }
344
be72cb5c
KL
345 /**
346 * Called by emulator when fresh data has come in.
347 */
348 public void displayChanged() {
349 doRepaint();
350 }
351
34a42e78
KL
352 /**
353 * Handle window close.
354 */
2ce6dab2
KL
355 @Override
356 public void onClose() {
5dfd1c11 357 emulator.close();
69345248 358 if (shell != null) {
55d2b2c2 359 terminateShellChildProcess();
69345248
KL
360 shell.destroy();
361 shell = null;
69345248 362 }
34a42e78
KL
363 }
364
365 /**
366 * Copy out variables from the emulator that TTerminal has to expose on
367 * screen.
368 */
369 private void readEmulatorState() {
370 // Synchronize against the emulator so we don't stomp on its reader
371 // thread.
372 synchronized (emulator) {
373
374 setCursorX(emulator.getCursorX() + 1);
375 setCursorY(emulator.getCursorY() + 1
56661844
KL
376 + (getHeight() - 2 - emulator.getHeight())
377 - getVerticalValue());
7c870d89 378 setCursorVisible(emulator.isCursorVisible());
34a42e78 379 if (getCursorX() > getWidth() - 2) {
7c870d89 380 setCursorVisible(false);
34a42e78
KL
381 }
382 if ((getCursorY() > getHeight() - 2) || (getCursorY() < 0)) {
7c870d89 383 setCursorVisible(false);
34a42e78
KL
384 }
385 if (emulator.getScreenTitle().length() > 0) {
386 // Only update the title if the shell is still alive
387 if (shell != null) {
388 setTitle(emulator.getScreenTitle());
389 }
390 }
34a42e78
KL
391
392 // Check to see if the shell has died.
393 if (!emulator.isReading() && (shell != null)) {
55b4f29b
KL
394 try {
395 int rc = shell.exitValue();
396 // The emulator exited on its own, all is fine
397 setTitle(String.format("%s [Completed - %d]",
0d47c546 398 getTitle(), rc));
55b4f29b
KL
399 shell = null;
400 emulator.close();
5dfd1c11 401 clearShortcutKeypresses();
2ce6dab2
KL
402 statusBar.setText("Terminal session completed, exit " +
403 "code " + rc + ".");
55b4f29b
KL
404 } catch (IllegalThreadStateException e) {
405 // The emulator thread has exited, but the shell Process
406 // hasn't figured that out yet. Do nothing, we will see
407 // this in a future tick.
408 }
34a42e78
KL
409 } else if (emulator.isReading() && (shell != null)) {
410 // The shell might be dead, let's check
411 try {
412 int rc = shell.exitValue();
413 // If we got here, the shell died.
414 setTitle(String.format("%s [Completed - %d]",
415 getTitle(), rc));
416 shell = null;
417 emulator.close();
5dfd1c11 418 clearShortcutKeypresses();
2ce6dab2
KL
419 statusBar.setText("Terminal session completed, exit " +
420 "code " + rc + ".");
34a42e78
KL
421 } catch (IllegalThreadStateException e) {
422 // The shell is still running, do nothing.
423 }
424 }
92554d64 425
34a42e78
KL
426 } // synchronized (emulator)
427 }
428
429 /**
430 * Handle window/screen resize events.
431 *
432 * @param resize resize event
433 */
434 @Override
435 public void onResize(final TResizeEvent resize) {
436
437 // Synchronize against the emulator so we don't stomp on its reader
438 // thread.
439 synchronized (emulator) {
440
441 if (resize.getType() == TResizeEvent.Type.WIDGET) {
442 // Resize the scroll bars
56661844
KL
443 reflowData();
444 placeScrollbars();
34a42e78
KL
445
446 // Get out of scrollback
56661844 447 setVerticalValue(0);
1d99a38f
KL
448
449 if (ptypipe) {
450 emulator.setWidth(getWidth() - 2);
451 emulator.setHeight(getHeight() - 2);
452
453 emulator.writeRemote("\033[8;" + (getHeight() - 2) + ";" +
454 (getWidth() - 2) + "t");
455 }
34a42e78
KL
456 }
457 return;
458
459 } // synchronized (emulator)
460 }
461
462 /**
463 * Resize scrollbars for a new width/height.
464 */
56661844
KL
465 @Override
466 public void reflowData() {
34a42e78
KL
467
468 // Synchronize against the emulator so we don't stomp on its reader
469 // thread.
470 synchronized (emulator) {
471
472 // Pull cursor information
473 readEmulatorState();
474
475 // Vertical scrollbar
56661844 476 setTopValue(getHeight() - 2
34a42e78
KL
477 - (emulator.getScrollbackBuffer().size()
478 + emulator.getDisplayBuffer().size()));
56661844 479 setVerticalBigChange(getHeight() - 2);
34a42e78
KL
480
481 } // synchronized (emulator)
482 }
483
bd8d51fa
KL
484 /**
485 * Check if a mouse press/release/motion event coordinate is over the
486 * emulator.
487 *
488 * @param mouse a mouse-based event
489 * @return whether or not the mouse is on the emulator
490 */
491 private final boolean mouseOnEmulator(final TMouseEvent mouse) {
492
493 synchronized (emulator) {
494 if (!emulator.isReading()) {
495 return false;
496 }
497 }
498
499 if ((mouse.getAbsoluteX() >= getAbsoluteX() + 1)
500 && (mouse.getAbsoluteX() < getAbsoluteX() + getWidth() - 1)
501 && (mouse.getAbsoluteY() >= getAbsoluteY() + 1)
502 && (mouse.getAbsoluteY() < getAbsoluteY() + getHeight() - 1)
503 ) {
504 return true;
505 }
506 return false;
507 }
508
34a42e78
KL
509 /**
510 * Handle keystrokes.
511 *
512 * @param keypress keystroke event
513 */
514 @Override
515 public void onKeypress(final TKeypressEvent keypress) {
516
517 // Scrollback up/down
518 if (keypress.equals(kbShiftPgUp)
519 || keypress.equals(kbCtrlPgUp)
520 || keypress.equals(kbAltPgUp)
521 ) {
56661844 522 bigVerticalDecrement();
34a42e78
KL
523 return;
524 }
525 if (keypress.equals(kbShiftPgDn)
526 || keypress.equals(kbCtrlPgDn)
527 || keypress.equals(kbAltPgDn)
528 ) {
56661844 529 bigVerticalIncrement();
34a42e78
KL
530 return;
531 }
532
533 // Synchronize against the emulator so we don't stomp on its reader
534 // thread.
535 synchronized (emulator) {
536 if (emulator.isReading()) {
537 // Get out of scrollback
56661844 538 setVerticalValue(0);
34a42e78 539 emulator.keypress(keypress.getKey());
92554d64
KL
540
541 // UGLY HACK TIME! cmd.exe needs CRLF, not just CR, so if
542 // this is kBEnter then also send kbCtrlJ.
543 if (System.getProperty("os.name").startsWith("Windows")) {
544 if (keypress.equals(kbEnter)) {
545 emulator.keypress(kbCtrlJ);
546 }
547 }
548
34a42e78
KL
549 readEmulatorState();
550 return;
551 }
552 }
553
554 // Process is closed, honor "normal" TUI keystrokes
555 super.onKeypress(keypress);
556 }
557
558 /**
559 * Handle mouse press events.
560 *
561 * @param mouse mouse button press event
562 */
563 @Override
564 public void onMouseDown(final TMouseEvent mouse) {
bd8d51fa
KL
565 if (inWindowMove || inWindowResize) {
566 // TWindow needs to deal with this.
567 super.onMouseDown(mouse);
568 return;
569 }
34a42e78 570
7c870d89 571 if (mouse.isMouseWheelUp()) {
56661844 572 verticalDecrement();
34a42e78
KL
573 return;
574 }
7c870d89 575 if (mouse.isMouseWheelDown()) {
56661844 576 verticalIncrement();
34a42e78
KL
577 return;
578 }
bd8d51fa
KL
579 if (mouseOnEmulator(mouse)) {
580 synchronized (emulator) {
581 mouse.setX(mouse.getX() - 1);
582 mouse.setY(mouse.getY() - 1);
583 emulator.mouse(mouse);
584 readEmulatorState();
585 return;
586 }
587 }
34a42e78 588
bd8d51fa 589 // Emulator didn't consume it, pass it on
34a42e78
KL
590 super.onMouseDown(mouse);
591 }
592
bd8d51fa
KL
593 /**
594 * Handle mouse release events.
595 *
596 * @param mouse mouse button release event
597 */
598 @Override
599 public void onMouseUp(final TMouseEvent mouse) {
600 if (inWindowMove || inWindowResize) {
601 // TWindow needs to deal with this.
602 super.onMouseUp(mouse);
603 return;
604 }
605
606 if (mouseOnEmulator(mouse)) {
607 synchronized (emulator) {
608 mouse.setX(mouse.getX() - 1);
609 mouse.setY(mouse.getY() - 1);
610 emulator.mouse(mouse);
611 readEmulatorState();
612 return;
613 }
614 }
615
616 // Emulator didn't consume it, pass it on
617 super.onMouseUp(mouse);
618 }
619
620 /**
621 * Handle mouse motion events.
622 *
623 * @param mouse mouse motion event
624 */
625 @Override
626 public void onMouseMotion(final TMouseEvent mouse) {
627 if (inWindowMove || inWindowResize) {
628 // TWindow needs to deal with this.
629 super.onMouseMotion(mouse);
630 return;
631 }
632
633 if (mouseOnEmulator(mouse)) {
634 synchronized (emulator) {
635 mouse.setX(mouse.getX() - 1);
636 mouse.setY(mouse.getY() - 1);
637 emulator.mouse(mouse);
638 readEmulatorState();
639 return;
640 }
641 }
642
643 // Emulator didn't consume it, pass it on
644 super.onMouseMotion(mouse);
645 }
646
34a42e78 647}