X-Git-Url: http://git.nikiroo.be/?a=blobdiff_plain;f=src%2Fjexer%2FTTerminalWindow.java;h=5970b6c77aff4791a389d13a7aa9a1012dbf5147;hb=ab215e38e98a76ad189ba537e37c420ae515e4c0;hp=c0af74bafa12ce9046277f0eab0746c7571bee43;hpb=55d2b2c2b29ce51f4f910448a115073371deeae8;p=fanfix.git diff --git a/src/jexer/TTerminalWindow.java b/src/jexer/TTerminalWindow.java index c0af74b..5970b6c 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,43 +28,629 @@ */ 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.lang.reflect.Field; -import java.util.LinkedList; +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 { +/** + * TTerminalWindow exposes a ECMA-48 / ANSI X3.64 style terminal in a window. + */ +public class TTerminalWindow extends TScrollableWindow + implements DisplayListener { + + /** + * Translated strings. + */ + private static final ResourceBundle i18n = ResourceBundle.getBundle(TTerminalWindow.class.getName()); + + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * The emulator. + */ + private ECMA48 emulator; + + /** + * The Process created by the shell spawning constructor. + */ + private Process shell; + + /** + * If true, we are using the ptypipe utility to support dynamic window + * resizing. ptypipe is available at + * https://gitlab.com/klamonte/ptypipe . + */ + private boolean ptypipe = false; + + /** + * If true, close the window when the shell exits. + */ + 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; + + /** + * The last seen scrollback lines. + */ + private List scrollback; + + /** + * The last seen display lines. + */ + private List display; + + /** + * If true, the display has changed and needs updating. + */ + private volatile boolean dirty = true; + + /** + * Time that the display was last updated. + */ + private long lastUpdateTime = 0; + + // ------------------------------------------------------------------------ + // 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 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 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. + 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); + } + + /** + * 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")); + + } + + /** + * 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 + // 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. + */ + @Override + public void draw() { + + int width = getDisplayWidth(); + boolean syncEmulator = false; + if ((System.currentTimeMillis() - lastUpdateTime > 125) + && (dirty == true) + ) { + // Too much time has passed, draw it all. + syncEmulator = true; + } else if (emulator.isReading() && (dirty == false)) { + // Wait until the emulator has brought more data in. + syncEmulator = false; + } else if (!emulator.isReading() && (dirty == true)) { + // The emulator won't receive more data, update the display. + syncEmulator = true; + } + + if ((syncEmulator == true) + || (scrollback == null) + || (display == null) + ) { + // We want to minimize the amount of time we have the emulator + // locked. Grab a copy of its display. + synchronized (emulator) { + // Update the scroll bars + reflowData(); + + if ((scrollback == null) || emulator.isReading()) { + scrollback = copyBuffer(emulator.getScrollbackBuffer()); + display = copyBuffer(emulator.getDisplayBuffer()); + } + width = emulator.getWidth(); + } + dirty = false; + } + + // Draw the box using my superclass + super.draw(); + + // Put together the visible rows + int visibleHeight = getHeight() - 2; + int visibleBottom = scrollback.size() + display.size() + + getVerticalValue(); + assert (visibleBottom >= 0); + + List preceedingBlankLines = new ArrayList(); + int visibleTop = visibleBottom - visibleHeight; + if (visibleTop < 0) { + for (int i = visibleTop; i < 0; i++) { + preceedingBlankLines.add(emulator.getBlankDisplayLine()); + } + visibleTop = 0; + } + assert (visibleTop >= 0); + + List displayLines = new ArrayList(); + displayLines.addAll(scrollback); + displayLines.addAll(display); + + List visibleLines = new ArrayList(); + visibleLines.addAll(preceedingBlankLines); + visibleLines.addAll(displayLines.subList(visibleTop, + visibleBottom)); + + visibleHeight -= visibleLines.size(); + assert (visibleHeight >= 0); + + // Now draw the emulator screen + int row = 1; + for (DisplayLine line: visibleLines) { + int widthMax = width; + if (line.isDoubleWidth()) { + widthMax /= 2; + } + if (widthMax > getWidth() - 2) { + widthMax = getWidth() - 2; + } + for (int i = 0; i < widthMax; i++) { + Cell ch = line.charAt(i); + + if (ch.isImage()) { + putCharXY(i + 1, row, ch); + continue; + } + + Cell newCell = new Cell(); + newCell.setTo(ch); + boolean reverse = line.isReverseColor() ^ ch.isReverse(); + newCell.setReverse(false); + if (reverse) { + 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()) { + putDoubleWidthCharXY(line, (i * 2) + 1, row, newCell); + } else { + putCharXY(i + 1, row, newCell); + } + } + row++; + if (row == getHeight() - 1) { + // Don't overwrite the box edge + break; + } + } + CellAttributes background = new CellAttributes(); + // Fill in the blank lines on bottom + for (int i = 0; i < visibleHeight; i++) { + hLineXY(1, i + row, getWidth() - 2, ' ', background); + } + + } + + /** + * Handle window close. + */ + @Override + public void onClose() { + emulator.close(); + if (shell != null) { + terminateShellChildProcess(); + shell.destroy(); + shell = null; + } + } + + /** + * Handle window/screen resize events. + * + * @param resize resize event + */ + @Override + public void onResize(final TResizeEvent resize) { + + // Synchronize against the emulator so we don't stomp on its reader + // thread. + synchronized (emulator) { + + if (resize.getType() == TResizeEvent.Type.WIDGET) { + // Resize the scroll bars + reflowData(); + placeScrollbars(); + + // Get out of scrollback + setVerticalValue(0); + + if (ptypipe) { + emulator.setWidth(getWidth() - 2); + emulator.setHeight(getHeight() - 2); + + emulator.writeRemote("\033[8;" + (getHeight() - 2) + ";" + + (getWidth() - 2) + "t"); + } + } + return; + + } // synchronized (emulator) + } + + /** + * Resize scrollbars for a new width/height. + */ + @Override + public void reflowData() { + + // Synchronize against the emulator so we don't stomp on its reader + // thread. + synchronized (emulator) { + + // Pull cursor information + readEmulatorState(); + + // Vertical scrollbar + setTopValue(getHeight() - 2 + - (emulator.getScrollbackBuffer().size() + + emulator.getDisplayBuffer().size())); + setVerticalBigChange(getHeight() - 2); + + } // synchronized (emulator) + } + + /** + * Handle keystrokes. + * + * @param keypress keystroke event + */ + @Override + public void onKeypress(final TKeypressEvent keypress) { + + // Scrollback up/down + if (keypress.equals(kbShiftPgUp) + || keypress.equals(kbCtrlPgUp) + || keypress.equals(kbAltPgUp) + ) { + bigVerticalDecrement(); + return; + } + if (keypress.equals(kbShiftPgDn) + || keypress.equals(kbCtrlPgDn) + || keypress.equals(kbAltPgDn) + ) { + bigVerticalIncrement(); + return; + } + + if (emulator.isReading()) { + // Get out of scrollback + setVerticalValue(0); + emulator.addUserEvent(keypress); + + // UGLY HACK TIME! cmd.exe needs CRLF, not just CR, so if + // this is kBEnter then also send kbCtrlJ. + if (System.getProperty("os.name").startsWith("Windows")) { + if (keypress.equals(kbEnter)) { + emulator.addUserEvent(new TKeypressEvent(kbCtrlJ)); + } + } + + readEmulatorState(); + return; + } + + // Process is closed, honor "normal" TUI keystrokes + super.onKeypress(keypress); + } + + /** + * Handle mouse press events. + * + * @param mouse mouse button press event + */ + @Override + public void onMouseDown(final TMouseEvent mouse) { + if (inWindowMove || inWindowResize) { + // TWindow needs to deal with this. + super.onMouseDown(mouse); + 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)) { + mouse.setX(mouse.getX() - 1); + mouse.setY(mouse.getY() - 1); + emulator.addUserEvent(mouse); + readEmulatorState(); + return; + } + + // Emulator didn't consume it, pass it on + super.onMouseDown(mouse); + } + + /** + * Handle mouse release events. + * + * @param mouse mouse button release event + */ + @Override + public void onMouseUp(final TMouseEvent mouse) { + if (inWindowMove || inWindowResize) { + // TWindow needs to deal with this. + super.onMouseUp(mouse); + return; + } + + if (mouseOnEmulator(mouse)) { + mouse.setX(mouse.getX() - 1); + mouse.setY(mouse.getY() - 1); + emulator.addUserEvent(mouse); + readEmulatorState(); + return; + } - /** - * The emulator. - */ - private ECMA48 emulator; + // Emulator didn't consume it, pass it on + super.onMouseUp(mouse); + } /** - * The Process created by the shell spawning constructor. + * Handle mouse motion events. + * + * @param mouse mouse motion event */ - private Process shell; + @Override + public void onMouseMotion(final TMouseEvent mouse) { + if (inWindowMove || inWindowResize) { + // TWindow needs to deal with this. + super.onMouseMotion(mouse); + return; + } - /** - * Vertical scrollbar. - */ - private TVScroller vScroller; + if (mouseOnEmulator(mouse)) { + mouse.setX(mouse.getX() - 1); + mouse.setY(mouse.getY() - 1); + emulator.addUserEvent(mouse); + readEmulatorState(); + return; + } + + // Emulator didn't consume it, pass it on + super.onMouseMotion(mouse); + } + + // ------------------------------------------------------------------------ + // TTerminalWindow -------------------------------------------------------- + // ------------------------------------------------------------------------ /** * Claim the keystrokes the emulator will need. @@ -137,50 +723,42 @@ public class TTerminalWindow extends TWindow { } /** - * Public constructor spawns a shell. + * Convert a string array to a whitespace-separated string. * - * @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 array the string array + * @return a single string */ - public TTerminalWindow(final TApplication application, final int x, - final int y, final int flags) { + 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(); + } - super(application, "Terminal", x, y, 80 + 2, 24 + 2, flags); + /** + * 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 { - String [] cmdShellWindows = { - "cmd.exe" - }; - - // 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", "-qe", "-F", "/dev/null" - }; - // Spawn a shell and pass its I/O to the other constructor. - - ProcessBuilder pb; - if (System.getProperty("os.name").startsWith("Windows")) { - pb = new ProcessBuilder(cmdShellWindows); - } else if (System.getProperty("os.name").startsWith("Mac")) { - pb = new ProcessBuilder(cmdShellBSD); - } else if (System.getProperty("os.name").startsWith("Linux")) { - pb = new ProcessBuilder(cmdShellGNU); - } else { - // When all else fails, assume GNU. - pb = new ProcessBuilder(cmdShellGNU); - } + ProcessBuilder pb = new ProcessBuilder(command); Map env = pb.environment(); env.put("TERM", ECMA48.deviceTypeTerm(deviceType)); env.put("LANG", ECMA48.deviceTypeLang(deviceType, "en")); @@ -189,9 +767,11 @@ public class TTerminalWindow extends TWindow { pb.redirectErrorStream(true); shell = pb.start(); emulator = new ECMA48(deviceType, shell.getInputStream(), - shell.getOutputStream()); + shell.getOutputStream(), this); } catch (IOException e) { - e.printStackTrace(); + messageBox(i18n.getString("errorLaunchingShellTitle"), + MessageFormat.format(i18n.getString("errorLaunchingShellText"), + e.getMessage())); } // Setup the scroll bars @@ -200,6 +780,13 @@ public class TTerminalWindow extends TWindow { // 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()); } /** @@ -236,135 +823,13 @@ public class TTerminalWindow extends TWindow { } /** - * Public constructor. - * - * @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 - */ - public TTerminalWindow(final TApplication application, final int x, - final int y, final int flags, final InputStream input, - final OutputStream output) throws UnsupportedEncodingException { - - super(application, "Terminal", x, y, 80 + 2, 24 + 2, flags); - - emulator = new ECMA48(ECMA48.DeviceType.XTERM, input, output); - - // Setup the scroll bars - onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(), - getHeight())); - - // Claim the keystrokes the emulator will need. - addShortcutKeys(); - } - - /** - * Draw the display buffer. - */ - @Override - public void draw() { - // Synchronize against the emulator so we don't stomp on its reader - // thread. - synchronized (emulator) { - - // Update the scroll bars - reflow(); - - // Draw the box using my superclass - super.draw(); - - List scrollback = emulator.getScrollbackBuffer(); - List display = emulator.getDisplayBuffer(); - - // Put together the visible rows - int visibleHeight = getHeight() - 2; - int visibleBottom = scrollback.size() + display.size() - + vScroller.getValue(); - assert (visibleBottom >= 0); - - List preceedingBlankLines = new LinkedList(); - int visibleTop = visibleBottom - visibleHeight; - if (visibleTop < 0) { - for (int i = visibleTop; i < 0; i++) { - preceedingBlankLines.add(emulator.getBlankDisplayLine()); - } - visibleTop = 0; - } - assert (visibleTop >= 0); - - List displayLines = new LinkedList(); - displayLines.addAll(scrollback); - displayLines.addAll(display); - - List visibleLines = new LinkedList(); - visibleLines.addAll(preceedingBlankLines); - visibleLines.addAll(displayLines.subList(visibleTop, - visibleBottom)); - - visibleHeight -= visibleLines.size(); - assert (visibleHeight >= 0); - - // Now draw the emulator screen - int row = 1; - for (DisplayLine line: visibleLines) { - int widthMax = emulator.getWidth(); - if (line.isDoubleWidth()) { - widthMax /= 2; - } - if (widthMax > getWidth() - 2) { - widthMax = getWidth() - 2; - } - for (int i = 0; i < widthMax; i++) { - Cell ch = line.charAt(i); - Cell newCell = new Cell(); - newCell.setTo(ch); - boolean reverse = line.isReverseColor() ^ ch.isReverse(); - newCell.setReverse(false); - if (reverse) { - newCell.setBackColor(ch.getForeColor()); - newCell.setForeColor(ch.getBackColor()); - } - if (line.isDoubleWidth()) { - getScreen().putCharXY((i * 2) + 1, row, newCell); - getScreen().putCharXY((i * 2) + 2, row, ' ', newCell); - } else { - getScreen().putCharXY(i + 1, row, newCell); - } - } - row++; - if (row == getHeight() - 1) { - // Don't overwrite the box edge - break; - } - } - 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); - } - - } // synchronized (emulator) - - } - - /** - * Handle window close. + * Hook for subclasses to be notified of the shell termination. */ - @Override public void onClose() { - emulator.close(); - if (shell != null) { - terminateShellChildProcess(); - shell.destroy(); - shell = null; + public void onShellExit() { + if (closeOnExit) { + close(); } + getApplication().postEvent(new TMenuEvent(TMenu.MID_REPAINT)); } /** @@ -375,13 +840,12 @@ public class TTerminalWindow extends TWindow { // 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())); - if (vScroller != null) { - setCursorY(getCursorY() - vScroller.getValue()); - } + + (getHeight() - 2 - emulator.getHeight()) + - getVerticalValue()); setCursorVisible(emulator.isCursorVisible()); if (getCursorX() > getWidth() - 2) { setCursorVisible(false); @@ -401,11 +865,14 @@ public class TTerminalWindow extends TWindow { try { int rc = shell.exitValue(); // The emulator exited on its own, all is fine - setTitle(String.format("%s [Completed - %d]", - getTitle(), rc)); + 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 @@ -416,11 +883,14 @@ public class TTerminalWindow extends TWindow { try { int rc = shell.exitValue(); // If we got here, the shell died. - setTitle(String.format("%s [Completed - %d]", - getTitle(), rc)); + 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. } @@ -429,60 +899,6 @@ public class TTerminalWindow extends TWindow { } // synchronized (emulator) } - /** - * Handle window/screen resize events. - * - * @param resize resize event - */ - @Override - public void onResize(final TResizeEvent resize) { - - // Synchronize against the emulator so we don't stomp on its reader - // thread. - synchronized (emulator) { - - if (resize.getType() == TResizeEvent.Type.WIDGET) { - // Resize the scroll bars - reflow(); - - // Get out of scrollback - vScroller.setValue(0); - } - return; - - } // synchronized (emulator) - } - - /** - * Resize scrollbars for a new width/height. - */ - private void reflow() { - - // Synchronize against the emulator so we don't stomp on its reader - // thread. - synchronized (emulator) { - - // Pull cursor information - 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 - - (emulator.getScrollbackBuffer().size() - + emulator.getDisplayBuffer().size())); - vScroller.setBigChange(getHeight() - 2); - - } // synchronized (emulator) - } - /** * Check if a mouse press/release/motion event coordinate is over the * emulator. @@ -490,12 +906,10 @@ public class TTerminalWindow extends TWindow { * @param mouse a mouse-based event * @return whether or not the mouse is on the emulator */ - private final boolean mouseOnEmulator(final TMouseEvent mouse) { + private boolean mouseOnEmulator(final TMouseEvent mouse) { - synchronized (emulator) { - if (!emulator.isReading()) { - return false; - } + if (!emulator.isReading()) { + return false; } if ((mouse.getAbsoluteX() >= getAbsoluteX() + 1) @@ -509,141 +923,190 @@ public class TTerminalWindow extends TWindow { } /** - * Handle keystrokes. + * Copy a display buffer. * - * @param keypress keystroke event + * @param buffer the buffer to copy + * @return a deep copy of the buffer's data */ - @Override - public void onKeypress(final TKeypressEvent keypress) { - - // Scrollback up/down - if (keypress.equals(kbShiftPgUp) - || keypress.equals(kbCtrlPgUp) - || keypress.equals(kbAltPgUp) - ) { - vScroller.bigDecrement(); - return; - } - if (keypress.equals(kbShiftPgDn) - || keypress.equals(kbCtrlPgDn) - || keypress.equals(kbAltPgDn) - ) { - vScroller.bigIncrement(); - return; - } - - // Synchronize against the emulator so we don't stomp on its reader - // thread. - synchronized (emulator) { - if (emulator.isReading()) { - // Get out of scrollback - vScroller.setValue(0); - emulator.keypress(keypress.getKey()); - - // UGLY HACK TIME! cmd.exe needs CRLF, not just CR, so if - // this is kBEnter then also send kbCtrlJ. - if (System.getProperty("os.name").startsWith("Windows")) { - if (keypress.equals(kbEnter)) { - emulator.keypress(kbCtrlJ); - } - } - - readEmulatorState(); - return; - } + private List copyBuffer(final List buffer) { + ArrayList result = new ArrayList(buffer.size()); + for (DisplayLine line: buffer) { + result.add(new DisplayLine(line)); } - - // Process is closed, honor "normal" TUI keystrokes - super.onKeypress(keypress); + return result; } /** - * Handle mouse press events. + * Draw glyphs for a double-width or double-height VT100 cell to two + * screen cells. * - * @param mouse mouse button press event + * @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 */ - @Override - public void onMouseDown(final TMouseEvent mouse) { - if (inWindowMove || inWindowResize) { - // TWindow needs to deal with this. - super.onMouseDown(mouse); + 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 (mouse.isMouseWheelUp()) { - vScroller.decrement(); - return; + if ((textWidth != lastTextWidth) || (textHeight != lastTextHeight)) { + // Screen size has changed, reset the font. + setupFont(textHeight); + lastTextWidth = textWidth; + lastTextHeight = textHeight; } - if (mouse.isMouseWheelDown()) { - vScroller.increment(); - return; - } - if (mouseOnEmulator(mouse)) { - synchronized (emulator) { - mouse.setX(mouse.getX() - 1); - mouse.setY(mouse.getY() - 1); - emulator.mouse(mouse); - readEmulatorState(); - return; - } + 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); } - // Emulator didn't consume it, pass it on - super.onMouseDown(mouse); + // 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); } /** - * Handle mouse release events. + * Set up the double-width font. * - * @param mouse mouse button release event + * @param fontSize the size of font to request for the single-width font. + * The double-width font will be 2x this value. */ - @Override - public void onMouseUp(final TMouseEvent mouse) { - if (inWindowMove || inWindowResize) { - // TWindow needs to deal with this. - super.onMouseUp(mouse); - return; - } - - if (mouseOnEmulator(mouse)) { - synchronized (emulator) { - mouse.setX(mouse.getX() - 1); - mouse.setY(mouse.getY() - 1); - emulator.mouse(mouse); - readEmulatorState(); - return; + private void setupFont(final int fontSize) { + doubleFont = GlyphMaker.getInstance(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; } } + } - // Emulator didn't consume it, pass it on - super.onMouseUp(mouse); + // ------------------------------------------------------------------------ + // DisplayListener -------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Called by emulator when fresh data has come in. + */ + public void displayChanged() { + dirty = true; + getApplication().postEvent(new TMenuEvent(TMenu.MID_REPAINT)); } /** - * Handle mouse motion events. + * Function to call to obtain the display width. * - * @param mouse mouse motion event + * @return the number of columns in the display */ - @Override - public void onMouseMotion(final TMouseEvent mouse) { - if (inWindowMove || inWindowResize) { - // TWindow needs to deal with this. - super.onMouseMotion(mouse); - return; + public int getDisplayWidth() { + if (ptypipe) { + return getWidth() - 2; } + return 80; + } - if (mouseOnEmulator(mouse)) { - synchronized (emulator) { - mouse.setX(mouse.getX() - 1); - mouse.setY(mouse.getY() - 1); - emulator.mouse(mouse); - readEmulatorState(); - return; - } + /** + * Function to call to obtain the display height. + * + * @return the number of rows in the display + */ + public int getDisplayHeight() { + if (ptypipe) { + return getHeight() - 2; } - - // Emulator didn't consume it, pass it on - super.onMouseMotion(mouse); + return 24; } }