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