UNPKG

polyv-live-cli

Version:

CLI tool for managing PolyV live streaming services.

596 lines 24.7 kB
"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