001    /**
002     * Copyright (C) 2009, Progress Software Corporation and/or its 
003     * subsidiaries or affiliates.  All rights reserved.
004     *
005     * Licensed under the Apache License, Version 2.0 (the "License");
006     * you may not use this file except in compliance with the License.
007     * You may obtain a copy of the License at
008     *
009     *     http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */
017    package org.fusesource.jansi;
018    
019    import static org.fusesource.jansi.internal.Kernel32.BACKGROUND_BLUE;
020    import static org.fusesource.jansi.internal.Kernel32.BACKGROUND_GREEN;
021    import static org.fusesource.jansi.internal.Kernel32.BACKGROUND_INTENSITY;
022    import static org.fusesource.jansi.internal.Kernel32.BACKGROUND_RED;
023    import static org.fusesource.jansi.internal.Kernel32.FOREGROUND_BLUE;
024    import static org.fusesource.jansi.internal.Kernel32.FOREGROUND_GREEN;
025    import static org.fusesource.jansi.internal.Kernel32.FOREGROUND_INTENSITY;
026    import static org.fusesource.jansi.internal.Kernel32.FOREGROUND_RED;
027    import static org.fusesource.jansi.internal.Kernel32.FillConsoleOutputCharacterW;
028    import static org.fusesource.jansi.internal.Kernel32.GetConsoleScreenBufferInfo;
029    import static org.fusesource.jansi.internal.Kernel32.GetStdHandle;
030    import static org.fusesource.jansi.internal.Kernel32.STD_OUTPUT_HANDLE;
031    import static org.fusesource.jansi.internal.Kernel32.SetConsoleCursorPosition;
032    import static org.fusesource.jansi.internal.Kernel32.SetConsoleTextAttribute;
033    
034    import java.io.IOException;
035    import java.io.OutputStream;
036    
037    import org.fusesource.jansi.internal.WindowsSupport;
038    import org.fusesource.jansi.internal.Kernel32.CONSOLE_SCREEN_BUFFER_INFO;
039    import org.fusesource.jansi.internal.Kernel32.COORD;
040    
041    /**
042     * A Windows ANSI escape processor, uses JNA to access native platform
043     * API's to change the console attributes.
044     * 
045     * @since 1.0
046     * @author <a href="http://hiramchirino.com">Hiram Chirino</a>
047     */
048    public final class WindowsAnsiOutputStream extends AnsiOutputStream {
049            
050            private static final long console = GetStdHandle(STD_OUTPUT_HANDLE);
051    
052        private static final short FOREGROUND_BLACK   = 0;
053        private static final short FOREGROUND_YELLOW  = (short) (FOREGROUND_RED|FOREGROUND_GREEN);    
054        private static final short FOREGROUND_MAGENTA = (short) (FOREGROUND_BLUE|FOREGROUND_RED);    
055        private static final short FOREGROUND_CYAN    = (short) (FOREGROUND_BLUE|FOREGROUND_GREEN);
056        private static final short FOREGROUND_WHITE   = (short) (FOREGROUND_RED|FOREGROUND_GREEN|FOREGROUND_BLUE);
057    
058        private static final short BACKGROUND_BLACK   = 0;
059        private static final short BACKGROUND_YELLOW  = (short) (BACKGROUND_RED|BACKGROUND_GREEN);    
060        private static final short BACKGROUND_MAGENTA = (short) (BACKGROUND_BLUE|BACKGROUND_RED);    
061        private static final short BACKGROUND_CYAN    = (short) (BACKGROUND_BLUE|BACKGROUND_GREEN);
062        private static final short BACKGROUND_WHITE   = (short) (BACKGROUND_RED|BACKGROUND_GREEN|BACKGROUND_BLUE);
063        
064        private static final short ANSI_FOREGROUND_COLOR_MAP[] = {
065            FOREGROUND_BLACK,
066            FOREGROUND_RED,
067            FOREGROUND_GREEN,
068            FOREGROUND_YELLOW,
069            FOREGROUND_BLUE,
070            FOREGROUND_MAGENTA,
071            FOREGROUND_CYAN,
072            FOREGROUND_WHITE,
073        };
074            
075        private static final short ANSI_BACKGROUND_COLOR_MAP[] = {
076            BACKGROUND_BLACK,
077            BACKGROUND_RED,
078            BACKGROUND_GREEN,
079            BACKGROUND_YELLOW,
080            BACKGROUND_BLUE,
081            BACKGROUND_MAGENTA,
082            BACKGROUND_CYAN,
083            BACKGROUND_WHITE,
084        };
085            
086            private final CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO();
087        private final short originalColors;
088        
089            private boolean negative;
090            private short savedX = -1;
091            private short savedY = -1;
092            
093            public WindowsAnsiOutputStream(OutputStream os) throws IOException {
094                    super(os);
095                    getConsoleInfo();
096                    originalColors = info.attributes;
097            }
098    
099            private void getConsoleInfo() throws IOException {
100                    out.flush();
101                    if( GetConsoleScreenBufferInfo(console, info) == 0 ) {
102                            throw new IOException("Could not get the screen info: "+WindowsSupport.getLastErrorMessage());
103                    }
104                    if( negative ) {
105                            info.attributes = invertAttributeColors(info.attributes); 
106                    }
107            }
108                    
109            private void applyAttribute() throws IOException {
110                    out.flush();
111                    short attributes = info.attributes;
112                    if( negative ) {
113                            attributes = invertAttributeColors(attributes); 
114                    }
115                    if( SetConsoleTextAttribute(console, attributes) == 0 ) {
116                            throw new IOException(WindowsSupport.getLastErrorMessage());
117                    }
118            }
119    
120            private short invertAttributeColors(short attibutes) {
121                    // Swap the the Foreground and Background bits.
122                    int fg = 0x000F & attibutes;
123                    fg <<= 8;
124                    int bg = 0X00F0 * attibutes;
125                    bg >>=8;
126                    attibutes = (short) ((attibutes & 0xFF00) | fg | bg);
127                    return attibutes;
128            }
129    
130            private void applyCursorPosition() throws IOException {
131                    if( SetConsoleCursorPosition(console, info.cursorPosition.copy()) == 0 ) {
132                            throw new IOException(WindowsSupport.getLastErrorMessage());
133                    }
134            }
135            
136            @Override
137            protected void processEraseScreen(int eraseOption) throws IOException {
138                    getConsoleInfo();
139                    int[] written = new int[1];
140                    switch(eraseOption) {
141                    case ERASE_SCREEN:
142                            COORD topLeft = new COORD();
143                            topLeft.x = 0;
144                            topLeft.y = info.window.top;
145                            int screenLength = info.window.height() * info.size.x;
146                            FillConsoleOutputCharacterW(console, ' ', screenLength, topLeft, written);
147                            break;
148                    case ERASE_SCREEN_TO_BEGINING:
149                            COORD topLeft2 = new COORD();
150                            topLeft2.x = 0;
151                            topLeft2.y = info.window.top;
152                            int lengthToCursor = (info.cursorPosition.y - info.window.top) * info.size.x 
153                                    + info.cursorPosition.x;
154                            FillConsoleOutputCharacterW(console, ' ', lengthToCursor, topLeft2, written);
155                            break;
156                    case ERASE_SCREEN_TO_END:
157                            int lengthToEnd = (info.window.bottom - info.cursorPosition.y) * info.size.x + 
158                                    (info.size.x - info.cursorPosition.x);
159                            FillConsoleOutputCharacterW(console, ' ', lengthToEnd, info.cursorPosition.copy(), written);
160                    }               
161            }
162            
163            @Override
164            protected void processEraseLine(int eraseOption) throws IOException {
165                    getConsoleInfo();
166                    int[] written = new int[1];
167                    switch(eraseOption) {
168                    case ERASE_LINE:
169                            COORD leftColCurrRow = info.cursorPosition.copy();
170                            leftColCurrRow.x = 0;
171                            FillConsoleOutputCharacterW(console, ' ', info.size.x, leftColCurrRow, written);
172                            break;
173                    case ERASE_LINE_TO_BEGINING:
174                            COORD leftColCurrRow2 = info.cursorPosition.copy();
175                            leftColCurrRow2.x = 0;
176                            FillConsoleOutputCharacterW(console, ' ', info.cursorPosition.x, leftColCurrRow2, written);
177                            break;
178                    case ERASE_LINE_TO_END:
179                            int lengthToLastCol = info.size.x - info.cursorPosition.x;
180                            FillConsoleOutputCharacterW(console, ' ', lengthToLastCol, info.cursorPosition.copy(), written);
181                    }
182            }
183            
184            @Override
185            protected void processCursorLeft(int count) throws IOException {
186                    getConsoleInfo();
187                    info.cursorPosition.x = (short) Math.max(0, info.cursorPosition.x-count);
188                    applyCursorPosition();          
189            }
190    
191            @Override
192            protected void processCursorRight(int count) throws IOException {
193                    getConsoleInfo();
194                    info.cursorPosition.x = (short)Math.min(info.window.width(), info.cursorPosition.x+count);
195                    applyCursorPosition();          
196            }
197            
198            @Override
199            protected void processCursorDown(int count) throws IOException {
200                    getConsoleInfo();
201                    info.cursorPosition.y = (short) Math.min(info.size.y, info.cursorPosition.y+count);
202                    applyCursorPosition();          
203            }
204            
205            @Override
206            protected void processCursorUp(int count) throws IOException {
207                    getConsoleInfo();
208                    info.cursorPosition.y = (short) Math.max(info.window.top, info.cursorPosition.y-count);
209                    applyCursorPosition();          
210            }
211            
212            @Override
213            protected void processCursorTo(int row, int col) throws IOException {
214                    getConsoleInfo();
215                    info.cursorPosition.y = (short) Math.max(info.window.top, Math.min(info.size.y, info.window.top+row-1));
216                    info.cursorPosition.x = (short) Math.max(0, Math.min(info.window.width(), col-1));
217                    applyCursorPosition();          
218            }
219    
220            @Override
221            protected void processCursorToColumn(int x) throws IOException {
222                    getConsoleInfo();
223                    info.cursorPosition.x = (short) Math.max(0, Math.min(info.window.width(), x-1));
224                    applyCursorPosition();
225            }
226            
227            @Override
228            protected void processSetForegroundColor(int color) throws IOException {
229                    info.attributes = (short)((info.attributes & ~0x0007 ) | ANSI_FOREGROUND_COLOR_MAP[color]);
230                    applyAttribute();
231            }
232    
233            @Override
234            protected void processSetBackgroundColor(int color) throws IOException {
235                    info.attributes = (short)((info.attributes & ~0x0070 ) | ANSI_BACKGROUND_COLOR_MAP[color]);
236                    applyAttribute();
237            }
238    
239            @Override
240            protected void processAttributeRest() throws IOException {
241                    info.attributes = (short)((info.attributes & ~0x00FF ) | originalColors);
242            this.negative = false;
243                    applyAttribute();
244            }
245            
246            @Override
247            protected void processSetAttribute(int attribute) throws IOException {
248                    switch(attribute) {
249                            case ATTRIBUTE_INTENSITY_BOLD:
250                                    info.attributes = (short)(info.attributes | FOREGROUND_INTENSITY );
251                                    applyAttribute();
252                                    break;
253                            case ATTRIBUTE_INTENSITY_NORMAL:
254                                    info.attributes = (short)(info.attributes & ~FOREGROUND_INTENSITY );
255                                    applyAttribute();
256                                    break;
257                            
258                            // Yeah, setting the background intensity is not underlining.. but it's best we can do 
259                            // using the Windows console API 
260                            case ATTRIBUTE_UNDERLINE:
261                                    info.attributes = (short)(info.attributes | BACKGROUND_INTENSITY );
262                                    applyAttribute();
263                                    break;
264                            case ATTRIBUTE_UNDERLINE_OFF:
265                                    info.attributes = (short)(info.attributes & ~BACKGROUND_INTENSITY );
266                                    applyAttribute();
267                                    break;
268                                    
269                            case ATTRIBUTE_NEGATIVE_ON:
270                                    negative = true;
271                                    applyAttribute();
272                                    break;
273                            case ATTRIBUTE_NEGATIVE_Off:
274                                    negative = false;
275                                    applyAttribute();
276                                    break;
277                    }
278            }
279            
280            @Override
281            protected void processSaveCursorPosition() throws IOException {
282                    getConsoleInfo();
283                    savedX = info.cursorPosition.x;
284                    savedY = info.cursorPosition.y;
285            }
286            
287            @Override
288            protected void processRestoreCursorPosition() throws IOException {
289                    // restore only if there was a save operation first
290                    if (savedX != -1 && savedY != -1) {
291                            out.flush();
292                            info.cursorPosition.x = savedX;
293                            info.cursorPosition.y = savedY;
294                            applyCursorPosition();
295                    }
296            }
297    }