Skip to content

Latest commit

 

History

History
190 lines (148 loc) · 9.69 KB

Tutorial03.md

File metadata and controls

190 lines (148 loc) · 9.69 KB

Tutorial 3

In the third tutorial, we will look at using the next layer available in Lanterna, which is built on top of the Terminal interface you saw in tutorial 1 and 2.

A Screen works similar to double-buffered video memory, it has two surfaces than can be directly addressed and modified and by calling a special method the content of the back-buffer is move to the front. Instead of pixels though, a Screen holds two text character surfaces (front and back) which corresponds to each "cell" in the terminal. You can freely modify the back "buffer" and you can read from the front "buffer", calling the refreshScreen() method to copy content from the back buffer to the front buffer, which will make Lanterna also apply the changes so that the user can see them in the terminal.

    DefaultTerminalFactory defaultTerminalFactory = new DefaultTerminalFactory();
    Screen screen = null;
    try {

You can use the DefaultTerminalFactory to create a Screen, this will generally give you the TerminalScreen implementation that is probably what you want to use. Please see VirtualScreen for more details on a separate implementation that allows you to create a terminal surface that is bigger than the physical size of the terminal emulator the software is running in. Just to demonstrate that a Screen sits on top of a Terminal, we are going to create one manually instead of using DefaultTerminalFactory.

        Terminal terminal = defaultTerminalFactory.createTerminal();
        screen = new TerminalScreen(terminal);

Screens will only work in private mode and while you can call methods to mutate its state, before you can make any of these changes visible, you'll need to call startScreen() which will prepare and setup the terminal.

        screen.startScreen();

Let's turn off the cursor for this tutorial

        screen.setCursorPosition(null);

Now let's draw some random content in the screen buffer

        Random random = new Random();
        TerminalSize terminalSize = screen.getTerminalSize();
        for(int column = 0; column < terminalSize.getColumns(); column++) {
            for(int row = 0; row < terminalSize.getRows(); row++) {
                screen.setCharacter(column, row, new TextCharacter(
                        ' ',
                        TextColor.ANSI.DEFAULT,
                        // This will pick a random background color
                        TextColor.ANSI.values()[random.nextInt(TextColor.ANSI.values().length)]));
            }
        }

So at this point, we've only modified the back buffer in the screen, nothing is visible yet. In order to move the content from the back buffer to the front buffer and refresh the screen, we need to call refresh()

        screen.refresh();

Now there should be completely random colored cells in the terminal (assuming your terminal (emulator) supports colors). Let's look at it for two seconds or until the user presses a key.

        long startTime = System.currentTimeMillis();
        while(System.currentTimeMillis() - startTime < 2000) {
            // The call to pollInput() is not blocking, unlike readInput()
            if(screen.pollInput() != null) {
                break;
            }
            try {
                Thread.sleep(1);
            }
            catch(InterruptedException ignore) {
                break;
            }
        }

Ok, now we loop and keep modifying the screen until the user exits by pressing escape on the keyboard or the input stream is closed. When using the Swing/AWT bundled emulator, if the user closes the window this will result in an EOF KeyStroke.

        while(true) {
            KeyStroke keyStroke = screen.pollInput();
            if(keyStroke != null && (keyStroke.getKeyType() == KeyType.Escape || keyStroke.getKeyType() == KeyType.EOF)) {
                break;
            }

Screens will automatically listen and record size changes, but you have to let the Screen know when is a good time to update its internal buffers. Usually you should do this at the start of your "drawing" loop, if you have one. This ensures that the dimensions of the buffers stays constant and doesn't change while you are drawing content. The method doResizeIfNecessary() will check if the terminal has been resized since last time it was called (or since the screen was created if this is the first time calling) and update the buffer dimensions accordingly. It returns null if the terminal has not changed size since last time.

            TerminalSize newSize = screen.doResizeIfNecessary();
            if(newSize != null) {
                terminalSize = newSize;
            }

            // Increase this to increase speed
            final int charactersToModifyPerLoop = 1;
            for(int i = 0; i < charactersToModifyPerLoop; i++) {

We pick a random location

                    TerminalPosition cellToModify = new TerminalPosition(
                            random.nextInt(terminalSize.getColumns()),
                            random.nextInt(terminalSize.getRows()));

Pick a random background color again

                    TextColor.ANSI color = TextColor.ANSI.values()[random.nextInt(TextColor.ANSI.values().length)];

Update it in the back buffer, notice that just like TerminalPosition and TerminalSize, TextCharacter objects are immutable so the withBackgroundColor(..) call below returns a copy with the background color modified.

                    TextCharacter characterInBackBuffer = screen.getBackCharacter(cellToModify);
                    characterInBackBuffer = characterInBackBuffer.withBackgroundColor(color);
                    characterInBackBuffer = characterInBackBuffer.withCharacter(' ');   // Because of the label box further down, if it shrinks
                    screen.setCharacter(cellToModify, characterInBackBuffer);
            }

Just like with Terminal, it's probably easier to draw using TextGraphics. Let's do that to put a little box with information on the size of the terminal window

            String sizeLabel = "Terminal Size: " + terminalSize;
            TerminalPosition labelBoxTopLeft = new TerminalPosition(1, 1);
            TerminalSize labelBoxSize = new TerminalSize(sizeLabel.length() + 2, 3);
            TerminalPosition labelBoxTopRightCorner = labelBoxTopLeft.withRelativeColumn(labelBoxSize.getColumns() - 1);
            TextGraphics textGraphics = screen.newTextGraphics();
            //This isn't really needed as we are overwriting everything below anyway, but just for demonstrative purpose
            textGraphics.fillRectangle(labelBoxTopLeft, labelBoxSize, ' ');

Draw horizontal lines, first upper then lower

            textGraphics.drawLine(
                    labelBoxTopLeft.withRelativeColumn(1),
                    labelBoxTopLeft.withRelativeColumn(labelBoxSize.getColumns() - 2),
                    Symbols.DOUBLE_LINE_HORIZONTAL);
            textGraphics.drawLine(
                    labelBoxTopLeft.withRelativeRow(2).withRelativeColumn(1),
                    labelBoxTopLeft.withRelativeRow(2).withRelativeColumn(labelBoxSize.getColumns() - 2),
                    Symbols.DOUBLE_LINE_HORIZONTAL);

Manually do the edges and (since it's only one) the vertical lines, first on the left then on the right

            textGraphics.setCharacter(labelBoxTopLeft, Symbols.DOUBLE_LINE_TOP_LEFT_CORNER);
            textGraphics.setCharacter(labelBoxTopLeft.withRelativeRow(1), Symbols.DOUBLE_LINE_VERTICAL);
            textGraphics.setCharacter(labelBoxTopLeft.withRelativeRow(2), Symbols.DOUBLE_LINE_BOTTOM_LEFT_CORNER);
            textGraphics.setCharacter(labelBoxTopRightCorner, Symbols.DOUBLE_LINE_TOP_RIGHT_CORNER);
            textGraphics.setCharacter(labelBoxTopRightCorner.withRelativeRow(1), Symbols.DOUBLE_LINE_VERTICAL);
            textGraphics.setCharacter(labelBoxTopRightCorner.withRelativeRow(2), Symbols.DOUBLE_LINE_BOTTOM_RIGHT_CORNER);

Finally put the text inside the box

            textGraphics.putString(labelBoxTopLeft.withRelative(1, 1), sizeLabel);

Ok, we are done and can display the change. Let's also be nice and allow the OS to schedule other threads so we don't clog up the core completely.

            screen.refresh();
            Thread.yield();

Every time we call refresh, the whole terminal is NOT re-drawn. Instead, the Screen will compare the back and front buffers and figure out only the parts that have changed and only update those. This is why in the code drawing the size information box above, we write it out every time we loop but it's actually not sent to the terminal except for the first time because the Screen knows the content is already there and has not changed. Because of this, you should never use the underlying Terminal object when working with a Screen because that will cause modifications that the Screen won't know about.

        }
    }
    catch(IOException e) {
        e.printStackTrace();
    }
    finally {
        if(screen != null) {
            try {

The close() call here will restore the terminal by exiting from private mode which was done in the call to startScreen()

                screen.close();
            }
            catch(IOException e) {
                e.printStackTrace();
            }
        }
    }

The full code to this tutorial is available in the test section of the source code

Move on to tutorial 4