001    /**
002     * Copyright (C) 2009, Progress Software Corporation and/or its 
003     * subsidiaries or affiliates.  All rights reserved.
004     *
005     * Licensed under the Apache License, Version 2.0 (the "License");
006     * you may not use this file except in compliance with the License.
007     * You may obtain a copy of the License at
008     *
009     *     http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */
017    
018    package org.fusesource.jansi;
019    
020    import java.io.FilterOutputStream;
021    import java.io.IOException;
022    import java.io.OutputStream;
023    import java.io.UnsupportedEncodingException;
024    import java.util.ArrayList;
025    
026    /**
027     * A ANSI output stream extracts ANSI escape codes written to 
028     * an output stream. 
029     * 
030     * For more information about ANSI escape codes, see:
031     * http://en.wikipedia.org/wiki/ANSI_escape_code
032     * 
033     * This class just filters out the escape codes so that they are not
034     * sent out to the underlying OutputStream.  Subclasses should
035     * actually perform the ANSI escape behaviors.
036     * 
037     * @author <a href="http://hiramchirino.com">Hiram Chirino</a>
038     * @since 1.0
039     */
040    public class AnsiOutputStream extends FilterOutputStream {
041    
042        public static final byte [] REST_CODE = resetCode();
043    
044            public AnsiOutputStream(OutputStream os) {
045                    super(os);
046            }
047    
048            private  final static int MAX_ESCAPE_SEQUENCE_LENGTH=100;
049            private byte buffer[] = new byte[MAX_ESCAPE_SEQUENCE_LENGTH];
050            private int pos=0;
051            private int startOfValue;
052            private final ArrayList<Object> options = new ArrayList<Object>();
053    
054            private static final int LOOKING_FOR_FIRST_ESC_CHAR = 0;
055            private static final int LOOKING_FOR_SECOND_ESC_CHAR = 1;
056            private static final int LOOKING_FOR_NEXT_ARG = 2;
057            private static final int LOOKING_FOR_STR_ARG_END = 3;
058            private static final int LOOKING_FOR_INT_ARG_END = 4;
059    
060            int state = LOOKING_FOR_FIRST_ESC_CHAR;
061            
062            private static final int FIRST_ESC_CHAR = 27;
063            private static final int SECOND_ESC_CHAR = '[';
064    
065            // TODO: implement to get perf boost: public void write(byte[] b, int off, int len)
066            
067            public void write(int data) throws IOException {
068                    switch( state ) {
069                    case LOOKING_FOR_FIRST_ESC_CHAR:
070                            if (data == FIRST_ESC_CHAR) {
071                                    buffer[pos++] = (byte) data;
072                                    state = LOOKING_FOR_SECOND_ESC_CHAR;
073                            } else {
074                                    out.write(data);
075                            }
076                            break;
077                            
078                    case LOOKING_FOR_SECOND_ESC_CHAR:
079                            buffer[pos++] = (byte) data;
080                            if( data == SECOND_ESC_CHAR ) {
081                                    state = LOOKING_FOR_NEXT_ARG;
082                            } else {
083                                    buffer[pos++] = (byte) data;
084                                    reset();
085                            }
086                            break;
087                            
088                    case LOOKING_FOR_NEXT_ARG:
089                            buffer[pos++] = (byte)data;
090                            if( '"' == data ) {
091                                    startOfValue=pos-1;
092                                    state = LOOKING_FOR_STR_ARG_END;
093                            } else if( '0' <= data && data <= '9') {
094                                    startOfValue=pos-1;
095                                    state = LOOKING_FOR_INT_ARG_END;                                
096                            } else if( ';' == data ) {
097                                    options.add(null);
098                            } else if( '?' == data ) {
099                                    options.add(new Character('?'));
100                            } else if( '=' == data ) {
101                                    options.add(new Character('='));
102                            } else {
103                                    if( processEscapeCommand(options, data) ) {
104                                            pos=0;
105                                    }
106                                    reset();
107                            }
108                            break;
109                            
110                    case LOOKING_FOR_INT_ARG_END:
111                            buffer[pos++] = (byte)data;
112                            if( !('0' <= data && data <= '9') ) {
113                                    String strValue = new String(buffer, startOfValue, (pos-1)-startOfValue, "UTF-8");
114                                    Integer value = new Integer(strValue);
115                                    options.add(value);
116                                    if( data == ';' ) {
117                                            state = LOOKING_FOR_NEXT_ARG;
118                                    } else {
119                                            if( processEscapeCommand(options, data) ) {
120                                                    pos=0;
121                                            }
122                                            reset();
123                                    }
124                            }
125                            break;
126                            
127                    case LOOKING_FOR_STR_ARG_END:
128                            buffer[pos++] = (byte)data;
129                            if( '"' != data ) {
130                                    String value = new String(buffer, startOfValue, (pos-1)-startOfValue, "UTF-8");
131                                    options.add(value);
132                                    if( data == ';' ) {
133                                            state = LOOKING_FOR_NEXT_ARG;
134                                    } else {
135                                            if( processEscapeCommand(options, data) ) {
136                                                    pos=0;
137                                            }
138                                            reset();
139                                    }
140                            }
141                            break;
142                    }
143                    
144                    // Is it just too long?
145                    if( pos >= buffer.length ) {
146                            reset();
147                    }
148            }
149    
150            private void reset() throws IOException {
151                    if( pos > 0 ) {
152                            out.write(buffer, 0, pos);
153                    }
154                    pos=0;
155                    startOfValue=0;
156                    options.clear();
157                    state = LOOKING_FOR_FIRST_ESC_CHAR;
158            }
159    
160            /**
161             * 
162             * @param options
163             * @param command
164             * @return true if the escape command was processed.
165             */
166            private boolean processEscapeCommand(ArrayList<Object> options, int command) throws IOException {
167                    try {
168                            switch(command) {
169                            case 'A':
170                                    processCursorUp(optionInt(options, 0, 1));
171                                    return true;
172                            case 'B':
173                                    processCursorDown(optionInt(options, 0, 1));
174                                    return true;
175                            case 'C':
176                                    processCursorRight(optionInt(options, 0, 1));
177                                    return true;
178                            case 'D':
179                                    processCursorLeft(optionInt(options, 0, 1));
180                                    return true;
181                            case 'E':
182                                    processCursorDownLine(optionInt(options, 0, 1));
183                                    return true;
184                            case 'F':
185                                    processCursorUpLine(optionInt(options, 0, 1));
186                                    return true;
187                            case 'G':
188                                    processCursorToColumn(optionInt(options, 0));
189                                    return true;
190                            case 'H':
191                            case 'f':
192                                    processCursorTo(optionInt(options, 0, 1), optionInt(options, 1, 1));
193                                    return true;
194                            case 'J':
195                                    processEraseScreen(optionInt(options, 0, 0));
196                                    return true;
197                            case 'K':
198                                    processEraseLine(optionInt(options, 0, 0));
199                                    return true;
200                            case 'S':
201                                    processScrollUp(optionInt(options, 0, 1));
202                                    return true;
203                            case 'T':
204                                    processScrollDown(optionInt(options, 0, 1));
205                                    return true;
206                            case 'm':                               
207                                    // Validate all options are ints...
208                                    for (Object next : options) {
209                                            if( next!=null && next.getClass()!=Integer.class) {
210                                                    throw new IllegalArgumentException();
211                                            }
212                                    }
213    
214                                    int count=0;
215                                    for (Object next : options) {
216                                            if( next!=null ) {
217                                                    count++;
218                                                    int value = ((Integer)next).intValue();
219                                                    if( 30 <= value && value <= 37 ) {
220                                                            processSetForegroundColor(value-30);
221                                                    } else if( 40 <= value && value <= 47 ) {
222                                                            processSetBackgroundColor(value-40);
223                                                    } else {
224                                                            switch ( value ) {
225                                                            case 39: 
226                                                            case 49:
227                                                            case 0: processAttributeRest(); break;
228                                                            default:
229                                                                    processSetAttribute(value);
230                                                            }
231                                                    }
232                                            }
233                                    }
234                                    if( count == 0 ) {
235                                            processAttributeRest();
236                                    }
237                                    return true;
238                            case 's':
239                                    processSaveCursorPosition();
240                                    return true;
241                            case 'u':
242                                    processRestoreCursorPosition();
243                                    return true;
244                                    
245                            default:
246                                    if( 'a' <= command && 'z' <=command ) {
247                                            processUnknownExtension(options, command);
248                                            return true;
249                                    }
250                                    if( 'A' <= command && 'Z' <=command ) {
251                                            processUnknownExtension(options, command);
252                                            return true;
253                                    }
254                                    return false;
255                            }
256                    } catch (IllegalArgumentException ignore)  {
257                    }
258                    return false;
259            }
260    
261            protected void processRestoreCursorPosition() throws IOException {
262            }
263            protected void processSaveCursorPosition() throws IOException {
264            }
265            protected void processScrollDown(int optionInt) throws IOException {
266            }
267            protected void processScrollUp(int optionInt) throws IOException {
268            }
269    
270            protected static final int ERASE_SCREEN_TO_END=0;
271            protected static final int ERASE_SCREEN_TO_BEGINING=1;
272            protected static final int ERASE_SCREEN=2;
273            
274            protected void processEraseScreen(int eraseOption) throws IOException {
275            }
276    
277            protected static final int ERASE_LINE_TO_END=0;
278            protected static final int ERASE_LINE_TO_BEGINING=1;
279            protected static final int ERASE_LINE=2;
280            
281            protected void processEraseLine(int eraseOption) throws IOException {
282            }
283    
284            protected static final int ATTRIBUTE_INTENSITY_BOLD     = 1; //         Intensity: Bold         
285            protected static final int ATTRIBUTE_INTENSITY_FAINT    = 2; //         Intensity; Faint        not widely supported
286            protected static final int ATTRIBUTE_ITALIC                     = 3; //         Italic; on      not widely supported. Sometimes treated as inverse.
287            protected static final int ATTRIBUTE_UNDERLINE                  = 4; //         Underline; Single       
288            protected static final int ATTRIBUTE_BLINK_SLOW                 = 5; //         Blink; Slow     less than 150 per minute
289            protected static final int ATTRIBUTE_BLINK_FAST                 = 6; //         Blink; Rapid    MS-DOS ANSI.SYS; 150 per minute or more
290            protected static final int ATTRIBUTE_NEGATIVE_ON                = 7; //         Image; Negative         inverse or reverse; swap foreground and background
291            protected static final int ATTRIBUTE_CONCEAL_ON                 = 8; //         Conceal on
292            protected static final int ATTRIBUTE_UNDERLINE_DOUBLE   = 21; //        Underline; Double       not widely supported
293            protected static final int ATTRIBUTE_INTENSITY_NORMAL   = 22; //        Intensity; Normal       not bold and not faint
294            protected static final int ATTRIBUTE_UNDERLINE_OFF              = 24; //        Underline; None         
295            protected static final int ATTRIBUTE_BLINK_OFF                  = 25; //        Blink; off      
296            protected static final int ATTRIBUTE_NEGATIVE_Off               = 27; //        Image; Positive         
297            protected static final int ATTRIBUTE_CONCEAL_OFF                = 28; //        Reveal  conceal off
298    
299            protected void processSetAttribute(int attribute) throws IOException {
300            }
301    
302            protected static final int BLACK        = 0;
303            protected static final int RED          = 1;
304            protected static final int GREEN        = 2;
305            protected static final int YELLOW       = 3;
306            protected static final int BLUE         = 4;
307            protected static final int MAGENTA      = 5;
308            protected static final int CYAN         = 6;
309            protected static final int WHITE        = 7;
310    
311            protected void processSetForegroundColor(int color) throws IOException {
312            }
313    
314            protected void processSetBackgroundColor(int color) throws IOException {
315            }
316    
317            protected void processAttributeRest() throws IOException {
318            }
319    
320            protected void processCursorTo(int row, int col) throws IOException {
321            }
322    
323            protected void processCursorToColumn(int x) throws IOException {
324            }
325    
326            protected void processCursorUpLine(int count) throws IOException {
327            }
328    
329            protected void processCursorDownLine(int count) throws IOException {
330                    // Poor mans impl..
331                    for(int i=0; i < count; i++) {
332                            out.write('\n');
333                    }
334            }
335    
336            protected void processCursorLeft(int count) throws IOException {
337            }
338    
339            protected void processCursorRight(int count) throws IOException {
340                    // Poor mans impl..
341                    for(int i=0; i < count; i++) {
342                            out.write(' ');
343                    }
344            }
345    
346            protected void processCursorDown(int count) throws IOException {
347            }
348    
349            protected void processCursorUp(int count) throws IOException {
350            }
351            
352            protected void processUnknownExtension(ArrayList<Object> options, int command) {
353            }
354    
355            private int optionInt(ArrayList<Object> options, int index) {
356                    if( options.size() <= index )
357                            throw new IllegalArgumentException();
358                    Object value = options.get(index);
359                    if( value == null )
360                            throw new IllegalArgumentException();
361                    if( !value.getClass().equals(Integer.class) )
362                            throw new IllegalArgumentException();
363                    return ((Integer)value).intValue();
364            }
365    
366            private int optionInt(ArrayList<Object> options, int index, int defaultValue) {
367                    if( options.size() > index ) {
368                            Object value = options.get(index);
369                            if( value == null ) {
370                                    return defaultValue;
371                            }
372                            return ((Integer)value).intValue();
373                    }
374                    return defaultValue;            
375            }
376            
377            @Override
378            public void close() throws IOException {
379                write(REST_CODE);
380                flush();
381                super.close();
382            }
383            
384        static private byte[] resetCode() {
385            try {
386                return new Ansi().reset().toString().getBytes("UTF-8");
387            } catch (UnsupportedEncodingException e) {
388                throw new RuntimeException(e);
389            }
390        }
391    
392    }