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 x, int y) 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 }