use super::client_js::live_client_js;
use super::escape::{esc, quote_js};
const PAGE_CSS: &str = r#"
:root { color-scheme: dark light; font-family: ui-sans-serif, system-ui, sans-serif; line-height: 1.45; }
html, body { box-sizing: border-box; width: 100%; margin: 0; }
body { padding: 1rem 1.25rem; max-width: none; }
h1 { font-size: 1.25rem; font-weight: 600; }
h2 { font-size: 1.05rem; font-weight: 600; margin: 1.5rem 0 0.25rem; }
.dim { opacity: 0.65; }
.images-controls {
display: flex; gap: 0.75rem; align-items: center; flex-wrap: wrap;
margin: 0 0 1.25rem; font-size: 0.85rem;
}
.images-controls button {
font: inherit; color: #7ab4ff; background: none; border: 1px solid #8886;
border-radius: 5px; padding: 0.15rem 0.6rem; cursor: pointer;
}
.images-controls button:hover:not(:disabled) { border-color: #7ab4ff; }
.images-controls button:disabled { opacity: 0.45; cursor: default; }
.images-controls label { cursor: pointer; user-select: none; }
.image-select-cell { width: 1.5rem; }
.daemon-status {
display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap;
font-size: 0.85rem; margin-bottom: 1rem; padding: 0.35rem 0.6rem;
border: 1px solid #8884; border-radius: 6px;
}
.daemon-status .dot { width: 0.55rem; height: 0.55rem; border-radius: 50%; background: #888; flex-shrink: 0; }
.daemon-status.live .dot { background: #3a8; }
.daemon-status.down .dot { background: #e55; }
.daemon-status.connecting .dot { background: #6af; }
.daemon-status.connecting { color: #6af; }
.table-scroll { overflow-x: auto; width: 100%; margin-bottom: 1rem; }
table { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
thead tr, tbody tr { border-bottom: 1px solid #8884; }
th, td { text-align: left; padding: 0.35rem 0.5rem; vertical-align: top; }
.session-status {
display: inline-block; font-size: 0.75rem; font-weight: 600;
padding: 0.12rem 0.45rem; border-radius: 4px;
}
.session-status.running { color: #6af; background: #6af22; }
.session-status.completed { color: #3a8; background: #3a822; }
.session-status.failed { color: #e55; background: #e5522; }
.session-status.cancelled { color: #ca6; background: #ca622; }
.session-status.terminated { color: #e55; background: #e5518; }
a { color: inherit; }
code { font-size: 0.85em; }
ul.skills { margin: 0.5rem 0 1rem; padding-left: 1.25rem; font-size: 0.9rem; }
.procs { margin-top: 1rem; width: 100%; }
details.proc { border: 1px solid #8884; border-radius: 6px; margin-bottom: 0.5rem; padding: 0.25rem 0.5rem; }
details.proc[open] { background: #8881; }
summary { cursor: pointer; list-style: none; display: flex; gap: 0.5rem; align-items: baseline; flex-wrap: wrap; }
summary::-webkit-details-marker { display: none; }
summary .triangle {
flex-shrink: 0; width: 0.85rem; text-align: center; font-size: 0.65rem;
line-height: 1; opacity: 0.75; align-self: center;
}
details.proc:not([open]) summary .triangle::before { content: '▶'; }
details.proc[open] summary .triangle::before { content: '▼'; }
.glyph { font-weight: 600; }
.fail .glyph { color: #e55; }
.ok .glyph { color: #3a8; }
.running .glyph { color: #6af; }
.proc-meta { font-size: 0.85rem; margin: 0.35rem 0 0.5rem; display: flex; flex-wrap: wrap; gap: 0.35rem 0.75rem; }
.proc-meta strong { font-weight: 600; margin-right: 0.25rem; }
.proc-stat { font-size: 0.8rem; opacity: 0.75; }
.proc-stat .idle { opacity: 0.85; }
.autoscroll-ctl {
display: block; font-size: 0.8rem; margin: 0.35rem 0 0.25rem;
cursor: pointer; user-select: none;
}
.autoscroll-ctl input { margin-right: 0.35rem; }
.output {
font-family: ui-monospace, monospace; font-size: 0.8rem; max-height: 24rem;
overflow: auto; margin: 0.5rem 0; padding: 0.5rem; background: #0002;
border-radius: 4px; width: 100%; box-sizing: border-box;
}
.line { white-space: pre; }
.detail, .container { overflow-x: auto; white-space: pre; max-width: 100%; }
.at { opacity: 0.5; margin-right: 0.35rem; }
.cast { position: relative; margin: 0.5rem 0; border: 1px solid #8884; border-radius: 6px; overflow: hidden; background: #000; }
.cast-toolbar {
display: flex; gap: 0.4rem; align-items: center; flex-wrap: wrap;
padding: 0.3rem 0.5rem; background: #1118; font-size: 0.8rem;
}
.cast-toolbar button, .cast-toolbar a {
font: inherit; color: #7ab4ff; background: none; border: 1px solid #8886;
border-radius: 5px; padding: 0.1rem 0.5rem; cursor: pointer; text-decoration: none;
}
.cast-toolbar button:hover, .cast-toolbar a:hover { border-color: #7ab4ff; }
.cast-toolbar button.on { border-color: #ff6a6a; color: #ff6a6a; }
.cast-copied { color: #3a8; visibility: hidden; }
.cast-keys { margin-left: auto; font-size: 0.72rem; }
.cast-summary {
padding: 0.4rem 0.6rem; font-size: 0.9rem; line-height: 1.4;
background: #1114; border-bottom: 1px solid #8883;
}
.cast-chapters { display: flex; flex-wrap: wrap; gap: 0.35rem; padding: 0.35rem 0.5rem; background: #1116; }
.cast-chapters button {
font: inherit; font-size: 0.78rem; color: #cdd; background: #2a2d36aa;
border: 1px solid #8884; border-radius: 5px; padding: 0.1rem 0.5rem; cursor: pointer;
}
.cast-chapters button:hover { border-color: #7ab4ff; color: #fff; }
.cast-player { width: 100%; height: 42vh; max-height: 460px; }
.cast-placeholder { padding: 1.5rem 1rem; color: #cdd; }
.cast-grew {
display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap;
padding: 0.3rem 0.5rem; background: #1116; font-size: 0.8rem; color: #cdd;
}
.cast-grew button {
font: inherit; color: #7ab4ff; background: none; border: 1px solid #8886;
border-radius: 5px; padding: 0.1rem 0.5rem; cursor: pointer;
}
.cast-grew button:hover { border-color: #7ab4ff; }
/* Brief fading chapter-name toast, bottom-centre of the player (screen, in fullscreen). */
.cast-toast {
position: absolute; left: 50%; bottom: 12%; transform: translateX(-50%);
max-width: 80%; padding: 0.4rem 0.9rem; border-radius: 7px; z-index: 5;
background: #000c; color: #fff; font-size: 1rem; white-space: nowrap;
overflow: hidden; text-overflow: ellipsis; pointer-events: none;
opacity: 0; transition: opacity 0.6s ease;
}
.cast-toast.show { opacity: 1; transition: opacity 0.12s ease; }
/* Fullscreen: the player fills the viewport (asciinema-player fit:'both' fits both ways).
When the terminal leaves horizontal room, `.has-side` reveals a chapters column. */
.cast:fullscreen {
display: grid; background: #000;
grid-template-columns: 1fr 0; grid-template-rows: auto 1fr;
}
.cast:fullscreen.has-side { grid-template-columns: 1fr var(--side-w, 360px); }
.cast:fullscreen .cast-toolbar { grid-column: 1 / -1; }
.cast:fullscreen .cast-summary, .cast:fullscreen .cast-chapters { display: none; }
.cast:fullscreen .cast-player { grid-column: 1; grid-row: 2; height: auto; max-height: none; min-height: 0; }
.cast-fs-chapters { display: none; }
.cast:fullscreen.has-side .cast-fs-chapters {
display: block; grid-column: 2; grid-row: 2; overflow-y: auto;
background: #14161c; border-left: 1px solid #333; padding: 0.6rem;
}
.cast-fs-chapters .fs-summary { font-size: 0.9rem; line-height: 1.45; margin-bottom: 0.8rem; opacity: 0.9; }
.cast-fs-chapters .fs-head { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.6; margin-bottom: 0.4rem; }
.cast-fs-chapters button {
display: block; width: 100%; text-align: left; font: inherit; font-size: 0.85rem;
color: #cdd; background: none; border: 0; border-radius: 5px; padding: 0.35rem 0.5rem; cursor: pointer;
}
.cast-fs-chapters button:hover { background: #2a2d36; color: #fff; }
.cast-fs-chapters .fs-t { color: #7ab4ff; margin-right: 0.4rem; font-variant-numeric: tabular-nums; }
.cast .ap-player { width: 100%; height: 100%; }
.permalink { margin-top: 1.5rem; font-size: 0.9rem; }
.session-export {
display: inline-block; font-size: 0.85rem; color: #7ab4ff; background: none;
border: 1px solid #8886; border-radius: 5px; padding: 0.15rem 0.6rem;
text-decoration: none; margin: 0 0 0.75rem;
}
.session-export:hover { border-color: #7ab4ff; }
.session-meta {
font-size: 0.9rem; margin: 0.75rem 0 1rem; display: grid;
grid-template-columns: max-content minmax(0, 1fr); gap: 0.2rem 1rem;
align-items: baseline; width: 100%;
}
.session-meta dt { font-weight: 600; opacity: 0.85; }
.session-meta dd { margin: 0; min-width: 0; }
.session-meta .repo-path { font-size: 0.85em; white-space: pre; overflow-x: auto; display: block; max-width: 100%; }
td.repo-path { font-size: 0.85em; white-space: nowrap; overflow-x: auto; max-width: 28rem; }
"#;
fn scsh_version_html() -> String {
let v = crate::version::pkg_version();
let git = crate::version::git_stamp();
if git.is_empty() {
format!("<span id=\"status-scsh-version\" class=\"dim\">scsh {}</span>", esc(v))
} else {
format!("<span id=\"status-scsh-version\" class=\"dim\">scsh {} · <code>{}</code></span>", esc(v), esc(&git))
}
}
pub(crate) const FAVICON_LINK: &str = "<link rel=\"icon\" href=\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='%23181a20'/%3E%3Ctext x='3.5' y='12.5' font-size='11' fill='%2333aa88'%3E%E2%9D%AF%3C/text%3E%3C/svg%3E\">";
pub(crate) fn wrap_page(title: &str, port: u16, session_id: Option<&str>, body: &str) -> String {
let session_js = match session_id {
Some(id) => format!("const SESSION_ID = {};", quote_js(id)),
None => "const SESSION_ID = null;".to_string(),
};
let (player_css, player_js) = if session_id.is_some() {
(
"<link rel=\"stylesheet\" href=\"/assets/asciinema-player.css\">",
"<script src=\"/assets/asciinema-player.js\"></script>",
)
} else {
("", "")
};
format!(
r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
{favicon}
<title>{title}</title>
{player_css}
<style>{css}</style>
</head>
<body>
<div id="daemon-status" class="daemon-status connecting">
<span class="dot" aria-hidden="true"></span><span id="status-label">connecting…</span>
<span id="status-uptime" class="dim"></span>{scsh_version}</div>
{body}
{player_js}
<script>
const WS_PORT = {port};
{session_js}
{live_js}
</script>
</body>
</html>
"#,
title = esc(title),
favicon = FAVICON_LINK,
player_css = player_css,
css = PAGE_CSS,
scsh_version = scsh_version_html(),
body = body,
player_js = player_js,
port = port,
session_js = session_js,
live_js = live_client_js()
)
}