Skip to content

Commit

Permalink
Provide an api to print output asynchronously above the prompt, fixes #…
Browse files Browse the repository at this point in the history
…292

Incorporate Minecrell's fixes, thx !
  • Loading branch information
gnodet committed Jul 17, 2018
1 parent cd29a53 commit d5fc7e8
Show file tree
Hide file tree
Showing 2 changed files with 157 additions and 82 deletions.
36 changes: 36 additions & 0 deletions reader/src/main/java/org/jline/reader/LineReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,19 @@
import org.jline.keymap.KeyMap;
import org.jline.terminal.MouseEvent;
import org.jline.terminal.Terminal;
import org.jline.utils.AttributedString;

/** Read lines from the console, with input editing.
*
* <h3>Thread safety</h3>
* The <code>LineReader</code> implementations are not thread safe,
* thus you should not attempt to use a single reader in several threads.
* Any attempt to call one of the <code>readLine</code> call while one is
* already executing in a different thread will immediately result in an
* <code>IllegalStateException</code> being thrown. Other calls may lead to
* unknown behaviors. There is one exception though: users are allowed to call
* {@link #printAbove(String)} or {@link #printAbove(AttributedString)} at
* any time to allow text to be printed above the current prompt.
*
* <h3>Prompt strings</h3>
* It is traditional for an interactive console-based program
Expand Down Expand Up @@ -537,6 +548,31 @@ enum RegionType {
*/
String readLine(String prompt, String rightPrompt, MaskingCallback maskingCallback, String buffer) throws UserInterruptException, EndOfFileException;

/**
* Prints a line above the prompt and redraw everything.
* If the LineReader is not actually reading a line, the string will simply be printed to the terminal.
*
* @see #printAbove(AttributedString)
* @param str the string to print
*/
void printAbove(String str);

/**
* Prints a string before the prompt and redraw everything.
* If the LineReader is not actually reading a line, the string will simply be printed to the terminal.
*
* @see #printAbove(String)
* @param str the string to print
*/
void printAbove(AttributedString str);

/**
* Check if a thread is currently in a <code>readLine()</code> call.
*
* @return <code>true</code> if there is an ongoing <code>readLine()</code> call.
*/
boolean isReading();

//
// Chainable setters
//
Expand Down
203 changes: 121 additions & 82 deletions reader/src/main/java/org/jline/reader/impl/LineReaderImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import java.time.Instant;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
Expand Down Expand Up @@ -213,6 +214,7 @@ protected enum BellType {
* Current internal state of the line reader
*/
protected State state = State.DONE;
protected final AtomicBoolean startedReading = new AtomicBoolean();
protected boolean reading;

protected Supplier<AttributedString> post;
Expand Down Expand Up @@ -455,7 +457,11 @@ public String readLine(String prompt, String rightPrompt, MaskingCallback maskin
// prompt may be null
// maskingCallback may be null
// buffer may be null


if (!startedReading.compareAndSet(false, true)) {
throw new IllegalStateException();
}

Thread readLineThread = Thread.currentThread();
SignalHandler previousIntrHandler = null;
SignalHandler previousWinchHandler = null;
Expand All @@ -464,10 +470,6 @@ public String readLine(String prompt, String rightPrompt, MaskingCallback maskin
boolean dumb = Terminal.TYPE_DUMB.equals(terminal.getType())
|| Terminal.TYPE_DUMB_COLOR.equals(terminal.getType());
try {
if (reading) {
throw new IllegalStateException();
}
reading = true;

this.maskingCallback = maskingCallback;

Expand Down Expand Up @@ -501,47 +503,51 @@ public String readLine(String prompt, String rightPrompt, MaskingCallback maskin
history.attach(this);
}

previousIntrHandler = terminal.handle(Signal.INT, signal -> readLineThread.interrupt());
previousWinchHandler = terminal.handle(Signal.WINCH, this::handleSignal);
previousContHandler = terminal.handle(Signal.CONT, this::handleSignal);
originalAttributes = terminal.enterRawMode();
synchronized (this) {
this.reading = true;

// Cache terminal size for the duration of the call to readLine()
// It will eventually be updated with WINCH signals
size.copy(terminal.getSize());
previousIntrHandler = terminal.handle(Signal.INT, signal -> readLineThread.interrupt());
previousWinchHandler = terminal.handle(Signal.WINCH, this::handleSignal);
previousContHandler = terminal.handle(Signal.CONT, this::handleSignal);
originalAttributes = terminal.enterRawMode();

display = new Display(terminal, false);
if (size.getRows() == 0 || size.getColumns() == 0) {
display.resize(1, Integer.MAX_VALUE);
} else {
display.resize(size.getRows(), size.getColumns());
}
if (isSet(Option.DELAY_LINE_WRAP))
display.setDelayLineWrap(true);

// Move into application mode
if (!dumb) {
terminal.puts(Capability.keypad_xmit);
if (isSet(Option.AUTO_FRESH_LINE))
callWidget(FRESH_LINE);
if (isSet(Option.MOUSE))
terminal.trackMouse(Terminal.MouseTracking.Normal);
if (isSet(Option.BRACKETED_PASTE))
terminal.writer().write(BRACKETED_PASTE_ON);
} else {
// For dumb terminals, we need to make sure that CR are ignored
Attributes attr = new Attributes(originalAttributes);
attr.setInputFlag(Attributes.InputFlag.IGNCR, true);
terminal.setAttributes(attr);
}
// Cache terminal size for the duration of the call to readLine()
// It will eventually be updated with WINCH signals
size.copy(terminal.getSize());

callWidget(CALLBACK_INIT);
display = new Display(terminal, false);
if (size.getRows() == 0 || size.getColumns() == 0) {
display.resize(1, Integer.MAX_VALUE);
} else {
display.resize(size.getRows(), size.getColumns());
}
if (isSet(Option.DELAY_LINE_WRAP))
display.setDelayLineWrap(true);

undo.newState(buf.copy());
// Move into application mode
if (!dumb) {
terminal.puts(Capability.keypad_xmit);
if (isSet(Option.AUTO_FRESH_LINE))
callWidget(FRESH_LINE);
if (isSet(Option.MOUSE))
terminal.trackMouse(Terminal.MouseTracking.Normal);
if (isSet(Option.BRACKETED_PASTE))
terminal.writer().write(BRACKETED_PASTE_ON);
} else {
// For dumb terminals, we need to make sure that CR are ignored
Attributes attr = new Attributes(originalAttributes);
attr.setInputFlag(Attributes.InputFlag.IGNCR, true);
terminal.setAttributes(attr);
}

// Draw initial prompt
redrawLine();
redisplay();
callWidget(CALLBACK_INIT);

undo.newState(buf.copy());

// Draw initial prompt
redrawLine();
redisplay();
}

while (true) {

Expand Down Expand Up @@ -572,36 +578,38 @@ public String readLine(String prompt, String rightPrompt, MaskingCallback maskin
regionActive = RegionType.NONE;
}

// Get executable widget
Buffer copy = buf.copy();
Widget w = getWidget(o);
if (!w.apply()) {
beep();
}
if (!isUndo && !copy.toString().equals(buf.toString())) {
undo.newState(buf.copy());
}
synchronized (this) {
// Get executable widget
Buffer copy = buf.copy();
Widget w = getWidget(o);
if (!w.apply()) {
beep();
}
if (!isUndo && !copy.toString().equals(buf.toString())) {
undo.newState(buf.copy());
}

switch (state) {
case DONE:
return finishBuffer();
case EOF:
throw new EndOfFileException();
case INTERRUPT:
throw new UserInterruptException(buf.toString());
}
switch (state) {
case DONE:
return finishBuffer();
case EOF:
throw new EndOfFileException();
case INTERRUPT:
throw new UserInterruptException(buf.toString());
}

if (!isArgDigit) {
/*
* If the operation performed wasn't a vi argument
* digit, then clear out the current repeatCount;
*/
repeatCount = 0;
mult = 1;
}
if (!isArgDigit) {
/*
* If the operation performed wasn't a vi argument
* digit, then clear out the current repeatCount;
*/
repeatCount = 0;
mult = 1;
}

if (!dumb) {
redisplay();
if (!dumb) {
redisplay();
}
}
}
} catch (IOError e) {
Expand All @@ -612,21 +620,52 @@ public String readLine(String prompt, String rightPrompt, MaskingCallback maskin
}
}
finally {
cleanup();
reading = false;
if (originalAttributes != null) {
terminal.setAttributes(originalAttributes);
}
if (previousIntrHandler != null) {
terminal.handle(Signal.INT, previousIntrHandler);
}
if (previousWinchHandler != null) {
terminal.handle(Signal.WINCH, previousWinchHandler);
}
if (previousContHandler != null) {
terminal.handle(Signal.CONT, previousContHandler);
synchronized (this) {
this.reading = false;

cleanup();
if (originalAttributes != null) {
terminal.setAttributes(originalAttributes);
}
if (previousIntrHandler != null) {
terminal.handle(Signal.INT, previousIntrHandler);
}
if (previousWinchHandler != null) {
terminal.handle(Signal.WINCH, previousWinchHandler);
}
if (previousContHandler != null) {
terminal.handle(Signal.CONT, previousContHandler);
}
}
startedReading.set(false);
}
}

@Override
public synchronized void printAbove(String str) {
boolean reading = this.reading;
if (reading) {
display.update(Collections.emptyList(), 0);
}
if (str.endsWith("\n")) {
terminal.writer().print(str);
} else {
terminal.writer().println(str);
}
if (reading) {
redisplay(false);
}
terminal.flush();
}

@Override
public void printAbove(AttributedString str) {
printAbove(str.toAnsi(terminal));
}

@Override
public synchronized boolean isReading() {
return reading;
}

/* Make sure we position the cursor on column 0 */
Expand Down Expand Up @@ -666,7 +705,7 @@ protected boolean freshLine() {
}

@Override
public void callWidget(String name) {
public synchronized void callWidget(String name) {
if (!reading) {
throw new IllegalStateException("Widgets can only be called during a `readLine` call");
}
Expand Down

0 comments on commit d5fc7e8

Please sign in to comment.