All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.morimekta.terminal.Terminal Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2016, Stein Eldar Johnsen
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package net.morimekta.terminal;

import net.morimekta.io.tty.TTY;
import net.morimekta.io.tty.TTYMode;
import net.morimekta.io.tty.TTYModeSwitcher;
import net.morimekta.strings.chr.Char;
import net.morimekta.strings.chr.CharReader;
import net.morimekta.terminal.input.InputConfirmation;
import net.morimekta.terminal.input.InputLine;

import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;

/**
 * Terminal interface. It sets proper TTY mode and reads complex characters from the input, and writes lines dependent
 * on terminal mode.
 */
public class Terminal
        extends CharReader
        implements Closeable, LinePrinter {
    /**
     * Construct a default RAW terminal.
     *
     * @throws IOException If unable to set TTY mode.
     */
    public Terminal() throws IOException {
        this(new TTY());
    }

    /**
     * Construct a default RAW terminal.
     *
     * @param tty The terminal device.
     * @throws IOException If unable to set TTY mode.
     */
    public Terminal(TTY tty) throws IOException {
        this(tty, TTYMode.RAW, null);
    }

    /**
     * Construct a terminal with given mode.
     *
     * @param tty  The terminal device.
     * @param mode The terminal mode.
     * @throws IOException If unable to set TTY mode.
     */
    public Terminal(TTY tty, TTYMode mode) throws IOException {
        this(tty, mode, null);
    }

    /**
     * Construct a terminal with a custom line printer.
     *
     * @param tty The terminal device.
     * @param lp  The line printer.
     * @throws IOException If unable to set TTY mode.
     */
    public Terminal(TTY tty, LinePrinter lp) throws IOException {
        this(tty, TTYMode.RAW, lp);
    }

    /**
     * Construct a terminal with a terminal mode and custom line printer.
     *
     * @param tty  The terminal device.
     * @param mode The terminal mode.
     * @param lp   The line printer.
     * @throws IOException If unable to set TTY mode.
     */
    public Terminal(TTY tty, TTYMode mode, LinePrinter lp) throws IOException {
        this(tty, System.in, System.out, lp, new TTYModeSwitcher(tty, mode));
    }

    /**
     * Constructor visible for testing.
     *
     * @param tty      The terminal device.
     * @param in       The input stream.
     * @param out      The output stream.
     * @param lp       The line printer or null.
     * @param switcher TTY mode switcher.
     * @throws IOException If unable to set TTY mode.
     */
    public Terminal(TTY tty, InputStream in, OutputStream out, LinePrinter lp, TTYModeSwitcher switcher)
            throws IOException {
        super(in);
        this.tty = tty;
        this.lp = lp == null ? this::printlnInternal : lp;
        this.out = out;
        this.switcher = switcher;
        this.lineCount = 0;
        if (lp == null && switcher.didChangeMode() && switcher.getBefore() == TTYMode.RAW) {
            this.out.write('\n');
            this.out.flush();
        }
    }

    /**
     * @return Get the terminal device.
     */
    public TTY getTTY() {
        return tty;
    }

    /**
     * Get a print stream that writes to the terminal according to the output
     * mode of the terminal. Handy for e.g. printing stack traces etc. while in
     * raw mode.
     *
     * @return A wrapping print stream.
     */
    public PrintStream printer() {
        return new TerminalPrintStream(this);
    }

    /**
     * Make a user confirmation. E.g.:
     * boolean really = term.confirm("Do you o'Really?");
     * 

* Will print out "Do you o'Really? [y/n]: ". If the user press * 'y' will pass (return true), if 'n', and 'backspace' will return false. * Enter is considered invalid input. Invalid characters will print a short * error message. * * @param what What to confirm. Basically the message before '[Y/n]'. * @return Confirmation result. */ public boolean confirm(String what) { String message = what + " [y/n]:"; return new InputConfirmation(this, message).getAsBoolean(); } /** * Make a user confirmation. E.g.: * boolean really = term.confirm("Do you o'Really?", false); *

* Will print out "Do you o'Really? [y/N]: ". If the user press * 'y' will pass (return true), if 'n', and 'backspace' will return false. * Enter will return the default value. Invalid characters will print a * short error message. * * @param what What to confirm. Basically the message before '[Y/n]'. * @param def the default response on 'enter'. * @return Confirmation result. */ public boolean confirm(String what, boolean def) { String message = what + " [" + (def ? "Y/n" : "y/N") + "]:"; return new InputConfirmation(this, message, def).getAsBoolean(); } /** * Show a "press any key to continue" message. If the user interrupts, an * exception is thrown, any other key just returns. * * @param message Message shown when waiting. */ public void pressToContinue(String message) { new InputConfirmation(this, message) { @Override protected boolean isConfirmation(Char c) { return true; } @Override protected void printConfirmation(boolean result) { } }.getAsBoolean(); } /** * Read a line from terminal. * * @param message The message to be shown before line input. * @return The read line. * @throws UncheckedIOException if interrupted or reached end of input. */ public String readLine(String message) { return new InputLine(this, message).readLine(); } /** * Execute callable, which may not be interruptable by itself, but listen to terminal input and abort the task if * CTRL-C is pressed. * * @param exec The executor to run task on. * @param callable The callable function. * @param The return type of the callable. * @return The result of the callable. * @throws IOException If aborted or read failure. * @throws InterruptedException If interrupted while waiting. * @throws ExecutionException If execution failed. */ public T executeAbortable(ExecutorService exec, Callable callable) throws IOException, InterruptedException, ExecutionException { Future task = exec.submit(callable); waitAbortable(task); return task.get(); } /** * Execute runnable, which may not be interruptable by itself, but listen to terminal input and abort the task if * CTRL-C is pressed. * * @param exec The executor to run task on. * @param callable The runnable function. * @throws IOException If aborted or read failure. * @throws InterruptedException If interrupted while waiting. * @throws ExecutionException If execution failed. */ public void executeAbortable(ExecutorService exec, Runnable callable) throws IOException, InterruptedException, ExecutionException { Future task = exec.submit(callable); waitAbortable(task); task.get(); } /** * Wait for future task to be done or canceled. React to terminal induced abort (ctrl-C) and cancel the task if so. * * @param task The task to wait for. * @param The task generic type. * @throws IOException On aborted or read failure. * @throws InterruptedException On thread interrupted. */ public void waitAbortable(Future task) throws IOException, InterruptedException { while (!task.isDone() && !task.isCancelled()) { Char c = readIfAvailable(); if (c != null && (c.codepoint() == Char.ABR || c.codepoint() == Char.ESC)) { task.cancel(true); throw new IOException("Aborted with '" + c.asString() + "'"); } sleep(79L); } } /** * Format and print string. * * @param format The string format. * @param args The argument passed to format. */ public void format(String format, Object... args) { print(String.format(Locale.US, format, args)); } /** * @param ch Character to print. */ public void print(char ch) { print("" + ch); } /** * @param ch Char instance to print. */ public void print(Char ch) { print(ch.toString()); } /** * @param message Message to print. */ public void print(String message) { try { out.write(message.getBytes(StandardCharsets.UTF_8)); out.flush(); } catch (IOException e) { throw new UncheckedIOException(e); } } @Override public void println(String message) { lp.println(message); } /** * Print a newline. */ public void println() { lp.println(null); } /** * Finish the current set of lines and continue below. */ public void finish() { try { if (switcher.getMode() == TTYMode.RAW && lineCount > 0) { out.write('\r'); out.write('\n'); out.flush(); } lineCount = 0; } catch (IOException e) { throw new UncheckedIOException(e); } } @Override public void close() throws IOException { try { if (switcher.didChangeMode() && switcher.getBefore() == TTYMode.COOKED) { finish(); } } catch (UncheckedIOException e) { // Ignore. } switcher.close(); } protected OutputStream getOutputStream() { return out; } protected void sleep(long millis) throws InterruptedException { Thread.sleep(millis); } private void printlnInternal(String message) { try { if (switcher.getMode() == TTYMode.RAW) { lnBefore(message); } else { lnAfter(message); } } catch (IOException e) { throw new UncheckedIOException(e); } } private void lnAfter(String message) throws IOException { if (message != null) { out.write('\r'); out.write(message.getBytes(StandardCharsets.UTF_8)); } out.write('\r'); out.write('\n'); out.flush(); ++lineCount; } private void lnBefore(String message) throws IOException { if (lineCount > 0) { out.write('\r'); out.write('\n'); } if (message != null) { out.write(message.getBytes(StandardCharsets.UTF_8)); } out.flush(); ++lineCount; } private final TTYModeSwitcher switcher; private final OutputStream out; private final LinePrinter lp; private final TTY tty; private int lineCount; /** * Terminal interface. It sets proper TTY mode and reads complex characters * from the input, and writes lines dependent on terminal mode. */ private static class TerminalPrintStream extends PrintStream { private final Terminal terminal; /** * Construct a default RAW terminal. * * @throws UncheckedIOException If unable to set TTY mode. */ public TerminalPrintStream(Terminal terminal) { super(terminal.getOutputStream(), true, StandardCharsets.UTF_8); this.terminal = terminal; } @Override public void write(int i) { if (i == Char.LF) { super.flush(); terminal.println(); } else { super.write(i); } } @Override public void write(byte[] bytes, int off, int len) { Objects.requireNonNull(bytes, "bytes == null"); for (int i = off; i < off + len; ++i) { int ch = bytes[i] < 0 ? 0x100 + bytes[i] : bytes[i]; this.write(ch); } super.flush(); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy