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}