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