diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000..839d647 --- /dev/null +++ b/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,5 @@ +eclipse.preferences.version=1 +encoding//src/main/java=UTF-8 +encoding//src/main/resources=UTF-8 +encoding//src/test/java=UTF-8 +encoding/=UTF-8 diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..ec4300d --- /dev/null +++ b/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,5 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7 +org.eclipse.jdt.core.compiler.compliance=1.7 +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.source=1.7 diff --git a/.settings/org.eclipse.m2e.core.prefs b/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 0000000..f897a7f --- /dev/null +++ b/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/src/main/java/com/loomcom/symon/jterminal/AbstractTerminalModel.java b/src/main/java/com/loomcom/symon/jterminal/AbstractTerminalModel.java new file mode 100644 index 0000000..534d3ff --- /dev/null +++ b/src/main/java/com/loomcom/symon/jterminal/AbstractTerminalModel.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2009-2011 Graham Edgecombe. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +package com.loomcom.symon.jterminal; + +/** + * A {@link TerminalModel} which implements some common behaviour. + * @author Graham Edgecombe + */ +public abstract class AbstractTerminalModel implements TerminalModel { + + @Override + public void clear() { + int rows = getRows(), columns = getColumns(); + for (int column = 0; column < columns; column++) { + for (int row = 0; row < rows; row++) { + setCell(column, row, null); + } + } + } + + @Override + public void moveCursorBack(int n) { + if (n < 0) { + throw new IllegalArgumentException("n must be positive"); + } + int cursorColumn = getCursorColumn() - n; + if (cursorColumn < 0) { + cursorColumn = 0; + } + setCursorColumn(cursorColumn); + } + + @Override + public void moveCursorForward(int n) { + if (n < 0) { + throw new IllegalArgumentException("n must be positive"); + } + int columns = getColumns(); + int cursorColumn = getCursorColumn() + n; + if (cursorColumn >= columns) { + cursorColumn = columns - 1; + } + setCursorColumn(cursorColumn); + } + + @Override + public void moveCursorDown(int n) { + if (n < 0) { + throw new IllegalArgumentException("n must be positive"); + } + int bufferSize = getBufferSize(); + int cursorRow = getCursorRow() + n; + if (cursorRow >= bufferSize) { + cursorRow = bufferSize - 1; + } + setCursorRow(cursorRow); + } + + @Override + public void moveCursorUp(int n) { + if (n < 0) { + throw new IllegalArgumentException("n must be positive"); + } + int cursorRow = getCursorRow() - n; + if (cursorRow < 0) { + cursorRow = 0; + } + setCursorRow(cursorRow); + } + +} + diff --git a/src/main/java/com/loomcom/symon/jterminal/JTerminal.java b/src/main/java/com/loomcom/symon/jterminal/JTerminal.java new file mode 100644 index 0000000..b3b519c --- /dev/null +++ b/src/main/java/com/loomcom/symon/jterminal/JTerminal.java @@ -0,0 +1,198 @@ +/* + * Copyright (c) 2016 Seth J. Morabito + * Portions Copyright (c) 2009-2011 Graham Edgecombe + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.loomcom.symon.jterminal; + +import com.loomcom.symon.jterminal.vt100.Vt100TerminalModel; +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.event.AdjustmentEvent; +import java.awt.event.AdjustmentListener; +import javax.swing.JComponent; +import javax.swing.JScrollBar; + +/** + * Swing terminal emulation component + * @author Seth J. Morabito + * + */ +public class JTerminal extends JComponent { + + private static final long serialVersionUID = 2871625194146986567L; + + private int borderWidth = 0; + + private JScrollBar scrollBar; + + /** + * The terminal emulation model + */ + private TerminalModel model; + + /** + * The font this terminal uses + */ + private Font font; + + /** + * The cell width in pixels + */ + private int cellWidth; + + /** + * The cell height in pixels + */ + private int cellHeight; + + /** + * Max descender for font + */ + private int maxDescender; + + public JTerminal(Font font) { + this(new Vt100TerminalModel(), font); + } + + public JTerminal(TerminalModel model, Font font) { + setModel(model); + setFont(font); + init(); + } + + public void setBorderWidth(int borderWidth) { + this.borderWidth = borderWidth; + revalidate(); + } + + public int getBorderWidth() { + return borderWidth; + } + + public void setFont(Font font) { + this.font = font; + setCellWidthAndHeight(font); + revalidate(); + } + + public Font getFont() { + return font; + } + + public void setModel(TerminalModel model) { + if (model == null) { + throw new NullPointerException("model"); + } + this.model = model; + } + + public TerminalModel getModel() { + return model; + } + + public void println(String str) { + if (str == null) { + throw new NullPointerException("str"); + } + print(str.concat("\r\n")); + } + + public void print(String str) { + model.print(str); + } + + public Dimension getMinimumSize() { + return new Dimension(model.getColumns() * cellWidth + borderWidth * 2, + model.getRows() * cellHeight + borderWidth * 2); + } + + public Dimension getMaximumSize() { + return getMinimumSize(); + } + + public Dimension getPreferredSize() { + return getMinimumSize(); + } + + public void paint(Graphics g) { + g.setFont(font); + + int width = model.getColumns(); + int height = model.getBufferSize(); + + g.setColor(model.getDefaultBackgroundColor()); + g.fillRect(0, 0, width * cellWidth + borderWidth * 2, height * cellHeight + borderWidth * 2); + + int start = scrollBar == null ? 0 : scrollBar.getValue(); + for (int y = start; y < height; y++) { + for (int x = 0; x < width; x++) { + TerminalCell cell = model.getCell(x, y); + boolean cursorHere = (model.getCursorRow() == y) && (model.getCursorColumn() == x); + + if ((cursorHere) && (cell == null)) { + cell = new TerminalCell(' ', model.getDefaultBackgroundColor(), model.getDefaultForegroundColor()); + } + + if (cell != null) { + int px = x * cellWidth + borderWidth; + int py = (y - start) * cellHeight + borderWidth; + + g.setColor(cursorHere ? cell.getForegroundColor() : cell.getBackgroundColor()); + g.fillRect(px, py, cellWidth, cellHeight); + + g.setColor(cursorHere ? cell.getBackgroundColor() : cell.getForegroundColor()); + g.drawChars(new char[] { cell.getCharacter() }, 0, 1, px, py + cellHeight - maxDescender); + } + } + } + } + + private void init() { + setLayout(new BorderLayout(0, 0)); + + int rows = model.getRows(); + int bufferSize = model.getBufferSize(); + + if (bufferSize > rows) { + scrollBar = new JScrollBar(1, 0, rows, 0, bufferSize + 1); + scrollBar.addAdjustmentListener(new AdjustmentListener() { + public void adjustmentValueChanged(AdjustmentEvent evt) { + repaint(); + } + }); + add("After", scrollBar); + } + + repaint(); + } + + private void setCellWidthAndHeight(Font font) { + FontMetrics metrics = getFontMetrics(font); + cellWidth = metrics.charWidth('W'); + cellHeight = metrics.getHeight(); + maxDescender = metrics.getMaxDescent(); + } +} diff --git a/src/main/java/com/loomcom/symon/jterminal/TerminalCell.java b/src/main/java/com/loomcom/symon/jterminal/TerminalCell.java new file mode 100644 index 0000000..55b9eff --- /dev/null +++ b/src/main/java/com/loomcom/symon/jterminal/TerminalCell.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2009-2011 Graham Edgecombe. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +package com.loomcom.symon.jterminal; + +import java.awt.Color; + +/** + * Represents a single terminal cell which contains a character, background + * color and foreground color. + * @author Graham Edgecombe + */ +public class TerminalCell { + + /** + * The character. + */ + private final char character; + + /** + * The background color. + */ + private final Color backgroundColor; + + /** + * The foreground color. + */ + private final Color foregroundColor; + + /** + * Creates a terminal cell with the specified character, background color + * and foreground color. + * @param character The character. + * @param backgroundColor The background color. + * @param foregroundColor The foreground color. + * @throws NullPointerException if the background or foreground color(s) + * are {@code null}. + */ + public TerminalCell(char character, Color backgroundColor, Color foregroundColor) { + if (backgroundColor == null) { + throw new NullPointerException("backgroundColor"); + } + if (foregroundColor == null) { + throw new NullPointerException("foregroundColor"); + } + + this.character = character; + this.backgroundColor = backgroundColor; + this.foregroundColor = foregroundColor; + } + + /** + * Gets the character. + * @return The character. + */ + public char getCharacter() { + return character; + } + + /** + * Gets the background color. + * @return The background color. + */ + public Color getBackgroundColor() { + return backgroundColor; + } + + /** + * Gets the foreground color. + * @return The foreground color. + */ + public Color getForegroundColor() { + return foregroundColor; + } + +} + diff --git a/src/main/java/com/loomcom/symon/jterminal/TerminalModel.java b/src/main/java/com/loomcom/symon/jterminal/TerminalModel.java new file mode 100644 index 0000000..7e83a2d --- /dev/null +++ b/src/main/java/com/loomcom/symon/jterminal/TerminalModel.java @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2009-2011 Graham Edgecombe. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +package com.loomcom.symon.jterminal; + +import java.awt.Color; + +import com.loomcom.symon.jterminal.bell.BellStrategy; + +/** + * Model for terminals - defines methods for getting/setting cells, printing + * text to a terminal and getting the size of the terminal and buffer. + */ +public interface TerminalModel { + + /** + * Gets the bell strategy. + * @return The bell strategy. + */ + public BellStrategy getBellStrategy(); + + /** + * Sets the bell strategy. + * @param strategy The bell strategy. + * @throws NullPointerException if the strategy is {@code null}. + */ + public void setBellStrategy(BellStrategy strategy); + + /** + * Clears the terminal. + */ + public void clear(); + + /** + * Moves the cursor back n characters. + * @param n The number of characters. + * @throws IllegalArgumentException if n is not positive. + */ + public void moveCursorBack(int n); + + /** + * Moves the cursor forward n characters. + * @param n The number of characters. + * @throws IllegalArgumentException if n is not positive. + */ + public void moveCursorForward(int n); + + /** + * Moves the cursor down n characters. + * @param n The number of characters. + * @throws IllegalArgumentException if n is not positive. + */ + public void moveCursorDown(int n); + + /** + * Moves the cursor up n characters. + * @param n The number of characters. + * @throws IllegalArgumentException if n is not positive. + */ + public void moveCursorUp(int n); + + /** + * Sets a cell. + * @param column The column. + * @param row The row. + * @param cell The cell. + * @throws IndexOutOfBoundsException if the column and/or row number(s) are + * out of bounds. + */ + public void setCell(int column, int row, TerminalCell cell); + + /** + * Gets a cell. + * @param column The column. + * @param row The row. + * @return The cell. + * @throws IndexOutOfBoundsException if the column and/or row number(s) are + * out of bounds. + */ + public TerminalCell getCell(int column, int row); + + /** + * Prints the specified string to the terminal at the cursor position, + * interpreting any escape sequences/special ASCII codes the model may + * support. Lines will be wrapped if necessary. + * @param str The string to print. + * @throws NullPointerException if the string is {@code null}. + */ + public void print(String str); + + /** + * Gets the number of columns. + * @return The number of columns. + */ + public int getColumns(); + + /** + * Gets the number of rows. + * @return The number of rows. + */ + public int getRows(); + + /** + * Gets the buffer size. + * @return The buffer size. + */ + public int getBufferSize(); + + /** + * Gets the cursor row. + * @return The cursor row. + */ + public int getCursorRow(); + + /** + * Sets the cursor row. + * @param row The cursor row. + * @throws IllegalArgumentException if the row is out of the valid range. + */ + public void setCursorRow(int row); + + /** + * Gets the cursor column. + * @return The cursor column. + */ + public int getCursorColumn(); + + /** + * Sets the cursor column. + * @param column The cursor column. + * @throws IllegalArgumentException if the column is out of the valid range. + */ + public void setCursorColumn(int column); + + /** + * Gets the default background color. + * @return The default background color. + */ + public Color getDefaultBackgroundColor(); + + /** + * Gets the default foreground color. + * @return The default foreground color. + */ + public Color getDefaultForegroundColor(); + +} + diff --git a/src/main/java/com/loomcom/symon/jterminal/bell/BeepBellStrategy.java b/src/main/java/com/loomcom/symon/jterminal/bell/BeepBellStrategy.java new file mode 100644 index 0000000..d368ae4 --- /dev/null +++ b/src/main/java/com/loomcom/symon/jterminal/bell/BeepBellStrategy.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2009-2011 Graham Edgecombe. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +package com.loomcom.symon.jterminal.bell; + +import java.awt.Toolkit; + +/** + * A {@link BellStrategy} which calls {@link Toolkit#beep()}. + * @author Graham Edgecombe + */ +public class BeepBellStrategy implements BellStrategy { + + @Override + public void soundBell() { + Toolkit.getDefaultToolkit().beep(); + } + +} + diff --git a/src/main/java/com/loomcom/symon/jterminal/bell/BellStrategy.java b/src/main/java/com/loomcom/symon/jterminal/bell/BellStrategy.java new file mode 100644 index 0000000..57a4fb9 --- /dev/null +++ b/src/main/java/com/loomcom/symon/jterminal/bell/BellStrategy.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2009-2011 Graham Edgecombe. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +package com.loomcom.symon.jterminal.bell; + +/** + * A 'strategy' used to sound the bell (US-ASCII character {@code 7}). + * @author Graham Edgecombe + */ +public interface BellStrategy { + + /** + * Sounds the bell. + */ + public void soundBell(); + +} + diff --git a/src/main/java/com/loomcom/symon/jterminal/bell/NopBellStrategy.java b/src/main/java/com/loomcom/symon/jterminal/bell/NopBellStrategy.java new file mode 100644 index 0000000..c878b79 --- /dev/null +++ b/src/main/java/com/loomcom/symon/jterminal/bell/NopBellStrategy.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2009-2011 Graham Edgecombe. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +package com.loomcom.symon.jterminal.bell; + +/** + * A {@link BellStrategy} which does nothing. + * @author Graham Edgecombe + */ +public class NopBellStrategy implements BellStrategy { + + @Override + public void soundBell() { + /* ignore */ + } + +} + diff --git a/src/main/java/com/loomcom/symon/jterminal/bell/package-info.java b/src/main/java/com/loomcom/symon/jterminal/bell/package-info.java new file mode 100644 index 0000000..a17185f --- /dev/null +++ b/src/main/java/com/loomcom/symon/jterminal/bell/package-info.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2009-2011 Graham Edgecombe. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +/** + * Contains bell strategy classes, which define how an application should + * respond to the US-ASCII {@code BEL} character. + */ +package com.loomcom.symon.jterminal.bell; + diff --git a/src/main/java/com/loomcom/symon/jterminal/package-info.java b/src/main/java/com/loomcom/symon/jterminal/package-info.java new file mode 100644 index 0000000..8e27871 --- /dev/null +++ b/src/main/java/com/loomcom/symon/jterminal/package-info.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2009-2011 Graham Edgecombe. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +/** + * Contains core JTerminal classes. + */ +package com.loomcom.symon.jterminal; + diff --git a/src/main/java/com/loomcom/symon/jterminal/vt100/AnsiControlSequence.java b/src/main/java/com/loomcom/symon/jterminal/vt100/AnsiControlSequence.java new file mode 100644 index 0000000..c752cbf --- /dev/null +++ b/src/main/java/com/loomcom/symon/jterminal/vt100/AnsiControlSequence.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2009-2011 Graham Edgecombe. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +package com.loomcom.symon.jterminal.vt100; + +/** + * Represents an ANSI control sequence. + * @author Graham Edgecombe + */ +public class AnsiControlSequence { + + /** + * The command character. + */ + private final char command; + + /** + * The parameters. + */ + private final String[] parameters; + + /** + * Creates an ANSI control sequence with the specified command and + * parameters. + * @param command The command character. + * @param parameters The parameters array. + * @throws NullPointerException if the parameters array is {@code null}. + */ + public AnsiControlSequence(char command, String[] parameters) { + if (parameters == null) { + throw new NullPointerException("parameters"); + } + this.command = command; + if (parameters.length == 1 && parameters[0].equals("")) { + this.parameters = new String[0]; + } else { + this.parameters = parameters.clone(); + } + } + + /** + * Gets the command character. + * @return The command character. + */ + public char getCommand() { + return command; + } + + /** + * Gets the parameters array. + * @return The parameters array. + */ + public String[] getParameters() { + return parameters; + } + +} + diff --git a/src/main/java/com/loomcom/symon/jterminal/vt100/AnsiControlSequenceListener.java b/src/main/java/com/loomcom/symon/jterminal/vt100/AnsiControlSequenceListener.java new file mode 100644 index 0000000..c5b20a2 --- /dev/null +++ b/src/main/java/com/loomcom/symon/jterminal/vt100/AnsiControlSequenceListener.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2009-2011 Graham Edgecombe. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +package com.loomcom.symon.jterminal.vt100; + +/** + * An interface which classes may use to listen to events from a + * {@link AnsiControlSequenceParser}. + */ +public interface AnsiControlSequenceListener { + + /** + * Called when a control sequence has been parsed. + * @param seq The control sequence. + */ + public void parsedControlSequence(AnsiControlSequence seq); + + /** + * Called when a string has been parsed. + * @param str The string. + */ + public void parsedString(String str); + +} + diff --git a/src/main/java/com/loomcom/symon/jterminal/vt100/AnsiControlSequenceParser.java b/src/main/java/com/loomcom/symon/jterminal/vt100/AnsiControlSequenceParser.java new file mode 100644 index 0000000..cf1a9d9 --- /dev/null +++ b/src/main/java/com/loomcom/symon/jterminal/vt100/AnsiControlSequenceParser.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2009-2011 Graham Edgecombe. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +package com.loomcom.symon.jterminal.vt100; + +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; + +/** + * A class which parses {@link AnsiControlSequence}s from {@link String}(s). + * @author Graham Edgecombe + */ +public class AnsiControlSequenceParser { + + /** + * The multi-byte control sequence introducer. + */ + private static final char[] MULTI_CSI = new char[] { 27, '[' }; + + /** + * The single-byte control sequence introducer. + */ + private static final char SINGLE_CSI = 155; + + /** + * The buffer of data from the last call to {@link #parse()}. This is + * populated with data if an escape sequence is not complete. + */ + private StringBuilder buffer = new StringBuilder(); + + /** + * The ANSI control sequence listener. + */ + private final AnsiControlSequenceListener listener; + + /** + * Creates the ANSI control sequence parser. + * @param listener The listener. + */ + public AnsiControlSequenceParser(AnsiControlSequenceListener listener) { + this.listener = listener; + } + + /** + * Parses the specified string. + * @param str The string to parse. + */ + public void parse(String str) { + if (buffer.length() > 0) { + str = buffer.toString().concat(str); + buffer = new StringBuilder(); + } + Reader reader = new StringReader(str); + try { + try { + parse(reader); + } finally { + reader.close(); + } + } catch (IOException ex) { + /* ignore */ + } + } + + /** + * Parses characters from the specified character reader. + * @param reader The character reader. + * @throws IOException if an I/O error occurs. + */ + private void parse(Reader reader) throws IOException { + StringBuilder text = new StringBuilder(); + int character; + while ((character = reader.read()) != -1) { + boolean introducedControlSequence = false; + if (character == SINGLE_CSI) { + introducedControlSequence = true; + } else if (character == MULTI_CSI[0]) { + int nextCharacter = reader.read(); + if (nextCharacter == -1) { + buffer.append((char) character); + break; + } else if (nextCharacter == MULTI_CSI[1]) { + introducedControlSequence = true; + } else { + text.append((char) character); + text.append((char) nextCharacter); + } + } else { + text.append((char) character); + } + + if (introducedControlSequence) { + if (text.length() > 0) { + listener.parsedString(text.toString()); + text = new StringBuilder(); + } + parseControlSequence(reader); + } + } + + if (text.length() > 0) { + listener.parsedString(text.toString()); + } + } + + /** + * Parses a control sequence. + * @param reader The character reader. + * @throws IOException if an I/O error occurs. + */ + private void parseControlSequence(Reader reader) throws IOException { + boolean finishedSequence = false; + StringBuilder parameters = new StringBuilder(); + int character; + while ((character = reader.read()) != -1) { + if ((character >= 'a' && character <= 'z') || (character >= 'A' && character <= 'Z')) { + String[] array = parameters.toString().split(";"); + AnsiControlSequence seq = new AnsiControlSequence((char) character, array); + listener.parsedControlSequence(seq); + + finishedSequence = true; + break; + } else { + parameters.append((char) character); + } + } + if (!finishedSequence) { + // not an ideal solution if they used the two byte CSI, but it's + // easier and cleaner than keeping track of it + buffer.append((char) SINGLE_CSI); + buffer.append(parameters); + } + } + +} + diff --git a/src/main/java/com/loomcom/symon/jterminal/vt100/SgrColor.java b/src/main/java/com/loomcom/symon/jterminal/vt100/SgrColor.java new file mode 100644 index 0000000..459cdd6 --- /dev/null +++ b/src/main/java/com/loomcom/symon/jterminal/vt100/SgrColor.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2009-2011 Graham Edgecombe. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +package com.loomcom.symon.jterminal.vt100; + +import java.awt.Color; + +/** + * Contains colors used by the SGR ANSI escape sequence. + * @author Graham Edgecombe + */ +final class SgrColor { + + /** + * An array of normal intensity colors. + */ + public static final Color[] COLOR_NORMAL = new Color[] { + new Color(0, 0, 0), + new Color(128, 0, 0), + new Color(0, 128, 0), + new Color(128, 128, 0), + new Color(0, 0, 128), + new Color(128, 0, 128), + new Color(0, 128, 128), + new Color(192, 192, 192) + }; + + /** + * An array of bright intensity colors. + */ + public static final Color[] COLOR_BRIGHT = new Color[] { + new Color(128, 128, 128), + new Color(255, 0, 0), + new Color(0, 255, 0), + new Color(255, 255, 0), + new Color(0, 0, 255), + new Color(0, 0, 255), + new Color(255, 0, 255), + new Color(0, 255, 255), + new Color(255, 255, 255) + }; + + /** + * Default private constructor to prevent instantiation. + */ + private SgrColor() { + + } + +} + diff --git a/src/main/java/com/loomcom/symon/jterminal/vt100/Vt100TerminalModel.java b/src/main/java/com/loomcom/symon/jterminal/vt100/Vt100TerminalModel.java new file mode 100644 index 0000000..019494d --- /dev/null +++ b/src/main/java/com/loomcom/symon/jterminal/vt100/Vt100TerminalModel.java @@ -0,0 +1,493 @@ +/* + * Copyright (c) 2009-2011 Graham Edgecombe. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +package com.loomcom.symon.jterminal.vt100; + +import java.awt.Color; + +import com.loomcom.symon.jterminal.AbstractTerminalModel; +import com.loomcom.symon.jterminal.TerminalCell; +import com.loomcom.symon.jterminal.TerminalModel; +import com.loomcom.symon.jterminal.bell.BellStrategy; +import com.loomcom.symon.jterminal.bell.NopBellStrategy; + +/** + * A VT100/ANSI-compatible terminal model. + * @author Graham Edgecombe + */ +public class Vt100TerminalModel extends AbstractTerminalModel { + + /** + * A {@link AnsiControlSequenceListener} which modifies the + * {@link TerminalModel} appropriately when an event happens. + * @author Graham Edgecombe + */ + private class Vt100Listener implements AnsiControlSequenceListener { + + /** + * The saved cursor row. + */ + private int savedCursorRow = -1; + + /** + * The saved cursor column. + */ + private int savedCursorColumn = -1; + + @Override + public void parsedControlSequence(AnsiControlSequence seq) { + char command = seq.getCommand(); + String[] parameters = seq.getParameters(); + + switch (command) { + case 'A': + case 'B': + case 'C': + case 'D': + int n = 1; + if (parameters.length == 1) { + n = Integer.parseInt(parameters[0]); + } + if (command == 'A') { + moveCursorUp(n); + } else if (command == 'B') { + moveCursorDown(n); + } else if (command == 'C') { + moveCursorForward(n); + } else if (command == 'D') { + moveCursorBack(n); + } + break; + case 'E': + case 'F': + n = 1; + if (parameters.length == 1) { + n = Integer.parseInt(parameters[0]); + } + if (command == 'E') { + moveCursorDown(n); + } else if (command == 'F') { + moveCursorUp(n); + } + setCursorColumn(0); + break; + case 'G': + if (parameters.length == 1) { + n = Integer.parseInt(parameters[0]); + setCursorColumn(n - 1); + } + break; + case 'H': + case 'f': + if (parameters.length == 2) { + n = 1; + int m = 1; + if (parameters[0].length() > 0) { + n = Integer.parseInt(parameters[0]); + } + if (parameters[1].length() > 0) { + m = Integer.parseInt(parameters[1]); + } + setCursorRow(n - 1); + setCursorColumn(m - 1); + } + break; + case 'J': + n = 0; + if (parameters.length == 1) { + n = Integer.parseInt(parameters[0]); + } + if (n == 0) { + int row = cursorRow; + int column = cursorColumn; + while(row < rows) { + while(column < columns) { + cells[row][column] = null; + column++; + } + column = 0; + row++; + } + } else if (n == 1) { + int row = cursorRow; + int column = cursorColumn; + while(row >= 0) { + while(column >= 0) { + cells[row][column] = null; + column--; + } + column = columns - 1; + row--; + } + } else if (n == 2) { + clear(); + } + break; + case 'K': + n = 0; + if (parameters.length == 1) { + n = Integer.parseInt(parameters[0]); + } + if (n == 0) { + for (int row = cursorRow; row < rows; row++) { + cells[row][cursorColumn] = null; + } + } else if (n == 1) { + for (int row = cursorRow; row >= 0; row--) { + cells[row][cursorColumn] = null; + } + } else if (n == 2) { + for (int column = 0; column < columns; column++) { + cells[cursorRow][column] = null; + } + } + break; + case 'm': + if (parameters.length == 0) { + parameters = new String[] { "0" }; + } + for (String parameter : parameters) { + if (parameter.equals("0")) { + foregroundColor = DEFAULT_FOREGROUND_COLOR; + backgroundColor = DEFAULT_BACKGROUND_COLOR; + backgroundBold = DEFAULT_BACKGROUND_BOLD; + foregroundBold = DEFAULT_FOREGROUND_BOLD; + } else if (parameter.equals("2")) { + backgroundBold = true; + foregroundBold = true; + } else if (parameter.equals("22")) { + backgroundBold = false; + foregroundBold = false; + } else if ((parameter.startsWith("3") || parameter.startsWith("4")) && parameter.length() == 2) { + int color = Integer.parseInt(parameter.substring(1)); + if (parameter.startsWith("3")) { + foregroundColor = color; + } else if (parameter.startsWith("4")) { + backgroundColor = color; + } + } + } + break; + case 'u': + if (savedCursorColumn != -1 && savedCursorRow != -1) { + cursorColumn = savedCursorColumn; + cursorRow = savedCursorRow; + } + break; + case 's': + savedCursorColumn = cursorColumn; + savedCursorRow = cursorRow; + break; + } + } + + @Override + public void parsedString(String str) { + for (char ch : str.toCharArray()) { + switch (ch) { + case '\0': + continue; + case '\r': + cursorColumn = 0; + continue; + case '\n': + cursorRow++; + break; + case '\t': + while ((++cursorColumn % TAB_WIDTH) != 0); + continue; + case 127: + if (cursorColumn > 0) { + cells[cursorRow][--cursorColumn] = null; + } + continue; + case 7: + bellStrategy.soundBell(); + continue; + } + + if (cursorColumn >= columns) { + cursorColumn = 0; + cursorRow++; + } + + if (cursorRow >= bufferSize) { + for (int i = 1; i < bufferSize; i++) { + System.arraycopy(cells[i], 0, cells[i - 1], 0, columns); + } + for (int i = 0; i < columns; i++) { + cells[bufferSize - 1][i] = null; + } + cursorRow--; + } + + Color back = backgroundBold ? SgrColor.COLOR_BRIGHT[backgroundColor] : SgrColor.COLOR_NORMAL[backgroundColor]; + Color fore = foregroundBold ? SgrColor.COLOR_BRIGHT[foregroundColor] : SgrColor.COLOR_NORMAL[foregroundColor]; + if (ch != '\n') { + cells[cursorRow][cursorColumn++] = new TerminalCell(ch, back, fore); + } + } + } + + } + + /** + * The default number of columns. + */ + private static final int DEFAULT_COLUMNS = 80; + + /** + * The default number of rows. + */ + private static final int DEFAULT_ROWS = 25; + + /** + * The tab width in characters. + */ + private static final int TAB_WIDTH = 8; + + /** + * The default foreground bold flag. + */ + private static final boolean DEFAULT_FOREGROUND_BOLD = false; + + /** + * The default background bold flag. + */ + private static final boolean DEFAULT_BACKGROUND_BOLD = false; + + /** + * The default foreground color. + */ + private static final int DEFAULT_FOREGROUND_COLOR = 7; + + /** + * The default background color. + */ + private static final int DEFAULT_BACKGROUND_COLOR = 0; + + /** + * The ANSI control sequence listener. + */ + private final AnsiControlSequenceListener listener = this.new Vt100Listener(); + + /** + * The ANSI control sequence parser. + */ + private final AnsiControlSequenceParser parser = new AnsiControlSequenceParser(listener); + + /** + * The current bell strategy. + */ + private BellStrategy bellStrategy = new NopBellStrategy(); + + /** + * The array of cells. + */ + private TerminalCell[][] cells; + + /** + * The number of columns. + */ + private int columns; + + /** + * The number of rows. + */ + private int rows; + + /** + * The buffer size. + */ + private int bufferSize; + + /** + * The cursor row. + */ + private int cursorRow = 0; + + /** + * The cursor column. + */ + private int cursorColumn = 0; + + /** + * The current foreground bold flag. + */ + private boolean foregroundBold = DEFAULT_FOREGROUND_BOLD; + + /** + * The current background bold flag. + */ + private boolean backgroundBold = DEFAULT_BACKGROUND_BOLD; + + /** + * The current foreground color. + */ + private int foregroundColor = DEFAULT_FOREGROUND_COLOR; + + /** + * The current background color. + */ + private int backgroundColor = DEFAULT_BACKGROUND_COLOR; + + /** + * Creates the terminal model with the default number of columns and rows, + * and the default buffer size. + */ + public Vt100TerminalModel() { + this(DEFAULT_COLUMNS, DEFAULT_ROWS); + } + + /** + * Creates the terminal model with the specified number of columns and + * rows. The buffer size is set to the number of rows. + * @param columns The number of columns. + * @param rows The number of rows. + * @throws IllegalArgumentException if the number of rows or columns is + * negative. + */ + public Vt100TerminalModel(int columns, int rows) { + this(columns, rows, rows); + } + + /** + * Creates the terminal model with the specified number of columns and rows + * and the specified buffer size. + * @param columns The number of columns. + * @param rows The number of rows. + * @param bufferSize The buffer size. + * @throws IllegalArgumentException if the number of rows or columns is + * negative, or if the buffer size is less than the number of rows. + */ + public Vt100TerminalModel(int columns, int rows, int bufferSize) { + if (columns < 0 || rows < 0 || bufferSize < 0) { + throw new IllegalArgumentException("Zero or positive values only allowed for columns, rows and buffer size."); + } + if (bufferSize < rows) { + throw new IllegalArgumentException("The buffer is too small"); + } + this.columns = columns; + this.rows = rows; + this.bufferSize = bufferSize; + init(); + } + + /** + * Initializes the terminal model. + */ + private void init() { + cells = new TerminalCell[bufferSize][columns]; + } + + @Override + public int getCursorRow() { + return cursorRow; + } + + @Override + public void setCursorRow(int row) { + if (row < 0 || row >= bufferSize) { + throw new IllegalArgumentException("row out of range"); + } + cursorRow = row; + } + + @Override + public int getCursorColumn() { + return cursorColumn; + } + + @Override + public void setCursorColumn(int column) { + if (column < 0 || column >= columns) { + throw new IllegalArgumentException("column out of range"); + } + cursorColumn = column; + } + + @Override + public TerminalCell getCell(int column, int row) { + if (column < 0 || row < 0 || column >= columns || row >= bufferSize) { + throw new IndexOutOfBoundsException(); + } + return cells[row][column]; + } + + @Override + public void setCell(int column, int row, TerminalCell cell) { + if (column < 0 || row < 0 || column >= columns || row >= bufferSize) { + throw new IndexOutOfBoundsException(); + } + cells[row][column] = cell; + } + + @Override + public void print(String str) { + if (str == null) { + throw new NullPointerException("str"); + } + parser.parse(str); + } + + @Override + public int getColumns() { + return columns; + } + + @Override + public int getRows() { + return rows; + } + + @Override + public int getBufferSize() { + return bufferSize; + } + + @Override + public BellStrategy getBellStrategy() { + return bellStrategy; + } + + @Override + public void setBellStrategy(BellStrategy strategy) { + if (strategy == null) { + throw new NullPointerException("strategy"); + } + this.bellStrategy = strategy; + } + + @Override + public Color getDefaultBackgroundColor() { + final int bg = DEFAULT_BACKGROUND_COLOR; + return DEFAULT_BACKGROUND_BOLD ? SgrColor.COLOR_BRIGHT[bg] : SgrColor.COLOR_NORMAL[bg]; + } + + @Override + public Color getDefaultForegroundColor() { + final int fg = DEFAULT_FOREGROUND_COLOR; + return DEFAULT_FOREGROUND_BOLD ? SgrColor.COLOR_BRIGHT[fg] : SgrColor.COLOR_NORMAL[fg]; + } + +} + diff --git a/src/main/java/com/loomcom/symon/jterminal/vt100/package-info.java b/src/main/java/com/loomcom/symon/jterminal/vt100/package-info.java new file mode 100644 index 0000000..2cd747d --- /dev/null +++ b/src/main/java/com/loomcom/symon/jterminal/vt100/package-info.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2009-2011 Graham Edgecombe. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +/** + * Contains a terminal model roughly compatible with the VT100/ANSI standard. + */ +package com.loomcom.symon.jterminal.vt100; + diff --git a/src/test/java/com/loomcom/symon/TestAnsiControlSequenceParser.java b/src/test/java/com/loomcom/symon/TestAnsiControlSequenceParser.java new file mode 100644 index 0000000..ac2c4b5 --- /dev/null +++ b/src/test/java/com/loomcom/symon/TestAnsiControlSequenceParser.java @@ -0,0 +1,231 @@ +/* + * Copyright (c) 2009-2011 Graham Edgecombe. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +package com.loomcom.symon; + +import static org.junit.Assert.*; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +import com.loomcom.symon.jterminal.vt100.AnsiControlSequenceParser; +import com.loomcom.symon.jterminal.vt100.AnsiControlSequence; +import com.loomcom.symon.jterminal.vt100.AnsiControlSequenceListener; + + +/** + * A test for the {@link AnsiControlSequenceParser} class. + * @author Graham Edgecombe + */ +public class TestAnsiControlSequenceParser implements AnsiControlSequenceListener { + + /** + * The current parser. + */ + private AnsiControlSequenceParser parser; + + /** + * The list of objects returned through the + * {@link AnsiControlSequenceListener} interface. + */ + private List objects = new ArrayList(); + + /** + * Sets up the parser and object list. + */ + @Before + public void setUp() { + objects.clear(); + parser = new AnsiControlSequenceParser(this); + } + + /** + * Tests a broken sequence with the single byte CSI. + */ + @Test + public void testBrokenSingleSequence() { + parser.parse(new String(new char[] { 155 })); + parser.parse(new String(new char[] { 'u' })); + + assertEquals(1, objects.size()); + + Object obj = objects.get(0); + assertEquals(AnsiControlSequence.class, obj.getClass()); + + AnsiControlSequence seq = (AnsiControlSequence) obj; + String[] params = seq.getParameters(); + + assertEquals('u', seq.getCommand()); + assertEquals(0, params.length); + } + + /** + * Tests a broken sequence with the double byte CSI. + */ + @Test + public void testBrokenDoubleSequence() { + char[] ch1 = { 27 }; + char[] ch2 = { '[' }; + char[] ch3 = { '3', '0', ';' }; + char[] ch4 = { '4', '0', 'm' }; + + parser.parse(new String(ch1)); + parser.parse(new String(ch2)); + parser.parse(new String(ch3)); + parser.parse(new String(ch4)); + + assertEquals(1, objects.size()); + Object obj = objects.get(0); + assertEquals(AnsiControlSequence.class, obj.getClass()); + + AnsiControlSequence seq = (AnsiControlSequence) obj; + String[] params = seq.getParameters(); + + assertEquals('m', seq.getCommand()); + assertEquals(2, params.length); + assertEquals("30", params[0]); + assertEquals("40", params[1]); + } + + /** + * Tests an empty string. + */ + @Test + public void testEmpty() { + parser.parse(""); + assertEquals(0, objects.size()); + } + + /** + * Tests a sequence embedded within some text. + */ + @Test + public void testTextAndSequence() { + char[] ch = { 'h', 'i', 155, 'u', 'b', 'y', 'e' }; + parser.parse(new String(ch)); + + assertEquals(3, objects.size()); + + Object o1 = objects.get(0); + Object o2 = objects.get(1); + Object o3 = objects.get(2); + + assertEquals(String.class, o1.getClass()); + assertEquals(AnsiControlSequence.class, o2.getClass()); + assertEquals(String.class, o3.getClass()); + + assertEquals("hi", o1); + assertEquals("bye", o3); + + AnsiControlSequence seq = (AnsiControlSequence) o2; + String[] params = seq.getParameters(); + + assertEquals(0, params.length); + assertEquals('u', seq.getCommand()); + } + + /** + * Tests parameters within a sequence. + */ + @Test + public void testParameters() { + char[] ch = { 155, '3', '0', ';', '4', '0', 'm' }; + parser.parse(new String(ch)); + + assertEquals(1, objects.size()); + Object obj = objects.get(0); + assertEquals(AnsiControlSequence.class, obj.getClass()); + + AnsiControlSequence seq = (AnsiControlSequence) obj; + String[] params = seq.getParameters(); + + assertEquals('m', seq.getCommand()); + assertEquals(2, params.length); + assertEquals("30", params[0]); + assertEquals("40", params[1]); + } + + /** + * Tests with plain text. + */ + @Test + public void testText() { + parser.parse("Hello, World!"); + + assertEquals(1, objects.size()); + Object obj = objects.get(0); + assertEquals(String.class, obj.getClass()); + assertEquals(obj, "Hello, World!"); + } + + /** + * Tests with a single byte CSI. + */ + @Test + public void testSingleCsi() { + char[] ch = { 155, '6', 'n' }; + parser.parse(new String(ch)); + + assertEquals(1, objects.size()); + Object obj = objects.get(0); + assertEquals(AnsiControlSequence.class, obj.getClass()); + + AnsiControlSequence seq = (AnsiControlSequence) obj; + String[] params = seq.getParameters(); + assertEquals('n', seq.getCommand()); + assertEquals(1, params.length); + assertEquals("6", params[0]); + } + + /** + * Tests with a double byte CSI. + */ + @Test + public void testDoubleCsi() { + char[] ch = { 27, '[', 's' }; + parser.parse(new String(ch)); + + assertEquals(1, objects.size()); + Object obj = objects.get(0); + assertEquals(AnsiControlSequence.class, obj.getClass()); + + AnsiControlSequence seq = (AnsiControlSequence) obj; + String[] params = seq.getParameters(); + assertEquals('s', seq.getCommand()); + assertEquals(0, params.length); + } + + @Override + public void parsedControlSequence(AnsiControlSequence seq) { + objects.add(seq); + } + + @Override + public void parsedString(String str) { + objects.add(str); + } + +} + diff --git a/src/test/java/com/loomcom/symon/TestVt100TerminalModel.java b/src/test/java/com/loomcom/symon/TestVt100TerminalModel.java new file mode 100644 index 0000000..cd6c490 --- /dev/null +++ b/src/test/java/com/loomcom/symon/TestVt100TerminalModel.java @@ -0,0 +1,233 @@ +/* + * Copyright (c) 2009-2011 Graham Edgecombe. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +package com.loomcom.symon; + +import static org.junit.Assert.*; + +import java.awt.Color; + +import org.junit.Before; +import org.junit.Test; + +import com.loomcom.symon.jterminal.TerminalCell; +import com.loomcom.symon.jterminal.TerminalModel; +import com.loomcom.symon.jterminal.bell.BellStrategy; +import com.loomcom.symon.jterminal.vt100.Vt100TerminalModel; + +/** + * A test for the {@link Vt100TerminalModel} class. + * @author Graham Edgecombe + */ +public class TestVt100TerminalModel { + + /** + * The terminal model. + */ + private TerminalModel model; + + /** + * Sets up the terminal model. + */ + @Before + public void setUp(){ + model = new Vt100TerminalModel(); + } + + /** + * Tests the word wrapping. + */ + @Test + public void testWordWrapping() { + int width = model.getColumns(); + for (int i = 0; i < (width + 1); i++) { + model.print("h"); + } + assertEquals('h', model.getCell(0, 1).getCharacter()); + } + + /** + * Tests special ASCII characters. + */ + @Test + public void testSpecialCharacters() { + model.print("\u0000"); + assertNull(model.getCell(0, 0)); + + model.print("a\rb\rc"); + assertEquals('c', model.getCell(0, 0).getCharacter()); + + model.print("\na"); + assertEquals('c', model.getCell(0, 0).getCharacter()); + assertEquals('a', model.getCell(1, 1).getCharacter()); + + model.print("\u007F"); + assertNull(model.getCell(0, 1)); + + model.print("A\tB"); + assertEquals('B', model.getCell(8, 1).getCharacter()); + } + + /** + * Tests that the terminal scrolls once the buffer is full. + */ + @Test + public void testBuffer() { + model = new Vt100TerminalModel(model.getColumns(), 2, 2); + model.print("This is line one.\r\n"); + model.print("This is line two. XXXXXX\r\n"); + model.print("And this is line three!"); + assertEquals('A', model.getCell(0, 1).getCharacter()); + assertNull(model.getCell(23, 1)); + } + + /** + * Tests the erase functionality. + */ + @Test + public void testErase() { + model.print("Hello"); + model.print("\u009B2J"); + for (int i = 0; i < 5; i++) { + assertNull(model.getCell(i, 0)); + } + } + + /** + * Tests moving the cursor. + */ + @Test + public void testMoveCursor() { + model.print("\u009B5B"); + assertEquals(5, model.getCursorRow()); + + model.print("\u009B3A"); + assertEquals(2, model.getCursorRow()); + + model.print("\u009B7C"); + assertEquals(7, model.getCursorColumn()); + + model.print("\u009B4D"); + assertEquals(3, model.getCursorColumn()); + + model.setCursorColumn(15); + model.setCursorRow(0); + + model.print("\u009B3E"); + assertEquals(0, model.getCursorColumn()); + assertEquals(3, model.getCursorRow()); + + model.setCursorColumn(7); + + model.print("\u009BF"); + assertEquals(0, model.getCursorColumn()); + assertEquals(2, model.getCursorRow()); + + model.print("\u009B4;8H"); + assertEquals(3, model.getCursorRow()); + assertEquals(7, model.getCursorColumn()); + } + + /** + * Tests the SGR escape sequence. + */ + @Test + public void testSgr() { + model.print("\u009B2;33;41mX"); + + TerminalCell cell = model.getCell(0, 0); + assertNotNull(cell); + assertEquals('X', cell.getCharacter()); + + assertEquals(Color.RED, cell.getBackgroundColor()); + assertEquals(Color.YELLOW, cell.getForegroundColor()); + + model.print("\u009B0m\rX"); + + cell = model.getCell(0, 0); + + assertNotNull(cell); + assertEquals('X', cell.getCharacter()); + + assertEquals(model.getDefaultBackgroundColor(), cell.getBackgroundColor()); + assertEquals(model.getDefaultForegroundColor(), cell.getForegroundColor()); + } + + /** + * Tests saving and restoring the cursor. + */ + @Test + public void testSaveAndRestoreCursor() { + model.setCursorColumn(3); + model.setCursorRow(17); + + model.print("\u009Bs"); + + model.setCursorColumn(5); + model.setCursorRow(23); + + model.print("\u009Bu"); + + assertEquals(3, model.getCursorColumn()); + assertEquals(17, model.getCursorRow()); + } + + /** + * Tests the printing of a simple message. + */ + @Test + public void testPrint() { + model.print("Hi"); + assertEquals('H', model.getCell(0, 0).getCharacter()); + assertEquals('i', model.getCell(1, 0).getCharacter()); + assertNull(model.getCell(2, 0)); + } + + /** + * Tests that the bell is sounded. + */ + @Test + public void testBell() { + final int[] counter = new int[1]; + + BellStrategy strategy = new BellStrategy() { + /* + * (non-Javadoc) + * @see com.loomcom.symon.jterminal.bell.BellStrategy#soundBell() + */ + @Override + public void soundBell() { + counter[0]++; + } + }; + + model.setBellStrategy(strategy); + + model.print("\u0007"); + assertEquals(1, counter[0]); + + model.print("Hello, \u0007World!\u0007\r\n"); + assertEquals(3, counter[0]); + } + +} +