TSplitPane initial
[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 *
a69ed767 6 * Copyright (C) 2019 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
d1115203
KL
31import java.awt.Font;
32import java.awt.FontMetrics;
33import java.awt.Graphics2D;
5fc7bf09 34import java.awt.image.BufferedImage;
d1115203
KL
35
36import java.io.InputStream;
34a42e78 37import java.io.IOException;
55d2b2c2 38import java.lang.reflect.Field;
339652cc 39import java.text.MessageFormat;
a69ed767 40import java.util.ArrayList;
d1115203 41import java.util.HashMap;
34a42e78 42import java.util.List;
bd8d51fa 43import java.util.Map;
339652cc 44import java.util.ResourceBundle;
34a42e78 45
d1115203 46import jexer.backend.ECMA48Terminal;
0d86ab84 47import jexer.backend.GlyphMaker;
d1115203
KL
48import jexer.backend.MultiScreen;
49import jexer.backend.SwingTerminal;
34a42e78
KL
50import jexer.bits.Cell;
51import jexer.bits.CellAttributes;
52import jexer.event.TKeypressEvent;
b2d49e0f 53import jexer.event.TMenuEvent;
34a42e78
KL
54import jexer.event.TMouseEvent;
55import jexer.event.TResizeEvent;
b2d49e0f 56import jexer.menu.TMenu;
34a42e78 57import jexer.tterminal.DisplayLine;
be72cb5c 58import jexer.tterminal.DisplayListener;
34a42e78
KL
59import jexer.tterminal.ECMA48;
60import static jexer.TKeypress.*;
61
62/**
63 * TTerminalWindow exposes a ECMA-48 / ANSI X3.64 style terminal in a window.
64 */
be72cb5c
KL
65public class TTerminalWindow extends TScrollableWindow
66 implements DisplayListener {
34a42e78 67
339652cc
KL
68 /**
69 * Translated strings.
70 */
71 private static final ResourceBundle i18n = ResourceBundle.getBundle(TTerminalWindow.class.getName());
72
615a0d99
KL
73 // ------------------------------------------------------------------------
74 // Variables --------------------------------------------------------------
75 // ------------------------------------------------------------------------
76
34a42e78
KL
77 /**
78 * The emulator.
79 */
80 private ECMA48 emulator;
81
82 /**
83 * The Process created by the shell spawning constructor.
84 */
85 private Process shell;
86
1d99a38f
KL
87 /**
88 * If true, we are using the ptypipe utility to support dynamic window
89 * resizing. ptypipe is available at
a69ed767 90 * https://gitlab.com/klamonte/ptypipe .
1d99a38f
KL
91 */
92 private boolean ptypipe = false;
93
a69ed767
KL
94 /**
95 * If true, close the window when the shell exits.
96 */
97 private boolean closeOnExit = false;
98
d1115203 99 /**
0d86ab84 100 * Double-height font.
d1115203 101 */
0d86ab84 102 private GlyphMaker doubleFont;
d1115203
KL
103
104 /**
105 * Last text width value.
106 */
107 private int lastTextWidth = -1;
108
109 /**
110 * Last text height value.
111 */
112 private int lastTextHeight = -1;
113
b3d79e99
KL
114 /**
115 * The blink state, used only by ECMA48 backend and when double-width
116 * chars must be drawn.
117 */
118 private boolean blinkState = true;
119
0d86ab84
KL
120 /**
121 * Timer flag, used only by ECMA48 backend and when double-width chars
122 * must be drawn.
123 */
124 private boolean haveTimer = false;
125
ab215e38
KL
126 /**
127 * The last seen scrollback lines.
128 */
129 private List<DisplayLine> scrollback;
130
131 /**
132 * The last seen display lines.
133 */
134 private List<DisplayLine> display;
135
136 /**
137 * If true, the display has changed and needs updating.
138 */
139 private volatile boolean dirty = true;
140
141 /**
142 * Time that the display was last updated.
143 */
144 private long lastUpdateTime = 0;
145
80b1b7b5
KL
146 /**
147 * If true, hide the mouse after typing a keystroke.
148 */
149 private boolean hideMouseWhenTyping = true;
150
151 /**
152 * If true, the mouse should not be displayed because a keystroke was
153 * typed.
154 */
155 private boolean typingHidMouse = false;
156
615a0d99
KL
157 // ------------------------------------------------------------------------
158 // Constructors -----------------------------------------------------------
159 // ------------------------------------------------------------------------
34a42e78 160
b2d49e0f
KL
161 /**
162 * Public constructor spawns a custom command line.
163 *
164 * @param application TApplication that manages this window
165 * @param x column relative to parent
166 * @param y row relative to parent
167 * @param commandLine the command line to execute
168 */
169 public TTerminalWindow(final TApplication application, final int x,
170 final int y, final String commandLine) {
171
00691e80 172 this(application, x, y, RESIZABLE, commandLine.split("\\s+"),
a69ed767
KL
173 System.getProperty("jexer.TTerminal.closeOnExit",
174 "false").equals("true"));
175 }
176
177 /**
178 * Public constructor spawns a custom command line.
179 *
180 * @param application TApplication that manages this window
181 * @param x column relative to parent
182 * @param y row relative to parent
183 * @param commandLine the command line to execute
184 * @param closeOnExit if true, close the window when the command exits
185 */
186 public TTerminalWindow(final TApplication application, final int x,
187 final int y, final String commandLine, final boolean closeOnExit) {
188
00691e80 189 this(application, x, y, RESIZABLE, commandLine.split("\\s+"),
a69ed767 190 closeOnExit);
b2d49e0f
KL
191 }
192
6f8ff91a
KL
193 /**
194 * Public constructor spawns a custom command line.
195 *
196 * @param application TApplication that manages this window
197 * @param x column relative to parent
198 * @param y row relative to parent
199 * @param flags mask of CENTERED, MODAL, or RESIZABLE
a0d734e6 200 * @param command the command line to execute
6f8ff91a
KL
201 */
202 public TTerminalWindow(final TApplication application, final int x,
a0d734e6 203 final int y, final int flags, final String [] command) {
6f8ff91a 204
a69ed767
KL
205 this(application, x, y, flags, command,
206 System.getProperty("jexer.TTerminal.closeOnExit",
207 "false").equals("true"));
208 }
209
210 /**
211 * Public constructor spawns a custom command line.
212 *
213 * @param application TApplication that manages this window
214 * @param x column relative to parent
215 * @param y row relative to parent
216 * @param flags mask of CENTERED, MODAL, or RESIZABLE
217 * @param command the command line to execute
218 * @param closeOnExit if true, close the window when the command exits
219 */
220 public TTerminalWindow(final TApplication application, final int x,
221 final int y, final int flags, final String [] command,
222 final boolean closeOnExit) {
223
6f8ff91a
KL
224 super(application, i18n.getString("windowTitle"), x, y,
225 80 + 2, 24 + 2, flags);
226
a69ed767
KL
227 this.closeOnExit = closeOnExit;
228
a0d734e6 229 String [] fullCommand;
6f8ff91a
KL
230
231 // Spawn a shell and pass its I/O to the other constructor.
232 if ((System.getProperty("jexer.TTerminal.ptypipe") != null)
233 && (System.getProperty("jexer.TTerminal.ptypipe").
234 equals("true"))
235 ) {
236 ptypipe = true;
a0d734e6
KL
237 fullCommand = new String[command.length + 1];
238 fullCommand[0] = "ptypipe";
239 System.arraycopy(command, 0, fullCommand, 1, command.length);
6f8ff91a 240 } else if (System.getProperty("os.name").startsWith("Windows")) {
a0d734e6
KL
241 fullCommand = new String[3];
242 fullCommand[0] = "cmd";
243 fullCommand[1] = "/c";
244 fullCommand[2] = stringArrayToString(command);
6f8ff91a 245 } else if (System.getProperty("os.name").startsWith("Mac")) {
a0d734e6
KL
246 fullCommand = new String[6];
247 fullCommand[0] = "script";
248 fullCommand[1] = "-q";
249 fullCommand[2] = "-F";
250 fullCommand[3] = "/dev/null";
251 fullCommand[4] = "-c";
252 fullCommand[5] = stringArrayToString(command);
6f8ff91a 253 } else {
a0d734e6
KL
254 // Default: behave like Linux
255 fullCommand = new String[5];
256 fullCommand[0] = "script";
257 fullCommand[1] = "-fqe";
258 fullCommand[2] = "/dev/null";
259 fullCommand[3] = "-c";
260 fullCommand[4] = stringArrayToString(command);
6f8ff91a 261 }
a0d734e6 262 spawnShell(fullCommand);
6f8ff91a
KL
263 }
264
265 /**
266 * Public constructor spawns a shell.
267 *
268 * @param application TApplication that manages this window
269 * @param x column relative to parent
270 * @param y row relative to parent
271 * @param flags mask of CENTERED, MODAL, or RESIZABLE
272 */
273 public TTerminalWindow(final TApplication application, final int x,
274 final int y, final int flags) {
275
a69ed767
KL
276 this(application, x, y, flags,
277 System.getProperty("jexer.TTerminal.closeOnExit",
278 "false").equals("true"));
279
280 }
281
282 /**
283 * Public constructor spawns a shell.
284 *
285 * @param application TApplication that manages this window
286 * @param x column relative to parent
287 * @param y row relative to parent
288 * @param flags mask of CENTERED, MODAL, or RESIZABLE
289 * @param closeOnExit if true, close the window when the shell exits
290 */
291 public TTerminalWindow(final TApplication application, final int x,
292 final int y, final int flags, final boolean closeOnExit) {
293
6f8ff91a
KL
294 super(application, i18n.getString("windowTitle"), x, y,
295 80 + 2, 24 + 2, flags);
296
a69ed767
KL
297 this.closeOnExit = closeOnExit;
298
c348e2b5
KL
299 if (System.getProperty("jexer.TTerminal.shell") != null) {
300 String shell = System.getProperty("jexer.TTerminal.shell");
301 if (shell.trim().startsWith("ptypipe")) {
302 ptypipe = true;
303 }
304 spawnShell(shell.split("\\s+"));
305 return;
306 }
307
6f8ff91a
KL
308 String cmdShellWindows = "cmd.exe";
309
310 // You cannot run a login shell in a bare Process interactively, due
311 // to libc's behavior of buffering when stdin/stdout aren't a tty.
312 // Use 'script' instead to run a shell in a pty. And because BSD and
313 // GNU differ on the '-f' vs '-F' flags, we need two different
314 // commands. Lovely.
315 String cmdShellGNU = "script -fqe /dev/null";
316 String cmdShellBSD = "script -q -F /dev/null";
317
318 // ptypipe is another solution that permits dynamic window resizing.
319 String cmdShellPtypipe = "ptypipe /bin/bash --login";
320
321 // Spawn a shell and pass its I/O to the other constructor.
322 if ((System.getProperty("jexer.TTerminal.ptypipe") != null)
323 && (System.getProperty("jexer.TTerminal.ptypipe").
324 equals("true"))
325 ) {
326 ptypipe = true;
00691e80 327 spawnShell(cmdShellPtypipe.split("\\s+"));
6f8ff91a 328 } else if (System.getProperty("os.name").startsWith("Windows")) {
00691e80 329 spawnShell(cmdShellWindows.split("\\s+"));
6f8ff91a 330 } else if (System.getProperty("os.name").startsWith("Mac")) {
00691e80 331 spawnShell(cmdShellBSD.split("\\s+"));
6f8ff91a 332 } else if (System.getProperty("os.name").startsWith("Linux")) {
00691e80 333 spawnShell(cmdShellGNU.split("\\s+"));
6f8ff91a
KL
334 } else {
335 // When all else fails, assume GNU.
00691e80 336 spawnShell(cmdShellGNU.split("\\s+"));
6f8ff91a
KL
337 }
338 }
339
615a0d99
KL
340 // ------------------------------------------------------------------------
341 // TScrollableWindow ------------------------------------------------------
342 // ------------------------------------------------------------------------
55d2b2c2 343
34a42e78
KL
344 /**
345 * Draw the display buffer.
346 */
347 @Override
348 public void draw() {
ab215e38
KL
349 int width = getDisplayWidth();
350 boolean syncEmulator = false;
027de5ae 351 if ((System.currentTimeMillis() - lastUpdateTime >= 25)
ab215e38
KL
352 && (dirty == true)
353 ) {
354 // Too much time has passed, draw it all.
355 syncEmulator = true;
356 } else if (emulator.isReading() && (dirty == false)) {
357 // Wait until the emulator has brought more data in.
358 syncEmulator = false;
359 } else if (!emulator.isReading() && (dirty == true)) {
360 // The emulator won't receive more data, update the display.
361 syncEmulator = true;
362 }
34a42e78 363
ab215e38
KL
364 if ((syncEmulator == true)
365 || (scrollback == null)
366 || (display == null)
367 ) {
368 // We want to minimize the amount of time we have the emulator
369 // locked. Grab a copy of its display.
370 synchronized (emulator) {
371 // Update the scroll bars
372 reflowData();
34a42e78 373
ab215e38
KL
374 if ((scrollback == null) || emulator.isReading()) {
375 scrollback = copyBuffer(emulator.getScrollbackBuffer());
376 display = copyBuffer(emulator.getDisplayBuffer());
34a42e78 377 }
ab215e38 378 width = emulator.getWidth();
34a42e78 379 }
ab215e38
KL
380 dirty = false;
381 }
34a42e78 382
ab215e38
KL
383 // Draw the box using my superclass
384 super.draw();
34a42e78 385
ab215e38
KL
386 // Put together the visible rows
387 int visibleHeight = getHeight() - 2;
388 int visibleBottom = scrollback.size() + display.size()
389 + getVerticalValue();
390 assert (visibleBottom >= 0);
34a42e78 391
ab215e38
KL
392 List<DisplayLine> preceedingBlankLines = new ArrayList<DisplayLine>();
393 int visibleTop = visibleBottom - visibleHeight;
394 if (visibleTop < 0) {
395 for (int i = visibleTop; i < 0; i++) {
396 preceedingBlankLines.add(emulator.getBlankDisplayLine());
397 }
398 visibleTop = 0;
399 }
400 assert (visibleTop >= 0);
401
402 List<DisplayLine> displayLines = new ArrayList<DisplayLine>();
403 displayLines.addAll(scrollback);
404 displayLines.addAll(display);
405
406 List<DisplayLine> visibleLines = new ArrayList<DisplayLine>();
407 visibleLines.addAll(preceedingBlankLines);
408 visibleLines.addAll(displayLines.subList(visibleTop,
409 visibleBottom));
410
411 visibleHeight -= visibleLines.size();
412 assert (visibleHeight >= 0);
413
414 // Now draw the emulator screen
415 int row = 1;
416 for (DisplayLine line: visibleLines) {
417 int widthMax = width;
418 if (line.isDoubleWidth()) {
419 widthMax /= 2;
420 }
421 if (widthMax > getWidth() - 2) {
422 widthMax = getWidth() - 2;
423 }
424 for (int i = 0; i < widthMax; i++) {
425 Cell ch = line.charAt(i);
34a42e78 426
ab215e38
KL
427 if (ch.isImage()) {
428 putCharXY(i + 1, row, ch);
429 continue;
34a42e78 430 }
9588c713 431
027de5ae 432 Cell newCell = new Cell(ch);
ab215e38
KL
433 boolean reverse = line.isReverseColor() ^ ch.isReverse();
434 newCell.setReverse(false);
435 if (reverse) {
436 if (ch.getForeColorRGB() < 0) {
437 newCell.setBackColor(ch.getForeColor());
438 newCell.setBackColorRGB(-1);
439 } else {
440 newCell.setBackColorRGB(ch.getForeColorRGB());
34a42e78 441 }
ab215e38
KL
442 if (ch.getBackColorRGB() < 0) {
443 newCell.setForeColor(ch.getBackColor());
444 newCell.setForeColorRGB(-1);
34a42e78 445 } else {
ab215e38 446 newCell.setForeColorRGB(ch.getBackColorRGB());
34a42e78
KL
447 }
448 }
ab215e38
KL
449 if (line.isDoubleWidth()) {
450 putDoubleWidthCharXY(line, (i * 2) + 1, row, newCell);
451 } else {
452 putCharXY(i + 1, row, newCell);
34a42e78
KL
453 }
454 }
ab215e38
KL
455 row++;
456 if (row == getHeight() - 1) {
457 // Don't overwrite the box edge
458 break;
34a42e78 459 }
ab215e38
KL
460 }
461 CellAttributes background = new CellAttributes();
462 // Fill in the blank lines on bottom
463 for (int i = 0; i < visibleHeight; i++) {
464 hLineXY(1, i + row, getWidth() - 2, ' ', background);
465 }
34a42e78
KL
466
467 }
468
107bba16
KL
469 /**
470 * Handle window close.
471 */
472 @Override
473 public void onClose() {
474 emulator.close();
475 if (shell != null) {
476 terminateShellChildProcess();
477 shell.destroy();
478 shell = null;
479 }
480 }
481
be72cb5c 482 /**
615a0d99 483 * Handle window/screen resize events.
aa77d682 484 *
615a0d99 485 * @param resize resize event
aa77d682 486 */
615a0d99
KL
487 @Override
488 public void onResize(final TResizeEvent resize) {
489
490 // Synchronize against the emulator so we don't stomp on its reader
491 // thread.
492 synchronized (emulator) {
493
494 if (resize.getType() == TResizeEvent.Type.WIDGET) {
495 // Resize the scroll bars
496 reflowData();
497 placeScrollbars();
498
499 // Get out of scrollback
500 setVerticalValue(0);
501
502 if (ptypipe) {
503 emulator.setWidth(getWidth() - 2);
504 emulator.setHeight(getHeight() - 2);
505
506 emulator.writeRemote("\033[8;" + (getHeight() - 2) + ";" +
507 (getWidth() - 2) + "t");
508 }
509 }
510 return;
511
512 } // synchronized (emulator)
aa77d682
KL
513 }
514
515 /**
615a0d99 516 * Resize scrollbars for a new width/height.
aa77d682 517 */
615a0d99
KL
518 @Override
519 public void reflowData() {
520
521 // Synchronize against the emulator so we don't stomp on its reader
522 // thread.
523 synchronized (emulator) {
524
525 // Pull cursor information
526 readEmulatorState();
527
528 // Vertical scrollbar
529 setTopValue(getHeight() - 2
530 - (emulator.getScrollbackBuffer().size()
531 + emulator.getDisplayBuffer().size()));
532 setVerticalBigChange(getHeight() - 2);
533
534 } // synchronized (emulator)
aa77d682
KL
535 }
536
b2d49e0f 537 /**
615a0d99
KL
538 * Handle keystrokes.
539 *
540 * @param keypress keystroke event
34a42e78 541 */
615a0d99
KL
542 @Override
543 public void onKeypress(final TKeypressEvent keypress) {
80b1b7b5
KL
544 if (hideMouseWhenTyping) {
545 typingHidMouse = true;
546 }
34a42e78
KL
547
548 // Scrollback up/down
549 if (keypress.equals(kbShiftPgUp)
550 || keypress.equals(kbCtrlPgUp)
551 || keypress.equals(kbAltPgUp)
552 ) {
56661844 553 bigVerticalDecrement();
34a42e78
KL
554 return;
555 }
556 if (keypress.equals(kbShiftPgDn)
557 || keypress.equals(kbCtrlPgDn)
558 || keypress.equals(kbAltPgDn)
559 ) {
56661844 560 bigVerticalIncrement();
34a42e78
KL
561 return;
562 }
563
ab215e38
KL
564 if (emulator.isReading()) {
565 // Get out of scrollback
566 setVerticalValue(0);
567 emulator.addUserEvent(keypress);
92554d64 568
ab215e38
KL
569 // UGLY HACK TIME! cmd.exe needs CRLF, not just CR, so if
570 // this is kBEnter then also send kbCtrlJ.
c348e2b5
KL
571 if (keypress.equals(kbEnter)) {
572 if (System.getProperty("os.name").startsWith("Windows")
573 && (System.getProperty("jexer.TTerminal.cmdHack",
574 "true").equals("true"))
575 ) {
ab215e38 576 emulator.addUserEvent(new TKeypressEvent(kbCtrlJ));
92554d64 577 }
34a42e78 578 }
ab215e38
KL
579
580 readEmulatorState();
581 return;
34a42e78
KL
582 }
583
584 // Process is closed, honor "normal" TUI keystrokes
585 super.onKeypress(keypress);
586 }
587
588 /**
589 * Handle mouse press events.
590 *
591 * @param mouse mouse button press event
592 */
593 @Override
594 public void onMouseDown(final TMouseEvent mouse) {
bd8d51fa
KL
595 if (inWindowMove || inWindowResize) {
596 // TWindow needs to deal with this.
597 super.onMouseDown(mouse);
598 return;
599 }
34a42e78 600
80b1b7b5
KL
601 if (hideMouseWhenTyping) {
602 typingHidMouse = false;
603 }
604
69a8c368
KL
605 // If the emulator is tracking mouse buttons, it needs to see wheel
606 // events.
607 if (emulator.getMouseProtocol() == ECMA48.MouseProtocol.OFF) {
608 if (mouse.isMouseWheelUp()) {
609 verticalDecrement();
610 return;
611 }
612 if (mouse.isMouseWheelDown()) {
613 verticalIncrement();
614 return;
615 }
34a42e78 616 }
bd8d51fa 617 if (mouseOnEmulator(mouse)) {
ab215e38
KL
618 mouse.setX(mouse.getX() - 1);
619 mouse.setY(mouse.getY() - 1);
620 emulator.addUserEvent(mouse);
621 readEmulatorState();
622 return;
bd8d51fa 623 }
34a42e78 624
bd8d51fa 625 // Emulator didn't consume it, pass it on
34a42e78
KL
626 super.onMouseDown(mouse);
627 }
628
bd8d51fa
KL
629 /**
630 * Handle mouse release events.
631 *
632 * @param mouse mouse button release event
633 */
634 @Override
635 public void onMouseUp(final TMouseEvent mouse) {
636 if (inWindowMove || inWindowResize) {
637 // TWindow needs to deal with this.
638 super.onMouseUp(mouse);
639 return;
640 }
641
80b1b7b5
KL
642 if (hideMouseWhenTyping) {
643 typingHidMouse = false;
644 }
645
bd8d51fa 646 if (mouseOnEmulator(mouse)) {
ab215e38
KL
647 mouse.setX(mouse.getX() - 1);
648 mouse.setY(mouse.getY() - 1);
649 emulator.addUserEvent(mouse);
650 readEmulatorState();
651 return;
bd8d51fa
KL
652 }
653
654 // Emulator didn't consume it, pass it on
655 super.onMouseUp(mouse);
656 }
657
658 /**
659 * Handle mouse motion events.
660 *
661 * @param mouse mouse motion event
662 */
663 @Override
664 public void onMouseMotion(final TMouseEvent mouse) {
665 if (inWindowMove || inWindowResize) {
666 // TWindow needs to deal with this.
667 super.onMouseMotion(mouse);
668 return;
669 }
670
80b1b7b5
KL
671 if (hideMouseWhenTyping) {
672 typingHidMouse = false;
673 }
674
bd8d51fa 675 if (mouseOnEmulator(mouse)) {
ab215e38
KL
676 mouse.setX(mouse.getX() - 1);
677 mouse.setY(mouse.getY() - 1);
678 emulator.addUserEvent(mouse);
679 readEmulatorState();
680 return;
bd8d51fa
KL
681 }
682
683 // Emulator didn't consume it, pass it on
684 super.onMouseMotion(mouse);
685 }
686
615a0d99
KL
687 // ------------------------------------------------------------------------
688 // TTerminalWindow --------------------------------------------------------
689 // ------------------------------------------------------------------------
690
80b1b7b5
KL
691 /**
692 * Returns true if this window does not want the application-wide mouse
693 * cursor drawn over it.
694 *
695 * @return true if this window does not want the application-wide mouse
696 * cursor drawn over it
697 */
698 @Override
699 public boolean hasHiddenMouse() {
700 return (super.hasHiddenMouse() || typingHidMouse);
701 }
702
615a0d99
KL
703 /**
704 * Claim the keystrokes the emulator will need.
705 */
706 private void addShortcutKeys() {
707 addShortcutKeypress(kbCtrlA);
708 addShortcutKeypress(kbCtrlB);
709 addShortcutKeypress(kbCtrlC);
710 addShortcutKeypress(kbCtrlD);
711 addShortcutKeypress(kbCtrlE);
712 addShortcutKeypress(kbCtrlF);
713 addShortcutKeypress(kbCtrlG);
714 addShortcutKeypress(kbCtrlH);
715 addShortcutKeypress(kbCtrlU);
716 addShortcutKeypress(kbCtrlJ);
717 addShortcutKeypress(kbCtrlK);
718 addShortcutKeypress(kbCtrlL);
719 addShortcutKeypress(kbCtrlM);
720 addShortcutKeypress(kbCtrlN);
721 addShortcutKeypress(kbCtrlO);
722 addShortcutKeypress(kbCtrlP);
723 addShortcutKeypress(kbCtrlQ);
724 addShortcutKeypress(kbCtrlR);
725 addShortcutKeypress(kbCtrlS);
726 addShortcutKeypress(kbCtrlT);
727 addShortcutKeypress(kbCtrlU);
728 addShortcutKeypress(kbCtrlV);
729 addShortcutKeypress(kbCtrlW);
730 addShortcutKeypress(kbCtrlX);
731 addShortcutKeypress(kbCtrlY);
732 addShortcutKeypress(kbCtrlZ);
733 addShortcutKeypress(kbF1);
734 addShortcutKeypress(kbF2);
735 addShortcutKeypress(kbF3);
736 addShortcutKeypress(kbF4);
737 addShortcutKeypress(kbF5);
738 addShortcutKeypress(kbF6);
739 addShortcutKeypress(kbF7);
740 addShortcutKeypress(kbF8);
741 addShortcutKeypress(kbF9);
742 addShortcutKeypress(kbF10);
743 addShortcutKeypress(kbF11);
744 addShortcutKeypress(kbF12);
745 addShortcutKeypress(kbAltA);
746 addShortcutKeypress(kbAltB);
747 addShortcutKeypress(kbAltC);
748 addShortcutKeypress(kbAltD);
749 addShortcutKeypress(kbAltE);
750 addShortcutKeypress(kbAltF);
751 addShortcutKeypress(kbAltG);
752 addShortcutKeypress(kbAltH);
753 addShortcutKeypress(kbAltU);
754 addShortcutKeypress(kbAltJ);
755 addShortcutKeypress(kbAltK);
756 addShortcutKeypress(kbAltL);
757 addShortcutKeypress(kbAltM);
758 addShortcutKeypress(kbAltN);
759 addShortcutKeypress(kbAltO);
760 addShortcutKeypress(kbAltP);
761 addShortcutKeypress(kbAltQ);
762 addShortcutKeypress(kbAltR);
763 addShortcutKeypress(kbAltS);
764 addShortcutKeypress(kbAltT);
765 addShortcutKeypress(kbAltU);
766 addShortcutKeypress(kbAltV);
767 addShortcutKeypress(kbAltW);
768 addShortcutKeypress(kbAltX);
769 addShortcutKeypress(kbAltY);
770 addShortcutKeypress(kbAltZ);
771 }
772
773 /**
774 * Convert a string array to a whitespace-separated string.
775 *
776 * @param array the string array
777 * @return a single string
778 */
779 private String stringArrayToString(final String [] array) {
780 StringBuilder sb = new StringBuilder(array[0].length());
781 for (int i = 0; i < array.length; i++) {
782 sb.append(array[i]);
783 if (i < array.length - 1) {
784 sb.append(' ');
785 }
786 }
787 return sb.toString();
788 }
789
790 /**
791 * Spawn the shell.
792 *
793 * @param command the command line to execute
794 */
795 private void spawnShell(final String [] command) {
796
797 /*
798 System.err.printf("spawnShell(): '%s'\n",
799 stringArrayToString(command));
800 */
801
802 vScroller = new TVScroller(this, getWidth() - 2, 0, getHeight() - 2);
803 setBottomValue(0);
804
805 // Assume XTERM
806 ECMA48.DeviceType deviceType = ECMA48.DeviceType.XTERM;
807
808 try {
809 ProcessBuilder pb = new ProcessBuilder(command);
810 Map<String, String> env = pb.environment();
811 env.put("TERM", ECMA48.deviceTypeTerm(deviceType));
812 env.put("LANG", ECMA48.deviceTypeLang(deviceType, "en"));
813 env.put("COLUMNS", "80");
814 env.put("LINES", "24");
815 pb.redirectErrorStream(true);
816 shell = pb.start();
817 emulator = new ECMA48(deviceType, shell.getInputStream(),
818 shell.getOutputStream(), this);
819 } catch (IOException e) {
820 messageBox(i18n.getString("errorLaunchingShellTitle"),
821 MessageFormat.format(i18n.getString("errorLaunchingShellText"),
822 e.getMessage()));
823 }
824
825 // Setup the scroll bars
826 onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
827 getHeight()));
828
829 // Claim the keystrokes the emulator will need.
830 addShortcutKeys();
831
832 // Add shortcut text
833 newStatusBar(i18n.getString("statusBarRunning"));
5fc7bf09
KL
834
835 // Pass the correct text cell width/height to the emulator
03ae544a
KL
836 emulator.setTextWidth(getScreen().getTextWidth());
837 emulator.setTextHeight(getScreen().getTextHeight());
80b1b7b5
KL
838
839 // Hide mouse when typing option
840 if (System.getProperty("jexer.TTerminal.hideMouseWhenTyping",
841 "true").equals("false")) {
842
843 hideMouseWhenTyping = false;
844 }
615a0d99
KL
845 }
846
847 /**
848 * Terminate the child of the 'script' process used on POSIX. This may
849 * or may not work.
850 */
851 private void terminateShellChildProcess() {
852 int pid = -1;
853 if (shell.getClass().getName().equals("java.lang.UNIXProcess")) {
854 /* get the PID on unix/linux systems */
855 try {
856 Field field = shell.getClass().getDeclaredField("pid");
857 field.setAccessible(true);
858 pid = field.getInt(shell);
859 } catch (Throwable e) {
860 // SQUASH, this didn't work. Just bail out quietly.
861 return;
862 }
863 }
864 if (pid != -1) {
865 // shell.destroy() works successfully at killing this side of
866 // 'script'. But we need to make sure the other side (child
867 // process) is also killed.
868 String [] cmdKillIt = {
869 "pkill", "-P", Integer.toString(pid)
870 };
871 try {
872 Runtime.getRuntime().exec(cmdKillIt);
873 } catch (Throwable e) {
874 // SQUASH, this didn't work. Just bail out quietly.
875 return;
876 }
877 }
878 }
879
615a0d99
KL
880 /**
881 * Hook for subclasses to be notified of the shell termination.
882 */
883 public void onShellExit() {
a69ed767
KL
884 if (closeOnExit) {
885 close();
886 }
615a0d99
KL
887 getApplication().postEvent(new TMenuEvent(TMenu.MID_REPAINT));
888 }
889
890 /**
891 * Copy out variables from the emulator that TTerminal has to expose on
892 * screen.
893 */
894 private void readEmulatorState() {
895 // Synchronize against the emulator so we don't stomp on its reader
896 // thread.
897 synchronized (emulator) {
978a5d8f 898 setHiddenMouse(emulator.hasHiddenMousePointer());
9696a8f6 899
615a0d99
KL
900 setCursorX(emulator.getCursorX() + 1);
901 setCursorY(emulator.getCursorY() + 1
902 + (getHeight() - 2 - emulator.getHeight())
903 - getVerticalValue());
904 setCursorVisible(emulator.isCursorVisible());
905 if (getCursorX() > getWidth() - 2) {
906 setCursorVisible(false);
907 }
908 if ((getCursorY() > getHeight() - 2) || (getCursorY() < 0)) {
909 setCursorVisible(false);
910 }
911 if (emulator.getScreenTitle().length() > 0) {
912 // Only update the title if the shell is still alive
913 if (shell != null) {
914 setTitle(emulator.getScreenTitle());
915 }
916 }
917
918 // Check to see if the shell has died.
919 if (!emulator.isReading() && (shell != null)) {
920 try {
921 int rc = shell.exitValue();
922 // The emulator exited on its own, all is fine
923 setTitle(MessageFormat.format(i18n.
924 getString("windowTitleCompleted"), getTitle(), rc));
925 shell = null;
926 emulator.close();
927 clearShortcutKeypresses();
928 statusBar.setText(MessageFormat.format(i18n.
929 getString("statusBarCompleted"), rc));
930 onShellExit();
931 } catch (IllegalThreadStateException e) {
932 // The emulator thread has exited, but the shell Process
933 // hasn't figured that out yet. Do nothing, we will see
934 // this in a future tick.
935 }
936 } else if (emulator.isReading() && (shell != null)) {
937 // The shell might be dead, let's check
938 try {
939 int rc = shell.exitValue();
940 // If we got here, the shell died.
941 setTitle(MessageFormat.format(i18n.
942 getString("windowTitleCompleted"), getTitle(), rc));
943 shell = null;
944 emulator.close();
945 clearShortcutKeypresses();
946 statusBar.setText(MessageFormat.format(i18n.
947 getString("statusBarCompleted"), rc));
948 onShellExit();
949 } catch (IllegalThreadStateException e) {
950 // The shell is still running, do nothing.
951 }
952 }
953
954 } // synchronized (emulator)
955 }
956
957 /**
958 * Check if a mouse press/release/motion event coordinate is over the
959 * emulator.
960 *
961 * @param mouse a mouse-based event
962 * @return whether or not the mouse is on the emulator
963 */
c88c4ced 964 private boolean mouseOnEmulator(final TMouseEvent mouse) {
615a0d99 965
ab215e38
KL
966 if (!emulator.isReading()) {
967 return false;
615a0d99
KL
968 }
969
970 if ((mouse.getAbsoluteX() >= getAbsoluteX() + 1)
971 && (mouse.getAbsoluteX() < getAbsoluteX() + getWidth() - 1)
972 && (mouse.getAbsoluteY() >= getAbsoluteY() + 1)
973 && (mouse.getAbsoluteY() < getAbsoluteY() + getHeight() - 1)
974 ) {
975 return true;
976 }
977 return false;
978 }
979
ab215e38
KL
980 /**
981 * Copy a display buffer.
982 *
983 * @param buffer the buffer to copy
984 * @return a deep copy of the buffer's data
985 */
986 private List<DisplayLine> copyBuffer(final List<DisplayLine> buffer) {
987 ArrayList<DisplayLine> result = new ArrayList<DisplayLine>(buffer.size());
988 for (DisplayLine line: buffer) {
989 result.add(new DisplayLine(line));
990 }
991 return result;
992 }
993
d1115203
KL
994 /**
995 * Draw glyphs for a double-width or double-height VT100 cell to two
996 * screen cells.
997 *
998 * @param line the line this VT100 cell is in
999 * @param x the X position to draw the left half to
1000 * @param y the Y position to draw to
1001 * @param cell the cell to draw
1002 */
1003 private void putDoubleWidthCharXY(final DisplayLine line, final int x,
1004 final int y, final Cell cell) {
1005
03ae544a
KL
1006 int textWidth = getScreen().getTextWidth();
1007 int textHeight = getScreen().getTextHeight();
b3d79e99 1008 boolean cursorBlinkVisible = true;
d1115203
KL
1009
1010 if (getScreen() instanceof SwingTerminal) {
1011 SwingTerminal terminal = (SwingTerminal) getScreen();
b3d79e99 1012 cursorBlinkVisible = terminal.getCursorBlinkVisible();
d1115203
KL
1013 } else if (getScreen() instanceof ECMA48Terminal) {
1014 ECMA48Terminal terminal = (ECMA48Terminal) getScreen();
1015
5fc7bf09
KL
1016 if (!terminal.hasSixel()) {
1017 // The backend does not have sixel support, draw this as text
1018 // and bail out.
1019 putCharXY(x, y, cell);
1020 putCharXY(x + 1, y, ' ', cell);
1021 return;
1022 }
b3d79e99 1023 cursorBlinkVisible = blinkState;
d1115203
KL
1024 } else {
1025 // We don't know how to dray glyphs to this screen, draw them as
1026 // text and bail out.
1027 putCharXY(x, y, cell);
1028 putCharXY(x + 1, y, ' ', cell);
1029 return;
1030 }
1031
1032 if ((textWidth != lastTextWidth) || (textHeight != lastTextHeight)) {
0d86ab84
KL
1033 // Screen size has changed, reset the font.
1034 setupFont(textHeight);
d1115203
KL
1035 lastTextWidth = textWidth;
1036 lastTextHeight = textHeight;
1037 }
1038 assert (doubleFont != null);
1039
0d86ab84
KL
1040 BufferedImage image;
1041 if (line.getDoubleHeight() == 1) {
1042 // Double-height top half: don't draw the underline.
027de5ae 1043 Cell newCell = new Cell(cell);
0d86ab84
KL
1044 newCell.setUnderline(false);
1045 image = doubleFont.getImage(newCell, textWidth * 2, textHeight * 2,
1046 cursorBlinkVisible);
b3d79e99 1047 } else {
0d86ab84
KL
1048 image = doubleFont.getImage(cell, textWidth * 2, textHeight * 2,
1049 cursorBlinkVisible);
d1115203
KL
1050 }
1051
1052 // Now that we have the double-wide glyph drawn, copy the right
1053 // pieces of it to the cells.
027de5ae
KL
1054 Cell left = new Cell(cell);
1055 Cell right = new Cell(cell);
d1115203
KL
1056 right.setChar(' ');
1057 BufferedImage leftImage = null;
1058 BufferedImage rightImage = null;
0d86ab84
KL
1059 /*
1060 System.err.println("image " + image + " textWidth " + textWidth +
1061 " textHeight " + textHeight);
1062 */
1063
d1115203
KL
1064 switch (line.getDoubleHeight()) {
1065 case 1:
1066 // Top half double height
1067 leftImage = image.getSubimage(0, 0, textWidth, textHeight);
1068 rightImage = image.getSubimage(textWidth, 0, textWidth, textHeight);
1069 break;
1070 case 2:
1071 // Bottom half double height
1072 leftImage = image.getSubimage(0, textHeight, textWidth, textHeight);
1073 rightImage = image.getSubimage(textWidth, textHeight,
1074 textWidth, textHeight);
1075 break;
1076 default:
1077 // Either single height double-width, or error fallback
1078 BufferedImage wideImage = new BufferedImage(textWidth * 2,
1079 textHeight, BufferedImage.TYPE_INT_ARGB);
1080 Graphics2D grWide = wideImage.createGraphics();
1081 grWide.drawImage(image, 0, 0, wideImage.getWidth(),
1082 wideImage.getHeight(), null);
1083 grWide.dispose();
1084 leftImage = wideImage.getSubimage(0, 0, textWidth, textHeight);
1085 rightImage = wideImage.getSubimage(textWidth, 0, textWidth,
1086 textHeight);
1087 break;
1088 }
1089 left.setImage(leftImage);
1090 right.setImage(rightImage);
49380c21
KL
1091 // Since we have image data, ditch the character here. Otherwise, a
1092 // drawBoxShadow() over the terminal window will show the characters
1093 // which looks wrong.
1094 left.setChar(' ');
1095 right.setChar(' ');
d1115203
KL
1096 putCharXY(x, y, left);
1097 putCharXY(x + 1, y, right);
1098 }
1099
1100 /**
0d86ab84 1101 * Set up the double-width font.
d1115203
KL
1102 *
1103 * @param fontSize the size of font to request for the single-width font.
1104 * The double-width font will be 2x this value.
1105 */
0d86ab84 1106 private void setupFont(final int fontSize) {
9588c713 1107 doubleFont = GlyphMaker.getInstance(fontSize * 2);
b3d79e99
KL
1108
1109 // Special case: the ECMA48 backend needs to have a timer to drive
1110 // its blink state.
1111 if (getScreen() instanceof jexer.backend.ECMA48Terminal) {
0d86ab84
KL
1112 if (!haveTimer) {
1113 // Blink every 500 millis.
1114 long millis = 500;
1115 getApplication().addTimer(millis, true,
1116 new TAction() {
1117 public void DO() {
1118 blinkState = !blinkState;
1119 getApplication().doRepaint();
1120 }
b3d79e99 1121 }
0d86ab84
KL
1122 );
1123 haveTimer = true;
1124 }
b3d79e99 1125 }
d1115203
KL
1126 }
1127
ab215e38
KL
1128 // ------------------------------------------------------------------------
1129 // DisplayListener --------------------------------------------------------
1130 // ------------------------------------------------------------------------
1131
1132 /**
1133 * Called by emulator when fresh data has come in.
1134 */
1135 public void displayChanged() {
1136 dirty = true;
1137 getApplication().postEvent(new TMenuEvent(TMenu.MID_REPAINT));
1138 }
1139
1140 /**
1141 * Function to call to obtain the display width.
1142 *
1143 * @return the number of columns in the display
1144 */
1145 public int getDisplayWidth() {
1146 if (ptypipe) {
1147 return getWidth() - 2;
1148 }
1149 return 80;
1150 }
1151
1152 /**
1153 * Function to call to obtain the display height.
1154 *
1155 * @return the number of rows in the display
1156 */
1157 public int getDisplayHeight() {
1158 if (ptypipe) {
1159 return getHeight() - 2;
1160 }
1161 return 24;
1162 }
1163
34a42e78 1164}