one
Version:
One is a new React Framework that makes Vite serve both native and web.
566 lines (565 loc) • 19.9 kB
JavaScript
import { getAllServers, setRoute, clearRoute, getLastActiveServer } from "./registry.mjs";
import { getBootedSimulators } from "./picker.mjs";
import { setRouteMode, setPendingMapping, clearMappingsForSimulator, getSimulatorMappings, setSimulatorMapping } from "./server.mjs";
import colors from "picocolors";
const CABLE_COLORS = [colors.green, colors.cyan, colors.magenta, colors.blue, colors.yellow];
const ESC = "\x1B";
const CSI = `${ESC}[`;
const ansi = {
hideCursor: `${CSI}?25l`,
showCursor: `${CSI}?25h`,
clearScreen: `${CSI}2J`,
home: `${CSI}H`
};
let tuiState = null;
let daemonState = null;
let refreshInterval = null;
let physicsInterval = null;
let stdinListener = null;
let resizeListener = null;
function getRouteMode() {
return tuiState?.routeMode || "ask";
}
function calcLayout(width) {
const simEndX = Math.floor(width * 0.25);
const serverStartX = Math.floor(width * 0.65);
return {
simEndX,
serverStartX
};
}
function showPopup(message, durationMs = 2e3) {
if (!tuiState) return;
if (tuiState.popup) {
clearTimeout(tuiState.popup.timeout);
}
const timeout = setTimeout(() => {
if (tuiState) {
tuiState.popup = null;
render();
}
}, durationMs);
tuiState.popup = {
message,
timeout
};
render();
}
function startTUI(state) {
daemonState = state;
const width = process.stdout.columns || 80;
const height = process.stdout.rows || 24;
const {
simEndX,
serverStartX
} = calcLayout(width);
tuiState = {
simulators: [],
servers: [],
cables: /* @__PURE__ */new Map(),
draggingSimIndex: null,
modeBeforeDrag: null,
selectedCol: 0,
selectedRow: 0,
routeMode: "most-recent",
lastRender: "",
width,
height,
simEndX,
serverStartX,
rowStartY: 5,
popup: null
};
process.stdout.write(ansi.clearScreen + ansi.home + ansi.hideCursor);
if (process.stdin.isTTY) {
process.stdin.setRawMode(true);
}
process.stdin.resume();
let resizePending = false;
resizeListener = () => {
if (!tuiState) return;
tuiState.width = process.stdout.columns || 80;
tuiState.height = process.stdout.rows || 24;
const layout = calcLayout(tuiState.width);
tuiState.simEndX = layout.simEndX;
tuiState.serverStartX = layout.serverStartX;
tuiState.lastRender = "";
if (!resizePending) {
resizePending = true;
setImmediate(() => {
resizePending = false;
process.stdout.write(ansi.clearScreen + ansi.home);
render();
});
}
};
process.stdout.on("resize", resizeListener);
stdinListener = key => {
const str = key.toString();
if (str === "" || str === "q") {
stopTUI();
process.exit(0);
}
if (!tuiState || !daemonState) return;
if (str === "\x1B[A") {
tuiState.selectedRow = Math.max(0, tuiState.selectedRow - 1);
} else if (str === "\x1B[B") {
const max = tuiState.selectedCol === 0 ? Math.max(0, tuiState.simulators.length - 1) : Math.max(0, tuiState.servers.length - 1);
tuiState.selectedRow = Math.min(max, tuiState.selectedRow + 1);
} else if (str === "\x1B[C") {
if (tuiState.selectedCol === 0) {
tuiState.selectedCol = 1;
tuiState.selectedRow = Math.min(tuiState.selectedRow, Math.max(0, tuiState.servers.length - 1));
}
} else if (str === "\x1B[D") {
if (tuiState.selectedCol === 1) {
tuiState.selectedCol = 0;
tuiState.selectedRow = Math.min(tuiState.selectedRow, Math.max(0, tuiState.simulators.length - 1));
}
} else if (str === " " || str === "\r") {
handleAction();
} else if (str === "d") {
handleDisconnect();
} else if (str === "m") {
tuiState.routeMode = tuiState.routeMode === "most-recent" ? "ask" : "most-recent";
setRouteMode(tuiState.routeMode);
} else if (str === "b") {
stopTUI();
console.log(colors.dim("\nDaemon running in background."));
return;
}
render();
};
process.stdin.on("data", stdinListener);
const signalHandler = () => {
stopTUI();
process.exit(0);
};
process.on("SIGINT", signalHandler);
process.on("SIGTERM", signalHandler);
physicsInterval = setInterval(updatePhysics, 50);
refreshInterval = setInterval(refreshData, 1e3);
refreshData();
}
function getRouteKey(sim) {
return `sim:${sim.udid}`;
}
function handleAction() {
if (!tuiState || !daemonState) return;
const isDragging = tuiState.draggingSimIndex !== null;
if (isDragging) {
if (tuiState.selectedCol === 1 && tuiState.servers.length > 0) {
const simIndex = tuiState.draggingSimIndex;
const sim = tuiState.simulators[simIndex];
const serverIndex = tuiState.selectedRow;
const server = tuiState.servers[serverIndex];
if (server && sim) {
setSimulatorMapping(sim.udid, server.id);
setPendingMapping(server.id, sim.udid);
setRoute(daemonState, getRouteKey(sim), server.id);
const cable = tuiState.cables.get(simIndex);
if (cable) {
cable.serverIndex = serverIndex;
}
tuiState.draggingSimIndex = null;
if (tuiState.modeBeforeDrag === "most-recent") {
tuiState.routeMode = "most-recent";
setRouteMode("most-recent");
}
tuiState.modeBeforeDrag = null;
}
}
return;
}
if (tuiState.selectedCol === 0 && tuiState.simulators.length > 0) {
const simIndex = tuiState.selectedRow;
const sim = tuiState.simulators[simIndex];
if (!sim) return;
tuiState.modeBeforeDrag = tuiState.routeMode;
if (tuiState.routeMode === "most-recent") {
tuiState.routeMode = "ask";
setRouteMode("ask");
}
clearRoute(daemonState, getRouteKey(sim));
clearMappingsForSimulator(sim.udid);
let cable = tuiState.cables.get(simIndex);
if (!cable) {
cable = {
serverIndex: null,
controlPoint: {
x: tuiState.simEndX + 5,
y: tuiState.rowStartY + simIndex
},
velocity: {
x: 0,
y: 0
}
};
tuiState.cables.set(simIndex, cable);
}
cable.serverIndex = null;
cable.velocity = {
x: 3,
y: -2
};
tuiState.draggingSimIndex = simIndex;
} else if (tuiState.selectedCol === 1 && tuiState.servers.length > 0) {
const serverIndex = tuiState.selectedRow;
for (const [simIndex, cable] of tuiState.cables) {
if (cable.serverIndex === serverIndex) {
const sim = tuiState.simulators[simIndex];
if (sim) {
tuiState.modeBeforeDrag = tuiState.routeMode;
if (tuiState.routeMode === "most-recent") {
tuiState.routeMode = "ask";
setRouteMode("ask");
}
clearRoute(daemonState, getRouteKey(sim));
cable.serverIndex = null;
cable.velocity = {
x: -3,
y: 2
};
tuiState.draggingSimIndex = simIndex;
}
break;
}
}
}
}
function handleDisconnect() {
if (!tuiState || !daemonState) return;
if (tuiState.selectedCol === 0) {
const simIndex = tuiState.selectedRow;
const sim = tuiState.simulators[simIndex];
const cable = tuiState.cables.get(simIndex);
if (!sim || !cable || cable.serverIndex === null) return;
if (tuiState.routeMode === "most-recent") {
tuiState.routeMode = "ask";
setRouteMode("ask");
showPopup("Switched to manual mode", 1500);
}
clearRoute(daemonState, getRouteKey(sim));
clearMappingsForSimulator(sim.udid);
cable.serverIndex = null;
cable.velocity = {
x: -4,
y: 3
};
} else {
const serverIndex = tuiState.selectedRow;
for (const [simIndex, cable] of tuiState.cables) {
if (cable.serverIndex === serverIndex) {
const sim = tuiState.simulators[simIndex];
if (sim) {
if (tuiState.routeMode === "most-recent") {
tuiState.routeMode = "ask";
setRouteMode("ask");
showPopup("Switched to manual mode", 1500);
}
clearRoute(daemonState, getRouteKey(sim));
clearMappingsForSimulator(sim.udid);
cable.serverIndex = null;
cable.velocity = {
x: -4,
y: 3
};
}
break;
}
}
}
}
function updatePhysics() {
if (!tuiState) return;
const gravity = 0.3;
const damping = 0.85;
let needsRender = false;
for (const [simIndex, cable] of tuiState.cables) {
const simY = tuiState.rowStartY + simIndex;
if (cable.serverIndex !== null) {
const sagCurve = i => {
if (i <= 4) return 6 - i;
return 2 + (i - 4) * 0.8;
};
const sag = sagCurve(simIndex);
const serverY = tuiState.rowStartY + cable.serverIndex;
const targetX = (tuiState.simEndX + tuiState.serverStartX) / 2;
const targetY = (simY + serverY) / 2 + sag;
const dx = targetX - cable.controlPoint.x;
const dy = targetY - cable.controlPoint.y;
cable.velocity.x += dx * 0.15;
cable.velocity.y += dy * 0.15;
cable.velocity.x *= damping;
cable.velocity.y *= damping;
cable.controlPoint.x += cable.velocity.x;
cable.controlPoint.y += cable.velocity.y;
if (Math.abs(cable.velocity.x) > 0.05 || Math.abs(cable.velocity.y) > 0.05) {
needsRender = true;
}
} else {
cable.velocity.y += gravity;
cable.velocity.x *= damping;
cable.velocity.y *= damping;
cable.controlPoint.x += cable.velocity.x;
cable.controlPoint.y += cable.velocity.y;
const anchorX = tuiState.simEndX;
const anchorY = simY;
if (cable.controlPoint.x < anchorX) {
cable.controlPoint.x = anchorX;
cable.velocity.x = Math.abs(cable.velocity.x) * 0.5;
}
if (cable.controlPoint.x > tuiState.serverStartX) {
cable.controlPoint.x = tuiState.serverStartX;
cable.velocity.x = -Math.abs(cable.velocity.x) * 0.5;
}
if (cable.controlPoint.y < anchorY) {
cable.controlPoint.y = anchorY;
cable.velocity.y = Math.abs(cable.velocity.y) * 0.3;
}
if (cable.controlPoint.y > tuiState.height - 5) {
cable.controlPoint.y = tuiState.height - 5;
cable.velocity.y = -Math.abs(cable.velocity.y) * 0.5;
}
needsRender = true;
}
}
if (needsRender) render();
}
async function refreshData() {
if (!tuiState || !daemonState) return;
const newSims = await getBootedSimulators();
const newServers = getAllServers(daemonState);
tuiState.simulators = newSims;
tuiState.servers = newServers;
const simMappings = getSimulatorMappings();
for (let simIndex = 0; simIndex < newSims.length; simIndex++) {
const sim = newSims[simIndex];
const mappedServerId = simMappings.get(sim.udid);
let routedServerIndex = null;
if (mappedServerId) {
routedServerIndex = newServers.findIndex(s => s.id === mappedServerId);
if (routedServerIndex === -1) routedServerIndex = null;
}
let cable = tuiState.cables.get(simIndex);
if (!cable) {
cable = {
serverIndex: routedServerIndex,
controlPoint: {
x: tuiState.simEndX + 5,
y: tuiState.rowStartY + simIndex
},
velocity: {
x: 0,
y: 0
}
};
tuiState.cables.set(simIndex, cable);
}
if (tuiState.draggingSimIndex !== simIndex) {
if (routedServerIndex !== cable.serverIndex) {
cable.serverIndex = routedServerIndex;
if (routedServerIndex !== null) {
cable.velocity = {
x: 0,
y: -2
};
}
}
}
}
for (const simIndex of tuiState.cables.keys()) {
if (simIndex >= newSims.length) {
tuiState.cables.delete(simIndex);
}
}
if (tuiState.selectedCol === 0) {
tuiState.selectedRow = Math.min(tuiState.selectedRow, Math.max(0, newSims.length - 1));
} else {
tuiState.selectedRow = Math.min(tuiState.selectedRow, Math.max(0, newServers.length - 1));
}
render();
}
function render() {
if (!tuiState) return;
const {
width,
height,
simEndX,
serverStartX
} = tuiState;
const lines = [];
const title = " one daemon ";
const headerPad = Math.max(0, width - title.length - 10);
lines.push(colors.cyan(`\u250C\u2500${title}${"\u2500".repeat(headerPad)}\u2500:8081\u2500\u2510`));
const isAuto = tuiState.routeMode === "most-recent";
const toggleLeft = isAuto ? colors.green("\u25B6") : colors.dim("\u25B7");
const toggleRight = isAuto ? colors.dim("\u25C1") : colors.yellow("\u25C0");
const autoLabel = isAuto ? colors.green("AUTO") : colors.dim("auto");
const askLabel = isAuto ? colors.dim("ask") : colors.yellow("ASK");
const toggle = ` ${autoLabel} ${toggleLeft}\u2550\u2550\u2550${toggleRight} ${askLabel} [m] toggle`;
const togglePad = Math.max(0, width - stripAnsi(toggle).length - 2);
lines.push(colors.cyan("\u2502") + toggle + " ".repeat(togglePad) + colors.cyan("\u2502"));
const simHeader = " SIMULATORS";
const srvHeader = "SERVERS ";
const gap = " ".repeat(Math.max(0, serverStartX - simEndX));
lines.push(colors.cyan("\u2502") + colors.bold(simHeader.padEnd(simEndX - 1)) + gap + colors.bold(srvHeader.padStart(width - serverStartX - 1)) + colors.cyan("\u2502"));
lines.push(colors.cyan("\u2502") + colors.dim("\u2500".repeat(width - 2)) + colors.cyan("\u2502"));
const contentRows = height - 7;
for (let row = 0; row < contentRows; row++) {
const y = tuiState.rowStartY + row;
let line = "";
line += colors.cyan("\u2502");
const sim = tuiState.simulators[row];
let simText = "";
if (sim) {
const isSelected = tuiState.selectedCol === 0 && tuiState.selectedRow === row;
const cable = tuiState.cables.get(row);
const hasConnection = cable?.serverIndex !== null;
const cableColor = CABLE_COLORS[row % CABLE_COLORS.length];
const plug = hasConnection ? cableColor("\u25CF") : colors.dim("\u25CB");
const name = truncate(sim.name, simEndX - 5);
simText = `${name} ${plug}`;
if (isSelected) simText = colors.inverse(simText);
}
const simTextLen = stripAnsi(simText).length;
const simPad = Math.max(0, simEndX - 1 - simTextLen);
line += " ".repeat(simPad) + simText;
let cableZone = "";
for (let x = simEndX; x < serverStartX; x++) {
const char = getCableCharAt(x, y);
cableZone += char || " ";
}
line += cableZone;
const server = tuiState.servers[row];
let srvLeft = "";
let srvRight = "";
if (server) {
const isSelected = tuiState.selectedCol === 1 && tuiState.selectedRow === row;
let connectedColor = null;
for (const [simIndex, cable] of tuiState.cables) {
if (cable.serverIndex === row) {
connectedColor = CABLE_COLORS[simIndex % CABLE_COLORS.length];
break;
}
}
const lastActive = daemonState ? getLastActiveServer(daemonState) : null;
const isLastActive = lastActive?.id === server.id;
const plug = connectedColor ? connectedColor("\u25CF") : colors.dim("\u25CB");
const star = isLastActive ? colors.yellow("\u2605") : " ";
const shortRoot = truncate(server.root.replace(process.env.HOME || "", "~"), width - serverStartX - 14);
srvLeft = `${plug} ${star}${shortRoot}`;
srvRight = colors.bold(colors.yellow(`:${server.port}`));
if (isSelected) {
srvLeft = colors.inverse(srvLeft);
srvRight = colors.inverse(srvRight);
}
}
const srvLeftLen = stripAnsi(srvLeft).length;
const srvRightLen = stripAnsi(srvRight).length;
const srvColWidth = width - serverStartX - 2;
const srvGap = Math.max(1, srvColWidth - srvLeftLen - srvRightLen);
line += srvLeft + " ".repeat(srvGap) + srvRight;
line += colors.cyan("\u2502");
lines.push(line);
}
if (tuiState.popup) {
const msg = tuiState.popup.message;
const padLeft = Math.floor((width - msg.length - 4) / 2);
const padRight = width - msg.length - padLeft - 4;
lines.push(colors.cyan("\u2502") + " ".repeat(Math.max(0, padLeft)) + colors.bgYellow(colors.black(` ${msg} `)) + " ".repeat(Math.max(0, padRight)) + colors.cyan("\u2502"));
} else {
lines.push(colors.cyan("\u2502") + colors.dim(" \u2191\u2193 select \u2190\u2192 move space grab/plug d disconnect b bg q quit").padEnd(width - 2) + colors.cyan("\u2502"));
}
lines.push(colors.cyan(`\u2514${"\u2500".repeat(width - 2)}\u2518`));
const output = lines.join("\n");
if (output !== tuiState.lastRender) {
tuiState.lastRender = output;
process.stdout.write(ansi.home + output);
}
}
function getCableCharAt(x, y) {
if (!tuiState) return null;
if (tuiState.simulators.length === 0) return null;
for (const [simIndex, cable] of tuiState.cables) {
const startX = tuiState.simEndX;
const startY = tuiState.rowStartY + simIndex;
let endX, endY;
if (cable.serverIndex !== null) {
endX = tuiState.serverStartX;
endY = tuiState.rowStartY + cable.serverIndex;
} else {
endX = Math.round(cable.controlPoint.x);
endY = Math.round(cable.controlPoint.y);
}
const ctrlX = Math.round(cable.controlPoint.x);
const ctrlY = Math.round(cable.controlPoint.y);
const steps = 30;
for (let i = 0; i <= steps; i++) {
const t = i / steps;
const invT = 1 - t;
const px = Math.round(invT * invT * startX + 2 * invT * t * ctrlX + t * t * endX);
const py = Math.round(invT * invT * startY + 2 * invT * t * ctrlY + t * t * endY);
if (px === x && py === y) {
const connected = cable.serverIndex !== null;
const baseColor = CABLE_COLORS[simIndex % CABLE_COLORS.length];
const color = connected ? baseColor : colors.dim;
const tPrev = Math.max(0, (i - 1) / steps);
const tNext = Math.min(1, (i + 1) / steps);
const prevX = Math.round((1 - tPrev) * (1 - tPrev) * startX + 2 * (1 - tPrev) * tPrev * ctrlX + tPrev * tPrev * endX);
const prevY = Math.round((1 - tPrev) * (1 - tPrev) * startY + 2 * (1 - tPrev) * tPrev * ctrlY + tPrev * tPrev * endY);
const nextX = Math.round((1 - tNext) * (1 - tNext) * startX + 2 * (1 - tNext) * tNext * ctrlX + tNext * tNext * endX);
const nextY = Math.round((1 - tNext) * (1 - tNext) * startY + 2 * (1 - tNext) * tNext * ctrlY + tNext * tNext * endY);
const dx = nextX - prevX;
const dy = nextY - prevY;
let char;
if (Math.abs(dx) > Math.abs(dy) * 2) {
char = "\u2500";
} else if (Math.abs(dy) > Math.abs(dx) * 2) {
char = "\u2502";
} else if (dx > 0 && dy > 0 || dx < 0 && dy < 0) {
char = "\u2572";
} else {
char = "\u2571";
}
return color(char);
}
}
}
return null;
}
function truncate(str, maxLen) {
if (maxLen <= 0) return "";
if (str.length <= maxLen) return str;
return str.slice(0, maxLen - 1) + "\u2026";
}
function stripAnsi(str) {
return str.replace(/\x1b\[[0-9;]*m/g, "");
}
function stopTUI() {
if (refreshInterval) {
clearInterval(refreshInterval);
refreshInterval = null;
}
if (physicsInterval) {
clearInterval(physicsInterval);
physicsInterval = null;
}
if (stdinListener) {
process.stdin.removeListener("data", stdinListener);
stdinListener = null;
}
if (resizeListener) {
process.stdout.removeListener("resize", resizeListener);
resizeListener = null;
}
process.stdout.write(ansi.clearScreen + ansi.home + ansi.showCursor);
if (process.stdin.isTTY) {
process.stdin.setRawMode(false);
}
tuiState = null;
daemonState = null;
}
function triggerPulse(_serverId, _direction) {}
export { getRouteMode, startTUI, stopTUI, triggerPulse };
//# sourceMappingURL=tui.mjs.map