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