many fixes
[nikiroo-utils.git] / src / jexer / TTerminalWindow.java
1 /**
2 * Jexer - Java Text User Interface
3 *
4 * License: LGPLv3 or later
5 *
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.
9 *
10 * Copyright (C) 2015 Kevin Lamonte
11 *
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.
16 *
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.
21 *
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
26 * 02110-1301 USA
27 *
28 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
29 * @version 1
30 */
31 package jexer;
32
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;
39 import java.util.Map;
40
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.*;
49
50 /**
51 * TTerminalWindow exposes a ECMA-48 / ANSI X3.64 style terminal in a window.
52 */
53 public class TTerminalWindow extends TWindow {
54
55 /**
56 * The emulator.
57 */
58 private ECMA48 emulator;
59
60 /**
61 * The Process created by the shell spawning constructor.
62 */
63 private Process shell;
64
65 /**
66 * Vertical scrollbar.
67 */
68 private TVScroller vScroller;
69
70 /**
71 * Public constructor spawns a shell.
72 *
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
77 */
78 public TTerminalWindow(final TApplication application, final int x,
79 final int y, final int flags) {
80
81 super(application, "Terminal", x, y, 80 + 2, 24 + 2, flags);
82
83 // Assume XTERM
84 ECMA48.DeviceType deviceType = ECMA48.DeviceType.XTERM;
85
86 try {
87 String [] cmdShellWindows = {
88 "cmd.exe"
89 };
90
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"
96 };
97 // Spawn a shell and pass its I/O to the other constructor.
98
99 ProcessBuilder pb;
100 if (System.getProperty("os.name").startsWith("Windows")) {
101 pb = new ProcessBuilder(cmdShellWindows);
102 } else {
103 pb = new ProcessBuilder(cmdShell);
104 }
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);
111 shell = pb.start();
112 emulator = new ECMA48(deviceType, shell.getInputStream(),
113 shell.getOutputStream());
114 } catch (IOException e) {
115 e.printStackTrace();
116 }
117
118 // Setup the scroll bars
119 onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
120 getHeight()));
121 }
122
123 /**
124 * Public constructor.
125 *
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
136 */
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 {
140
141 super(application, "Terminal", x, y, 80 + 2, 24 + 2, flags);
142
143 emulator = new ECMA48(ECMA48.DeviceType.XTERM, input, output);
144
145 // Setup the scroll bars
146 onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
147 getHeight()));
148
149 }
150
151 /**
152 * Draw the display buffer.
153 */
154 @Override
155 public void draw() {
156 // Synchronize against the emulator so we don't stomp on its reader
157 // thread.
158 synchronized (emulator) {
159
160 // Update the scroll bars
161 reflow();
162
163 // Draw the box using my superclass
164 super.draw();
165
166 List<DisplayLine> scrollback = emulator.getScrollbackBuffer();
167 List<DisplayLine> display = emulator.getDisplayBuffer();
168
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);
174
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());
180 }
181 visibleTop = 0;
182 }
183 assert (visibleTop >= 0);
184
185 List<DisplayLine> displayLines = new LinkedList<DisplayLine>();
186 displayLines.addAll(scrollback);
187 displayLines.addAll(display);
188
189 List<DisplayLine> visibleLines = new LinkedList<DisplayLine>();
190 visibleLines.addAll(preceedingBlankLines);
191 visibleLines.addAll(displayLines.subList(visibleTop,
192 visibleBottom));
193
194 visibleHeight -= visibleLines.size();
195 assert (visibleHeight >= 0);
196
197 // Now draw the emulator screen
198 int row = 1;
199 for (DisplayLine line: visibleLines) {
200 int widthMax = emulator.getWidth();
201 if (line.isDoubleWidth()) {
202 widthMax /= 2;
203 }
204 if (widthMax > getWidth() - 2) {
205 widthMax = getWidth() - 2;
206 }
207 for (int i = 0; i < widthMax; i++) {
208 Cell ch = line.charAt(i);
209 Cell newCell = new Cell();
210 newCell.setTo(ch);
211 boolean reverse = line.isReverseColor() ^ ch.getReverse();
212 newCell.setReverse(false);
213 if (reverse) {
214 newCell.setBackColor(ch.getForeColor());
215 newCell.setForeColor(ch.getBackColor());
216 }
217 if (line.isDoubleWidth()) {
218 getScreen().putCharXY((i * 2) + 1, row, newCell);
219 getScreen().putCharXY((i * 2) + 2, row, ' ', newCell);
220 } else {
221 getScreen().putCharXY(i + 1, row, newCell);
222 }
223 }
224 row++;
225 if (row == getHeight() - 1) {
226 // Don't overwrite the box edge
227 break;
228 }
229 }
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, ' ',
234 background);
235 }
236
237 } // synchronized (emulator)
238
239 }
240
241 /**
242 * Handle window close.
243 */
244 @Override public void onClose() {
245 emulator.close();
246 }
247
248 /**
249 * Copy out variables from the emulator that TTerminal has to expose on
250 * screen.
251 */
252 private void readEmulatorState() {
253 // Synchronize against the emulator so we don't stomp on its reader
254 // thread.
255 synchronized (emulator) {
256
257 setCursorX(emulator.getCursorX() + 1);
258 setCursorY(emulator.getCursorY() + 1
259 + (getHeight() - 2 - emulator.getHeight()));
260 if (vScroller != null) {
261 setCursorY(getCursorY() - vScroller.getValue());
262 }
263 setHasCursor(emulator.visibleCursor());
264 if (getCursorX() > getWidth() - 2) {
265 setHasCursor(false);
266 }
267 if ((getCursorY() > getHeight() - 2) || (getCursorY() < 0)) {
268 setHasCursor(false);
269 }
270 if (emulator.getScreenTitle().length() > 0) {
271 // Only update the title if the shell is still alive
272 if (shell != null) {
273 setTitle(emulator.getScreenTitle());
274 }
275 }
276
277 // Check to see if the shell has died.
278 if (!emulator.isReading() && (shell != null)) {
279 // The emulator exited on its own, all is fine
280 setTitle(String.format("%s [Completed - %d]",
281 getTitle(), shell.exitValue()));
282 shell = null;
283 emulator.close();
284 } else if (emulator.isReading() && (shell != null)) {
285 // The shell might be dead, let's check
286 try {
287 int rc = shell.exitValue();
288 // If we got here, the shell died.
289 setTitle(String.format("%s [Completed - %d]",
290 getTitle(), rc));
291 shell = null;
292 emulator.close();
293 } catch (IllegalThreadStateException e) {
294 // The shell is still running, do nothing.
295 }
296 }
297
298 } // synchronized (emulator)
299 }
300
301 /**
302 * Handle window/screen resize events.
303 *
304 * @param resize resize event
305 */
306 @Override
307 public void onResize(final TResizeEvent resize) {
308
309 // Synchronize against the emulator so we don't stomp on its reader
310 // thread.
311 synchronized (emulator) {
312
313 if (resize.getType() == TResizeEvent.Type.WIDGET) {
314 // Resize the scroll bars
315 reflow();
316
317 // Get out of scrollback
318 vScroller.setValue(0);
319 }
320 return;
321
322 } // synchronized (emulator)
323 }
324
325 /**
326 * Resize scrollbars for a new width/height.
327 */
328 private void reflow() {
329
330 // Synchronize against the emulator so we don't stomp on its reader
331 // thread.
332 synchronized (emulator) {
333
334 // Pull cursor information
335 readEmulatorState();
336
337 // Vertical scrollbar
338 if (vScroller == null) {
339 vScroller = new TVScroller(this, getWidth() - 2, 0,
340 getHeight() - 2);
341 vScroller.setBottomValue(0);
342 vScroller.setValue(0);
343 } else {
344 vScroller.setX(getWidth() - 2);
345 vScroller.setHeight(getHeight() - 2);
346 }
347 vScroller.setTopValue(getHeight() - 2
348 - (emulator.getScrollbackBuffer().size()
349 + emulator.getDisplayBuffer().size()));
350 vScroller.setBigChange(getHeight() - 2);
351
352 } // synchronized (emulator)
353 }
354
355 /**
356 * Check if a mouse press/release/motion event coordinate is over the
357 * emulator.
358 *
359 * @param mouse a mouse-based event
360 * @return whether or not the mouse is on the emulator
361 */
362 private final boolean mouseOnEmulator(final TMouseEvent mouse) {
363
364 synchronized (emulator) {
365 if (!emulator.isReading()) {
366 return false;
367 }
368 }
369
370 if ((mouse.getAbsoluteX() >= getAbsoluteX() + 1)
371 && (mouse.getAbsoluteX() < getAbsoluteX() + getWidth() - 1)
372 && (mouse.getAbsoluteY() >= getAbsoluteY() + 1)
373 && (mouse.getAbsoluteY() < getAbsoluteY() + getHeight() - 1)
374 ) {
375 return true;
376 }
377 return false;
378 }
379
380 /**
381 * Handle keystrokes.
382 *
383 * @param keypress keystroke event
384 */
385 @Override
386 public void onKeypress(final TKeypressEvent keypress) {
387
388 // Scrollback up/down
389 if (keypress.equals(kbShiftPgUp)
390 || keypress.equals(kbCtrlPgUp)
391 || keypress.equals(kbAltPgUp)
392 ) {
393 vScroller.bigDecrement();
394 return;
395 }
396 if (keypress.equals(kbShiftPgDn)
397 || keypress.equals(kbCtrlPgDn)
398 || keypress.equals(kbAltPgDn)
399 ) {
400 vScroller.bigIncrement();
401 return;
402 }
403
404 // Synchronize against the emulator so we don't stomp on its reader
405 // thread.
406 synchronized (emulator) {
407 if (emulator.isReading()) {
408 // Get out of scrollback
409 vScroller.setValue(0);
410 emulator.keypress(keypress.getKey());
411
412 // UGLY HACK TIME! cmd.exe needs CRLF, not just CR, so if
413 // this is kBEnter then also send kbCtrlJ.
414 if (System.getProperty("os.name").startsWith("Windows")) {
415 if (keypress.equals(kbEnter)) {
416 emulator.keypress(kbCtrlJ);
417 }
418 }
419
420 readEmulatorState();
421 return;
422 }
423 }
424
425 // Process is closed, honor "normal" TUI keystrokes
426 super.onKeypress(keypress);
427 }
428
429 /**
430 * Handle mouse press events.
431 *
432 * @param mouse mouse button press event
433 */
434 @Override
435 public void onMouseDown(final TMouseEvent mouse) {
436 if (inWindowMove || inWindowResize) {
437 // TWindow needs to deal with this.
438 super.onMouseDown(mouse);
439 return;
440 }
441
442 if (mouse.getMouseWheelUp()) {
443 vScroller.decrement();
444 return;
445 }
446 if (mouse.getMouseWheelDown()) {
447 vScroller.increment();
448 return;
449 }
450 if (mouseOnEmulator(mouse)) {
451 synchronized (emulator) {
452 mouse.setX(mouse.getX() - 1);
453 mouse.setY(mouse.getY() - 1);
454 emulator.mouse(mouse);
455 readEmulatorState();
456 return;
457 }
458 }
459
460 // Emulator didn't consume it, pass it on
461 super.onMouseDown(mouse);
462 }
463
464 /**
465 * Handle mouse release events.
466 *
467 * @param mouse mouse button release event
468 */
469 @Override
470 public void onMouseUp(final TMouseEvent mouse) {
471 if (inWindowMove || inWindowResize) {
472 // TWindow needs to deal with this.
473 super.onMouseUp(mouse);
474 return;
475 }
476
477 if (mouseOnEmulator(mouse)) {
478 synchronized (emulator) {
479 mouse.setX(mouse.getX() - 1);
480 mouse.setY(mouse.getY() - 1);
481 emulator.mouse(mouse);
482 readEmulatorState();
483 return;
484 }
485 }
486
487 // Emulator didn't consume it, pass it on
488 super.onMouseUp(mouse);
489 }
490
491 /**
492 * Handle mouse motion events.
493 *
494 * @param mouse mouse motion event
495 */
496 @Override
497 public void onMouseMotion(final TMouseEvent mouse) {
498 if (inWindowMove || inWindowResize) {
499 // TWindow needs to deal with this.
500 super.onMouseMotion(mouse);
501 return;
502 }
503
504 if (mouseOnEmulator(mouse)) {
505 synchronized (emulator) {
506 mouse.setX(mouse.getX() - 1);
507 mouse.setY(mouse.getY() - 1);
508 emulator.mouse(mouse);
509 readEmulatorState();
510 return;
511 }
512 }
513
514 // Emulator didn't consume it, pass it on
515 super.onMouseMotion(mouse);
516 }
517
518 }