v0.17.0 ab8e178

hx-sse.min.js RawHistoryFolder

1(()=>{let e,t=new Set;function n(e){for(let t of Object.keys(e))"last-event-id"===t.toLowerCase()&&delete e[t]}async function*r(e){let t=e.reader,n=e.lastEventId,r=new TextDecoder,s="",a=!1,i=!1,o={data:"",event:"",retry:null},c=!0;try{for(;;){let{done:e,value:l}=await t.read();if(e)break;let m=r.decode(l,{stream:!0});c&&(65279===m.charCodeAt(0)&&(m=m.slice(1)),c=!1),s+=m;let d=s.split(/\r\n|\r|\n/);s=d.pop()||"";for(let e of d){if(!e){(a||i||o.event)&&(yield{...o,id:n,hasData:a,hasId:i}),a=!1,i=!1,o={data:"",event:"",retry:null};continue}let t,r,s=e.indexOf(":");if(0!==s)if(s<0?(t=e,r=""):(t=e.slice(0,s),r=e.slice(s+1)," "===r[0]&&(r=r.slice(1))),"data"===t)o.data+=(a?"\n":"")+r,a=!0;else if("event"===t)o.event=r;else if("id"===t)r.includes("\0")||(n=r,i=!0);else if("retry"===t){let e=parseInt(r,10);isNaN(e)||(o.retry=e)}}}}finally{t.releaseLock(),e.reader=null}}async function s(t,s){let i=t.sourceElement,o=function(t){let n=null!=e.attributeValue(t,"hx-sse:connect"),r=e.HCON.parse(e.attributeValue(t,"hx-config")).sse||{};return{reconnect:n,reconnectDelay:500,reconnectMaxDelay:6e4,reconnectMaxAttempts:1/0,reconnectJitter:.3,pauseOnBackground:n,releaseOn:n?"immediate":"end",...htmx.config.sse,...r}}(i),c=!1;function l(){s&&(s(),s=null)}"immediate"===o.releaseOn&&l();let m={url:t.request.action,config:o,abortController:{abort:t.request.abort,signal:t.request.signal},reader:null,lastEventId:"",delayCanceller:null,visibilityHandler:null,attempt:0,cancelled:!1,status:null};e.htmxProp(i).sse=m;let d=!1,u=null;if(o.pauseOnBackground){let e=()=>{document.hidden?(d=!0,m.reader?.cancel()):d&&(d=!1,u&&u())};document.addEventListener("visibilitychange",e),m.visibilityHandler=e}if(m.cancelled=!1,!e.triggerHtmxEvent(i,"htmx:sse:before:connection",{connection:m})||m.cancelled)return void a(i,"cancelled");m.status=t.response.status,e.triggerHtmxEvent(i,"htmx:sse:after:connection",{connection:m});let h=t.response.raw;try{for(;i.isConnected;){if(m.attempt>0){if(d){if(await new Promise(e=>{u=e}),u=null,!i.isConnected)break;m.attempt=1,c=!0}if(!c&&(!o.reconnect||m.attempt>o.reconnectMaxAttempts))break;let r=htmx.parseInterval(o.reconnectDelay)??o.reconnectDelay,s=htmx.parseInterval(o.reconnectMaxDelay)??o.reconnectMaxDelay,a=Math.min(r*Math.pow(2,m.attempt-1),s);if(o.reconnectJitter>0){let e=a*o.reconnectJitter;a=Math.max(0,a+(2*Math.random()-1)*e)}if(m.cancelled=!1,!e.triggerHtmxEvent(i,"htmx:sse:before:connection",{connection:m})||m.cancelled)break;if(await new Promise(e=>{m.delayCanceller=e,setTimeout(e,a)}),m.delayCanceller=null,!i.isConnected)break;let l=new AbortController;m.abortController=l;try{n(t.request.headers),m.lastEventId&&(t.request.headers["Last-Event-ID"]=m.lastEventId),h=await fetch(t.request.action,{...t.request,signal:l.signal})}catch(t){if(l.signal.aborted)break;e.triggerHtmxEvent(i,"htmx:sse:error",{connection:m,error:t}),c=!1,m.attempt++;continue}if(!h.ok){e.triggerHtmxEvent(i,"htmx:sse:error",{connection:m,error:new Error(`SSE reconnect failed with status ${h.status}`),status:h.status}),c=!1,m.attempt++;continue}m.status=h.status,e.triggerHtmxEvent(i,"htmx:sse:after:connection",{connection:m}),m.attempt=0}c=!1;try{m.reader=h.body.getReader();for await(let s of r(m)){if(!i.isConnected||c)break;if(s.hasId&&(m.lastEventId=s.id,s.id||n(t.request.headers)),!s.hasData&&!s.event)continue;let r=[],d={connection:m,message:{data:s.data,event:s.event,id:s.id},cancelled:!1,waitUntil:e=>r.push(Promise.resolve(e))},u=e.triggerHtmxEvent(i,"htmx:sse:before:message",d);if(await Promise.all(r),u&&!d.cancelled){if(null!=s.retry&&(o.reconnectDelay=s.retry),d.message.event){"hx:release"===d.message.event&&l(),htmx.trigger(i,d.message.event,{data:d.message.data,id:d.message.id}),e.triggerHtmxEvent(i,"htmx:sse:after:message",{connection:m,message:d.message});let t=e.attributeValue(i,"hx-sse:close");if(t&&d.message.event===t)return void a(i,"message");continue}t.text=d.message.data,t.swap.includes("swapEmpty")||(t.swap+=" swapEmpty:false"),await htmx.swap(t),"first"===o.releaseOn&&l(),e.triggerHtmxEvent(i,"htmx:sse:after:message",{connection:m,message:d.message})}}}catch(t){m.abortController?.signal?.aborted||"AbortError"===t.name||e.triggerHtmxEvent(i,"htmx:sse:error",{connection:m,error:t})}if(!i.isConnected)break;m.attempt++}}finally{l(),a(i,i.isConnected?"ended":"removed")}}function a(t,n){let r=t?._htmx?.sse;r&&(r.abortController?.abort(),r.reader?.cancel?.(),r.delayCanceller&&r.delayCanceller(),r.visibilityHandler&&document.removeEventListener("visibilitychange",r.visibilityHandler),e.triggerHtmxEvent(t,"htmx:sse:close",{connection:r,reason:n||"cleanup"}),delete t._htmx.sse)}htmx.registerExtension("sse",{init:t=>{e=t},htmx_config_request:(e,{ctx:{request:t}})=>{t.headers.Accept=`${t.headers.Accept??t.headers.accept??"text/html"}, text/event-stream`},htmx_before_response:(t,n)=>{let r,a=n.ctx,i=a.response.raw.headers.get("Content-Type");if(i?.includes("text/event-stream"))return clearTimeout(a.requestTimeout),a.extensionPromise=new Promise(e=>r=e),s(a,r).catch(n=>{"AbortError"!==n.name&&e.triggerHtmxEvent(t,"htmx:sse:error",{error:n,url:a.request.action})}),!1},htmx_after_process:n=>{let r=htmx.config.metaCharacter||":",s=n=>{!function(e){for(let n of["sse-connect","sse-close","sse-swap"])e.hasAttribute(n)&&!t.has(n)&&("sse-swap"===n?console.warn("htmx: [hx-sse] sse-swap is removed in htmx 4. Unnamed SSE messages are swapped automatically. Named events are dispatched as DOM events."):console.warn(`htmx: [hx-sse] legacy attribute ${n} is deprecated; use hx-sse:${n.slice(4)} instead`),t.add(n))}(n);for(let e of["connect","close"]){let t=`sse-${e}`;if(!n.hasAttribute(t))continue;let s=(htmx.config.prefix||"hx-")+"sse"+r+e;n.hasAttribute(s)||n.setAttribute(s,n.getAttribute(t))}!function(t){let n=e.attributeValue(t,"hx-sse:connect");if(!n)return;if(t._htmx?.sse)return;let r=e.attributeValue(t,"hx-trigger")||"load";e.onTrigger(t,r,()=>{t._htmx?.sse||htmx.ajax("GET",n,{source:t})})}(n)};s(n);let a=`[${CSS.escape("hx-sse"+r+"connect")}]`;htmx.config.prefix&&(a+=`,[${CSS.escape(htmx.config.prefix+"sse"+r+"connect")}]`),n.querySelectorAll(`${a},[sse-connect],[sse-close],[sse-swap]`).forEach(s)},htmx_before_cleanup:e=>{a(e)}})})();

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 45

↑↓ moveenter openesc close