dev version bump to 0.0.6
[fanfix.git] / src / jexer / TTerminalWindow.java
CommitLineData
daa4106c 1/*
34a42e78
KL
2 * Jexer - Java Text User Interface
3 *
e16dda65 4 * The MIT License (MIT)
34a42e78 5 *
a2018e99 6 * Copyright (C) 2017 Kevin Lamonte
34a42e78 7 *
e16dda65
KL
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:
34a42e78 14 *
e16dda65
KL
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
34a42e78 17 *
e16dda65
KL
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.
34a42e78
KL
25 *
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
27 * @version 1
28 */
29package jexer;
30
34a42e78 31import java.io.IOException;
55d2b2c2 32import java.lang.reflect.Field;
34a42e78
KL
33import java.util.LinkedList;
34import java.util.List;
bd8d51fa 35import java.util.Map;
34a42e78
KL
36
37import jexer.bits.Cell;
38import jexer.bits.CellAttributes;
39import jexer.event.TKeypressEvent;
40import jexer.event.TMouseEvent;
41import jexer.event.TResizeEvent;
42import jexer.tterminal.DisplayLine;
43import jexer.tterminal.ECMA48;
44import static jexer.TKeypress.*;
45
46/**
47 * TTerminalWindow exposes a ECMA-48 / ANSI X3.64 style terminal in a window.
48 */
56661844 49public class TTerminalWindow extends TScrollableWindow {
34a42e78
KL
50
51 /**
52 * The emulator.
53 */
54 private ECMA48 emulator;
55
56 /**
57 * The Process created by the shell spawning constructor.
58 */
59 private Process shell;
60
5dfd1c11
KL
61 /**
62 * Claim the keystrokes the emulator will need.
63 */
64 private void addShortcutKeys() {
65 addShortcutKeypress(kbCtrlA);
66 addShortcutKeypress(kbCtrlB);
67 addShortcutKeypress(kbCtrlC);
68 addShortcutKeypress(kbCtrlD);
69 addShortcutKeypress(kbCtrlE);
70 addShortcutKeypress(kbCtrlF);
71 addShortcutKeypress(kbCtrlG);
72 addShortcutKeypress(kbCtrlH);
73 addShortcutKeypress(kbCtrlU);
74 addShortcutKeypress(kbCtrlJ);
75 addShortcutKeypress(kbCtrlK);
76 addShortcutKeypress(kbCtrlL);
77 addShortcutKeypress(kbCtrlM);
78 addShortcutKeypress(kbCtrlN);
79 addShortcutKeypress(kbCtrlO);
80 addShortcutKeypress(kbCtrlP);
81 addShortcutKeypress(kbCtrlQ);
82 addShortcutKeypress(kbCtrlR);
83 addShortcutKeypress(kbCtrlS);
84 addShortcutKeypress(kbCtrlT);
85 addShortcutKeypress(kbCtrlU);
86 addShortcutKeypress(kbCtrlV);
87 addShortcutKeypress(kbCtrlW);
88 addShortcutKeypress(kbCtrlX);
89 addShortcutKeypress(kbCtrlY);
90 addShortcutKeypress(kbCtrlZ);
91 addShortcutKeypress(kbF1);
92 addShortcutKeypress(kbF2);
93 addShortcutKeypress(kbF3);
94 addShortcutKeypress(kbF4);
95 addShortcutKeypress(kbF5);
96 addShortcutKeypress(kbF6);
97 addShortcutKeypress(kbF7);
98 addShortcutKeypress(kbF8);
99 addShortcutKeypress(kbF9);
100 addShortcutKeypress(kbF10);
101 addShortcutKeypress(kbF11);
102 addShortcutKeypress(kbF12);
103 addShortcutKeypress(kbAltA);
104 addShortcutKeypress(kbAltB);
105 addShortcutKeypress(kbAltC);
106 addShortcutKeypress(kbAltD);
107 addShortcutKeypress(kbAltE);
108 addShortcutKeypress(kbAltF);
109 addShortcutKeypress(kbAltG);
110 addShortcutKeypress(kbAltH);
111 addShortcutKeypress(kbAltU);
112 addShortcutKeypress(kbAltJ);
113 addShortcutKeypress(kbAltK);
114 addShortcutKeypress(kbAltL);
115 addShortcutKeypress(kbAltM);
116 addShortcutKeypress(kbAltN);
117 addShortcutKeypress(kbAltO);
118 addShortcutKeypress(kbAltP);
119 addShortcutKeypress(kbAltQ);
120 addShortcutKeypress(kbAltR);
121 addShortcutKeypress(kbAltS);
122 addShortcutKeypress(kbAltT);
123 addShortcutKeypress(kbAltU);
124 addShortcutKeypress(kbAltV);
125 addShortcutKeypress(kbAltW);
126 addShortcutKeypress(kbAltX);
127 addShortcutKeypress(kbAltY);
128 addShortcutKeypress(kbAltZ);
129 }
130
34a42e78
KL
131 /**
132 * Public constructor spawns a shell.
133 *
134 * @param application TApplication that manages this window
135 * @param x column relative to parent
136 * @param y row relative to parent
137 * @param flags mask of CENTERED, MODAL, or RESIZABLE
138 */
139 public TTerminalWindow(final TApplication application, final int x,
140 final int y, final int flags) {
141
142 super(application, "Terminal", x, y, 80 + 2, 24 + 2, flags);
143
56661844
KL
144 vScroller = new TVScroller(this, getWidth() - 2, 0, getHeight() - 2);
145 setBottomValue(0);
146
bd8d51fa
KL
147 // Assume XTERM
148 ECMA48.DeviceType deviceType = ECMA48.DeviceType.XTERM;
149
34a42e78
KL
150 try {
151 String [] cmdShellWindows = {
152 "cmd.exe"
153 };
154
155 // You cannot run a login shell in a bare Process interactively,
156 // due to libc's behavior of buffering when stdin/stdout aren't a
55d2b2c2
KL
157 // tty. Use 'script' instead to run a shell in a pty. And
158 // because BSD and GNU differ on the '-f' vs '-F' flags, we need
159 // two different commands. Lovely.
160 String [] cmdShellGNU = {
34a42e78
KL
161 "script", "-fqe", "/dev/null"
162 };
55d2b2c2 163 String [] cmdShellBSD = {
2ce6dab2 164 "script", "-q", "-F", "/dev/null"
55d2b2c2 165 };
34a42e78 166 // Spawn a shell and pass its I/O to the other constructor.
bd8d51fa 167
34a42e78
KL
168 ProcessBuilder pb;
169 if (System.getProperty("os.name").startsWith("Windows")) {
170 pb = new ProcessBuilder(cmdShellWindows);
55d2b2c2
KL
171 } else if (System.getProperty("os.name").startsWith("Mac")) {
172 pb = new ProcessBuilder(cmdShellBSD);
173 } else if (System.getProperty("os.name").startsWith("Linux")) {
174 pb = new ProcessBuilder(cmdShellGNU);
34a42e78 175 } else {
55d2b2c2
KL
176 // When all else fails, assume GNU.
177 pb = new ProcessBuilder(cmdShellGNU);
34a42e78 178 }
bd8d51fa
KL
179 Map<String, String> env = pb.environment();
180 env.put("TERM", ECMA48.deviceTypeTerm(deviceType));
181 env.put("LANG", ECMA48.deviceTypeLang(deviceType, "en"));
182 env.put("COLUMNS", "80");
183 env.put("LINES", "24");
34a42e78
KL
184 pb.redirectErrorStream(true);
185 shell = pb.start();
bd8d51fa 186 emulator = new ECMA48(deviceType, shell.getInputStream(),
34a42e78
KL
187 shell.getOutputStream());
188 } catch (IOException e) {
fe0770f9 189 messageBox("Error", "Error launching shell: " + e.getMessage());
34a42e78
KL
190 }
191
192 // Setup the scroll bars
193 onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
194 getHeight()));
5dfd1c11
KL
195
196 // Claim the keystrokes the emulator will need.
197 addShortcutKeys();
2ce6dab2
KL
198
199 // Add shortcut text
200 newStatusBar("Terminal session executing...");
34a42e78
KL
201 }
202
55d2b2c2
KL
203 /**
204 * Terminate the child of the 'script' process used on POSIX. This may
205 * or may not work.
206 */
207 private void terminateShellChildProcess() {
208 int pid = -1;
209 if (shell.getClass().getName().equals("java.lang.UNIXProcess")) {
210 /* get the PID on unix/linux systems */
211 try {
212 Field field = shell.getClass().getDeclaredField("pid");
213 field.setAccessible(true);
214 pid = field.getInt(shell);
215 } catch (Throwable e) {
216 // SQUASH, this didn't work. Just bail out quietly.
217 return;
218 }
219 }
220 if (pid != -1) {
221 // shell.destroy() works successfully at killing this side of
222 // 'script'. But we need to make sure the other side (child
223 // process) is also killed.
224 String [] cmdKillIt = {
225 "pkill", "-P", Integer.toString(pid)
226 };
227 try {
228 Runtime.getRuntime().exec(cmdKillIt);
229 } catch (Throwable e) {
230 // SQUASH, this didn't work. Just bail out quietly.
231 return;
232 }
233 }
234 }
235
34a42e78
KL
236 /**
237 * Draw the display buffer.
238 */
239 @Override
240 public void draw() {
241 // Synchronize against the emulator so we don't stomp on its reader
242 // thread.
243 synchronized (emulator) {
244
245 // Update the scroll bars
56661844 246 reflowData();
34a42e78
KL
247
248 // Draw the box using my superclass
249 super.draw();
250
251 List<DisplayLine> scrollback = emulator.getScrollbackBuffer();
252 List<DisplayLine> display = emulator.getDisplayBuffer();
253
254 // Put together the visible rows
34a42e78 255 int visibleHeight = getHeight() - 2;
34a42e78 256 int visibleBottom = scrollback.size() + display.size()
56661844 257 + getVerticalValue();
34a42e78
KL
258 assert (visibleBottom >= 0);
259
260 List<DisplayLine> preceedingBlankLines = new LinkedList<DisplayLine>();
261 int visibleTop = visibleBottom - visibleHeight;
34a42e78
KL
262 if (visibleTop < 0) {
263 for (int i = visibleTop; i < 0; i++) {
264 preceedingBlankLines.add(emulator.getBlankDisplayLine());
265 }
266 visibleTop = 0;
267 }
268 assert (visibleTop >= 0);
269
270 List<DisplayLine> displayLines = new LinkedList<DisplayLine>();
271 displayLines.addAll(scrollback);
272 displayLines.addAll(display);
34a42e78
KL
273
274 List<DisplayLine> visibleLines = new LinkedList<DisplayLine>();
275 visibleLines.addAll(preceedingBlankLines);
276 visibleLines.addAll(displayLines.subList(visibleTop,
277 visibleBottom));
34a42e78
KL
278
279 visibleHeight -= visibleLines.size();
34a42e78
KL
280 assert (visibleHeight >= 0);
281
282 // Now draw the emulator screen
283 int row = 1;
284 for (DisplayLine line: visibleLines) {
285 int widthMax = emulator.getWidth();
286 if (line.isDoubleWidth()) {
287 widthMax /= 2;
288 }
289 if (widthMax > getWidth() - 2) {
290 widthMax = getWidth() - 2;
291 }
292 for (int i = 0; i < widthMax; i++) {
293 Cell ch = line.charAt(i);
294 Cell newCell = new Cell();
295 newCell.setTo(ch);
7c870d89 296 boolean reverse = line.isReverseColor() ^ ch.isReverse();
34a42e78
KL
297 newCell.setReverse(false);
298 if (reverse) {
299 newCell.setBackColor(ch.getForeColor());
300 newCell.setForeColor(ch.getBackColor());
301 }
302 if (line.isDoubleWidth()) {
303 getScreen().putCharXY((i * 2) + 1, row, newCell);
304 getScreen().putCharXY((i * 2) + 2, row, ' ', newCell);
305 } else {
306 getScreen().putCharXY(i + 1, row, newCell);
307 }
308 }
309 row++;
310 if (row == getHeight() - 1) {
311 // Don't overwrite the box edge
312 break;
313 }
314 }
315 CellAttributes background = new CellAttributes();
316 // Fill in the blank lines on bottom
317 for (int i = 0; i < visibleHeight; i++) {
318 getScreen().hLineXY(1, i + row, getWidth() - 2, ' ',
319 background);
320 }
321
322 } // synchronized (emulator)
323
324 }
325
326 /**
327 * Handle window close.
328 */
2ce6dab2
KL
329 @Override
330 public void onClose() {
5dfd1c11 331 emulator.close();
69345248 332 if (shell != null) {
55d2b2c2 333 terminateShellChildProcess();
69345248
KL
334 shell.destroy();
335 shell = null;
69345248 336 }
34a42e78
KL
337 }
338
339 /**
340 * Copy out variables from the emulator that TTerminal has to expose on
341 * screen.
342 */
343 private void readEmulatorState() {
344 // Synchronize against the emulator so we don't stomp on its reader
345 // thread.
346 synchronized (emulator) {
347
348 setCursorX(emulator.getCursorX() + 1);
349 setCursorY(emulator.getCursorY() + 1
56661844
KL
350 + (getHeight() - 2 - emulator.getHeight())
351 - getVerticalValue());
7c870d89 352 setCursorVisible(emulator.isCursorVisible());
34a42e78 353 if (getCursorX() > getWidth() - 2) {
7c870d89 354 setCursorVisible(false);
34a42e78
KL
355 }
356 if ((getCursorY() > getHeight() - 2) || (getCursorY() < 0)) {
7c870d89 357 setCursorVisible(false);
34a42e78
KL
358 }
359 if (emulator.getScreenTitle().length() > 0) {
360 // Only update the title if the shell is still alive
361 if (shell != null) {
362 setTitle(emulator.getScreenTitle());
363 }
364 }
34a42e78
KL
365
366 // Check to see if the shell has died.
367 if (!emulator.isReading() && (shell != null)) {
55b4f29b
KL
368 try {
369 int rc = shell.exitValue();
370 // The emulator exited on its own, all is fine
371 setTitle(String.format("%s [Completed - %d]",
0d47c546 372 getTitle(), rc));
55b4f29b
KL
373 shell = null;
374 emulator.close();
5dfd1c11 375 clearShortcutKeypresses();
2ce6dab2
KL
376 statusBar.setText("Terminal session completed, exit " +
377 "code " + rc + ".");
55b4f29b
KL
378 } catch (IllegalThreadStateException e) {
379 // The emulator thread has exited, but the shell Process
380 // hasn't figured that out yet. Do nothing, we will see
381 // this in a future tick.
382 }
34a42e78
KL
383 } else if (emulator.isReading() && (shell != null)) {
384 // The shell might be dead, let's check
385 try {
386 int rc = shell.exitValue();
387 // If we got here, the shell died.
388 setTitle(String.format("%s [Completed - %d]",
389 getTitle(), rc));
390 shell = null;
391 emulator.close();
5dfd1c11 392 clearShortcutKeypresses();
2ce6dab2
KL
393 statusBar.setText("Terminal session completed, exit " +
394 "code " + rc + ".");
34a42e78
KL
395 } catch (IllegalThreadStateException e) {
396 // The shell is still running, do nothing.
397 }
398 }
92554d64 399
34a42e78
KL
400 } // synchronized (emulator)
401 }
402
403 /**
404 * Handle window/screen resize events.
405 *
406 * @param resize resize event
407 */
408 @Override
409 public void onResize(final TResizeEvent resize) {
410
411 // Synchronize against the emulator so we don't stomp on its reader
412 // thread.
413 synchronized (emulator) {
414
415 if (resize.getType() == TResizeEvent.Type.WIDGET) {
416 // Resize the scroll bars
56661844
KL
417 reflowData();
418 placeScrollbars();
34a42e78
KL
419
420 // Get out of scrollback
56661844 421 setVerticalValue(0);
34a42e78
KL
422 }
423 return;
424
425 } // synchronized (emulator)
426 }
427
428 /**
429 * Resize scrollbars for a new width/height.
430 */
56661844
KL
431 @Override
432 public void reflowData() {
34a42e78
KL
433
434 // Synchronize against the emulator so we don't stomp on its reader
435 // thread.
436 synchronized (emulator) {
437
438 // Pull cursor information
439 readEmulatorState();
440
441 // Vertical scrollbar
56661844 442 setTopValue(getHeight() - 2
34a42e78
KL
443 - (emulator.getScrollbackBuffer().size()
444 + emulator.getDisplayBuffer().size()));
56661844 445 setVerticalBigChange(getHeight() - 2);
34a42e78
KL
446
447 } // synchronized (emulator)
448 }
449
bd8d51fa
KL
450 /**
451 * Check if a mouse press/release/motion event coordinate is over the
452 * emulator.
453 *
454 * @param mouse a mouse-based event
455 * @return whether or not the mouse is on the emulator
456 */
457 private final boolean mouseOnEmulator(final TMouseEvent mouse) {
458
459 synchronized (emulator) {
460 if (!emulator.isReading()) {
461 return false;
462 }
463 }
464
465 if ((mouse.getAbsoluteX() >= getAbsoluteX() + 1)
466 && (mouse.getAbsoluteX() < getAbsoluteX() + getWidth() - 1)
467 && (mouse.getAbsoluteY() >= getAbsoluteY() + 1)
468 && (mouse.getAbsoluteY() < getAbsoluteY() + getHeight() - 1)
469 ) {
470 return true;
471 }
472 return false;
473 }
474
34a42e78
KL
475 /**
476 * Handle keystrokes.
477 *
478 * @param keypress keystroke event
479 */
480 @Override
481 public void onKeypress(final TKeypressEvent keypress) {
482
483 // Scrollback up/down
484 if (keypress.equals(kbShiftPgUp)
485 || keypress.equals(kbCtrlPgUp)
486 || keypress.equals(kbAltPgUp)
487 ) {
56661844 488 bigVerticalDecrement();
34a42e78
KL
489 return;
490 }
491 if (keypress.equals(kbShiftPgDn)
492 || keypress.equals(kbCtrlPgDn)
493 || keypress.equals(kbAltPgDn)
494 ) {
56661844 495 bigVerticalIncrement();
34a42e78
KL
496 return;
497 }
498
499 // Synchronize against the emulator so we don't stomp on its reader
500 // thread.
501 synchronized (emulator) {
502 if (emulator.isReading()) {
503 // Get out of scrollback
56661844 504 setVerticalValue(0);
34a42e78 505 emulator.keypress(keypress.getKey());
92554d64
KL
506
507 // UGLY HACK TIME! cmd.exe needs CRLF, not just CR, so if
508 // this is kBEnter then also send kbCtrlJ.
509 if (System.getProperty("os.name").startsWith("Windows")) {
510 if (keypress.equals(kbEnter)) {
511 emulator.keypress(kbCtrlJ);
512 }
513 }
514
34a42e78
KL
515 readEmulatorState();
516 return;
517 }
518 }
519
520 // Process is closed, honor "normal" TUI keystrokes
521 super.onKeypress(keypress);
522 }
523
524 /**
525 * Handle mouse press events.
526 *
527 * @param mouse mouse button press event
528 */
529 @Override
530 public void onMouseDown(final TMouseEvent mouse) {
bd8d51fa
KL
531 if (inWindowMove || inWindowResize) {
532 // TWindow needs to deal with this.
533 super.onMouseDown(mouse);
534 return;
535 }
34a42e78 536
7c870d89 537 if (mouse.isMouseWheelUp()) {
56661844 538 verticalDecrement();
34a42e78
KL
539 return;
540 }
7c870d89 541 if (mouse.isMouseWheelDown()) {
56661844 542 verticalIncrement();
34a42e78
KL
543 return;
544 }
bd8d51fa
KL
545 if (mouseOnEmulator(mouse)) {
546 synchronized (emulator) {
547 mouse.setX(mouse.getX() - 1);
548 mouse.setY(mouse.getY() - 1);
549 emulator.mouse(mouse);
550 readEmulatorState();
551 return;
552 }
553 }
34a42e78 554
bd8d51fa 555 // Emulator didn't consume it, pass it on
34a42e78
KL
556 super.onMouseDown(mouse);
557 }
558
bd8d51fa
KL
559 /**
560 * Handle mouse release events.
561 *
562 * @param mouse mouse button release event
563 */
564 @Override
565 public void onMouseUp(final TMouseEvent mouse) {
566 if (inWindowMove || inWindowResize) {
567 // TWindow needs to deal with this.
568 super.onMouseUp(mouse);
569 return;
570 }
571
572 if (mouseOnEmulator(mouse)) {
573 synchronized (emulator) {
574 mouse.setX(mouse.getX() - 1);
575 mouse.setY(mouse.getY() - 1);
576 emulator.mouse(mouse);
577 readEmulatorState();
578 return;
579 }
580 }
581
582 // Emulator didn't consume it, pass it on
583 super.onMouseUp(mouse);
584 }
585
586 /**
587 * Handle mouse motion events.
588 *
589 * @param mouse mouse motion event
590 */
591 @Override
592 public void onMouseMotion(final TMouseEvent mouse) {
593 if (inWindowMove || inWindowResize) {
594 // TWindow needs to deal with this.
595 super.onMouseMotion(mouse);
596 return;
597 }
598
599 if (mouseOnEmulator(mouse)) {
600 synchronized (emulator) {
601 mouse.setX(mouse.getX() - 1);
602 mouse.setY(mouse.getY() - 1);
603 emulator.mouse(mouse);
604 readEmulatorState();
605 return;
606 }
607 }
608
609 // Emulator didn't consume it, pass it on
610 super.onMouseMotion(mouse);
611 }
612
34a42e78 613}