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

com.googlecode.lanterna.terminal.ansi.ANSITerminal Maven / Gradle / Ivy

There is a newer version: 3.2.0-alpha1
Show newest version
/*
 * This file is part of lanterna (http://code.google.com/p/lanterna/).
 *
 * lanterna is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see .
 *
 * Copyright (C) 2010-2017 Martin Berglund
 */
package com.googlecode.lanterna.terminal.ansi;

import com.googlecode.lanterna.SGR;
import com.googlecode.lanterna.TerminalPosition;
import com.googlecode.lanterna.input.*;
import com.googlecode.lanterna.TerminalSize;
import com.googlecode.lanterna.TextColor;
import com.googlecode.lanterna.terminal.ExtendedTerminal;
import com.googlecode.lanterna.terminal.MouseCaptureMode;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;

/**
 * Class containing graphics code for ANSI compliant text terminals and terminal emulators. All the methods inside of
 * this class uses ANSI escape codes written to the underlying output stream.
 *
 * @see Wikipedia
 * @author Martin
 */
public abstract class ANSITerminal extends StreamBasedTerminal implements ExtendedTerminal {

    private MouseCaptureMode requestedMouseCaptureMode;
    private MouseCaptureMode mouseCaptureMode;
    private boolean inPrivateMode;

    @SuppressWarnings("WeakerAccess")
    protected ANSITerminal(
            InputStream terminalInput,
            OutputStream terminalOutput,
            Charset terminalCharset) {

        super(terminalInput, terminalOutput, terminalCharset);
        this.inPrivateMode = false;
        this.requestedMouseCaptureMode = null;
        this.mouseCaptureMode = null;
        getInputDecoder().addProfile(getDefaultKeyDecodingProfile());
    }

    /**
     * This method can be overridden in a custom terminal implementation to change the default key decoders.
     * @return The KeyDecodingProfile used by the terminal when translating character sequences to keystrokes
     */
    protected KeyDecodingProfile getDefaultKeyDecodingProfile() {
        return new DefaultKeyDecodingProfile();
    }

    private void writeCSISequenceToTerminal(byte... tail) throws IOException {
        byte[] completeSequence = new byte[tail.length + 2];
        completeSequence[0] = (byte)0x1b;
        completeSequence[1] = (byte)'[';
        System.arraycopy(tail, 0, completeSequence, 2, tail.length);
        writeToTerminal(completeSequence);
    }

    private void writeSGRSequenceToTerminal(byte... sgrParameters) throws IOException {
        byte[] completeSequence = new byte[sgrParameters.length + 3];
        completeSequence[0] = (byte)0x1b;
        completeSequence[1] = (byte)'[';
        completeSequence[completeSequence.length - 1] = (byte)'m';
        System.arraycopy(sgrParameters, 0, completeSequence, 2, sgrParameters.length);
        writeToTerminal(completeSequence);
    }

    private void writeOSCSequenceToTerminal(byte... tail) throws IOException {
        byte[] completeSequence = new byte[tail.length + 2];
        completeSequence[0] = (byte)0x1b;
        completeSequence[1] = (byte)']';
        System.arraycopy(tail, 0, completeSequence, 2, tail.length);
        writeToTerminal(completeSequence);
    }

    // Final because we handle the onResized logic here; extending classes should override #findTerminalSize instead
    @Override
    public final synchronized TerminalSize getTerminalSize() throws IOException {
        TerminalSize size = findTerminalSize();
        onResized(size);
        return size;
    }

    protected TerminalSize findTerminalSize() throws IOException {
        saveCursorPosition();
        setCursorPosition(5000, 5000);
        resetMemorizedCursorPosition();
        reportPosition();
        restoreCursorPosition();
        TerminalPosition terminalPosition = waitForCursorPositionReport();
        if (terminalPosition == null) {
            terminalPosition = new TerminalPosition(80,24);
        }
        return new TerminalSize(terminalPosition.getColumn(), terminalPosition.getRow());
    }

    @Override
    public void setTerminalSize(int columns, int rows) throws IOException {
        writeCSISequenceToTerminal(("8;" + rows + ";" + columns + "t").getBytes());

        //We can't trust that the previous call was honoured by the terminal so force a re-query here, which will
        //trigger a resize event if one actually took place
        getTerminalSize();
    }

    @Override
    public void setTitle(String title) throws IOException {
        //The bell character is our 'null terminator', make sure there's none in the title
        title = title.replace("\007", "");
        writeOSCSequenceToTerminal(("2;" + title + "\007").getBytes());
    }

    @Override
    public void setForegroundColor(TextColor color) throws IOException {
        writeSGRSequenceToTerminal(color.getForegroundSGRSequence());
    }

    @Override
    public void setBackgroundColor(TextColor color) throws IOException {
        writeSGRSequenceToTerminal(color.getBackgroundSGRSequence());
    }

    @Override
    public void enableSGR(SGR sgr) throws IOException {
        switch(sgr) {
            case BLINK:
                writeCSISequenceToTerminal((byte) '5', (byte) 'm');
                break;
            case BOLD:
                writeCSISequenceToTerminal((byte) '1', (byte) 'm');
                break;
            case BORDERED:
                writeCSISequenceToTerminal((byte) '5', (byte) '1', (byte) 'm');
                break;
            case CIRCLED:
                writeCSISequenceToTerminal((byte) '5', (byte) '2', (byte) 'm');
                break;
            case CROSSED_OUT:
                writeCSISequenceToTerminal((byte) '9', (byte) 'm');
                break;
            case FRAKTUR:
                writeCSISequenceToTerminal((byte) '2', (byte) '0', (byte) 'm');
                break;
            case REVERSE:
                writeCSISequenceToTerminal((byte) '7', (byte) 'm');
                break;
            case UNDERLINE:
                writeCSISequenceToTerminal((byte) '4', (byte) 'm');
                break;
            case ITALIC:
                writeCSISequenceToTerminal((byte) '3', (byte) 'm');
                break;
        }
    }

    @Override
    public void disableSGR(SGR sgr) throws IOException {
        switch(sgr) {
            case BLINK:
                writeCSISequenceToTerminal((byte) '2', (byte) '5', (byte) 'm');
                break;
            case BOLD:
                writeCSISequenceToTerminal((byte) '2', (byte) '2', (byte) 'm');
                break;
            case BORDERED:
                writeCSISequenceToTerminal((byte) '5', (byte) '4', (byte) 'm');
                break;
            case CIRCLED:
                writeCSISequenceToTerminal((byte) '5', (byte) '4', (byte) 'm');
                break;
            case CROSSED_OUT:
                writeCSISequenceToTerminal((byte) '2', (byte) '9', (byte) 'm');
                break;
            case FRAKTUR:
                writeCSISequenceToTerminal((byte) '2', (byte) '3', (byte) 'm');
                break;
            case REVERSE:
                writeCSISequenceToTerminal((byte) '2', (byte) '7', (byte) 'm');
                break;
            case UNDERLINE:
                writeCSISequenceToTerminal((byte) '2', (byte) '4', (byte) 'm');
                break;
            case ITALIC:
                writeCSISequenceToTerminal((byte) '2', (byte) '3', (byte) 'm');
                break;
        }
    }

    @Override
    public void resetColorAndSGR() throws IOException {
        writeCSISequenceToTerminal((byte) '0', (byte) 'm');
    }

    @Override
    public void clearScreen() throws IOException {
        writeCSISequenceToTerminal((byte) '2', (byte) 'J');
    }

    @Override
    public void enterPrivateMode() throws IOException {
        if(inPrivateMode) {
            throw new IllegalStateException("Cannot call enterPrivateMode() when already in private mode");
        }
        writeCSISequenceToTerminal((byte) '?', (byte) '1', (byte) '0', (byte) '4', (byte) '9', (byte) 'h');
        if (requestedMouseCaptureMode != null) {
            this.mouseCaptureMode = requestedMouseCaptureMode;
            updateMouseCaptureMode(this.mouseCaptureMode, 'h');
        }
        flush();
        inPrivateMode = true;
    }

    @Override
    public void exitPrivateMode() throws IOException {
        if(!inPrivateMode) {
            throw new IllegalStateException("Cannot call exitPrivateMode() when not in private mode");
        }
        resetColorAndSGR();
        setCursorVisible(true);
        writeCSISequenceToTerminal((byte) '?', (byte) '1', (byte) '0', (byte) '4', (byte) '9', (byte) 'l');
        if (null != mouseCaptureMode) {
            updateMouseCaptureMode(this.mouseCaptureMode, 'l');
            this.mouseCaptureMode = null;
        }
        flush();
        inPrivateMode = false;
    }

    @Override
    public void close() throws IOException {
        if(isInPrivateMode()) {
            exitPrivateMode();
        }
        super.close();
    }

    @Override
    public void setCursorPosition(int x, int y) throws IOException {
        writeCSISequenceToTerminal(((y + 1) + ";" + (x + 1) + "H").getBytes());
    }

    @Override
    public void setCursorPosition(TerminalPosition position) throws IOException {
        setCursorPosition(position.getColumn(), position.getRow());
    }

    @Override
    public synchronized TerminalPosition getCursorPosition() throws IOException {
        resetMemorizedCursorPosition();
        reportPosition();

        // ANSI terminal positions are 1-indexed so top-left corner is 1x1 instead of 0x0, that's why we need to adjust it here
        TerminalPosition terminalPosition = waitForCursorPositionReport();
        if (terminalPosition == null) {
            terminalPosition = TerminalPosition.OFFSET_1x1;
        }
        return terminalPosition.withRelative(-1, -1);
    }

    @Override
    public void setCursorVisible(boolean visible) throws IOException {
        writeCSISequenceToTerminal(("?25" + (visible ? "h" : "l")).getBytes());
    }

    @Override
    public KeyStroke readInput() throws IOException {
        KeyStroke keyStroke;
        do {
            // KeyStroke may because null by filterMouseEvents, so that's why we have the while(true) loop here
            keyStroke = filterMouseEvents(super.readInput());
        } while(keyStroke == null);
        return keyStroke;
    }

    @Override
    public KeyStroke pollInput() throws IOException {
        return filterMouseEvents(super.pollInput());
    }

    private KeyStroke filterMouseEvents(KeyStroke keyStroke) {
        //Remove bad input events from terminals that are not following the xterm protocol properly
        if(keyStroke == null || keyStroke.getKeyType() != KeyType.MouseEvent) {
            return keyStroke;
        }

        MouseAction mouseAction = (MouseAction)keyStroke;
        switch(mouseAction.getActionType()) {
            case CLICK_RELEASE:
                if(mouseCaptureMode == MouseCaptureMode.CLICK) {
                    return null;
                }
                break;
            case DRAG:
                if(mouseCaptureMode == MouseCaptureMode.CLICK ||
                        mouseCaptureMode == MouseCaptureMode.CLICK_RELEASE) {
                    return null;
                }
                break;
            case MOVE:
                if(mouseCaptureMode == MouseCaptureMode.CLICK ||
                        mouseCaptureMode == MouseCaptureMode.CLICK_RELEASE ||
                        mouseCaptureMode == MouseCaptureMode.CLICK_RELEASE_DRAG) {
                    return null;
                }
                break;
            default:
        }
        return mouseAction;
    }

    @Override
    public void pushTitle() throws IOException {
        throw new UnsupportedOperationException("Not implemented yet");
    }

    @Override
    public void popTitle() throws IOException {
        throw new UnsupportedOperationException("Not implemented yet");
    }

    @Override
    public void iconify() throws IOException {
        writeCSISequenceToTerminal((byte)'2', (byte)'t');
    }

    @Override
    public void deiconify() throws IOException {
        writeCSISequenceToTerminal((byte)'1', (byte)'t');
    }

    @Override
    public void maximize() throws IOException {
        writeCSISequenceToTerminal((byte)'9', (byte)';', (byte)'1', (byte)'t');
    }

    @Override
    public void unmaximize() throws IOException {
        writeCSISequenceToTerminal((byte)'9', (byte)';', (byte)'0', (byte)'t');
    }

    private void updateMouseCaptureMode(MouseCaptureMode mouseCaptureMode, char l_or_h) throws IOException {
        if (mouseCaptureMode == null) { return; }

        switch(mouseCaptureMode) {
        case CLICK:
            writeCSISequenceToTerminal((byte)'?', (byte)'9', (byte)l_or_h);
            break;
        case CLICK_RELEASE:
            writeCSISequenceToTerminal((byte)'?', (byte)'1', (byte)'0', (byte)'0', (byte)'0', (byte)l_or_h);
            break;
        case CLICK_RELEASE_DRAG:
            writeCSISequenceToTerminal((byte)'?', (byte)'1', (byte)'0', (byte)'0', (byte)'2', (byte)l_or_h);
            break;
        case CLICK_RELEASE_DRAG_MOVE:
            writeCSISequenceToTerminal((byte)'?', (byte)'1', (byte)'0', (byte)'0', (byte)'3', (byte)l_or_h);
            break;
        }
        if(getCharset().equals(Charset.forName("UTF-8"))) {
            writeCSISequenceToTerminal((byte)'?', (byte)'1', (byte)'0', (byte)'0', (byte)'5', (byte)l_or_h);
        }
    }

    @Override
    public void setMouseCaptureMode(MouseCaptureMode mouseCaptureMode) throws IOException {
        requestedMouseCaptureMode = mouseCaptureMode;
        if (inPrivateMode && requestedMouseCaptureMode != this.mouseCaptureMode) {
            updateMouseCaptureMode(this.mouseCaptureMode, 'l');
            this.mouseCaptureMode = requestedMouseCaptureMode;
            updateMouseCaptureMode(this.mouseCaptureMode, 'h');
        }
    }

    @Override
    public void scrollLines(int firstLine, int lastLine, int distance) throws IOException {
        final String CSI = "\033[";

        // some sanity checks:
        if (distance == 0) { return; }
        if (firstLine < 0) { firstLine = 0; }
        if (lastLine < firstLine) { return; }
        StringBuilder sb = new StringBuilder();

        // define range:
        sb.append(CSI).append(firstLine+1)
          .append(';').append(lastLine+1).append('r');

        // place cursor on line to scroll away from:
        int target = distance > 0 ? lastLine : firstLine;
        sb.append(CSI).append(target+1).append(";1H");

        // do scroll:
        if (distance > 0) {
            int num = Math.min( distance, lastLine - firstLine + 1);
            for (int i = 0; i < num; i++) { sb.append('\n'); }
        } else { // distance < 0
            int num = Math.min( -distance, lastLine - firstLine + 1);
            for (int i = 0; i < num; i++) { sb.append("\033M"); }
        }

        // reset range:
        sb.append(CSI).append('r');

        // off we go!
        writeToTerminal(sb.toString().getBytes());
    }

    /**
     * Method to test if the terminal (as far as the library knows) is in private mode.
     *
     * @return True if there has been a call to enterPrivateMode() but not yet exitPrivateMode()
     */
    boolean isInPrivateMode() {
        return inPrivateMode;
    }

    void reportPosition() throws IOException {
        writeCSISequenceToTerminal("6n".getBytes());
    }

    void restoreCursorPosition() throws IOException {
        writeCSISequenceToTerminal("u".getBytes());
    }

    void saveCursorPosition() throws IOException {
        writeCSISequenceToTerminal("s".getBytes());
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy