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