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>
This commit is contained in:
@@ -0,0 +1,325 @@
|
||||
<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. Ctrl+V=coller 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>
|
||||
Reference in New Issue
Block a user