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