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