001/*
002 * Copyright (C) 2009-2017 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.IOException;
019import java.io.OutputStream;
020import java.util.ArrayList;
021import java.util.Iterator;
022
023/**
024 * ANSI processor providing <code>process*</code> corresponding to ANSI escape codes. 
025 * This class methods implementations are empty: subclasses should actually perform the
026 * ANSI escape behaviors by implementing active code in <code>process*</code> methods.
027 * 
028 * <p>For more information about ANSI escape codes, see
029 * <a href="http://en.wikipedia.org/wiki/ANSI_escape_code">Wikipedia article</a>
030 *
031 * @since 1.19
032 */
033@SuppressWarnings("unused")
034public class AnsiProcessor {
035    protected final OutputStream os;
036
037    public AnsiProcessor(OutputStream os) {
038        this.os = os;
039    }
040
041    /**
042     * Helper for processEscapeCommand() to iterate over integer options
043     * @param  optionsIterator  the underlying iterator
044     * @throws IOException      if no more non-null values left
045     */
046    protected int getNextOptionInt(Iterator<Object> optionsIterator) throws IOException {
047        for (;;) {
048            if (!optionsIterator.hasNext())
049                throw new IllegalArgumentException();
050            Object arg = optionsIterator.next();
051            if (arg != null)
052                return (Integer) arg;
053        }
054    }
055
056    /**
057     * @return true if the escape command was processed.
058     */
059    protected boolean processEscapeCommand(ArrayList<Object> options, int command) throws IOException {
060        try {
061            switch (command) {
062                case 'A':
063                    processCursorUp(optionInt(options, 0, 1));
064                    return true;
065                case 'B':
066                    processCursorDown(optionInt(options, 0, 1));
067                    return true;
068                case 'C':
069                    processCursorRight(optionInt(options, 0, 1));
070                    return true;
071                case 'D':
072                    processCursorLeft(optionInt(options, 0, 1));
073                    return true;
074                case 'E':
075                    processCursorDownLine(optionInt(options, 0, 1));
076                    return true;
077                case 'F':
078                    processCursorUpLine(optionInt(options, 0, 1));
079                    return true;
080                case 'G':
081                    processCursorToColumn(optionInt(options, 0));
082                    return true;
083                case 'H':
084                case 'f':
085                    processCursorTo(optionInt(options, 0, 1), optionInt(options, 1, 1));
086                    return true;
087                case 'J':
088                    processEraseScreen(optionInt(options, 0, 0));
089                    return true;
090                case 'K':
091                    processEraseLine(optionInt(options, 0, 0));
092                    return true;
093                case 'L':
094                    processInsertLine(optionInt(options, 0, 1));
095                    return true;
096                case 'M':
097                    processDeleteLine(optionInt(options, 0, 1));
098                    return true;
099                case 'S':
100                    processScrollUp(optionInt(options, 0, 1));
101                    return true;
102                case 'T':
103                    processScrollDown(optionInt(options, 0, 1));
104                    return true;
105                case 'm':
106                    // Validate all options are ints...
107                    for (Object next : options) {
108                        if (next != null && next.getClass() != Integer.class) {
109                            throw new IllegalArgumentException();
110                        }
111                    }
112
113                    int count = 0;
114                    Iterator<Object> optionsIterator = options.iterator();
115                    while (optionsIterator.hasNext()) {
116                        Object next = optionsIterator.next();
117                        if (next != null) {
118                            count++;
119                            int value = (Integer) next;
120                            if (30 <= value && value <= 37) {
121                                processSetForegroundColor(value - 30);
122                            } else if (40 <= value && value <= 47) {
123                                processSetBackgroundColor(value - 40);
124                            } else if (90 <= value && value <= 97) {
125                                processSetForegroundColor(value - 90, true);
126                            } else if (100 <= value && value <= 107) {
127                                processSetBackgroundColor(value - 100, true);
128                            } else if (value == 38 || value == 48) {
129                                if (!optionsIterator.hasNext()) {
130                                    continue;
131                                }
132                                // extended color like `esc[38;5;<index>m` or `esc[38;2;<r>;<g>;<b>m`
133                                int arg2or5 = getNextOptionInt(optionsIterator);
134                                if (arg2or5 == 2) {
135                                    // 24 bit color style like `esc[38;2;<r>;<g>;<b>m`
136                                    int r = getNextOptionInt(optionsIterator);
137                                    int g = getNextOptionInt(optionsIterator);
138                                    int b = getNextOptionInt(optionsIterator);
139                                    if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) {
140                                        if (value == 38)
141                                            processSetForegroundColorExt(r, g, b);
142                                        else
143                                            processSetBackgroundColorExt(r, g, b);
144                                    } else {
145                                        throw new IllegalArgumentException();
146                                    }
147                                }
148                                else if (arg2or5 == 5) {
149                                    // 256 color style like `esc[38;5;<index>m`
150                                    int paletteIndex = getNextOptionInt(optionsIterator);
151                                    if (paletteIndex >= 0 && paletteIndex <= 255) {
152                                        if (value == 38)
153                                            processSetForegroundColorExt(paletteIndex);
154                                        else
155                                            processSetBackgroundColorExt(paletteIndex);
156                                    } else {
157                                        throw new IllegalArgumentException();
158                                    }
159                                }
160                                else {
161                                    throw new IllegalArgumentException();
162                                }
163                            } else {
164                                switch (value) {
165                                    case 39:
166                                        processDefaultTextColor();
167                                        break;
168                                    case 49:
169                                        processDefaultBackgroundColor();
170                                        break;
171                                    case 0:
172                                        processAttributeReset();
173                                        break;
174                                    default:
175                                        processSetAttribute(value);
176                                }
177                            }
178                        }
179                    }
180                    if (count == 0) {
181                        processAttributeReset();
182                    }
183                    return true;
184                case 's':
185                    processSaveCursorPosition();
186                    return true;
187                case 'u':
188                    processRestoreCursorPosition();
189                    return true;
190
191                default:
192                    if ('a' <= command && command <= 'z') {
193                        processUnknownExtension(options, command);
194                        return true;
195                    }
196                    if ('A' <= command && command <= 'Z') {
197                        processUnknownExtension(options, command);
198                        return true;
199                    }
200                    return false;
201            }
202        } catch (IllegalArgumentException ignore) {
203        }
204        return false;
205    }
206
207    /**
208     * @return true if the operating system command was processed.
209     */
210    protected boolean processOperatingSystemCommand(ArrayList<Object> options) {
211        int command = optionInt(options, 0);
212        String label = (String) options.get(1);
213        // for command > 2 label could be composed (i.e. contain ';'), but we'll leave
214        // it to processUnknownOperatingSystemCommand implementations to handle that
215        try {
216            switch (command) {
217                case 0:
218                    processChangeIconNameAndWindowTitle(label);
219                    return true;
220                case 1:
221                    processChangeIconName(label);
222                    return true;
223                case 2:
224                    processChangeWindowTitle(label);
225                    return true;
226
227                default:
228                    // not exactly unknown, but not supported through dedicated process methods:
229                    processUnknownOperatingSystemCommand(command, label);
230                    return true;
231            }
232        } catch (IllegalArgumentException ignore) {
233        }
234        return false;
235    }
236
237    /**
238     * Process character set sequence.
239     * @param options options
240     * @return true if the charcter set select command was processed.
241     */
242    protected boolean processCharsetSelect(ArrayList<Object> options) {
243        int set = optionInt(options, 0);
244        char seq = (Character) options.get(1);
245        processCharsetSelect(set, seq);
246        return true;
247    }
248
249    private int optionInt(ArrayList<Object> options, int index) {
250        if (options.size() <= index)
251            throw new IllegalArgumentException();
252        Object value = options.get(index);
253        if (value == null)
254            throw new IllegalArgumentException();
255        if (!value.getClass().equals(Integer.class))
256            throw new IllegalArgumentException();
257        return (Integer) value;
258    }
259
260    private int optionInt(ArrayList<Object> options, int index, int defaultValue) {
261        if (options.size() > index) {
262            Object value = options.get(index);
263            if (value == null) {
264                return defaultValue;
265            }
266            return (Integer) value;
267        }
268        return defaultValue;
269    }
270
271    /**
272     * Process <code>CSI u</code> ANSI code, corresponding to <code>RCP – Restore Cursor Position</code>
273     * @throws IOException IOException
274     */
275    protected void processRestoreCursorPosition() throws IOException {
276    }
277
278    /**
279     * Process <code>CSI s</code> ANSI code, corresponding to <code>SCP – Save Cursor Position</code>
280     * @throws IOException IOException
281     */
282    protected void processSaveCursorPosition() throws IOException {
283    }
284
285    /**
286     * Process <code>CSI L</code> ANSI code, corresponding to <code>IL – Insert Line</code>
287     * @param optionInt option
288     * @throws IOException IOException
289     * @since 1.16
290     */
291    protected void processInsertLine(int optionInt) throws IOException {
292    }
293
294    /**
295     * Process <code>CSI M</code> ANSI code, corresponding to <code>DL – Delete Line</code>
296     * @param optionInt option
297     * @throws IOException IOException
298     * @since 1.16
299     */
300    protected void processDeleteLine(int optionInt) throws IOException {
301    }
302
303    /**
304     * Process <code>CSI n T</code> ANSI code, corresponding to <code>SD – Scroll Down</code>
305     * @param optionInt option
306     * @throws IOException IOException
307     */
308    protected void processScrollDown(int optionInt) throws IOException {
309    }
310
311    /**
312     * Process <code>CSI n U</code> ANSI code, corresponding to <code>SU – Scroll Up</code>
313     * @param optionInt option
314     * @throws IOException IOException
315     */
316    protected void processScrollUp(int optionInt) throws IOException {
317    }
318
319    protected static final int ERASE_SCREEN_TO_END = 0;
320    protected static final int ERASE_SCREEN_TO_BEGINING = 1;
321    protected static final int ERASE_SCREEN = 2;
322
323    /**
324     * Process <code>CSI n J</code> ANSI code, corresponding to <code>ED – Erase in Display</code>
325     * @param eraseOption eraseOption
326     * @throws IOException IOException
327     */
328    protected void processEraseScreen(int eraseOption) throws IOException {
329    }
330
331    protected static final int ERASE_LINE_TO_END = 0;
332    protected static final int ERASE_LINE_TO_BEGINING = 1;
333    protected static final int ERASE_LINE = 2;
334
335    /**
336     * Process <code>CSI n K</code> ANSI code, corresponding to <code>ED – Erase in Line</code>
337     * @param eraseOption eraseOption
338     * @throws IOException IOException
339     */
340    protected void processEraseLine(int eraseOption) throws IOException {
341    }
342
343    protected static final int ATTRIBUTE_INTENSITY_BOLD = 1; //         Intensity: Bold
344    protected static final int ATTRIBUTE_INTENSITY_FAINT = 2; //        Intensity; Faint        not widely supported
345    protected static final int ATTRIBUTE_ITALIC = 3; //         Italic; on      not widely supported. Sometimes treated as inverse.
346    protected static final int ATTRIBUTE_UNDERLINE = 4; //      Underline; Single
347    protected static final int ATTRIBUTE_BLINK_SLOW = 5; //     Blink; Slow     less than 150 per minute
348    protected static final int ATTRIBUTE_BLINK_FAST = 6; //     Blink; Rapid    MS-DOS ANSI.SYS; 150 per minute or more
349    protected static final int ATTRIBUTE_NEGATIVE_ON = 7; //    Image; Negative         inverse or reverse; swap foreground and background
350    protected static final int ATTRIBUTE_CONCEAL_ON = 8; //     Conceal on
351    protected static final int ATTRIBUTE_UNDERLINE_DOUBLE = 21; //      Underline; Double       not widely supported
352    protected static final int ATTRIBUTE_INTENSITY_NORMAL = 22; //      Intensity; Normal       not bold and not faint
353    protected static final int ATTRIBUTE_UNDERLINE_OFF = 24; //         Underline; None
354    protected static final int ATTRIBUTE_BLINK_OFF = 25; //     Blink; off
355    protected static final int ATTRIBUTE_NEGATIVE_OFF = 27; //  Image; Positive
356    protected static final int ATTRIBUTE_CONCEAL_OFF = 28; //   Reveal  conceal off
357
358    /**
359     * process <code>SGR</code> other than <code>0</code> (reset), <code>30-39</code> (foreground),
360     * <code>40-49</code> (background), <code>90-97</code> (foreground high intensity) or
361     * <code>100-107</code> (background high intensity)
362     * @param attribute attribute
363     * @throws IOException IOException
364     * @see #processAttributeReset()
365     * @see #processSetForegroundColor(int)
366     * @see #processSetForegroundColor(int, boolean)
367     * @see #processSetForegroundColorExt(int)
368     * @see #processSetForegroundColorExt(int, int, int)
369     * @see #processDefaultTextColor()
370     * @see #processDefaultBackgroundColor()
371     */
372    protected void processSetAttribute(int attribute) throws IOException {
373    }
374
375    protected static final int BLACK = 0;
376    protected static final int RED = 1;
377    protected static final int GREEN = 2;
378    protected static final int YELLOW = 3;
379    protected static final int BLUE = 4;
380    protected static final int MAGENTA = 5;
381    protected static final int CYAN = 6;
382    protected static final int WHITE = 7;
383
384    /**
385     * process <code>SGR 30-37</code> corresponding to <code>Set text color (foreground)</code>.
386     * @param color the text color
387     * @throws IOException IOException
388     */
389    protected void processSetForegroundColor(int color) throws IOException {
390        processSetForegroundColor(color, false);
391    }
392
393    /**
394     * process <code>SGR 30-37</code> or <code>SGR 90-97</code> corresponding to
395     * <code>Set text color (foreground)</code> either in normal mode or high intensity.
396     * @param color the text color
397     * @param bright is high intensity?
398     * @throws IOException IOException
399     */
400    protected void processSetForegroundColor(int color, boolean bright) throws IOException {
401    }
402
403    /**
404     * process <code>SGR 38</code> corresponding to <code>extended set text color (foreground)</code>
405     * with a palette of 255 colors.
406     * @param paletteIndex the text color in the palette
407     * @throws IOException IOException
408     */
409    protected void processSetForegroundColorExt(int paletteIndex) throws IOException {
410    }
411
412    /**
413     * process <code>SGR 38</code> corresponding to <code>extended set text color (foreground)</code>
414     * with a 24 bits RGB definition of the color.
415     * @param r red
416     * @param g green
417     * @param b blue
418     * @throws IOException IOException
419     */
420    protected void processSetForegroundColorExt(int r, int g, int b) throws IOException {
421    }
422
423    /**
424     * process <code>SGR 40-47</code> corresponding to <code>Set background color</code>.
425     * @param color the background color
426     * @throws IOException IOException
427     */
428    protected void processSetBackgroundColor(int color) throws IOException {
429        processSetBackgroundColor(color, false);
430    }
431
432    /**
433     * process <code>SGR 40-47</code> or <code>SGR 100-107</code> corresponding to
434     * <code>Set background color</code> either in normal mode or high intensity.
435     * @param color the background color
436     * @param bright is high intensity?
437     * @throws IOException IOException
438     */
439    protected void processSetBackgroundColor(int color, boolean bright) throws IOException {
440    }
441
442    /**
443     * process <code>SGR 48</code> corresponding to <code>extended set background color</code>
444     * with a palette of 255 colors.
445     * @param paletteIndex the background color in the palette
446     * @throws IOException IOException
447     */
448    protected void processSetBackgroundColorExt(int paletteIndex) throws IOException {
449    }
450
451    /**
452     * process <code>SGR 48</code> corresponding to <code>extended set background color</code>
453     * with a 24 bits RGB definition of the color.
454     * @param r red
455     * @param g green
456     * @param b blue
457     * @throws IOException IOException
458     */
459    protected void processSetBackgroundColorExt(int r, int g, int b) throws IOException {
460    }
461
462    /**
463     * process <code>SGR 39</code> corresponding to <code>Default text color (foreground)</code>
464     * @throws IOException IOException
465     */
466    protected void processDefaultTextColor() throws IOException {
467    }
468
469    /**
470     * process <code>SGR 49</code> corresponding to <code>Default background color</code>
471     * @throws IOException IOException
472     */
473    protected void processDefaultBackgroundColor() throws IOException {
474    }
475
476    /**
477     * process <code>SGR 0</code> corresponding to <code>Reset / Normal</code>
478     * @throws IOException IOException
479     */
480    protected void processAttributeReset() throws IOException {
481    }
482
483    /**
484     * process <code>CSI n ; m H</code> corresponding to <code>CUP – Cursor Position</code> or
485     * <code>CSI n ; m f</code> corresponding to <code>HVP – Horizontal and Vertical Position</code>
486     * @param row row
487     * @param col col
488     * @throws IOException IOException
489     */
490    protected void processCursorTo(int row, int col) throws IOException {
491    }
492
493    /**
494     * process <code>CSI n G</code> corresponding to <code>CHA – Cursor Horizontal Absolute</code>
495     * @param x the column
496     * @throws IOException IOException
497     */
498    protected void processCursorToColumn(int x) throws IOException {
499    }
500
501    /**
502     * process <code>CSI n F</code> corresponding to <code>CPL – Cursor Previous Line</code>
503     * @param count line count
504     * @throws IOException IOException
505     */
506    protected void processCursorUpLine(int count) throws IOException {
507    }
508
509    /**
510     * process <code>CSI n E</code> corresponding to <code>CNL – Cursor Next Line</code>
511     * @param count line count
512     * @throws IOException IOException
513     */
514    protected void processCursorDownLine(int count) throws IOException {
515        // Poor mans impl..
516        for (int i = 0; i < count; i++) {
517            os.write('\n');
518        }
519    }
520
521    /**
522     * process <code>CSI n D</code> corresponding to <code>CUB – Cursor Back</code>
523     * @param count count
524     * @throws IOException IOException
525     */
526    protected void processCursorLeft(int count) throws IOException {
527    }
528
529    /**
530     * process <code>CSI n C</code> corresponding to <code>CUF – Cursor Forward</code>
531     * @param count count
532     * @throws IOException IOException
533     */
534    protected void processCursorRight(int count) throws IOException {
535        // Poor mans impl..
536        for (int i = 0; i < count; i++) {
537            os.write(' ');
538        }
539    }
540
541    /**
542     * process <code>CSI n B</code> corresponding to <code>CUD – Cursor Down</code>
543     * @param count count
544     * @throws IOException IOException
545     */
546    protected void processCursorDown(int count) throws IOException {
547    }
548
549    /**
550     * process <code>CSI n A</code> corresponding to <code>CUU – Cursor Up</code>
551     * @param count count
552     * @throws IOException IOException
553     */
554    protected void processCursorUp(int count) throws IOException {
555    }
556
557    /**
558     * Process Unknown Extension
559     * @param options options
560     * @param command command
561     */
562    protected void processUnknownExtension(ArrayList<Object> options, int command) {
563    }
564
565    /**
566     * process <code>OSC 0;text BEL</code> corresponding to <code>Change Window and Icon label</code>
567     * @param label window title name
568     */
569    protected void processChangeIconNameAndWindowTitle(String label) {
570        processChangeIconName(label);
571        processChangeWindowTitle(label);
572    }
573
574    /**
575     * process <code>OSC 1;text BEL</code> corresponding to <code>Change Icon label</code>
576     * @param label icon label
577     */
578    protected void processChangeIconName(String label) {
579    }
580
581    /**
582     * process <code>OSC 2;text BEL</code> corresponding to <code>Change Window title</code>
583     * @param label window title text
584     */
585    protected void processChangeWindowTitle(String label) {
586    }
587
588    /**
589     * Process unknown <code>OSC</code> command.
590     * @param command command
591     * @param param param
592     */
593    protected void processUnknownOperatingSystemCommand(int command, String param) {
594    }
595
596    protected void processCharsetSelect(int set, char seq) {
597    }
598}