2 * Jexer - Java Text User Interface
4 * The MIT License (MIT)
6 * Copyright (C) 2017 Kevin Lamonte
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:
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
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.
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
31 import java
.io
.IOException
;
32 import java
.lang
.reflect
.Field
;
33 import java
.text
.MessageFormat
;
34 import java
.util
.LinkedList
;
35 import java
.util
.List
;
37 import java
.util
.ResourceBundle
;
39 import jexer
.bits
.Cell
;
40 import jexer
.bits
.CellAttributes
;
41 import jexer
.event
.TKeypressEvent
;
42 import jexer
.event
.TMouseEvent
;
43 import jexer
.event
.TResizeEvent
;
44 import jexer
.tterminal
.DisplayLine
;
45 import jexer
.tterminal
.DisplayListener
;
46 import jexer
.tterminal
.ECMA48
;
47 import static jexer
.TKeypress
.*;
50 * TTerminalWindow exposes a ECMA-48 / ANSI X3.64 style terminal in a window.
52 public class TTerminalWindow
extends TScrollableWindow
53 implements DisplayListener
{
59 private static final ResourceBundle i18n
= ResourceBundle
.getBundle(TTerminalWindow
.class.getName());
64 private ECMA48 emulator
;
67 * The Process created by the shell spawning constructor.
69 private Process shell
;
72 * If true, we are using the ptypipe utility to support dynamic window
73 * resizing. ptypipe is available at
74 * https://github.com/klamonte/ptypipe .
76 private boolean ptypipe
= false;
79 * Claim the keystrokes the emulator will need.
81 private void addShortcutKeys() {
82 addShortcutKeypress(kbCtrlA
);
83 addShortcutKeypress(kbCtrlB
);
84 addShortcutKeypress(kbCtrlC
);
85 addShortcutKeypress(kbCtrlD
);
86 addShortcutKeypress(kbCtrlE
);
87 addShortcutKeypress(kbCtrlF
);
88 addShortcutKeypress(kbCtrlG
);
89 addShortcutKeypress(kbCtrlH
);
90 addShortcutKeypress(kbCtrlU
);
91 addShortcutKeypress(kbCtrlJ
);
92 addShortcutKeypress(kbCtrlK
);
93 addShortcutKeypress(kbCtrlL
);
94 addShortcutKeypress(kbCtrlM
);
95 addShortcutKeypress(kbCtrlN
);
96 addShortcutKeypress(kbCtrlO
);
97 addShortcutKeypress(kbCtrlP
);
98 addShortcutKeypress(kbCtrlQ
);
99 addShortcutKeypress(kbCtrlR
);
100 addShortcutKeypress(kbCtrlS
);
101 addShortcutKeypress(kbCtrlT
);
102 addShortcutKeypress(kbCtrlU
);
103 addShortcutKeypress(kbCtrlV
);
104 addShortcutKeypress(kbCtrlW
);
105 addShortcutKeypress(kbCtrlX
);
106 addShortcutKeypress(kbCtrlY
);
107 addShortcutKeypress(kbCtrlZ
);
108 addShortcutKeypress(kbF1
);
109 addShortcutKeypress(kbF2
);
110 addShortcutKeypress(kbF3
);
111 addShortcutKeypress(kbF4
);
112 addShortcutKeypress(kbF5
);
113 addShortcutKeypress(kbF6
);
114 addShortcutKeypress(kbF7
);
115 addShortcutKeypress(kbF8
);
116 addShortcutKeypress(kbF9
);
117 addShortcutKeypress(kbF10
);
118 addShortcutKeypress(kbF11
);
119 addShortcutKeypress(kbF12
);
120 addShortcutKeypress(kbAltA
);
121 addShortcutKeypress(kbAltB
);
122 addShortcutKeypress(kbAltC
);
123 addShortcutKeypress(kbAltD
);
124 addShortcutKeypress(kbAltE
);
125 addShortcutKeypress(kbAltF
);
126 addShortcutKeypress(kbAltG
);
127 addShortcutKeypress(kbAltH
);
128 addShortcutKeypress(kbAltU
);
129 addShortcutKeypress(kbAltJ
);
130 addShortcutKeypress(kbAltK
);
131 addShortcutKeypress(kbAltL
);
132 addShortcutKeypress(kbAltM
);
133 addShortcutKeypress(kbAltN
);
134 addShortcutKeypress(kbAltO
);
135 addShortcutKeypress(kbAltP
);
136 addShortcutKeypress(kbAltQ
);
137 addShortcutKeypress(kbAltR
);
138 addShortcutKeypress(kbAltS
);
139 addShortcutKeypress(kbAltT
);
140 addShortcutKeypress(kbAltU
);
141 addShortcutKeypress(kbAltV
);
142 addShortcutKeypress(kbAltW
);
143 addShortcutKeypress(kbAltX
);
144 addShortcutKeypress(kbAltY
);
145 addShortcutKeypress(kbAltZ
);
149 * Public constructor spawns a shell.
151 * @param application TApplication that manages this window
152 * @param x column relative to parent
153 * @param y row relative to parent
154 * @param flags mask of CENTERED, MODAL, or RESIZABLE
156 public TTerminalWindow(final TApplication application
, final int x
,
157 final int y
, final int flags
) {
159 super(application
, i18n
.getString("windowTitle"), x
, y
,
160 80 + 2, 24 + 2, flags
);
162 vScroller
= new TVScroller(this, getWidth() - 2, 0, getHeight() - 2);
166 ECMA48
.DeviceType deviceType
= ECMA48
.DeviceType
.XTERM
;
169 String
[] cmdShellWindows
= {
173 // You cannot run a login shell in a bare Process interactively,
174 // due to libc's behavior of buffering when stdin/stdout aren't a
175 // tty. Use 'script' instead to run a shell in a pty. And
176 // because BSD and GNU differ on the '-f' vs '-F' flags, we need
177 // two different commands. Lovely.
178 String
[] cmdShellGNU
= {
179 "script", "-fqe", "/dev/null"
181 String
[] cmdShellBSD
= {
182 "script", "-q", "-F", "/dev/null"
184 String
[] cmdShellPtypipe
= {
185 "ptypipe", "/bin/bash", "--login"
187 // Spawn a shell and pass its I/O to the other constructor.
190 if ((System
.getProperty("jexer.TTerminal.ptypipe") != null)
191 && (System
.getProperty("jexer.TTerminal.ptypipe").
194 pb
= new ProcessBuilder(cmdShellPtypipe
);
196 } else if (System
.getProperty("os.name").startsWith("Windows")) {
197 pb
= new ProcessBuilder(cmdShellWindows
);
198 } else if (System
.getProperty("os.name").startsWith("Mac")) {
199 pb
= new ProcessBuilder(cmdShellBSD
);
200 } else if (System
.getProperty("os.name").startsWith("Linux")) {
201 pb
= new ProcessBuilder(cmdShellGNU
);
203 // When all else fails, assume GNU.
204 pb
= new ProcessBuilder(cmdShellGNU
);
206 Map
<String
, String
> env
= pb
.environment();
207 env
.put("TERM", ECMA48
.deviceTypeTerm(deviceType
));
208 env
.put("LANG", ECMA48
.deviceTypeLang(deviceType
, "en"));
209 env
.put("COLUMNS", "80");
210 env
.put("LINES", "24");
211 pb
.redirectErrorStream(true);
213 emulator
= new ECMA48(deviceType
, shell
.getInputStream(),
214 shell
.getOutputStream(), this);
215 } catch (IOException e
) {
216 messageBox(i18n
.getString("errorLaunchingShellTitle"),
217 MessageFormat
.format(i18n
.getString("errorLaunchingShellText"),
221 // Setup the scroll bars
222 onResize(new TResizeEvent(TResizeEvent
.Type
.WIDGET
, getWidth(),
225 // Claim the keystrokes the emulator will need.
229 newStatusBar(i18n
.getString("statusBarRunning"));
233 * Terminate the child of the 'script' process used on POSIX. This may
236 private void terminateShellChildProcess() {
238 if (shell
.getClass().getName().equals("java.lang.UNIXProcess")) {
239 /* get the PID on unix/linux systems */
241 Field field
= shell
.getClass().getDeclaredField("pid");
242 field
.setAccessible(true);
243 pid
= field
.getInt(shell
);
244 } catch (Throwable e
) {
245 // SQUASH, this didn't work. Just bail out quietly.
250 // shell.destroy() works successfully at killing this side of
251 // 'script'. But we need to make sure the other side (child
252 // process) is also killed.
253 String
[] cmdKillIt
= {
254 "pkill", "-P", Integer
.toString(pid
)
257 Runtime
.getRuntime().exec(cmdKillIt
);
258 } catch (Throwable e
) {
259 // SQUASH, this didn't work. Just bail out quietly.
266 * Draw the display buffer.
270 // Synchronize against the emulator so we don't stomp on its reader
272 synchronized (emulator
) {
274 // Update the scroll bars
277 // Draw the box using my superclass
280 List
<DisplayLine
> scrollback
= emulator
.getScrollbackBuffer();
281 List
<DisplayLine
> display
= emulator
.getDisplayBuffer();
283 // Put together the visible rows
284 int visibleHeight
= getHeight() - 2;
285 int visibleBottom
= scrollback
.size() + display
.size()
286 + getVerticalValue();
287 assert (visibleBottom
>= 0);
289 List
<DisplayLine
> preceedingBlankLines
= new LinkedList
<DisplayLine
>();
290 int visibleTop
= visibleBottom
- visibleHeight
;
291 if (visibleTop
< 0) {
292 for (int i
= visibleTop
; i
< 0; i
++) {
293 preceedingBlankLines
.add(emulator
.getBlankDisplayLine());
297 assert (visibleTop
>= 0);
299 List
<DisplayLine
> displayLines
= new LinkedList
<DisplayLine
>();
300 displayLines
.addAll(scrollback
);
301 displayLines
.addAll(display
);
303 List
<DisplayLine
> visibleLines
= new LinkedList
<DisplayLine
>();
304 visibleLines
.addAll(preceedingBlankLines
);
305 visibleLines
.addAll(displayLines
.subList(visibleTop
,
308 visibleHeight
-= visibleLines
.size();
309 assert (visibleHeight
>= 0);
311 // Now draw the emulator screen
313 for (DisplayLine line
: visibleLines
) {
314 int widthMax
= emulator
.getWidth();
315 if (line
.isDoubleWidth()) {
318 if (widthMax
> getWidth() - 2) {
319 widthMax
= getWidth() - 2;
321 for (int i
= 0; i
< widthMax
; i
++) {
322 Cell ch
= line
.charAt(i
);
323 Cell newCell
= new Cell();
325 boolean reverse
= line
.isReverseColor() ^ ch
.isReverse();
326 newCell
.setReverse(false);
328 newCell
.setBackColor(ch
.getForeColor());
329 newCell
.setForeColor(ch
.getBackColor());
331 if (line
.isDoubleWidth()) {
332 getScreen().putCharXY((i
* 2) + 1, row
, newCell
);
333 getScreen().putCharXY((i
* 2) + 2, row
, ' ', newCell
);
335 getScreen().putCharXY(i
+ 1, row
, newCell
);
339 if (row
== getHeight() - 1) {
340 // Don't overwrite the box edge
344 CellAttributes background
= new CellAttributes();
345 // Fill in the blank lines on bottom
346 for (int i
= 0; i
< visibleHeight
; i
++) {
347 getScreen().hLineXY(1, i
+ row
, getWidth() - 2, ' ',
351 } // synchronized (emulator)
356 * Called by emulator when fresh data has come in.
358 public void displayChanged() {
363 * Function to call to obtain the display width.
365 * @return the number of columns in the display
367 public int getDisplayWidth() {
369 return getWidth() - 2;
375 * Function to call to obtain the display height.
377 * @return the number of rows in the display
379 public int getDisplayHeight() {
381 return getHeight() - 2;
387 * Handle window close.
390 public void onClose() {
393 terminateShellChildProcess();
400 * Copy out variables from the emulator that TTerminal has to expose on
403 private void readEmulatorState() {
404 // Synchronize against the emulator so we don't stomp on its reader
406 synchronized (emulator
) {
408 setCursorX(emulator
.getCursorX() + 1);
409 setCursorY(emulator
.getCursorY() + 1
410 + (getHeight() - 2 - emulator
.getHeight())
411 - getVerticalValue());
412 setCursorVisible(emulator
.isCursorVisible());
413 if (getCursorX() > getWidth() - 2) {
414 setCursorVisible(false);
416 if ((getCursorY() > getHeight() - 2) || (getCursorY() < 0)) {
417 setCursorVisible(false);
419 if (emulator
.getScreenTitle().length() > 0) {
420 // Only update the title if the shell is still alive
422 setTitle(emulator
.getScreenTitle());
426 // Check to see if the shell has died.
427 if (!emulator
.isReading() && (shell
!= null)) {
429 int rc
= shell
.exitValue();
430 // The emulator exited on its own, all is fine
431 setTitle(MessageFormat
.format(i18n
.
432 getString("windowTitleCompleted"), getTitle(), rc
));
435 clearShortcutKeypresses();
436 statusBar
.setText(MessageFormat
.format(i18n
.
437 getString("statusBarCompleted"), rc
));
438 } catch (IllegalThreadStateException e
) {
439 // The emulator thread has exited, but the shell Process
440 // hasn't figured that out yet. Do nothing, we will see
441 // this in a future tick.
443 } else if (emulator
.isReading() && (shell
!= null)) {
444 // The shell might be dead, let's check
446 int rc
= shell
.exitValue();
447 // If we got here, the shell died.
448 setTitle(MessageFormat
.format(i18n
.
449 getString("windowTitleCompleted"), getTitle(), rc
));
452 clearShortcutKeypresses();
453 statusBar
.setText(MessageFormat
.format(i18n
.
454 getString("statusBarCompleted"), rc
));
455 } catch (IllegalThreadStateException e
) {
456 // The shell is still running, do nothing.
460 } // synchronized (emulator)
464 * Handle window/screen resize events.
466 * @param resize resize event
469 public void onResize(final TResizeEvent resize
) {
471 // Synchronize against the emulator so we don't stomp on its reader
473 synchronized (emulator
) {
475 if (resize
.getType() == TResizeEvent
.Type
.WIDGET
) {
476 // Resize the scroll bars
480 // Get out of scrollback
484 emulator
.setWidth(getWidth() - 2);
485 emulator
.setHeight(getHeight() - 2);
487 emulator
.writeRemote("\033[8;" + (getHeight() - 2) + ";" +
488 (getWidth() - 2) + "t");
493 } // synchronized (emulator)
497 * Resize scrollbars for a new width/height.
500 public void reflowData() {
502 // Synchronize against the emulator so we don't stomp on its reader
504 synchronized (emulator
) {
506 // Pull cursor information
509 // Vertical scrollbar
510 setTopValue(getHeight() - 2
511 - (emulator
.getScrollbackBuffer().size()
512 + emulator
.getDisplayBuffer().size()));
513 setVerticalBigChange(getHeight() - 2);
515 } // synchronized (emulator)
519 * Check if a mouse press/release/motion event coordinate is over the
522 * @param mouse a mouse-based event
523 * @return whether or not the mouse is on the emulator
525 private final boolean mouseOnEmulator(final TMouseEvent mouse
) {
527 synchronized (emulator
) {
528 if (!emulator
.isReading()) {
533 if ((mouse
.getAbsoluteX() >= getAbsoluteX() + 1)
534 && (mouse
.getAbsoluteX() < getAbsoluteX() + getWidth() - 1)
535 && (mouse
.getAbsoluteY() >= getAbsoluteY() + 1)
536 && (mouse
.getAbsoluteY() < getAbsoluteY() + getHeight() - 1)
546 * @param keypress keystroke event
549 public void onKeypress(final TKeypressEvent keypress
) {
551 // Scrollback up/down
552 if (keypress
.equals(kbShiftPgUp
)
553 || keypress
.equals(kbCtrlPgUp
)
554 || keypress
.equals(kbAltPgUp
)
556 bigVerticalDecrement();
559 if (keypress
.equals(kbShiftPgDn
)
560 || keypress
.equals(kbCtrlPgDn
)
561 || keypress
.equals(kbAltPgDn
)
563 bigVerticalIncrement();
567 // Synchronize against the emulator so we don't stomp on its reader
569 synchronized (emulator
) {
570 if (emulator
.isReading()) {
571 // Get out of scrollback
573 emulator
.keypress(keypress
.getKey());
575 // UGLY HACK TIME! cmd.exe needs CRLF, not just CR, so if
576 // this is kBEnter then also send kbCtrlJ.
577 if (System
.getProperty("os.name").startsWith("Windows")) {
578 if (keypress
.equals(kbEnter
)) {
579 emulator
.keypress(kbCtrlJ
);
588 // Process is closed, honor "normal" TUI keystrokes
589 super.onKeypress(keypress
);
593 * Handle mouse press events.
595 * @param mouse mouse button press event
598 public void onMouseDown(final TMouseEvent mouse
) {
599 if (inWindowMove
|| inWindowResize
) {
600 // TWindow needs to deal with this.
601 super.onMouseDown(mouse
);
605 if (mouse
.isMouseWheelUp()) {
609 if (mouse
.isMouseWheelDown()) {
613 if (mouseOnEmulator(mouse
)) {
614 synchronized (emulator
) {
615 mouse
.setX(mouse
.getX() - 1);
616 mouse
.setY(mouse
.getY() - 1);
617 emulator
.mouse(mouse
);
623 // Emulator didn't consume it, pass it on
624 super.onMouseDown(mouse
);
628 * Handle mouse release events.
630 * @param mouse mouse button release event
633 public void onMouseUp(final TMouseEvent mouse
) {
634 if (inWindowMove
|| inWindowResize
) {
635 // TWindow needs to deal with this.
636 super.onMouseUp(mouse
);
640 if (mouseOnEmulator(mouse
)) {
641 synchronized (emulator
) {
642 mouse
.setX(mouse
.getX() - 1);
643 mouse
.setY(mouse
.getY() - 1);
644 emulator
.mouse(mouse
);
650 // Emulator didn't consume it, pass it on
651 super.onMouseUp(mouse
);
655 * Handle mouse motion events.
657 * @param mouse mouse motion event
660 public void onMouseMotion(final TMouseEvent mouse
) {
661 if (inWindowMove
|| inWindowResize
) {
662 // TWindow needs to deal with this.
663 super.onMouseMotion(mouse
);
667 if (mouseOnEmulator(mouse
)) {
668 synchronized (emulator
) {
669 mouse
.setX(mouse
.getX() - 1);
670 mouse
.setY(mouse
.getY() - 1);
671 emulator
.mouse(mouse
);
677 // Emulator didn't consume it, pass it on
678 super.onMouseMotion(mouse
);