polyv-live-cli
Version:
CLI tool for managing PolyV live streaming services.
596 lines • 24.7 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.layoutManager = exports.LayoutManager = void 0;
const events_1 = require("events");
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const os = __importStar(require("os"));
const config_validator_1 = require("./config-validator");
const errors_1 = require("../utils/errors");
class LayoutManager extends events_1.EventEmitter {
constructor(options = {}) {
super();
this.layouts = new Map();
this.currentLayoutId = 'default';
this.customLayouts = new Map();
this.currentTerminalSize = { width: 120, height: 40 };
this.options = {
layoutsDir: options.layoutsDir || path.join(os.homedir(), '.polyv-cli', 'layouts'),
autoSave: options.autoSave ?? true,
enableResponsive: options.enableResponsive ?? true,
};
this.layoutsDir = this.options.layoutsDir;
this.ensureLayoutsDirectory();
this.loadBuiltInLayouts();
this.loadCustomLayouts();
}
getAvailableLayouts() {
return Array.from(this.layouts.keys());
}
getLayout(layoutId) {
return this.layouts.get(layoutId);
}
getCurrentLayout() {
const layout = this.layouts.get(this.currentLayoutId);
if (!layout) {
throw new errors_1.ConfigurationError(`Current layout '${this.currentLayoutId}' not found`);
}
return layout;
}
getCurrentLayoutId() {
return this.currentLayoutId;
}
async applyLayout(layoutId) {
const layout = this.layouts.get(layoutId);
if (!layout) {
throw new errors_1.ConfigurationError(`Layout '${layoutId}' not found`);
}
const validation = config_validator_1.ConfigValidator.validateLayoutConfig(layout);
if (!validation.valid) {
throw new errors_1.ConfigurationError(`Cannot apply invalid layout: ${config_validator_1.ConfigValidator.createErrorMessage(validation)}`);
}
if (this.currentTerminalSize.width < layout.minTerminalSize.width ||
this.currentTerminalSize.height < layout.minTerminalSize.height) {
throw new errors_1.ConfigurationError(`Terminal size ${this.currentTerminalSize.width}x${this.currentTerminalSize.height} ` +
`is smaller than layout minimum ${layout.minTerminalSize.width}x${layout.minTerminalSize.height}`);
}
const previousLayoutId = this.currentLayoutId;
this.currentLayoutId = layoutId;
try {
const adjustedLayout = this.options.enableResponsive
? this.applyResponsiveAdjustments(layout)
: layout;
await this.applyLayoutToInterface(adjustedLayout);
this.emit('layout:applied', adjustedLayout, previousLayoutId);
}
catch (error) {
this.currentLayoutId = previousLayoutId;
throw new errors_1.ConfigurationError(`Failed to apply layout '${layoutId}': ${error instanceof Error ? error.message : String(error)}`);
}
}
async createCustomLayout(layout) {
const validation = config_validator_1.ConfigValidator.validateLayoutConfig(layout);
if (!validation.valid) {
throw new errors_1.ConfigurationError(`Cannot create invalid layout: ${config_validator_1.ConfigValidator.createErrorMessage(validation)}`);
}
if (this.layouts.has(layout.id)) {
throw new errors_1.ConfigurationError(`Layout with ID '${layout.id}' already exists`);
}
const customLayout = {
...layout,
isBuiltIn: false,
};
await this.saveCustomLayout(customLayout);
this.layouts.set(customLayout.id, customLayout);
this.customLayouts.set(customLayout.id, customLayout);
this.emit('layout:created', customLayout);
}
async updateCustomLayout(layoutId, updates) {
const existingLayout = this.customLayouts.get(layoutId);
if (!existingLayout) {
throw new errors_1.ConfigurationError(`Custom layout '${layoutId}' not found`);
}
if (existingLayout.isBuiltIn) {
throw new errors_1.ConfigurationError(`Cannot update built-in layout '${layoutId}'`);
}
const updatedLayout = {
...existingLayout,
...updates,
id: layoutId,
isBuiltIn: false,
};
const validation = config_validator_1.ConfigValidator.validateLayoutConfig(updatedLayout);
if (!validation.valid) {
throw new errors_1.ConfigurationError(`Cannot update to invalid layout: ${config_validator_1.ConfigValidator.createErrorMessage(validation)}`);
}
await this.saveCustomLayout(updatedLayout);
this.layouts.set(layoutId, updatedLayout);
this.customLayouts.set(layoutId, updatedLayout);
if (this.currentLayoutId === layoutId) {
await this.applyLayoutToInterface(updatedLayout);
}
this.emit('layout:updated', updatedLayout, existingLayout);
}
async deleteCustomLayout(layoutId) {
const layout = this.customLayouts.get(layoutId);
if (!layout) {
throw new errors_1.ConfigurationError(`Custom layout '${layoutId}' not found`);
}
if (layout.isBuiltIn) {
throw new errors_1.ConfigurationError(`Cannot delete built-in layout '${layoutId}'`);
}
if (this.currentLayoutId === layoutId) {
await this.applyLayout('default');
}
const layoutFile = path.join(this.layoutsDir, `${layoutId}.json`);
if (fs.existsSync(layoutFile)) {
await fs.promises.unlink(layoutFile);
}
this.layouts.delete(layoutId);
this.customLayouts.delete(layoutId);
this.emit('layout:deleted', layout);
}
async updateTerminalSize(size) {
const previousSize = { ...this.currentTerminalSize };
this.currentTerminalSize = size;
if (this.options.enableResponsive) {
const currentLayout = this.getCurrentLayout();
if (size.width < currentLayout.minTerminalSize.width ||
size.height < currentLayout.minTerminalSize.height) {
this.emit('layout:size-warning', {
current: size,
minimum: currentLayout.minTerminalSize,
layout: currentLayout.id
});
}
const adjustedLayout = this.applyResponsiveAdjustments(currentLayout);
await this.applyLayoutToInterface(adjustedLayout);
}
this.emit('layout:terminal-resized', size, previousSize);
}
getLayoutMetrics(layoutId) {
const layout = layoutId
? this.layouts.get(layoutId)
: this.getCurrentLayout();
if (!layout) {
throw new errors_1.ConfigurationError(`Layout '${layoutId}' not found`);
}
const metrics = {
totalComponents: layout.components.length,
activeComponents: layout.components.filter(c => c.config['visible'] !== false).length,
gridUtilization: this.calculateGridUtilization(layout),
overlappingComponents: this.findOverlappingComponents(layout),
};
return metrics;
}
findOverlappingComponents(layout) {
const overlapping = [];
const components = layout.components;
for (let i = 0; i < components.length; i++) {
for (let j = i + 1; j < components.length; j++) {
const componentI = components[i];
const componentJ = components[j];
if (componentI && componentJ && this.componentsOverlap(componentI, componentJ)) {
if (!overlapping.includes(componentI)) {
overlapping.push(componentI);
}
if (!overlapping.includes(componentJ)) {
overlapping.push(componentJ);
}
}
}
}
return overlapping;
}
optimizeLayout(layout) {
const optimizedLayout = JSON.parse(JSON.stringify(layout));
this.resolveOverlaps(optimizedLayout);
this.packComponents(optimizedLayout);
return optimizedLayout;
}
getCustomLayouts() {
return Array.from(this.customLayouts.values());
}
getBuiltInLayouts() {
return Array.from(this.layouts.values()).filter(layout => layout.isBuiltIn);
}
async exportLayout(layoutId, filePath) {
const layout = this.layouts.get(layoutId);
if (!layout) {
throw new errors_1.ConfigurationError(`Layout '${layoutId}' not found`);
}
try {
const layoutJson = JSON.stringify(layout, null, 2);
await fs.promises.writeFile(filePath, layoutJson, 'utf8');
this.emit('layout:exported', layout, filePath);
}
catch (error) {
throw new errors_1.ConfigurationError(`Failed to export layout to '${filePath}': ${error instanceof Error ? error.message : String(error)}`);
}
}
async importLayout(filePath) {
try {
const fileContent = await fs.promises.readFile(filePath, 'utf8');
const layout = JSON.parse(fileContent);
const validation = config_validator_1.ConfigValidator.validateLayoutConfig(layout);
if (!validation.valid) {
throw new errors_1.ConfigurationError(`Invalid layout file: ${config_validator_1.ConfigValidator.createErrorMessage(validation)}`);
}
if (this.layouts.has(layout.id)) {
const originalId = layout.id;
let counter = 1;
while (this.layouts.has(`${originalId}-${counter}`)) {
counter++;
}
layout.id = `${originalId}-${counter}`;
}
layout.isBuiltIn = false;
await this.createCustomLayout(layout);
this.emit('layout:imported', layout, filePath);
return layout;
}
catch (error) {
if (error instanceof errors_1.ConfigurationError) {
throw error;
}
throw new errors_1.ConfigurationError(`Failed to import layout from '${filePath}': ${error instanceof Error ? error.message : String(error)}`);
}
}
ensureLayoutsDirectory() {
try {
if (!fs.existsSync(this.layoutsDir)) {
fs.mkdirSync(this.layoutsDir, { recursive: true });
}
}
catch (error) {
throw new errors_1.ConfigurationError(`Failed to create layouts directory: ${error}`);
}
}
loadBuiltInLayouts() {
const defaultLayout = this.createDefaultLayout();
this.layouts.set(defaultLayout.id, defaultLayout);
const compactLayout = this.createCompactLayout();
this.layouts.set(compactLayout.id, compactLayout);
const widescreenLayout = this.createWidescreenLayout();
this.layouts.set(widescreenLayout.id, widescreenLayout);
const singleColumnLayout = this.createSingleColumnLayout();
this.layouts.set(singleColumnLayout.id, singleColumnLayout);
}
async loadCustomLayouts() {
try {
if (!fs.existsSync(this.layoutsDir)) {
return;
}
const files = await fs.promises.readdir(this.layoutsDir);
const layoutFiles = files.filter(file => file.endsWith('.json'));
for (const file of layoutFiles) {
try {
const filePath = path.join(this.layoutsDir, file);
const fileContent = await fs.promises.readFile(filePath, 'utf8');
const layout = JSON.parse(fileContent);
const validation = config_validator_1.ConfigValidator.validateLayoutConfig(layout);
if (validation.valid) {
this.layouts.set(layout.id, layout);
this.customLayouts.set(layout.id, layout);
}
else {
console.warn(`Invalid layout file ${file}: ${validation.errors.join(', ')}`);
}
}
catch (error) {
console.warn(`Failed to load layout file ${file}: ${error}`);
}
}
}
catch (error) {
console.warn(`Failed to load custom layouts: ${error}`);
}
}
async saveCustomLayout(layout) {
const layoutFile = path.join(this.layoutsDir, `${layout.id}.json`);
const layoutJson = JSON.stringify(layout, null, 2);
try {
await fs.promises.writeFile(layoutFile, layoutJson, 'utf8');
}
catch (error) {
throw new errors_1.ConfigurationError(`Failed to save layout '${layout.id}': ${error instanceof Error ? error.message : String(error)}`);
}
}
async applyLayoutToInterface(layout) {
this.emit('layout:interface-update', layout);
const delay = process.env['NODE_ENV'] === 'test' ? 1 : 100;
await new Promise(resolve => setTimeout(resolve, delay));
}
applyResponsiveAdjustments(layout) {
if (!layout.responsive) {
return layout;
}
const adjustedLayout = JSON.parse(JSON.stringify(layout));
const { width, height } = this.currentTerminalSize;
const scaleX = width / layout.minTerminalSize.width;
const scaleY = height / layout.minTerminalSize.height;
adjustedLayout.components.forEach(component => {
component.position.width = Math.floor(component.position.width * scaleX);
component.position.height = Math.floor(component.position.height * scaleY);
component.position.width = Math.max(component.position.width, component.size.minWidth);
component.position.height = Math.max(component.position.height, component.size.minHeight);
if (component.size.maxWidth) {
component.position.width = Math.min(component.position.width, component.size.maxWidth);
}
if (component.size.maxHeight) {
component.position.height = Math.min(component.position.height, component.size.maxHeight);
}
});
return adjustedLayout;
}
componentsOverlap(comp1, comp2) {
const r1 = comp1.position;
const r2 = comp2.position;
return !(r1.x + r1.width <= r2.x ||
r2.x + r2.width <= r1.x ||
r1.y + r1.height <= r2.y ||
r2.y + r2.height <= r1.y);
}
calculateGridUtilization(layout) {
const totalCells = layout.grid.rows * layout.grid.cols;
let usedCells = 0;
layout.components.forEach(component => {
usedCells += component.position.width * component.position.height;
});
return Math.min(100, (usedCells / totalCells) * 100);
}
resolveOverlaps(layout) {
const components = layout.components;
for (let i = 0; i < components.length; i++) {
for (let j = i + 1; j < components.length; j++) {
const componentI = components[i];
const componentJ = components[j];
if (componentI && componentJ && this.componentsOverlap(componentI, componentJ)) {
this.repositionComponent(layout, componentJ, components.slice(0, j));
}
}
}
}
repositionComponent(layout, component, existingComponents) {
const grid = layout.grid;
for (let y = 0; y <= grid.rows - component.position.height; y++) {
for (let x = 0; x <= grid.cols - component.position.width; x++) {
const testPosition = {
...component.position,
x,
y,
};
const testComponent = { ...component, position: testPosition };
if (!existingComponents.some(existing => this.componentsOverlap(testComponent, existing))) {
component.position.x = x;
component.position.y = y;
return;
}
}
}
}
packComponents(layout) {
layout.components.sort((a, b) => {
const aSize = a.position.width * a.position.height;
const bSize = b.position.width * b.position.height;
return bSize - aSize;
});
layout.components.forEach((component, index) => {
const otherComponents = layout.components.slice(0, index);
this.repositionComponent(layout, component, otherComponents);
});
}
createDefaultLayout() {
return {
id: 'default',
name: 'Default',
description: 'Standard monitoring layout with balanced component arrangement',
isBuiltIn: true,
responsive: true,
grid: {
rows: 12,
cols: 12,
cellWidth: 10,
cellHeight: 3,
padding: 1,
},
minTerminalSize: { width: 120, height: 40 },
components: [
{
type: 'stream-metrics',
position: { x: 0, y: 0, width: 8, height: 6 },
size: { minWidth: 40, minHeight: 15 },
config: { priority: 1 },
},
{
type: 'channel-status',
position: { x: 8, y: 0, width: 4, height: 6 },
size: { minWidth: 30, minHeight: 15 },
config: { priority: 2 },
},
{
type: 'system-resources',
position: { x: 0, y: 6, width: 6, height: 6 },
size: { minWidth: 30, minHeight: 15 },
config: { priority: 3 },
},
{
type: 'alert-panel',
position: { x: 6, y: 6, width: 6, height: 6 },
size: { minWidth: 30, minHeight: 15 },
config: { priority: 4 },
},
],
};
}
createCompactLayout() {
return {
id: 'compact',
name: 'Compact',
description: 'Space-efficient layout for smaller terminals',
isBuiltIn: true,
responsive: true,
grid: {
rows: 8,
cols: 8,
cellWidth: 8,
cellHeight: 2,
padding: 0,
},
minTerminalSize: { width: 80, height: 24 },
components: [
{
type: 'stream-metrics',
position: { x: 0, y: 0, width: 4, height: 4 },
size: { minWidth: 30, minHeight: 10 },
config: { priority: 1 },
},
{
type: 'channel-status',
position: { x: 4, y: 0, width: 4, height: 4 },
size: { minWidth: 25, minHeight: 10 },
config: { priority: 2 },
},
{
type: 'system-resources',
position: { x: 0, y: 4, width: 4, height: 4 },
size: { minWidth: 25, minHeight: 10 },
config: { priority: 3 },
},
{
type: 'alert-panel',
position: { x: 4, y: 4, width: 4, height: 4 },
size: { minWidth: 25, minHeight: 10 },
config: { priority: 4 },
},
],
};
}
createWidescreenLayout() {
return {
id: 'widescreen',
name: 'Widescreen',
description: 'Optimized layout for wide terminal displays',
isBuiltIn: true,
responsive: true,
grid: {
rows: 8,
cols: 16,
cellWidth: 12,
cellHeight: 4,
padding: 1,
},
minTerminalSize: { width: 160, height: 40 },
components: [
{
type: 'stream-metrics',
position: { x: 0, y: 0, width: 6, height: 4 },
size: { minWidth: 40, minHeight: 15 },
config: { priority: 1 },
},
{
type: 'channel-status',
position: { x: 6, y: 0, width: 5, height: 4 },
size: { minWidth: 35, minHeight: 15 },
config: { priority: 2 },
},
{
type: 'system-resources',
position: { x: 11, y: 0, width: 5, height: 4 },
size: { minWidth: 35, minHeight: 15 },
config: { priority: 3 },
},
{
type: 'alert-panel',
position: { x: 0, y: 4, width: 16, height: 4 },
size: { minWidth: 80, minHeight: 15 },
config: { priority: 4 },
},
],
};
}
createSingleColumnLayout() {
return {
id: 'single-column',
name: 'Single Column',
description: 'Vertical layout suitable for narrow terminals',
isBuiltIn: true,
responsive: false,
grid: {
rows: 16,
cols: 6,
cellWidth: 12,
cellHeight: 3,
padding: 1,
},
minTerminalSize: { width: 80, height: 60 },
components: [
{
type: 'stream-metrics',
position: { x: 0, y: 0, width: 6, height: 4 },
size: { minWidth: 60, minHeight: 12 },
config: { priority: 1 },
},
{
type: 'channel-status',
position: { x: 0, y: 4, width: 6, height: 4 },
size: { minWidth: 60, minHeight: 12 },
config: { priority: 2 },
},
{
type: 'system-resources',
position: { x: 0, y: 8, width: 6, height: 4 },
size: { minWidth: 60, minHeight: 12 },
config: { priority: 3 },
},
{
type: 'alert-panel',
position: { x: 0, y: 12, width: 6, height: 4 },
size: { minWidth: 60, minHeight: 12 },
config: { priority: 4 },
},
],
};
}
destroy() {
this.removeAllListeners();
this.layouts.clear();
this.customLayouts.clear();
}
}
exports.LayoutManager = LayoutManager;
exports.layoutManager = new LayoutManager();
//# sourceMappingURL=layout-manager.js.map