blinking cursor and SGR 1006 mode
[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.isReverse();
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 if (shell != null) {
246 shell.destroy();
247 shell = null;
248 } else {
249 emulator.close();
250 }
251 }
252
253 /**
254 * Copy out variables from the emulator that TTerminal has to expose on
255 * screen.
256 */
257 private void readEmulatorState() {
258 // Synchronize against the emulator so we don't stomp on its reader
259 // thread.
260 synchronized (emulator) {
261
262 setCursorX(emulator.getCursorX() + 1);
263 setCursorY(emulator.getCursorY() + 1
264 + (getHeight() - 2 - emulator.getHeight()));
265 if (vScroller != null) {
266 setCursorY(getCursorY() - vScroller.getValue());
267 }
268 setCursorVisible(emulator.isCursorVisible());
269 if (getCursorX() > getWidth() - 2) {
270 setCursorVisible(false);
271 }
272 if ((getCursorY() > getHeight() - 2) || (getCursorY() < 0)) {
273 setCursorVisible(false);
274 }
275 if (emulator.getScreenTitle().length() > 0) {
276 // Only update the title if the shell is still alive
277 if (shell != null) {
278 setTitle(emulator.getScreenTitle());
279 }
280 }
281
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()));
287 shell = null;
288 emulator.close();
289 } else if (emulator.isReading() && (shell != null)) {
290 // The shell might be dead, let's check
291 try {
292 int rc = shell.exitValue();
293 // If we got here, the shell died.
294 setTitle(String.format("%s [Completed - %d]",
295 getTitle(), rc));
296 shell = null;
297 emulator.close();
298 } catch (IllegalThreadStateException e) {
299 // The shell is still running, do nothing.
300 }
301 }
302
303 } // synchronized (emulator)
304 }
305
306 /**
307 * Handle window/screen resize events.
308 *
309 * @param resize resize event
310 */
311 @Override
312 public void onResize(final TResizeEvent resize) {
313
314 // Synchronize against the emulator so we don't stomp on its reader
315 // thread.
316 synchronized (emulator) {
317
318 if (resize.getType() == TResizeEvent.Type.WIDGET) {
319 // Resize the scroll bars
320 reflow();
321
322 // Get out of scrollback
323 vScroller.setValue(0);
324 }
325 return;
326
327 } // synchronized (emulator)
328 }
329
330 /**
331 * Resize scrollbars for a new width/height.
332 */
333 private void reflow() {
334
335 // Synchronize against the emulator so we don't stomp on its reader
336 // thread.
337 synchronized (emulator) {
338
339 // Pull cursor information
340 readEmulatorState();
341
342 // Vertical scrollbar
343 if (vScroller == null) {
344 vScroller = new TVScroller(this, getWidth() - 2, 0,
345 getHeight() - 2);
346 vScroller.setBottomValue(0);
347 vScroller.setValue(0);
348 } else {
349 vScroller.setX(getWidth() - 2);
350 vScroller.setHeight(getHeight() - 2);
351 }
352 vScroller.setTopValue(getHeight() - 2
353 - (emulator.getScrollbackBuffer().size()
354 + emulator.getDisplayBuffer().size()));
355 vScroller.setBigChange(getHeight() - 2);
356
357 } // synchronized (emulator)
358 }
359
360 /**
361 * Check if a mouse press/release/motion event coordinate is over the
362 * emulator.
363 *
364 * @param mouse a mouse-based event
365 * @return whether or not the mouse is on the emulator
366 */
367 private final boolean mouseOnEmulator(final TMouseEvent mouse) {
368
369 synchronized (emulator) {
370 if (!emulator.isReading()) {
371 return false;
372 }
373 }
374
375 if ((mouse.getAbsoluteX() >= getAbsoluteX() + 1)
376 && (mouse.getAbsoluteX() < getAbsoluteX() + getWidth() - 1)
377 && (mouse.getAbsoluteY() >= getAbsoluteY() + 1)
378 && (mouse.getAbsoluteY() < getAbsoluteY() + getHeight() - 1)
379 ) {
380 return true;
381 }
382 return false;
383 }
384
385 /**
386 * Handle keystrokes.
387 *
388 * @param keypress keystroke event
389 */
390 @Override
391 public void onKeypress(final TKeypressEvent keypress) {
392
393 // Scrollback up/down
394 if (keypress.equals(kbShiftPgUp)
395 || keypress.equals(kbCtrlPgUp)
396 || keypress.equals(kbAltPgUp)
397 ) {
398 vScroller.bigDecrement();
399 return;
400 }
401 if (keypress.equals(kbShiftPgDn)
402 || keypress.equals(kbCtrlPgDn)
403 || keypress.equals(kbAltPgDn)
404 ) {
405 vScroller.bigIncrement();
406 return;
407 }
408
409 // Synchronize against the emulator so we don't stomp on its reader
410 // thread.
411 synchronized (emulator) {
412 if (emulator.isReading()) {
413 // Get out of scrollback
414 vScroller.setValue(0);
415 emulator.keypress(keypress.getKey());
416
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);
422 }
423 }
424
425 readEmulatorState();
426 return;
427 }
428 }
429
430 // Process is closed, honor "normal" TUI keystrokes
431 super.onKeypress(keypress);
432 }
433
434 /**
435 * Handle mouse press events.
436 *
437 * @param mouse mouse button press event
438 */
439 @Override
440 public void onMouseDown(final TMouseEvent mouse) {
441 if (inWindowMove || inWindowResize) {
442 // TWindow needs to deal with this.
443 super.onMouseDown(mouse);
444 return;
445 }
446
447 if (mouse.isMouseWheelUp()) {
448 vScroller.decrement();
449 return;
450 }
451 if (mouse.isMouseWheelDown()) {
452 vScroller.increment();
453 return;
454 }
455 if (mouseOnEmulator(mouse)) {
456 synchronized (emulator) {
457 mouse.setX(mouse.getX() - 1);
458 mouse.setY(mouse.getY() - 1);
459 emulator.mouse(mouse);
460 readEmulatorState();
461 return;
462 }
463 }
464
465 // Emulator didn't consume it, pass it on
466 super.onMouseDown(mouse);
467 }
468
469 /**
470 * Handle mouse release events.
471 *
472 * @param mouse mouse button release event
473 */
474 @Override
475 public void onMouseUp(final TMouseEvent mouse) {
476 if (inWindowMove || inWindowResize) {
477 // TWindow needs to deal with this.
478 super.onMouseUp(mouse);
479 return;
480 }
481
482 if (mouseOnEmulator(mouse)) {
483 synchronized (emulator) {
484 mouse.setX(mouse.getX() - 1);
485 mouse.setY(mouse.getY() - 1);
486 emulator.mouse(mouse);
487 readEmulatorState();
488 return;
489 }
490 }
491
492 // Emulator didn't consume it, pass it on
493 super.onMouseUp(mouse);
494 }
495
496 /**
497 * Handle mouse motion events.
498 *
499 * @param mouse mouse motion event
500 */
501 @Override
502 public void onMouseMotion(final TMouseEvent mouse) {
503 if (inWindowMove || inWindowResize) {
504 // TWindow needs to deal with this.
505 super.onMouseMotion(mouse);
506 return;
507 }
508
509 if (mouseOnEmulator(mouse)) {
510 synchronized (emulator) {
511 mouse.setX(mouse.getX() - 1);
512 mouse.setY(mouse.getY() - 1);
513 emulator.mouse(mouse);
514 readEmulatorState();
515 return;
516 }
517 }
518
519 // Emulator didn't consume it, pass it on
520 super.onMouseMotion(mouse);
521 }
522
523 }