-
Notifications
You must be signed in to change notification settings - Fork 213
/
TerminalBuilder.java
489 lines (456 loc) · 18.8 KB
/
TerminalBuilder.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
/*
* Copyright (c) 2002-2019, the original author or authors.
*
* 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.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.UnsupportedCharsetException;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.concurrent.atomic.AtomicReference;
import org.jline.terminal.impl.AbstractPosixTerminal;
import org.jline.terminal.impl.AbstractTerminal;
import org.jline.terminal.impl.DumbTerminal;
import org.jline.terminal.impl.ExecPty;
import org.jline.terminal.impl.ExternalTerminal;
import org.jline.terminal.impl.PosixPtyTerminal;
import org.jline.terminal.impl.PosixSysTerminal;
import org.jline.terminal.spi.JansiSupport;
import org.jline.terminal.spi.JnaSupport;
import org.jline.terminal.spi.Pty;
import org.jline.utils.Log;
import org.jline.utils.OSUtils;
/**
* Builder class to create terminals.
*/
public final class TerminalBuilder {
//
// System properties
//
public static final String PROP_ENCODING = "org.jline.terminal.encoding";
public static final String PROP_CODEPAGE = "org.jline.terminal.codepage";
public static final String PROP_TYPE = "org.jline.terminal.type";
public static final String PROP_JNA = "org.jline.terminal.jna";
public static final String PROP_JANSI = "org.jline.terminal.jansi";
public static final String PROP_EXEC = "org.jline.terminal.exec";
public static final String PROP_DUMB = "org.jline.terminal.dumb";
public static final String PROP_DUMB_COLOR = "org.jline.terminal.dumb.color";
//
// Other system properties controlling various jline parts
//
public static final String PROP_NON_BLOCKING_READS = "org.jline.terminal.pty.nonBlockingReads";
public static final String PROP_COLOR_DISTANCE = "org.jline.utils.colorDistance";
public static final String PROP_DISABLE_ALTERNATE_CHARSET = "org.jline.utils.disableAlternateCharset";
/**
* Returns the default system terminal.
* Terminals should be closed properly using the {@link Terminal#close()}
* method in order to restore the original terminal state.
*
* <p>
* This call is equivalent to:
* <code>builder().build()</code>
* </p>
*
* @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<Terminal> SYSTEM_TERMINAL = new AtomicReference<>();
private String name;
private InputStream in;
private OutputStream out;
private String type;
private Charset encoding;
private int codepage;
private Boolean system;
private Boolean jna;
private Boolean jansi;
private Boolean exec;
private Boolean dumb;
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;
}
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;
}
/**
* 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.
*
* <p>Use {@link Terminal#encoding()} to get the {@link Charset} that
* should be used for a {@link Terminal}.</p>
*
* @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.
*
* <p>Use {@link Terminal#encoding()} to get the {@link Charset} that
* should be used to read/write from a {@link Terminal}.</p>
*
* @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
* outut streams using the {@link #streams(InputStream, OutputStream)} method
* or when {@link #system(boolean)} has been explicitely called with
* <code>false</code>.
*
* @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
* outut streams using the {@link #streams(InputStream, OutputStream)} method
* or when {@link #system(boolean)} has been explicitely called with
* <code>false</code>.
*
* @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 terminal = doBuild();
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);
}
}
int codepage = this.codepage;
if (codepage <= 0) {
String str = System.getProperty(PROP_CODEPAGE);
if (str != null) {
codepage = Integer.parseInt(str);
}
}
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);
}
if ((system != null && system) || (system == null && in == null && out == null)) {
if (attributes != null || size != null) {
Log.warn("Attributes and size fields are ignored when creating a system terminal");
}
IllegalStateException exception = new IllegalStateException("Unable to create a system terminal");
Terminal terminal = null;
if (OSUtils.IS_WINDOWS) {
boolean ansiPassThrough = OSUtils.IS_CONEMU;
boolean winConsole = true;
if (terminal == null && jna) {
try {
JnaSupport support = load(JnaSupport.class);
winConsole = support.isWindowsConsole();
terminal = support.winSysTerminal(name, type, ansiPassThrough, encoding, codepage, nativeSignals, signalHandler, paused);
} catch (Throwable t) {
Log.debug("Error creating JNA based terminal: ", t.getMessage(), t);
exception.addSuppressed(t);
}
}
if (terminal == null && jansi) {
try {
JansiSupport support = load(JansiSupport.class);
winConsole = support.isWindowsConsole();
terminal = support.winSysTerminal(name, type, ansiPassThrough, encoding, codepage, nativeSignals, signalHandler, paused);
} catch (Throwable t) {
Log.debug("Error creating JANSI based terminal: ", t.getMessage(), t);
exception.addSuppressed(t);
}
}
//
// Cygwin support
//
if (terminal == null && exec && (OSUtils.IS_CYGWIN || OSUtils.IS_MSYSTEM) && winConsole) {
try {
Pty pty = ExecPty.current();
// Cygwin defaults to XTERM, but actually supports 256 colors,
// so if the value comes from the environment, change it to xterm-256color
if ("xterm".equals(type) && this.type == null && System.getProperty(PROP_TYPE) == null) {
type = "xterm-256color";
}
terminal = new PosixSysTerminal(name, type, pty, encoding, nativeSignals, signalHandler);
} catch (IOException e) {
// Ignore if not a tty
Log.debug("Error creating EXEC based terminal: ", e.getMessage(), e);
exception.addSuppressed(e);
}
}
if (terminal == null && !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.");
}
} else {
if (terminal == null && jna) {
try {
Pty pty = load(JnaSupport.class).current();
terminal = new PosixSysTerminal(name, type, pty, encoding, nativeSignals, signalHandler);
} catch (Throwable t) {
// ignore
Log.debug("Error creating JNA based terminal: ", t.getMessage(), t);
exception.addSuppressed(t);
}
}
if (terminal == null && jansi) {
try {
Pty pty = load(JansiSupport.class).current();
terminal = new PosixSysTerminal(name, type, pty, encoding, nativeSignals, signalHandler);
} catch (Throwable t) {
Log.debug("Error creating JANSI based terminal: ", t.getMessage(), t);
exception.addSuppressed(t);
}
}
if (terminal == null && exec) {
try {
Pty pty = ExecPty.current();
terminal = new PosixSysTerminal(name, type, pty, encoding, nativeSignals, signalHandler);
} catch (Throwable t) {
// Ignore if not a tty
Log.debug("Error creating EXEC based terminal: ", t.getMessage(), t);
exception.addSuppressed(t);
}
}
}
if (terminal instanceof AbstractTerminal) {
AbstractTerminal t = (AbstractTerminal) terminal;
if (SYSTEM_TERMINAL.compareAndSet(null, t)) {
t.setOnClose(new Runnable() {
@Override
public void run() {
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 = getBoolean(PROP_DUMB_COLOR, false);
// detect emacs using the env variable
if (!color) {
color = System.getenv("INSIDE_EMACS") != null;
}
// detect Intellij Idea
if (!color) {
String command = getParentProcessCommand();
color = command != null && command.contains("idea");
}
if (!color && dumb == null) {
if (Log.isDebugEnabled()) {
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)");
}
}
terminal = new DumbTerminal(name, color ? Terminal.TYPE_DUMB_COLOR : Terminal.TYPE_DUMB,
new FileInputStream(FileDescriptor.in),
new FileOutputStream(FileDescriptor.out),
encoding, signalHandler);
}
if (terminal == null) {
throw exception;
}
return terminal;
} else {
if (jna) {
try {
Pty pty = load(JnaSupport.class).open(attributes, size);
return new PosixPtyTerminal(name, type, pty, in, out, encoding, signalHandler, paused);
} catch (Throwable t) {
Log.debug("Error creating JNA based terminal: ", t.getMessage(), t);
}
}
if (jansi) {
try {
Pty pty = load(JansiSupport.class).open(attributes, size);
return new PosixPtyTerminal(name, type, pty, in, out, encoding, signalHandler, paused);
} catch (Throwable t) {
Log.debug("Error creating JANSI based terminal: ", t.getMessage(), t);
}
}
return new ExternalTerminal(name, type, in, out, encoding, signalHandler, paused, attributes, size);
}
}
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 <S> S load(Class<S> clazz) {
return ServiceLoader.load(clazz, clazz.getClassLoader()).iterator().next();
}
}