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
;
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 * Public constructor spawns a shell.
72 * @param application TApplication that manages this window
73 * @param x column relative to parent
74 * @param y row relative to parent
75 * @param flags mask of CENTERED, MODAL, or RESIZABLE
77 public TTerminalWindow(final TApplication application
, final int x
,
78 final int y
, final int flags
) {
80 super(application
, "Terminal", x
, y
, 80 + 2, 24 + 2, flags
);
83 String
[] cmdShellWindows
= {
87 // You cannot run a login shell in a bare Process interactively,
88 // due to libc's behavior of buffering when stdin/stdout aren't a
89 // tty. Use 'script' instead to run a shell in a pty.
90 String
[] cmdShell
= {
91 "script", "-fqe", "/dev/null"
93 // Spawn a shell and pass its I/O to the other constructor.
95 if (System
.getProperty("os.name").startsWith("Windows")) {
96 pb
= new ProcessBuilder(cmdShellWindows
);
98 pb
= new ProcessBuilder(cmdShell
);
100 // shell = Runtime.getRuntime().exec(cmdShell);
102 // TODO: add LANG, TERM, LINES, and COLUMNS
103 pb
.redirectErrorStream(true);
105 emulator
= new ECMA48(ECMA48
.DeviceType
.XTERM
,
106 shell
.getInputStream(),
107 shell
.getOutputStream());
108 } catch (IOException e
) {
112 // Setup the scroll bars
113 onResize(new TResizeEvent(TResizeEvent
.Type
.WIDGET
, getWidth(),
118 * Public constructor.
120 * @param application TApplication that manages this window
121 * @param x column relative to parent
122 * @param y row relative to parent
123 * @param flags mask of CENTERED, MODAL, or RESIZABLE
124 * @param input an InputStream connected to the remote side. For type ==
125 * XTERM, input is converted to a Reader with UTF-8 encoding.
126 * @param output an OutputStream connected to the remote user. For type
127 * == XTERM, output is converted to a Writer with UTF-8 encoding.
128 * @throws UnsupportedEncodingException if an exception is thrown when
129 * creating the InputStreamReader
131 public TTerminalWindow(final TApplication application
, final int x
,
132 final int y
, final int flags
, final InputStream input
,
133 final OutputStream output
) throws UnsupportedEncodingException
{
135 super(application
, "Terminal", x
, y
, 80 + 2, 24 + 2, flags
);
137 emulator
= new ECMA48(ECMA48
.DeviceType
.XTERM
, input
, output
);
139 // Setup the scroll bars
140 onResize(new TResizeEvent(TResizeEvent
.Type
.WIDGET
, getWidth(),
146 * Draw the display buffer.
150 // Synchronize against the emulator so we don't stomp on its reader
152 synchronized (emulator
) {
154 // Update the scroll bars
157 // Draw the box using my superclass
160 List
<DisplayLine
> scrollback
= emulator
.getScrollbackBuffer();
161 List
<DisplayLine
> display
= emulator
.getDisplayBuffer();
163 // Put together the visible rows
164 // System.err.printf("----------------------------\n");
165 // System.err.printf("vScroller.value %d\n", vScroller.getValue());
166 int visibleHeight
= getHeight() - 2;
167 // System.err.printf("visibleHeight %d\n", visibleHeight);
168 int visibleBottom
= scrollback
.size() + display
.size()
169 + vScroller
.getValue();
170 // System.err.printf("visibleBottom %d\n", visibleBottom);
171 assert (visibleBottom
>= 0);
173 List
<DisplayLine
> preceedingBlankLines
= new LinkedList
<DisplayLine
>();
174 int visibleTop
= visibleBottom
- visibleHeight
;
175 // System.err.printf("visibleTop %d\n", visibleTop);
176 if (visibleTop
< 0) {
177 for (int i
= visibleTop
; i
< 0; i
++) {
178 preceedingBlankLines
.add(emulator
.getBlankDisplayLine());
182 assert (visibleTop
>= 0);
184 List
<DisplayLine
> displayLines
= new LinkedList
<DisplayLine
>();
185 displayLines
.addAll(scrollback
);
186 displayLines
.addAll(display
);
187 // System.err.printf("displayLines.size %d\n", displayLines.size());
189 List
<DisplayLine
> visibleLines
= new LinkedList
<DisplayLine
>();
190 visibleLines
.addAll(preceedingBlankLines
);
191 visibleLines
.addAll(displayLines
.subList(visibleTop
,
193 // System.err.printf("visibleLines.size %d\n", visibleLines.size());
195 visibleHeight
-= visibleLines
.size();
196 // System.err.printf("visibleHeight %d\n", visibleHeight);
197 assert (visibleHeight
>= 0);
199 // Now draw the emulator screen
201 for (DisplayLine line
: visibleLines
) {
202 int widthMax
= emulator
.getWidth();
203 if (line
.isDoubleWidth()) {
206 if (widthMax
> getWidth() - 2) {
207 widthMax
= getWidth() - 2;
209 for (int i
= 0; i
< widthMax
; i
++) {
210 Cell ch
= line
.charAt(i
);
211 Cell newCell
= new Cell();
213 boolean reverse
= line
.isReverseColor() ^ ch
.getReverse();
214 newCell
.setReverse(false);
216 newCell
.setBackColor(ch
.getForeColor());
217 newCell
.setForeColor(ch
.getBackColor());
219 if (line
.isDoubleWidth()) {
220 getScreen().putCharXY((i
* 2) + 1, row
, newCell
);
221 getScreen().putCharXY((i
* 2) + 2, row
, ' ', newCell
);
223 getScreen().putCharXY(i
+ 1, row
, newCell
);
227 if (row
== getHeight() - 1) {
228 // Don't overwrite the box edge
232 CellAttributes background
= new CellAttributes();
233 // Fill in the blank lines on bottom
234 for (int i
= 0; i
< visibleHeight
; i
++) {
235 getScreen().hLineXY(1, i
+ row
, getWidth() - 2, ' ',
239 } // synchronized (emulator)
244 * Handle window close.
246 @Override public void onClose() {
251 * Copy out variables from the emulator that TTerminal has to expose on
254 private void readEmulatorState() {
255 // Synchronize against the emulator so we don't stomp on its reader
257 synchronized (emulator
) {
259 setCursorX(emulator
.getCursorX() + 1);
260 setCursorY(emulator
.getCursorY() + 1
261 + (getHeight() - 2 - emulator
.getHeight()));
262 if (vScroller
!= null) {
263 setCursorY(getCursorY() - vScroller
.getValue());
265 setHasCursor(emulator
.visibleCursor());
266 if (getCursorX() > getWidth() - 2) {
269 if ((getCursorY() > getHeight() - 2) || (getCursorY() < 0)) {
272 if (emulator
.getScreenTitle().length() > 0) {
273 // Only update the title if the shell is still alive
275 setTitle(emulator
.getScreenTitle());
278 setMaximumWindowWidth(emulator
.getWidth() + 2);
280 // Check to see if the shell has died.
281 if (!emulator
.isReading() && (shell
!= null)) {
282 // The emulator exited on its own, all is fine
283 setTitle(String
.format("%s [Completed - %d]",
284 getTitle(), shell
.exitValue()));
287 } else if (emulator
.isReading() && (shell
!= null)) {
288 // The shell might be dead, let's check
290 int rc
= shell
.exitValue();
291 // If we got here, the shell died.
292 setTitle(String
.format("%s [Completed - %d]",
296 } catch (IllegalThreadStateException e
) {
297 // The shell is still running, do nothing.
301 } // synchronized (emulator)
305 * Handle window/screen resize events.
307 * @param resize resize event
310 public void onResize(final TResizeEvent resize
) {
312 // Synchronize against the emulator so we don't stomp on its reader
314 synchronized (emulator
) {
316 if (resize
.getType() == TResizeEvent
.Type
.WIDGET
) {
317 // Resize the scroll bars
320 // Get out of scrollback
321 vScroller
.setValue(0);
325 } // synchronized (emulator)
329 * Resize scrollbars for a new width/height.
331 private void reflow() {
333 // Synchronize against the emulator so we don't stomp on its reader
335 synchronized (emulator
) {
337 // Pull cursor information
340 // Vertical scrollbar
341 if (vScroller
== null) {
342 vScroller
= new TVScroller(this, getWidth() - 2, 0,
344 vScroller
.setBottomValue(0);
345 vScroller
.setValue(0);
347 vScroller
.setX(getWidth() - 2);
348 vScroller
.setHeight(getHeight() - 2);
350 vScroller
.setTopValue(getHeight() - 2
351 - (emulator
.getScrollbackBuffer().size()
352 + emulator
.getDisplayBuffer().size()));
353 vScroller
.setBigChange(getHeight() - 2);
355 } // synchronized (emulator)
361 * @param keypress keystroke event
364 public void onKeypress(final TKeypressEvent keypress
) {
366 // Scrollback up/down
367 if (keypress
.equals(kbShiftPgUp
)
368 || keypress
.equals(kbCtrlPgUp
)
369 || keypress
.equals(kbAltPgUp
)
371 vScroller
.bigDecrement();
374 if (keypress
.equals(kbShiftPgDn
)
375 || keypress
.equals(kbCtrlPgDn
)
376 || keypress
.equals(kbAltPgDn
)
378 vScroller
.bigIncrement();
382 // Synchronize against the emulator so we don't stomp on its reader
384 synchronized (emulator
) {
385 if (emulator
.isReading()) {
386 // Get out of scrollback
387 vScroller
.setValue(0);
388 emulator
.keypress(keypress
.getKey());
390 // UGLY HACK TIME! cmd.exe needs CRLF, not just CR, so if
391 // this is kBEnter then also send kbCtrlJ.
392 if (System
.getProperty("os.name").startsWith("Windows")) {
393 if (keypress
.equals(kbEnter
)) {
394 emulator
.keypress(kbCtrlJ
);
403 // Process is closed, honor "normal" TUI keystrokes
404 super.onKeypress(keypress
);
408 * Handle mouse press events.
410 * @param mouse mouse button press event
413 public void onMouseDown(final TMouseEvent mouse
) {
415 if (mouse
.getMouseWheelUp()) {
416 vScroller
.decrement();
419 if (mouse
.getMouseWheelDown()) {
420 vScroller
.increment();
425 super.onMouseDown(mouse
);