All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.pkl.thirdparty.jline.terminal.TerminalBuilder Maven / Gradle / Ivy

Go to download

Fat Jar containing pkl-cli, pkl-codegen-java, pkl-codegen-kotlin, pkl-config-java, pkl-core, pkl-doc, and their shaded third-party dependencies.

There is a newer version: 0.27.1
Show newest version
/*
 * Copyright (c) 2002-2021, the original author(s).
 *
 * This software is distributable under the BSD license. See the terms of the
 * BSD license in the documentation provided with this software.
 *
 * https://opensource.org/licenses/BSD-3-Clause
 */
package org.pkl.thirdparty.jline.terminal;

import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.charset.UnsupportedCharsetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.pkl.thirdparty.jline.terminal.impl.AbstractPosixTerminal;
import org.pkl.thirdparty.jline.terminal.impl.AbstractTerminal;
import org.pkl.thirdparty.jline.terminal.impl.DumbTerminal;
import org.pkl.thirdparty.jline.terminal.spi.TerminalProvider;
import org.pkl.thirdparty.jline.utils.Log;
import org.pkl.thirdparty.jline.utils.OSUtils;

/**
 * Builder class to create terminals.
 */
public final class TerminalBuilder {

    //
    // System properties
    //

    public static final String PROP_ENCODING = "org.pkl.thirdparty.jline.terminal.encoding";
    public static final String PROP_CODEPAGE = "org.pkl.thirdparty.jline.terminal.codepage";
    public static final String PROP_TYPE = "org.pkl.thirdparty.jline.terminal.type";
    public static final String PROP_PROVIDERS = "org.pkl.thirdparty.jline.terminal.providers";
    public static final String PROP_PROVIDERS_DEFAULT = "jansi,jna,exec";
    public static final String PROP_JNA = "org.pkl.thirdparty.jline.terminal.jna";
    public static final String PROP_JANSI = "org.pkl.thirdparty.jline.terminal.jansi";
    public static final String PROP_EXEC = "org.pkl.thirdparty.jline.terminal.exec";
    public static final String PROP_DUMB = "org.pkl.thirdparty.jline.terminal.dumb";
    public static final String PROP_DUMB_COLOR = "org.pkl.thirdparty.jline.terminal.dumb.color";
    public static final String PROP_OUTPUT = "org.pkl.thirdparty.jline.terminal.output";
    public static final String PROP_OUTPUT_OUT = "out";
    public static final String PROP_OUTPUT_ERR = "err";
    public static final String PROP_OUTPUT_OUT_ERR = "out-err";
    public static final String PROP_OUTPUT_ERR_OUT = "err-out";

    //
    // Other system properties controlling various jline parts
    //

    public static final String PROP_NON_BLOCKING_READS = "org.pkl.thirdparty.jline.terminal.pty.nonBlockingReads";
    public static final String PROP_COLOR_DISTANCE = "org.pkl.thirdparty.jline.utils.colorDistance";
    public static final String PROP_DISABLE_ALTERNATE_CHARSET = "org.pkl.thirdparty.jline.utils.disableAlternateCharset";

    //
    // System properties controlling how FileDescriptor are create.
    // The value can be a comma separated list of defined mechanisms.
    //
    public static final String PROP_FILE_DESCRIPTOR_CREATION_MODE = "org.pkl.thirdparty.jline.terminal.pty.fileDescriptorCreationMode";
    public static final String PROP_FILE_DESCRIPTOR_CREATION_MODE_NATIVE = "native";
    public static final String PROP_FILE_DESCRIPTOR_CREATION_MODE_REFLECTION = "reflection";
    public static final String PROP_FILE_DESCRIPTOR_CREATION_MODE_DEFAULT = "reflection,native";

    //
    // Terminal output control
    //
    public enum SystemOutput {
        SysOut,
        SysErr,
        SysOutOrSysErr,
        SysErrOrSysOut
    }

    /**
     * Returns the default system terminal.
     * Terminals should be closed properly using the {@link Terminal#close()}
     * method in order to restore the original terminal state.
     *
     * 

* This call is equivalent to: * builder().build() *

* * @return the default system terminal * @throws IOException if an error occurs */ public static Terminal terminal() throws IOException { return builder().build(); } /** * Creates a new terminal builder instance. * * @return a builder */ public static TerminalBuilder builder() { return new TerminalBuilder(); } private static final AtomicReference SYSTEM_TERMINAL = new AtomicReference<>(); private static final AtomicReference TERMINAL_OVERRIDE = new AtomicReference<>(); private String name; private InputStream in; private OutputStream out; private String type; private Charset encoding; private int codepage; private Boolean system; private SystemOutput systemOutput; private Boolean jna; private Boolean jansi; private Boolean exec; private Boolean dumb; private Boolean color; private Attributes attributes; private Size size; private boolean nativeSignals = false; private Terminal.SignalHandler signalHandler = Terminal.SignalHandler.SIG_DFL; private boolean paused = false; private TerminalBuilder() {} public TerminalBuilder name(String name) { this.name = name; return this; } public TerminalBuilder streams(InputStream in, OutputStream out) { this.in = in; this.out = out; return this; } public TerminalBuilder system(boolean system) { this.system = system; return this; } /** * Indicates which standard stream should be used when displaying to the terminal. * The default is to use the system output stream. * Building a system terminal will fail if one of the stream specified is not linked * to the controlling terminal. * * @param systemOutput The mode to choose the output stream. * @return The builder. */ public TerminalBuilder systemOutput(SystemOutput systemOutput) { this.systemOutput = systemOutput; return this; } public TerminalBuilder jna(boolean jna) { this.jna = jna; return this; } public TerminalBuilder jansi(boolean jansi) { this.jansi = jansi; return this; } public TerminalBuilder exec(boolean exec) { this.exec = exec; return this; } public TerminalBuilder dumb(boolean dumb) { this.dumb = dumb; return this; } public TerminalBuilder type(String type) { this.type = type; return this; } public TerminalBuilder color(boolean color) { this.color = color; return this; } /** * Set the encoding to use for reading/writing from the console. * If {@code null} (the default value), JLine will automatically select * a {@link Charset}, usually the default system encoding. However, * on some platforms (e.g. Windows) it may use a different one depending * on the {@link Terminal} implementation. * *

Use {@link Terminal#encoding()} to get the {@link Charset} that * should be used for a {@link Terminal}.

* * @param encoding The encoding to use or null to automatically select one * @return The builder * @throws UnsupportedCharsetException If the given encoding is not supported * @see Terminal#encoding() */ public TerminalBuilder encoding(String encoding) throws UnsupportedCharsetException { return encoding(encoding != null ? Charset.forName(encoding) : null); } /** * Set the {@link Charset} to use for reading/writing from the console. * If {@code null} (the default value), JLine will automatically select * a {@link Charset}, usually the default system encoding. However, * on some platforms (e.g. Windows) it may use a different one depending * on the {@link Terminal} implementation. * *

Use {@link Terminal#encoding()} to get the {@link Charset} that * should be used to read/write from a {@link Terminal}.

* * @param encoding The encoding to use or null to automatically select one * @return The builder * @see Terminal#encoding() */ public TerminalBuilder encoding(Charset encoding) { this.encoding = encoding; return this; } /** * @param codepage the codepage * @return The builder * @deprecated JLine now writes Unicode output independently from the selected * code page. Using this option will only make it emulate the selected code * page for {@link Terminal#input()} and {@link Terminal#output()}. */ @Deprecated public TerminalBuilder codepage(int codepage) { this.codepage = codepage; return this; } /** * Attributes to use when creating a non system terminal, * i.e. when the builder has been given the input and * output streams using the {@link #streams(InputStream, OutputStream)} method * or when {@link #system(boolean)} has been explicitly called with * false. * * @param attributes the attributes to use * @return The builder * @see #size(Size) * @see #system(boolean) */ public TerminalBuilder attributes(Attributes attributes) { this.attributes = attributes; return this; } /** * Initial size to use when creating a non system terminal, * i.e. when the builder has been given the input and * output streams using the {@link #streams(InputStream, OutputStream)} method * or when {@link #system(boolean)} has been explicitly called with * false. * * @param size the initial size * @return The builder * @see #attributes(Attributes) * @see #system(boolean) */ public TerminalBuilder size(Size size) { this.size = size; return this; } public TerminalBuilder nativeSignals(boolean nativeSignals) { this.nativeSignals = nativeSignals; return this; } public TerminalBuilder signalHandler(Terminal.SignalHandler signalHandler) { this.signalHandler = signalHandler; return this; } /** * Initial paused state of the terminal (defaults to false). * By default, the terminal is started, but in some cases, * one might want to make sure the input stream is not consumed * before needed, in which case the terminal needs to be created * in a paused state. * @param paused the initial paused state * @return The builder * @see Terminal#pause() */ public TerminalBuilder paused(boolean paused) { this.paused = paused; return this; } public Terminal build() throws IOException { Terminal override = TERMINAL_OVERRIDE.get(); Terminal terminal = override != null ? override : doBuild(); if (override != null) { Log.debug(() -> "Overriding terminal with global value set by TerminalBuilder.setTerminalOverride"); } Log.debug(() -> "Using terminal " + terminal.getClass().getSimpleName()); if (terminal instanceof AbstractPosixTerminal) { Log.debug(() -> "Using pty " + ((AbstractPosixTerminal) terminal).getPty().getClass().getSimpleName()); } return terminal; } private Terminal doBuild() throws IOException { String name = this.name; if (name == null) { name = "JLine terminal"; } Charset encoding = this.encoding; if (encoding == null) { String charsetName = System.getProperty(PROP_ENCODING); if (charsetName != null && Charset.isSupported(charsetName)) { encoding = Charset.forName(charsetName); } } if (encoding == null) { int codepage = this.codepage; if (codepage <= 0) { String str = System.getProperty(PROP_CODEPAGE); if (str != null) { codepage = Integer.parseInt(str); } } if (codepage >= 0) { encoding = getCodepageCharset(codepage); } else { encoding = StandardCharsets.UTF_8; } } String type = this.type; if (type == null) { type = System.getProperty(PROP_TYPE); } if (type == null) { type = System.getenv("TERM"); } Boolean jna = this.jna; if (jna == null) { jna = getBoolean(PROP_JNA, true); } Boolean jansi = this.jansi; if (jansi == null) { jansi = getBoolean(PROP_JANSI, true); } Boolean exec = this.exec; if (exec == null) { exec = getBoolean(PROP_EXEC, true); } Boolean dumb = this.dumb; if (dumb == null) { dumb = getBoolean(PROP_DUMB, null); } IllegalStateException exception = new IllegalStateException("Unable to create a terminal"); List providers = new ArrayList<>(); if (jna) { try { TerminalProvider provider = TerminalProvider.load("jna"); providers.add(provider); } catch (Throwable t) { Log.debug("Unable to load JNA support: ", t); exception.addSuppressed(t); } } if (jansi) { try { TerminalProvider provider = TerminalProvider.load("jansi"); providers.add(provider); } catch (Throwable t) { Log.debug("Unable to load JANSI support: ", t); exception.addSuppressed(t); } } if (exec) { try { TerminalProvider provider = TerminalProvider.load("exec"); providers.add(provider); } catch (Throwable t) { Log.debug("Unable to load EXEC support: ", t); exception.addSuppressed(t); } } List order = Arrays.asList( System.getProperty(PROP_PROVIDERS, PROP_PROVIDERS_DEFAULT).split(",")); providers.sort(Comparator.comparing(l -> { int idx = order.indexOf(l); return idx >= 0 ? idx : Integer.MAX_VALUE; })); Terminal terminal = null; if ((system != null && system) || (system == null && in == null && out == null)) { if (system != null && ((in != null && !in.equals(System.in)) || (out != null && !out.equals(System.out) && !out.equals(System.err)))) { throw new IllegalArgumentException("Cannot create a system terminal using non System streams"); } if (attributes != null || size != null) { Log.warn("Attributes and size fields are ignored when creating a system terminal"); } if (out != null) { if (out.equals(System.out)) { systemOutput = SystemOutput.SysOut; } else if (out.equals(System.err)) { systemOutput = SystemOutput.SysErr; } } if (systemOutput == null) { String str = System.getProperty(PROP_OUTPUT); if (str != null) { switch (str.trim().toLowerCase(Locale.ROOT)) { case PROP_OUTPUT_OUT: systemOutput = SystemOutput.SysOut; break; case PROP_OUTPUT_ERR: systemOutput = SystemOutput.SysErr; break; case PROP_OUTPUT_OUT_ERR: systemOutput = SystemOutput.SysOutOrSysErr; break; case PROP_OUTPUT_ERR_OUT: systemOutput = SystemOutput.SysErrOrSysOut; break; default: Log.debug("Unsupported value for " + PROP_OUTPUT + ": " + str + ". Supported values are: " + String.join( ", ", PROP_OUTPUT_OUT, PROP_OUTPUT_ERR, PROP_OUTPUT_OUT_ERR, PROP_OUTPUT_ERR_OUT) + "."); } } } if (systemOutput == null) { systemOutput = SystemOutput.SysOutOrSysErr; } Map system = Stream.of(TerminalProvider.Stream.values()) .collect(Collectors.toMap( stream -> stream, stream -> providers.stream().anyMatch(p -> p.isSystemStream(stream)))); TerminalProvider.Stream console = select(system, systemOutput); if (system.get(TerminalProvider.Stream.Input) && console != null) { if (attributes != null || size != null) { Log.warn("Attributes and size fields are ignored when creating a system terminal"); } boolean ansiPassThrough = OSUtils.IS_CONEMU; // Cygwin defaults to XTERM, but actually supports 256 colors, // so if the value comes from the environment, change it to xterm-256color if ((OSUtils.IS_CYGWIN || OSUtils.IS_MSYSTEM) && "xterm".equals(type) && this.type == null && System.getProperty(PROP_TYPE) == null) { type = "xterm-256color"; } for (TerminalProvider provider : providers) { if (terminal == null) { try { terminal = provider.sysTerminal( name, type, ansiPassThrough, encoding, nativeSignals, signalHandler, paused, console); } catch (Throwable t) { Log.debug("Error creating " + provider.name() + " based terminal: ", t.getMessage(), t); exception.addSuppressed(t); } } } if (terminal == null && OSUtils.IS_WINDOWS && !jna && !jansi && (dumb == null || !dumb)) { throw new IllegalStateException("Unable to create a system terminal. On windows, either " + "JNA or JANSI library is required. Make sure to add one of those in the classpath."); } } if (terminal instanceof AbstractTerminal) { AbstractTerminal t = (AbstractTerminal) terminal; if (SYSTEM_TERMINAL.compareAndSet(null, t)) { t.setOnClose(() -> SYSTEM_TERMINAL.compareAndSet(t, null)); } else { exception.addSuppressed(new IllegalStateException("A system terminal is already running. " + "Make sure to use the created system Terminal on the LineReaderBuilder if you're using one " + "or that previously created system Terminals have been correctly closed.")); terminal.close(); terminal = null; } } if (terminal == null && (dumb == null || dumb)) { // forced colored dumb terminal Boolean color = this.color; if (color == null) { color = getBoolean(PROP_DUMB_COLOR, null); } if (dumb == null) { // detect emacs using the env variable if (color == null) { String emacs = System.getenv("INSIDE_EMACS"); if (emacs != null && emacs.contains("comint")) { color = true; } } // detect Intellij Idea if (color == null) { // using the env variable on windows String ideHome = System.getenv("IDE_HOME"); if (ideHome != null) { color = true; } else { // using the parent process command on unix/mac String command = getParentProcessCommand(); if (command != null && command.endsWith("/idea")) { color = true; } } } if (color == null) { color = console != null && System.getenv("TERM") != null; } if (Log.isDebugEnabled()) { Log.warn("input is tty: {}", system.get(TerminalProvider.Stream.Input)); Log.warn("output is tty: {}", system.get(TerminalProvider.Stream.Output)); Log.warn("error is tty: {}", system.get(TerminalProvider.Stream.Error)); Log.warn("Creating a dumb terminal", exception); } else { Log.warn( "Unable to create a system terminal, creating a dumb terminal (enable debug logging for more information)"); } } else { if (color == null) { color = false; } } terminal = new DumbTerminal( name, color ? Terminal.TYPE_DUMB_COLOR : Terminal.TYPE_DUMB, new FileInputStream(FileDescriptor.in), new FileOutputStream( console == TerminalProvider.Stream.Output ? FileDescriptor.out : FileDescriptor.err), encoding, signalHandler); } } else { for (TerminalProvider provider : providers) { if (terminal == null) { try { terminal = provider.newTerminal( name, type, in, out, encoding, signalHandler, paused, attributes, size); } catch (Throwable t) { Log.debug("Error creating " + provider.name() + " based terminal: ", t.getMessage(), t); exception.addSuppressed(t); } } } } if (terminal == null) { throw exception; } return terminal; } private TerminalProvider.Stream select(Map system, SystemOutput systemOutput) { switch (systemOutput) { case SysOut: return select(system, TerminalProvider.Stream.Output); case SysErr: return select(system, TerminalProvider.Stream.Error); case SysOutOrSysErr: return select(system, TerminalProvider.Stream.Output, TerminalProvider.Stream.Error); case SysErrOrSysOut: return select(system, TerminalProvider.Stream.Error, TerminalProvider.Stream.Output); } return null; } private static TerminalProvider.Stream select( Map system, TerminalProvider.Stream... streams) { for (TerminalProvider.Stream s : streams) { if (system.get(s)) { return s; } } return null; } private static String getParentProcessCommand() { try { Class phClass = Class.forName("java.lang.ProcessHandle"); Object current = phClass.getMethod("current").invoke(null); Object parent = ((Optional) phClass.getMethod("parent").invoke(current)).orElse(null); Method infoMethod = phClass.getMethod("info"); Object info = infoMethod.invoke(parent); Object command = ((Optional) infoMethod.getReturnType().getMethod("command").invoke(info)) .orElse(null); return (String) command; } catch (Throwable t) { return null; } } private static Boolean getBoolean(String name, Boolean def) { try { String str = System.getProperty(name); if (str != null) { return Boolean.parseBoolean(str); } } catch (IllegalArgumentException | NullPointerException e) { } return def; } private static S load(Class clazz) { return ServiceLoader.load(clazz, clazz.getClassLoader()).iterator().next(); } private static final int UTF8_CODE_PAGE = 65001; private static Charset getCodepageCharset(int codepage) { // http://docs.oracle.com/javase/6/docs/technotes/guides/intl/encoding.doc.html if (codepage == UTF8_CODE_PAGE) { return StandardCharsets.UTF_8; } String charsetMS = "ms" + codepage; if (Charset.isSupported(charsetMS)) { return Charset.forName(charsetMS); } String charsetCP = "cp" + codepage; if (Charset.isSupported(charsetCP)) { return Charset.forName(charsetCP); } return Charset.defaultCharset(); } /** * Allows an application to override the result of {@link #build()}. The * intended use case is to allow a container or server application to control * an embedded application that uses a LineReader that uses Terminal * constructed with TerminalBuilder.build but provides no public api for setting * the LineReader of the {@link Terminal}. For example, the sbt * build tool uses a LineReader to implement an interactive shell. * One of its supported commands is console which invokes * the scala REPL. The scala REPL also uses a LineReader and it * is necessary to override the {@link Terminal} used by the the REPL to * share the same {@link Terminal} instance used by sbt. * *

* When this method is called with a non-null {@link Terminal}, all subsequent * calls to {@link #build()} will return the provided {@link Terminal} regardless * of how the {@link TerminalBuilder} was constructed. The default behavior * of {@link TerminalBuilder} can be restored by calling setTerminalOverride * with a null {@link Terminal} *

* *

* Usage of setTerminalOverride should be restricted to cases where it * isn't possible to update the api of the nested application to accept * a {@link Terminal instance}. *

* * @param terminal the {@link Terminal} to globally override */ @Deprecated public static void setTerminalOverride(final Terminal terminal) { TERMINAL_OVERRIDE.set(terminal); } }