001package org.jdrupes.builder.core.progessui; 002 003import java.util.ArrayDeque; 004import java.util.ArrayList; 005import java.util.HashMap; 006import java.util.Iterator; 007import java.util.LinkedHashMap; 008import java.util.List; 009import java.util.Map; 010import java.util.Objects; 011import java.util.Optional; 012import java.util.Queue; 013import java.util.stream.IntStream; 014 015import io.vavr.Tuple; 016import io.vavr.Tuple2; 017import sun.misc.Signal; 018import sun.misc.SignalHandler; 019 020public class SplitConsole { 021 022 private class ManagedLine { 023 public Thread thread; 024 public String text = ""; 025 public String lastRendered = null; 026 public Spinner spinner = Spinner.DOTS; 027 public Color color = Color.BLUE; 028 } 029 030 private final Terminal term; 031 032 private final List<ManagedLine> managedLines = new ArrayList<>(); 033 private final Map<Thread, String> offScreenLines = new LinkedHashMap<>(); 034 private final Spinner[] spinners = Spinner.values(); 035 036 public SplitConsole() { 037 this.term = new Terminal(); 038 039 if (term.supportsAnsi) { 040 for (int i = 0; i < term.lines() * 1 / 3; i++) { 041 System.out.println(); 042 } 043 recomputeLayout(term.lines() * 1 / 3); 044 redraw(); 045 installSigWinchHandler(); 046 } 047 } 048 049 private ManagedLine managedLine(Thread thread) { 050 return managedLines.stream().filter(l -> l.thread == thread).findFirst() 051 .orElse(null); 052 } 053 054 public synchronized void allocateLine() { 055 Thread thread = Thread.currentThread(); 056 if (managedLine(thread) != null || offScreenLines.containsKey(thread)) { 057 return; 058 } 059 060 // Find free and allocate or use overflow 061 IntStream.range(0, managedLines.size()) 062 .filter(i -> managedLines.get(i).thread == null).findFirst() 063 .ifPresentOrElse(i -> { 064 initManaged(i, thread, ""); 065 }, () -> { 066 offScreenLines.put(thread, ""); 067 }); 068 } 069 070 private void initManaged(int index, Thread thread, String text) { 071 var line = managedLines.get(index); 072 line.thread = thread; 073 line.text = text; 074 line.spinner = Spinner.CYCLE; 075 line.color = pickColor(thread); 076 } 077 078 public synchronized void deallocateLine() { 079 Thread thread = Thread.currentThread(); 080 var line = managedLine(thread); 081 082 if (line != null) { 083 line.thread = null; 084 line.text = ""; 085 promoteOffScreenLines(); 086 redraw(); 087 } else { 088 offScreenLines.remove(thread); 089 } 090 } 091 092 private synchronized void recomputeLayout(int managedHeight) { 093 while (managedLines.size() > managedHeight) { 094 if (managedLines.get(managedLines.size() - 1).thread == null) { 095 managedLines.remove(managedLines.size() - 1); 096 break; 097 } 098 int freeSlot; 099 for (freeSlot = 0; freeSlot < managedLines.size() - 1; freeSlot++) { 100 if (managedLines.get(freeSlot).thread == null) { 101 break; 102 } 103 } 104 if (freeSlot < managedLines.size() - 1) { 105 ManagedLine last = managedLines.get(managedLines.size() - 1); 106 managedLines.set(freeSlot, last); 107 continue; 108 } 109 ManagedLine last = managedLines.get(managedLines.size() - 1); 110 offScreenLines.put(last.thread, last.text); 111 last.thread = null; 112 } 113 114 if (managedLines.size() < managedHeight) { 115 while (managedLines.size() < managedHeight) { 116 managedLines.add(new ManagedLine()); 117 } 118 promoteOffScreenLines(); 119 } 120 } 121 122 private void promoteOffScreenLines() { 123 for (int i = 0; i < managedLines.size(); i++) { 124 if (offScreenLines.isEmpty()) { 125 break; 126 } 127 if (managedLines.get(i).thread == null) { 128 shiftManaged(i); 129 var offLine = offScreenLines.entrySet().iterator().next(); 130 initManaged(managedLines.size() - 1, offLine.getKey(), 131 offLine.getValue()); 132 offScreenLines.remove(offLine.getKey()); 133 } 134 } 135 } 136 137 private void shiftManaged(int index) { 138 var lastManaged = managedLines.size() - 1; 139 for (int i = index; i < lastManaged; i++) { 140 var toMove = managedLines.get(i + 1); 141 managedLines.set(i, toMove); 142 toMove.lastRendered = null; 143 } 144 managedLines.set(lastManaged, new ManagedLine()); 145 } 146 147 private void installSigWinchHandler() { 148 try { 149 Signal.handle(new Signal("WINCH"), new SignalHandler() { 150 @Override 151 public void handle(Signal sig) { 152 term.refreshSize(); 153// recomputeLayout(); 154 redraw(); 155 } 156 }); 157 } catch (Throwable ignored) { 158 } 159 } 160 161 public synchronized void writeLine(String text) { 162 Thread thread = Thread.currentThread(); 163// Integer index = managedLineByThread.get(thread); 164 var line = managedLine(thread); 165 166// String spinnerChar = ""; 167// Spinner spinner = threadSpinners.get(thread); 168// if (spinner != null) { 169// spinnerChar = spinner.next(); 170// } 171// 172// String coloredText = applyColor(thread, spinnerChar + " " + text); 173 174 if (line != null) { 175 line.text = text; 176 redraw(); 177 } else if (offScreenLines.containsKey(thread)) { 178 offScreenLines.put(thread, text); 179 } 180 } 181 182// private String applyColor(Thread thread, String text) { 183// Integer color = threadColors.get(thread); 184// if (color != null) { 185// return Ansi.color(color) + text + Ansi.RESET; 186// } 187// return text; 188// } 189 190 private Color pickColor(Thread thread) { 191 Color[] codes = Color.values(); 192 return codes[Math.abs(thread.hashCode()) % codes.length]; 193 } 194 195 public void println(String text) { 196 int lastScrolled = term.lines() - managedLines.size() - 1; 197 System.out.println(Ansi.hideCursor() + Ansi.scrollRange(0, lastScrolled) 198 + Ansi.scrollUp() + Ansi.scrollAll() 199 + Ansi.cursorAt(lastScrolled - 1, 0) 200 + Ansi.showCursor() + text); 201 System.out.flush(); 202 } 203 204 private synchronized void redraw() { 205 redraw(false); 206 } 207 208 private synchronized void redraw(boolean force) { 209 if (!term.supportsAnsi) { 210 return; 211 } 212 213 var managedStart = term.lines() - managedLines.size(); 214 System.out.print(Ansi.hideCursor()); 215 for (int i = 0; i < managedLines.size(); i++) { 216 ManagedLine line = managedLines.get(i); 217 var row = managedStart + i; 218 if (!Objects.equals(line.text, line.lastRendered) || force) { 219 System.out.print(Ansi.cursorAt(row, 0) + Ansi.clearLine() 220 + line.text); 221 line.lastRendered = line.text; 222 } 223 } 224 225 System.out.print(Ansi.cursorAt(managedStart - 1, 0) 226 + Ansi.showCursor()); 227 System.out.flush(); 228 } 229 230 public void close() { 231 if (term.supportsAnsi) { 232 System.out.print(Ansi.showCursor()); 233 } 234 } 235 236 enum Spinner { 237 CYCLE(new String[] { "-", "\\", "|", "/" }), 238 DOTS(new String[] { ".", "..", "...", " " }); 239 240 private final String[] frames; 241 private int idx = 0; 242 243 Spinner(String[] frames) { 244 this.frames = frames; 245 } 246 247 public String next() { 248 String frame = frames[idx % frames.length]; 249 idx++; 250 return frame; 251 } 252 } 253 254 public enum Color { 255 RED(31), 256 GREEN(32), 257 YELLOW(33), 258 BLUE(34), 259 MAGENTA(35), 260 CYAN(36); 261 262 private final int code; 263 264 Color(int code) { 265 this.code = code; 266 } 267 268 public int code() { 269 return code; 270 } 271 } 272 273}