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
.util
.LinkedList
;
36 import java
.util
.List
;
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
.ECMA48
;
46 import static jexer
.TKeypress
.*;
49 * TTerminalWindow exposes a ECMA-48 / ANSI X3.64 style terminal in a window.
51 public class TTerminalWindow
extends TWindow
{
56 private ECMA48 emulator
;
59 * The Process created by the shell spawning constructor.
61 private Process shell
;
66 private TVScroller vScroller
;
69 * Public constructor spawns a shell.
71 * @param application TApplication that manages this window
72 * @param x column relative to parent
73 * @param y row relative to parent
74 * @param flags mask of CENTERED, MODAL, or RESIZABLE
76 public TTerminalWindow(final TApplication application
, final int x
,
77 final int y
, final int flags
) {
79 super(application
, "Terminal", x
, y
, 80 + 2, 24 + 2, flags
);
82 ECMA48
.DeviceType deviceType
= ECMA48
.DeviceType
.XTERM
;
85 String
[] cmdShellWindows
= {
89 // You cannot run a login shell in a bare Process interactively,
90 // due to libc's behavior of buffering when stdin/stdout aren't a
91 // tty. Use 'script' instead to run a shell in a pty.
92 String
[] cmdShell
= {
93 "script", "-fqe", "/dev/null"
95 // Spawn a shell and pass its I/O to the other constructor.
98 if (System
.getProperty("os.name").startsWith("Windows")) {
99 pb
= new ProcessBuilder(cmdShellWindows
);
101 pb
= new ProcessBuilder(cmdShell
);
103 Map
<String
, String
> env
= pb
.environment();
104 env
.put("TERM", ECMA48
.deviceTypeTerm(deviceType
));
105 env
.put("LANG", ECMA48
.deviceTypeLang(deviceType
, "en"));
106 env
.put("COLUMNS", "80");
107 env
.put("LINES", "24");
108 pb
.redirectErrorStream(true);
110 emulator
= new ECMA48(deviceType
, shell
.getInputStream(),
111 shell
.getOutputStream());
112 } catch (IOException e
) {
116 // Setup the scroll bars
117 onResize(new TResizeEvent(TResizeEvent
.Type
.WIDGET
, getWidth(),
122 * Public constructor.
124 * @param application TApplication that manages this window
125 * @param x column relative to parent
126 * @param y row relative to parent
127 * @param flags mask of CENTERED, MODAL, or RESIZABLE
128 * @param input an InputStream connected to the remote side. For type ==
129 * XTERM, input is converted to a Reader with UTF-8 encoding.
130 * @param output an OutputStream connected to the remote user. For type
131 * == XTERM, output is converted to a Writer with UTF-8 encoding.
132 * @throws UnsupportedEncodingException if an exception is thrown when
133 * creating the InputStreamReader
135 public TTerminalWindow(final TApplication application
, final int x
,
136 final int y
, final int flags
, final InputStream input
,
137 final OutputStream output
) throws UnsupportedEncodingException
{
139 super(application
, "Terminal", x
, y
, 80 + 2, 24 + 2, flags
);
141 emulator
= new ECMA48(ECMA48
.DeviceType
.XTERM
, input
, output
);
143 // Setup the scroll bars
144 onResize(new TResizeEvent(TResizeEvent
.Type
.WIDGET
, getWidth(),
150 * Draw the display buffer.
154 // Synchronize against the emulator so we don't stomp on its reader
156 synchronized (emulator
) {
158 // Update the scroll bars
161 // Draw the box using my superclass
164 List
<DisplayLine
> scrollback
= emulator
.getScrollbackBuffer();
165 List
<DisplayLine
> display
= emulator
.getDisplayBuffer();
167 // Put together the visible rows
168 int visibleHeight
= getHeight() - 2;
169 int visibleBottom
= scrollback
.size() + display
.size()
170 + vScroller
.getValue();
171 assert (visibleBottom
>= 0);
173 List
<DisplayLine
> preceedingBlankLines
= new LinkedList
<DisplayLine
>();
174 int visibleTop
= visibleBottom
- visibleHeight
;
175 if (visibleTop
< 0) {
176 for (int i
= visibleTop
; i
< 0; i
++) {
177 preceedingBlankLines
.add(emulator
.getBlankDisplayLine());
181 assert (visibleTop
>= 0);
183 List
<DisplayLine
> displayLines
= new LinkedList
<DisplayLine
>();
184 displayLines
.addAll(scrollback
);
185 displayLines
.addAll(display
);
187 List
<DisplayLine
> visibleLines
= new LinkedList
<DisplayLine
>();
188 visibleLines
.addAll(preceedingBlankLines
);
189 visibleLines
.addAll(displayLines
.subList(visibleTop
,
192 visibleHeight
-= visibleLines
.size();
193 assert (visibleHeight
>= 0);
195 // Now draw the emulator screen
197 for (DisplayLine line
: visibleLines
) {
198 int widthMax
= emulator
.getWidth();
199 if (line
.isDoubleWidth()) {
202 if (widthMax
> getWidth() - 2) {
203 widthMax
= getWidth() - 2;
205 for (int i
= 0; i
< widthMax
; i
++) {
206 Cell ch
= line
.charAt(i
);
207 Cell newCell
= new Cell();
209 boolean reverse
= line
.isReverseColor() ^ ch
.isReverse();
210 newCell
.setReverse(false);
212 newCell
.setBackColor(ch
.getForeColor());
213 newCell
.setForeColor(ch
.getBackColor());
215 if (line
.isDoubleWidth()) {
216 getScreen().putCharXY((i
* 2) + 1, row
, newCell
);
217 getScreen().putCharXY((i
* 2) + 2, row
, ' ', newCell
);
219 getScreen().putCharXY(i
+ 1, row
, newCell
);
223 if (row
== getHeight() - 1) {
224 // Don't overwrite the box edge
228 CellAttributes background
= new CellAttributes();
229 // Fill in the blank lines on bottom
230 for (int i
= 0; i
< visibleHeight
; i
++) {
231 getScreen().hLineXY(1, i
+ row
, getWidth() - 2, ' ',
235 } // synchronized (emulator)
240 * Handle window close.
242 @Override public void onClose() {
252 * Copy out variables from the emulator that TTerminal has to expose on
255 private void readEmulatorState() {
256 // Synchronize against the emulator so we don't stomp on its reader
258 synchronized (emulator
) {
260 setCursorX(emulator
.getCursorX() + 1);
261 setCursorY(emulator
.getCursorY() + 1
262 + (getHeight() - 2 - emulator
.getHeight()));
263 if (vScroller
!= null) {
264 setCursorY(getCursorY() - vScroller
.getValue());
266 setCursorVisible(emulator
.isCursorVisible());
267 if (getCursorX() > getWidth() - 2) {
268 setCursorVisible(false);
270 if ((getCursorY() > getHeight() - 2) || (getCursorY() < 0)) {
271 setCursorVisible(false);
273 if (emulator
.getScreenTitle().length() > 0) {
274 // Only update the title if the shell is still alive
276 setTitle(emulator
.getScreenTitle());
280 // Check to see if the shell has died.
281 if (!emulator
.isReading() && (shell
!= null)) {
283 int rc
= shell
.exitValue();
284 // The emulator exited on its own, all is fine
285 setTitle(String
.format("%s [Completed - %d]",
289 } catch (IllegalThreadStateException e
) {
290 // The emulator thread has exited, but the shell Process
291 // hasn't figured that out yet. Do nothing, we will see
292 // this in a future tick.
294 } else if (emulator
.isReading() && (shell
!= null)) {
295 // The shell might be dead, let's check
297 int rc
= shell
.exitValue();
298 // If we got here, the shell died.
299 setTitle(String
.format("%s [Completed - %d]",
303 } catch (IllegalThreadStateException e
) {
304 // The shell is still running, do nothing.
308 } // synchronized (emulator)
312 * Handle window/screen resize events.
314 * @param resize resize event
317 public void onResize(final TResizeEvent resize
) {
319 // Synchronize against the emulator so we don't stomp on its reader
321 synchronized (emulator
) {
323 if (resize
.getType() == TResizeEvent
.Type
.WIDGET
) {
324 // Resize the scroll bars
327 // Get out of scrollback
328 vScroller
.setValue(0);
332 } // synchronized (emulator)
336 * Resize scrollbars for a new width/height.
338 private void reflow() {
340 // Synchronize against the emulator so we don't stomp on its reader
342 synchronized (emulator
) {
344 // Pull cursor information
347 // Vertical scrollbar
348 if (vScroller
== null) {
349 vScroller
= new TVScroller(this, getWidth() - 2, 0,
351 vScroller
.setBottomValue(0);
352 vScroller
.setValue(0);
354 vScroller
.setX(getWidth() - 2);
355 vScroller
.setHeight(getHeight() - 2);
357 vScroller
.setTopValue(getHeight() - 2
358 - (emulator
.getScrollbackBuffer().size()
359 + emulator
.getDisplayBuffer().size()));
360 vScroller
.setBigChange(getHeight() - 2);
362 } // synchronized (emulator)
366 * Check if a mouse press/release/motion event coordinate is over the
369 * @param mouse a mouse-based event
370 * @return whether or not the mouse is on the emulator
372 private final boolean mouseOnEmulator(final TMouseEvent mouse
) {
374 synchronized (emulator
) {
375 if (!emulator
.isReading()) {
380 if ((mouse
.getAbsoluteX() >= getAbsoluteX() + 1)
381 && (mouse
.getAbsoluteX() < getAbsoluteX() + getWidth() - 1)
382 && (mouse
.getAbsoluteY() >= getAbsoluteY() + 1)
383 && (mouse
.getAbsoluteY() < getAbsoluteY() + getHeight() - 1)
393 * @param keypress keystroke event
396 public void onKeypress(final TKeypressEvent keypress
) {
398 // Scrollback up/down
399 if (keypress
.equals(kbShiftPgUp
)
400 || keypress
.equals(kbCtrlPgUp
)
401 || keypress
.equals(kbAltPgUp
)
403 vScroller
.bigDecrement();
406 if (keypress
.equals(kbShiftPgDn
)
407 || keypress
.equals(kbCtrlPgDn
)
408 || keypress
.equals(kbAltPgDn
)
410 vScroller
.bigIncrement();
414 // Synchronize against the emulator so we don't stomp on its reader
416 synchronized (emulator
) {
417 if (emulator
.isReading()) {
418 // Get out of scrollback
419 vScroller
.setValue(0);
420 emulator
.keypress(keypress
.getKey());
422 // UGLY HACK TIME! cmd.exe needs CRLF, not just CR, so if
423 // this is kBEnter then also send kbCtrlJ.
424 if (System
.getProperty("os.name").startsWith("Windows")) {
425 if (keypress
.equals(kbEnter
)) {
426 emulator
.keypress(kbCtrlJ
);
435 // Process is closed, honor "normal" TUI keystrokes
436 super.onKeypress(keypress
);
440 * Handle mouse press events.
442 * @param mouse mouse button press event
445 public void onMouseDown(final TMouseEvent mouse
) {
446 if (inWindowMove
|| inWindowResize
) {
447 // TWindow needs to deal with this.
448 super.onMouseDown(mouse
);
452 if (mouse
.isMouseWheelUp()) {
453 vScroller
.decrement();
456 if (mouse
.isMouseWheelDown()) {
457 vScroller
.increment();
460 if (mouseOnEmulator(mouse
)) {
461 synchronized (emulator
) {
462 mouse
.setX(mouse
.getX() - 1);
463 mouse
.setY(mouse
.getY() - 1);
464 emulator
.mouse(mouse
);
470 // Emulator didn't consume it, pass it on
471 super.onMouseDown(mouse
);
475 * Handle mouse release events.
477 * @param mouse mouse button release event
480 public void onMouseUp(final TMouseEvent mouse
) {
481 if (inWindowMove
|| inWindowResize
) {
482 // TWindow needs to deal with this.
483 super.onMouseUp(mouse
);
487 if (mouseOnEmulator(mouse
)) {
488 synchronized (emulator
) {
489 mouse
.setX(mouse
.getX() - 1);
490 mouse
.setY(mouse
.getY() - 1);
491 emulator
.mouse(mouse
);
497 // Emulator didn't consume it, pass it on
498 super.onMouseUp(mouse
);
502 * Handle mouse motion events.
504 * @param mouse mouse motion event
507 public void onMouseMotion(final TMouseEvent mouse
) {
508 if (inWindowMove
|| inWindowResize
) {
509 // TWindow needs to deal with this.
510 super.onMouseMotion(mouse
);
514 if (mouseOnEmulator(mouse
)) {
515 synchronized (emulator
) {
516 mouse
.setX(mouse
.getX() - 1);
517 mouse
.setY(mouse
.getY() - 1);
518 emulator
.mouse(mouse
);
524 // Emulator didn't consume it, pass it on
525 super.onMouseMotion(mouse
);