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; 017 018import static org.fusesource.jansi.internal.Kernel32.BACKGROUND_BLUE; 019import static org.fusesource.jansi.internal.Kernel32.BACKGROUND_GREEN; 020import static org.fusesource.jansi.internal.Kernel32.BACKGROUND_INTENSITY; 021import static org.fusesource.jansi.internal.Kernel32.BACKGROUND_RED; 022import static org.fusesource.jansi.internal.Kernel32.CHAR_INFO; 023import static org.fusesource.jansi.internal.Kernel32.FOREGROUND_BLUE; 024import static org.fusesource.jansi.internal.Kernel32.FOREGROUND_GREEN; 025import static org.fusesource.jansi.internal.Kernel32.FOREGROUND_INTENSITY; 026import static org.fusesource.jansi.internal.Kernel32.FOREGROUND_RED; 027import static org.fusesource.jansi.internal.Kernel32.FillConsoleOutputAttribute; 028import static org.fusesource.jansi.internal.Kernel32.FillConsoleOutputCharacterW; 029import static org.fusesource.jansi.internal.Kernel32.GetConsoleScreenBufferInfo; 030import static org.fusesource.jansi.internal.Kernel32.GetStdHandle; 031import static org.fusesource.jansi.internal.Kernel32.SMALL_RECT; 032import static org.fusesource.jansi.internal.Kernel32.STD_OUTPUT_HANDLE; 033import static org.fusesource.jansi.internal.Kernel32.ScrollConsoleScreenBuffer; 034import static org.fusesource.jansi.internal.Kernel32.SetConsoleCursorPosition; 035import static org.fusesource.jansi.internal.Kernel32.SetConsoleTextAttribute; 036import static org.fusesource.jansi.internal.Kernel32.SetConsoleTitle; 037 038import java.io.IOException; 039import java.io.OutputStream; 040 041import org.fusesource.jansi.internal.Kernel32; 042import org.fusesource.jansi.internal.WindowsSupport; 043import org.fusesource.jansi.internal.Kernel32.CONSOLE_SCREEN_BUFFER_INFO; 044import org.fusesource.jansi.internal.Kernel32.COORD; 045 046/** 047 * A Windows ANSI escape processor, that uses JNA to access native platform 048 * API's to change the console attributes. 049 * 050 * @since 1.0 051 * @author <a href="http://hiramchirino.com">Hiram Chirino</a> 052 * @author Joris Kuipers 053 */ 054public final class WindowsAnsiOutputStream extends AnsiOutputStream { 055 056 private static final long console = GetStdHandle(STD_OUTPUT_HANDLE); 057 058 private static final short FOREGROUND_BLACK = 0; 059 private static final short FOREGROUND_YELLOW = (short) (FOREGROUND_RED | FOREGROUND_GREEN); 060 private static final short FOREGROUND_MAGENTA = (short) (FOREGROUND_BLUE | FOREGROUND_RED); 061 private static final short FOREGROUND_CYAN = (short) (FOREGROUND_BLUE | FOREGROUND_GREEN); 062 private static final short FOREGROUND_WHITE = (short) (FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE); 063 064 private static final short BACKGROUND_BLACK = 0; 065 private static final short BACKGROUND_YELLOW = (short) (BACKGROUND_RED | BACKGROUND_GREEN); 066 private static final short BACKGROUND_MAGENTA = (short) (BACKGROUND_BLUE | BACKGROUND_RED); 067 private static final short BACKGROUND_CYAN = (short) (BACKGROUND_BLUE | BACKGROUND_GREEN); 068 private static final short BACKGROUND_WHITE = (short) (BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE); 069 070 private static final short[] ANSI_FOREGROUND_COLOR_MAP = { 071 FOREGROUND_BLACK, 072 FOREGROUND_RED, 073 FOREGROUND_GREEN, 074 FOREGROUND_YELLOW, 075 FOREGROUND_BLUE, 076 FOREGROUND_MAGENTA, 077 FOREGROUND_CYAN, 078 FOREGROUND_WHITE, 079 }; 080 081 private static final short[] ANSI_BACKGROUND_COLOR_MAP = { 082 BACKGROUND_BLACK, 083 BACKGROUND_RED, 084 BACKGROUND_GREEN, 085 BACKGROUND_YELLOW, 086 BACKGROUND_BLUE, 087 BACKGROUND_MAGENTA, 088 BACKGROUND_CYAN, 089 BACKGROUND_WHITE, 090 }; 091 092 private final CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO(); 093 private final short originalColors; 094 095 private boolean negative; 096 private short savedX = -1; 097 private short savedY = -1; 098 099 public WindowsAnsiOutputStream(OutputStream os) throws IOException { 100 super(os); 101 getConsoleInfo(); 102 originalColors = info.attributes; 103 } 104 105 private void getConsoleInfo() throws IOException { 106 out.flush(); 107 if (GetConsoleScreenBufferInfo(console, info) == 0) { 108 throw new IOException("Could not get the screen info: " + WindowsSupport.getLastErrorMessage()); 109 } 110 if (negative) { 111 info.attributes = invertAttributeColors(info.attributes); 112 } 113 } 114 115 private void applyAttribute() throws IOException { 116 out.flush(); 117 short attributes = info.attributes; 118 if (negative) { 119 attributes = invertAttributeColors(attributes); 120 } 121 if (SetConsoleTextAttribute(console, attributes) == 0) { 122 throw new IOException(WindowsSupport.getLastErrorMessage()); 123 } 124 } 125 126 private short invertAttributeColors(short attributes) { 127 // Swap the the Foreground and Background bits. 128 int fg = 0x000F & attributes; 129 fg <<= 4; 130 int bg = 0X00F0 & attributes; 131 bg >>= 4; 132 attributes = (short) ((attributes & 0xFF00) | fg | bg); 133 return attributes; 134 } 135 136 private void applyCursorPosition() throws IOException { 137 if (SetConsoleCursorPosition(console, info.cursorPosition.copy()) == 0) { 138 throw new IOException(WindowsSupport.getLastErrorMessage()); 139 } 140 } 141 142 @Override 143 protected void processEraseScreen(int eraseOption) throws IOException { 144 getConsoleInfo(); 145 int[] written = new int[1]; 146 switch (eraseOption) { 147 case ERASE_SCREEN: 148 COORD topLeft = new COORD(); 149 topLeft.x = 0; 150 topLeft.y = info.window.top; 151 int screenLength = info.window.height() * info.size.x; 152 FillConsoleOutputAttribute(console, originalColors, screenLength, topLeft, written); 153 FillConsoleOutputCharacterW(console, ' ', screenLength, topLeft, written); 154 break; 155 case ERASE_SCREEN_TO_BEGINING: 156 COORD topLeft2 = new COORD(); 157 topLeft2.x = 0; 158 topLeft2.y = info.window.top; 159 int lengthToCursor = (info.cursorPosition.y - info.window.top) * info.size.x 160 + info.cursorPosition.x; 161 FillConsoleOutputAttribute(console, originalColors, lengthToCursor, topLeft2, written); 162 FillConsoleOutputCharacterW(console, ' ', lengthToCursor, topLeft2, written); 163 break; 164 case ERASE_SCREEN_TO_END: 165 int lengthToEnd = (info.window.bottom - info.cursorPosition.y) * info.size.x + 166 (info.size.x - info.cursorPosition.x); 167 FillConsoleOutputAttribute(console, originalColors, lengthToEnd, info.cursorPosition.copy(), written); 168 FillConsoleOutputCharacterW(console, ' ', lengthToEnd, info.cursorPosition.copy(), written); 169 break; 170 default: 171 break; 172 } 173 } 174 175 @Override 176 protected void processEraseLine(int eraseOption) throws IOException { 177 getConsoleInfo(); 178 int[] written = new int[1]; 179 switch (eraseOption) { 180 case ERASE_LINE: 181 COORD leftColCurrRow = info.cursorPosition.copy(); 182 leftColCurrRow.x = 0; 183 FillConsoleOutputAttribute(console, originalColors, info.size.x, leftColCurrRow, written); 184 FillConsoleOutputCharacterW(console, ' ', info.size.x, leftColCurrRow, written); 185 break; 186 case ERASE_LINE_TO_BEGINING: 187 COORD leftColCurrRow2 = info.cursorPosition.copy(); 188 leftColCurrRow2.x = 0; 189 FillConsoleOutputAttribute(console, originalColors, info.cursorPosition.x, leftColCurrRow2, written); 190 FillConsoleOutputCharacterW(console, ' ', info.cursorPosition.x, leftColCurrRow2, written); 191 break; 192 case ERASE_LINE_TO_END: 193 int lengthToLastCol = info.size.x - info.cursorPosition.x; 194 FillConsoleOutputAttribute(console, originalColors, lengthToLastCol, info.cursorPosition.copy(), written); 195 FillConsoleOutputCharacterW(console, ' ', lengthToLastCol, info.cursorPosition.copy(), written); 196 break; 197 default: 198 break; 199 } 200 } 201 202 @Override 203 protected void processCursorLeft(int count) throws IOException { 204 getConsoleInfo(); 205 info.cursorPosition.x = (short) Math.max(0, info.cursorPosition.x - count); 206 applyCursorPosition(); 207 } 208 209 @Override 210 protected void processCursorRight(int count) throws IOException { 211 getConsoleInfo(); 212 info.cursorPosition.x = (short) Math.min(info.window.width(), info.cursorPosition.x + count); 213 applyCursorPosition(); 214 } 215 216 @Override 217 protected void processCursorDown(int count) throws IOException { 218 getConsoleInfo(); 219 info.cursorPosition.y = (short) Math.min(Math.max(0, info.size.y - 1), info.cursorPosition.y + count); 220 applyCursorPosition(); 221 } 222 223 @Override 224 protected void processCursorUp(int count) throws IOException { 225 getConsoleInfo(); 226 info.cursorPosition.y = (short) Math.max(info.window.top, info.cursorPosition.y - count); 227 applyCursorPosition(); 228 } 229 230 @Override 231 protected void processCursorTo(int row, int col) throws IOException { 232 getConsoleInfo(); 233 info.cursorPosition.y = (short) Math.max(info.window.top, Math.min(info.size.y, info.window.top + row - 1)); 234 info.cursorPosition.x = (short) Math.max(0, Math.min(info.window.width(), col - 1)); 235 applyCursorPosition(); 236 } 237 238 @Override 239 protected void processCursorToColumn(int x) throws IOException { 240 getConsoleInfo(); 241 info.cursorPosition.x = (short) Math.max(0, Math.min(info.window.width(), x - 1)); 242 applyCursorPosition(); 243 } 244 245 @Override 246 protected void processSetForegroundColor(int color, boolean bright) throws IOException { 247 info.attributes = (short) ((info.attributes & ~0x0007) | ANSI_FOREGROUND_COLOR_MAP[color]); 248 info.attributes = (short) ((info.attributes & ~FOREGROUND_INTENSITY) | (bright ? FOREGROUND_INTENSITY : 0)); 249 applyAttribute(); 250 } 251 252 @Override 253 protected void processSetBackgroundColor(int color, boolean bright) throws IOException { 254 info.attributes = (short) ((info.attributes & ~0x0070) | ANSI_BACKGROUND_COLOR_MAP[color]); 255 info.attributes = (short) ((info.attributes & ~BACKGROUND_INTENSITY) | (bright ? BACKGROUND_INTENSITY : 0)); 256 applyAttribute(); 257 } 258 259 @Override 260 protected void processDefaultTextColor() throws IOException { 261 info.attributes = (short) ((info.attributes & ~0x000F) | (originalColors & 0xF)); 262 info.attributes = (short) (info.attributes & ~FOREGROUND_INTENSITY); 263 applyAttribute(); 264 } 265 266 @Override 267 protected void processDefaultBackgroundColor() throws IOException { 268 info.attributes = (short) ((info.attributes & ~0x00F0) | (originalColors & 0xF0)); 269 info.attributes = (short) (info.attributes & ~BACKGROUND_INTENSITY); 270 applyAttribute(); 271 } 272 273 @Override 274 protected void processAttributeRest() throws IOException { 275 info.attributes = (short) ((info.attributes & ~0x00FF) | originalColors); 276 this.negative = false; 277 applyAttribute(); 278 } 279 280 @Override 281 protected void processSetAttribute(int attribute) throws IOException { 282 switch (attribute) { 283 case ATTRIBUTE_INTENSITY_BOLD: 284 info.attributes = (short) (info.attributes | FOREGROUND_INTENSITY); 285 applyAttribute(); 286 break; 287 case ATTRIBUTE_INTENSITY_NORMAL: 288 info.attributes = (short) (info.attributes & ~FOREGROUND_INTENSITY); 289 applyAttribute(); 290 break; 291 292 // Yeah, setting the background intensity is not underlining.. but it's best we can do 293 // using the Windows console API 294 case ATTRIBUTE_UNDERLINE: 295 info.attributes = (short) (info.attributes | BACKGROUND_INTENSITY); 296 applyAttribute(); 297 break; 298 case ATTRIBUTE_UNDERLINE_OFF: 299 info.attributes = (short) (info.attributes & ~BACKGROUND_INTENSITY); 300 applyAttribute(); 301 break; 302 303 case ATTRIBUTE_NEGATIVE_ON: 304 negative = true; 305 applyAttribute(); 306 break; 307 case ATTRIBUTE_NEGATIVE_OFF: 308 negative = false; 309 applyAttribute(); 310 break; 311 default: 312 break; 313 } 314 } 315 316 @Override 317 protected void processSaveCursorPosition() throws IOException { 318 getConsoleInfo(); 319 savedX = info.cursorPosition.x; 320 savedY = info.cursorPosition.y; 321 } 322 323 @Override 324 protected void processRestoreCursorPosition() throws IOException { 325 // restore only if there was a save operation first 326 if (savedX != -1 && savedY != -1) { 327 out.flush(); 328 info.cursorPosition.x = savedX; 329 info.cursorPosition.y = savedY; 330 applyCursorPosition(); 331 } 332 } 333 334 @Override 335 protected void processInsertLine(int optionInt) throws IOException { 336 getConsoleInfo(); 337 SMALL_RECT scroll = info.window.copy(); 338 scroll.top = info.cursorPosition.y; 339 COORD org = new COORD(); 340 org.x = 0; 341 org.y = (short)(info.cursorPosition.y + optionInt); 342 CHAR_INFO info = new CHAR_INFO(); 343 info.attributes = originalColors; 344 info.unicodeChar = ' '; 345 if (ScrollConsoleScreenBuffer(console, scroll, scroll, org, info) == 0) { 346 throw new IOException(WindowsSupport.getLastErrorMessage()); 347 } 348 } 349 350 @Override 351 protected void processDeleteLine(int optionInt) throws IOException { 352 getConsoleInfo(); 353 SMALL_RECT scroll = info.window.copy(); 354 scroll.top = info.cursorPosition.y; 355 COORD org = new COORD(); 356 org.x = 0; 357 org.y = (short)(info.cursorPosition.y - optionInt); 358 CHAR_INFO info = new CHAR_INFO(); 359 info.attributes = originalColors; 360 info.unicodeChar = ' '; 361 if (ScrollConsoleScreenBuffer(console, scroll, scroll, org, info) == 0) { 362 throw new IOException(WindowsSupport.getLastErrorMessage()); 363 } 364 } 365 366 @Override 367 protected void processChangeWindowTitle(String label) { 368 SetConsoleTitle(label); 369 } 370}