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.CLibrary.STDERR_FILENO;
019import static org.fusesource.jansi.internal.CLibrary.STDOUT_FILENO;
020import static org.fusesource.jansi.internal.CLibrary.isatty;
021
022import java.io.FilterOutputStream;
023import java.io.IOException;
024import java.io.OutputStream;
025import java.io.PrintStream;
026import java.util.Locale;
027
028/**
029 * Provides consistent access to an ANSI aware console PrintStream or an ANSI codes stripping PrintStream
030 * if not on a terminal (see 
031 * <a href="http://fusesource.github.io/jansi/documentation/native-api/index.html?org/fusesource/jansi/internal/CLibrary.html">Jansi native isatty(int)</a>).
032 *
033 * @author <a href="http://hiramchirino.com">Hiram Chirino</a>
034 * @since 1.0
035 * @see #systemInstall()
036 * @see #wrapPrintStream(PrintStream, int) wrapPrintStream(PrintStream, int) for more details on ANSI mode selection
037 */
038public class AnsiConsole {
039
040    public static final PrintStream system_out = System.out;
041    public static final PrintStream out;
042
043    public static final PrintStream system_err = System.err;
044    public static final PrintStream err;
045
046    static final boolean IS_WINDOWS = System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("win");
047
048    static final boolean IS_CYGWIN = IS_WINDOWS
049            && System.getenv("PWD") != null
050            && System.getenv("PWD").startsWith("/")
051            && !"cygwin".equals(System.getenv("TERM"));
052
053    static final boolean IS_MINGW_XTERM = IS_WINDOWS
054            && System.getenv("MSYSTEM") != null
055            && System.getenv("MSYSTEM").startsWith("MINGW")
056            && "xterm".equals(System.getenv("TERM"));
057
058    private static JansiOutputType jansiOutputType;
059    static final JansiOutputType JANSI_STDOUT_TYPE;
060    static final JansiOutputType JANSI_STDERR_TYPE;
061    static {
062        out = wrapSystemOut(system_out);
063        JANSI_STDOUT_TYPE = jansiOutputType;
064        err = wrapSystemErr(system_err);
065        JANSI_STDERR_TYPE = jansiOutputType;
066    }
067
068    private static int installed;
069
070    private AnsiConsole() {
071    }
072
073    @Deprecated
074    public static OutputStream wrapOutputStream(final OutputStream stream) {
075        try {
076            return wrapOutputStream(stream, STDOUT_FILENO);
077        } catch (Throwable ignore) {
078            return wrapOutputStream(stream, 1);
079        }
080    }
081
082    public static PrintStream wrapSystemOut(final PrintStream ps) {
083        try {
084            return wrapPrintStream(ps, STDOUT_FILENO);
085        } catch (Throwable ignore) {
086            return wrapPrintStream(ps, 1);
087        }
088    }
089
090    @Deprecated
091    public static OutputStream wrapErrorOutputStream(final OutputStream stream) {
092        try {
093            return wrapOutputStream(stream, STDERR_FILENO);
094        } catch (Throwable ignore) {
095            return wrapOutputStream(stream, 2);
096        }
097    }
098
099    public static PrintStream wrapSystemErr(final PrintStream ps) {
100        try {
101            return wrapPrintStream(ps, STDERR_FILENO);
102        } catch (Throwable ignore) {
103            return wrapPrintStream(ps, 2);
104        }
105    }
106
107    @Deprecated
108    public static OutputStream wrapOutputStream(final OutputStream stream, int fileno) {
109
110        // If the jansi.passthrough property is set, then don't interpret
111        // any of the ansi sequences.
112        if (Boolean.getBoolean("jansi.passthrough")) {
113            jansiOutputType = JansiOutputType.PASSTHROUGH;
114            return stream;
115        }
116
117        // If the jansi.strip property is set, then we just strip the
118        // the ansi escapes.
119        if (Boolean.getBoolean("jansi.strip")) {
120            jansiOutputType = JansiOutputType.STRIP_ANSI;
121            return new AnsiOutputStream(stream);
122        }
123
124        if (IS_WINDOWS && !IS_CYGWIN && !IS_MINGW_XTERM) {
125
126            // On windows we know the console does not interpret ANSI codes..
127            try {
128                jansiOutputType = JansiOutputType.WINDOWS;
129                return new WindowsAnsiOutputStream(stream);
130            } catch (Throwable ignore) {
131                // this happens when JNA is not in the path.. or
132                // this happens when the stdout is being redirected to a file.
133            }
134
135            // Use the ANSIOutputStream to strip out the ANSI escape sequences.
136            jansiOutputType = JansiOutputType.STRIP_ANSI;
137            return new AnsiOutputStream(stream);
138        }
139
140        // We must be on some Unix variant, including Cygwin or MSYS(2) on Windows...
141        try {
142            // If the jansi.force property is set, then we force to output
143            // the ansi escapes for piping it into ansi color aware commands (e.g. less -r)
144            boolean forceColored = Boolean.getBoolean("jansi.force");
145            // If we can detect that stdout is not a tty.. then setup
146            // to strip the ANSI sequences..
147            if (!forceColored && isatty(fileno) == 0) {
148                jansiOutputType = JansiOutputType.STRIP_ANSI;
149                return new AnsiOutputStream(stream);
150            }
151        } catch (Throwable ignore) {
152            // These errors happen if the JNI lib is not available for your platform.
153            // But since we are on ANSI friendly platform, assume the user is on the console.
154        }
155
156        // By default we assume your Unix tty can handle ANSI codes.
157        // Just wrap it up so that when we get closed, we reset the
158        // attributes.
159        jansiOutputType = JansiOutputType.RESET_ANSI_AT_CLOSE;
160        return new FilterOutputStream(stream) {
161            @Override
162            public void close() throws IOException {
163                write(AnsiOutputStream.RESET_CODE);
164                flush();
165                super.close();
166            }
167        };
168    }
169
170    /**
171     * Wrap PrintStream applying rules in following order:<ul>
172     * <li>if <code>jansi.passthrough</code> is <code>true</code>, don't wrap but just passthrough (console is
173     * expected to natively support ANSI escape codes),</li>
174     * <li>if <code>jansi.strip</code> is <code>true</code>, just strip ANSI escape codes inconditionally,</li>
175     * <li>if OS is Windows and terminal is not Cygwin or Mingw, wrap as WindowsAnsiPrintStream to process ANSI escape codes,</li>
176     * <li>if file descriptor is a terminal (see <code>isatty(int)</code>) or <code>jansi.force</code> is <code>true</code>,
177     * just passthrough,</li>
178     * <li>else strip ANSI escape codes (not a terminal).</li>
179     * </ul>
180     * 
181     * @param ps original PrintStream to wrap
182     * @param fileno file descriptor
183     * @return wrapped PrintStream depending on OS and system properties
184     * @since 1.17
185     */
186    public static PrintStream wrapPrintStream(final PrintStream ps, int fileno) {
187
188        // If the jansi.passthrough property is set, then don't interpret
189        // any of the ansi sequences.
190        if (Boolean.getBoolean("jansi.passthrough")) {
191            jansiOutputType = JansiOutputType.PASSTHROUGH;
192            return ps;
193        }
194
195        // If the jansi.strip property is set, then we just strip the
196        // the ansi escapes.
197        if (Boolean.getBoolean("jansi.strip")) {
198            jansiOutputType = JansiOutputType.STRIP_ANSI;
199            return new AnsiPrintStream(ps);
200        }
201
202        if (IS_WINDOWS && !IS_CYGWIN && !IS_MINGW_XTERM) {
203
204            // On windows we know the console does not interpret ANSI codes..
205            try {
206                jansiOutputType = JansiOutputType.WINDOWS;
207                return new WindowsAnsiPrintStream(ps);
208            } catch (Throwable ignore) {
209                // this happens when JNA is not in the path.. or
210                // this happens when the stdout is being redirected to a file.
211            }
212
213            // Use the AnsiPrintStream to strip out the ANSI escape sequences.
214            jansiOutputType = JansiOutputType.STRIP_ANSI;
215            return new AnsiPrintStream(ps);
216        }
217
218        // We must be on some Unix variant, including Cygwin or MSYS(2) on Windows...
219        try {
220            // If the jansi.force property is set, then we force to output
221            // the ansi escapes for piping it into ansi color aware commands (e.g. less -r)
222            boolean forceColored = Boolean.getBoolean("jansi.force");
223            // If we can detect that stdout is not a tty.. then setup
224            // to strip the ANSI sequences..
225            if (!forceColored && isatty(fileno) == 0) {
226                jansiOutputType = JansiOutputType.STRIP_ANSI;
227                return new AnsiPrintStream(ps);
228            }
229        } catch (Throwable ignore) {
230            // These errors happen if the JNI lib is not available for your platform.
231            // But since we are on ANSI friendly platform, assume the user is on the console.
232        }
233
234        // By default we assume your Unix tty can handle ANSI codes.
235        // Just wrap it up so that when we get closed, we reset the
236        // attributes.
237        jansiOutputType = JansiOutputType.RESET_ANSI_AT_CLOSE;
238        return new FilterPrintStream(ps) {
239            @Override
240            public void close() {
241                ps.print(AnsiPrintStream.RESET_CODE);
242                ps.flush();
243                super.close();
244            }
245        };
246    }
247
248    /**
249     * If the standard out natively supports ANSI escape codes, then this just
250     * returns System.out, otherwise it will provide an ANSI aware PrintStream
251     * which strips out the ANSI escape sequences or which implement the escape
252     * sequences.
253     *
254     * @return a PrintStream which is ANSI aware.
255     * @see #wrapPrintStream(PrintStream, int)
256     */
257    public static PrintStream out() {
258        return out;
259    }
260
261    /**
262     * If the standard out natively supports ANSI escape codes, then this just
263     * returns System.err, otherwise it will provide an ANSI aware PrintStream
264     * which strips out the ANSI escape sequences or which implement the escape
265     * sequences.
266     *
267     * @return a PrintStream which is ANSI aware.
268     * @see #wrapPrintStream(PrintStream, int)
269     */
270    public static PrintStream err() {
271        return err;
272    }
273
274    /**
275     * Install <code>AnsiConsole.out</code> to <code>System.out</code> and
276     * <code>AnsiConsole.err</code> to <code>System.err</code>.
277     * @see #systemUninstall()
278     */
279    synchronized static public void systemInstall() {
280        installed++;
281        if (installed == 1) {
282            System.setOut(out);
283            System.setErr(err);
284        }
285    }
286
287    /**
288     * undo a previous {@link #systemInstall()}.  If {@link #systemInstall()} was called
289     * multiple times, {@link #systemUninstall()} must be called the same number of times before
290     * it is actually uninstalled.
291     */
292    synchronized public static void systemUninstall() {
293        installed--;
294        if (installed == 0) {
295            System.setOut(system_out);
296            System.setErr(system_err);
297        }
298    }
299
300    /**
301     * Type of output installed by AnsiConsole.
302     */
303    enum JansiOutputType {
304        PASSTHROUGH("just pass through, ANSI escape codes are supposed to be supported by terminal"),
305        RESET_ANSI_AT_CLOSE("like pass through but reset ANSI attributes when closing the stream"),
306        STRIP_ANSI("strip ANSI escape codes, for example when output is not a terminal"),
307        WINDOWS("detect ANSI escape codes and transform Jansi-supported ones into a Windows API to get desired effect" +
308                " (since ANSI escape codes are not natively supported by Windows terminals like cmd.exe or PowerShell)");
309
310        private final String description;
311
312        private JansiOutputType(String description) {
313            this.description = description;
314        }
315
316        String getDescription() {
317            return description;
318        }
319    };
320}