2 * Jexer - Java Text User Interface
4 * The MIT License (MIT)
6 * Copyright (C) 2016 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
.InputStream
;
32 import java
.io
.IOException
;
33 import java
.io
.OutputStream
;
34 import java
.io
.UnsupportedEncodingException
;
35 import java
.lang
.reflect
.Field
;
36 import java
.util
.LinkedList
;
37 import java
.util
.List
;
40 import jexer
.bits
.Cell
;
41 import jexer
.bits
.CellAttributes
;
42 import jexer
.event
.TKeypressEvent
;
43 import jexer
.event
.TMouseEvent
;
44 import jexer
.event
.TResizeEvent
;
45 import jexer
.tterminal
.DisplayLine
;
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 TWindow
{
57 private ECMA48 emulator
;
60 * The Process created by the shell spawning constructor.
62 private Process shell
;
67 private TVScroller vScroller
;
70 * Claim the keystrokes the emulator will need.
72 private void addShortcutKeys() {
73 addShortcutKeypress(kbCtrlA
);
74 addShortcutKeypress(kbCtrlB
);
75 addShortcutKeypress(kbCtrlC
);
76 addShortcutKeypress(kbCtrlD
);
77 addShortcutKeypress(kbCtrlE
);
78 addShortcutKeypress(kbCtrlF
);
79 addShortcutKeypress(kbCtrlG
);
80 addShortcutKeypress(kbCtrlH
);
81 addShortcutKeypress(kbCtrlU
);
82 addShortcutKeypress(kbCtrlJ
);
83 addShortcutKeypress(kbCtrlK
);
84 addShortcutKeypress(kbCtrlL
);
85 addShortcutKeypress(kbCtrlM
);
86 addShortcutKeypress(kbCtrlN
);
87 addShortcutKeypress(kbCtrlO
);
88 addShortcutKeypress(kbCtrlP
);
89 addShortcutKeypress(kbCtrlQ
);
90 addShortcutKeypress(kbCtrlR
);
91 addShortcutKeypress(kbCtrlS
);
92 addShortcutKeypress(kbCtrlT
);
93 addShortcutKeypress(kbCtrlU
);
94 addShortcutKeypress(kbCtrlV
);
95 addShortcutKeypress(kbCtrlW
);
96 addShortcutKeypress(kbCtrlX
);
97 addShortcutKeypress(kbCtrlY
);
98 addShortcutKeypress(kbCtrlZ
);
99 addShortcutKeypress(kbF1
);
100 addShortcutKeypress(kbF2
);
101 addShortcutKeypress(kbF3
);
102 addShortcutKeypress(kbF4
);
103 addShortcutKeypress(kbF5
);
104 addShortcutKeypress(kbF6
);
105 addShortcutKeypress(kbF7
);
106 addShortcutKeypress(kbF8
);
107 addShortcutKeypress(kbF9
);
108 addShortcutKeypress(kbF10
);
109 addShortcutKeypress(kbF11
);
110 addShortcutKeypress(kbF12
);
111 addShortcutKeypress(kbAltA
);
112 addShortcutKeypress(kbAltB
);
113 addShortcutKeypress(kbAltC
);
114 addShortcutKeypress(kbAltD
);
115 addShortcutKeypress(kbAltE
);
116 addShortcutKeypress(kbAltF
);
117 addShortcutKeypress(kbAltG
);
118 addShortcutKeypress(kbAltH
);
119 addShortcutKeypress(kbAltU
);
120 addShortcutKeypress(kbAltJ
);
121 addShortcutKeypress(kbAltK
);
122 addShortcutKeypress(kbAltL
);
123 addShortcutKeypress(kbAltM
);
124 addShortcutKeypress(kbAltN
);
125 addShortcutKeypress(kbAltO
);
126 addShortcutKeypress(kbAltP
);
127 addShortcutKeypress(kbAltQ
);
128 addShortcutKeypress(kbAltR
);
129 addShortcutKeypress(kbAltS
);
130 addShortcutKeypress(kbAltT
);
131 addShortcutKeypress(kbAltU
);
132 addShortcutKeypress(kbAltV
);
133 addShortcutKeypress(kbAltW
);
134 addShortcutKeypress(kbAltX
);
135 addShortcutKeypress(kbAltY
);
136 addShortcutKeypress(kbAltZ
);
140 * Public constructor spawns a shell.
142 * @param application TApplication that manages this window
143 * @param x column relative to parent
144 * @param y row relative to parent
145 * @param flags mask of CENTERED, MODAL, or RESIZABLE
147 public TTerminalWindow(final TApplication application
, final int x
,
148 final int y
, final int flags
) {
150 super(application
, "Terminal", x
, y
, 80 + 2, 24 + 2, flags
);
153 ECMA48
.DeviceType deviceType
= ECMA48
.DeviceType
.XTERM
;
156 String
[] cmdShellWindows
= {
160 // You cannot run a login shell in a bare Process interactively,
161 // due to libc's behavior of buffering when stdin/stdout aren't a
162 // tty. Use 'script' instead to run a shell in a pty. And
163 // because BSD and GNU differ on the '-f' vs '-F' flags, we need
164 // two different commands. Lovely.
165 String
[] cmdShellGNU
= {
166 "script", "-fqe", "/dev/null"
168 String
[] cmdShellBSD
= {
169 "script", "-q", "-F", "/dev/null"
171 // Spawn a shell and pass its I/O to the other constructor.
174 if (System
.getProperty("os.name").startsWith("Windows")) {
175 pb
= new ProcessBuilder(cmdShellWindows
);
176 } else if (System
.getProperty("os.name").startsWith("Mac")) {
177 pb
= new ProcessBuilder(cmdShellBSD
);
178 } else if (System
.getProperty("os.name").startsWith("Linux")) {
179 pb
= new ProcessBuilder(cmdShellGNU
);
181 // When all else fails, assume GNU.
182 pb
= new ProcessBuilder(cmdShellGNU
);
184 Map
<String
, String
> env
= pb
.environment();
185 env
.put("TERM", ECMA48
.deviceTypeTerm(deviceType
));
186 env
.put("LANG", ECMA48
.deviceTypeLang(deviceType
, "en"));
187 env
.put("COLUMNS", "80");
188 env
.put("LINES", "24");
189 pb
.redirectErrorStream(true);
191 emulator
= new ECMA48(deviceType
, shell
.getInputStream(),
192 shell
.getOutputStream());
193 } catch (IOException e
) {
197 // Setup the scroll bars
198 onResize(new TResizeEvent(TResizeEvent
.Type
.WIDGET
, getWidth(),
201 // Claim the keystrokes the emulator will need.
205 newStatusBar("Terminal session executing...");
209 * Terminate the child of the 'script' process used on POSIX. This may
212 private void terminateShellChildProcess() {
214 if (shell
.getClass().getName().equals("java.lang.UNIXProcess")) {
215 /* get the PID on unix/linux systems */
217 Field field
= shell
.getClass().getDeclaredField("pid");
218 field
.setAccessible(true);
219 pid
= field
.getInt(shell
);
220 } catch (Throwable e
) {
221 // SQUASH, this didn't work. Just bail out quietly.
226 // shell.destroy() works successfully at killing this side of
227 // 'script'. But we need to make sure the other side (child
228 // process) is also killed.
229 String
[] cmdKillIt
= {
230 "pkill", "-P", Integer
.toString(pid
)
233 Runtime
.getRuntime().exec(cmdKillIt
);
234 } catch (Throwable e
) {
235 // SQUASH, this didn't work. Just bail out quietly.
242 * Public constructor.
244 * @param application TApplication that manages this window
245 * @param x column relative to parent
246 * @param y row relative to parent
247 * @param flags mask of CENTERED, MODAL, or RESIZABLE
248 * @param input an InputStream connected to the remote side. For type ==
249 * XTERM, input is converted to a Reader with UTF-8 encoding.
250 * @param output an OutputStream connected to the remote user. For type
251 * == XTERM, output is converted to a Writer with UTF-8 encoding.
252 * @throws UnsupportedEncodingException if an exception is thrown when
253 * creating the InputStreamReader
255 public TTerminalWindow(final TApplication application
, final int x
,
256 final int y
, final int flags
, final InputStream input
,
257 final OutputStream output
) throws UnsupportedEncodingException
{
259 super(application
, "Terminal", x
, y
, 80 + 2, 24 + 2, flags
);
261 emulator
= new ECMA48(ECMA48
.DeviceType
.XTERM
, input
, output
);
263 // Setup the scroll bars
264 onResize(new TResizeEvent(TResizeEvent
.Type
.WIDGET
, getWidth(),
267 // Claim the keystrokes the emulator will need.
271 newStatusBar("Terminal session executing...");
275 * Draw the display buffer.
279 // Synchronize against the emulator so we don't stomp on its reader
281 synchronized (emulator
) {
283 // Update the scroll bars
286 // Draw the box using my superclass
289 List
<DisplayLine
> scrollback
= emulator
.getScrollbackBuffer();
290 List
<DisplayLine
> display
= emulator
.getDisplayBuffer();
292 // Put together the visible rows
293 int visibleHeight
= getHeight() - 2;
294 int visibleBottom
= scrollback
.size() + display
.size()
295 + vScroller
.getValue();
296 assert (visibleBottom
>= 0);
298 List
<DisplayLine
> preceedingBlankLines
= new LinkedList
<DisplayLine
>();
299 int visibleTop
= visibleBottom
- visibleHeight
;
300 if (visibleTop
< 0) {
301 for (int i
= visibleTop
; i
< 0; i
++) {
302 preceedingBlankLines
.add(emulator
.getBlankDisplayLine());
306 assert (visibleTop
>= 0);
308 List
<DisplayLine
> displayLines
= new LinkedList
<DisplayLine
>();
309 displayLines
.addAll(scrollback
);
310 displayLines
.addAll(display
);
312 List
<DisplayLine
> visibleLines
= new LinkedList
<DisplayLine
>();
313 visibleLines
.addAll(preceedingBlankLines
);
314 visibleLines
.addAll(displayLines
.subList(visibleTop
,
317 visibleHeight
-= visibleLines
.size();
318 assert (visibleHeight
>= 0);
320 // Now draw the emulator screen
322 for (DisplayLine line
: visibleLines
) {
323 int widthMax
= emulator
.getWidth();
324 if (line
.isDoubleWidth()) {
327 if (widthMax
> getWidth() - 2) {
328 widthMax
= getWidth() - 2;
330 for (int i
= 0; i
< widthMax
; i
++) {
331 Cell ch
= line
.charAt(i
);
332 Cell newCell
= new Cell();
334 boolean reverse
= line
.isReverseColor() ^ ch
.isReverse();
335 newCell
.setReverse(false);
337 newCell
.setBackColor(ch
.getForeColor());
338 newCell
.setForeColor(ch
.getBackColor());
340 if (line
.isDoubleWidth()) {
341 getScreen().putCharXY((i
* 2) + 1, row
, newCell
);
342 getScreen().putCharXY((i
* 2) + 2, row
, ' ', newCell
);
344 getScreen().putCharXY(i
+ 1, row
, newCell
);
348 if (row
== getHeight() - 1) {
349 // Don't overwrite the box edge
353 CellAttributes background
= new CellAttributes();
354 // Fill in the blank lines on bottom
355 for (int i
= 0; i
< visibleHeight
; i
++) {
356 getScreen().hLineXY(1, i
+ row
, getWidth() - 2, ' ',
360 } // synchronized (emulator)
365 * Handle window close.
368 public void onClose() {
371 terminateShellChildProcess();
378 * Copy out variables from the emulator that TTerminal has to expose on
381 private void readEmulatorState() {
382 // Synchronize against the emulator so we don't stomp on its reader
384 synchronized (emulator
) {
386 setCursorX(emulator
.getCursorX() + 1);
387 setCursorY(emulator
.getCursorY() + 1
388 + (getHeight() - 2 - emulator
.getHeight()));
389 if (vScroller
!= null) {
390 setCursorY(getCursorY() - vScroller
.getValue());
392 setCursorVisible(emulator
.isCursorVisible());
393 if (getCursorX() > getWidth() - 2) {
394 setCursorVisible(false);
396 if ((getCursorY() > getHeight() - 2) || (getCursorY() < 0)) {
397 setCursorVisible(false);
399 if (emulator
.getScreenTitle().length() > 0) {
400 // Only update the title if the shell is still alive
402 setTitle(emulator
.getScreenTitle());
406 // Check to see if the shell has died.
407 if (!emulator
.isReading() && (shell
!= null)) {
409 int rc
= shell
.exitValue();
410 // The emulator exited on its own, all is fine
411 setTitle(String
.format("%s [Completed - %d]",
415 clearShortcutKeypresses();
416 statusBar
.setText("Terminal session completed, exit " +
418 } catch (IllegalThreadStateException e
) {
419 // The emulator thread has exited, but the shell Process
420 // hasn't figured that out yet. Do nothing, we will see
421 // this in a future tick.
423 } else if (emulator
.isReading() && (shell
!= null)) {
424 // The shell might be dead, let's check
426 int rc
= shell
.exitValue();
427 // If we got here, the shell died.
428 setTitle(String
.format("%s [Completed - %d]",
432 clearShortcutKeypresses();
433 statusBar
.setText("Terminal session completed, exit " +
435 } catch (IllegalThreadStateException e
) {
436 // The shell is still running, do nothing.
440 } // synchronized (emulator)
444 * Handle window/screen resize events.
446 * @param resize resize event
449 public void onResize(final TResizeEvent resize
) {
451 // Synchronize against the emulator so we don't stomp on its reader
453 synchronized (emulator
) {
455 if (resize
.getType() == TResizeEvent
.Type
.WIDGET
) {
456 // Resize the scroll bars
459 // Get out of scrollback
460 vScroller
.setValue(0);
464 } // synchronized (emulator)
468 * Resize scrollbars for a new width/height.
470 private void reflow() {
472 // Synchronize against the emulator so we don't stomp on its reader
474 synchronized (emulator
) {
476 // Pull cursor information
479 // Vertical scrollbar
480 if (vScroller
== null) {
481 vScroller
= new TVScroller(this, getWidth() - 2, 0,
483 vScroller
.setBottomValue(0);
484 vScroller
.setValue(0);
486 vScroller
.setX(getWidth() - 2);
487 vScroller
.setHeight(getHeight() - 2);
489 vScroller
.setTopValue(getHeight() - 2
490 - (emulator
.getScrollbackBuffer().size()
491 + emulator
.getDisplayBuffer().size()));
492 vScroller
.setBigChange(getHeight() - 2);
494 } // synchronized (emulator)
498 * Check if a mouse press/release/motion event coordinate is over the
501 * @param mouse a mouse-based event
502 * @return whether or not the mouse is on the emulator
504 private final boolean mouseOnEmulator(final TMouseEvent mouse
) {
506 synchronized (emulator
) {
507 if (!emulator
.isReading()) {
512 if ((mouse
.getAbsoluteX() >= getAbsoluteX() + 1)
513 && (mouse
.getAbsoluteX() < getAbsoluteX() + getWidth() - 1)
514 && (mouse
.getAbsoluteY() >= getAbsoluteY() + 1)
515 && (mouse
.getAbsoluteY() < getAbsoluteY() + getHeight() - 1)
525 * @param keypress keystroke event
528 public void onKeypress(final TKeypressEvent keypress
) {
530 // Scrollback up/down
531 if (keypress
.equals(kbShiftPgUp
)
532 || keypress
.equals(kbCtrlPgUp
)
533 || keypress
.equals(kbAltPgUp
)
535 vScroller
.bigDecrement();
538 if (keypress
.equals(kbShiftPgDn
)
539 || keypress
.equals(kbCtrlPgDn
)
540 || keypress
.equals(kbAltPgDn
)
542 vScroller
.bigIncrement();
546 // Synchronize against the emulator so we don't stomp on its reader
548 synchronized (emulator
) {
549 if (emulator
.isReading()) {
550 // Get out of scrollback
551 vScroller
.setValue(0);
552 emulator
.keypress(keypress
.getKey());
554 // UGLY HACK TIME! cmd.exe needs CRLF, not just CR, so if
555 // this is kBEnter then also send kbCtrlJ.
556 if (System
.getProperty("os.name").startsWith("Windows")) {
557 if (keypress
.equals(kbEnter
)) {
558 emulator
.keypress(kbCtrlJ
);
567 // Process is closed, honor "normal" TUI keystrokes
568 super.onKeypress(keypress
);
572 * Handle mouse press events.
574 * @param mouse mouse button press event
577 public void onMouseDown(final TMouseEvent mouse
) {
578 if (inWindowMove
|| inWindowResize
) {
579 // TWindow needs to deal with this.
580 super.onMouseDown(mouse
);
584 if (mouse
.isMouseWheelUp()) {
585 vScroller
.decrement();
588 if (mouse
.isMouseWheelDown()) {
589 vScroller
.increment();
592 if (mouseOnEmulator(mouse
)) {
593 synchronized (emulator
) {
594 mouse
.setX(mouse
.getX() - 1);
595 mouse
.setY(mouse
.getY() - 1);
596 emulator
.mouse(mouse
);
602 // Emulator didn't consume it, pass it on
603 super.onMouseDown(mouse
);
607 * Handle mouse release events.
609 * @param mouse mouse button release event
612 public void onMouseUp(final TMouseEvent mouse
) {
613 if (inWindowMove
|| inWindowResize
) {
614 // TWindow needs to deal with this.
615 super.onMouseUp(mouse
);
619 if (mouseOnEmulator(mouse
)) {
620 synchronized (emulator
) {
621 mouse
.setX(mouse
.getX() - 1);
622 mouse
.setY(mouse
.getY() - 1);
623 emulator
.mouse(mouse
);
629 // Emulator didn't consume it, pass it on
630 super.onMouseUp(mouse
);
634 * Handle mouse motion events.
636 * @param mouse mouse motion event
639 public void onMouseMotion(final TMouseEvent mouse
) {
640 if (inWindowMove
|| inWindowResize
) {
641 // TWindow needs to deal with this.
642 super.onMouseMotion(mouse
);
646 if (mouseOnEmulator(mouse
)) {
647 synchronized (emulator
) {
648 mouse
.setX(mouse
.getX() - 1);
649 mouse
.setY(mouse
.getY() - 1);
650 emulator
.mouse(mouse
);
656 // Emulator didn't consume it, pass it on
657 super.onMouseMotion(mouse
);