tmux-weblink 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- const O="https://esm.sh/ghostty-web@0.4.0";let m;function q(){return m||(document.querySelector("link[data-tmux-web-xterm-css]")?m=Promise.resolve():(m=new Promise(t=>{const n=document.createElement("link");n.rel="stylesheet",n.href="/assets/xterm.css",n.setAttribute("data-tmux-web-xterm-css",""),n.addEventListener("load",()=>t(),{once:!0}),n.addEventListener("error",()=>t(),{once:!0}),document.head.appendChild(n)}),m))}class B{terminal;fitAddon;container;constructor(t,n,r){this.container=t,this.terminal=n,this.fitAddon=r,this.terminal.loadAddon(this.fitAddon),this.terminal.open(t)}static async create(t,n,r){const[{Terminal:a},{FitAddon:f}]=await Promise.all([import("@xterm/xterm"),import("@xterm/addon-fit"),q()]),b=new a({fontSize:14,fontFamily:"'JetBrains Mono', 'SF Mono', 'Menlo', monospace",cursorBlink:!0,cursorStyle:"bar",scrollback:n,convertEol:!1,theme:r});return new B(t,b,new f)}get cols(){return this.terminal.cols}get rows(){return this.terminal.rows}write(t){return new Promise(n=>this.terminal.write(t,n))}reset(){this.terminal.reset()}scrollToBottom(){this.terminal.scrollToBottom()}scrollToLine(t){this.terminal.scrollToLine(Math.max(0,t))}isNearScrollbackTop(){return this.terminal.buffer.active.viewportY<=1}viewportY(){return this.terminal.buffer.active.viewportY}baseY(){return this.terminal.buffer.active.baseY}fit(){const t=R(this.container);return t.width<=0||t.height<=0?!1:(this.fitAddon.fit(),!0)}focus(){this.terminal.focus()}pasteText(t){this.terminal.paste(t)}input(t){this.terminal.input(t,!1)}onData(t){this.terminal.onData(t),this.terminal.onBinary(t)}onResize(t){this.terminal.onResize(t)}onScroll(t){this.terminal.onScroll(t)}attachCustomKeyEventHandler(t){this.terminal.attachCustomKeyEventHandler(t)}isFocused(){const t=document.activeElement;return!!t&&(t===this.container||this.container.contains(t))}}class F{terminal;container;colsValue=80;rowsValue=24;charW=0;charH=0;resizeCallbacks=[];constructor(t,n){this.container=t,this.terminal=n,this.terminal.open(t)}static async create(t,n,r){const a=await import(O);await a.init();const f=new a.Terminal({fontSize:14,fontFamily:"'JetBrains Mono', 'SF Mono', 'Menlo', monospace",cursorBlink:!0,cursorStyle:"bar",scrollback:n,convertEol:!1,theme:r});return new F(t,f)}get cols(){return this.colsValue}get rows(){return this.rowsValue}write(t){return new Promise(n=>this.terminal.write(t,n))}reset(){this.terminal.reset()}scrollToBottom(){this.terminal.scrollToBottom?.()}scrollToLine(t){this.terminal.scrollToLine?.(Math.max(0,t))}isNearScrollbackTop(){const t=this.terminal.buffer?.active;return t?t.viewportY>=t.baseY:!1}viewportY(){return this.terminal.buffer?.active?.viewportY??0}baseY(){return this.terminal.buffer?.active?.baseY??0}fit(){const t=R(this.container);if(t.width<=0||t.height<=0)return!1;this.updateCellMetrics(!this.charW||!this.charH);const n=Math.floor(t.width/this.charW),r=Math.floor(t.height/this.charH);if(n<10||r<5)return!1;if(n!==this.colsValue||r!==this.rowsValue){this.colsValue=n,this.rowsValue=r,this.terminal.resize(n,r);for(const a of this.resizeCallbacks)a({cols:n,rows:r})}return!0}focus(){(this.terminal.textarea||this.container.querySelector("textarea")||this.container)?.focus?.()}pasteText(t){const n=this.terminal.getMode?.(2004)??!1;this.input(n?"\x1B[200~"+t+"\x1B[201~":t)}input(t){this.terminal.input?.(t,!1)}onData(t){this.terminal.onData(n=>t(n))}onResize(t){this.resizeCallbacks.push(t)}onScroll(t){this.terminal.onScroll(t)}attachCustomKeyEventHandler(t){this.terminal.attachCustomKeyEventHandler(t)}isFocused(){const t=document.activeElement;return!!t&&(t===this.container||t===this.terminal.textarea||this.container.contains(t))}updateCellMetrics(t=!1){const n=this.container.querySelector("canvas");if(n instanceof HTMLElement&&n.offsetWidth>0&&n.offsetHeight>0&&this.colsValue>0&&this.rowsValue>0){const r=n.offsetWidth/this.colsValue,a=n.offsetHeight/this.rowsValue;(t||!this.charW||!this.charH)&&(this.charW=r,this.charH=a)}(!this.charW||!this.charH)&&(this.charW=9,this.charH=18)}}const A=window.__TMUX_WEB_TERMINAL__;if(!A)throw new Error("missing tmux-web terminal config");const u=A,H=document.getElementById("terminal-container");if(!H)throw new Error("missing terminal container");const o=H;function J(){const e=document.createElement("div");e.textContent="ghostty-web unavailable \u2014 using xterm.js",e.style.cssText='position:fixed;bottom:12px;right:12px;z-index:2000;background:rgba(0,0,0,0.85);color:#fbbf24;font:12px/1.4 "JetBrains Mono",monospace;padding:8px 12px;border:1px solid rgba(251,191,36,0.4);border-radius:6px;',document.body.appendChild(e),setTimeout(()=>e.remove(),6e3)}async function G(){if(u.renderer==="ghostty")try{return await F.create(o,u.scrollback,u.theme)}catch(e){console.error("[tmux-web] ghostty-web failed to load; falling back to xterm.js:",e),J()}return B.create(o,u.scrollback,u.theme)}const i=await G();let c,v=0,C,s=null,P=0,d="connecting",x=0,T=!1,E=[],h="";function Q(){return E.join("")+h}async function Z(e,t=0){const n=e?i.viewportY():0,r=e?i.baseY():0,a=Q();if(i.reset(),!a){e||i.scrollToBottom();return}await i.write(a),e?i.scrollToLine(r+n+t):i.scrollToBottom()}function $(e){let t;try{t=JSON.parse(e)}catch{d==="live"&&(h+=e,i.write(e));return}if(t.type==="auth.required"){const n=localStorage.getItem("tmux-web-token");n?g({type:"auth.token",token:n}):location.href="/login?returnTo="+encodeURIComponent(location.pathname+location.search);return}if(t.type==="auth.ok"){t.token&&localStorage.setItem("tmux-web-token",t.token),S(!0),w=1e3,p(),g({type:"resize",cols:i.cols,rows:i.rows}),l();return}if(t.type==="auth.failed"){if(S(!1),t.permanentLock||t.retryAfterMs&&t.retryAfterMs>6e4){localStorage.removeItem("tmux-web-token"),location.href="/login?error="+encodeURIComponent(t.message);return}const n=document.createElement("div");n.textContent="Auth failed: "+t.message,n.style.cssText='position:fixed;top:12px;right:12px;z-index:2000;background:rgba(0,0,0,0.85);color:#f87171;font:12px/1.4 "JetBrains Mono",monospace;padding:8px 12px;border:1px solid rgba(248,113,113,0.4);border-radius:6px;',document.body.appendChild(n),setTimeout(()=>n.remove(),4e3);return}if(t.type==="snapshot"&&typeof t.data=="string"){E=[t.data],h="",x=typeof t.lines=="number"?t.lines:u.terminal.initialLines,d="live",i.reset(),i.write(t.data).then(()=>i.scrollToBottom());return}if(t.type==="window_changed"&&typeof t.activeIndex=="number"){const n=new URL(location.href);n.searchParams.get("window")!==String(t.activeIndex)&&(n.searchParams.set("window",String(t.activeIndex)),history.replaceState(history.state,"",n)),window.dispatchEvent(new CustomEvent("tmux:windows",{detail:t.windows}));return}if(t.type==="history"&&typeof t.data=="string"){T=!1,t.lines>0&&t.data&&(E.unshift(t.data),x+=t.lines,Z(!0,t.lines));return}if(t.type==="data"&&typeof t.data=="string"){if(d==="connecting"){d="live",h=t.data,i.write(t.data);return}d==="live"&&(h+=t.data,i.write(t.data))}}function R(e){const t=e.getBoundingClientRect(),n=window.visualViewport;return n?{width:Math.min(t.width,n.width),height:Math.max(0,Math.min(t.height,n.height-Math.max(0,t.top)))}:t}function p(){return i.fit()}function Y(){o.classList.remove("terminal-pending")}function l(){v&&cancelAnimationFrame(v),C&&clearTimeout(C),p(),v=requestAnimationFrame(()=>{v=0,p()&&Y()}),C=setTimeout(()=>{p()&&Y()},120)}function V(){l(),setTimeout(l,50),setTimeout(l,150),setTimeout(l,300)}const N=document.getElementById("status-dot"),k=document.getElementById("status-text");function S(e){N&&(N.className="dot"+(e?" connected":"")),k&&(k.textContent=e?"connected":"reconnecting")}const tt=location.protocol==="https:"?"wss:":"ws:",et=tt+"//"+location.host+"/ws/"+encodeURIComponent(u.sessionName),nt="/api/session/"+encodeURIComponent(u.sessionName)+"/upload";let w=1e3;const rt=/^((?!chrome|android|crios|fxios|edgios).)*safari/i.test(navigator.userAgent);function g(e){c?.readyState===WebSocket.OPEN&&c.send(JSON.stringify(e))}function K(){let e=et;const t=localStorage.getItem("tmux-web-token");t&&(e+=(e.includes("?")?"&":"?")+"token="+encodeURIComponent(t)),c=new WebSocket(e),c.onopen=()=>{d="connecting",x=0,T=!1,E=[],h="",i.reset(),w=1e3},c.onmessage=n=>{typeof n.data=="string"&&$(n.data)},c.onclose=()=>{S(!1),d="connecting",setTimeout(()=>{w=Math.min(w*2,1e4),K()},w)},c.onerror=()=>c?.close()}function L(e){g({type:"input",data:e})}function it(e){L(e)}function U(e){return e.split(`\r
1
+ const q="https://esm.sh/ghostty-web@0.4.0",J=/\x1b\](?:10|11|110|111|104)(?:;[^\x07\x1b]*)?\x1b?(?:\\|\x07)/g;function v(e){return e.replace(J,"")}let m;function G(){return m||(document.querySelector("link[data-tmux-web-xterm-css]")?m=Promise.resolve():(m=new Promise(t=>{const n=document.createElement("link");n.rel="stylesheet",n.href="/assets/xterm.css",n.setAttribute("data-tmux-web-xterm-css",""),n.addEventListener("load",()=>t(),{once:!0}),n.addEventListener("error",()=>t(),{once:!0}),document.head.appendChild(n)}),m))}class F{terminal;fitAddon;container;constructor(t,n,r){this.container=t,this.terminal=n,this.fitAddon=r,this.terminal.loadAddon(this.fitAddon),this.terminal.open(t)}static async create(t,n,r){const[{Terminal:a},{FitAddon:d}]=await Promise.all([import("@xterm/xterm"),import("@xterm/addon-fit"),G()]),b=new a({fontSize:14,fontFamily:"'JetBrains Mono', 'SF Mono', 'Menlo', monospace",cursorBlink:!0,cursorStyle:"bar",scrollback:n,convertEol:!1,theme:r});return new F(t,b,new d)}get cols(){return this.terminal.cols}get rows(){return this.terminal.rows}write(t){return new Promise(n=>this.terminal.write(t,n))}reset(){this.terminal.reset()}scrollToBottom(){this.terminal.scrollToBottom()}scrollToLine(t){this.terminal.scrollToLine(Math.max(0,t))}isNearScrollbackTop(){return this.terminal.buffer.active.viewportY<=1}viewportY(){return this.terminal.buffer.active.viewportY}baseY(){return this.terminal.buffer.active.baseY}fit(){const t=V(this.container);return t.width<=0||t.height<=0?!1:(this.fitAddon.fit(),!0)}focus(){this.terminal.focus()}pasteText(t){this.terminal.paste(t)}input(t){this.terminal.input(t,!1)}onData(t){this.terminal.onData(t),this.terminal.onBinary(t)}onResize(t){this.terminal.onResize(t)}onScroll(t){this.terminal.onScroll(t)}attachCustomKeyEventHandler(t){this.terminal.attachCustomKeyEventHandler(t)}isFocused(){const t=document.activeElement;return!!t&&(t===this.container||this.container.contains(t))}}class W{terminal;container;colsValue=80;rowsValue=24;charW=0;charH=0;resizeCallbacks=[];constructor(t,n){this.container=t,this.terminal=n,this.terminal.open(t)}static async create(t,n,r){const a=await import(q);await a.init();const d=new a.Terminal({fontSize:14,fontFamily:"'JetBrains Mono', 'SF Mono', 'Menlo', monospace",cursorBlink:!0,cursorStyle:"bar",scrollback:n,convertEol:!1,theme:r});return new W(t,d)}get cols(){return this.colsValue}get rows(){return this.rowsValue}write(t){return new Promise(n=>this.terminal.write(t,n))}reset(){this.terminal.reset()}scrollToBottom(){this.terminal.scrollToBottom?.()}scrollToLine(t){this.terminal.scrollToLine?.(Math.max(0,t))}isNearScrollbackTop(){const t=this.terminal.buffer?.active;return t?t.viewportY>=t.baseY:!1}viewportY(){return this.terminal.buffer?.active?.viewportY??0}baseY(){return this.terminal.buffer?.active?.baseY??0}fit(){const t=V(this.container);if(t.width<=0||t.height<=0)return!1;this.updateCellMetrics(!this.charW||!this.charH);const n=Math.floor(t.width/this.charW),r=Math.floor(t.height/this.charH);if(n<10||r<5)return!1;if(n!==this.colsValue||r!==this.rowsValue){this.colsValue=n,this.rowsValue=r,this.terminal.resize(n,r);for(const a of this.resizeCallbacks)a({cols:n,rows:r})}return!0}focus(){(this.terminal.textarea||this.container.querySelector("textarea")||this.container)?.focus?.()}pasteText(t){const n=this.terminal.getMode?.(2004)??!1;this.input(n?"\x1B[200~"+t+"\x1B[201~":t)}input(t){this.terminal.input?.(t,!1)}onData(t){this.terminal.onData(n=>t(n))}onResize(t){this.resizeCallbacks.push(t)}onScroll(t){this.terminal.onScroll(t)}attachCustomKeyEventHandler(t){this.terminal.attachCustomKeyEventHandler(t)}isFocused(){const t=document.activeElement;return!!t&&(t===this.container||t===this.terminal.textarea||this.container.contains(t))}updateCellMetrics(t=!1){const n=this.container.querySelector("canvas");if(n instanceof HTMLElement&&n.offsetWidth>0&&n.offsetHeight>0&&this.colsValue>0&&this.rowsValue>0){const r=n.offsetWidth/this.colsValue,a=n.offsetHeight/this.rowsValue;(t||!this.charW||!this.charH)&&(this.charW=r,this.charH=a)}(!this.charW||!this.charH)&&(this.charW=9,this.charH=18)}}const R=window.__TMUX_WEB_TERMINAL__;if(!R)throw new Error("missing tmux-web terminal config");const u=R,H=document.getElementById("terminal-container");if(!H)throw new Error("missing terminal container");const o=H;function Q(){const e=document.createElement("div");e.textContent="ghostty-web unavailable \u2014 using xterm.js",e.style.cssText='position:fixed;bottom:12px;right:12px;z-index:2000;background:rgba(0,0,0,0.85);color:#fbbf24;font:12px/1.4 "JetBrains Mono",monospace;padding:8px 12px;border:1px solid rgba(251,191,36,0.4);border-radius:6px;',document.body.appendChild(e),setTimeout(()=>e.remove(),6e3)}async function Z(){if(u.renderer==="ghostty")try{return await W.create(o,u.scrollback,u.theme)}catch(e){console.error("[tmux-web] ghostty-web failed to load; falling back to xterm.js:",e),Q()}return F.create(o,u.scrollback,u.theme)}const i=await Z();let c,x=0,P,s=null,Y=0,f="connecting",T=0,E=!1,k=[],h="";function $(){return k.join("")+h}async function tt(e,t=0){const n=e?i.viewportY():0,r=e?i.baseY():0,a=v($());if(i.reset(),!a){e||i.scrollToBottom();return}await i.write(a),e?i.scrollToLine(r+n+t):i.scrollToBottom()}function et(e){let t;try{t=JSON.parse(e)}catch{f==="live"&&(h+=e,i.write(v(e)));return}if(t.type==="auth.required"){const n=localStorage.getItem("tmux-web-token");n?g({type:"auth.token",token:n}):location.href="/login?returnTo="+encodeURIComponent(location.pathname+location.search);return}if(t.type==="auth.ok"){t.token&&localStorage.setItem("tmux-web-token",t.token),L(!0),w=1e3,p(),g({type:"resize",cols:i.cols,rows:i.rows}),l();return}if(t.type==="auth.failed"){if(L(!1),t.permanentLock||t.retryAfterMs&&t.retryAfterMs>6e4){localStorage.removeItem("tmux-web-token"),location.href="/login?error="+encodeURIComponent(t.message);return}const n=document.createElement("div");n.textContent="Auth failed: "+t.message,n.style.cssText='position:fixed;top:12px;right:12px;z-index:2000;background:rgba(0,0,0,0.85);color:#f87171;font:12px/1.4 "JetBrains Mono",monospace;padding:8px 12px;border:1px solid rgba(248,113,113,0.4);border-radius:6px;',document.body.appendChild(n),setTimeout(()=>n.remove(),4e3);return}if(t.type==="snapshot"&&typeof t.data=="string"){const n=v(t.data);k=[n],h="",T=typeof t.lines=="number"?t.lines:u.terminal.initialLines,f="live",i.reset(),i.write(n).then(()=>i.scrollToBottom());return}if(t.type==="window_changed"&&typeof t.activeIndex=="number"){const n=new URL(location.href);n.searchParams.get("window")!==String(t.activeIndex)&&(n.searchParams.set("window",String(t.activeIndex)),history.replaceState(history.state,"",n)),window.dispatchEvent(new CustomEvent("tmux:windows",{detail:t.windows}));return}if(t.type==="history"&&typeof t.data=="string"){E=!1,t.lines>0&&t.data&&(k.unshift(t.data),T+=t.lines,tt(!0,t.lines));return}if(t.type==="data"&&typeof t.data=="string"){const n=v(t.data);if(f==="connecting"){f="live",h=n,i.write(n);return}f==="live"&&(h+=n,i.write(n))}}function V(e){const t=e.getBoundingClientRect(),n=window.visualViewport;return n?{width:Math.min(t.width,n.width),height:Math.max(0,Math.min(t.height,n.height-Math.max(0,t.top)))}:t}function p(){return i.fit()}function I(){o.classList.remove("terminal-pending")}function l(){x&&cancelAnimationFrame(x),P&&clearTimeout(P),p(),x=requestAnimationFrame(()=>{x=0,p()&&I()}),P=setTimeout(()=>{p()&&I()},120)}function N(){l(),setTimeout(l,50),setTimeout(l,150),setTimeout(l,300)}const O=document.getElementById("status-dot"),S=document.getElementById("status-text");function L(e){O&&(O.className="dot"+(e?" connected":"")),S&&(S.textContent=e?"connected":"reconnecting")}const nt=location.protocol==="https:"?"wss:":"ws:",rt=nt+"//"+location.host+"/ws/"+encodeURIComponent(u.sessionName),it="/api/session/"+encodeURIComponent(u.sessionName)+"/upload";let w=1e3;const ot=/^((?!chrome|android|crios|fxios|edgios).)*safari/i.test(navigator.userAgent);function g(e){c?.readyState===WebSocket.OPEN&&c.send(JSON.stringify(e))}function _(){let e=rt;const t=localStorage.getItem("tmux-web-token");t&&(e+=(e.includes("?")?"&":"?")+"token="+encodeURIComponent(t)),c=new WebSocket(e),c.onopen=()=>{f="connecting",T=0,E=!1,k=[],h="",i.reset(),w=1e3},c.onmessage=n=>{typeof n.data=="string"&&et(n.data)},c.onclose=()=>{L(!1),f="connecting",setTimeout(()=>{w=Math.min(w*2,1e4),_()},w)},c.onerror=()=>c?.close()}function C(e){g({type:"input",data:e})}function at(e){C(e)}function K(e){return e.split(`\r
2
2
  `).join(`
3
3
  `).split(`
4
- `).join("\r")}function X(e){return typeof e=="string"&&e.startsWith("image/")}function _(e){return e?.files?.length?e.files[0]??null:null}function ot(e){if(!e?.items)return null;for(let t=0;t<e.items.length;t++){const n=e.items[t];if(n?.kind==="file"&&X(n.type))return n.getAsFile()}return null}function D(e){e&&k?k.textContent=e:S(c?.readyState===WebSocket.OPEN)}async function at(e){const t=new FormData,n=e instanceof File&&e.name?e.name:"upload";t.append("file",e,n);const r=localStorage.getItem("tmux-web-token"),a={};r&&(a.Authorization="Bearer "+r);const f=await fetch(nt,{method:"POST",body:t,headers:a});if(!f.ok){let W="upload failed";try{const z=await f.json();z.error&&(W=z.error)}catch{}throw new Error(W)}const b=await f.json();if(!b.path)throw new Error("no path in response");return b.path}async function I(e){try{D("uploading...");const t=await at(e);it(t),D(null)}catch(t){console.warn("image upload:",t),D("upload failed"),setTimeout(()=>D(null),2e3)}}i.onData(e=>L(e)),i.onResize(({cols:e,rows:t})=>g({type:"resize",cols:e,rows:t})),window.tmuxWeb={sendInput:L,focusTerminal:j},i.onScroll(()=>{d!=="live"||T||!i.isNearScrollbackTop()||(T=!0,g({type:"load_history",before:x}))}),i.attachCustomKeyEventHandler(e=>e.type!=="keydown"?!0:(e.ctrlKey||e.metaKey)&&e.code==="KeyV"?(e.preventDefault(),(async()=>{try{if(navigator.clipboard?.read){const t=await navigator.clipboard.read();for(const n of t)for(const r of n.types)if(X(r)){const a=await n.getType(r);await I(a);return}}}catch{}try{const t=await navigator.clipboard?.readText();t&&i.pasteText(U(t))}catch{}})(),!1):!0);let M=0;async function st(e){const t=ot(e.clipboardData);if(t){e.preventDefault(),e.stopPropagation();const a=Date.now();if(a-M<50)return;M=a,await I(t);return}const n=e.clipboardData?.getData("text/plain");if(!n)return;e.preventDefault(),e.stopPropagation();const r=Date.now();r-M<50||(M=r,i.pasteText(U(n)))}o.addEventListener("paste",st);let y=0;o.addEventListener("dragenter",e=>{e.preventDefault(),y++,_(e.dataTransfer)&&o.classList.add("terminal-drag-over")}),o.addEventListener("dragover",e=>{e.preventDefault(),e.dataTransfer&&(e.dataTransfer.dropEffect="copy")}),o.addEventListener("dragleave",e=>{e.preventDefault(),y--,y<=0&&(y=0,o.classList.remove("terminal-drag-over"))}),o.addEventListener("drop",async e=>{e.preventDefault(),y=0,o.classList.remove("terminal-drag-over");const t=_(e.dataTransfer);t&&await I(t)}),document.addEventListener("keydown",e=>{!rt||e.key!=="Escape"||e.defaultPrevented||e.repeat||e.metaKey||e.ctrlKey||e.altKey||i.isFocused()&&(e.preventDefault(),e.stopPropagation(),L("\x1B"))},!0);function lt(e,t,n){(o.querySelector(".xterm-screen")??o).dispatchEvent(new WheelEvent("wheel",{deltaY:e,deltaMode:WheelEvent.DOM_DELTA_PIXEL,clientX:t,clientY:n,bubbles:!0,cancelable:!0}))}function j(){i.focus(),V()}o.addEventListener("touchstart",e=>{if(e.touches.length!==1){s=null;return}const t=e.touches[0];t&&(s={startX:t.clientX,startY:t.clientY,lastX:t.clientX,lastY:t.clientY,scrolling:!1},e.stopPropagation())},{passive:!0,capture:!0}),o.addEventListener("touchmove",e=>{if(!s||e.touches.length!==1)return;const t=e.touches[0];if(!t)return;const n=t.clientY-s.startY,r=t.clientX-s.startX;if(!s.scrolling){if(Math.abs(n)<8||Math.abs(n)<Math.abs(r))return;s.scrolling=!0,P=Date.now()+500}e.preventDefault(),e.stopPropagation(),lt(-(t.clientY-s.lastY),t.clientX,t.clientY),s.lastX=t.clientX,s.lastY=t.clientY},{passive:!1,capture:!0}),o.addEventListener("touchend",e=>{if(!s)return;const t=s.scrolling;s=null,e.stopPropagation(),t?(P=Date.now()+500,e.preventDefault()):j()},{passive:!1,capture:!0}),o.addEventListener("touchcancel",e=>{s=null,e.stopPropagation()},{passive:!0,capture:!0}),o.addEventListener("pointerup",e=>{e.pointerType==="touch"&&Date.now()<P&&(e.preventDefault(),e.stopPropagation())},!0),window.addEventListener("resize",l),window.visualViewport?.addEventListener("resize",l),window.visualViewport?.addEventListener("scroll",l),new ResizeObserver(l).observe(o),document.fonts?.ready.then(()=>{l(),setTimeout(l,50),setTimeout(l,200)}),o.addEventListener("focusin",V,!0),requestAnimationFrame(()=>{p()&&Y(),K(),l()});
4
+ `).join("\r")}function U(e){return typeof e=="string"&&e.startsWith("image/")}function X(e){return e?.files?.length?e.files[0]??null:null}function st(e){if(!e?.items)return null;for(let t=0;t<e.items.length;t++){const n=e.items[t];if(n?.kind==="file"&&U(n.type))return n.getAsFile()}return null}function D(e){e&&S?S.textContent=e:L(c?.readyState===WebSocket.OPEN)}async function lt(e){const t=new FormData,n=e instanceof File&&e.name?e.name:"upload";t.append("file",e,n);const r=localStorage.getItem("tmux-web-token"),a={};r&&(a.Authorization="Bearer "+r);const d=await fetch(it,{method:"POST",body:t,headers:a});if(!d.ok){let z="upload failed";try{const A=await d.json();A.error&&(z=A.error)}catch{}throw new Error(z)}const b=await d.json();if(!b.path)throw new Error("no path in response");return b.path}async function B(e){try{D("uploading...");const t=await lt(e);at(t),D(null)}catch(t){console.warn("image upload:",t),D("upload failed"),setTimeout(()=>D(null),2e3)}}i.onData(e=>C(e)),i.onResize(({cols:e,rows:t})=>g({type:"resize",cols:e,rows:t})),window.tmuxWeb={sendInput:C,focusTerminal:j},i.onScroll(()=>{f!=="live"||E||!i.isNearScrollbackTop()||(E=!0,g({type:"load_history",before:T}))}),i.attachCustomKeyEventHandler(e=>e.type!=="keydown"?!0:(e.ctrlKey||e.metaKey)&&e.code==="KeyV"?(e.preventDefault(),(async()=>{try{if(navigator.clipboard?.read){const t=await navigator.clipboard.read();for(const n of t)for(const r of n.types)if(U(r)){const a=await n.getType(r);await B(a);return}}}catch{}try{const t=await navigator.clipboard?.readText();t&&i.pasteText(K(t))}catch{}})(),!1):!0);let M=0;async function ct(e){const t=st(e.clipboardData);if(t){e.preventDefault(),e.stopPropagation();const a=Date.now();if(a-M<50)return;M=a,await B(t);return}const n=e.clipboardData?.getData("text/plain");if(!n)return;e.preventDefault(),e.stopPropagation();const r=Date.now();r-M<50||(M=r,i.pasteText(K(n)))}o.addEventListener("paste",ct);let y=0;o.addEventListener("dragenter",e=>{e.preventDefault(),y++,X(e.dataTransfer)&&o.classList.add("terminal-drag-over")}),o.addEventListener("dragover",e=>{e.preventDefault(),e.dataTransfer&&(e.dataTransfer.dropEffect="copy")}),o.addEventListener("dragleave",e=>{e.preventDefault(),y--,y<=0&&(y=0,o.classList.remove("terminal-drag-over"))}),o.addEventListener("drop",async e=>{e.preventDefault(),y=0,o.classList.remove("terminal-drag-over");const t=X(e.dataTransfer);t&&await B(t)}),document.addEventListener("keydown",e=>{!ot||e.key!=="Escape"||e.defaultPrevented||e.repeat||e.metaKey||e.ctrlKey||e.altKey||i.isFocused()&&(e.preventDefault(),e.stopPropagation(),C("\x1B"))},!0);function ut(e,t,n){(o.querySelector(".xterm-screen")??o).dispatchEvent(new WheelEvent("wheel",{deltaY:e,deltaMode:WheelEvent.DOM_DELTA_PIXEL,clientX:t,clientY:n,bubbles:!0,cancelable:!0}))}function j(){i.focus(),N()}o.addEventListener("touchstart",e=>{if(e.touches.length!==1){s=null;return}const t=e.touches[0];t&&(s={startX:t.clientX,startY:t.clientY,lastX:t.clientX,lastY:t.clientY,scrolling:!1},e.stopPropagation())},{passive:!0,capture:!0}),o.addEventListener("touchmove",e=>{if(!s||e.touches.length!==1)return;const t=e.touches[0];if(!t)return;const n=t.clientY-s.startY,r=t.clientX-s.startX;if(!s.scrolling){if(Math.abs(n)<8||Math.abs(n)<Math.abs(r))return;s.scrolling=!0,Y=Date.now()+500}e.preventDefault(),e.stopPropagation(),ut(-(t.clientY-s.lastY),t.clientX,t.clientY),s.lastX=t.clientX,s.lastY=t.clientY},{passive:!1,capture:!0}),o.addEventListener("touchend",e=>{if(!s)return;const t=s.scrolling;s=null,e.stopPropagation(),t?(Y=Date.now()+500,e.preventDefault()):j()},{passive:!1,capture:!0}),o.addEventListener("touchcancel",e=>{s=null,e.stopPropagation()},{passive:!0,capture:!0}),o.addEventListener("pointerup",e=>{e.pointerType==="touch"&&Date.now()<Y&&(e.preventDefault(),e.stopPropagation())},!0),window.addEventListener("resize",l),window.visualViewport?.addEventListener("resize",l),window.visualViewport?.addEventListener("scroll",l),new ResizeObserver(l).observe(o),document.fonts?.ready.then(()=>{l(),setTimeout(l,50),setTimeout(l,200)}),o.addEventListener("focusin",N,!0),requestAnimationFrame(()=>{p()&&I(),_(),l()});
package/dist/index.js CHANGED
@@ -9,4 +9,4 @@ import{chmodSync as Oe,existsSync as Y,readdirSync as le,statSync as Z}from"node
9
9
  \x1B[31mFailed to attach to tmux session "${r}": ${c.message}\x1B[0m\r
10
10
  `}),oe(e,1011,"pty spawn failed");return}H.add(i);let g=-1,m="";P=Lt(r,({activeIndex:c,windows:j})=>{x(e,{type:"window_changed",activeIndex:c,windows:j});const L=j.map(S=>S.index).join(","),$=c!==g||L!==m;g=c,m=L,$&&(K(r),or())}),B=setTimeout(ce,Q),i.onData(c=>{if(E){Ue();return}x(e,{type:"data",data:c})}),i.onExit(({exitCode:c})=>{F(),i&&H.delete(i),i=null,x(e,{type:"data",data:`\r
11
11
  \x1B[2m--- tmux exited (code ${c}) ---\x1B[0m\r
12
- `}),oe(e,1e3,"pty exited")})}function J(g,m){o.authenticated=!0,o.authTimeout&&(clearTimeout(o.authTimeout),o.authTimeout=null),u("auth_success",{ip:n,method:g,tokenName:m}),x(e,{type:"auth.ok",setupMode:!l.passwordHash}),ue()}function C(g,m={}){x(e,{type:"auth.failed",message:g,...m})}async function We(g){let m;try{m=JSON.parse(g)}catch{return!1}if(m.type==="auth.token"&&typeof m.token=="string"){const c=p.validateToken(m.token);if(!c){u("token_auth_failed",{ip:n});const j=R.recordFailure(n);return j.permanentLock&&(p.revokeAll(),u("permanent_lock",{ip:n,failures:j.failures})),C("Invalid or expired token",{permanentLock:j.permanentLock,retryAfterMs:j.retryAfterMs}),!0}return p.touch(c.tokenHash),J("token",c.name),!0}if(m.type==="auth"&&typeof m.password=="string"){const c=R.check(n);if(!c.allowed)return u("rate_limited",{ip:n,retryAfterMs:c.retryAfterMs,permanentLock:c.permanentLock}),C("Too many attempts",{permanentLock:c.permanentLock,retryAfterMs:c.retryAfterMs}),!0;if(!l.passwordHash){if(!l.security.allowRemoteSetup&&!Se(n))return u("setup_rejected_remote",{ip:n}),C("First-run setup must be performed from localhost"),!0;if(U)return C("Password setup in progress"),!0;const S=ye(m.password);if(S)return C(S),!0;U=!0;try{l.passwordHash=await fe(m.password),pe(l),u("password_set",{ip:n})}finally{U=!1}const{stored:X,plaintext:He}=p.createAccessToken("setup",l.security.tokenTtlDays);return x(e,{type:"auth.ok",setupMode:!0,token:He}),u("token_created",{ip:n,name:X.name,tokenId:X.id}),u("auth_success",{ip:n,method:"password",tokenName:X.name}),ue(),!0}if(!l.passwordHash)return C("Server not configured"),!0;if(!await he(m.password,l.passwordHash)){const S=R.recordFailure(n);return u("auth_failed",{ip:n,method:"password",failures:S.failures,permanentLock:S.permanentLock}),S.permanentLock&&(p.revokeAll(),u("permanent_lock",{ip:n,failures:S.failures})),C("Incorrect password",{permanentLock:S.permanentLock,retryAfterMs:S.retryAfterMs}),!0}R.recordSuccess(n);const L=`ws-${n}`,{plaintext:$}=p.createAccessToken(L,l.security.tokenTtlDays);return J("password",L),x(e,{type:"auth.ok",setupMode:!1,token:$}),!0}return!1}s?(p.touch(s.tokenHash),J("token",s.name)):(x(e,{type:"auth.required",setupMode:!l.passwordHash}),o.authTimeout=setTimeout(()=>{o.authenticated||(u("auth_timeout",{ip:n}),C("Authentication timeout"),oe(e,4e3,"Auth timeout"))},l.security.authTimeoutMs)),e.on("message",async g=>{const m=typeof g=="string"?g:g.toString("utf-8");if(!o.authenticated){if(m.length>1e6){x(e,{type:"auth.failed",message:"Message too large"});return}await We(m)||C("Authentication required");return}if(!i)return;try{if(wt(m,i))return}catch{return}let c;try{c=JSON.parse(m)}catch{return}if(c.type==="load_history"&&typeof c.before=="number"){const j=Math.max(0,Math.floor(c.before));try{const{data:L,lines:$}=Bt(N,j,f);x(e,{type:"history",data:xe(L),before:j,lines:$})}catch{x(e,{type:"history",data:"",before:j,lines:0})}}}),e.on("close",()=>{o.authTimeout&&clearTimeout(o.authTimeout),W.delete(e),u("ws_disconnected",{ip:n}),F(),P&&(P(),P=null),i&&(H.delete(i),i.kill(),i=null)}),e.on("error",()=>{o.authTimeout&&clearTimeout(o.authTimeout),W.delete(e),F(),P&&(P(),P=null),i&&(H.delete(i),i.kill(),i=null)})});function De(){_.cleanup(),Dt();for(const[e,t]of W){try{e.close(1001,"Server shutting down")}catch{}t.authTimeout&&clearTimeout(t.authTimeout)}W.clear();for(const e of H)try{e.kill()}catch{}H.clear();for(const e of Ce)try{e.kill("SIGTERM")}catch{}R.dispose(),process.exit(0)}process.on("SIGINT",De),process.on("SIGTERM",De),setInterval(()=>{p.purgeExpired()},60*60*1e3).unref();
12
+ `}),oe(e,1e3,"pty exited")})}function J(g,m){o.authenticated=!0,o.authTimeout&&(clearTimeout(o.authTimeout),o.authTimeout=null),u("auth_success",{ip:n,method:g,tokenName:m}),x(e,{type:"auth.ok",setupMode:!l.passwordHash}),ue()}function C(g,m={}){x(e,{type:"auth.failed",message:g,...m})}async function We(g){let m;try{m=JSON.parse(g)}catch{return!1}if(m.type==="auth.token"&&typeof m.token=="string"){const c=p.validateToken(m.token);if(!c){u("token_auth_failed",{ip:n});const j=R.recordFailure(n);return j.permanentLock&&(p.revokeAll(),u("permanent_lock",{ip:n,failures:j.failures})),C("Invalid or expired token",{permanentLock:j.permanentLock,retryAfterMs:j.retryAfterMs}),!0}return p.touch(c.tokenHash),J("token",c.name),!0}if(m.type==="auth"&&typeof m.password=="string"){const c=R.check(n);if(!c.allowed)return u("rate_limited",{ip:n,retryAfterMs:c.retryAfterMs,permanentLock:c.permanentLock}),C("Too many attempts",{permanentLock:c.permanentLock,retryAfterMs:c.retryAfterMs}),!0;if(!l.passwordHash){if(!l.security.allowRemoteSetup&&!Se(n))return u("setup_rejected_remote",{ip:n}),C("First-run setup must be performed from localhost"),!0;if(U)return C("Password setup in progress"),!0;const S=ye(m.password);if(S)return C(S),!0;U=!0;try{l.passwordHash=await fe(m.password),pe(l),u("password_set",{ip:n})}finally{U=!1}const{stored:X,plaintext:He}=p.createAccessToken("setup",l.security.tokenTtlDays);return x(e,{type:"auth.ok",setupMode:!0,token:He}),u("token_created",{ip:n,name:X.name,tokenId:X.id}),u("auth_success",{ip:n,method:"password",tokenName:X.name}),ue(),!0}if(!l.passwordHash)return C("Server not configured"),!0;if(!await he(m.password,l.passwordHash)){const S=R.recordFailure(n);return u("auth_failed",{ip:n,method:"password",failures:S.failures,permanentLock:S.permanentLock}),S.permanentLock&&(p.revokeAll(),u("permanent_lock",{ip:n,failures:S.failures})),C("Incorrect password",{permanentLock:S.permanentLock,retryAfterMs:S.retryAfterMs}),!0}R.recordSuccess(n);const L=`ws-${n}`,{plaintext:$}=p.createAccessToken(L,l.security.tokenTtlDays);return J("password",L),x(e,{type:"auth.ok",setupMode:!1,token:$}),!0}return!1}s?(p.touch(s.tokenHash),J("token",s.name)):(x(e,{type:"auth.required",setupMode:!l.passwordHash}),o.authTimeout=setTimeout(()=>{o.authenticated||(u("auth_timeout",{ip:n}),C("Authentication timeout"),oe(e,4e3,"Auth timeout"))},l.security.authTimeoutMs)),e.on("message",async g=>{const m=typeof g=="string"?g:g.toString("utf-8");if(!o.authenticated){if(m.length>1e6){x(e,{type:"auth.failed",message:"Message too large"});return}await We(m)||C("Authentication required");return}if(!i)return;try{if(wt(m,i))return}catch{return}let c;try{c=JSON.parse(m)}catch{return}if(c.type==="load_history"&&typeof c.before=="number"){const j=Math.max(0,Math.floor(c.before));try{const{data:L,lines:$}=Bt(N,j,f);x(e,{type:"history",data:xe(L),before:j,lines:$})}catch{x(e,{type:"history",data:"",before:j,lines:0})}}}),e.on("close",()=>{o.authTimeout&&clearTimeout(o.authTimeout),W.delete(e),u("ws_disconnected",{ip:n}),F(),P&&(P(),P=null),i&&(H.delete(i),i.kill(),i=null)}),e.on("error",()=>{o.authTimeout&&clearTimeout(o.authTimeout),W.delete(e),F(),P&&(P(),P=null),i&&(H.delete(i),i.kill(),i=null)})});function De(){_.cleanup(),Dt();for(const[e,t]of W){try{e.close(1001,"Server shutting down")}catch{}t.authTimeout&&clearTimeout(t.authTimeout)}W.clear();for(const e of H)try{e.kill()}catch{}H.clear();for(const e of Ce)try{e.kill("SIGTERM")}catch{}R.dispose(),process.exit(0)}process.on("SIGINT",De),process.on("SIGTERM",De),setInterval(()=>{p.purgeExpired()},3600*1e3).unref();
@@ -1 +1 @@
1
- import{execFileSync as o}from"node:child_process";import{randomUUID as c}from"node:crypto";import{ARM_SCAN_INTERVAL_MS as u,isValidDelayMs as h,MAX_TIMER_MS as n}from"./schedule-delay.js";const T=7*24*60*60*1e3;function m(a,e,s){const t=`${a}:${e}`;o("tmux",["send-keys","-t",t,"-l",s],{timeout:5e3}),o("tmux",["send-keys","-t",t,"Enter"],{timeout:5e3})}function w(a){if(!a||typeof a!="object")return!1;const e=a;return typeof e.delayMs=="number"&&h(e.delayMs)}function y(a){if(!a||typeof a!="object")return!1;const e=a;return typeof e.sessionName=="string"&&e.sessionName.length>0&&typeof e.windowIndex=="number"&&Number.isInteger(e.windowIndex)&&e.windowIndex>=0&&typeof e.text=="string"&&e.text.length>0&&e.text.length<=4096&&typeof e.delayMs=="number"&&h(e.delayMs)}class I{deps;scheduledTasks=new Map;now;setTimer;clearTimer;setIntervalFn;clearIntervalFn;armScanIntervalMs;createId;sendKeys;onError;onMissedTask;historyRetentionMs;scanIntervalHandle=null;constructor(e){this.deps=e,this.now=e.now??Date.now,this.setTimer=e.setTimer??setTimeout,this.clearTimer=e.clearTimer??clearTimeout,this.setIntervalFn=e.setInterval??setInterval,this.clearIntervalFn=e.clearInterval??clearInterval,this.armScanIntervalMs=e.armScanIntervalMs??u,this.createId=e.createId??c,this.sendKeys=e.sendKeys??m,this.onError=e.onError??(s=>console.error(s)),this.onMissedTask=e.onMissedTask??(()=>{}),this.historyRetentionMs=e.historyRetentionMs??T}record(e){this.deps.db.data.triggeredTasks??=[],this.deps.db.data.triggeredTasks.push(e);const s=this.now()-this.historyRetentionMs;this.deps.db.data.triggeredTasks=this.deps.db.data.triggeredTasks.filter(t=>t.triggeredAt>=s)}listTriggered(){this.deps.db.data.triggeredTasks??=[];const e=this.now()-this.historyRetentionMs;return this.deps.db.data.triggeredTasks.filter(s=>s.triggeredAt>=e).sort((s,t)=>t.triggeredAt-s.triggeredAt)}async restoreFromDb(){const e=this.now(),s=[];for(const r of this.deps.db.data.scheduledTasks)r.fireAt<=e?(s.push(r.id),this.onMissedTask(r),this.record({...r,triggeredAt:e,status:"missed"})):this.registerTask(r);this.deps.db.data.triggeredTasks??=[];const t=this.deps.db.data.triggeredTasks.length,i=e-this.historyRetentionMs;this.deps.db.data.triggeredTasks=this.deps.db.data.triggeredTasks.filter(r=>r.triggeredAt>=i);const d=this.deps.db.data.triggeredTasks.length!==t;s.length&&(this.deps.db.data.scheduledTasks=this.deps.db.data.scheduledTasks.filter(r=>!s.includes(r.id))),(s.length||d)&&await this.deps.db.write(),this.scanPending(),this.startScanLoop()}list(e){return[...this.scheduledTasks.values()].filter(s=>!e||s.sessionName===e).map(({id:s,sessionName:t,windowIndex:i,text:d,fireAt:r,createdAt:l})=>({id:s,sessionName:t,windowIndex:i,text:d,fireAt:r,createdAt:l,remainingMs:Math.max(0,r-this.now())})).sort((s,t)=>s.fireAt-t.fireAt)}async create(e){const s=this.createId(),t=this.now(),i={id:s,sessionName:e.sessionName,windowIndex:e.windowIndex,text:e.text,fireAt:t+e.delayMs,createdAt:t};return this.registerTask(i),this.deps.db.data.scheduledTasks.push(i),await this.deps.db.write(),this.tryArm(i.id),i}async reschedule(e,s){const t=this.scheduledTasks.get(e);if(!t)return null;this.disarmTask(e);const i={...t,fireAt:this.now()+s};this.registerTask(i);const d=this.deps.db.data.scheduledTasks.findIndex(r=>r.id===e);return d>=0&&(this.deps.db.data.scheduledTasks[d]=i),await this.deps.db.write(),this.tryArm(e),i}async delete(e){return this.scheduledTasks.get(e)?(this.disarmTask(e),this.scheduledTasks.delete(e),this.deps.db.data.scheduledTasks=this.deps.db.data.scheduledTasks.filter(t=>t.id!==e),await this.deps.db.write(),!0):!1}cleanup(){if(this.scanIntervalHandle!=null){try{this.clearIntervalFn(this.scanIntervalHandle)}catch{}this.scanIntervalHandle=null}for(const e of this.scheduledTasks.values())if(e.timeoutHandle!=null)try{this.clearTimer(e.timeoutHandle)}catch{}this.scheduledTasks.clear()}scanPending(){for(const e of[...this.scheduledTasks.values()]){const s=e.fireAt-this.now();if(s<=0){e.timeoutHandle!=null&&this.disarmTask(e.id),this.fireTask(e);continue}s<=n&&e.timeoutHandle==null&&this.armTask(e)}}registerTask(e){this.scheduledTasks.set(e.id,{...e,timeoutHandle:null})}disarmTask(e){const s=this.scheduledTasks.get(e);s&&s.timeoutHandle!=null&&(this.clearTimer(s.timeoutHandle),s.timeoutHandle=null)}tryArm(e){const s=this.scheduledTasks.get(e);if(!s)return;const t=s.fireAt-this.now();if(t<=0){this.fireTask(s);return}t<=n&&s.timeoutHandle==null&&this.armTask(s)}armTask(e){const s=e.fireAt-this.now();if(s<=0){this.fireTask(e);return}this.disarmTask(e.id);const t=Math.min(s,n),i=this.setTimer(()=>this.onTimer(e.id),t);this.scheduledTasks.set(e.id,{...e,timeoutHandle:i})}onTimer(e){const s=this.scheduledTasks.get(e);if(s){if(s.timeoutHandle=null,s.fireAt<=this.now()){this.fireTask(s);return}this.armTask(s)}}startScanLoop(){this.scanIntervalHandle==null&&(this.scanIntervalHandle=this.setIntervalFn(()=>this.scanPending(),this.armScanIntervalMs))}fireTask(e){if(!this.scheduledTasks.has(e.id))return;this.disarmTask(e.id);const s=this.now(),t={id:e.id,sessionName:e.sessionName,windowIndex:e.windowIndex,text:e.text,fireAt:e.fireAt,createdAt:e.createdAt};try{this.sendKeys(e.sessionName,e.windowIndex,e.text),this.record({...t,triggeredAt:s,status:"ok"})}catch(i){const d=String(i?.message??i);this.onError(`[scheduler] send-keys to ${e.sessionName}:${e.windowIndex} failed: ${d}`),this.record({...t,triggeredAt:s,status:"error",error:d})}this.scheduledTasks.delete(e.id),this.deps.db.data.scheduledTasks=this.deps.db.data.scheduledTasks.filter(i=>i.id!==e.id),this.deps.db.write().catch(this.onError)}}export{I as SchedulerService,w as isValidRescheduleInput,y as isValidScheduleInput,m as sendTmuxKeys};
1
+ import{execFileSync as o}from"node:child_process";import{randomUUID as c}from"node:crypto";import{ARM_SCAN_INTERVAL_MS as u,isValidDelayMs as h,MAX_TIMER_MS as n}from"./schedule-delay.js";const T=10080*60*1e3;function m(a,e,s){const t=`${a}:${e}`;o("tmux",["send-keys","-t",t,"-l",s],{timeout:5e3}),o("tmux",["send-keys","-t",t,"Enter"],{timeout:5e3})}function w(a){if(!a||typeof a!="object")return!1;const e=a;return typeof e.delayMs=="number"&&h(e.delayMs)}function y(a){if(!a||typeof a!="object")return!1;const e=a;return typeof e.sessionName=="string"&&e.sessionName.length>0&&typeof e.windowIndex=="number"&&Number.isInteger(e.windowIndex)&&e.windowIndex>=0&&typeof e.text=="string"&&e.text.length>0&&e.text.length<=4096&&typeof e.delayMs=="number"&&h(e.delayMs)}class I{deps;scheduledTasks=new Map;now;setTimer;clearTimer;setIntervalFn;clearIntervalFn;armScanIntervalMs;createId;sendKeys;onError;onMissedTask;historyRetentionMs;scanIntervalHandle=null;constructor(e){this.deps=e,this.now=e.now??Date.now,this.setTimer=e.setTimer??setTimeout,this.clearTimer=e.clearTimer??clearTimeout,this.setIntervalFn=e.setInterval??setInterval,this.clearIntervalFn=e.clearInterval??clearInterval,this.armScanIntervalMs=e.armScanIntervalMs??u,this.createId=e.createId??c,this.sendKeys=e.sendKeys??m,this.onError=e.onError??(s=>console.error(s)),this.onMissedTask=e.onMissedTask??(()=>{}),this.historyRetentionMs=e.historyRetentionMs??T}record(e){this.deps.db.data.triggeredTasks??=[],this.deps.db.data.triggeredTasks.push(e);const s=this.now()-this.historyRetentionMs;this.deps.db.data.triggeredTasks=this.deps.db.data.triggeredTasks.filter(t=>t.triggeredAt>=s)}listTriggered(){this.deps.db.data.triggeredTasks??=[];const e=this.now()-this.historyRetentionMs;return this.deps.db.data.triggeredTasks.filter(s=>s.triggeredAt>=e).sort((s,t)=>t.triggeredAt-s.triggeredAt)}async restoreFromDb(){const e=this.now(),s=[];for(const r of this.deps.db.data.scheduledTasks)r.fireAt<=e?(s.push(r.id),this.onMissedTask(r),this.record({...r,triggeredAt:e,status:"missed"})):this.registerTask(r);this.deps.db.data.triggeredTasks??=[];const t=this.deps.db.data.triggeredTasks.length,i=e-this.historyRetentionMs;this.deps.db.data.triggeredTasks=this.deps.db.data.triggeredTasks.filter(r=>r.triggeredAt>=i);const d=this.deps.db.data.triggeredTasks.length!==t;s.length&&(this.deps.db.data.scheduledTasks=this.deps.db.data.scheduledTasks.filter(r=>!s.includes(r.id))),(s.length||d)&&await this.deps.db.write(),this.scanPending(),this.startScanLoop()}list(e){return[...this.scheduledTasks.values()].filter(s=>!e||s.sessionName===e).map(({id:s,sessionName:t,windowIndex:i,text:d,fireAt:r,createdAt:l})=>({id:s,sessionName:t,windowIndex:i,text:d,fireAt:r,createdAt:l,remainingMs:Math.max(0,r-this.now())})).sort((s,t)=>s.fireAt-t.fireAt)}async create(e){const s=this.createId(),t=this.now(),i={id:s,sessionName:e.sessionName,windowIndex:e.windowIndex,text:e.text,fireAt:t+e.delayMs,createdAt:t};return this.registerTask(i),this.deps.db.data.scheduledTasks.push(i),await this.deps.db.write(),this.tryArm(i.id),i}async reschedule(e,s){const t=this.scheduledTasks.get(e);if(!t)return null;this.disarmTask(e);const i={...t,fireAt:this.now()+s};this.registerTask(i);const d=this.deps.db.data.scheduledTasks.findIndex(r=>r.id===e);return d>=0&&(this.deps.db.data.scheduledTasks[d]=i),await this.deps.db.write(),this.tryArm(e),i}async delete(e){return this.scheduledTasks.get(e)?(this.disarmTask(e),this.scheduledTasks.delete(e),this.deps.db.data.scheduledTasks=this.deps.db.data.scheduledTasks.filter(t=>t.id!==e),await this.deps.db.write(),!0):!1}cleanup(){if(this.scanIntervalHandle!=null){try{this.clearIntervalFn(this.scanIntervalHandle)}catch{}this.scanIntervalHandle=null}for(const e of this.scheduledTasks.values())if(e.timeoutHandle!=null)try{this.clearTimer(e.timeoutHandle)}catch{}this.scheduledTasks.clear()}scanPending(){for(const e of[...this.scheduledTasks.values()]){const s=e.fireAt-this.now();if(s<=0){e.timeoutHandle!=null&&this.disarmTask(e.id),this.fireTask(e);continue}s<=n&&e.timeoutHandle==null&&this.armTask(e)}}registerTask(e){this.scheduledTasks.set(e.id,{...e,timeoutHandle:null})}disarmTask(e){const s=this.scheduledTasks.get(e);s&&s.timeoutHandle!=null&&(this.clearTimer(s.timeoutHandle),s.timeoutHandle=null)}tryArm(e){const s=this.scheduledTasks.get(e);if(!s)return;const t=s.fireAt-this.now();if(t<=0){this.fireTask(s);return}t<=n&&s.timeoutHandle==null&&this.armTask(s)}armTask(e){const s=e.fireAt-this.now();if(s<=0){this.fireTask(e);return}this.disarmTask(e.id);const t=Math.min(s,n),i=this.setTimer(()=>this.onTimer(e.id),t);this.scheduledTasks.set(e.id,{...e,timeoutHandle:i})}onTimer(e){const s=this.scheduledTasks.get(e);if(s){if(s.timeoutHandle=null,s.fireAt<=this.now()){this.fireTask(s);return}this.armTask(s)}}startScanLoop(){this.scanIntervalHandle==null&&(this.scanIntervalHandle=this.setIntervalFn(()=>this.scanPending(),this.armScanIntervalMs))}fireTask(e){if(!this.scheduledTasks.has(e.id))return;this.disarmTask(e.id);const s=this.now(),t={id:e.id,sessionName:e.sessionName,windowIndex:e.windowIndex,text:e.text,fireAt:e.fireAt,createdAt:e.createdAt};try{this.sendKeys(e.sessionName,e.windowIndex,e.text),this.record({...t,triggeredAt:s,status:"ok"})}catch(i){const d=String(i?.message??i);this.onError(`[scheduler] send-keys to ${e.sessionName}:${e.windowIndex} failed: ${d}`),this.record({...t,triggeredAt:s,status:"error",error:d})}this.scheduledTasks.delete(e.id),this.deps.db.data.scheduledTasks=this.deps.db.data.scheduledTasks.filter(i=>i.id!==e.id),this.deps.db.write().catch(this.onError)}}export{I as SchedulerService,w as isValidRescheduleInput,y as isValidScheduleInput,m as sendTmuxKeys};
@@ -0,0 +1,253 @@
1
+ function s(){return`
2
+ #terminal-layout {
3
+ display: flex;
4
+ flex: 1;
5
+ min-height: 0;
6
+ overflow: hidden;
7
+ }
8
+ #session-sidebar {
9
+ width: 220px;
10
+ flex-shrink: 0;
11
+ background: var(--panel-bg);
12
+ border-right: 1px solid var(--panel-border);
13
+ display: flex;
14
+ flex-direction: column;
15
+ overflow: hidden;
16
+ transition: width 0.2s ease, border-color 0.2s ease;
17
+ }
18
+ #session-sidebar.collapsed {
19
+ width: 0;
20
+ border-right-color: transparent;
21
+ }
22
+ #session-sidebar .sidebar-header {
23
+ display: flex;
24
+ align-items: center;
25
+ justify-content: space-between;
26
+ padding: 10px 12px;
27
+ border-bottom: 1px solid var(--panel-border);
28
+ font-size: var(--text-xs);
29
+ font-weight: 600;
30
+ letter-spacing: 0.05em;
31
+ text-transform: uppercase;
32
+ color: var(--panel-accent);
33
+ font-family: var(--font-mono);
34
+ flex-shrink: 0;
35
+ }
36
+ #session-sidebar .sidebar-list {
37
+ flex: 1;
38
+ overflow-y: auto;
39
+ padding: 8px 0;
40
+ scrollbar-width: thin;
41
+ scrollbar-color: var(--panel-border) transparent;
42
+ }
43
+ #session-sidebar .sidebar-list::-webkit-scrollbar { width: 4px; }
44
+ #session-sidebar .sidebar-list::-webkit-scrollbar-track { background: transparent; }
45
+ #session-sidebar .sidebar-list::-webkit-scrollbar-thumb {
46
+ background: var(--panel-border); border-radius: 2px;
47
+ }
48
+ #session-sidebar .sidebar-list::-webkit-scrollbar-thumb:hover { background: var(--panel-muted); }
49
+ #session-sidebar .sidebar-empty {
50
+ padding: 16px;
51
+ text-align: center;
52
+ color: var(--panel-muted);
53
+ font-size: var(--text-xs);
54
+ font-family: var(--font-mono);
55
+ }
56
+ #session-sidebar .sidebar-row {
57
+ display: flex;
58
+ align-items: center;
59
+ gap: 8px;
60
+ width: 100%;
61
+ min-height: 40px;
62
+ padding: 8px 12px;
63
+ background: none;
64
+ border: none;
65
+ border-bottom: 1px solid var(--panel-border);
66
+ color: var(--page-fg);
67
+ cursor: pointer;
68
+ text-align: left;
69
+ font-family: var(--font-mono);
70
+ font-size: var(--text-sm);
71
+ transition: background 0.15s;
72
+ text-decoration: none;
73
+ }
74
+ #session-sidebar .sidebar-row:last-child { border-bottom: none; }
75
+ #session-sidebar .sidebar-row:hover { background: color-mix(in srgb, var(--panel-accent) 8%, transparent); }
76
+ #session-sidebar .sidebar-row.current {
77
+ background: color-mix(in srgb, var(--panel-accent) 12%, transparent);
78
+ color: var(--panel-accent);
79
+ font-weight: 500;
80
+ }
81
+ #session-sidebar .sidebar-row-name {
82
+ flex: 1;
83
+ min-width: 0;
84
+ overflow: hidden;
85
+ text-overflow: ellipsis;
86
+ white-space: nowrap;
87
+ }
88
+ #session-sidebar .sidebar-row-dot {
89
+ width: 8px;
90
+ height: 8px;
91
+ border-radius: 50%;
92
+ background: var(--panel-muted);
93
+ flex-shrink: 0;
94
+ }
95
+ #session-sidebar .sidebar-row.current .sidebar-row-dot { background: var(--panel-accent); }
96
+ #session-sidebar .sidebar-row.attached .sidebar-row-dot { background: var(--panel-success); }
97
+ #session-sidebar-toggle {
98
+ display: none;
99
+ align-items: center;
100
+ justify-content: center;
101
+ background: none;
102
+ border: none;
103
+ color: var(--panel-muted);
104
+ cursor: pointer;
105
+ min-width: 44px;
106
+ min-height: 44px;
107
+ padding: 8px;
108
+ border-radius: 10px;
109
+ transition: color 0.15s, background 0.15s;
110
+ font-size: var(--text-sm);
111
+ }
112
+ #session-sidebar-toggle:hover { color: var(--panel-accent); background: color-mix(in srgb, var(--panel-accent) 8%, transparent); }
113
+ #session-sidebar-toggle svg { width: 18px; height: 18px; fill: currentColor; }
114
+ @media (max-width: 768px) {
115
+ #session-sidebar {
116
+ position: fixed;
117
+ left: 0;
118
+ top: 0;
119
+ height: 100%;
120
+ z-index: 1000;
121
+ width: min(280px, 80vw);
122
+ transform: translateX(-100%);
123
+ transition: transform 0.25s ease;
124
+ }
125
+ #session-sidebar.open { transform: translateX(0); }
126
+ #session-sidebar.collapsed { width: min(280px, 80vw); }
127
+ #session-sidebar-backdrop {
128
+ position: fixed;
129
+ inset: 0;
130
+ background: rgba(0,0,0,0.45);
131
+ z-index: 999;
132
+ opacity: 0;
133
+ pointer-events: none;
134
+ transition: opacity 0.2s ease;
135
+ }
136
+ #session-sidebar-backdrop.open { opacity: 1; pointer-events: auto; }
137
+ #session-sidebar-toggle { display: flex; }
138
+ }
139
+ @media (min-width: 769px) {
140
+ #session-sidebar-backdrop { display: none; }
141
+ }
142
+ `}function i(e){return`
143
+ <div id="session-sidebar-backdrop"></div>
144
+ <aside id="session-sidebar" aria-label="Sessions">
145
+ <div class="sidebar-header">
146
+ <span>Sessions</span>
147
+ <button type="button" id="session-sidebar-close" class="header-icon-btn" aria-label="Close sessions">\xD7</button>
148
+ </div>
149
+ <div class="sidebar-list" id="session-sidebar-list">
150
+ <div class="sidebar-empty">Loading sessions\u2026</div>
151
+ </div>
152
+ </aside>`}function n(e){return`
153
+ (function () {
154
+ const SIDEBAR_CURRENT = ${JSON.stringify(e)};
155
+ const sidebar = document.getElementById('session-sidebar');
156
+ const backdrop = document.getElementById('session-sidebar-backdrop');
157
+ const list = document.getElementById('session-sidebar-list');
158
+ const toggleBtn = document.getElementById('session-sidebar-toggle');
159
+ const closeBtn = document.getElementById('session-sidebar-close');
160
+
161
+ function isMobile() {
162
+ return window.innerWidth <= 768;
163
+ }
164
+
165
+ function openSidebar() {
166
+ if (isMobile()) {
167
+ sidebar.classList.add('open');
168
+ backdrop.classList.add('open');
169
+ } else {
170
+ sidebar.classList.remove('collapsed');
171
+ try { localStorage.setItem('tmux-web:session-sidebar', 'open'); } catch {}
172
+ }
173
+ }
174
+
175
+ function closeSidebar() {
176
+ if (isMobile()) {
177
+ sidebar.classList.remove('open');
178
+ backdrop.classList.remove('open');
179
+ } else {
180
+ sidebar.classList.add('collapsed');
181
+ try { localStorage.setItem('tmux-web:session-sidebar', 'collapsed'); } catch {}
182
+ }
183
+ }
184
+
185
+ function toggleSidebar() {
186
+ if (isMobile()) {
187
+ sidebar.classList.contains('open') ? closeSidebar() : openSidebar();
188
+ } else {
189
+ sidebar.classList.contains('collapsed') ? openSidebar() : closeSidebar();
190
+ }
191
+ }
192
+
193
+ function renderSessions(sessions) {
194
+ list.innerHTML = '';
195
+ if (!sessions || !sessions.length) {
196
+ list.innerHTML = '<div class="sidebar-empty">No tmux sessions</div>';
197
+ return;
198
+ }
199
+ sessions.forEach((session) => {
200
+ const row = document.createElement('a');
201
+ row.className = 'sidebar-row' +
202
+ (session.name === SIDEBAR_CURRENT ? ' current' : '') +
203
+ (session.attached ? ' attached' : '');
204
+ row.href = '/s/' + encodeURIComponent(session.name);
205
+ row.title = session.name + ' \xB7 ' + session.windows + ' window' + (session.windows === 1 ? '' : 's');
206
+
207
+ const dot = document.createElement('span');
208
+ dot.className = 'sidebar-row-dot';
209
+
210
+ const name = document.createElement('span');
211
+ name.className = 'sidebar-row-name';
212
+ name.textContent = session.name;
213
+
214
+ row.appendChild(dot);
215
+ row.appendChild(name);
216
+ list.appendChild(row);
217
+ });
218
+ }
219
+
220
+ function renderSidebar() {
221
+ const cfg = window.__TMUX_WEB_TERMINAL__;
222
+ const data = cfg && cfg.sidebarSessions ? cfg.sidebarSessions : { pinned: [], recent: [] };
223
+ const sessions = (data.recent || []).concat(data.pinned ? data.pinned.map((p) => ({
224
+ name: p.sessionName,
225
+ windows: p.windows ?? 0,
226
+ attached: p.attached ?? false,
227
+ })) : []);
228
+ const seen = new Set();
229
+ const unique = [];
230
+ sessions.forEach((s) => { if (!seen.has(s.name)) { seen.add(s.name); unique.push(s); } });
231
+ unique.sort((a, b) => {
232
+ if (a.name === SIDEBAR_CURRENT) return -1;
233
+ if (b.name === SIDEBAR_CURRENT) return 1;
234
+ return a.name.localeCompare(b.name);
235
+ });
236
+ renderSessions(unique);
237
+ }
238
+
239
+ if (toggleBtn) toggleBtn.addEventListener('click', toggleSidebar);
240
+ if (closeBtn) closeBtn.addEventListener('click', closeSidebar);
241
+ if (backdrop) backdrop.addEventListener('click', closeSidebar);
242
+
243
+ // Restore desktop collapsed state.
244
+ try {
245
+ const saved = localStorage.getItem('tmux-web:session-sidebar');
246
+ if (saved === 'collapsed' && !isMobile()) sidebar.classList.add('collapsed');
247
+ } catch {}
248
+
249
+ renderSidebar();
250
+ })();
251
+ `}function o(){return`<button type="button" id="session-sidebar-toggle" class="header-icon-btn" title="Toggle sessions sidebar" aria-label="Toggle sessions sidebar">
252
+ <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/></svg>
253
+ </button>`}export{s as sessionSidebarCSS,i as sessionSidebarHTML,n as sessionSidebarScript,o as sessionSidebarToggleButtonHTML};
@@ -1 +1 @@
1
- const e={template:"warm-clay",shell:{pageBg:"#faf8f5",pageFg:"#2d2926",panelBg:"#ffffff",panelBorder:"#ebe5df",panelMuted:"#7b726b",panelAccent:"#b86b52",panelAccentOn:"#ffffff",panelSuccess:"#6b9e7c",terminalBg:"#2b2623",headerGradient:"linear-gradient(180deg, rgba(255,255,255,0.98), rgba(250,248,245,0.98))"},terminal:{foreground:"#e8e2dc",background:"#2b2623",cursor:"#e8e2dc",cursorAccent:"#2b2623",selectionBackground:"rgba(184, 107, 82, 0.35)",selectionForeground:"#e8e2dc",black:"#2b2623",red:"#d88a8a",green:"#8fc49f",yellow:"#e0c28a",blue:"#8ab4d8",magenta:"#c9a5c9",cyan:"#8ad0d0",white:"#d8d0c8",brightBlack:"#4a433e",brightRed:"#e8a0a0",brightGreen:"#a5d6b2",brightYellow:"#efd69a",brightBlue:"#a5c8e8",brightMagenta:"#d9b8d9",brightCyan:"#a0dede",brightWhite:"#f5f0eb"}};export{e as warmClayTheme};
1
+ const e={template:"warm-clay",shell:{pageBg:"#faf8f5",pageFg:"#2d2926",panelBg:"#ffffff",panelBorder:"#ebe5df",panelMuted:"#7b726b",panelAccent:"#b86b52",panelAccentOn:"#ffffff",panelSuccess:"#6b9e7c",terminalBg:"#faf8f5",headerGradient:"linear-gradient(180deg, rgba(255,255,255,0.98), rgba(250,248,245,0.98))"},terminal:{foreground:"#2d2926",background:"#faf8f5",cursor:"#2d2926",cursorAccent:"#faf8f5",selectionBackground:"rgba(184, 107, 82, 0.35)",selectionForeground:"#2d2926",black:"#2d2926",red:"#b94a4a",green:"#3d7a4f",yellow:"#9a7a2f",blue:"#3b6b9e",magenta:"#8a5a8a",cyan:"#2f8a8a",white:"#7b726b",brightBlack:"#5a524c",brightRed:"#c94f4f",brightGreen:"#4a9060",brightYellow:"#b08630",brightBlue:"#4477b0",brightMagenta:"#9c6a9c",brightCyan:"#389898",brightWhite:"#2d2926"}};export{e as warmClayTheme};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmux-weblink",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Access your tmux sessions from the browser. A continuation of tmux-web and persalink.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -36,7 +36,7 @@
36
36
  "license": "MIT",
37
37
  "repository": {
38
38
  "type": "git",
39
- "url": "https://github.com/Willlee11/tmux-weblink"
39
+ "url": "git+https://github.com/Willlee11/tmux-weblink.git"
40
40
  },
41
41
  "engines": {
42
42
  "node": ">=22"