feat(displays): add display detection and management (sway) with daemon APIs and UI controls

This commit is contained in:
2026-01-09 18:14:26 +00:00
parent ee631c21c4
commit 06cea4bb37
8 changed files with 239 additions and 3 deletions

View File

@@ -129,6 +129,28 @@ export class UIServer {
}
}
if (path === '/api/displays') {
const displays = await this.daemon.getDisplays();
return new Response(JSON.stringify({ displays }), { headers });
}
// Display control endpoints: /api/displays/{name}/{action}
const displayMatch = path.match(/^\/api\/displays\/([^/]+)\/(enable|disable|primary)$/);
if (displayMatch && req.method === 'POST') {
const name = decodeURIComponent(displayMatch[1]);
const action = displayMatch[2];
let result;
if (action === 'enable') {
result = await this.daemon.setDisplayEnabled(name, true);
} else if (action === 'disable') {
result = await this.daemon.setDisplayEnabled(name, false);
} else if (action === 'primary') {
result = await this.daemon.setKioskDisplay(name);
}
return new Response(JSON.stringify(result), { headers });
}
return new Response(JSON.stringify({ error: 'Not Found' }), {
status: 404,
headers,
@@ -384,6 +406,10 @@ export class UIServer {
Check for Updates
</button>
</div>
<div class="card">
<h2>Displays</h2>
<div id="displays-list"></div>
</div>
<div class="card">
<h2>Input Devices</h2>
<div id="input-devices-list"></div>
@@ -698,6 +724,64 @@ export class UIServer {
fetchUpdates();
setInterval(fetchUpdates, 60000); // Check every minute
// Display management
function updateDisplaysUI(data) {
const list = document.getElementById('displays-list');
if (!data.displays || data.displays.length === 0) {
list.innerHTML = '<div style="color: var(--text-dim);">No displays detected</div>';
return;
}
list.innerHTML = data.displays.map(d =>
'<div class="device-item" style="flex-wrap: wrap; gap: 8px;">' +
'<div style="flex: 1; min-width: 150px;">' +
'<div class="device-name">' + d.name + '</div>' +
'<div style="font-size: 11px; color: var(--text-dim);">' +
d.width + 'x' + d.height + ' @ ' + d.refreshRate + 'Hz' +
(d.make !== 'Unknown' ? ' • ' + d.make : '') +
'</div>' +
'</div>' +
'<div style="display: flex; gap: 4px;">' +
(d.isPrimary
? '<span class="device-default">Primary</span>'
: '<button class="btn btn-primary" style="padding: 2px 8px; margin: 0; font-size: 11px;" onclick="setKioskDisplay(\\'' + d.name + '\\')">Set Primary</button>') +
'<button class="btn ' + (d.active ? 'btn-danger' : 'btn-primary') + '" style="padding: 2px 8px; margin: 0; font-size: 11px;" onclick="toggleDisplay(\\'' + d.name + '\\', ' + !d.active + ')">' +
(d.active ? 'Disable' : 'Enable') +
'</button>' +
'</div>' +
'</div>'
).join('');
}
function fetchDisplays() {
fetch('/api/displays')
.then(r => r.json())
.then(updateDisplaysUI)
.catch(err => console.error('Failed to fetch displays:', err));
}
function toggleDisplay(name, enable) {
fetch('/api/displays/' + encodeURIComponent(name) + '/' + (enable ? 'enable' : 'disable'), { method: 'POST' })
.then(r => r.json())
.then(result => {
if (!result.success) alert(result.message);
fetchDisplays();
})
.catch(err => alert('Error: ' + err));
}
function setKioskDisplay(name) {
fetch('/api/displays/' + encodeURIComponent(name) + '/primary', { method: 'POST' })
.then(r => r.json())
.then(result => {
if (!result.success) alert(result.message);
fetchDisplays();
})
.catch(err => alert('Error: ' + err));
}
fetchDisplays();
setInterval(fetchDisplays, 5000); // Refresh every 5 seconds
// Initial fetch
fetch('/api/status')
.then(r => r.json())