X-Git-Url: http://git.nikiroo.be/?a=blobdiff_plain;f=src%2Fjexer%2FTTerminalWindow.java;h=c7456358df7193db8d8be27481ecac6133e863a5;hb=0d86ab8480cabbe32fc87588304ddc795a4df14f;hp=6d97531aabbdb76e4d896127d284c3766a7e16f5;hpb=5dfd1c11947e9cb32fcac4772f1b16879d9ffe67;p=fanfix.git diff --git a/src/jexer/TTerminalWindow.java b/src/jexer/TTerminalWindow.java index 6d97531..c745635 100644 --- a/src/jexer/TTerminalWindow.java +++ b/src/jexer/TTerminalWindow.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2016 Kevin Lamonte + * Copyright (C) 2019 Kevin Lamonte * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), @@ -28,27 +28,51 @@ */ package jexer; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; + import java.io.InputStream; import java.io.IOException; -import java.io.OutputStream; -import java.io.UnsupportedEncodingException; -import java.util.LinkedList; +import java.lang.reflect.Field; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.ResourceBundle; +import jexer.backend.ECMA48Terminal; +import jexer.backend.GlyphMaker; +import jexer.backend.MultiScreen; +import jexer.backend.SwingTerminal; import jexer.bits.Cell; import jexer.bits.CellAttributes; import jexer.event.TKeypressEvent; +import jexer.event.TMenuEvent; import jexer.event.TMouseEvent; import jexer.event.TResizeEvent; +import jexer.menu.TMenu; import jexer.tterminal.DisplayLine; +import jexer.tterminal.DisplayListener; import jexer.tterminal.ECMA48; import static jexer.TKeypress.*; /** * TTerminalWindow exposes a ECMA-48 / ANSI X3.64 style terminal in a window. */ -public class TTerminalWindow extends TWindow { +public class TTerminalWindow extends TScrollableWindow + implements DisplayListener { + + /** + * Translated strings. + */ + private static final ResourceBundle i18n = ResourceBundle.getBundle(TTerminalWindow.class.getName()); + + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ /** * The emulator. @@ -61,166 +85,222 @@ public class TTerminalWindow extends TWindow { private Process shell; /** - * Vertical scrollbar. + * If true, we are using the ptypipe utility to support dynamic window + * resizing. ptypipe is available at + * https://gitlab.com/klamonte/ptypipe . */ - private TVScroller vScroller; + private boolean ptypipe = false; /** - * Claim the keystrokes the emulator will need. + * If true, close the window when the shell exits. */ - private void addShortcutKeys() { - addShortcutKeypress(kbCtrlA); - addShortcutKeypress(kbCtrlB); - addShortcutKeypress(kbCtrlC); - addShortcutKeypress(kbCtrlD); - addShortcutKeypress(kbCtrlE); - addShortcutKeypress(kbCtrlF); - addShortcutKeypress(kbCtrlG); - addShortcutKeypress(kbCtrlH); - addShortcutKeypress(kbCtrlU); - addShortcutKeypress(kbCtrlJ); - addShortcutKeypress(kbCtrlK); - addShortcutKeypress(kbCtrlL); - addShortcutKeypress(kbCtrlM); - addShortcutKeypress(kbCtrlN); - addShortcutKeypress(kbCtrlO); - addShortcutKeypress(kbCtrlP); - addShortcutKeypress(kbCtrlQ); - addShortcutKeypress(kbCtrlR); - addShortcutKeypress(kbCtrlS); - addShortcutKeypress(kbCtrlT); - addShortcutKeypress(kbCtrlU); - addShortcutKeypress(kbCtrlV); - addShortcutKeypress(kbCtrlW); - addShortcutKeypress(kbCtrlX); - addShortcutKeypress(kbCtrlY); - addShortcutKeypress(kbCtrlZ); - addShortcutKeypress(kbF1); - addShortcutKeypress(kbF2); - addShortcutKeypress(kbF3); - addShortcutKeypress(kbF4); - addShortcutKeypress(kbF5); - addShortcutKeypress(kbF6); - addShortcutKeypress(kbF7); - addShortcutKeypress(kbF8); - addShortcutKeypress(kbF9); - addShortcutKeypress(kbF10); - addShortcutKeypress(kbF11); - addShortcutKeypress(kbF12); - addShortcutKeypress(kbAltA); - addShortcutKeypress(kbAltB); - addShortcutKeypress(kbAltC); - addShortcutKeypress(kbAltD); - addShortcutKeypress(kbAltE); - addShortcutKeypress(kbAltF); - addShortcutKeypress(kbAltG); - addShortcutKeypress(kbAltH); - addShortcutKeypress(kbAltU); - addShortcutKeypress(kbAltJ); - addShortcutKeypress(kbAltK); - addShortcutKeypress(kbAltL); - addShortcutKeypress(kbAltM); - addShortcutKeypress(kbAltN); - addShortcutKeypress(kbAltO); - addShortcutKeypress(kbAltP); - addShortcutKeypress(kbAltQ); - addShortcutKeypress(kbAltR); - addShortcutKeypress(kbAltS); - addShortcutKeypress(kbAltT); - addShortcutKeypress(kbAltU); - addShortcutKeypress(kbAltV); - addShortcutKeypress(kbAltW); - addShortcutKeypress(kbAltX); - addShortcutKeypress(kbAltY); - addShortcutKeypress(kbAltZ); + private boolean closeOnExit = false; + + /** + * Double-height font. + */ + private GlyphMaker doubleFont; + + /** + * Last text width value. + */ + private int lastTextWidth = -1; + + /** + * Last text height value. + */ + private int lastTextHeight = -1; + + /** + * The blink state, used only by ECMA48 backend and when double-width + * chars must be drawn. + */ + private boolean blinkState = true; + + /** + * Timer flag, used only by ECMA48 backend and when double-width chars + * must be drawn. + */ + private boolean haveTimer = false; + + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Public constructor spawns a custom command line. + * + * @param application TApplication that manages this window + * @param x column relative to parent + * @param y row relative to parent + * @param commandLine the command line to execute + */ + public TTerminalWindow(final TApplication application, final int x, + final int y, final String commandLine) { + + this(application, x, y, RESIZABLE, commandLine.split("\\s+"), + System.getProperty("jexer.TTerminal.closeOnExit", + "false").equals("true")); } /** - * Public constructor spawns a shell. + * Public constructor spawns a custom command line. + * + * @param application TApplication that manages this window + * @param x column relative to parent + * @param y row relative to parent + * @param commandLine the command line to execute + * @param closeOnExit if true, close the window when the command exits + */ + public TTerminalWindow(final TApplication application, final int x, + final int y, final String commandLine, final boolean closeOnExit) { + + this(application, x, y, RESIZABLE, commandLine.split("\\s+"), + closeOnExit); + } + + /** + * Public constructor spawns a custom command line. * * @param application TApplication that manages this window * @param x column relative to parent * @param y row relative to parent * @param flags mask of CENTERED, MODAL, or RESIZABLE + * @param command the command line to execute */ public TTerminalWindow(final TApplication application, final int x, - final int y, final int flags) { + final int y, final int flags, final String [] command) { - super(application, "Terminal", x, y, 80 + 2, 24 + 2, flags); + this(application, x, y, flags, command, + System.getProperty("jexer.TTerminal.closeOnExit", + "false").equals("true")); + } - // Assume XTERM - ECMA48.DeviceType deviceType = ECMA48.DeviceType.XTERM; + /** + * Public constructor spawns a custom command line. + * + * @param application TApplication that manages this window + * @param x column relative to parent + * @param y row relative to parent + * @param flags mask of CENTERED, MODAL, or RESIZABLE + * @param command the command line to execute + * @param closeOnExit if true, close the window when the command exits + */ + public TTerminalWindow(final TApplication application, final int x, + final int y, final int flags, final String [] command, + final boolean closeOnExit) { - try { - String [] cmdShellWindows = { - "cmd.exe" - }; + super(application, i18n.getString("windowTitle"), x, y, + 80 + 2, 24 + 2, flags); - // You cannot run a login shell in a bare Process interactively, - // due to libc's behavior of buffering when stdin/stdout aren't a - // tty. Use 'script' instead to run a shell in a pty. - String [] cmdShell = { - "script", "-fqe", "/dev/null" - }; - // Spawn a shell and pass its I/O to the other constructor. + this.closeOnExit = closeOnExit; - ProcessBuilder pb; - if (System.getProperty("os.name").startsWith("Windows")) { - pb = new ProcessBuilder(cmdShellWindows); - } else { - pb = new ProcessBuilder(cmdShell); - } - Map env = pb.environment(); - env.put("TERM", ECMA48.deviceTypeTerm(deviceType)); - env.put("LANG", ECMA48.deviceTypeLang(deviceType, "en")); - env.put("COLUMNS", "80"); - env.put("LINES", "24"); - pb.redirectErrorStream(true); - shell = pb.start(); - emulator = new ECMA48(deviceType, shell.getInputStream(), - shell.getOutputStream()); - } catch (IOException e) { - e.printStackTrace(); + String [] fullCommand; + + // Spawn a shell and pass its I/O to the other constructor. + if ((System.getProperty("jexer.TTerminal.ptypipe") != null) + && (System.getProperty("jexer.TTerminal.ptypipe"). + equals("true")) + ) { + ptypipe = true; + fullCommand = new String[command.length + 1]; + fullCommand[0] = "ptypipe"; + System.arraycopy(command, 0, fullCommand, 1, command.length); + } else if (System.getProperty("os.name").startsWith("Windows")) { + fullCommand = new String[3]; + fullCommand[0] = "cmd"; + fullCommand[1] = "/c"; + fullCommand[2] = stringArrayToString(command); + } else if (System.getProperty("os.name").startsWith("Mac")) { + fullCommand = new String[6]; + fullCommand[0] = "script"; + fullCommand[1] = "-q"; + fullCommand[2] = "-F"; + fullCommand[3] = "/dev/null"; + fullCommand[4] = "-c"; + fullCommand[5] = stringArrayToString(command); + } else { + // Default: behave like Linux + fullCommand = new String[5]; + fullCommand[0] = "script"; + fullCommand[1] = "-fqe"; + fullCommand[2] = "/dev/null"; + fullCommand[3] = "-c"; + fullCommand[4] = stringArrayToString(command); } + spawnShell(fullCommand); + } - // Setup the scroll bars - onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(), - getHeight())); + /** + * Public constructor spawns a shell. + * + * @param application TApplication that manages this window + * @param x column relative to parent + * @param y row relative to parent + * @param flags mask of CENTERED, MODAL, or RESIZABLE + */ + public TTerminalWindow(final TApplication application, final int x, + final int y, final int flags) { + + this(application, x, y, flags, + System.getProperty("jexer.TTerminal.closeOnExit", + "false").equals("true")); - // Claim the keystrokes the emulator will need. - addShortcutKeys(); } /** - * Public constructor. + * Public constructor spawns a shell. * * @param application TApplication that manages this window * @param x column relative to parent * @param y row relative to parent * @param flags mask of CENTERED, MODAL, or RESIZABLE - * @param input an InputStream connected to the remote side. For type == - * XTERM, input is converted to a Reader with UTF-8 encoding. - * @param output an OutputStream connected to the remote user. For type - * == XTERM, output is converted to a Writer with UTF-8 encoding. - * @throws UnsupportedEncodingException if an exception is thrown when - * creating the InputStreamReader + * @param closeOnExit if true, close the window when the shell exits */ public TTerminalWindow(final TApplication application, final int x, - final int y, final int flags, final InputStream input, - final OutputStream output) throws UnsupportedEncodingException { + final int y, final int flags, final boolean closeOnExit) { - super(application, "Terminal", x, y, 80 + 2, 24 + 2, flags); + super(application, i18n.getString("windowTitle"), x, y, + 80 + 2, 24 + 2, flags); - emulator = new ECMA48(ECMA48.DeviceType.XTERM, input, output); + this.closeOnExit = closeOnExit; - // Setup the scroll bars - onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(), - getHeight())); + String cmdShellWindows = "cmd.exe"; - // Claim the keystrokes the emulator will need. - addShortcutKeys(); + // You cannot run a login shell in a bare Process interactively, due + // to libc's behavior of buffering when stdin/stdout aren't a tty. + // Use 'script' instead to run a shell in a pty. And because BSD and + // GNU differ on the '-f' vs '-F' flags, we need two different + // commands. Lovely. + String cmdShellGNU = "script -fqe /dev/null"; + String cmdShellBSD = "script -q -F /dev/null"; + + // ptypipe is another solution that permits dynamic window resizing. + String cmdShellPtypipe = "ptypipe /bin/bash --login"; + + // Spawn a shell and pass its I/O to the other constructor. + if ((System.getProperty("jexer.TTerminal.ptypipe") != null) + && (System.getProperty("jexer.TTerminal.ptypipe"). + equals("true")) + ) { + ptypipe = true; + spawnShell(cmdShellPtypipe.split("\\s+")); + } else if (System.getProperty("os.name").startsWith("Windows")) { + spawnShell(cmdShellWindows.split("\\s+")); + } else if (System.getProperty("os.name").startsWith("Mac")) { + spawnShell(cmdShellBSD.split("\\s+")); + } else if (System.getProperty("os.name").startsWith("Linux")) { + spawnShell(cmdShellGNU.split("\\s+")); + } else { + // When all else fails, assume GNU. + spawnShell(cmdShellGNU.split("\\s+")); + } } + // ------------------------------------------------------------------------ + // TScrollableWindow ------------------------------------------------------ + // ------------------------------------------------------------------------ + /** * Draw the display buffer. */ @@ -231,7 +311,7 @@ public class TTerminalWindow extends TWindow { synchronized (emulator) { // Update the scroll bars - reflow(); + reflowData(); // Draw the box using my superclass super.draw(); @@ -242,10 +322,10 @@ public class TTerminalWindow extends TWindow { // Put together the visible rows int visibleHeight = getHeight() - 2; int visibleBottom = scrollback.size() + display.size() - + vScroller.getValue(); + + getVerticalValue(); assert (visibleBottom >= 0); - List preceedingBlankLines = new LinkedList(); + List preceedingBlankLines = new ArrayList(); int visibleTop = visibleBottom - visibleHeight; if (visibleTop < 0) { for (int i = visibleTop; i < 0; i++) { @@ -255,11 +335,11 @@ public class TTerminalWindow extends TWindow { } assert (visibleTop >= 0); - List displayLines = new LinkedList(); + List displayLines = new ArrayList(); displayLines.addAll(scrollback); displayLines.addAll(display); - List visibleLines = new LinkedList(); + List visibleLines = new ArrayList(); visibleLines.addAll(preceedingBlankLines); visibleLines.addAll(displayLines.subList(visibleTop, visibleBottom)); @@ -284,14 +364,23 @@ public class TTerminalWindow extends TWindow { boolean reverse = line.isReverseColor() ^ ch.isReverse(); newCell.setReverse(false); if (reverse) { - newCell.setBackColor(ch.getForeColor()); - newCell.setForeColor(ch.getBackColor()); + if (ch.getForeColorRGB() < 0) { + newCell.setBackColor(ch.getForeColor()); + newCell.setBackColorRGB(-1); + } else { + newCell.setBackColorRGB(ch.getForeColorRGB()); + } + if (ch.getBackColorRGB() < 0) { + newCell.setForeColor(ch.getBackColor()); + newCell.setForeColorRGB(-1); + } else { + newCell.setForeColorRGB(ch.getBackColorRGB()); + } } if (line.isDoubleWidth()) { - getScreen().putCharXY((i * 2) + 1, row, newCell); - getScreen().putCharXY((i * 2) + 2, row, ' ', newCell); + putDoubleWidthCharXY(line, (i * 2) + 1, row, newCell); } else { - getScreen().putCharXY(i + 1, row, newCell); + putCharXY(i + 1, row, newCell); } } row++; @@ -303,8 +392,7 @@ public class TTerminalWindow extends TWindow { CellAttributes background = new CellAttributes(); // Fill in the blank lines on bottom for (int i = 0; i < visibleHeight; i++) { - getScreen().hLineXY(1, i + row, getWidth() - 2, ' ', - background); + hLineXY(1, i + row, getWidth() - 2, ' ', background); } } // synchronized (emulator) @@ -314,77 +402,16 @@ public class TTerminalWindow extends TWindow { /** * Handle window close. */ - @Override public void onClose() { + @Override + public void onClose() { emulator.close(); if (shell != null) { - // System.err.println("shell.destroy()"); + terminateShellChildProcess(); shell.destroy(); shell = null; } } - /** - * Copy out variables from the emulator that TTerminal has to expose on - * screen. - */ - private void readEmulatorState() { - // Synchronize against the emulator so we don't stomp on its reader - // thread. - synchronized (emulator) { - - setCursorX(emulator.getCursorX() + 1); - setCursorY(emulator.getCursorY() + 1 - + (getHeight() - 2 - emulator.getHeight())); - if (vScroller != null) { - setCursorY(getCursorY() - vScroller.getValue()); - } - setCursorVisible(emulator.isCursorVisible()); - if (getCursorX() > getWidth() - 2) { - setCursorVisible(false); - } - if ((getCursorY() > getHeight() - 2) || (getCursorY() < 0)) { - setCursorVisible(false); - } - if (emulator.getScreenTitle().length() > 0) { - // Only update the title if the shell is still alive - if (shell != null) { - setTitle(emulator.getScreenTitle()); - } - } - - // Check to see if the shell has died. - if (!emulator.isReading() && (shell != null)) { - try { - int rc = shell.exitValue(); - // The emulator exited on its own, all is fine - setTitle(String.format("%s [Completed - %d]", - getTitle(), rc)); - shell = null; - emulator.close(); - clearShortcutKeypresses(); - } catch (IllegalThreadStateException e) { - // The emulator thread has exited, but the shell Process - // hasn't figured that out yet. Do nothing, we will see - // this in a future tick. - } - } else if (emulator.isReading() && (shell != null)) { - // The shell might be dead, let's check - try { - int rc = shell.exitValue(); - // If we got here, the shell died. - setTitle(String.format("%s [Completed - %d]", - getTitle(), rc)); - shell = null; - emulator.close(); - clearShortcutKeypresses(); - } catch (IllegalThreadStateException e) { - // The shell is still running, do nothing. - } - } - - } // synchronized (emulator) - } - /** * Handle window/screen resize events. * @@ -399,10 +426,19 @@ public class TTerminalWindow extends TWindow { if (resize.getType() == TResizeEvent.Type.WIDGET) { // Resize the scroll bars - reflow(); + reflowData(); + placeScrollbars(); // Get out of scrollback - vScroller.setValue(0); + setVerticalValue(0); + + if (ptypipe) { + emulator.setWidth(getWidth() - 2); + emulator.setHeight(getHeight() - 2); + + emulator.writeRemote("\033[8;" + (getHeight() - 2) + ";" + + (getWidth() - 2) + "t"); + } } return; @@ -412,7 +448,8 @@ public class TTerminalWindow extends TWindow { /** * Resize scrollbars for a new width/height. */ - private void reflow() { + @Override + public void reflowData() { // Synchronize against the emulator so we don't stomp on its reader // thread. @@ -422,48 +459,14 @@ public class TTerminalWindow extends TWindow { readEmulatorState(); // Vertical scrollbar - if (vScroller == null) { - vScroller = new TVScroller(this, getWidth() - 2, 0, - getHeight() - 2); - vScroller.setBottomValue(0); - vScroller.setValue(0); - } else { - vScroller.setX(getWidth() - 2); - vScroller.setHeight(getHeight() - 2); - } - vScroller.setTopValue(getHeight() - 2 + setTopValue(getHeight() - 2 - (emulator.getScrollbackBuffer().size() + emulator.getDisplayBuffer().size())); - vScroller.setBigChange(getHeight() - 2); + setVerticalBigChange(getHeight() - 2); } // synchronized (emulator) } - /** - * Check if a mouse press/release/motion event coordinate is over the - * emulator. - * - * @param mouse a mouse-based event - * @return whether or not the mouse is on the emulator - */ - private final boolean mouseOnEmulator(final TMouseEvent mouse) { - - synchronized (emulator) { - if (!emulator.isReading()) { - return false; - } - } - - if ((mouse.getAbsoluteX() >= getAbsoluteX() + 1) - && (mouse.getAbsoluteX() < getAbsoluteX() + getWidth() - 1) - && (mouse.getAbsoluteY() >= getAbsoluteY() + 1) - && (mouse.getAbsoluteY() < getAbsoluteY() + getHeight() - 1) - ) { - return true; - } - return false; - } - /** * Handle keystrokes. * @@ -477,14 +480,14 @@ public class TTerminalWindow extends TWindow { || keypress.equals(kbCtrlPgUp) || keypress.equals(kbAltPgUp) ) { - vScroller.bigDecrement(); + bigVerticalDecrement(); return; } if (keypress.equals(kbShiftPgDn) || keypress.equals(kbCtrlPgDn) || keypress.equals(kbAltPgDn) ) { - vScroller.bigIncrement(); + bigVerticalIncrement(); return; } @@ -493,7 +496,7 @@ public class TTerminalWindow extends TWindow { synchronized (emulator) { if (emulator.isReading()) { // Get out of scrollback - vScroller.setValue(0); + setVerticalValue(0); emulator.keypress(keypress.getKey()); // UGLY HACK TIME! cmd.exe needs CRLF, not just CR, so if @@ -526,13 +529,17 @@ public class TTerminalWindow extends TWindow { return; } - if (mouse.isMouseWheelUp()) { - vScroller.decrement(); - return; - } - if (mouse.isMouseWheelDown()) { - vScroller.increment(); - return; + // If the emulator is tracking mouse buttons, it needs to see wheel + // events. + if (emulator.getMouseProtocol() == ECMA48.MouseProtocol.OFF) { + if (mouse.isMouseWheelUp()) { + verticalDecrement(); + return; + } + if (mouse.isMouseWheelDown()) { + verticalIncrement(); + return; + } } if (mouseOnEmulator(mouse)) { synchronized (emulator) { @@ -602,4 +609,448 @@ public class TTerminalWindow extends TWindow { super.onMouseMotion(mouse); } + // ------------------------------------------------------------------------ + // TTerminalWindow -------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Claim the keystrokes the emulator will need. + */ + private void addShortcutKeys() { + addShortcutKeypress(kbCtrlA); + addShortcutKeypress(kbCtrlB); + addShortcutKeypress(kbCtrlC); + addShortcutKeypress(kbCtrlD); + addShortcutKeypress(kbCtrlE); + addShortcutKeypress(kbCtrlF); + addShortcutKeypress(kbCtrlG); + addShortcutKeypress(kbCtrlH); + addShortcutKeypress(kbCtrlU); + addShortcutKeypress(kbCtrlJ); + addShortcutKeypress(kbCtrlK); + addShortcutKeypress(kbCtrlL); + addShortcutKeypress(kbCtrlM); + addShortcutKeypress(kbCtrlN); + addShortcutKeypress(kbCtrlO); + addShortcutKeypress(kbCtrlP); + addShortcutKeypress(kbCtrlQ); + addShortcutKeypress(kbCtrlR); + addShortcutKeypress(kbCtrlS); + addShortcutKeypress(kbCtrlT); + addShortcutKeypress(kbCtrlU); + addShortcutKeypress(kbCtrlV); + addShortcutKeypress(kbCtrlW); + addShortcutKeypress(kbCtrlX); + addShortcutKeypress(kbCtrlY); + addShortcutKeypress(kbCtrlZ); + addShortcutKeypress(kbF1); + addShortcutKeypress(kbF2); + addShortcutKeypress(kbF3); + addShortcutKeypress(kbF4); + addShortcutKeypress(kbF5); + addShortcutKeypress(kbF6); + addShortcutKeypress(kbF7); + addShortcutKeypress(kbF8); + addShortcutKeypress(kbF9); + addShortcutKeypress(kbF10); + addShortcutKeypress(kbF11); + addShortcutKeypress(kbF12); + addShortcutKeypress(kbAltA); + addShortcutKeypress(kbAltB); + addShortcutKeypress(kbAltC); + addShortcutKeypress(kbAltD); + addShortcutKeypress(kbAltE); + addShortcutKeypress(kbAltF); + addShortcutKeypress(kbAltG); + addShortcutKeypress(kbAltH); + addShortcutKeypress(kbAltU); + addShortcutKeypress(kbAltJ); + addShortcutKeypress(kbAltK); + addShortcutKeypress(kbAltL); + addShortcutKeypress(kbAltM); + addShortcutKeypress(kbAltN); + addShortcutKeypress(kbAltO); + addShortcutKeypress(kbAltP); + addShortcutKeypress(kbAltQ); + addShortcutKeypress(kbAltR); + addShortcutKeypress(kbAltS); + addShortcutKeypress(kbAltT); + addShortcutKeypress(kbAltU); + addShortcutKeypress(kbAltV); + addShortcutKeypress(kbAltW); + addShortcutKeypress(kbAltX); + addShortcutKeypress(kbAltY); + addShortcutKeypress(kbAltZ); + } + + /** + * Convert a string array to a whitespace-separated string. + * + * @param array the string array + * @return a single string + */ + private String stringArrayToString(final String [] array) { + StringBuilder sb = new StringBuilder(array[0].length()); + for (int i = 0; i < array.length; i++) { + sb.append(array[i]); + if (i < array.length - 1) { + sb.append(' '); + } + } + return sb.toString(); + } + + /** + * Spawn the shell. + * + * @param command the command line to execute + */ + private void spawnShell(final String [] command) { + + /* + System.err.printf("spawnShell(): '%s'\n", + stringArrayToString(command)); + */ + + vScroller = new TVScroller(this, getWidth() - 2, 0, getHeight() - 2); + setBottomValue(0); + + // Assume XTERM + ECMA48.DeviceType deviceType = ECMA48.DeviceType.XTERM; + + try { + ProcessBuilder pb = new ProcessBuilder(command); + Map env = pb.environment(); + env.put("TERM", ECMA48.deviceTypeTerm(deviceType)); + env.put("LANG", ECMA48.deviceTypeLang(deviceType, "en")); + env.put("COLUMNS", "80"); + env.put("LINES", "24"); + pb.redirectErrorStream(true); + shell = pb.start(); + emulator = new ECMA48(deviceType, shell.getInputStream(), + shell.getOutputStream(), this); + } catch (IOException e) { + messageBox(i18n.getString("errorLaunchingShellTitle"), + MessageFormat.format(i18n.getString("errorLaunchingShellText"), + e.getMessage())); + } + + // Setup the scroll bars + onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(), + getHeight())); + + // Claim the keystrokes the emulator will need. + addShortcutKeys(); + + // Add shortcut text + newStatusBar(i18n.getString("statusBarRunning")); + + // Pass the correct text cell width/height to the emulator + emulator.setTextWidth(getScreen().getTextWidth()); + emulator.setTextHeight(getScreen().getTextHeight()); + } + + /** + * Terminate the child of the 'script' process used on POSIX. This may + * or may not work. + */ + private void terminateShellChildProcess() { + int pid = -1; + if (shell.getClass().getName().equals("java.lang.UNIXProcess")) { + /* get the PID on unix/linux systems */ + try { + Field field = shell.getClass().getDeclaredField("pid"); + field.setAccessible(true); + pid = field.getInt(shell); + } catch (Throwable e) { + // SQUASH, this didn't work. Just bail out quietly. + return; + } + } + if (pid != -1) { + // shell.destroy() works successfully at killing this side of + // 'script'. But we need to make sure the other side (child + // process) is also killed. + String [] cmdKillIt = { + "pkill", "-P", Integer.toString(pid) + }; + try { + Runtime.getRuntime().exec(cmdKillIt); + } catch (Throwable e) { + // SQUASH, this didn't work. Just bail out quietly. + return; + } + } + } + + /** + * Called by emulator when fresh data has come in. + */ + public void displayChanged() { + getApplication().postEvent(new TMenuEvent(TMenu.MID_REPAINT)); + } + + /** + * Function to call to obtain the display width. + * + * @return the number of columns in the display + */ + public int getDisplayWidth() { + if (ptypipe) { + return getWidth() - 2; + } + return 80; + } + + /** + * Function to call to obtain the display height. + * + * @return the number of rows in the display + */ + public int getDisplayHeight() { + if (ptypipe) { + return getHeight() - 2; + } + return 24; + } + + /** + * Hook for subclasses to be notified of the shell termination. + */ + public void onShellExit() { + if (closeOnExit) { + close(); + } + getApplication().postEvent(new TMenuEvent(TMenu.MID_REPAINT)); + } + + /** + * Copy out variables from the emulator that TTerminal has to expose on + * screen. + */ + private void readEmulatorState() { + // Synchronize against the emulator so we don't stomp on its reader + // thread. + synchronized (emulator) { + setHiddenMouse(emulator.hasHiddenMousePointer()); + + setCursorX(emulator.getCursorX() + 1); + setCursorY(emulator.getCursorY() + 1 + + (getHeight() - 2 - emulator.getHeight()) + - getVerticalValue()); + setCursorVisible(emulator.isCursorVisible()); + if (getCursorX() > getWidth() - 2) { + setCursorVisible(false); + } + if ((getCursorY() > getHeight() - 2) || (getCursorY() < 0)) { + setCursorVisible(false); + } + if (emulator.getScreenTitle().length() > 0) { + // Only update the title if the shell is still alive + if (shell != null) { + setTitle(emulator.getScreenTitle()); + } + } + + // Check to see if the shell has died. + if (!emulator.isReading() && (shell != null)) { + try { + int rc = shell.exitValue(); + // The emulator exited on its own, all is fine + setTitle(MessageFormat.format(i18n. + getString("windowTitleCompleted"), getTitle(), rc)); + shell = null; + emulator.close(); + clearShortcutKeypresses(); + statusBar.setText(MessageFormat.format(i18n. + getString("statusBarCompleted"), rc)); + onShellExit(); + } catch (IllegalThreadStateException e) { + // The emulator thread has exited, but the shell Process + // hasn't figured that out yet. Do nothing, we will see + // this in a future tick. + } + } else if (emulator.isReading() && (shell != null)) { + // The shell might be dead, let's check + try { + int rc = shell.exitValue(); + // If we got here, the shell died. + setTitle(MessageFormat.format(i18n. + getString("windowTitleCompleted"), getTitle(), rc)); + shell = null; + emulator.close(); + clearShortcutKeypresses(); + statusBar.setText(MessageFormat.format(i18n. + getString("statusBarCompleted"), rc)); + onShellExit(); + } catch (IllegalThreadStateException e) { + // The shell is still running, do nothing. + } + } + + } // synchronized (emulator) + } + + /** + * Check if a mouse press/release/motion event coordinate is over the + * emulator. + * + * @param mouse a mouse-based event + * @return whether or not the mouse is on the emulator + */ + private boolean mouseOnEmulator(final TMouseEvent mouse) { + + synchronized (emulator) { + if (!emulator.isReading()) { + return false; + } + } + + if ((mouse.getAbsoluteX() >= getAbsoluteX() + 1) + && (mouse.getAbsoluteX() < getAbsoluteX() + getWidth() - 1) + && (mouse.getAbsoluteY() >= getAbsoluteY() + 1) + && (mouse.getAbsoluteY() < getAbsoluteY() + getHeight() - 1) + ) { + return true; + } + return false; + } + + /** + * Draw glyphs for a double-width or double-height VT100 cell to two + * screen cells. + * + * @param line the line this VT100 cell is in + * @param x the X position to draw the left half to + * @param y the Y position to draw to + * @param cell the cell to draw + */ + private void putDoubleWidthCharXY(final DisplayLine line, final int x, + final int y, final Cell cell) { + + int textWidth = getScreen().getTextWidth(); + int textHeight = getScreen().getTextHeight(); + boolean cursorBlinkVisible = true; + + if (getScreen() instanceof SwingTerminal) { + SwingTerminal terminal = (SwingTerminal) getScreen(); + cursorBlinkVisible = terminal.getCursorBlinkVisible(); + } else if (getScreen() instanceof ECMA48Terminal) { + ECMA48Terminal terminal = (ECMA48Terminal) getScreen(); + + if (!terminal.hasSixel()) { + // The backend does not have sixel support, draw this as text + // and bail out. + putCharXY(x, y, cell); + putCharXY(x + 1, y, ' ', cell); + return; + } + cursorBlinkVisible = blinkState; + } else { + // We don't know how to dray glyphs to this screen, draw them as + // text and bail out. + putCharXY(x, y, cell); + putCharXY(x + 1, y, ' ', cell); + return; + } + + if ((textWidth != lastTextWidth) || (textHeight != lastTextHeight)) { + // Screen size has changed, reset the font. + setupFont(textHeight); + lastTextWidth = textWidth; + lastTextHeight = textHeight; + } + assert (doubleFont != null); + + BufferedImage image; + if (line.getDoubleHeight() == 1) { + // Double-height top half: don't draw the underline. + Cell newCell = new Cell(); + newCell.setTo(cell); + newCell.setUnderline(false); + image = doubleFont.getImage(newCell, textWidth * 2, textHeight * 2, + cursorBlinkVisible); + } else { + image = doubleFont.getImage(cell, textWidth * 2, textHeight * 2, + cursorBlinkVisible); + } + + // Now that we have the double-wide glyph drawn, copy the right + // pieces of it to the cells. + Cell left = new Cell(); + Cell right = new Cell(); + left.setTo(cell); + right.setTo(cell); + right.setChar(' '); + BufferedImage leftImage = null; + BufferedImage rightImage = null; + /* + System.err.println("image " + image + " textWidth " + textWidth + + " textHeight " + textHeight); + */ + + switch (line.getDoubleHeight()) { + case 1: + // Top half double height + leftImage = image.getSubimage(0, 0, textWidth, textHeight); + rightImage = image.getSubimage(textWidth, 0, textWidth, textHeight); + break; + case 2: + // Bottom half double height + leftImage = image.getSubimage(0, textHeight, textWidth, textHeight); + rightImage = image.getSubimage(textWidth, textHeight, + textWidth, textHeight); + break; + default: + // Either single height double-width, or error fallback + BufferedImage wideImage = new BufferedImage(textWidth * 2, + textHeight, BufferedImage.TYPE_INT_ARGB); + Graphics2D grWide = wideImage.createGraphics(); + grWide.drawImage(image, 0, 0, wideImage.getWidth(), + wideImage.getHeight(), null); + grWide.dispose(); + leftImage = wideImage.getSubimage(0, 0, textWidth, textHeight); + rightImage = wideImage.getSubimage(textWidth, 0, textWidth, + textHeight); + break; + } + left.setImage(leftImage); + right.setImage(rightImage); + // Since we have image data, ditch the character here. Otherwise, a + // drawBoxShadow() over the terminal window will show the characters + // which looks wrong. + left.setChar(' '); + right.setChar(' '); + putCharXY(x, y, left); + putCharXY(x + 1, y, right); + } + + /** + * Set up the double-width font. + * + * @param fontSize the size of font to request for the single-width font. + * The double-width font will be 2x this value. + */ + private void setupFont(final int fontSize) { + doubleFont = GlyphMaker.getDefault().size(fontSize * 2); + + // Special case: the ECMA48 backend needs to have a timer to drive + // its blink state. + if (getScreen() instanceof jexer.backend.ECMA48Terminal) { + if (!haveTimer) { + // Blink every 500 millis. + long millis = 500; + getApplication().addTimer(millis, true, + new TAction() { + public void DO() { + blinkState = !blinkState; + getApplication().doRepaint(); + } + } + ); + haveTimer = true; + } + } + } + }