Closed Osiris-Team closed 3 years ago
I've tried to make them work on my OSX terminal, but I can't find a way to reliably use the restoreCursorPosition / saveCursorPosition commands. Even using the sequences provided by the terminal using infocmp
seems to fail. So I'm not sure the problem is in jansi...
If you want to do advanced stuff, I suggest you use JLine
's terminal and in particular the Display
class which provides a really and easy way to update the few last lines of the console (which seems to be your case).
@gnodet Oh that's sad :( But thanks for the help anyway. And I'll take a look at JLine.
I've tried to make them work on my OSX terminal, but I can't find a way to reliably use the restoreCursorPosition / saveCursorPosition commands. Even using the sequences provided by the terminal using
infocmp
seems to fail. So I'm not sure the problem is in jansi...
So using \E7
and \E8
instead of \E[s
and \E[u
seems to work better on some terminals, at least in the simple case where the number of printed lines is small, but still fails when scrolling has been toggled. I really suggest JLine's Display
.
@gnodet Are there any examples of how to do this with Jline? I couldn't figure it out...
Try something like
Terminal terminal = TerminalBuilder.terminal();
for (int i = 0; i < 30; i++) {
terminal.writer().println("TEST");
}
Display display = new Display(terminal, false);
for (int i = 0; i < 5; i++) {
display.update(Arrays.asList(
AttributedString.fromAnsi("Line 1 with data " + i),
AttributedString.fromAnsi("Line 2 with data " + i)
), -1);
Thread.sleep(1000);
}
@gnodet My output running your code:
Line 1 with data 0java.lang.ArithmeticException: / by zero
at org.jline.utils.Display.update(Display.java:304)
at org.jline.utils.Display.update(Display.java:103)
Yeah, I haven't tested it. You need to initialize the size on the display with
Size size = terminal.getSize();
display.resize(size.getRows(), size.getColumns());
@gnodet Thanks for the help ;)
@gnodet Hey it's me again! Sry for bothering you again, but I'm not able to make it work for my application :/ The example you provided just works fine as expected though.
The code below is responsible for printing stuff to the console. What I am trying to achieve with it is actually pretty simple: Each line represents one currently running thread. That line contains information related to its progress. But somehow the output is completely buggy.
Easy way to reproduce my error:
Run the "Startup-Windows.cmd" script
/**
* Uses jansi & jline to display the live status of
* all active BetterThreads.
* Runs until there are no more active BetterThreads.
*/
public class BetterThreadDisplayer extends Thread {
private Terminal terminal;
private Display display;
private BetterThreadManager manager;
private String label = "[MyAppName]";
private String threadType = "[PROCESS]";
private LocalDateTime now;
private boolean showWarnings;
private boolean showDetailedWarnings;
private int refreshInterval;
private DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("HH:mm:ss");
private List<BetterWarning> allWarnings = new ArrayList<>();
private byte anim;
/**
* Creates a new ThreadDisplayer with default
* values set.
* To customize these values use the other constructor.
* @param manager
*/
public BetterThreadDisplayer(BetterThreadManager manager) {
this(manager, null);
}
public BetterThreadDisplayer(BetterThreadManager manager, String label) {
this(manager, label, null);
}
public BetterThreadDisplayer(BetterThreadManager manager, String label, String threadType) {
this(manager, label, threadType, null);
}
public BetterThreadDisplayer(BetterThreadManager manager, String label, String threadType,
DateTimeFormatter formatter) {
this(manager, label, threadType, formatter, false);
}
public BetterThreadDisplayer(BetterThreadManager manager, String label, String threadType,
DateTimeFormatter formatter, boolean showWarnings) {
this(manager, label, threadType, formatter, showWarnings, false);
}
public BetterThreadDisplayer(BetterThreadManager manager, String label, String threadType,
DateTimeFormatter formatter, boolean showWarnings, boolean showDetailedWarnings) {
this(manager, label, threadType, formatter, showWarnings, showDetailedWarnings, 250);
}
/**
* Creates a new ThreadDisplayer.
* You can customize very aspect of it by
* passing over the wanted values.
* @param manager
* @param label
* @param threadType
* @param formatter
* @param showWarnings
* @param showDetailedWarnings
* @param refreshInterval
* @throws RuntimeException if there was an error getting the systems terminal.
*/
public BetterThreadDisplayer(BetterThreadManager manager, String label, String threadType, DateTimeFormatter formatter,
boolean showWarnings, boolean showDetailedWarnings, int refreshInterval) {
this.manager = manager;
if (label!=null) this.label = label;
if (threadType!=null) this.threadType = threadType;
if (formatter!=null) this.dateFormatter = formatter;
this.showWarnings = showWarnings;
this.showDetailedWarnings = showDetailedWarnings;
this.refreshInterval = refreshInterval;
// Check if Jansi console was already started
//AnsiConsole.systemInstall();
try{
terminal = TerminalBuilder.terminal();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("There was an error getting the systems terminal!");
}
}
@Override
public void run() {
super.run();
try{
resize();
terminal.writer().println(" ");
while (printAll()){
sleep(refreshInterval);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private void resize(){
display = new Display(terminal, false);
Size size = terminal.getSize(); // Need to initialize the size on the display with
display.resize(size.getRows(), size.getColumns());
}
/**
* Prints the current status of all processes.
* This method should be used with a pause of 250ms.
* Returns false when all processes have finished!
* @return false when all processes have finished!
*/
public boolean printAll(){
// Get current date
now = LocalDateTime.now();
// Fill this list with threads details and update the console after this
List<AttributedString> list = new ArrayList<>();
manager.getAll().forEach(thread -> {
// Display the information for each thread.
// Each thread gets one line.
StringBuilder builder = new StringBuilder();
//Format the output for a single process
//First add the date, label and process info labels
builder.append(ansi()
.bg(WHITE)
.fg(BLACK).a("["+dateFormatter.format(now)+"]")
.fg(CYAN).a(label)
.fg(BLACK).a(threadType)
.reset());
//Add the loading animation
if (thread.isFinished()){
if(thread.isSkipped()){
builder.append(ansi()
.fg(BLUE).a(" [#] ")
.reset());
}
else if (thread.isSuccess()){
builder.append(ansi()
.fg(GREEN).a(" [#] ")
.reset());
}
else{
builder.append(ansi()
.fg(RED).a(" [#] ")
.reset());
}
}
else{
switch (anim) {
case 1:
builder.append(ansi().a(" [*] ")); // \\
break;
case 2:
builder.append(ansi().a(" [|] "));
break;
case 3:
builder.append(ansi().a(" [+] "));
break;
default:
anim = 0;
builder.append(ansi().a(" [-] "));
}
}
// Add the actual process details and finish the line
final String name = thread.getName();
final long now = thread.getNow();
final long max = thread.getMax();
final byte percent = thread.getPercent();
final String status = thread.getStatus();
if (now > 0){
if (thread.isSkipped())
builder.append(ansi()
.a("> ["+name+"] "+status));
else
builder.append(ansi()
.a("> ["+name+"]["+percent+"%] "+status));
}
else{
builder.append(ansi()
.a("> ["+name+"] "+status));
}
builder.append(ansi().reset());
// Add this message to the list
list.add(new AttributedString(builder.toString()));
});
// This must be done outside the for loop otherwise the animation wont work
anim++;
if (manager.getAll().size()==0){
list.add(new AttributedString("No threads! Waiting..."));
}
else{
// This means we finished and should stop looping
// We print the last warnings message and stop.
if (manager.isFinished()){
display.update(list, -1); // Update one last time
this.allWarnings = manager.getAllWarnings();
formatWarnings();
return false;
}
}
display.update(list, -1);
return true;
}
private long getPercentage(long now, long max){
return (now*100/max);
}
/**
* This is will be shown when all processes finished.
*/
private void formatWarnings(){
Ansi ansiDate = ansi()
.bg(WHITE)
.fg(BLACK).a("["+dateFormatter.format(now)+"]")
.fg(CYAN).a(label)
.fg(BLACK).a("[SUMMARY]")
.reset();
if (allWarnings.isEmpty()){
System.out.println(ansi()
.a(ansiDate)
.fg(GREEN)
.a(" Executed all tasks successfully!")
.reset());
}
else if (showWarnings) {
System.out.println(ansi()
.a(ansiDate)
.fg(YELLOW)
.a(" There are " + allWarnings.size() + " warnings:")
.reset());
if (showDetailedWarnings) {
BetterWarning betterWarning;
for (int i = 0; i < allWarnings.size(); i++) {
betterWarning = allWarnings.get(i);
StringBuilder builder = new StringBuilder();
builder.append(ansiDate);
builder.append(ansi()
.fg(YELLOW).a("[WARNING-" + i + "][" + betterWarning.getThread().getName() + "][Message: " + betterWarning.getException().getMessage() +
"][Cause: " + betterWarning.getException().getCause() +
"][Extra: " + betterWarning.getExtraInfo() +
"][Trace: " + Arrays.toString(betterWarning.getException().getStackTrace())).reset());
System.out.println(builder.toString());
}
}
else {
BetterWarning betterWarning;
for (int i = 0; i < allWarnings.size(); i++) {
betterWarning = allWarnings.get(i);
StringBuilder builder = new StringBuilder();
builder.append(ansiDate);
builder.append(ansi()
.fg(YELLOW).a("[WARNING-" + i + "][" + betterWarning.getThread().getName() + "][Message: " + betterWarning.getException().getMessage() + "]").reset());
System.out.println(builder.toString());
}
}
}
else{
System.out.println(ansi()
.fg(YELLOW).a(" There are "+ allWarnings.size()+" warnings! Enable 'show-warnings' to view them, or check your log for further details!")
.reset());
}
}
public BetterThreadManager getManager() {
return manager;
}
public void setManager(BetterThreadManager manager) {
this.manager = manager;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
public String getThreadType() {
return threadType;
}
public void setThreadType(String threadType) {
this.threadType = threadType;
}
public boolean isShowWarnings() {
return showWarnings;
}
public void setShowWarnings(boolean showWarnings) {
this.showWarnings = showWarnings;
}
public boolean isShowDetailedWarnings() {
return showDetailedWarnings;
}
public void setShowDetailedWarnings(boolean showDetailedWarnings) {
this.showDetailedWarnings = showDetailedWarnings;
}
public int getRefreshInterval() {
return refreshInterval;
}
public void setRefreshInterval(int refreshInterval) {
this.refreshInterval = refreshInterval;
}
public DateTimeFormatter getDateFormatter() {
return dateFormatter;
}
public void setDateFormatter(DateTimeFormatter dateFormatter) {
this.dateFormatter = dateFormatter;
}
public List<BetterWarning> getAllWarnings() {
return allWarnings;
}
public void setAllWarnings(List<BetterWarning> allWarnings) {
this.allWarnings = allWarnings;
}
}
@Osiris-Team the problem is that you're not building the strings correctly. In order to build an AttributedString
, you need to use AttributedString.fromAnsi(ansiString)
instead of new AttributedString(ansiString)
. Ideally, you'd use an AttributedStringBuilder
instead of using a StringBuilder
and writing ansi codes directly, but that's not a big problem.
So try with the following patch:
diff --git a/src/main/java/com/osiris/betterthread/BetterThreadDisplayer.java b/src/main/java/com/osiris/betterthread/BetterThreadDisplayer.java
index 2ee493f..4144b6e 100644
--- a/src/main/java/com/osiris/betterthread/BetterThreadDisplayer.java
+++ b/src/main/java/com/osiris/betterthread/BetterThreadDisplayer.java
@@ -220,7 +220,7 @@ public class BetterThreadDisplayer extends Thread {
builder.append(ansi().reset());
// Add this message to the list
- list.add(new AttributedString(builder.toString()));
+ list.add(AttributedString.fromAnsi(builder.toString()));
});
// This must be done outside the for loop otherwise the animation wont work
(END)
@gnodet Wow that really did it! Never thought that that would be the issue. Thank you so much!
@gnodet Ran into another issue :/ For the first run, everything works fine. But the second run is malformatted like below.
[12:06:19][MyAppName][PROCESS] [#] > [BetterThread][100%] Initialising...
[12:06:19][MyAppName][PROCESS] [#] > [BetterThread][100%] Initialising...
[12:06:19][MyAppName][PROCESS] [#] > [BetterThread][100%] Initialising...
[12:06:19][MyAppName][SUMMARY] Executed all tasks successfully!
SIOJASDIOASD
SIOJASDIOASD
20][MyAp5][MyAppName][PROCESS] [#] > [BetterThread][100%] Initialising...
SIOJASD25][MyAppName][PROCESS] [#] > [BetterThread][100%] Initialising...
25][MyAppName][PROCESS] [#] > [BetterThread][100%] Initialising...
[12:06:25][MyAppName][SUMMARY] Executed all tasks successfully!
Also "SIOJASDIOASD" has been printed 4 times, but its last 2 lines get somehow overwritten.
@gnodet Oh, and there is another thing. It also breaks if another Thread writes stuff to the console, while it's displaying the progresses.
The Display
class has to be the only one writing to the console in order to have a correct display. If you have threads writing to the console directly using System.out, you need to write a custom System.out stream that can grab the output and hand it to the Terminal
in an orderly way.
@gnodet Ok did as you said: https://github.com/Osiris-Team/Better-Thread/commit/9e110ce3830e021ba3b8d5f7a3f65e7ae8ebecc3 Added two new classes: MyDisplay and MyLine MyDisplay extends Jlines Display class but provides functionality for adding lines and updating lines more precisely. MyLine represents one line in the terminal/console.
Now i can do this:
// This will update/replace the line at position 0.
// Adds the line to the console, if position 0 doesn't exist yet.
MY_DISPLAY.updateLines(new MyLine("hello1", 0));
// This adds the line to the console.
MY_DISPLAY.addLines(new MyLine("hello1"));
// This does the same:
MY_DISPLAY.addLines("hello1"));
The only thing that I'm not completly happy with is the way Im doing this. Because the way Im achieving this is by calling update() in MyDisplay.updateLines(MyLine... lines), which causes all old lines to get updated to I guess:
this.update(convertToAttributedStringList(allLines), -1);
Maybe you got a better way of doing this?
*Edit: And also that means that I would have to keep every single line in memory :/
The
Display
class has to be the only one writing to the console in order to have a correct display. If you have threads writing to the console directly using System.out, you need to write a custom System.out stream that can grab the output and hand it to theTerminal
in an orderly way.
I thought instead of using System.out, I would now use:
MY_DISPLAY.addLines(new MyLine("hello1"));
Yes, this is not the correct way. You need to use
// erase the lines
display.update(Collections.emptyList(), 0);
// ... print lines to the terminal
terminal.writer().println(str);
// usual print
display.update(lines, -1);
So basically the display knows of the lines printed by the terminal.writer()? But not of the lines printed by System.out
So basically the display knows of the lines printed by the terminal.writer()? But not of the lines printed by System.out
Not really. The first line erases what the lines previously written to the terminal by Display
. You can then use terminal.writer()
(or System.out
if you really want to, but those could be different if the terminal is not the system terminal). After that, we ask the Display
to draw again the lines. This needs to be synced with the normal display loop somehow.
@gnodet I quite dont understand what -1
in display.update(lines, -1)
means?
*Edit: Its says targetCursorPostion
but which position exactly? Horizontal or vertical?
Oh and this happens when running this code (I have absolutely no clue why): *Note that when we loop only 30 times it works fine.
for (int i = 0; i < 100; i++) {
MY_DISPLAY.add("TEST");
}
Thread.sleep(1000);
MY_DISPLAY.updateLine(0, "0");
Thread.sleep(1000);
MY_DISPLAY.updateLine(10, "10");
Thread.sleep(1000);
MY_DISPLAY.updateLine(25, "25");
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
0
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
TEST
10
25
D:\Coding\JAVA\Better-Thread>pause
@gnodet I quite dont understand what
-1
indisplay.update(lines, -1)
means? *Edit: Its saystargetCursorPostion
but which position exactly? Horizontal or vertical?
The position is expressed as a number of characters, so if your terminal is 80 columns, 79 will be the last column on the first line and 80 the first column on the second line. -1 means the last written character.
As for your last question, the Display
class is not supposed to be given a number of lines bigger than the terminal's height. You need to trim down the number of lines before handing them to the Display
class.
As for your last question, the
Display
class is not supposed to be given a number of lines bigger than the terminal's height. You need to trim down the number of lines before handing them to theDisplay
class.
Ok got it chef! Thanks!
@gnodet I quite dont understand what
-1
indisplay.update(lines, -1)
means? *Edit: Its saystargetCursorPostion
but which position exactly? Horizontal or vertical?The position is expressed as a number of characters, so if your terminal is 80 columns, 79 will be the last column on the first line and 80 the first column on the second line. -1 means the last written character.
Ok but if -1 means the last written char, then why does this happen:
display.update(lines, -1);
display.update(lines, -1); // This method starts writting at the same old -1 position instead of the new last written char
// and thus replaces/updates these lines
*Edit: This means that the "real" -1 position doesn't get updated through the update(lines, -1) method? **Edit: In that case how would I update that position?
I think you misunderstood the purpose of the Display
class. That class is used to display one or more lines (there's also the fullscreen mode), and update those managed lines with whatever input you give it.
The update(lines, pos)
will update the display so that lines written by a previous call to update
will be erased and the new ones will appear. The lines to display are given by the first parameter and the cursor position after the update is given by the second parameter. The original use case is the raw update of the LineReader
: the prompt + editing line (or multiple lines) are completely handled by Display
and the cursor position (you can move using arrow keys for example) is handled by the second parameter.
Yeah that makes much more sense. Thanks!
So basically the display knows of the lines printed by the terminal.writer()? But not of the lines printed by System.out
Not really. The first line erases what the lines previously written to the terminal by
Display
. You can then useterminal.writer()
(orSystem.out
if you really want to, but those could be different if the terminal is not the system terminal). After that, we ask theDisplay
to draw again the lines. This needs to be synced with the normal display loop somehow.
Found a workaround for that btw:
PrintStream old = System.out;
MyPrintStream myPrintStream = new MyPrintStream(old);
System.setOut(myPrintStream);
The idea is to "cache" all the stuff that was written to the custom PrintStream, while doing the "updating-loop", instead of printing/writing it. When the "updating-loop" finishes we reset the PrintStream and display the stuff that was written to it.
Note: This took me several hours to figure out, so please bear 1-2 minutes to read this, thanks! Context: Trying to update/replace multiple lines with new data. Bug: When then console is already filled with stuff, eraseLine()/restoreCursorPostion() wont work! Confirmed broken on versions: Jansi 2.2.0 (Windows and Linux) and Jansi 1.18 (Linux) Confirmed working on versions: Jansi 1.18 (Windows) Java version: 1.8 Code: