Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
org.jline.builtins.Tmux Maven / Gradle / Ivy
/*
* Copyright (c) 2002-2019, the original author or authors.
*
* This software is distributable under the BSD license. See the terms of the
* BSD license in the documentation provided with this software.
*
* https://opensource.org/licenses/BSD-3-Clause
*/
package org.jline.builtins;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CoderResult;
import java.nio.charset.CodingErrorAction;
import java.text.DateFormat;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.IntStream;
import org.jline.builtins.Options.HelpException;
import org.jline.keymap.BindingReader;
import org.jline.keymap.KeyMap;
import org.jline.reader.ParsedLine;
import org.jline.reader.impl.DefaultParser;
import org.jline.terminal.Attributes;
import org.jline.terminal.MouseEvent;
import org.jline.terminal.Size;
import org.jline.terminal.Terminal;
import org.jline.terminal.Terminal.Signal;
import org.jline.terminal.Terminal.SignalHandler;
import org.jline.terminal.impl.LineDisciplineTerminal;
import org.jline.utils.*;
import org.jline.utils.InfoCmp.Capability;
import static org.jline.builtins.Tmux.Layout.Type.LeftRight;
import static org.jline.builtins.Tmux.Layout.Type.TopBottom;
import static org.jline.builtins.Tmux.Layout.Type.WindowPane;
import static org.jline.keymap.KeyMap.*;
/**
* Terminal multiplexer
*/
public class Tmux {
public static final String OPT_PREFIX = "prefix";
public static final String CMD_COMMANDS = "commands";
public static final String CMD_SEND_PREFIX = "send-prefix";
public static final String CMD_SPLIT_WINDOW = "split-window";
public static final String CMD_SPLITW = "splitw";
public static final String CMD_SELECT_PANE = "select-pane";
public static final String CMD_SELECTP = "selectp";
public static final String CMD_RESIZE_PANE = "resize-pane";
public static final String CMD_RESIZEP = "resizep";
public static final String CMD_DISPLAY_PANES = "display-panes";
public static final String CMD_DISPLAYP = "displayp";
public static final String CMD_CLOCK_MODE = "clock-mode";
public static final String CMD_SET_OPTION = "set-option";
public static final String CMD_SET = "set";
public static final String CMD_LIST_KEYS = "list-keys";
public static final String CMD_LSK = "lsk";
public static final String CMD_SEND_KEYS = "send-keys";
public static final String CMD_SEND = "send";
public static final String CMD_BIND_KEY = "bind-key";
public static final String CMD_BIND = "bind";
public static final String CMD_UNBIND_KEY = "unbind-key";
public static final String CMD_UNBIND = "unbind";
public static final String CMD_NEW_WINDOW = "new-window";
public static final String CMD_NEWW = "neww";
public static final String CMD_NEXT_WINDOW = "next-window";
public static final String CMD_NEXT = "next";
public static final String CMD_PREVIOUS_WINDOW = "previous-window";
public static final String CMD_PREV = "prev";
public static final String CMD_LIST_WINDOWS = "list-windows";
public static final String CMD_LSW = "lsw";
private static final int[][][] WINDOW_CLOCK_TABLE = {
{ { 1,1,1,1,1 }, /* 0 */
{ 1,0,0,0,1 },
{ 1,0,0,0,1 },
{ 1,0,0,0,1 },
{ 1,1,1,1,1 } },
{ { 0,0,0,0,1 }, /* 1 */
{ 0,0,0,0,1 },
{ 0,0,0,0,1 },
{ 0,0,0,0,1 },
{ 0,0,0,0,1 } },
{ { 1,1,1,1,1 }, /* 2 */
{ 0,0,0,0,1 },
{ 1,1,1,1,1 },
{ 1,0,0,0,0 },
{ 1,1,1,1,1 } },
{ { 1,1,1,1,1 }, /* 3 */
{ 0,0,0,0,1 },
{ 1,1,1,1,1 },
{ 0,0,0,0,1 },
{ 1,1,1,1,1 } },
{ { 1,0,0,0,1 }, /* 4 */
{ 1,0,0,0,1 },
{ 1,1,1,1,1 },
{ 0,0,0,0,1 },
{ 0,0,0,0,1 } },
{ { 1,1,1,1,1 }, /* 5 */
{ 1,0,0,0,0 },
{ 1,1,1,1,1 },
{ 0,0,0,0,1 },
{ 1,1,1,1,1 } },
{ { 1,1,1,1,1 }, /* 6 */
{ 1,0,0,0,0 },
{ 1,1,1,1,1 },
{ 1,0,0,0,1 },
{ 1,1,1,1,1 } },
{ { 1,1,1,1,1 }, /* 7 */
{ 0,0,0,0,1 },
{ 0,0,0,0,1 },
{ 0,0,0,0,1 },
{ 0,0,0,0,1 } },
{ { 1,1,1,1,1 }, /* 8 */
{ 1,0,0,0,1 },
{ 1,1,1,1,1 },
{ 1,0,0,0,1 },
{ 1,1,1,1,1 } },
{ { 1,1,1,1,1 }, /* 9 */
{ 1,0,0,0,1 },
{ 1,1,1,1,1 },
{ 0,0,0,0,1 },
{ 1,1,1,1,1 } },
{ { 0,0,0,0,0 }, /* : */
{ 0,0,1,0,0 },
{ 0,0,0,0,0 },
{ 0,0,1,0,0 },
{ 0,0,0,0,0 } },
{ { 1,1,1,1,1 }, /* A */
{ 1,0,0,0,1 },
{ 1,1,1,1,1 },
{ 1,0,0,0,1 },
{ 1,0,0,0,1 } },
{ { 1,1,1,1,1 }, /* P */
{ 1,0,0,0,1 },
{ 1,1,1,1,1 },
{ 1,0,0,0,0 },
{ 1,0,0,0,0 } },
{ { 1,0,0,0,1 }, /* M */
{ 1,1,0,1,1 },
{ 1,0,1,0,1 },
{ 1,0,0,0,1 },
{ 1,0,0,0,1 } },
};
private final AtomicBoolean dirty = new AtomicBoolean(true);
private final AtomicBoolean resized = new AtomicBoolean(true);
private final Terminal terminal;
private final Display display;
private final PrintStream err;
private final String term;
private final Consumer runner;
private List windows = new ArrayList<>();
private Integer windowsId = 0;
private int activeWindow = 0;
private final AtomicBoolean running = new AtomicBoolean(true);
private final Size size = new Size();
private boolean identify;
private ScheduledExecutorService executor;
private ScheduledFuture> clockFuture;
private final Map serverOptions = new HashMap<>();
private KeyMap keyMap;
enum Binding {
Discard, SelfInsert, Mouse
}
private class Window {
private List panes = new CopyOnWriteArrayList<>();
private VirtualConsole active;
private int lastActive;
private final AtomicInteger paneId = new AtomicInteger();
private Layout layout;
private Tmux tmux;
private String name;
public Window(Tmux tmux) throws IOException {
this.tmux = tmux;
layout = new Layout();
layout.sx = size.getColumns();
layout.sy = size.getRows();
layout.type = WindowPane;
active = new VirtualConsole(paneId.incrementAndGet(), term
, 0, 0, size.getColumns(), size.getRows() - 1
, tmux::setDirty, tmux::close, layout);
active.active = lastActive++;
active.getConsole().setAttributes(terminal.getAttributes());
panes.add(active);
name = "win" + (windowsId < 10 ? "0" + windowsId : windowsId);
windowsId++;
}
public String getName() {
return name;
}
public List getPanes() {
return panes;
}
public VirtualConsole getActive() {
return active;
}
public void remove(VirtualConsole console) {
panes.remove(console);
if (!panes.isEmpty()) {
console.layout.remove();
if (active == console) {
active = panes.stream()
.sorted(Comparator.comparingInt(p -> p.active).reversed())
.findFirst().get();
}
layout = active.layout;
while (layout.parent != null) {
layout = layout.parent;
}
layout.fixOffsets();
layout.fixPanes(size.getColumns(), size.getRows());
}
}
public void handleResize() {
layout.resize(size.getColumns(), size.getRows() - 1);
panes.forEach(vc -> {
if (vc.width() != vc.layout.sx || vc.height() != vc.layout.sy
|| vc.left() != vc.layout.xoff || vc.top() != vc.layout.yoff) {
vc.resize(vc.layout.xoff, vc.layout.yoff, vc.layout.sx, vc.layout.sy);
display.clear();
}
});
}
public VirtualConsole splitPane(Options opt) throws IOException {
Layout.Type type = opt.isSet("horizontal") ? LeftRight : TopBottom;
// If we're splitting the main pane, create a parent
if (layout.type == WindowPane) {
Layout p = new Layout();
p.sx = layout.sx;
p.sy = layout.sy;
p.type = type;
p.cells.add(layout);
layout.parent = p;
layout = p;
}
Layout cell = active.layout();
if (opt.isSet("f")) {
while (cell.parent != layout) {
cell = cell.parent;
}
}
int size = -1;
if (opt.isSet("size")) {
size = opt.getNumber("size");
} else if (opt.isSet("perc")) {
int p = opt.getNumber("perc");
if (type == TopBottom) {
size = (cell.sy * p) / 100;
} else {
size = (cell.sx * p) / 100;
}
}
// Split now
Layout newCell = cell.split(type, size, opt.isSet("before"));
if (newCell == null) {
err.println("create pane failed: pane too small");
return null;
}
VirtualConsole newConsole = new VirtualConsole(paneId.incrementAndGet(), term
, newCell.xoff, newCell.yoff, newCell.sx, newCell.sy
, tmux::setDirty, tmux::close, newCell);
panes.add(newConsole);
newConsole.getConsole().setAttributes(terminal.getAttributes());
if (!opt.isSet("d")) {
active = newConsole;
active.active = lastActive++;
}
return newConsole;
}
public boolean selectPane(Options opt) {
VirtualConsole prevActive = active;
if (opt.isSet("L")) {
active = panes.stream()
.filter(c -> c.bottom() > active.top() && c.top() < active.bottom())
.filter(c -> c != active)
.sorted(Comparator
.comparingInt(c -> c.left() > active.left() ? c.left() : c.left() + size.getColumns()).reversed()
.thenComparingInt(c -> - c.active))
.findFirst().orElse(active);
}
else if (opt.isSet("R")) {
active = panes.stream()
.filter(c -> c.bottom() > active.top() && c.top() < active.bottom())
.filter(c -> c != active)
.sorted(Comparator
.comparingInt(c -> c.left() > active.left() ? c.left() : c.left() + size.getColumns())
.thenComparingInt(c -> - c.active))
.findFirst().orElse(active);
} else if (opt.isSet("U")) {
active = panes.stream()
.filter(c -> c.right() > active.left() && c.left() < active.right())
.filter(c -> c != active)
.sorted(Comparator
.comparingInt(c -> c.top() > active.top() ? c.top() : c.top() + size.getRows()).reversed()
.thenComparingInt(c -> - c.active))
.findFirst().orElse(active);
} else if (opt.isSet("D")) {
active = panes.stream()
.filter(c -> c.right() > active.left() && c.left() < active.right())
.filter(c -> c != active)
.sorted(Comparator
.comparingInt(c -> c.top() > active.top() ? c.top() : c.top() + size.getRows())
.thenComparingInt(c -> - c.active))
.findFirst().orElse(active);
}
boolean out = false;
if (prevActive != active) {
active.active = lastActive++;
out = true;
}
return out;
}
public void resizePane(Options opt, int adjust) {
if (opt.isSet("width")) {
int x = opt.getNumber("width");
active.layout().resizeTo(LeftRight, x);
}
if (opt.isSet("height")) {
int y = opt.getNumber("height");
active.layout().resizeTo(TopBottom, y);
}
if (opt.isSet("L")) {
active.layout().resize(LeftRight, -adjust, true);
} else if (opt.isSet("R")) {
active.layout().resize(LeftRight, adjust, true);
} else if (opt.isSet("U")) {
active.layout().resize(TopBottom, -adjust, true);
} else if (opt.isSet("D")) {
active.layout().resize(TopBottom, adjust, true);
}
}
}
public Tmux(Terminal terminal, PrintStream err, Consumer runner) throws IOException {
this.terminal = terminal;
this.err = err;
this.runner = runner;
display = new Display(terminal, true);
// Find terminal to use
Integer colors = terminal.getNumericCapability(Capability.max_colors);
term = (colors != null && colors >= 256) ? "screen-256color" : "screen";
// Setup defaults bindings
serverOptions.put(OPT_PREFIX, "`");
keyMap = createKeyMap(serverOptions.get(OPT_PREFIX));
}
protected KeyMap createKeyMap(String prefix) {
KeyMap keyMap = createEmptyKeyMap(prefix);
keyMap.bind(CMD_SEND_PREFIX, prefix + prefix);
keyMap.bind(CMD_SPLIT_WINDOW + " -v", prefix + "\"");
keyMap.bind(CMD_SPLIT_WINDOW + " -h", prefix + "%");
keyMap.bind(CMD_SELECT_PANE + " -U", prefix + key(terminal, Capability.key_up));
keyMap.bind(CMD_SELECT_PANE + " -D", prefix + key(terminal, Capability.key_down));
keyMap.bind(CMD_SELECT_PANE + " -L", prefix + key(terminal, Capability.key_left));
keyMap.bind(CMD_SELECT_PANE + " -R", prefix + key(terminal, Capability.key_right));
keyMap.bind(CMD_RESIZE_PANE + " -U 5", prefix + esc() + key(terminal, Capability.key_up));
keyMap.bind(CMD_RESIZE_PANE + " -D 5", prefix + esc() + key(terminal, Capability.key_down));
keyMap.bind(CMD_RESIZE_PANE + " -L 5", prefix + esc() + key(terminal, Capability.key_left));
keyMap.bind(CMD_RESIZE_PANE + " -R 5", prefix + esc() + key(terminal, Capability.key_right));
keyMap.bind(CMD_RESIZE_PANE + " -U", prefix + translate("^[[1;5A"), prefix + alt(translate("^[[A"))); // ctrl-up
keyMap.bind(CMD_RESIZE_PANE + " -D", prefix + translate("^[[1;5B"), prefix + alt(translate("^[[B"))); // ctrl-down
keyMap.bind(CMD_RESIZE_PANE + " -L", prefix + translate("^[[1;5C"), prefix + alt(translate("^[[C"))); // ctrl-left
keyMap.bind(CMD_RESIZE_PANE + " -R", prefix + translate("^[[1;5D"), prefix + alt(translate("^[[D"))); // ctrl-right
keyMap.bind(CMD_DISPLAY_PANES, prefix + "q");
keyMap.bind(CMD_CLOCK_MODE, prefix + "t");
keyMap.bind(CMD_NEW_WINDOW, prefix + "c");
keyMap.bind(CMD_NEXT_WINDOW, prefix + "n");
keyMap.bind(CMD_PREVIOUS_WINDOW, prefix + "p");
return keyMap;
}
protected KeyMap createEmptyKeyMap(String prefix) {
KeyMap keyMap = new KeyMap<>();
keyMap.setUnicode(Binding.SelfInsert);
keyMap.setNomatch(Binding.SelfInsert);
for (int i = 0; i < 255; i++) {
keyMap.bind(Binding.Discard, prefix + (char)(i));
}
keyMap.bind(Binding.Mouse, key(terminal, Capability.key_mouse));
return keyMap;
}
public void run() throws IOException {
SignalHandler prevWinchHandler = terminal.handle(Signal.WINCH, this::resize);
SignalHandler prevIntHandler = terminal.handle(Signal.INT, this::interrupt);
SignalHandler prevSuspHandler = terminal.handle(Signal.TSTP, this::suspend);
Attributes attributes = terminal.enterRawMode();
terminal.puts(Capability.enter_ca_mode);
terminal.puts(Capability.keypad_xmit);
terminal.trackMouse(Terminal.MouseTracking.Any);
terminal.flush();
executor = Executors.newSingleThreadScheduledExecutor();
try {
// Create first pane
size.copy(terminal.getSize());
windows.add(new Window(this));
activeWindow = 0;
runner.accept(active().getConsole());
// Start input loop
new Thread(this::inputLoop, "Mux input loop").start();
// Redraw loop
redrawLoop();
} catch (RuntimeException e) {
throw e;
} finally {
executor.shutdown();
terminal.trackMouse(Terminal.MouseTracking.Off);
terminal.puts(Capability.keypad_local);
terminal.puts(Capability.exit_ca_mode);
terminal.flush();
terminal.setAttributes(attributes);
terminal.handle(Signal.WINCH, prevWinchHandler);
terminal.handle(Signal.INT, prevIntHandler);
terminal.handle(Signal.TSTP, prevSuspHandler);
}
}
private VirtualConsole active() {
return windows.get(activeWindow).getActive();
}
private List panes() {
return windows.get(activeWindow).getPanes();
}
private Window window() {
return windows.get(activeWindow);
}
private void redrawLoop() {
while (running.get()) {
try {
synchronized (dirty) {
while (running.get() && !dirty.compareAndSet(true, false)) {
dirty.wait();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
handleResize();
redraw();
}
}
private void setDirty() {
synchronized (dirty) {
dirty.set(true);
dirty.notifyAll();
}
}
private void inputLoop() {
try {
BindingReader reader = new BindingReader(terminal.reader());
boolean first = true;
while (running.get()) {
Object b;
if (first) {
b = reader.readBinding(keyMap);
} else if (reader.peekCharacter(100) >= 0) {
b = reader.readBinding(keyMap, null, false);
} else {
b = null;
}
if (b == Binding.SelfInsert) {
if (active().clock) {
active().clock = false;
if (clockFuture != null && panes().stream().noneMatch(vc -> vc.clock)) {
clockFuture.cancel(false);
clockFuture = null;
}
setDirty();
} else {
active().getMasterInputOutput().write(reader.getLastBinding().getBytes());
first = false;
}
} else {
if (first) {
first = false;
} else {
active().getMasterInputOutput().flush();
first = true;
}
if (b == Binding.Mouse) {
MouseEvent event = terminal.readMouseEvent();
//System.err.println(event.toString());
} else if (b instanceof String || b instanceof String[]) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayOutputStream err = new ByteArrayOutputStream();
try (PrintStream pout = new PrintStream(out);
PrintStream perr = new PrintStream(err)) {
if (b instanceof String) {
execute(pout, perr, (String) b);
} else {
execute(pout, perr, Arrays.asList((String[]) b));
}
} catch (Exception e) {
// TODO: log
}
}
}
}
} catch (IOException e) {
if (running.get()) {
Log.info("Error in tmux input loop", e);
}
} finally {
running.set(false);
setDirty();
}
}
private synchronized void close(VirtualConsole terminal) {
int idx = -1;
Window window = null;
for (Window w: windows) {
idx = w.getPanes().indexOf(terminal);
if (idx >= 0) {
window = w;
break;
}
}
if (idx >= 0) {
window.remove(terminal);
if (window.getPanes().isEmpty()) {
if (windows.size() > 1) {
windows.remove(window);
if (activeWindow >= windows.size()) {
activeWindow--;
}
resize(Signal.WINCH);
} else {
running.set(false);
setDirty();
}
} else {
resize(Signal.WINCH);
}
}
}
private void resize(Signal signal) {
resized.set(true);
setDirty();
}
private void interrupt(Signal signal) {
active().getConsole().raise(signal);
}
private void suspend(Signal signal) {
active().getConsole().raise(signal);
}
private void handleResize() {
// Re-compute the layout
if (resized.compareAndSet(true, false)) {
size.copy(terminal.getSize());
}
window().handleResize();
}
public void execute(PrintStream out, PrintStream err, String command) throws Exception {
ParsedLine line = new DefaultParser().parse(command.trim(), 0);
execute(out, err, line.words());
}
public synchronized void execute(PrintStream out, PrintStream err, List command) throws Exception {
String name = command.get(0);
List args = command.subList(1, command.size());
switch (name) {
case CMD_SEND_PREFIX:
sendPrefix(out, err, args);
break;
case CMD_SPLIT_WINDOW:
case CMD_SPLITW:
splitWindow(out, err, args);
break;
case CMD_SELECT_PANE:
case CMD_SELECTP:
selectPane(out, err, args);
break;
case CMD_RESIZE_PANE:
case CMD_RESIZEP:
resizePane(out, err, args);
break;
case CMD_DISPLAY_PANES:
case CMD_DISPLAYP:
displayPanes(out, err, args);
break;
case CMD_CLOCK_MODE:
clockMode(out, err, args);
break;
case CMD_BIND_KEY:
case CMD_BIND:
bindKey(out, err, args);
break;
case CMD_UNBIND_KEY:
case CMD_UNBIND:
unbindKey(out, err, args);
break;
case CMD_LIST_KEYS:
case CMD_LSK:
listKeys(out, err, args);
break;
case CMD_SEND_KEYS:
case CMD_SEND:
sendKeys(out, err, args);
break;
case CMD_SET_OPTION:
case CMD_SET:
setOption(out, err, args);
break;
case CMD_NEW_WINDOW:
case CMD_NEWW:
newWindow(out, err, args);
break;
case CMD_NEXT_WINDOW:
case CMD_NEXT:
nextWindow(out, err, args);
break;
case CMD_PREVIOUS_WINDOW:
case CMD_PREV:
previousWindow(out, err, args);
break;
case CMD_LIST_WINDOWS:
case CMD_LSW:
listWindows(out, err, args);
break;
}
}
protected void listWindows(PrintStream out, PrintStream err, List args) throws Exception {
final String[] usage = {
"list-windows - ",
"Usage: list-windows",
" -? --help Show help"
};
Options opt = Options.compile(usage).parse(args);
if (opt.isSet("help")) {
throw new HelpException(opt.usage());
}
IntStream.range(0, windows.size())
.mapToObj(i -> {
StringBuilder sb = new StringBuilder();
sb.append(i);
sb.append(": ");
sb.append(windows.get(i).getName());
sb.append(i == activeWindow ? "* " : " ");
sb.append("(");
sb.append(windows.get(i).getPanes().size());
sb.append(" panes)");
if (i == activeWindow) {
sb.append(" (active)");
}
return sb.toString();
})
.sorted()
.forEach(out::println);
}
protected void previousWindow(PrintStream out, PrintStream err, List args) throws Exception {
final String[] usage = {
"previous-window - ",
"Usage: previous-window",
" -? --help Show help"
};
Options opt = Options.compile(usage).parse(args);
if (opt.isSet("help")) {
throw new HelpException(opt.usage());
}
if (windows.size() > 1) {
activeWindow--;
if (activeWindow < 0) {
activeWindow = windows.size() - 1;
}
setDirty();
}
}
protected void nextWindow(PrintStream out, PrintStream err, List args) throws Exception {
final String[] usage = {
"next-window - ",
"Usage: next-window",
" -? --help Show help"
};
Options opt = Options.compile(usage).parse(args);
if (opt.isSet("help")) {
throw new HelpException(opt.usage());
}
if (windows.size() > 1) {
activeWindow++;
if (activeWindow >= windows.size()) {
activeWindow = 0;
}
setDirty();
}
}
protected void newWindow(PrintStream out, PrintStream err, List args) throws Exception {
final String[] usage = {
"new-window - ",
"Usage: new-window",
" -? --help Show help"
};
Options opt = Options.compile(usage).parse(args);
if (opt.isSet("help")) {
throw new HelpException(opt.usage());
}
windows.add(new Window(this));
activeWindow = windows.size() - 1;
runner.accept(active().getConsole());
setDirty();
}
protected void setOption(PrintStream out, PrintStream err, List args) throws Exception {
final String[] usage = {
"set-option - ",
"Usage: set-option [-agosquw] option [value]",
" -? --help Show help",
" -u --unset Unset the option"
};
Options opt = Options.compile(usage).parse(args);
if (opt.isSet("help")) {
throw new HelpException(opt.usage());
}
int nbargs = opt.args().size();
if (nbargs < 1 || nbargs > 2) {
throw new HelpException(opt.usage());
}
String name = opt.args().get(0);
String value = nbargs > 1 ? opt.args().get(1) : null;
if (name.startsWith("@")) {
// set user option
} else {
// set server option
switch (name) {
case OPT_PREFIX:
if (value == null) {
throw new IllegalArgumentException("Missing argument");
}
String prefix = translate(value);
String oldPrefix = serverOptions.put(OPT_PREFIX, prefix);
KeyMap newKeys = createEmptyKeyMap(prefix);
for (Map.Entry e : keyMap.getBoundKeys().entrySet()) {
if (e.getValue() instanceof String) {
if (e.getKey().equals(oldPrefix + oldPrefix)) {
newKeys.bind(e.getValue(), prefix + prefix);
} else if (e.getKey().startsWith(oldPrefix)) {
newKeys.bind(e.getValue(), prefix + e.getKey().substring(oldPrefix.length()));
} else {
newKeys.bind(e.getValue(), e.getKey());
}
}
}
keyMap = newKeys;
break;
}
}
}
protected void bindKey(PrintStream out, PrintStream err, List args) throws Exception {
final String[] usage = {
"bind-key - ",
"Usage: bind-key key command [arguments]", /* [-cnr] [-t mode-table] [-T key-table] */
" -? --help Show help"
};
Options opt = Options.compile(usage).setOptionsFirst(true).parse(args);
if (opt.isSet("help")) {
throw new HelpException(opt.usage());
}
List vargs = opt.args();
if (vargs.size() < 2) {
throw new HelpException(opt.usage());
}
String prefix = serverOptions.get(OPT_PREFIX);
String key = prefix + KeyMap.translate(vargs.remove(0));
keyMap.unbind(key.substring(0, 2));
keyMap.bind(vargs.toArray(new String[vargs.size()]), key);
}
protected void unbindKey(PrintStream out, PrintStream err, List args) throws Exception {
final String[] usage = {
"unbind-key - ",
"Usage: unbind-key key", /* [-an] [-t mode-table] [-T key-table] */
" -? --help Show help"
};
Options opt = Options.compile(usage).setOptionsFirst(true).parse(args);
if (opt.isSet("help")) {
throw new HelpException(opt.usage());
}
List vargs = opt.args();
if (vargs.size() != 1) {
throw new HelpException(opt.usage());
}
String prefix = serverOptions.get(OPT_PREFIX);
String key = prefix + KeyMap.translate(vargs.remove(0));
keyMap.unbind(key);
keyMap.bind(Binding.Discard, key);
}
protected void listKeys(PrintStream out, PrintStream err, List args) throws Exception {
final String[] usage = {
"list-keys - ",
"Usage: list-keys ", /* [-t mode-table] [-T key-table] */
" -? --help Show help",
};
Options opt = Options.compile(usage).parse(args);
if (opt.isSet("help")) {
throw new HelpException(opt.usage());
}
String prefix = serverOptions.get(OPT_PREFIX);
keyMap.getBoundKeys().entrySet().stream()
.filter(e -> e.getValue() instanceof String)
.map(e -> {
String key = e.getKey();
String val = (String) e.getValue();
StringBuilder sb = new StringBuilder();
sb.append("bind-key -T ");
if (key.startsWith(prefix)) {
sb.append("prefix ");
key = key.substring(prefix.length());
} else {
sb.append("root ");
}
sb.append(display(key));
while (sb.length() < 32) {
sb.append(" ");
}
sb.append(val);
return sb.toString();
})
.sorted()
.forEach(out::println);
}
protected void sendKeys(PrintStream out, PrintStream err, List args) throws Exception {
final String[] usage = {
"send-keys - ",
"Usage: send-keys [-lXRM] [-N repeat-count] [-t target-pane] key...",
" -? --help Show help",
" -l --literal Send key literally",
" -N --number=repeat-count Specifies a repeat count"
};
Options opt = Options.compile(usage).parse(args);
if (opt.isSet("help")) {
throw new HelpException(opt.usage());
}
for (int i = 0, n = opt.getNumber("number"); i < n; i++) {
for (String arg : opt.args()) {
String s = opt.isSet("literal") ? arg : KeyMap.translate(arg);
active().getMasterInputOutput().write(s.getBytes());
}
}
}
protected void clockMode(PrintStream out, PrintStream err, List args) throws Exception {
final String[] usage = {
"clock-mode - ",
"Usage: clock-mode",
" -? --help Show help"
};
Options opt = Options.compile(usage).parse(args);
if (opt.isSet("help")) {
throw new HelpException(opt.usage());
}
active().clock = true;
if (clockFuture == null) {
long initial = Instant.now().until(Instant.now().truncatedTo(ChronoUnit.MINUTES).plusSeconds(60), ChronoUnit.MILLIS);
long delay = TimeUnit.MILLISECONDS.convert(1, TimeUnit.SECONDS);
clockFuture = executor.scheduleWithFixedDelay(this::setDirty, initial, delay, TimeUnit.MILLISECONDS);
}
setDirty();
}
protected void displayPanes(PrintStream out, PrintStream err, List args) throws Exception {
final String[] usage = {
"display-panes - ",
"Usage: display-panes",
" -? --help Show help"
};
Options opt = Options.compile(usage).parse(args);
if (opt.isSet("help")) {
throw new HelpException(opt.usage());
}
identify = true;
setDirty();
executor.schedule(() -> {
identify = false;
setDirty();
}, 1, TimeUnit.SECONDS);
}
protected void resizePane(PrintStream out, PrintStream err, List args) throws Exception {
final String[] usage = {
"resize-pane - ",
"Usage: resize-pane [-UDLR] [-x width] [-y height] [-t target-pane] [adjustment]",
" -? --help Show help",
" -U Resize pane upward",
" -D Select pane downward",
" -L Select pane to the left",
" -R Select pane to the right",
" -x --width=width Set the width of the pane",
" -y --height=height Set the height of the pane"
};
Options opt = Options.compile(usage).parse(args);
if (opt.isSet("help")) {
throw new HelpException(opt.usage());
}
int adjust;
if (opt.args().size() == 0) {
adjust = 1;
} else if (opt.args().size() == 1) {
adjust = Integer.parseInt(opt.args().get(0));
} else {
throw new HelpException(opt.usage());
}
window().resizePane(opt, adjust);
setDirty();
}
protected void selectPane(PrintStream out, PrintStream err, List args) throws Exception {
final String[] usage = {
"select-pane - ",
"Usage: select-pane [-UDLR] [-t target-pane]",
" -? --help Show help",
" -U Select pane up",
" -D Select pane down",
" -L Select pane left",
" -R Select pane right",
};
Options opt = Options.compile(usage).parse(args);
if (opt.isSet("help")) {
throw new HelpException(opt.usage());
}
if (window().selectPane(opt)) {
setDirty();
}
}
protected void sendPrefix(PrintStream out, PrintStream err, List args) throws Exception {
final String[] usage = {
"send-prefix - ",
"Usage: send-prefix [-2] [-t target-pane]",
" -? --help Show help",
};
Options opt = Options.compile(usage).parse(args);
if (opt.isSet("help")) {
throw new HelpException(opt.usage());
}
active().getMasterInputOutput().write(serverOptions.get(OPT_PREFIX).getBytes());
}
protected void splitWindow(PrintStream out, PrintStream err, List args) throws Exception {
final String[] usage = {
"split-window - ",
"Usage: split-window [-bdfhvP] [-c start-directory] [-F format] [-p percentage|-l size] [-t target-pane] [command]",
" -? --help Show help",
" -h --horizontal Horizontal split",
" -v --vertical Vertical split",
" -l --size=size Size",
" -p --perc=percentage Percentage",
" -b --before Insert the new pane before the active one",
" -f Split the full window instead of the active pane",
" -d Do not make the new pane the active one"
};
Options opt = Options.compile(usage).parse(args);
if (opt.isSet("help")) {
throw new HelpException(opt.usage());
}
VirtualConsole newConsole = window().splitPane(opt);
runner.accept(newConsole.getConsole());
setDirty();
}
protected void layoutResize() {
// See layout_resize
}
int ACTIVE_COLOR = 0xF44;
int INACTIVE_COLOR = 0x44F;
int CLOCK_COLOR = 0x44F;
protected synchronized void redraw() {
long[] screen = new long[size.getRows() * size.getColumns()];
// Fill
Arrays.fill(screen, 0x00000020L);
int[] cursor = new int[2];
for (VirtualConsole terminal : panes()) {
if (terminal.clock) {
String str = DateFormat.getTimeInstance(DateFormat.SHORT).format(new Date());
print(screen, terminal, str, CLOCK_COLOR);
} else {
// Dump terminal
terminal.dump(screen, terminal.top(), terminal.left(), size.getRows(), size.getColumns(),
terminal == active() ? cursor : null);
}
if (identify) {
String id = Integer.toString(terminal.id);
print(screen, terminal, id, terminal == active() ? ACTIVE_COLOR : INACTIVE_COLOR);
}
// Draw border
drawBorder(screen, size, terminal, 0x0L);
}
drawBorder(screen, size, active(), 0x010080000L << 32);
// Draw status
Arrays.fill(screen, (size.getRows() - 1) * size.getColumns(), size.getRows() * size.getColumns(),
0x20000080L << 32 | 0x0020L);
// Attribute mask: 0xYXFFFBBB00000000L
// X: Bit 0 - Underlined
// Bit 1 - Negative
// Bit 2 - Concealed
// Bit 3 - Bold
// Y: Bit 0 - Foreground set
// Bit 1 - Background set
// F: Foreground r-g-b
// B: Background r-g-b
List lines = new ArrayList<>();
int prevBg = 0;
int prevFg = 0;
boolean prevInv = false;
boolean prevUl = false;
boolean prevBold = false;
boolean prevConceal = false;
boolean prevHasFg = false;
boolean prevHasBg = false;
for (int y = 0; y < size.getRows(); y++) {
AttributedStringBuilder sb = new AttributedStringBuilder(size.getColumns());
for (int x = 0; x < size.getColumns(); x++) {
long d = screen[y * size.getColumns() + x];
int c = (int) (d & 0xffffffffL);
int a = (int) (d >> 32);
int bg = a & 0x000fff;
int fg = (a & 0xfff000) >> 12;
boolean ul = ((a & 0x01000000) != 0);
boolean inv = ((a & 0x02000000) != 0);
boolean conceal = ((a & 0x04000000) != 0);
boolean bold = ((a & 0x08000000) != 0);
boolean hasFg = ((a & 0x10000000) != 0);
boolean hasBg = ((a & 0x20000000) != 0);
if ((hasBg && prevHasBg && bg != prevBg) || prevHasBg != hasBg) {
if (!hasBg) {
sb.style(sb.style().backgroundDefault());
} else {
int col = bg;
col = Colors.roundRgbColor((col & 0xF00) >> 4, (col & 0x0F0), (col & 0x00F) << 4, 256);
sb.style(sb.style().background(col));
}
prevBg = bg;
prevHasBg = hasBg;
}
if ((hasFg && prevHasFg && fg != prevFg) || prevHasFg != hasFg) {
if (!hasFg) {
sb.style(sb.style().foregroundDefault());
} else {
int col = fg;
col = Colors.roundRgbColor((col & 0xF00) >> 4, (col & 0x0F0), (col & 0x00F) << 4, 256);
sb.style(sb.style().foreground(col));
}
prevFg = fg;
prevHasFg = hasFg;
}
if (conceal != prevConceal) {
sb.style(conceal ? sb.style().conceal() : sb.style().concealOff());
prevConceal = conceal;
}
if (inv != prevInv) {
sb.style(inv ? sb.style().inverse() : sb.style().inverseOff());
prevInv = inv;
}
if (ul != prevUl) {
sb.style(ul ? sb.style().underline() : sb.style().underlineOff());
prevUl = ul;
}
if (bold != prevBold) {
sb.style(bold ? sb.style().bold() : sb.style().boldOff());
prevBold = bold;
}
sb.append((char) c);
}
lines.add(sb.toAttributedString());
}
display.resize(size.getRows(), size.getColumns());
display.update(lines, size.cursorPos(cursor[1], cursor[0]));
}
private void print(long[] screen, VirtualConsole terminal, String id, int color) {
if (terminal.height() > 5) {
long attr = ((long) color << 32) | 0x02000000000000000L;
int yoff = (terminal.height() - 5) / 2;
int xoff = (terminal.width() - id.length() * 6) / 2;
for (int i = 0; i < id.length(); i++) {
char ch = id.charAt(i);
int idx;
switch (ch) {
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9': idx = ch - '0'; break;
case ':': idx = 10; break;
case 'A': idx = 11; break;
case 'P': idx = 12; break;
case 'M': idx = 13; break;
default: idx = -1; break;
}
if (idx >= 0) {
int[][] data = WINDOW_CLOCK_TABLE[idx];
for (int y = 0; y < data.length; y++) {
for (int x = 0; x < data[y].length; x++) {
if (data[y][x] != 0) {
int off = (terminal.top + yoff + y) * size.getColumns() + terminal.left() + xoff + x + 6 * i;
screen[off] = attr | ' ';
}
}
}
}
}
} else {
long attr = ((long) color << 44) | 0x01000000000000000L;
int yoff = (terminal.height() + 1) / 2;
int xoff = (terminal.width() - id.length()) / 2;
int off = (terminal.top + yoff) * size.getColumns() + terminal.left() + xoff;
for (int i = 0; i < id.length(); i++) {
screen[off + i] = attr | id.charAt(i);
}
}
}
private void drawBorder(long[] screen, Size size, VirtualConsole terminal, long attr) {
for (int i = terminal.left(); i < terminal.right(); i++) {
int y0 = terminal.top() - 1;
int y1 = terminal.bottom();
drawBorderChar(screen, size, i, y0, attr, '─');
drawBorderChar(screen, size, i, y1, attr, '─');
}
for (int i = terminal.top(); i < terminal.bottom(); i++) {
int x0 = terminal.left() - 1;
int x1 = terminal.right();
drawBorderChar(screen, size, x0, i, attr, '│');
drawBorderChar(screen, size, x1, i, attr, '│');
}
drawBorderChar(screen, size, terminal.left() - 1, terminal.top() - 1, attr, '┌');
drawBorderChar(screen, size, terminal.right(), terminal.top() - 1, attr, '┐');
drawBorderChar(screen, size, terminal.left() - 1, terminal.bottom(), attr, '└');
drawBorderChar(screen, size, terminal.right(), terminal.bottom(), attr, '┘');
}
private void drawBorderChar(long[] screen, Size size, int x, int y, long attr, int c) {
if (x >= 0 && x < size.getColumns() && y >= 0 && y < size.getRows() - 1) {
int oldc = (int)(screen[y * size.getColumns() + x] & 0xFFFFFFFFL);
c = addBorder(c, oldc);
screen[y * size.getColumns() + x] = attr | c;
}
}
private int addBorder(int c, int oldc) {
if (oldc == ' ') {
return c;
}
if (oldc == '┼') {
return '┼';
}
switch (c) {
case '│':
return addBorder('╷', addBorder('╵', oldc));
case '─':
return addBorder('╴', addBorder('╶', oldc));
case '┌':
return addBorder('╶', addBorder('╷', oldc));
case '┐':
return addBorder('╴', addBorder('╷', oldc));
case '└':
return addBorder('╶', addBorder('╵', oldc));
case '┘':
return addBorder('╴', addBorder('╵', oldc));
case '├':
return addBorder('╶', addBorder('│', oldc));
case '┤':
return addBorder('╴', addBorder('│', oldc));
case '┬':
return addBorder('╷', addBorder('─', oldc));
case '┴':
return addBorder('╵', addBorder('─', oldc));
case '╴':
switch (oldc) {
case '│': return '┤';
case '─': return '─';
case '┌': return '┬';
case '┐': return '┐';
case '└': return '┴';
case '┘': return '┘';
case '├': return '┼';
case '┤': return '┤';
case '┬': return '┬';
case '┴': return '┴';
default:
throw new IllegalArgumentException();
}
case '╵':
switch (oldc) {
case '│': return '│';
case '─': return '┴';
case '┌': return '├';
case '┐': return '┤';
case '└': return '└';
case '┘': return '┘';
case '├': return '├';
case '┤': return '┤';
case '┬': return '┼';
case '┴': return '┴';
default:
throw new IllegalArgumentException();
}
case '╶':
switch (oldc) {
case '│': return '├';
case '─': return '─';
case '┌': return '┌';
case '┐': return '┬';
case '└': return '└';
case '┘': return '┴';
case '├': return '├';
case '┤': return '┼';
case '┬': return '┬';
case '┴': return '┴';
default:
throw new IllegalArgumentException();
}
case '╷':
switch (oldc) {
case '│': return '│';
case '─': return '┬';
case '┌': return '┌';
case '┐': return '┐';
case '└': return '├';
case '┘': return '┤';
case '├': return '├';
case '┤': return '┤';
case '┬': return '┬';
case '┴': return '┼';
default:
throw new IllegalArgumentException();
}
default:
throw new IllegalArgumentException();
}
}
static class Layout {
static final Pattern PATTERN = Pattern.compile("([0-9]+)x([0-9]+),([0-9]+),([0-9]+)([^0-9]\\S*)?");
private static final int PANE_MINIMUM = 3;
enum Type { LeftRight, TopBottom, WindowPane }
Type type;
Layout parent;
int sx;
int sy;
int xoff;
int yoff;
List cells = new CopyOnWriteArrayList<>();
public static Layout parse(String layout) {
if (layout.length() < 6) {
throw new IllegalArgumentException("Bad syntax");
}
String chk = layout.substring(0, 4);
if (layout.charAt(4) != ',') {
throw new IllegalArgumentException("Bad syntax");
}
layout = layout.substring(5);
if (Integer.parseInt(chk, 16) != checksum(layout)) {
throw new IllegalArgumentException("Bad checksum");
}
return parseCell(null, layout);
}
public String dump() {
StringBuilder sb = new StringBuilder(64);
sb.append("0000,");
doDump(sb);
int chk = checksum(sb, 5);
sb.setCharAt(0, toHexChar((chk >> 12) & 0x000F));
sb.setCharAt(1, toHexChar((chk >> 8) & 0x000F));
sb.setCharAt(2, toHexChar((chk >> 4) & 0x000F));
sb.setCharAt(3, toHexChar(chk & 0x000F));
return sb.toString();
}
private static char toHexChar(int i) {
return (i < 10) ? (char)(i + '0') : (char)(i - 10 + 'a');
}
private void doDump(StringBuilder sb) {
sb.append(sx).append('x').append(sy).append(',').append(xoff).append(',').append(yoff);
switch (type) {
case WindowPane:
sb.append(',').append('0');
break;
case TopBottom:
case LeftRight:
sb.append(type == Type.TopBottom ? '[' : '{');
boolean first = true;
for (Layout c : cells) {
if (first) {
first = false;
} else {
sb.append(',');
}
c.doDump(sb);
}
sb.append(type == Type.TopBottom ? ']' : '}');
break;
}
}
public void resize(Type type, int change, boolean opposite) {
/* Find next parent of the same type. */
Layout lc = this;
Layout lcparent = lc.parent;
while (lcparent != null && lcparent.type != type) {
lc = lcparent;
lcparent = lc.parent;
}
if (lcparent == null) {
return;
}
/* If this is the last cell, move back one. */
if (lc.nextSibling() == null) {
lc = lc.prevSibling();
}
/* Grow or shrink the cell. */
int size;
int needed = change;
while (needed != 0) {
if (change > 0) {
size = lc.resizePaneGrow(type, needed, opposite);
needed -= size;
} else {
size = lc.resizePaneShrink(type, needed);
needed += size;
}
if (size == 0) {
/* no more change possible */
break;
}
}
fixOffsets();
fixPanes();
}
int resizePaneGrow(Type type, int needed, boolean opposite) {
int size = 0;
/* Growing. Always add to the current cell. */
Layout lcadd = this;
/* Look towards the tail for a suitable cell for reduction. */
Layout lcremove = this.nextSibling();
while (lcremove != null) {
size = lcremove.resizeCheck(type);
if (size > 0) {
break;
}
lcremove = lcremove.nextSibling();
}
/* If none found, look towards the head. */
if (opposite && lcremove == null) {
lcremove = this.prevSibling();
while (lcremove != null) {
size = lcremove.resizeCheck(type);
if (size > 0) {
break;
}
lcremove = lcremove.prevSibling();
}
}
if (lcremove == null) {
return 0;
}
/* Change the cells. */
if (size > needed) {
size = needed;
}
lcadd.resizeAdjust(type, size);
lcremove.resizeAdjust(type, -size);
return size;
}
int resizePaneShrink(Type type, int needed) {
int size = 0;
/* Shrinking. Find cell to remove from by walking towards head. */
Layout lcremove = this;
do {
size = lcremove.resizeCheck(type);
if (size > 0) {
break;
}
lcremove = lcremove.prevSibling();
} while (lcremove != null);
if (lcremove == null) {
return 0;
}
/* And add onto the next cell (from the original cell). */
Layout lcadd = this.nextSibling();
if (lcadd == null) {
return 0;
}
/* Change the cells. */
if (size > -needed) {
size = -needed;
}
lcadd.resizeAdjust(type, size);
lcremove.resizeAdjust(type, -size);
return size;
}
Layout prevSibling() {
int idx = parent.cells.indexOf(this);
if (idx > 0) {
return parent.cells.get(idx - 1);
} else {
return null;
}
}
Layout nextSibling() {
int idx = parent.cells.indexOf(this);
if (idx < parent.cells.size() - 1) {
return parent.cells.get(idx + 1);
} else {
return null;
}
}
public void resizeTo(Type type, int new_size) {
/* Find next parent of the same type. */
Layout lc = this;
Layout lcparent = lc.parent;
while (lcparent != null && lcparent.type != type) {
lc = lcparent;
lcparent = lc.parent;
}
if (lcparent == null) {
return;
}
/* Work out the size adjustment. */
int size = type == LeftRight ? lc.sx : lc.sy;
int change = lc.nextSibling() == null ? size - new_size : new_size - size;
/* Resize the pane. */
lc.resize(type, change, true);
}
public void resize(int sx, int sy) {
// Horizontal
int xchange = sx - this.sx;
int xlimit = resizeCheck(LeftRight);
if (xchange < 0 && xchange < -xlimit) {
xchange = -xlimit;
}
if (xlimit == 0) {
if (sx <= this.sx) {
xchange = 0;
} else {
xchange = sx - this.sx;
}
}
if (xchange != 0) {
resizeAdjust(LeftRight, xchange);
}
// Horizontal
int ychange = sy - this.sy;
int ylimit = resizeCheck(Type.TopBottom);
if (ychange < 0 && ychange < -ylimit) {
ychange = -ylimit;
}
if (ylimit == 0) {
if (sy <= this.sy) {
ychange = 0;
} else {
ychange = sy - this.sy;
}
}
if (ychange != 0) {
resizeAdjust(Type.TopBottom, ychange);
}
// Fix offsets
fixOffsets();
fixPanes(sx, sy);
}
public void remove() {
if (parent == null) {
throw new IllegalStateException();
}
int idx = parent.cells.indexOf(this);
Layout other = parent.cells.get(idx == 0 ? 1 : idx - 1);
other.resizeAdjust(parent.type, parent.type == LeftRight ? (sx + 1) : (sy + 1));
parent.cells.remove(this);
if (other.parent.cells.size() == 1) {
if (other.parent.parent == null) {
other.parent = null;
} else {
other.parent.parent.cells.set(other.parent.parent.cells.indexOf(other.parent), other);
other.parent = other.parent.parent;
}
}
}
private int resizeCheck(Type type) {
if (this.type == Type.WindowPane) {
int min = PANE_MINIMUM;
int avail;
if (type == LeftRight) {
avail = this.sx;
} else {
avail = this.sy;
min += 1; // TODO: need status
}
if (avail > min) {
avail -= min;
} else {
avail = 0;
}
return avail;
} else if (this.type == type) {
return this.cells.stream()
.mapToInt(c -> c != null ? c.resizeCheck(type) : 0)
.sum();
} else {
return this.cells.stream()
.mapToInt(c -> c != null ? c.resizeCheck(type) : Integer.MAX_VALUE)
.min()
.orElse(Integer.MAX_VALUE);
}
}
private void resizeAdjust(Type type, int change) {
if (type == LeftRight) {
this.sx += change;
} else {
this.sy += change;
}
if (this.type == Type.WindowPane) {
return;
}
if (this.type != type) {
for (Layout c : cells) {
c.resizeAdjust(type, change);
}
return;
}
while (change != 0) {
for (Layout c : cells) {
if (change == 0) {
break;
}
if (change > 0) {
c.resizeAdjust(type, 1);
change--;
continue;
}
if (c.resizeCheck(type) > 0) {
c.resizeAdjust(type, -1);
change++;
}
};
}
}
public void fixOffsets() {
if (type == LeftRight) {
int xoff = this.xoff;
for (Layout cell : cells) {
cell.xoff = xoff;
cell.yoff = this.yoff;
cell.fixOffsets();
xoff += cell.sx + 1;
}
} else if (type == TopBottom) {
int yoff = this.yoff;
for (Layout cell : cells) {
cell.xoff = this.xoff;
cell.yoff = yoff;
cell.fixOffsets();
yoff += cell.sy + 1;
}
}
}
public void fixPanes() {
}
public void fixPanes(int sx, int sy) {
}
public int countCells() {
switch (type) {
case LeftRight:
case TopBottom:
return cells.stream().mapToInt(Layout::countCells).sum();
default:
return 1;
}
}
public Layout split(Type type, int size, boolean insertBefore) {
if (type == WindowPane) {
throw new IllegalStateException();
}
if ((type == LeftRight ? sx : sy) < PANE_MINIMUM * 2 + 1) {
return null;
}
if (parent == null) {
throw new IllegalStateException();
}
int saved_size = type == LeftRight ? sx : sy;
int size2 = size < 0 ? ((saved_size + 1) / 2) - 1 : insertBefore ? saved_size - size - 1 : size;
if (size2 < PANE_MINIMUM) {
size2 = PANE_MINIMUM;
} else if (size2 > saved_size - 2) {
size2 = saved_size - 2;
}
int size1 = saved_size - 1 - size2;
if (parent.type != type) {
Layout p = new Layout();
p.type = type;
p.parent = parent;
p.sx = sx;
p.sy = sy;
p.xoff = xoff;
p.yoff = yoff;
parent.cells.set(parent.cells.indexOf(this), p);
p.cells.add(this);
parent = p;
}
Layout cell = new Layout();
cell.type = WindowPane;
cell.parent = parent;
parent.cells.add(parent.cells.indexOf(this) + (insertBefore ? 0 : 1), cell);
int sx = this.sx;
int sy = this.sy;
int xoff = this.xoff;
int yoff = this.yoff;
Layout cell1, cell2;
if (insertBefore) {
cell1 = cell;
cell2 = this;
} else {
cell1 = this;
cell2 = cell;
}
if (type == LeftRight) {
cell1.setSize(size1, sy, xoff, yoff);
cell2.setSize(size2, sy, xoff + size1 + 1, yoff);
} else {
cell1.setSize(sx, size1, xoff, yoff);
cell2.setSize(sx, size2, xoff, yoff + size1 + 1);
}
return cell;
}
private void setSize(int sx, int sy, int xoff, int yoff) {
this.sx = sx;
this.sy = sy;
this.xoff = xoff;
this.yoff = yoff;
}
private static int checksum(CharSequence layout) {
return checksum(layout, 0);
}
private static int checksum(CharSequence layout, int start) {
int csum = 0;
for (int i = start; i < layout.length(); i++) {
csum = (csum >> 1) + ((csum & 1) << 15);
csum += layout.charAt(i);
}
return csum;
}
private static Layout parseCell(Layout parent, String layout) {
Matcher matcher = PATTERN.matcher(layout);
if (matcher.matches()) {
Layout cell = new Layout();
cell.type = Type.WindowPane;
cell.parent = parent;
cell.sx = Integer.parseInt(matcher.group(1));
cell.sy = Integer.parseInt(matcher.group(2));
cell.xoff = Integer.parseInt(matcher.group(3));
cell.yoff = Integer.parseInt(matcher.group(4));
if (parent != null) {
parent.cells.add(cell);
}
layout = matcher.group(5);
if (layout == null || layout.isEmpty()) {
return cell;
}
if (layout.charAt(0) == ',') {
int i = 1;
while (i < layout.length() && Character.isDigit(layout.charAt(i))) {
i++;
}
if (i == layout.length()) {
return cell;
}
if (layout.charAt(i) == ',') {
layout = layout.substring(i);
}
}
int i;
switch (layout.charAt(0)) {
case '{':
cell.type = LeftRight;
i = findMatch(layout, '{', '}');
parseCell(cell, layout.substring(1, i));
layout = layout.substring(i + 1);
if (!layout.isEmpty() && layout.charAt(0) == ',') {
parseCell(parent, layout.substring(1));
}
return cell;
case '[':
cell.type = Type.TopBottom;
i = findMatch(layout, '[', ']');
parseCell(cell, layout.substring(1, i));
layout = layout.substring(i + 1);
if (!layout.isEmpty() && layout.charAt(0) == ',') {
parseCell(parent, layout.substring(1));
}
return cell;
case ',':
parseCell(parent, layout.substring(1));
return cell;
default:
throw new IllegalArgumentException("Unexpected '" + layout.charAt(0) + "'");
}
} else {
throw new IllegalArgumentException("Bad syntax");
}
}
}
private static int findMatch(String layout, char c0, char c1) {
if (layout.charAt(0) != c0) {
throw new IllegalArgumentException();
}
int nb = 0;
int i = 0;
while (i < layout.length()) {
char c = layout.charAt(i);
if (c == c0) {
nb++;
} else if (c == c1) {
if (--nb == 0) {
return i;
}
}
i++;
}
if (nb > 0) {
throw new IllegalArgumentException("No matching '" + c1 + "'");
}
return i;
}
private static class VirtualConsole implements Closeable {
private final ScreenTerminal terminal;
private final Consumer closer;
private final int id;
private int left;
private int top;
private final Layout layout;
private int active;
private boolean clock;
private final OutputStream masterOutput;
private final OutputStream masterInputOutput;
private final LineDisciplineTerminal console;
public VirtualConsole(int id, String type, int left, int top, int columns, int rows, Runnable dirty, Consumer closer, Layout layout) throws IOException {
String name = String.format("tmux%02d", id);
this.id = id;
this.left = left;
this.top = top;
this.closer = closer;
this.terminal = new ScreenTerminal(columns, rows) {
@Override
protected void setDirty() {
super.setDirty();
dirty.run();
}
};
this.masterOutput = new MasterOutputStream();
this.masterInputOutput = new OutputStream() {
@Override
public void write(int b) throws IOException {
console.processInputByte(b);
}
};
this.console = new LineDisciplineTerminal(
name,
type,
masterOutput,
null) {
@Override
protected void doClose() throws IOException {
super.doClose();
closer.accept(VirtualConsole.this);
}
};
this.console.setSize(new Size(columns, rows));
this.layout = layout;
}
Layout layout() {
return layout;
}
public int left() {
return left;
}
public int top() {
return top;
}
public int right() {
return left() + width();
}
public int bottom() {
return top() + height();
}
public int width() {
return console.getWidth();
}
public int height() {
return console.getHeight();
}
public LineDisciplineTerminal getConsole() {
return console;
}
public OutputStream getMasterInputOutput() {
return masterInputOutput;
}
public void resize(int left, int top, int width, int height) {
this.left = left;
this.top = top;
console.setSize(new Size(width, height));
terminal.setSize(width, height);
console.raise(Signal.WINCH);
}
public void dump(long[] fullscreen, int ftop, int fleft, int fheight, int fwidth, int[] cursor) {
terminal.dump(fullscreen, ftop, fleft, fheight, fwidth, cursor);
}
@Override
public void close() throws IOException {
console.close();
}
private class MasterOutputStream extends OutputStream {
private final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
private final CharsetDecoder decoder = Charset.defaultCharset().newDecoder()
.onMalformedInput(CodingErrorAction.REPLACE)
.onUnmappableCharacter(CodingErrorAction.REPLACE);
@Override
public synchronized void write(int b) {
buffer.write(b);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
buffer.write(b, off, len);
}
@Override
public synchronized void flush() throws IOException {
int size = buffer.size();
if (size > 0) {
CharBuffer out;
for (; ; ) {
out = CharBuffer.allocate(size);
ByteBuffer in = ByteBuffer.wrap(buffer.toByteArray());
CoderResult result = decoder.decode(in, out, false);
if (result.isOverflow()) {
size *= 2;
} else {
buffer.reset();
buffer.write(in.array(), in.arrayOffset(), in.remaining());
break;
}
}
if (out.position() > 0) {
out.flip();
terminal.write(out);
masterInputOutput.write(terminal.read().getBytes());
}
}
}
@Override
public void close() throws IOException {
flush();
}
}
}
}