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 }