001/*
002 * Copyright (C) 2009-2020 the original author(s).
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.fusesource.jansi.io;
017
018import java.io.FilterOutputStream;
019import java.io.IOException;
020import java.io.OutputStream;
021import java.nio.charset.Charset;
022import java.util.ArrayList;
023
024import org.fusesource.jansi.AnsiColors;
025import org.fusesource.jansi.AnsiMode;
026import org.fusesource.jansi.AnsiType;
027
028/**
029 * A ANSI print stream extracts ANSI escape codes written to 
030 * an output stream and calls corresponding <code>AnsiProcessor.process*</code> methods.
031 * This particular class is not synchronized for improved performances.
032 *
033 * <p>For more information about ANSI escape codes, see
034 * <a href="http://en.wikipedia.org/wiki/ANSI_escape_code">Wikipedia article</a>
035 *
036 * @author Guillaume Nodet
037 * @since 1.0
038 * @see AnsiProcessor
039 */
040public class AnsiOutputStream extends FilterOutputStream {
041
042    public static final byte[] RESET_CODE = "\033[0m".getBytes();
043
044    public interface IoRunnable {
045        void run() throws IOException;
046    }
047
048    public interface WidthSupplier {
049        int getTerminalWidth();
050    }
051
052    public static class ZeroWidthSupplier implements WidthSupplier {
053        @Override
054        public int getTerminalWidth() {
055            return 0;
056        }
057    }
058
059    private static final int LOOKING_FOR_FIRST_ESC_CHAR = 0;
060    private static final int LOOKING_FOR_SECOND_ESC_CHAR = 1;
061    private static final int LOOKING_FOR_NEXT_ARG = 2;
062    private static final int LOOKING_FOR_STR_ARG_END = 3;
063    private static final int LOOKING_FOR_INT_ARG_END = 4;
064    private static final int LOOKING_FOR_OSC_COMMAND = 5;
065    private static final int LOOKING_FOR_OSC_COMMAND_END = 6;
066    private static final int LOOKING_FOR_OSC_PARAM = 7;
067    private static final int LOOKING_FOR_ST = 8;
068    private static final int LOOKING_FOR_CHARSET = 9;
069
070    private static final int FIRST_ESC_CHAR = 27;
071    private static final int SECOND_ESC_CHAR = '[';
072    private static final int SECOND_OSC_CHAR = ']';
073    private static final int BEL = 7;
074    private static final int SECOND_ST_CHAR = '\\';
075    private static final int SECOND_CHARSET0_CHAR = '(';
076    private static final int SECOND_CHARSET1_CHAR = ')';
077
078    private AnsiProcessor ap;
079    private final static int MAX_ESCAPE_SEQUENCE_LENGTH = 100;
080    private final byte[] buffer = new byte[MAX_ESCAPE_SEQUENCE_LENGTH];
081    private int pos = 0;
082    private int startOfValue;
083    private final ArrayList<Object> options = new ArrayList<Object>();
084    private int state = LOOKING_FOR_FIRST_ESC_CHAR;
085    private final Charset cs;
086
087    private final WidthSupplier width;
088    private final AnsiProcessor processor;
089    private final AnsiType type;
090    private final AnsiColors colors;
091    private final IoRunnable installer;
092    private final IoRunnable uninstaller;
093    private AnsiMode mode;
094    private boolean resetAtUninstall;
095
096    public AnsiOutputStream(OutputStream os, WidthSupplier width, AnsiMode mode,
097                            AnsiProcessor processor, AnsiType type, AnsiColors colors,
098                            Charset cs, IoRunnable installer, IoRunnable uninstaller, boolean resetAtUninstall) {
099        super(os);
100        this.width = width;
101        this.processor = processor;
102        this.type = type;
103        this.colors = colors;
104        this.installer = installer;
105        this.uninstaller = uninstaller;
106        this.resetAtUninstall = resetAtUninstall;
107        this.cs = cs;
108        setMode(mode);
109    }
110
111    public int getTerminalWidth() {
112        return width.getTerminalWidth();
113    }
114
115    public AnsiType getType() {
116        return type;
117    }
118
119    public AnsiColors getColors() {
120        return colors;
121    }
122
123    public AnsiMode getMode() {
124        return mode;
125    }
126
127    public void setMode(AnsiMode mode) {
128        ap = mode == AnsiMode.Strip
129                ? new AnsiProcessor(out)
130                : mode == AnsiMode.Force || processor == null ? new ColorsAnsiProcessor(out, colors) : processor;
131        this.mode = mode;
132    }
133
134    public boolean isResetAtUninstall() {
135        return resetAtUninstall;
136    }
137
138    public void setResetAtUninstall(boolean resetAtUninstall) {
139        this.resetAtUninstall = resetAtUninstall;
140    }
141
142    /**
143     * {@inheritDoc}
144     */
145    @Override
146    public void write(int data) throws IOException {
147        switch (state) {
148            case LOOKING_FOR_FIRST_ESC_CHAR:
149                if (data == FIRST_ESC_CHAR) {
150                    buffer[pos++] = (byte) data;
151                    state = LOOKING_FOR_SECOND_ESC_CHAR;
152                } else {
153                    out.write(data);
154                }
155                break;
156
157            case LOOKING_FOR_SECOND_ESC_CHAR:
158                buffer[pos++] = (byte) data;
159                if (data == SECOND_ESC_CHAR) {
160                    state = LOOKING_FOR_NEXT_ARG;
161                } else if (data == SECOND_OSC_CHAR) {
162                    state = LOOKING_FOR_OSC_COMMAND;
163                } else if (data == SECOND_CHARSET0_CHAR) {
164                    options.add(0);
165                    state = LOOKING_FOR_CHARSET;
166                } else if (data == SECOND_CHARSET1_CHAR) {
167                    options.add(1);
168                    state = LOOKING_FOR_CHARSET;
169                } else {
170                    reset(false);
171                }
172                break;
173
174            case LOOKING_FOR_NEXT_ARG:
175                buffer[pos++] = (byte) data;
176                if ('"' == data) {
177                    startOfValue = pos - 1;
178                    state = LOOKING_FOR_STR_ARG_END;
179                } else if ('0' <= data && data <= '9') {
180                    startOfValue = pos - 1;
181                    state = LOOKING_FOR_INT_ARG_END;
182                } else if (';' == data) {
183                    options.add(null);
184                } else if ('?' == data) {
185                    options.add('?');
186                } else if ('=' == data) {
187                    options.add('=');
188                } else {
189                    processEscapeCommand(data);
190                }
191                break;
192            default:
193                break;
194
195            case LOOKING_FOR_INT_ARG_END:
196                buffer[pos++] = (byte) data;
197                if (!('0' <= data && data <= '9')) {
198                    String strValue = new String(buffer, startOfValue, (pos - 1) - startOfValue);
199                    Integer value = Integer.valueOf(strValue);
200                    options.add(value);
201                    if (data == ';') {
202                        state = LOOKING_FOR_NEXT_ARG;
203                    } else {
204                        processEscapeCommand(data);
205                    }
206                }
207                break;
208
209            case LOOKING_FOR_STR_ARG_END:
210                buffer[pos++] = (byte) data;
211                if ('"' != data) {
212                    String value = new String(buffer, startOfValue, (pos - 1) - startOfValue, cs);
213                    options.add(value);
214                    if (data == ';') {
215                        state = LOOKING_FOR_NEXT_ARG;
216                    } else {
217                        processEscapeCommand(data);
218                    }
219                }
220                break;
221
222            case LOOKING_FOR_OSC_COMMAND:
223                buffer[pos++] = (byte) data;
224                if ('0' <= data && data <= '9') {
225                    startOfValue = pos - 1;
226                    state = LOOKING_FOR_OSC_COMMAND_END;
227                } else {
228                    reset(false);
229                }
230                break;
231
232            case LOOKING_FOR_OSC_COMMAND_END:
233                buffer[pos++] = (byte) data;
234                if (';' == data) {
235                    String strValue = new String(buffer, startOfValue, (pos - 1) - startOfValue);
236                    Integer value = Integer.valueOf(strValue);
237                    options.add(value);
238                    startOfValue = pos;
239                    state = LOOKING_FOR_OSC_PARAM;
240                } else if ('0' <= data && data <= '9') {
241                    // already pushed digit to buffer, just keep looking
242                } else {
243                    // oops, did not expect this
244                    reset(false);
245                }
246                break;
247
248            case LOOKING_FOR_OSC_PARAM:
249                buffer[pos++] = (byte) data;
250                if (BEL == data) {
251                    String value = new String(buffer, startOfValue, (pos - 1) - startOfValue, cs);
252                    options.add(value);
253                    processOperatingSystemCommand();
254                } else if (FIRST_ESC_CHAR == data) {
255                    state = LOOKING_FOR_ST;
256                } else {
257                    // just keep looking while adding text
258                }
259                break;
260
261            case LOOKING_FOR_ST:
262                buffer[pos++] = (byte) data;
263                if (SECOND_ST_CHAR == data) {
264                    String value = new String(buffer, startOfValue, (pos - 2) - startOfValue, cs);
265                    options.add(value);
266                    processOperatingSystemCommand();
267                } else {
268                    state = LOOKING_FOR_OSC_PARAM;
269                }
270                break;
271
272            case LOOKING_FOR_CHARSET:
273                options.add((char) data);
274                processCharsetSelect();
275                break;
276        }
277
278        // Is it just too long?
279        if (pos >= buffer.length) {
280            reset(false);
281        }
282    }
283
284    private void processCharsetSelect() throws IOException {
285        try {
286            reset(ap != null && ap.processCharsetSelect(options));
287        } catch (RuntimeException e) {
288            reset(true);
289            throw e;
290        }
291    }
292
293    private void processOperatingSystemCommand() throws IOException {
294        try {
295            reset(ap != null && ap.processOperatingSystemCommand(options));
296        } catch (RuntimeException e) {
297            reset(true);
298            throw e;
299        }
300    }
301
302    private void processEscapeCommand(int data) throws IOException {
303        try {
304            reset(ap != null && ap.processEscapeCommand(options, data));
305        } catch (RuntimeException e) {
306            reset(true);
307            throw e;
308        }
309    }
310
311    /**
312     * Resets all state to continue with regular parsing
313     * @param skipBuffer if current buffer should be skipped or written to out
314     * @throws IOException
315     */
316    private void reset(boolean skipBuffer) throws IOException {
317        if (!skipBuffer) {
318            out.write(buffer, 0, pos);
319        }
320        pos = 0;
321        startOfValue = 0;
322        options.clear();
323        state = LOOKING_FOR_FIRST_ESC_CHAR;
324    }
325
326    public void install() throws IOException {
327        if (installer != null) {
328            installer.run();
329        }
330    }
331
332    public void uninstall() throws IOException {
333        if (resetAtUninstall
334                && type != AnsiType.Redirected
335                && type != AnsiType.Unsupported) {
336            setMode(AnsiMode.Default);
337            write(RESET_CODE);
338            flush();
339        }
340        if (uninstaller != null) {
341            uninstaller.run();
342        }
343    }
344
345    @Override
346    public void close() throws IOException {
347        uninstall();
348        super.close();
349    }
350}