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}