admin管理员组

文章数量:1302376

I'm trying to use the Jline3 library to make my UI more responsive, mainly I'd like to refresh program output in fixed intervals, with the input line at the bottom of the terminal. Here's my current implementation of class responsible for reading input and withing the output:

CliView

package .mga44.timer.adapter.view.cli;

import lombok.SneakyThrows;
import .jline.reader.LineReader;
import .jline.reader.LineReaderBuilder;
import .jline.reader.impl.history.DefaultHistory;
import .jline.terminal.Size;
import .jline.terminal.Terminal;
import .jline.terminal.TerminalBuilder;
import .jline.utils.AttributedString;
import .jline.utils.Display;
import .mga44.timer.adapter.view.View;

import java.util.Arrays;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;

public class CliView implements View {
    private final Display display;
    private final Terminal terminal;
    private final LineReader reader;
    private volatile String latestOutput = "Welcome!";
    private int lastLineCount = 0;

    private final int width = 100;
    private final int height = 10;
    private final Size displaySize = new Size(width, height);

    @SneakyThrows
    public CliView() {
        this.terminal = TerminalBuilder.builder()
                .system(true)
                 .build();

        this.reader = LineReaderBuilder.builder()
                .terminal(terminal)
                .history(new DefaultHistory())
                .option(LineReader.Option.ERASE_LINE_ON_FINISH, true)
                .build();

        this.display = new Display(terminal, false);
        display.resize(height, width);
        display.clear();

        startAutoRefresh();
    }

    @Override
    public void write(String content) {
        latestOutput = content;
        refreshDisplay();
    }

    @Override
    public String read() {
        return reader.readLine("> ");
    }

    private void startAutoRefresh() {
        Timer t = new Timer(true);
        t.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                refreshDisplay();
            }
        }, 0, 1000);
    }

    private void refreshDisplay() {
        List<AttributedString> lines = Arrays.stream(latestOutput.split("\n"))
                .map(AttributedString::new)
                .toList();

        int newLineCount = lines.size();
        if (newLineCount < lastLineCount) {
            for (int i = 0; i < (lastLineCount - newLineCount); i++) {
                lines.add(new AttributedString(" "));
            }
        }
        lastLineCount = newLineCount;

        display.resize(height, width);
        display.update(lines, displaySize.cursorPos(height, 0));
        display.clear();
        terminal.flush();
    }
}

Also, I'm using below versions of relevant dependencies:

build.gradle

    implementation '.jline:jline:3.21.0'
    implementation 'net.java.dev.jna:jna:5.12.1'

The issue I'm struggling with is that currently I have to manually press enter to refresh the application output. I suspect that somewhere in the implementation of LineReader#readLine the redrawing of entire terminal take place, but I can't pinpoint it. Before I tried concurrency I've tried calling Display#clear and Terminal#flush but with no effect.

I'm trying to use the Jline3 library to make my UI more responsive, mainly I'd like to refresh program output in fixed intervals, with the input line at the bottom of the terminal. Here's my current implementation of class responsible for reading input and withing the output:

CliView

package .mga44.timer.adapter.view.cli;

import lombok.SneakyThrows;
import .jline.reader.LineReader;
import .jline.reader.LineReaderBuilder;
import .jline.reader.impl.history.DefaultHistory;
import .jline.terminal.Size;
import .jline.terminal.Terminal;
import .jline.terminal.TerminalBuilder;
import .jline.utils.AttributedString;
import .jline.utils.Display;
import .mga44.timer.adapter.view.View;

import java.util.Arrays;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;

public class CliView implements View {
    private final Display display;
    private final Terminal terminal;
    private final LineReader reader;
    private volatile String latestOutput = "Welcome!";
    private int lastLineCount = 0;

    private final int width = 100;
    private final int height = 10;
    private final Size displaySize = new Size(width, height);

    @SneakyThrows
    public CliView() {
        this.terminal = TerminalBuilder.builder()
                .system(true)
                 .build();

        this.reader = LineReaderBuilder.builder()
                .terminal(terminal)
                .history(new DefaultHistory())
                .option(LineReader.Option.ERASE_LINE_ON_FINISH, true)
                .build();

        this.display = new Display(terminal, false);
        display.resize(height, width);
        display.clear();

        startAutoRefresh();
    }

    @Override
    public void write(String content) {
        latestOutput = content;
        refreshDisplay();
    }

    @Override
    public String read() {
        return reader.readLine("> ");
    }

    private void startAutoRefresh() {
        Timer t = new Timer(true);
        t.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                refreshDisplay();
            }
        }, 0, 1000);
    }

    private void refreshDisplay() {
        List<AttributedString> lines = Arrays.stream(latestOutput.split("\n"))
                .map(AttributedString::new)
                .toList();

        int newLineCount = lines.size();
        if (newLineCount < lastLineCount) {
            for (int i = 0; i < (lastLineCount - newLineCount); i++) {
                lines.add(new AttributedString(" "));
            }
        }
        lastLineCount = newLineCount;

        display.resize(height, width);
        display.update(lines, displaySize.cursorPos(height, 0));
        display.clear();
        terminal.flush();
    }
}

Also, I'm using below versions of relevant dependencies:

build.gradle

    implementation '.jline:jline:3.21.0'
    implementation 'net.java.dev.jna:jna:5.12.1'

The issue I'm struggling with is that currently I have to manually press enter to refresh the application output. I suspect that somewhere in the implementation of LineReader#readLine the redrawing of entire terminal take place, but I can't pinpoint it. Before I tried concurrency I've tried calling Display#clear and Terminal#flush but with no effect.

Share Improve this question asked Feb 10 at 20:02 mga44mga44 113 bronze badges
Add a comment  | 

1 Answer 1

Reset to default 0

Synchronizing printing between the reader and another thread is a common problem. The best way to handle that is to use LineReader#printAbove() method which will:

  • pause the reader
  • erase the current line
  • print the given line
  • redraw the edited line
  • resume the reader

The LineReader is tries to optimise the display usage and thus only prints what's needed to update the display, which means it needs to know exactly what the display is, which can only happen if he is the only one controlling it.

So do not create a Display manually, just use printAbove.

本文标签: jlineImplementing automatic ui update for java jline3 CLI appStack Overflow