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