*
* 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"),
*/
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;
public class TTerminalWindow extends TScrollableWindow
implements DisplayListener {
-
/**
* Translated strings.
*/
/**
* 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;
+ /**
+ * If true, close the window when the shell exits.
+ */
+ private boolean closeOnExit = false;
+
+ /**
+ * System-dependent Y adjustment for text in the character cell
+ * (double-height).
+ */
+ private int doubleTextAdjustY = 0;
+
+ /**
+ * System-dependent X adjustment for text in the character cell
+ * (double-height).
+ */
+ private int doubleTextAdjustX = 0;
+
+ /**
+ * Descent of a character cell in pixels (double-height).
+ */
+ private int doubleMaxDescent = 0;
+
+ /**
+ * Double-width font.
+ */
+ private Font doubleFont = null;
+
+ /**
+ * Last text width value.
+ */
+ private int lastTextWidth = -1;
+
+ /**
+ * Last text height value.
+ */
+ private int lastTextHeight = -1;
+
+ /**
+ * A cache of previously-rendered double-width glyphs.
+ */
+ private Map<Cell, BufferedImage> glyphCache;
+
+ /**
+ * A cache of previously-rendered double-width glyphs for blinking text,
+ * when it is not visible.
+ */
+ private Map<Cell, BufferedImage> glyphCacheBlink;
+
+ /**
+ * The blink state, used only by ECMA48 backend and when double-width
+ * chars must be drawn.
+ */
+ private boolean blinkState = true;
+
// ------------------------------------------------------------------------
// Constructors -----------------------------------------------------------
// ------------------------------------------------------------------------
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);
}
/**
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.
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
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+"));
}
}
+ getVerticalValue();
assert (visibleBottom >= 0);
- List<DisplayLine> preceedingBlankLines = new LinkedList<DisplayLine>();
+ List<DisplayLine> preceedingBlankLines = new ArrayList<DisplayLine>();
int visibleTop = visibleBottom - visibleHeight;
if (visibleTop < 0) {
for (int i = visibleTop; i < 0; i++) {
}
assert (visibleTop >= 0);
- List<DisplayLine> displayLines = new LinkedList<DisplayLine>();
+ List<DisplayLine> displayLines = new ArrayList<DisplayLine>();
displayLines.addAll(scrollback);
displayLines.addAll(display);
- List<DisplayLine> visibleLines = new LinkedList<DisplayLine>();
+ List<DisplayLine> visibleLines = new ArrayList<DisplayLine>();
visibleLines.addAll(preceedingBlankLines);
visibleLines.addAll(displayLines.subList(visibleTop,
visibleBottom));
}
}
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++;
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)
}
+ /**
+ * Handle window close.
+ */
+ @Override
+ public void onClose() {
+ emulator.close();
+ if (shell != null) {
+ terminateShellChildProcess();
+ shell.destroy();
+ shell = null;
+ }
+ }
+
/**
* Handle window/screen resize events.
*
} // synchronized (emulator)
}
- /**
- * Handle window close.
- */
- @Override
- public void onClose() {
- emulator.close();
- if (shell != null) {
- terminateShellChildProcess();
- shell.destroy();
- shell = null;
- }
- }
-
/**
* Handle keystrokes.
*
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) {
// Add shortcut text
newStatusBar(i18n.getString("statusBarRunning"));
+
+ // Pass the correct text cell width/height to the emulator
+ int textWidth = 16;
+ int textHeight = 20;
+ if (getScreen() instanceof SwingTerminal) {
+ SwingTerminal terminal = (SwingTerminal) getScreen();
+
+ textWidth = terminal.getTextWidth();
+ textHeight = terminal.getTextHeight();
+ } else if (getScreen() instanceof ECMA48Terminal) {
+ ECMA48Terminal terminal = (ECMA48Terminal) getScreen();
+ textWidth = terminal.getTextWidth();
+ textHeight = terminal.getTextHeight();
+ }
+ emulator.setTextWidth(textWidth);
+ emulator.setTextHeight(textHeight);
}
/**
* Hook for subclasses to be notified of the shell termination.
*/
public void onShellExit() {
+ if (closeOnExit) {
+ close();
+ }
getApplication().postEvent(new TMenuEvent(TMenu.MID_REPAINT));
}
// 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
* @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;
}
+ /**
+ * 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 = 16;
+ int textHeight = 20;
+ boolean cursorBlinkVisible = true;
+
+ if (getScreen() instanceof SwingTerminal) {
+ SwingTerminal terminal = (SwingTerminal) getScreen();
+
+ textWidth = terminal.getTextWidth();
+ textHeight = terminal.getTextHeight();
+ 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;
+ }
+
+ textWidth = terminal.getTextWidth();
+ textHeight = terminal.getTextHeight();
+ 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.
+ if (getScreen() instanceof ECMA48Terminal) {
+ // BUG: the background color is coming in the same as the
+ // foreground color. For now, don't draw it.
+ } else {
+ 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<Cell, BufferedImage>();
+ glyphCacheBlink = new HashMap<Cell, BufferedImage>();
+
+ // 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();
+ }
+ }
+ );
+ }
+
+ }
+
}