1818 */
1919package grails .build .logging ;
2020
21- import java .io .ByteArrayOutputStream ;
2221import java .io .File ;
23- import java .io .Flushable ;
2422import java .io .IOException ;
2523import java .io .InputStream ;
2624import java .io .PrintStream ;
2725import java .io .PrintWriter ;
2826import java .io .StringWriter ;
29- import java .util .Collection ;
3027import java .util .List ;
3128import java .util .Stack ;
3229
3330import org .codehaus .groovy .runtime .DefaultGroovyMethods ;
3431import org .codehaus .groovy .runtime .StackTraceUtils ;
3532import org .codehaus .groovy .runtime .typehandling .NumberMath ;
3633
37- import jline .Terminal ;
38- import jline .TerminalFactory ;
39- import jline .UnixTerminal ;
40- import jline .console .ConsoleReader ;
41- import jline .console .completer .Completer ;
42- import jline .console .history .FileHistory ;
43- import jline .console .history .History ;
44- import jline .internal .Log ;
45- import jline .internal .ShutdownHooks ;
46- import jline .internal .TerminalLineSettings ;
4734import org .apache .tools .ant .BuildException ;
4835import org .fusesource .jansi .Ansi ;
4936import org .fusesource .jansi .Ansi .Color ;
5037import org .fusesource .jansi .AnsiConsole ;
38+ import org .jline .reader .Completer ;
39+ import org .jline .reader .History ;
40+ import org .jline .reader .LineReader ;
41+ import org .jline .reader .LineReaderBuilder ;
42+ import org .jline .reader .impl .completer .AggregateCompleter ;
43+ import org .jline .reader .impl .history .DefaultHistory ;
44+ import org .jline .terminal .Terminal ;
45+ import org .jline .terminal .TerminalBuilder ;
5146
5247import grails .util .Environment ;
53- import org .grails .build .interactive .CandidateListCompletionHandler ;
5448import org .grails .build .logging .GrailsConsoleErrorPrintStream ;
5549import org .grails .build .logging .GrailsConsolePrintStream ;
5650
@@ -114,7 +108,7 @@ public class GrailsConsole implements ConsoleLogger {
114108 /**
115109 * The reader to read info from the console
116110 */
117- ConsoleReader reader ;
111+ LineReader reader ;
118112
119113 Terminal terminal ;
120114
@@ -123,6 +117,11 @@ public class GrailsConsole implements ConsoleLogger {
123117
124118 History history ;
125119
120+ /**
121+ * List of completers to be aggregated for tab completion
122+ */
123+ private final List <Completer > completers = new java .util .ArrayList <>();
124+
126125 /**
127126 * The category of the current output
128127 */
@@ -179,8 +178,8 @@ protected GrailsConsole() throws IOException {
179178 * @throws IOException
180179 */
181180 public void reinitialize (InputStream systemIn , PrintStream systemOut , PrintStream systemErr ) throws IOException {
182- if (reader != null ) {
183- reader . shutdown ();
181+ if (terminal != null ) {
182+ terminal . close ();
184183 }
185184 initialize (systemIn , systemOut , systemErr );
186185 }
@@ -190,20 +189,10 @@ protected void initialize(InputStream systemIn, PrintStream systemOut, PrintStre
190189
191190 redirectSystemOutAndErr (true );
192191
193- System .setProperty (ShutdownHooks .JLINE_SHUTDOWNHOOK , "false" );
194-
195192 if (isInteractiveEnabled ()) {
196- reader = createConsoleReader (systemIn );
197- reader .setBellEnabled (false );
198- reader .setCompletionHandler (new CandidateListCompletionHandler ());
199- if (isActivateTerminal ()) {
200- terminal = createTerminal ();
201- }
202-
193+ terminal = createTerminal ();
203194 history = prepareHistory ();
204- if (history != null ) {
205- reader .setHistory (history );
206- }
195+ reader = createLineReader (terminal , history );
207196 } else if (isActivateTerminal ()) {
208197 terminal = createTerminal ();
209198 }
@@ -251,51 +240,67 @@ private boolean readPropOrTrue(String prop) {
251240 return property == null ? true : Boolean .valueOf (property );
252241 }
253242
254- protected ConsoleReader createConsoleReader (InputStream systemIn ) throws IOException {
255- // need to swap out the output to avoid logging during init
256- final PrintStream nullOutput = new PrintStream (new ByteArrayOutputStream ());
257- final PrintStream originalOut = Log .getOutput ();
258- try {
259- Log .setOutput (nullOutput );
260- ConsoleReader consoleReader = new ConsoleReader (systemIn , out );
261- consoleReader .setExpandEvents (false );
262- return consoleReader ;
263- } finally {
264- Log .setOutput (originalOut );
243+ protected LineReader createLineReader (Terminal terminal , History history ) throws IOException {
244+ LineReaderBuilder builder = LineReaderBuilder .builder ()
245+ .terminal (terminal )
246+ .option (LineReader .Option .DISABLE_EVENT_EXPANSION , true );
247+ if (history != null ) {
248+ builder .variable (LineReader .HISTORY_FILE , new File (System .getProperty ("user.home" ), HISTORYFILE ).toPath ());
249+ builder .history (history );
265250 }
251+ return builder .build ();
266252 }
267253
268254 /**
269- * Creates the instance of Terminal used directly in GrailsConsole. Note that there is also
270- * another terminal instance created implicitly inside of ConsoleReader. That instance
271- * is controlled by the jline.terminal system property.
255+ * Creates the instance of Terminal used directly in GrailsConsole.
272256 */
273- protected Terminal createTerminal () {
274- terminal = TerminalFactory .create ();
275- if (isWindows ()) {
276- terminal .setEchoEnabled (true );
277- }
257+ protected Terminal createTerminal () throws IOException {
258+ Terminal terminal = TerminalBuilder .builder ()
259+ .system (true )
260+ .build ();
278261 return terminal ;
279262 }
280263
281264 public void resetCompleters () {
282- final ConsoleReader reader = getReader ();
283- if (reader != null ) {
284- Collection <Completer > completers = reader .getCompleters ();
285- for (Completer completer : completers ) {
286- reader .removeCompleter (completer );
287- }
265+ completers .clear ();
266+ rebuildLineReader ();
267+ }
268+
269+ public void addCompleter (Completer completer ) {
270+ if (completer != null ) {
271+ completers .add (completer );
272+ rebuildLineReader ();
273+ }
274+ }
275+
276+ /**
277+ * Rebuilds the LineReader with all registered completers using an AggregateCompleter.
278+ * This is necessary because JLine 3 LineReader is immutable once created.
279+ */
280+ private void rebuildLineReader () {
281+ if (terminal != null ) {
282+ try {
283+ LineReaderBuilder builder = LineReaderBuilder .builder ()
284+ .terminal (terminal )
285+ .option (LineReader .Option .DISABLE_EVENT_EXPANSION , true );
286+
287+ if (!completers .isEmpty ()) {
288+ builder .completer (new AggregateCompleter (completers ));
289+ }
288290
289- // for some unknown reason / bug in JLine you have to iterate over twice to clear the completers (WTF)
290- completers = reader .getCompleters ();
291- for (Completer completer : completers ) {
292- reader .removeCompleter (completer );
291+ if (history != null ) {
292+ builder .variable (LineReader .HISTORY_FILE , new File (System .getProperty ("user.home" ), HISTORYFILE ).toPath ());
293+ builder .history (history );
294+ }
295+ reader = builder .build ();
296+ } catch (Exception e ) {
297+ // ignore
293298 }
294299 }
295300 }
296301
297302 /**
298- * Prepares a history file to be used by the ConsoleReader . This file
303+ * Prepares a history file to be used by the LineReader . This file
299304 * will live in the home directory of the user.
300305 */
301306 protected History prepareHistory () throws IOException {
@@ -307,7 +312,7 @@ protected History prepareHistory() throws IOException {
307312 // can't create the file, so no history for you
308313 }
309314 }
310- return file .canWrite () ? new FileHistory ( file ) : null ;
315+ return file .canWrite () ? new DefaultHistory ( ) : null ;
311316 }
312317
313318 public boolean isWindows () {
@@ -334,8 +339,12 @@ public static synchronized void removeInstance() {
334339 if (instance != null ) {
335340 instance .removeShutdownHook ();
336341 instance .restoreOriginalSystemOutAndErr ();
337- if (instance .getReader () != null ) {
338- instance .getReader ().shutdown ();
342+ if (instance .terminal != null ) {
343+ try {
344+ instance .terminal .close ();
345+ } catch (IOException e ) {
346+ // ignore
347+ }
339348 }
340349 instance = null ;
341350 }
@@ -348,24 +357,18 @@ public void beforeShutdown() {
348357
349358 protected void restoreTerminal () {
350359 try {
351- terminal .restore ();
360+ if (terminal != null ) {
361+ terminal .close ();
362+ }
352363 } catch (Exception e ) {
353364 // ignore
354365 }
355- if (terminal instanceof UnixTerminal ) {
356- // workaround for GRAILS-11494
357- try {
358- new TerminalLineSettings ().set ("sane" );
359- } catch (Exception e ) {
360- // ignore
361- }
362- }
363366 }
364367
365368 protected void persistHistory () {
366- if (history instanceof Flushable ) {
369+ if (history != null && reader != null ) {
367370 try {
368- (( Flushable ) history ). flush ();
371+ history . save ();
369372 } catch (Throwable e ) {
370373 // ignore exception
371374 }
@@ -442,7 +445,7 @@ public boolean isStacktrace() {
442445 */
443446 public InputStream getInput () {
444447 assertAllowInput ();
445- return reader . getInput () ;
448+ return terminal != null ? terminal . input () : System . in ;
446449 }
447450
448451 private void assertAllowInput () {
@@ -471,7 +474,7 @@ public void setLastMessage(String lastMessage) {
471474 this .lastMessage = lastMessage ;
472475 }
473476
474- public ConsoleReader getReader () {
477+ public LineReader getReader () {
475478 return reader ;
476479 }
477480
@@ -674,7 +677,7 @@ private void logSimpleError(String msg) {
674677 }
675678
676679 public boolean isAnsiEnabled () {
677- return Ansi .isEnabled () && (terminal != null && terminal .isAnsiSupported ( )) && ansiEnabled ;
680+ return Ansi .isEnabled () && (terminal != null && ! "dumb" . equals ( terminal .getType () )) && ansiEnabled ;
678681 }
679682
680683 /**
@@ -875,10 +878,17 @@ private String readLine(String prompt, boolean secure) {
875878 assertAllowInput (prompt );
876879 userInputActive = true ;
877880 try {
878- Character inputMask = secure ? SECURE_MASK_CHAR : defaultInputMask ;
879- return reader .readLine (prompt , inputMask );
880- } catch (IOException e ) {
881- throw new RuntimeException ("Error reading input: " + e .getMessage ());
881+ if (secure ) {
882+ return reader .readLine (prompt , SECURE_MASK_CHAR );
883+ } else if (defaultInputMask == null ) {
884+ return reader .readLine (prompt );
885+ } else {
886+ return reader .readLine (prompt , defaultInputMask );
887+ }
888+ } catch (org .jline .reader .UserInterruptException e ) {
889+ return null ;
890+ } catch (org .jline .reader .EndOfFileException e ) {
891+ return null ;
882892 } finally {
883893 userInputActive = false ;
884894 }
@@ -1041,4 +1051,12 @@ public Character getDefaultInputMask() {
10411051 public void setDefaultInputMask (Character defaultInputMask ) {
10421052 this .defaultInputMask = defaultInputMask ;
10431053 }
1054+
1055+ /**
1056+ * Gets the history for the LineReader
1057+ * @return the history
1058+ */
1059+ public History getHistory () {
1060+ return history ;
1061+ }
10441062}
0 commit comments