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