All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jline.utils.AnsiWriter Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (C) 2009-2018 the original author(s).
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jline.utils;

import java.io.FilterWriter;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Iterator;

/**
 * A ANSI writer extracts ANSI escape codes written to
 * a {@link Writer} and calls corresponding process* methods.
 *
 * For more information about ANSI escape codes, see:
 * http://en.wikipedia.org/wiki/ANSI_escape_code
 *
 * This class just filters out the escape codes so that they are not
 * sent out to the underlying {@link Writer}: process* methods
 * are empty. Subclasses should actually perform the ANSI escape behaviors
 * by implementing active code in process* methods.
 *
 * @author Hiram Chirino
 * @author Joris Kuipers
 * @since 1.0
 */
public class AnsiWriter extends FilterWriter {

    private static final char[] RESET_CODE = "\033[0m".toCharArray();

    public AnsiWriter(Writer out) {
        super(out);
    }

    private final static int MAX_ESCAPE_SEQUENCE_LENGTH = 100;
    private final char[] buffer = new char[MAX_ESCAPE_SEQUENCE_LENGTH];
    private int pos = 0;
    private int startOfValue;
    private final ArrayList options = new ArrayList<>();

    private static final int LOOKING_FOR_FIRST_ESC_CHAR = 0;
    private static final int LOOKING_FOR_SECOND_ESC_CHAR = 1;
    private static final int LOOKING_FOR_NEXT_ARG = 2;
    private static final int LOOKING_FOR_STR_ARG_END = 3;
    private static final int LOOKING_FOR_INT_ARG_END = 4;
    private static final int LOOKING_FOR_OSC_COMMAND = 5;
    private static final int LOOKING_FOR_OSC_COMMAND_END = 6;
    private static final int LOOKING_FOR_OSC_PARAM = 7;
    private static final int LOOKING_FOR_ST = 8;
    private static final int LOOKING_FOR_CHARSET = 9;

    int state = LOOKING_FOR_FIRST_ESC_CHAR;

    private static final int FIRST_ESC_CHAR = 27;
    private static final int SECOND_ESC_CHAR = '[';
    private static final int SECOND_OSC_CHAR = ']';
    private static final int BEL = 7;
    private static final int SECOND_ST_CHAR = '\\';
    private static final int SECOND_CHARSET0_CHAR = '(';
    private static final int SECOND_CHARSET1_CHAR = ')';

    @Override
    public synchronized void write(int data) throws IOException {
        switch (state) {
            case LOOKING_FOR_FIRST_ESC_CHAR:
                if (data == FIRST_ESC_CHAR) {
                    buffer[pos++] = (char) data;
                    state = LOOKING_FOR_SECOND_ESC_CHAR;
                } else {
                    out.write(data);
                }
                break;

            case LOOKING_FOR_SECOND_ESC_CHAR:
                buffer[pos++] = (char) data;
                if (data == SECOND_ESC_CHAR) {
                    state = LOOKING_FOR_NEXT_ARG;
                } else if (data == SECOND_OSC_CHAR) {
                    state = LOOKING_FOR_OSC_COMMAND;
                } else if (data == SECOND_CHARSET0_CHAR) {
                    options.add((int) '0');
                    state = LOOKING_FOR_CHARSET;
                } else if (data == SECOND_CHARSET1_CHAR) {
                    options.add((int) '1');
                    state = LOOKING_FOR_CHARSET;
                } else {
                    reset(false);
                }
                break;

            case LOOKING_FOR_NEXT_ARG:
                buffer[pos++] = (char) data;
                if ('"' == data) {
                    startOfValue = pos - 1;
                    state = LOOKING_FOR_STR_ARG_END;
                } else if ('0' <= data && data <= '9') {
                    startOfValue = pos - 1;
                    state = LOOKING_FOR_INT_ARG_END;
                } else if (';' == data) {
                    options.add(null);
                } else if ('?' == data) {
                    options.add('?');
                } else if ('=' == data) {
                    options.add('=');
                } else {
                    boolean skip = true;
                    try {
                        skip = processEscapeCommand(options, data);
                    } finally {
                        reset(skip);
                    }
                }
                break;
            default:
                break;

            case LOOKING_FOR_INT_ARG_END:
                buffer[pos++] = (char) data;
                if (!('0' <= data && data <= '9')) {
                    String strValue = new String(buffer, startOfValue, (pos - 1) - startOfValue);
                    Integer value = Integer.valueOf(strValue);
                    options.add(value);
                    if (data == ';') {
                        state = LOOKING_FOR_NEXT_ARG;
                    } else {
                        boolean skip = true;
                        try {
                            skip = processEscapeCommand(options, data);
                        } finally {
                            reset(skip);
                        }
                    }
                }
                break;

            case LOOKING_FOR_STR_ARG_END:
                buffer[pos++] = (char) data;
                if ('"' != data) {
                    String value = new String(buffer, startOfValue, (pos - 1) - startOfValue);
                    options.add(value);
                    if (data == ';') {
                        state = LOOKING_FOR_NEXT_ARG;
                    } else {
                        reset(processEscapeCommand(options, data));
                    }
                }
                break;

            case LOOKING_FOR_OSC_COMMAND:
                buffer[pos++] = (char) data;
                if ('0' <= data && data <= '9') {
                    startOfValue = pos - 1;
                    state = LOOKING_FOR_OSC_COMMAND_END;
                } else {
                    reset(false);
                }
                break;

            case LOOKING_FOR_OSC_COMMAND_END:
                buffer[pos++] = (char) data;
                if (';' == data) {
                    String strValue = new String(buffer, startOfValue, (pos - 1) - startOfValue);
                    Integer value = Integer.valueOf(strValue);
                    options.add(value);
                    startOfValue = pos;
                    state = LOOKING_FOR_OSC_PARAM;
                } else if ('0' <= data && data <= '9') {
                    // already pushed digit to buffer, just keep looking
                } else {
                    // oops, did not expect this
                    reset(false);
                }
                break;

            case LOOKING_FOR_OSC_PARAM:
                buffer[pos++] = (char) data;
                if (BEL == data) {
                    String value = new String(buffer, startOfValue, (pos - 1) - startOfValue);
                    options.add(value);
                    boolean skip = true;
                    try {
                        skip = processOperatingSystemCommand(options);
                    } finally {
                        reset(skip);
                    }
                } else if (FIRST_ESC_CHAR == data) {
                    state = LOOKING_FOR_ST;
                } else {
                    // just keep looking while adding text
                }
                break;

            case LOOKING_FOR_ST:
                buffer[pos++] = (char) data;
                if (SECOND_ST_CHAR == data) {
                    String value = new String(buffer, startOfValue, (pos - 2) - startOfValue);
                    options.add(value);
                    boolean skip = true;
                    try {
                        skip = processOperatingSystemCommand(options);
                    } finally {
                        reset(skip);
                    }
                } else {
                    state = LOOKING_FOR_OSC_PARAM;
                }
                break;

            case LOOKING_FOR_CHARSET:
                options.add((char) data);
                reset(processCharsetSelect(options));
                break;
        }

        // Is it just too long?
        if (pos >= buffer.length) {
            reset(false);
        }
    }

    /**
     * Resets all state to continue with regular parsing
     * @param skipBuffer if current buffer should be skipped or written to out
     * @throws IOException if an error occurs
     */
    private void reset(boolean skipBuffer) throws IOException {
        if (!skipBuffer) {
            out.write(buffer, 0, pos);
        }
        pos = 0;
        startOfValue = 0;
        options.clear();
        state = LOOKING_FOR_FIRST_ESC_CHAR;
    }

    /**
     * Helper for processEscapeCommand() to iterate over integer options
     * @param  optionsIterator  the underlying iterator
     * @throws IOException      if no more non-null values left
     */
    private int getNextOptionInt(Iterator optionsIterator) throws IOException {
        for (;;) {
            if (!optionsIterator.hasNext())
                throw new IllegalArgumentException();
            Object arg = optionsIterator.next();
            if (arg != null)
                return (Integer) arg;
        }
    }

    /**
     * Process escape command
     * @param options the list of options
     * @param command the command
     * @throws IOException if an error occurs
     * @return true if the escape command was processed.
     */
    private boolean processEscapeCommand(ArrayList options, int command) throws IOException {
        try {
            switch (command) {
                case 'A':
                    processCursorUp(optionInt(options, 0, 1));
                    return true;
                case 'B':
                    processCursorDown(optionInt(options, 0, 1));
                    return true;
                case 'C':
                    processCursorRight(optionInt(options, 0, 1));
                    return true;
                case 'D':
                    processCursorLeft(optionInt(options, 0, 1));
                    return true;
                case 'E':
                    processCursorDownLine(optionInt(options, 0, 1));
                    return true;
                case 'F':
                    processCursorUpLine(optionInt(options, 0, 1));
                    return true;
                case 'G':
                    processCursorToColumn(optionInt(options, 0));
                    return true;
                case 'H':
                case 'f':
                    processCursorTo(optionInt(options, 0, 1), optionInt(options, 1, 1));
                    return true;
                case 'J':
                    processEraseScreen(optionInt(options, 0, 0));
                    return true;
                case 'K':
                    processEraseLine(optionInt(options, 0, 0));
                    return true;
                case 'L':
                    processInsertLine(optionInt(options, 0, 1));
                    return true;
                case 'M':
                    processDeleteLine(optionInt(options, 0, 1));
                    return true;
                case 'S':
                    processScrollUp(optionInt(options, 0, 1));
                    return true;
                case 'T':
                    processScrollDown(optionInt(options, 0, 1));
                    return true;
                case 'm':
                    // Validate all options are ints...
                    for (Object next : options) {
                        if (next != null && next.getClass() != Integer.class) {
                            throw new IllegalArgumentException();
                        }
                    }

                    int count = 0;
                    Iterator optionsIterator = options.iterator();
                    while (optionsIterator.hasNext()) {
                        Object next = optionsIterator.next();
                        if (next != null) {
                            count++;
                            int value = (Integer) next;
                            if (30 <= value && value <= 37) {
                                processSetForegroundColor(value - 30);
                            } else if (40 <= value && value <= 47) {
                                processSetBackgroundColor(value - 40);
                            } else if (90 <= value && value <= 97) {
                                processSetForegroundColor(value - 90, true);
                            } else if (100 <= value && value <= 107) {
                                processSetBackgroundColor(value - 100, true);
                            } else if (value == 38 || value == 48) {
                                // extended color like `esc[38;5;m` or `esc[38;2;;;m`
                                int arg2or5 = getNextOptionInt(optionsIterator);
                                if (arg2or5 == 2) {
                                    // 24 bit color style like `esc[38;2;;;m`
                                    int r = getNextOptionInt(optionsIterator);
                                    int g = getNextOptionInt(optionsIterator);
                                    int b = getNextOptionInt(optionsIterator);
                                    if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) {
                                        if (value == 38)
                                            processSetForegroundColorExt(r, g, b);
                                        else
                                            processSetBackgroundColorExt(r, g, b);
                                    } else {
                                        throw new IllegalArgumentException();
                                    }
                                }
                                else if (arg2or5 == 5) {
                                    // 256 color style like `esc[38;5;m`
                                    int paletteIndex = getNextOptionInt(optionsIterator);
                                    if (paletteIndex >= 0 && paletteIndex <= 255) {
                                        if (value == 38)
                                            processSetForegroundColorExt(paletteIndex);
                                        else
                                            processSetBackgroundColorExt(paletteIndex);
                                    } else {
                                        throw new IllegalArgumentException();
                                    }
                                }
                                else {
                                    throw new IllegalArgumentException();
                                }
                            } else {
                                switch (value) {
                                    case 39:
                                        processDefaultTextColor();
                                        break;
                                    case 49:
                                        processDefaultBackgroundColor();
                                        break;
                                    case 0:
                                        processAttributeRest();
                                        break;
                                    default:
                                        processSetAttribute(value);
                                }
                            }
                        }
                    }
                    if (count == 0) {
                        processAttributeRest();
                    }
                    return true;
                case 's':
                    processSaveCursorPosition();
                    return true;
                case 'u':
                    processRestoreCursorPosition();
                    return true;

                default:
                    if ('a' <= command && 'z' <= command) {
                        processUnknownExtension(options, command);
                        return true;
                    }
                    if ('A' <= command && 'Z' <= command) {
                        processUnknownExtension(options, command);
                        return true;
                    }
                    return false;
            }
        } catch (IllegalArgumentException ignore) {
        }
        return false;
    }

    /**
     * Process operating system command.
     * @param options the options list
     * @return true if the operating system command was processed.
     */
    private boolean processOperatingSystemCommand(ArrayList options) throws IOException {
        int command = optionInt(options, 0);
        String label = (String) options.get(1);
        // for command > 2 label could be composed (i.e. contain ';'), but we'll leave
        // it to processUnknownOperatingSystemCommand implementations to handle that
        try {
            switch (command) {
                case 0:
                    processChangeIconNameAndWindowTitle(label);
                    return true;
                case 1:
                    processChangeIconName(label);
                    return true;
                case 2:
                    processChangeWindowTitle(label);
                    return true;

                default:
                    // not exactly unknown, but not supported through dedicated process methods:
                    processUnknownOperatingSystemCommand(command, label);
                    return true;
            }
        } catch (IllegalArgumentException ignore) {
        }
        return false;
    }

    /**
     * Process CSI u ANSI code, corresponding to RCP – Restore Cursor Position
     * @throws IOException if an error occurs
     */
    protected void processRestoreCursorPosition() throws IOException {
    }

    /**
     * Process CSI s ANSI code, corresponding to SCP – Save Cursor Position
     * @throws IOException if an error occurs
     */
    protected void processSaveCursorPosition() throws IOException {
    }

    /**
     * Process CSI s ANSI code, corresponding to IL – Insert Line
     * @param optionInt the option
     * @throws IOException if an error occurs
     */
    protected void processInsertLine(int optionInt) throws IOException {
    }

    /**
     * Process CSI s ANSI code, corresponding to DL – Delete Line
     * @param optionInt the option
     * @throws IOException if an error occurs
     */
    protected void processDeleteLine(int optionInt) throws IOException {
    }

    /**
     * Process CSI n T ANSI code, corresponding to SD – Scroll Down
     * @param optionInt the option
     * @throws IOException if an error occurs
     */
    protected void processScrollDown(int optionInt) throws IOException {
    }

    /**
     * Process CSI n U ANSI code, corresponding to SU – Scroll Up
     * @param optionInt the option
     * @throws IOException if an error occurs
     */
    protected void processScrollUp(int optionInt) throws IOException {
    }

    protected static final int ERASE_SCREEN_TO_END = 0;
    protected static final int ERASE_SCREEN_TO_BEGINING = 1;
    protected static final int ERASE_SCREEN = 2;

    /**
     * Process CSI n J ANSI code, corresponding to ED – Erase in Display
     * @param eraseOption the erase option
     * @throws IOException if an error occurs
     */
    protected void processEraseScreen(int eraseOption) throws IOException {
    }

    protected static final int ERASE_LINE_TO_END = 0;
    protected static final int ERASE_LINE_TO_BEGINING = 1;
    protected static final int ERASE_LINE = 2;

    /**
     * Process CSI n K ANSI code, corresponding to ED – Erase in Line
     * @param eraseOption the erase option
     * @throws IOException if an error occurs
     */
    protected void processEraseLine(int eraseOption) throws IOException {
    }

    protected static final int ATTRIBUTE_INTENSITY_BOLD = 1; // 	Intensity: Bold
    protected static final int ATTRIBUTE_INTENSITY_FAINT = 2; // 	Intensity; Faint 	not widely supported
    protected static final int ATTRIBUTE_ITALIC = 3; // 	Italic; on 	not widely supported. Sometimes treated as inverse.
    protected static final int ATTRIBUTE_UNDERLINE = 4; // 	Underline; Single
    protected static final int ATTRIBUTE_BLINK_SLOW = 5; // 	Blink; Slow 	less than 150 per minute
    protected static final int ATTRIBUTE_BLINK_FAST = 6; // 	Blink; Rapid 	MS-DOS ANSI.SYS; 150 per minute or more
    protected static final int ATTRIBUTE_NEGATIVE_ON = 7; // 	Image; Negative 	inverse or reverse; swap foreground and background
    protected static final int ATTRIBUTE_CONCEAL_ON = 8; // 	Conceal on
    protected static final int ATTRIBUTE_UNDERLINE_DOUBLE = 21; // 	Underline; Double 	not widely supported
    protected static final int ATTRIBUTE_INTENSITY_NORMAL = 22; // 	Intensity; Normal 	not bold and not faint
    protected static final int ATTRIBUTE_UNDERLINE_OFF = 24; // 	Underline; None
    protected static final int ATTRIBUTE_BLINK_OFF = 25; // 	Blink; off
    @Deprecated
    protected static final int ATTRIBUTE_NEGATIVE_Off = 27; // 	Image; Positive
    protected static final int ATTRIBUTE_NEGATIVE_OFF = 27; // 	Image; Positive
    protected static final int ATTRIBUTE_CONCEAL_OFF = 28; // 	Reveal 	conceal off

    /**
     * process SGR other than 0 (reset), 30-39 (foreground),
     * 40-49 (background), 90-97 (foreground high intensity) or
     * 100-107 (background high intensity)
     * @param attribute the attribute to set
     * @throws IOException if an error occurs
     * @see #processAttributeRest()
     * @see #processSetForegroundColor(int)
     * @see #processSetForegroundColor(int, boolean)
     * @see #processSetForegroundColorExt(int)
     * @see #processSetForegroundColorExt(int, int, int)
     * @see #processDefaultTextColor()
     * @see #processDefaultBackgroundColor()
     */
    protected void processSetAttribute(int attribute) throws IOException {
    }

    protected static final int BLACK = 0;
    protected static final int RED = 1;
    protected static final int GREEN = 2;
    protected static final int YELLOW = 3;
    protected static final int BLUE = 4;
    protected static final int MAGENTA = 5;
    protected static final int CYAN = 6;
    protected static final int WHITE = 7;

    /**
     * process SGR 30-37 corresponding to Set text color (foreground).
     * @param color the text color
     * @throws IOException if an error occurs
     */
    protected void processSetForegroundColor(int color) throws IOException {
        processSetForegroundColor(color, false);
    }

    /**
     * process SGR 30-37 or SGR 90-97 corresponding to
     * Set text color (foreground) either in normal mode or high intensity.
     * @param color the text color
     * @param bright is high intensity?
     * @throws IOException if an error occurs
     */
    protected void processSetForegroundColor(int color, boolean bright) throws IOException {
        processSetForegroundColorExt(bright ? color + 8 : color);
    }

    /**
     * process SGR 38 corresponding to extended set text color (foreground)
     * with a palette of 255 colors.
     * @param paletteIndex the text color in the palette
     * @throws IOException if an error occurs
     */
    protected void processSetForegroundColorExt(int paletteIndex) throws IOException {
    }

    /**
     * process SGR 38 corresponding to extended set text color (foreground)
     * with a 24 bits RGB definition of the color.
     * @param r red
     * @param g green
     * @param b blue
     * @throws IOException if an error occurs
     */
    protected void processSetForegroundColorExt(int r, int g, int b) throws IOException {
        processSetForegroundColorExt(Colors.roundRgbColor(r, g, b, 16));
    }

    /**
     * process SGR 40-47 corresponding to Set background color.
     * @param color the background color
     * @throws IOException if an error occurs
     */
    protected void processSetBackgroundColor(int color) throws IOException {
        processSetBackgroundColor(color, false);
    }

    /**
     * process SGR 40-47 or SGR 100-107 corresponding to
     * Set background color either in normal mode or high intensity.
     * @param color the background color
     * @param bright is high intensity?
     * @throws IOException if an error occurs
     */
    protected void processSetBackgroundColor(int color, boolean bright) throws IOException {
        processSetBackgroundColorExt(bright ? color + 8 : color);
    }

    /**
     * process SGR 48 corresponding to extended set background color
     * with a palette of 255 colors.
     * @param paletteIndex the background color in the palette
     * @throws IOException if an error occurs
     */
    protected void processSetBackgroundColorExt(int paletteIndex) throws IOException {
    }

    /**
     * process SGR 48 corresponding to extended set background color
     * with a 24 bits RGB definition of the color.
     * @param r red
     * @param g green
     * @param b blue
     * @throws IOException if an error occurs
     */
    protected void processSetBackgroundColorExt(int r, int g, int b) throws IOException {
        processSetBackgroundColorExt(Colors.roundRgbColor(r, g, b, 16));
    }

    /**
     * process SGR 39 corresponding to Default text color (foreground)
     * @throws IOException if an error occurs
     */
    protected void processDefaultTextColor() throws IOException {
    }

    /**
     * process SGR 49 corresponding to Default background color
     * @throws IOException if an error occurs
     */
    protected void processDefaultBackgroundColor() throws IOException {
    }

    /**
     * process SGR 0 corresponding to Reset / Normal
     * @throws IOException if an error occurs
     */
    protected void processAttributeRest() throws IOException {
    }

    /**
     * process CSI n ; m H corresponding to CUP – Cursor Position or
     * CSI n ; m f corresponding to HVP – Horizontal and Vertical Position
     * @param row the row
     * @param col the column
     * @throws IOException if an error occurs
     */
    protected void processCursorTo(int row, int col) throws IOException {
    }

    /**
     * process CSI n G corresponding to CHA – Cursor Horizontal Absolute
     * @param x the column
     * @throws IOException if an error occurs
     */
    protected void processCursorToColumn(int x) throws IOException {
    }

    /**
     * process CSI n F corresponding to CPL – Cursor Previous Line
     * @param count line count
     * @throws IOException if an error occurs
     */
    protected void processCursorUpLine(int count) throws IOException {
    }

    /**
     * process CSI n E corresponding to CNL – Cursor Next Line
     * @param count line count
     * @throws IOException if an error occurs
     */
    protected void processCursorDownLine(int count) throws IOException {
        // Poor mans impl..
        for (int i = 0; i < count; i++) {
            out.write('\n');
        }
    }

    /**
     * process CSI n D corresponding to CUB – Cursor Back
     * @param count the count
     * @throws IOException if an error occurs
     */
    protected void processCursorLeft(int count) throws IOException {
    }

    /**
     * process CSI n C corresponding to CUF – Cursor Forward
     * @param count the count
     * @throws IOException if an error occurs
     */
    protected void processCursorRight(int count) throws IOException {
        // Poor mans impl..
        for (int i = 0; i < count; i++) {
            out.write(' ');
        }
    }

    /**
     * process CSI n B corresponding to CUD – Cursor Down
     * @param count the count
     * @throws IOException if an error occurs
     */
    protected void processCursorDown(int count) throws IOException {
    }

    /**
     * process CSI n A corresponding to CUU – Cursor Up
     * @param count the count
     * @throws IOException if an error occurs
     */
    protected void processCursorUp(int count) throws IOException {
    }

    protected void processUnknownExtension(ArrayList options, int command) {
    }

    /**
     * process OSC 0;text BEL corresponding to Change Window and Icon label
     * @param label the label
     */
    protected void processChangeIconNameAndWindowTitle(String label) {
        processChangeIconName(label);
        processChangeWindowTitle(label);
    }

    /**
     * process OSC 1;text BEL corresponding to Change Icon label
     * @param name the icon name
     */
    protected void processChangeIconName(String name) {
    }

    /**
     * process OSC 2;text BEL corresponding to Change Window title
     * @param title the title
     */
    protected void processChangeWindowTitle(String title) {
    }

    /**
     * Process unknown OSC command.
     * @param command the command
     * @param param the param
     */
    protected void processUnknownOperatingSystemCommand(int command, String param) {
    }

    /**
     * Process character set sequence.
     * @param options
     * @return true if the charcter set select command was processed.
     */
    private boolean processCharsetSelect(ArrayList options) throws IOException {
        int set = optionInt(options, 0);
        char seq = (Character) options.get(1);
        processCharsetSelect(set, seq);
        return true;
    }

    protected void processCharsetSelect(int set, char seq) {
    }

    private int optionInt(ArrayList options, int index) {
        if (options.size() <= index)
            throw new IllegalArgumentException();
        Object value = options.get(index);
        if (value == null)
            throw new IllegalArgumentException();
        if (!value.getClass().equals(Integer.class))
            throw new IllegalArgumentException();
        return (Integer) value;
    }

    private int optionInt(ArrayList options, int index, int defaultValue) {
        if (options.size() > index) {
            Object value = options.get(index);
            if (value == null) {
                return defaultValue;
            }
            return (Integer) value;
        }
        return defaultValue;
    }

    @Override
    public void write(char[] cbuf, int off, int len) throws IOException {
        // TODO: Optimize this
        for (int i = 0; i < len; i++) {
            write(cbuf[off + i]);
        }
    }

    @Override
    public void write(String str, int off, int len) throws IOException {
        // TODO: Optimize this
        for (int i = 0; i < len; i++) {
            write(str.charAt(off + i));
        }
    }

    @Override
    public void close() throws IOException {
        write(RESET_CODE);
        flush();
        super.close();
    }

}