Files
Claude Code Bot 830cb5d679 Mode operatoire : session Claude Code persistante dans un navigateur
Stack ttyd + tmux + Caddy, reproduction d'une installation en production.
Inclut le frontend personnalise et les correctifs deja valides (FitAddon,
refit apres changement de police, double collage Ctrl+V, OSC 52, window.open).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 07:28:02 +00:00

325 lines
16 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<style>
#ttyd-bar{position:fixed;top:0;left:0;right:0;height:30px;background:#111;display:flex;align-items:center;padding:0 6px;gap:4px;z-index:8000;border-bottom:1px solid #2a2a2a;}
#ttyd-bar button{background:#222;color:#aaa;border:1px solid #333;border-radius:3px;padding:1px 7px;cursor:pointer;font:11px monospace;white-space:nowrap;}
#ttyd-bar button:hover{background:#333;color:#fff;}
#ttyd-bar select{background:#222;color:#aaa;border:1px solid #333;border-radius:3px;padding:1px 4px;font:11px monospace;cursor:pointer;}
#ttyd-conn{width:8px;height:8px;border-radius:50%;background:#555;margin:0 4px;flex-shrink:0;}
#ttyd-conn.ok{background:#4caf50;}
#ttyd-conn.err{background:#f44336;animation:blink 1s infinite;}
@keyframes blink{0%,100%{opacity:1}50%{opacity:.3}}
#ttyd-spacer{flex:1;}
#ttyd-search-bar{position:fixed;top:30px;right:0;background:#1a1a1a;border:1px solid #333;border-top:none;border-right:none;border-radius:0 0 0 6px;display:flex;align-items:center;gap:4px;padding:4px 8px;z-index:8001;display:none;}
#ttyd-search-bar input{background:#111;color:#ddd;border:1px solid #333;border-radius:3px;padding:2px 6px;font:12px monospace;width:200px;outline:none;}
#ttyd-search-bar span{color:#666;font:11px monospace;min-width:60px;}
#ttyd-search-bar button{background:#222;color:#aaa;border:1px solid #333;border-radius:3px;padding:1px 7px;cursor:pointer;font:11px monospace;}
#terminal-container{position:absolute !important;top:30px;left:0;right:0;bottom:0;height:auto !important;padding-top:0 !important;}
#ttyd-ctx{position:fixed;background:#1e1e1e;border:1px solid #444;border-radius:4px;padding:4px 0;z-index:9999;min-width:150px;box-shadow:0 4px 12px rgba(0,0,0,.5);display:none;}
#ttyd-ctx div{padding:5px 14px;cursor:pointer;color:#ccc;font:12px monospace;}
#ttyd-ctx div:hover{background:#2d2d2d;color:#fff;}
#ttyd-ctx hr{border:none;border-top:1px solid #333;margin:3px 0;}
#ttyd-toast{position:fixed;bottom:8px;right:8px;background:rgba(20,20,20,.93);color:#7fff7f;font:12px monospace;padding:4px 10px;border-radius:4px;z-index:9999;pointer-events:none;opacity:0;transition:opacity .2s;}
#ttyd-toast.show{opacity:1;}
</style>
<div id="ttyd-bar">
<div id="ttyd-conn" title="Connexion WebSocket"></div>
<button onclick="ttydFontSize(-1)" title="Police -">A</button>
<span id="ttyd-fsize" style="color:#555;font:11px monospace;min-width:28px;text-align:center;">13</span>
<button onclick="ttydFontSize(+1)" title="Police +">A+</button>
<button onclick="ttydClear()" title="Vider">✕ Clear</button>
<button onclick="ttydSearch()" title="Rechercher (Ctrl+F)">⌕ Rech.</button>
<select id="ttyd-theme-sel" onchange="ttydSetTheme(this.value)" title="Thème"></select>
<button onclick="ttydSplit('h')" title="Split vertical (côte à côte)">⬜ ⬜</button>
<button onclick="ttydSplit('v')" title="Split horizontal (haut/bas)"><br style='line-height:3px'></button>
<button onclick="ttydPaneClose()" title="Fermer ce panneau">✕ Pan.</button>
<button onclick="ttydPaneNav('Left')" title="Panneau gauche"></button>
<button onclick="ttydPaneNav('Right')" title="Panneau droite"></button>
<button onclick="ttydPaneNav('Up')" title="Panneau haut"></button>
<button onclick="ttydPaneNav('Down')" title="Panneau bas"></button>
<div id="ttyd-spacer"></div>
<span style="color:#333;font:10px monospace;">Ctrl+C=copier/inter. &nbsp; Ctrl+V=coller &nbsp; clic droit=menu</span>
</div>
<div id="ttyd-search-bar">
<input id="ttyd-sq" placeholder="Rechercher…" oninput="ttydDoSearch()" onkeydown="ttydSearchKey(event)">
<span id="ttyd-sr"></span>
<button onclick="ttydSearchNav(-1)"></button>
<button onclick="ttydSearchNav(+1)"></button>
<button onclick="ttydSearchClose()"></button>
</div>
<div id="ttyd-ctx"></div>
<div id="ttyd-toast"></div>
<script>
(function(){
var _fontSize = parseInt(localStorage.getItem('ttyd-font') || '13');
var _themes = {"Defaut": {"background": "#2b2b2b", "foreground": "#d2d2d2", "cursor": "#adadad", "black": "#000000", "red": "#d81e00", "green": "#5ea702", "yellow": "#cfae00", "blue": "#427ab3", "magenta": "#89658e", "cyan": "#00a7aa", "white": "#dbded8", "brightBlack": "#686a66", "brightRed": "#f54235", "brightGreen": "#99e343", "brightYellow": "#fdeb61", "brightBlue": "#84b0d8", "brightMagenta": "#bc94b7", "brightCyan": "#37e6e8", "brightWhite": "#f1f1f0"}, "Dracula": {"background": "#282a36", "foreground": "#f8f8f2", "cursor": "#f8f8f2", "black": "#21222c", "red": "#ff5555", "green": "#50fa7b", "yellow": "#f1fa8c", "blue": "#bd93f9", "magenta": "#ff79c6", "cyan": "#8be9fd", "white": "#f8f8f2", "brightBlack": "#6272a4", "brightRed": "#ff6e6e", "brightGreen": "#69ff94", "brightYellow": "#ffffa5", "brightBlue": "#d6acff", "brightMagenta": "#ff92df", "brightCyan": "#a4ffff", "brightWhite": "#ffffff"}, "One Dark": {"background": "#282c34", "foreground": "#abb2bf", "cursor": "#528bff", "black": "#3f4451", "red": "#e06c75", "green": "#98c379", "yellow": "#e5c07b", "blue": "#61afef", "magenta": "#c678dd", "cyan": "#56b6c2", "white": "#abb2bf", "brightBlack": "#4f5666", "brightRed": "#ff616e", "brightGreen": "#a5e075", "brightYellow": "#f0a45d", "brightBlue": "#4dc4ff", "brightMagenta": "#de73ff", "brightCyan": "#4cd1e0", "brightWhite": "#ffffff"}, "Solarized": {"background": "#002b36", "foreground": "#839496", "cursor": "#839496", "black": "#073642", "red": "#dc322f", "green": "#859900", "yellow": "#b58900", "blue": "#268bd2", "magenta": "#d33682", "cyan": "#2aa198", "white": "#eee8d5", "brightBlack": "#002b36", "brightRed": "#cb4b16", "brightGreen": "#586e75", "brightYellow": "#657b83", "brightBlue": "#839496", "brightMagenta": "#6c71c4", "brightCyan": "#93a1a1", "brightWhite": "#fdf6e3"}, "Clair": {"background": "#ffffff", "foreground": "#383a42", "cursor": "#526fff", "black": "#383a42", "red": "#e45649", "green": "#50a14f", "yellow": "#c18401", "blue": "#4078f2", "magenta": "#a626a4", "cyan": "#0184bc", "white": "#fafafa", "brightBlack": "#4f525e", "brightRed": "#e06c75", "brightGreen": "#98c379", "brightYellow": "#e5c07b", "brightBlue": "#61afef", "brightMagenta": "#c678dd", "brightCyan": "#56b6c2", "brightWhite": "#ffffff"}};
var _themeName = localStorage.getItem('ttyd-theme') || 'Defaut';
var _searchMatches = [], _searchIdx = 0;
var _notifPerm = false;
var _lastCmdStart = 0;
/* ---- Toast ---- */
function toast(msg, dur) {
var t = document.getElementById('ttyd-toast');
t.textContent = msg; t.classList.add('show');
clearTimeout(t._tid);
t._tid = setTimeout(function(){ t.classList.remove('show'); }, dur||1800);
}
/* ---- Clipboard ---- */
function writeClip(text) {
if (!text) return;
var ok = function(){ toast('Copié : ' + text.slice(0,40).replace(/\n/g,' ') + (text.length>40?'..':'')); };
if (navigator.clipboard && navigator.clipboard.writeText)
navigator.clipboard.writeText(text).then(ok).catch(function(){ legacyCopy(text); });
else legacyCopy(text);
}
function legacyCopy(text) {
var ta = document.createElement('textarea');
ta.value = text; ta.style.cssText='position:fixed;top:0;left:0;width:1px;height:1px;opacity:0;';
document.body.appendChild(ta); ta.focus(); ta.select();
var ok=false; try{ ok=document.execCommand('copy'); }catch(e){}
document.body.removeChild(ta);
toast(ok ? 'Copié (legacy)' : 'Erreur copie');
}
function readClip(cb) {
if (navigator.clipboard && navigator.clipboard.readText)
navigator.clipboard.readText().then(cb).catch(function(){ toast('Accès presse-papier refusé'); });
else toast('API clipboard non disponible');
}
/* ---- Police ---- */
window.ttydFontSize = function(d) {
_fontSize = Math.max(8, Math.min(28, _fontSize + d));
localStorage.setItem('ttyd-font', _fontSize);
if (window.term) { window.term.options.fontSize = _fontSize; window.dispatchEvent(new Event('resize')); }
document.getElementById('ttyd-fsize').textContent = _fontSize;
};
/* ---- Clear ---- */
window.ttydClear = function() { if (window.term) window.term.clear(); };
/* ---- Tmux split & navigation ---- */
function sendRaw(data) {
// Envoie données brutes au PTY sans bracketed paste
var term = window.term;
if (!term) return;
if (term._core && term._core.coreService)
term._core.coreService.triggerDataEvent(data, true);
else
term.paste(data);
}
window.ttydSplit = function(dir) {
// Ctrl+B (\x02) + % pour vertical, + " pour horizontal
sendRaw('\x02' + (dir === 'h' ? '%' : '"'));
};
window.ttydPaneClose = function() {
// Ctrl+B + x, puis y pour confirmer
sendRaw('\x02x');
setTimeout(function(){ sendRaw('y'); }, 200);
};
window.ttydPaneNav = function(dir) {
var arrows = {Left:'\x1b[D', Right:'\x1b[C', Up:'\x1b[A', Down:'\x1b[B'};
if (arrows[dir]) sendRaw('\x02' + arrows[dir]);
};
/* ---- Thèmes ---- */
var sel = document.getElementById('ttyd-theme-sel');
Object.keys(_themes).forEach(function(n){
var o = document.createElement('option'); o.value=n; o.textContent=n;
if(n===_themeName) o.selected=true;
sel.appendChild(o);
});
window.ttydSetTheme = function(name) {
if (!_themes[name] || !window.term) return;
window.term.options.theme = _themes[name];
_themeName = name;
localStorage.setItem('ttyd-theme', name);
toast('Thème : ' + name);
};
/* ---- Recherche ---- */
var _searchOpen = false;
window.ttydSearch = function() {
var bar = document.getElementById('ttyd-search-bar');
_searchOpen = !_searchOpen;
bar.style.display = _searchOpen ? 'flex' : 'none';
if (_searchOpen) setTimeout(function(){ document.getElementById('ttyd-sq').focus(); }, 50);
};
window.ttydSearchClose = function() {
_searchOpen = false;
document.getElementById('ttyd-search-bar').style.display = 'none';
_searchMatches = []; document.getElementById('ttyd-sr').textContent = '—';
};
window.ttydDoSearch = function() {
var q = document.getElementById('ttyd-sq').value;
var sr = document.getElementById('ttyd-sr');
if (!q || !window.term) { _searchMatches=[]; sr.textContent='—'; return; }
var buf = window.term.buffer.active;
var matches = [];
var total = buf.length;
for (var i = 0; i < total; i++) {
var line = buf.getLine(i);
if (line && line.translateToString().indexOf(q) >= 0) matches.push(i);
}
_searchMatches = matches; _searchIdx = matches.length - 1;
sr.textContent = matches.length ? ((_searchIdx+1)+'/'+matches.length) : '0 résultat';
if (matches.length) window.term.scrollToLine(matches[_searchIdx]);
};
window.ttydSearchNav = function(dir) {
if (!_searchMatches.length) return;
_searchIdx = (_searchIdx + dir + _searchMatches.length) % _searchMatches.length;
document.getElementById('ttyd-sr').textContent = (_searchIdx+1)+'/'+_searchMatches.length;
window.term.scrollToLine(_searchMatches[_searchIdx]);
};
window.ttydSearchKey = function(e) {
if (e.key === 'Enter') ttydSearchNav(e.shiftKey ? -1 : 1);
if (e.key === 'Escape') ttydSearchClose();
};
/* ---- Connexion ---- */
var _connEl = document.getElementById('ttyd-conn');
function checkConn() {
fetch('/token', {cache:'no-store'})
.then(function(r){ _connEl.className = r.ok ? 'ok' : 'err'; })
.catch(function(){ _connEl.className = 'err'; });
}
checkConn();
setInterval(checkConn, 5000);
/* ---- Patch window.open (liens WebLinksAddon) ---- */
var _open = window.open;
window.open = function(url, target, features) {
if (url === undefined || url === null || url === '') {
var loc = {};
Object.defineProperty(loc,'href',{set:function(u){
var a=document.createElement('a'); a.href=u; a.target='_blank'; a.rel='noopener noreferrer';
document.body.appendChild(a); a.click(); document.body.removeChild(a);
}});
return {opener:null, location:loc};
}
return _open.apply(this, arguments);
};
/* ---- Menu contextuel ---- */
var _ctx = document.getElementById('ttyd-ctx');
function hideCtx(){ _ctx.style.display='none'; }
document.addEventListener('click', hideCtx);
document.addEventListener('keydown', function(e){ if(e.key==='Escape'){ hideCtx(); ttydSearchClose(); }});
function showCtx(x,y) {
hideCtx();
_ctx.innerHTML = '';
var items = [
['Copier', function(){ if(window.term){var s=window.term.getSelection();if(s)writeClip(s);} }],
['Coller', function(){ if(window.term) readClip(function(t){window.term.paste(t);}); }],
null,
['Rechercher…', ttydSearch],
null,
['Effacer', ttydClear],
['Police +', function(){ttydFontSize(+1);}],
['Police ', function(){ttydFontSize(-1);}],
];
items.forEach(function(item){
if(!item){var hr=document.createElement('hr');_ctx.appendChild(hr);return;}
var d=document.createElement('div'); d.textContent=item[0];
d.addEventListener('click',function(e){e.stopPropagation();hideCtx();item[1]();});
_ctx.appendChild(d);
});
_ctx.style.cssText='display:block;left:'+x+'px;top:'+y+'px;';
var r=_ctx.getBoundingClientRect();
if(r.right>window.innerWidth) _ctx.style.left=(x-r.width)+'px';
if(r.bottom>window.innerHeight) _ctx.style.top=(y-r.height)+'px';
}
/* ---- Notifications ---- */
function requestNotifPerm() {
if ('Notification' in window && Notification.permission === 'default')
Notification.requestPermission().then(function(p){ _notifPerm = p==='granted'; });
else _notifPerm = ('Notification' in window && Notification.permission === 'granted');
}
function desktopNotif(title, body) {
if (_notifPerm && 'Notification' in window)
new Notification(title, {body:body, icon:''});
}
requestNotifPerm();
/* ---- Initialisation terminal ---- */
function waitForTerm() {
var term = window.term;
if (!term || !term.parser) { setTimeout(waitForTerm, 100); return; }
/* Police initiale */
term.options.fontSize = _fontSize;
document.getElementById('ttyd-fsize').textContent = _fontSize;
/* Thème initial */
if (_themes[_themeName]) term.options.theme = _themes[_themeName];
/* Refit : le fit initial a ete calcule avec l'ancienne taille de cellule */
setTimeout(function(){ window.dispatchEvent(new Event('resize')); }, 50);
/* OSC 52 : tmux set-clipboard -> presse-papier */
try {
term.parser.registerOscHandler(52, function(data) {
var parts = data.split(';');
if (parts.length>=2 && parts[1] && parts[1]!=='?') {
try { writeClip(atob(parts[1])); } catch(e){}
}
return true;
});
} catch(e){}
/* OSC 777 : PROMPT_COMMAND -> notification fin de commande */
try {
term.parser.registerOscHandler(777, function(data) {
var code = parseInt(data) || 0;
var elapsed = Date.now() - _lastCmdStart;
/* Notifier si onglet caché ET commande > 5s */
if (document.hidden && elapsed > 5000) {
var msg = code === 0 ? 'Commande terminée (succès)' : 'Commande terminée (code '+code+')';
desktopNotif('Terminal', msg);
}
_lastCmdStart = Date.now();
return true;
});
} catch(e){}
/* Ctrl+C = copie si sélection, sinon SIGINT */
/* Ctrl+V = colle */
/* Ctrl+F = recherche */
term.attachCustomKeyEventHandler(function(e) {
if (e.type !== 'keydown') return true;
if (e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey) {
if (e.code==='KeyC') {
var s = term.getSelection ? term.getSelection() : '';
if (s) { writeClip(s); return false; }
return true;
}
if (e.code==='KeyV') {
e.preventDefault();
readClip(function(t){ term.paste(t); }); return false;
}
if (e.code==='KeyF') { ttydSearch(); return false; }
}
if (e.ctrlKey && e.shiftKey) {
if (e.code==='KeyC') { var s=term.getSelection?term.getSelection():''; if(s) writeClip(s); return false; }
if (e.code==='KeyV') { e.preventDefault(); readClip(function(t){ term.paste(t); }); return false; }
}
return true;
});
/* Clic droit -> menu contextuel */
term.element.addEventListener('contextmenu', function(e){
e.preventDefault(); showCtx(e.clientX, e.clientY);
});
/* Zoom souris */
term.element.addEventListener('wheel', function(e){
if (!e.ctrlKey) return;
e.preventDefault();
ttydFontSize(e.deltaY < 0 ? 1 : -1);
}, {passive:false});
}
waitForTerm();
})();
</script>