X-Git-Url: http://git.nikiroo.be/?a=blobdiff_plain;f=src%2Fjexer%2FTTerminalWindow.java;h=d1853310349b80c6646d06b6b32e12ea72260cee;hb=9d990083da421ec7151d7462afa96305273af483;hp=828a35310c6f6a78cac64594438e8911f1a9e7f6;hpb=a0d734e68fc28e441d74075e4d8d0166bbcde180;p=fanfix.git diff --git a/src/jexer/TTerminalWindow.java b/src/jexer/TTerminalWindow.java index 828a353..d185331 100644 --- a/src/jexer/TTerminalWindow.java +++ b/src/jexer/TTerminalWindow.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 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,14 +28,24 @@ */ 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.lang.reflect.Field; import java.text.MessageFormat; -import java.util.LinkedList; +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.MultiScreen; +import jexer.backend.SwingTerminal; import jexer.bits.Cell; import jexer.bits.CellAttributes; import jexer.event.TKeypressEvent; @@ -54,12 +64,15 @@ import static jexer.TKeypress.*; public class TTerminalWindow extends TScrollableWindow implements DisplayListener { - /** * Translated strings. */ private static final ResourceBundle i18n = ResourceBundle.getBundle(TTerminalWindow.class.getName()); + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * The emulator. */ @@ -73,142 +86,67 @@ public class TTerminalWindow extends TScrollableWindow /** * If true, we are using the ptypipe utility to support dynamic window * resizing. ptypipe is available at - * https://github.com/klamonte/ptypipe . + * https://gitlab.com/klamonte/ptypipe . */ 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; /** - * Convert a string array to a whitespace-separated string. - * - * @param array the string array - * @return a single string + * System-dependent Y adjustment for text in the character cell + * (double-height). */ - 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(); - } + private int doubleTextAdjustY = 0; /** - * Spawn the shell. - * - * @param command the command line to execute + * System-dependent X adjustment for text in the character cell + * (double-height). */ - private void spawnShell(final String [] command) { + private int doubleTextAdjustX = 0; - /* - System.err.printf("spawnShell(): '%s'\n", - stringArrayToString(command)); - */ + /** + * Descent of a character cell in pixels (double-height). + */ + private int doubleMaxDescent = 0; - vScroller = new TVScroller(this, getWidth() - 2, 0, getHeight() - 2); - setBottomValue(0); + /** + * Double-width font. + */ + private Font doubleFont = null; - // Assume XTERM - ECMA48.DeviceType deviceType = ECMA48.DeviceType.XTERM; + /** + * Last text width value. + */ + private int lastTextWidth = -1; - 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())); - } + /** + * Last text height value. + */ + private int lastTextHeight = -1; - // Setup the scroll bars - onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(), - getHeight())); + /** + * A cache of previously-rendered double-width glyphs. + */ + private Map glyphCache; - // Claim the keystrokes the emulator will need. - addShortcutKeys(); + /** + * A cache of previously-rendered double-width glyphs for blinking text, + * when it is not visible. + */ + private Map glyphCacheBlink; - // Add shortcut text - newStatusBar(i18n.getString("statusBarRunning")); - } + /** + * The blink state, used only by ECMA48 backend and when double-width + * chars must be drawn. + */ + private boolean blinkState = true; + + // ------------------------------------------------------------------------ + // Constructors ----------------------------------------------------------- + // ------------------------------------------------------------------------ /** * Public constructor spawns a custom command line. @@ -221,7 +159,25 @@ public class TTerminalWindow extends TScrollableWindow public TTerminalWindow(final TApplication application, final int x, final int y, final String commandLine) { - this(application, x, y, RESIZABLE, commandLine.split("\\s")); + this(application, x, y, RESIZABLE, commandLine.split("\\s+"), + System.getProperty("jexer.TTerminal.closeOnExit", + "false").equals("true")); + } + + /** + * 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); } /** @@ -236,9 +192,30 @@ public class TTerminalWindow extends TScrollableWindow public TTerminalWindow(final TApplication application, final int x, final int y, final int flags, final String [] command) { + this(application, x, y, flags, command, + System.getProperty("jexer.TTerminal.closeOnExit", + "false").equals("true")); + } + + /** + * 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) { + super(application, i18n.getString("windowTitle"), x, y, 80 + 2, 24 + 2, flags); + this.closeOnExit = closeOnExit; + String [] fullCommand; // Spawn a shell and pass its I/O to the other constructor. @@ -286,9 +263,29 @@ public class TTerminalWindow extends TScrollableWindow 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")); + + } + + /** + * 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 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 boolean closeOnExit) { + super(application, i18n.getString("windowTitle"), x, y, 80 + 2, 24 + 2, flags); + this.closeOnExit = closeOnExit; + String cmdShellWindows = "cmd.exe"; // You cannot run a login shell in a bare Process interactively, due @@ -308,51 +305,22 @@ public class TTerminalWindow extends TScrollableWindow equals("true")) ) { ptypipe = true; - spawnShell(cmdShellPtypipe.split("\\s")); + spawnShell(cmdShellPtypipe.split("\\s+")); } else if (System.getProperty("os.name").startsWith("Windows")) { - spawnShell(cmdShellWindows.split("\\s")); + spawnShell(cmdShellWindows.split("\\s+")); } else if (System.getProperty("os.name").startsWith("Mac")) { - spawnShell(cmdShellBSD.split("\\s")); + spawnShell(cmdShellBSD.split("\\s+")); } else if (System.getProperty("os.name").startsWith("Linux")) { - spawnShell(cmdShellGNU.split("\\s")); + spawnShell(cmdShellGNU.split("\\s+")); } else { // When all else fails, assume GNU. - spawnShell(cmdShellGNU.split("\\s")); + spawnShell(cmdShellGNU.split("\\s+")); } } - /** - * 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; - } - } - } + // ------------------------------------------------------------------------ + // TScrollableWindow ------------------------------------------------------ + // ------------------------------------------------------------------------ /** * Draw the display buffer. @@ -378,7 +346,7 @@ public class TTerminalWindow extends TScrollableWindow + 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++) { @@ -388,11 +356,11 @@ public class TTerminalWindow extends TScrollableWindow } 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)); @@ -417,14 +385,23 @@ public class TTerminalWindow extends TScrollableWindow 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++; @@ -436,45 +413,13 @@ public class TTerminalWindow extends TScrollableWindow 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) } - /** - * 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; - } - /** * Handle window close. */ @@ -489,85 +434,12 @@ public class TTerminalWindow extends TScrollableWindow } /** - * Hook for subclasses to be notified of the shell termination. + * Handle window/screen resize events. + * + * @param resize resize event */ - public void onShellExit() { - 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) { - - 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) - } - - /** - * Handle window/screen resize events. - * - * @param resize resize event - */ - @Override - public void onResize(final TResizeEvent resize) { + @Override + public void onResize(final TResizeEvent resize) { // Synchronize against the emulator so we don't stomp on its reader // thread. @@ -616,31 +488,6 @@ public class TTerminalWindow extends TScrollableWindow } // 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. * @@ -703,13 +550,17 @@ public class TTerminalWindow extends TScrollableWindow return; } - if (mouse.isMouseWheelUp()) { - verticalDecrement(); - return; - } - if (mouse.isMouseWheelDown()) { - verticalIncrement(); - 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) { @@ -779,4 +630,492 @@ public class TTerminalWindow extends TScrollableWindow 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 all fonts. + setupFonts(textHeight); + lastTextWidth = textWidth; + lastTextHeight = textHeight; + } + assert (doubleFont != null); + + BufferedImage image = null; + if (cell.isBlink() && !cursorBlinkVisible) { + image = glyphCacheBlink.get(cell); + } else { + image = glyphCache.get(cell); + } + if (image == null) { + // Generate glyph and draw it to an image. + image = new BufferedImage(textWidth * 2, textHeight * 2, + BufferedImage.TYPE_INT_ARGB); + Graphics2D gr2 = image.createGraphics(); + gr2.setFont(doubleFont); + + // Draw the background rectangle, then the foreground character. + gr2.setColor(SwingTerminal.attrToBackgroundColor(cell)); + gr2.fillRect(0, 0, image.getWidth(), image.getHeight()); + if (!cell.isBlink() + || (cell.isBlink() && cursorBlinkVisible) + ) { + gr2.setColor(SwingTerminal.attrToForegroundColor(cell)); + char [] chars = new char[1]; + chars[0] = cell.getChar(); + gr2.drawChars(chars, 0, 1, doubleTextAdjustX, + (textHeight * 2) - doubleMaxDescent + doubleTextAdjustY); + + if (cell.isUnderline() && (line.getDoubleHeight() != 1)) { + gr2.fillRect(0, textHeight - 2, textWidth, 2); + } + } + gr2.dispose(); + + // Now save this generated image, using a new key that will not + // be mutated by invertCell(). + Cell key = new Cell(); + key.setTo(cell); + if (cell.isBlink() && !cursorBlinkVisible) { + glyphCacheBlink.put(key, image); + } else { + glyphCache.put(key, image); + } + } + + // 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; + 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); + putCharXY(x, y, left); + putCharXY(x + 1, y, right); + } + + /** + * Set up the single and double-width fonts. + * + * @param fontSize the size of font to request for the single-width font. + * The double-width font will be 2x this value. + */ + private void setupFonts(final int fontSize) { + try { + ClassLoader loader = Thread.currentThread().getContextClassLoader(); + InputStream in = loader.getResourceAsStream(SwingTerminal.FONTFILE); + Font terminusRoot = Font.createFont(Font.TRUETYPE_FONT, in); + Font terminusDouble = terminusRoot.deriveFont(Font.PLAIN, + fontSize * 2); + doubleFont = terminusDouble; + } catch (java.awt.FontFormatException e) { + new TExceptionDialog(getApplication(), e); + doubleFont = new Font(Font.MONOSPACED, Font.PLAIN, fontSize * 2); + } catch (java.io.IOException e) { + new TExceptionDialog(getApplication(), e); + doubleFont = new Font(Font.MONOSPACED, Font.PLAIN, fontSize * 2); + } + + // Get font descent. + BufferedImage image = new BufferedImage(fontSize * 10, fontSize * 10, + BufferedImage.TYPE_INT_ARGB); + Graphics2D gr = image.createGraphics(); + gr.setFont(doubleFont); + FontMetrics fm = gr.getFontMetrics(); + doubleMaxDescent = fm.getMaxDescent(); + + gr.dispose(); + + // (Re)create the glyph caches. + glyphCache = new HashMap(); + glyphCacheBlink = new HashMap(); + + // Special case: the ECMA48 backend needs to have a timer to drive + // its blink state. + if (getScreen() instanceof jexer.backend.ECMA48Terminal) { + // Blink every 500 millis. + long millis = 500; + getApplication().addTimer(millis, true, + new TAction() { + public void DO() { + blinkState = !blinkState; + getApplication().doRepaint(); + } + } + ); + } + + } + }