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