2 * Jexer - Java Text User Interface
4 * License: LGPLv3 or later
6 * This module is licensed under the GNU Lesser General Public License
7 * Version 3. Please see the file "COPYING" in this directory for more
8 * information about the GNU Lesser General Public License Version 3.
10 * Copyright (C) 2015 Kevin Lamonte
12 * This program is free software; you can redistribute it and/or
13 * modify it under the terms of the GNU Lesser General Public License
14 * as published by the Free Software Foundation; either version 3 of
15 * the License, or (at your option) any later version.
17 * This program is distributed in the hope that it will be useful, but
18 * WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20 * General Public License for more details.
22 * You should have received a copy of the GNU Lesser General Public
23 * License along with this program; if not, see
24 * http://www.gnu.org/licenses/, or write to the Free Software
25 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
28 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
33 import java
.io
.InputStream
;
34 import java
.io
.IOException
;
35 import java
.io
.OutputStream
;
36 import java
.io
.UnsupportedEncodingException
;
37 import java
.util
.LinkedList
;
38 import java
.util
.List
;
41 import jexer
.bits
.Cell
;
42 import jexer
.bits
.CellAttributes
;
43 import jexer
.event
.TKeypressEvent
;
44 import jexer
.event
.TMouseEvent
;
45 import jexer
.event
.TResizeEvent
;
46 import jexer
.tterminal
.DisplayLine
;
47 import jexer
.tterminal
.ECMA48
;
48 import static jexer
.TKeypress
.*;
51 * TTerminalWindow exposes a ECMA-48 / ANSI X3.64 style terminal in a window.
53 public class TTerminalWindow
extends TWindow
{
58 private ECMA48 emulator
;
61 * The Process created by the shell spawning constructor.
63 private Process shell
;
68 private TVScroller vScroller
;
71 * Public constructor spawns a shell.
73 * @param application TApplication that manages this window
74 * @param x column relative to parent
75 * @param y row relative to parent
76 * @param flags mask of CENTERED, MODAL, or RESIZABLE
78 public TTerminalWindow(final TApplication application
, final int x
,
79 final int y
, final int flags
) {
81 super(application
, "Terminal", x
, y
, 80 + 2, 24 + 2, flags
);
84 ECMA48
.DeviceType deviceType
= ECMA48
.DeviceType
.XTERM
;
87 String
[] cmdShellWindows
= {
91 // You cannot run a login shell in a bare Process interactively,
92 // due to libc's behavior of buffering when stdin/stdout aren't a
93 // tty. Use 'script' instead to run a shell in a pty.
94 String
[] cmdShell
= {
95 "script", "-fqe", "/dev/null"
97 // Spawn a shell and pass its I/O to the other constructor.
100 if (System
.getProperty("os.name").startsWith("Windows")) {
101 pb
= new ProcessBuilder(cmdShellWindows
);
103 pb
= new ProcessBuilder(cmdShell
);
105 Map
<String
, String
> env
= pb
.environment();
106 env
.put("TERM", ECMA48
.deviceTypeTerm(deviceType
));
107 env
.put("LANG", ECMA48
.deviceTypeLang(deviceType
, "en"));
108 env
.put("COLUMNS", "80");
109 env
.put("LINES", "24");
110 pb
.redirectErrorStream(true);
112 emulator
= new ECMA48(deviceType
, shell
.getInputStream(),
113 shell
.getOutputStream());
114 } catch (IOException e
) {
118 // Setup the scroll bars
119 onResize(new TResizeEvent(TResizeEvent
.Type
.WIDGET
, getWidth(),
124 * Public constructor.
126 * @param application TApplication that manages this window
127 * @param x column relative to parent
128 * @param y row relative to parent
129 * @param flags mask of CENTERED, MODAL, or RESIZABLE
130 * @param input an InputStream connected to the remote side. For type ==
131 * XTERM, input is converted to a Reader with UTF-8 encoding.
132 * @param output an OutputStream connected to the remote user. For type
133 * == XTERM, output is converted to a Writer with UTF-8 encoding.
134 * @throws UnsupportedEncodingException if an exception is thrown when
135 * creating the InputStreamReader
137 public TTerminalWindow(final TApplication application
, final int x
,
138 final int y
, final int flags
, final InputStream input
,
139 final OutputStream output
) throws UnsupportedEncodingException
{
141 super(application
, "Terminal", x
, y
, 80 + 2, 24 + 2, flags
);
143 emulator
= new ECMA48(ECMA48
.DeviceType
.XTERM
, input
, output
);
145 // Setup the scroll bars
146 onResize(new TResizeEvent(TResizeEvent
.Type
.WIDGET
, getWidth(),
152 * Draw the display buffer.
156 // Synchronize against the emulator so we don't stomp on its reader
158 synchronized (emulator
) {
160 // Update the scroll bars
163 // Draw the box using my superclass
166 List
<DisplayLine
> scrollback
= emulator
.getScrollbackBuffer();
167 List
<DisplayLine
> display
= emulator
.getDisplayBuffer();
169 // Put together the visible rows
170 int visibleHeight
= getHeight() - 2;
171 int visibleBottom
= scrollback
.size() + display
.size()
172 + vScroller
.getValue();
173 assert (visibleBottom
>= 0);
175 List
<DisplayLine
> preceedingBlankLines
= new LinkedList
<DisplayLine
>();
176 int visibleTop
= visibleBottom
- visibleHeight
;
177 if (visibleTop
< 0) {
178 for (int i
= visibleTop
; i
< 0; i
++) {
179 preceedingBlankLines
.add(emulator
.getBlankDisplayLine());
183 assert (visibleTop
>= 0);
185 List
<DisplayLine
> displayLines
= new LinkedList
<DisplayLine
>();
186 displayLines
.addAll(scrollback
);
187 displayLines
.addAll(display
);
189 List
<DisplayLine
> visibleLines
= new LinkedList
<DisplayLine
>();
190 visibleLines
.addAll(preceedingBlankLines
);
191 visibleLines
.addAll(displayLines
.subList(visibleTop
,
194 visibleHeight
-= visibleLines
.size();
195 assert (visibleHeight
>= 0);
197 // Now draw the emulator screen
199 for (DisplayLine line
: visibleLines
) {
200 int widthMax
= emulator
.getWidth();
201 if (line
.isDoubleWidth()) {
204 if (widthMax
> getWidth() - 2) {
205 widthMax
= getWidth() - 2;
207 for (int i
= 0; i
< widthMax
; i
++) {
208 Cell ch
= line
.charAt(i
);
209 Cell newCell
= new Cell();
211 boolean reverse
= line
.isReverseColor() ^ ch
.isReverse();
212 newCell
.setReverse(false);
214 newCell
.setBackColor(ch
.getForeColor());
215 newCell
.setForeColor(ch
.getBackColor());
217 if (line
.isDoubleWidth()) {
218 getScreen().putCharXY((i
* 2) + 1, row
, newCell
);
219 getScreen().putCharXY((i
* 2) + 2, row
, ' ', newCell
);
221 getScreen().putCharXY(i
+ 1, row
, newCell
);
225 if (row
== getHeight() - 1) {
226 // Don't overwrite the box edge
230 CellAttributes background
= new CellAttributes();
231 // Fill in the blank lines on bottom
232 for (int i
= 0; i
< visibleHeight
; i
++) {
233 getScreen().hLineXY(1, i
+ row
, getWidth() - 2, ' ',
237 } // synchronized (emulator)
242 * Handle window close.
244 @Override public void onClose() {
254 * Copy out variables from the emulator that TTerminal has to expose on
257 private void readEmulatorState() {
258 // Synchronize against the emulator so we don't stomp on its reader
260 synchronized (emulator
) {
262 setCursorX(emulator
.getCursorX() + 1);
263 setCursorY(emulator
.getCursorY() + 1
264 + (getHeight() - 2 - emulator
.getHeight()));
265 if (vScroller
!= null) {
266 setCursorY(getCursorY() - vScroller
.getValue());
268 setCursorVisible(emulator
.isCursorVisible());
269 if (getCursorX() > getWidth() - 2) {
270 setCursorVisible(false);
272 if ((getCursorY() > getHeight() - 2) || (getCursorY() < 0)) {
273 setCursorVisible(false);
275 if (emulator
.getScreenTitle().length() > 0) {
276 // Only update the title if the shell is still alive
278 setTitle(emulator
.getScreenTitle());
282 // Check to see if the shell has died.
283 if (!emulator
.isReading() && (shell
!= null)) {
284 // The emulator exited on its own, all is fine
285 setTitle(String
.format("%s [Completed - %d]",
286 getTitle(), shell
.exitValue()));
289 } else if (emulator
.isReading() && (shell
!= null)) {
290 // The shell might be dead, let's check
292 int rc
= shell
.exitValue();
293 // If we got here, the shell died.
294 setTitle(String
.format("%s [Completed - %d]",
298 } catch (IllegalThreadStateException e
) {
299 // The shell is still running, do nothing.
303 } // synchronized (emulator)
307 * Handle window/screen resize events.
309 * @param resize resize event
312 public void onResize(final TResizeEvent resize
) {
314 // Synchronize against the emulator so we don't stomp on its reader
316 synchronized (emulator
) {
318 if (resize
.getType() == TResizeEvent
.Type
.WIDGET
) {
319 // Resize the scroll bars
322 // Get out of scrollback
323 vScroller
.setValue(0);
327 } // synchronized (emulator)
331 * Resize scrollbars for a new width/height.
333 private void reflow() {
335 // Synchronize against the emulator so we don't stomp on its reader
337 synchronized (emulator
) {
339 // Pull cursor information
342 // Vertical scrollbar
343 if (vScroller
== null) {
344 vScroller
= new TVScroller(this, getWidth() - 2, 0,
346 vScroller
.setBottomValue(0);
347 vScroller
.setValue(0);
349 vScroller
.setX(getWidth() - 2);
350 vScroller
.setHeight(getHeight() - 2);
352 vScroller
.setTopValue(getHeight() - 2
353 - (emulator
.getScrollbackBuffer().size()
354 + emulator
.getDisplayBuffer().size()));
355 vScroller
.setBigChange(getHeight() - 2);
357 } // synchronized (emulator)
361 * Check if a mouse press/release/motion event coordinate is over the
364 * @param mouse a mouse-based event
365 * @return whether or not the mouse is on the emulator
367 private final boolean mouseOnEmulator(final TMouseEvent mouse
) {
369 synchronized (emulator
) {
370 if (!emulator
.isReading()) {
375 if ((mouse
.getAbsoluteX() >= getAbsoluteX() + 1)
376 && (mouse
.getAbsoluteX() < getAbsoluteX() + getWidth() - 1)
377 && (mouse
.getAbsoluteY() >= getAbsoluteY() + 1)
378 && (mouse
.getAbsoluteY() < getAbsoluteY() + getHeight() - 1)
388 * @param keypress keystroke event
391 public void onKeypress(final TKeypressEvent keypress
) {
393 // Scrollback up/down
394 if (keypress
.equals(kbShiftPgUp
)
395 || keypress
.equals(kbCtrlPgUp
)
396 || keypress
.equals(kbAltPgUp
)
398 vScroller
.bigDecrement();
401 if (keypress
.equals(kbShiftPgDn
)
402 || keypress
.equals(kbCtrlPgDn
)
403 || keypress
.equals(kbAltPgDn
)
405 vScroller
.bigIncrement();
409 // Synchronize against the emulator so we don't stomp on its reader
411 synchronized (emulator
) {
412 if (emulator
.isReading()) {
413 // Get out of scrollback
414 vScroller
.setValue(0);
415 emulator
.keypress(keypress
.getKey());
417 // UGLY HACK TIME! cmd.exe needs CRLF, not just CR, so if
418 // this is kBEnter then also send kbCtrlJ.
419 if (System
.getProperty("os.name").startsWith("Windows")) {
420 if (keypress
.equals(kbEnter
)) {
421 emulator
.keypress(kbCtrlJ
);
430 // Process is closed, honor "normal" TUI keystrokes
431 super.onKeypress(keypress
);
435 * Handle mouse press events.
437 * @param mouse mouse button press event
440 public void onMouseDown(final TMouseEvent mouse
) {
441 if (inWindowMove
|| inWindowResize
) {
442 // TWindow needs to deal with this.
443 super.onMouseDown(mouse
);
447 if (mouse
.isMouseWheelUp()) {
448 vScroller
.decrement();
451 if (mouse
.isMouseWheelDown()) {
452 vScroller
.increment();
455 if (mouseOnEmulator(mouse
)) {
456 synchronized (emulator
) {
457 mouse
.setX(mouse
.getX() - 1);
458 mouse
.setY(mouse
.getY() - 1);
459 emulator
.mouse(mouse
);
465 // Emulator didn't consume it, pass it on
466 super.onMouseDown(mouse
);
470 * Handle mouse release events.
472 * @param mouse mouse button release event
475 public void onMouseUp(final TMouseEvent mouse
) {
476 if (inWindowMove
|| inWindowResize
) {
477 // TWindow needs to deal with this.
478 super.onMouseUp(mouse
);
482 if (mouseOnEmulator(mouse
)) {
483 synchronized (emulator
) {
484 mouse
.setX(mouse
.getX() - 1);
485 mouse
.setY(mouse
.getY() - 1);
486 emulator
.mouse(mouse
);
492 // Emulator didn't consume it, pass it on
493 super.onMouseUp(mouse
);
497 * Handle mouse motion events.
499 * @param mouse mouse motion event
502 public void onMouseMotion(final TMouseEvent mouse
) {
503 if (inWindowMove
|| inWindowResize
) {
504 // TWindow needs to deal with this.
505 super.onMouseMotion(mouse
);
509 if (mouseOnEmulator(mouse
)) {
510 synchronized (emulator
) {
511 mouse
.setX(mouse
.getX() - 1);
512 mouse
.setY(mouse
.getY() - 1);
513 emulator
.mouse(mouse
);
519 // Emulator didn't consume it, pass it on
520 super.onMouseMotion(mouse
);