buninu-linux 0.17.60 → 0.18.62

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.
Files changed (38) hide show
  1. package/NOTICE.md +46 -46
  2. package/README.md +4 -4
  3. package/README.zh.md +4 -4
  4. package/initramfs/bin/tar +32 -15
  5. package/initramfs/bin/unzip +74 -19
  6. package/initramfs/buninu/CHANGELOG.md +19 -0
  7. package/initramfs/buninu/apps/bunmsh/CHANGELOG.md +21 -0
  8. package/initramfs/buninu/apps/bunmsh/package.json +1 -1
  9. package/initramfs/buninu/apps/bunmsh/src/find.js +135 -56
  10. package/initramfs/buninu/apps/bunmsh/src/main.js +12 -1
  11. package/initramfs/buninu/apps/bunmsh/src/shell.js +8 -2
  12. package/initramfs/buninu/apps/bunmsh/test/find.test.js +103 -0
  13. package/initramfs/buninu/apps/bunmsh/test/interactive-typeahead.test.js +40 -0
  14. package/initramfs/buninu/apps/bunmsh/test/posix-bootstrap.test.js +9 -5
  15. package/initramfs/buninu/apps/bunmsh/test/reference.test.js +2 -5
  16. package/initramfs/buninu/apps/bunmsh/test/shell2.test.js +6 -5
  17. package/initramfs/buninu/apps/bunmsh/test/shell3.test.js +8 -7
  18. package/initramfs/buninu/apps/bunmsh/test/support/terminal.js +10 -0
  19. package/initramfs/buninu/apps/jsgotty/bunfig.toml +3 -0
  20. package/initramfs/buninu/apps/jsgotty/gotty.js +14 -1
  21. package/initramfs/buninu/apps/jsgotty/kitty-flow.js +40 -0
  22. package/initramfs/buninu/apps/jsgotty/package-lock.json +2 -2
  23. package/initramfs/buninu/apps/jsgotty/package.json +1 -1
  24. package/initramfs/buninu/apps/jsgotty/static/js/kitty.js +32 -4
  25. package/initramfs/buninu/apps/jsgotty/tests/kitty-flow-browser.test.js +43 -0
  26. package/initramfs/buninu/apps/jsgotty/tests/kitty-flow.test.js +55 -0
  27. package/initramfs/buninu/apps/jsgotty/tests/manual/fps-collector.mjs +18 -0
  28. package/initramfs/buninu/apps/jsgotty/tests/manual/fps-debug.js +31 -0
  29. package/initramfs/buninu/apps/jsgotty/tests/manual/js-eval-show-fps.js +35 -0
  30. package/initramfs/buninu/apps/jsgotty/tests/manual/kitty-background-debug.js +24 -0
  31. package/initramfs/buninu/package.json +1 -1
  32. package/package.json +1 -1
  33. package/scripts/arch.js +5 -5
  34. package/scripts/arch.sh +5 -5
  35. package/scripts/fetch.js +50 -15
  36. package/scripts/npm-packages.md +11 -11
  37. package/test/archive-bounds.test.js +128 -0
  38. package/test/fetch-npm.test.js +1 -1
@@ -16,6 +16,7 @@ const { TerminalModeTracker } = require("./terminal-modes.js");
16
16
  const { WebSocketServer } = WebSocket;
17
17
  const { ClipboardHost, handleOSC52 } = require("./clipboard.js");
18
18
  const clipboardHost = new ClipboardHost();
19
+ const { KittyFrameFlow } = require("./kitty-flow.js");
19
20
 
20
21
  const MSG_INPUT = "1";
21
22
  const MSG_PING = "2";
@@ -1856,7 +1857,8 @@ class PtySession {
1856
1857
  remoteAddr,
1857
1858
  encoding: "null",
1858
1859
  readOnly,
1859
- sendQueue: Promise.resolve()
1860
+ sendQueue: Promise.resolve(),
1861
+ kittyFlow: new KittyFrameFlow(graphic => this.queueSend(ws, MSG_KITTY_GRAPHICS + JSON.stringify(graphic)))
1860
1862
  });
1861
1863
  if (this.permitWrite && !readOnly && !this.writerWs) {
1862
1864
  this.writerWs = ws;
@@ -1919,6 +1921,7 @@ class PtySession {
1919
1921
  const writable = this.isWriter(ws);
1920
1922
  this.send(MSG_SET_ROLE, JSON.stringify({
1921
1923
  role: writable ? "writer" : "viewer",
1924
+ kittyFrameFlow: 1,
1922
1925
  writable
1923
1926
  }), ws);
1924
1927
  }
@@ -2452,6 +2455,9 @@ class PtySession {
2452
2455
  }
2453
2456
 
2454
2457
  switch (type) {
2458
+ case "G":
2459
+ try { client.kittyFlow.control(JSON.parse(payload)); } catch {}
2460
+ break;
2455
2461
  case "C":
2456
2462
  handleOSC52(this, ws, payload, clipboardHost);
2457
2463
  break;
@@ -2580,6 +2586,13 @@ class PtySession {
2580
2586
  return;
2581
2587
  }
2582
2588
 
2589
+ // Negotiate browser-only image credits; never throttle terminal byte output.
2590
+ if (!binary && client.kittyFlow.enabled && payload[0] === MSG_KITTY_GRAPHICS) {
2591
+ const graphic = JSON.parse(payload.slice(1));
2592
+ if (!graphic.frameSequence && client.kittyFlow.graphic(graphic)) return;
2593
+ }
2594
+ if (!binary && payload[0] === MSG_OUTPUT) client.kittyFlow.output(payload.slice(1));
2595
+
2583
2596
  const sendOne = async () => {
2584
2597
  await new Promise((resolve, reject) => {
2585
2598
  try {
@@ -0,0 +1,40 @@
1
+ // Per browser connection. Only replaceable C=1 placements use frame credits;
2
+ // ordinary terminal output, scrolling placements and CLI clients stay ordered.
3
+ class KittyFrameFlow {
4
+ constructor(send) { this.send=send;this.enabled=false;this.visible=true;this.sequence=0;this.inFlight=new Set();this.pending=new Map();this.tail=''; }
5
+ control(message) {
6
+ if(message?.version!==1)return;
7
+ this.enabled=true;
8
+ if(typeof message.visible==='boolean')this.visible=message.visible;
9
+ if(Number.isSafeInteger(message.ack))this.inFlight.delete(message.ack);
10
+ this.flush();
11
+ }
12
+ graphic(graphic) {
13
+ if(!this.enabled)return false;
14
+ if(graphic.kind==='delete'){
15
+ const d=graphic.delete||{};
16
+ for(const [key,g] of this.pending)if(d.scope==='a'||d.scope==='A'||String(g.image.id)===String(d.imageId))this.pending.delete(key);
17
+ return false;
18
+ }
19
+ const c=graphic.control||{},id=c.i||c.I||graphic.image?.id;
20
+ if(graphic.kind!=='placement'||!graphic.image||!id)return false;
21
+ if(String(c.C)!=='1'){this.pending.delete(`${id}:${c.p||'0'}`);return false;}
22
+ const key=`${id}:${c.p||'0'}`;
23
+ // Preserve ordering among different placements when replacing an update.
24
+ this.pending.delete(key);this.pending.set(key,graphic);this.flush();return true;
25
+ }
26
+ output(payload) {
27
+ if(!this.enabled)return;
28
+ const text=this.tail+Buffer.from(payload,'base64').toString('utf8');
29
+ if(text.includes('\x1bc')||text.includes('\f')||/\x1b\[\?(?:1049|1047|47)[hl]/.test(text))this.pending.clear();
30
+ this.tail=text.slice(-32);
31
+ }
32
+ flush() {
33
+ while(this.enabled&&this.visible&&this.inFlight.size<2&&this.pending.size){
34
+ const [key,graphic]=this.pending.entries().next().value;this.pending.delete(key);
35
+ const sequence=++this.sequence;this.inFlight.add(sequence);
36
+ this.send({...graphic,frameSequence:sequence});
37
+ }
38
+ }
39
+ }
40
+ module.exports={KittyFrameFlow};
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "jsgotty",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "jsgotty",
9
- "version": "1.4.0",
9
+ "version": "1.4.1",
10
10
  "dependencies": {
11
11
  "node-addon-api": "^7.1.1",
12
12
  "ws": "^8.18.3"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsgotty",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "Share your terminal as a web application: A JavaScript rewrite of GoTTY",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -1079,7 +1079,31 @@
1079
1079
  state.outputTail = text.slice(-64);
1080
1080
  }
1081
1081
 
1082
- function handleKittyMessage(payload) {
1082
+ // Only a server advertising this capability receives the private control.
1083
+ const frameFlowSockets = new WeakSet();
1084
+ function sendFrameControl(socket, fields) {
1085
+ if (socket?.readyState === 1 && frameFlowSockets.has(socket))
1086
+ socket.send("G" + JSON.stringify({version: 1, ...fields}));
1087
+ }
1088
+ function inspectFrameCapability(socket, data) {
1089
+ if (typeof data !== "string" || data[0] !== "a") return;
1090
+ try {
1091
+ if (JSON.parse(data.slice(1)).kittyFrameFlow !== 1 || frameFlowSockets.has(socket)) return;
1092
+ frameFlowSockets.add(socket);
1093
+ sendFrameControl(socket, {visible: !document.hidden});
1094
+ } catch {}
1095
+ }
1096
+ document.addEventListener("visibilitychange", () => {
1097
+ sendFrameControl(state.socket, {visible: !document.hidden});
1098
+ });
1099
+ function acknowledgeFrame(socket, sequence) {
1100
+ if (!Number.isSafeInteger(sequence)) return;
1101
+ // rAF stops while hidden and also bounds delivery if WebView is suspended
1102
+ // without a visibility event. Credits belong to their original connection.
1103
+ requestAnimationFrame(() => sendFrameControl(socket, {ack: sequence}));
1104
+ }
1105
+
1106
+ function handleKittyMessage(payload, socket) {
1083
1107
  const message = JSON.parse(payload);
1084
1108
  sendBrowserDebug("kitty-message", {
1085
1109
  kind: message.kind,
@@ -1091,7 +1115,9 @@
1091
1115
  // The server records this cursor exactly where the Kitty APC occurred.
1092
1116
  // Do not replace it with xterm's later cursor, which may already have
1093
1117
  // moved because of redraw or scrolling output queued after the APC.
1094
- placeImage(message);
1118
+ Promise.resolve(placeImage(message)).catch(error => {
1119
+ console.warn("Kitty image decode failed", error);
1120
+ }).finally(() => acknowledgeFrame(socket, message.frameSequence));
1095
1121
  return true;
1096
1122
  }
1097
1123
  if (message.kind === "delete") {
@@ -1119,9 +1145,10 @@
1119
1145
  const wrapped = function (event) {
1120
1146
  state.socket = this;
1121
1147
  watchTouchSocket(this);
1148
+ inspectFrameCapability(this, event.data);
1122
1149
  inspectTerminalOutput(event.data);
1123
1150
  if (typeof event.data === "string" && event.data[0] === MSG_KITTY) {
1124
- if (handleKittyMessage(event.data.slice(1))) {
1151
+ if (handleKittyMessage(event.data.slice(1), this)) {
1125
1152
  return;
1126
1153
  }
1127
1154
  }
@@ -1166,9 +1193,10 @@
1166
1193
  const wrapped = function (event) {
1167
1194
  state.socket = this;
1168
1195
  watchTouchSocket(this);
1196
+ inspectFrameCapability(this, event.data);
1169
1197
  inspectTerminalOutput(event.data);
1170
1198
  if (typeof event.data === "string" && event.data[0] === MSG_KITTY) {
1171
- if (handleKittyMessage(event.data.slice(1))) {
1199
+ if (handleKittyMessage(event.data.slice(1), this)) {
1172
1200
  return;
1173
1201
  }
1174
1202
  }
@@ -0,0 +1,43 @@
1
+ // CHROMIUM_HEADLESS=/usr/local/bin/chromium-headless-shell bun test tests/kitty-flow-browser.test.js
2
+ const test=require('node:test'),assert=require('node:assert/strict');
3
+ const fs=require('node:fs'),path=require('node:path'),http=require('node:http');
4
+ const {spawn}=require('node:child_process'),{once}=require('node:events');
5
+ const WebSocket=require('ws'),{KittyFrameFlow}=require('../kitty-flow.js');
6
+ test('real browser resumes latest image after visibility loss or suspended animation callbacks',{skip:!process.env.CHROMIUM_HEADLESS,timeout:30000},async()=>{
7
+ const source=fs.readFileSync(path.join(__dirname,'../static/js/kitty.js'));
8
+ const server=http.createServer((req,res)=>{
9
+ if(req.url==='/kitty.js'){res.setHeader('Content-Type','text/javascript');res.end(source);return;}
10
+ res.end(`<div id="terminal" style="width:800px;height:600px"><div class="xterm-screen"></div></div><div id="kitty-overlay"></div>
11
+ <script>window.hidden=false;Object.defineProperty(document,'hidden',{get:()=>window.hidden});window.held=[];window.suspended=false;const raf=window.requestAnimationFrame.bind(window);window.requestAnimationFrame=fn=>suspended?(held.push(fn),0):raf(fn);</script>
12
+ <script src="/kitty.js"></script><script>window.messages=[];window.socket=new WebSocket('ws://'+location.host);socket.onmessage=e=>messages.push(e.data);</script>`);
13
+ });server.listen(0,'127.0.0.1');await once(server,'listening');
14
+ const wss=new WebSocket.WebSocketServer({server});let flow,socket,count=0,last=null;
15
+ wss.on('connection',ws=>{socket=ws;flow=new KittyFrameFlow(g=>{count++;last=g;ws.send('7'+JSON.stringify(g));});ws.on('message',m=>{if(String(m)[0]==='G')flow.control(JSON.parse(String(m).slice(1)))});ws.send('a{"kittyFrameFlow":1}');});
16
+ const child=spawn(process.env.CHROMIUM_HEADLESS,['--no-sandbox','--disable-gpu','--remote-debugging-port=0','http://127.0.0.1:'+server.address().port],{stdio:['ignore','ignore','pipe']});let cdp;
17
+ try{
18
+ const endpoint=await new Promise((resolve,reject)=>{let log='';child.on('error',reject);child.stderr.on('data',b=>{log+=b;const m=log.match(/DevTools listening on (ws:\/\/[^\s]+)/);if(m)resolve(m[1]);});});
19
+ let tab;for(let i=0;i<100;i++){tab=(await(await fetch('http://'+new URL(endpoint).host+'/json/list')).json()).find(x=>x.type==='page');if(tab)break;await new Promise(r=>setTimeout(r,20));}
20
+ cdp=new WebSocket(tab.webSocketDebuggerUrl);await once(cdp,'open');let id=0;const pending=new Map();cdp.on('message',data=>{const m=JSON.parse(data),p=pending.get(m.id);if(p){pending.delete(m.id);m.error?p.reject(m.error):p.resolve(m.result)}});
21
+ const call=(method,params)=>new Promise((resolve,reject)=>{pending.set(++id,{resolve,reject});cdp.send(JSON.stringify({id,method,params}));});
22
+ const evaluate=async expression=>{const r=await call('Runtime.evaluate',{expression,returnByValue:true,awaitPromise:true});if(r.exceptionDetails)throw Error(JSON.stringify(r.exceptionDetails));return r.result.value;};
23
+ const until=async check=>{for(let i=0;i<100;i++){if(await check())return;await new Promise(r=>setTimeout(r,20));}throw Error('fixture timed out')};
24
+ await until(()=>flow?.enabled);
25
+ const png=await evaluate(`(()=>{const c=document.createElement('canvas');c.width=c.height=1;c.getContext('2d').fillRect(0,0,1,1);return c.toDataURL().split(',')[1]})()`);
26
+ const frame=n=>({kind:'placement',control:{i:'1',C:'1',c:'2',r:'2'},cursor:{row:1,col:1},image:{id:'1',format:100,data:png},testFrame:n});
27
+ flow.graphic(frame(0));await until(()=>flow.inFlight.size===0);
28
+ await evaluate('window.hidden=true;document.dispatchEvent(new Event("visibilitychange"))');await until(()=>flow.visible===false);
29
+ const before=count;for(let n=1;n<=1800;n++)flow.graphic(frame(n));assert.equal(count,before);assert.equal(flow.pending.size,1);
30
+ socket.send('1'+Buffer.from('TEXT_WHILE_HIDDEN').toString('base64'));
31
+ await until(async()=>await evaluate('messages.some(x=>x.startsWith("1"))'));
32
+ await evaluate('window.hidden=false;document.dispatchEvent(new Event("visibilitychange"))');await until(()=>flow.inFlight.size===0&&last.testFrame===1800);
33
+ assert.equal(count,before+1);
34
+ // A renderer can freeze without delivering visibilitychange. Missing rAF
35
+ // acknowledgements still bound the server to two outstanding frames.
36
+ await evaluate('window.suspended=true');const baseline=count;
37
+ for(let n=1801;n<=3600;n++)flow.graphic(frame(n));assert.equal(count,baseline+2);assert.equal(flow.pending.size,1);
38
+ await until(async()=>await evaluate('held.length>=2'));
39
+ await evaluate('window.suspended=false;for(const fn of held.splice(0))requestAnimationFrame(fn)');
40
+ await until(()=>flow.inFlight.size===0&&last.testFrame===3600);assert.equal(count,baseline+3);
41
+ await until(async()=>await evaluate('document.querySelector("img.kitty-image__content").naturalWidth===1'));
42
+ }finally{cdp?.close();for(const ws of wss.clients)ws.terminate();wss.close();server.close();child.kill();}
43
+ });
@@ -0,0 +1,55 @@
1
+ const {test}=require('node:test');
2
+ const assert=require('node:assert/strict');
3
+ const {KittyFrameFlow}=require('../kitty-flow.js');
4
+ const graphic=(n,id='1')=>({kind:'placement',control:{i:id,C:'1'},cursor:{row:2,col:1},image:{id,data:String(n)}});
5
+ test('browser credits bound suspension to two frames and latest replacement',()=>{
6
+ const sent=[],flow=new KittyFrameFlow(g=>sent.push(g));
7
+ assert.equal(flow.graphic(graphic(0)),false); // CLI and old WebViews unchanged
8
+ flow.control({version:1,visible:true});
9
+ for(let n=0;n<1800;n++)assert.equal(flow.graphic(graphic(n)),true);
10
+ assert.equal(sent.length,2);assert.equal(flow.pending.size,1);
11
+ flow.control({version:1,ack:9999});assert.equal(sent.length,2);
12
+ flow.control({version:1,ack:sent[0].frameSequence});
13
+ assert.equal(sent.length,3);assert.equal(sent[2].image.data,'1799');
14
+ flow.control({version:1,ack:sent[0].frameSequence});assert.equal(sent.length,3);
15
+ });
16
+ test('hidden viewers keep only latest per placement and resume without another update',()=>{
17
+ const sent=[],flow=new KittyFrameFlow(g=>sent.push(g));flow.control({version:1,visible:false});
18
+ for(let n=0;n<100;n++){flow.graphic(graphic(n,'1'));flow.graphic(graphic(n,'2'));}
19
+ assert.equal(sent.length,0);assert.equal(flow.pending.size,2);
20
+ flow.control({version:1,visible:true});assert.deepEqual(sent.map(g=>g.image.data),['99','99']);
21
+ });
22
+ test('delete, terminal reset and alternate screen do not resurrect pending images',()=>{
23
+ const sent=[],flow=new KittyFrameFlow(g=>sent.push(g));flow.control({version:1,visible:false});
24
+ flow.graphic(graphic(1));flow.graphic(graphic(2,'2'));
25
+ assert.equal(flow.graphic({kind:'delete',delete:{imageId:'1'}}),false);
26
+ assert.equal(flow.pending.size,1);
27
+ flow.output(Buffer.from('\x1b[?104').toString('base64'));
28
+ flow.output(Buffer.from('9l').toString('base64'));assert.equal(flow.pending.size,0);
29
+ flow.graphic(graphic(3));flow.graphic({kind:'delete',delete:{scope:'a'}});
30
+ flow.control({version:1,visible:true});assert.equal(sent.length,0);
31
+ });
32
+ test('connections have independent credits; normal scrolling images are unchanged',()=>{
33
+ const one=[],two=[],a=new KittyFrameFlow(g=>one.push(g)),b=new KittyFrameFlow(g=>two.push(g));
34
+ a.control({version:1,visible:false});b.control({version:1,visible:true});
35
+ a.graphic(graphic(1));b.graphic(graphic(1));assert.equal(one.length,0);assert.equal(two.length,1);
36
+ const scroll=graphic(2);delete scroll.control.C;assert.equal(a.graphic(scroll),false);
37
+ // A later ordinary placement replaces the same deferred position immediately.
38
+ assert.equal(a.pending.size,0);a.graphic(graphic(3));
39
+ a.output(Buffer.from('ordinary text').toString('base64'));assert.equal(a.pending.size,1);
40
+ });
41
+
42
+ test('browser negotiates only on advertisement and acknowledges after animation opportunity',async()=>{
43
+ const fs=require('node:fs'),vm=require('node:vm'),source=fs.readFileSync(require('node:path').join(__dirname,'../static/js/kitty.js'),'utf8');
44
+ const start=source.indexOf(' // Only a server advertising'),end=source.indexOf(' function deleteImages');
45
+ const helpers=source.slice(start,source.indexOf(' function handleKittyMessage',start));
46
+ const frames=[],events={},sent=[],socket={readyState:1,send:m=>sent.push(JSON.parse(m.slice(1)))};
47
+ const document={hidden:true,addEventListener:(k,fn)=>events[k]=fn};
48
+ const api=vm.runInNewContext(`(()=>{${helpers};return {inspectFrameCapability,acknowledgeFrame}})()`,{document,state:{socket},requestAnimationFrame:fn=>frames.push(fn)});
49
+ api.inspectFrameCapability(socket,'a{"role":"viewer"}');assert.equal(sent.length,0);
50
+ api.inspectFrameCapability(socket,'a{"kittyFrameFlow":1}');assert.equal(sent[0].visible,false);
51
+ api.acknowledgeFrame(socket,1);assert.equal(sent.length,1); // suspended rAF
52
+ document.hidden=false;events.visibilitychange();assert.equal(sent[1].visible,true);
53
+ frames.shift()();assert.equal(sent[2].ack,1);
54
+ socket.readyState=3;api.acknowledgeFrame(socket,2);frames.shift()();assert.equal(sent.length,3);
55
+ });
@@ -0,0 +1,18 @@
1
+ // bun tests/manual/fps-collector.mjs [log.jsonl]
2
+ import {appendFileSync} from 'node:fs';
3
+ import {resolve} from 'node:path';
4
+ const log=resolve(process.argv[2]||'jsgotty-fps.jsonl'),token=crypto.randomUUID();
5
+ const source=await Bun.file(new URL('./fps-debug.js',import.meta.url)).text();
6
+ let rows=0;
7
+ const headers={'Access-Control-Allow-Origin':'*','Access-Control-Allow-Methods':'POST, OPTIONS','Access-Control-Allow-Headers':'Content-Type','Access-Control-Allow-Private-Network':'true','Cache-Control':'no-store'};
8
+ const server=Bun.serve({hostname:'127.0.0.1',port:0,maxRequestBodySize:262144,async fetch(req){
9
+ if(new URL(req.url).pathname!=='/'+token)return new Response('',{status:404});
10
+ if(req.method==='OPTIONS')return new Response(null,{status:204,headers});
11
+ if(req.method!=='POST')return new Response('',{status:405,headers});
12
+ if(rows>=1200)return new Response('',{status:429,headers});
13
+ try{const row=await req.json();appendFileSync(log,JSON.stringify({receivedAt:Date.now(),...row})+'\n',{mode:0o600});rows++;return new Response(null,{status:204,headers});}
14
+ catch{return new Response('',{status:400,headers});}
15
+ }});
16
+ const endpoint=`http://127.0.0.1:${server.port}/${token}`;
17
+ const script=log+'.inject.js';await Bun.write(script,source+'\nwindow.startGottyFpsDebug({endpoint:'+JSON.stringify(endpoint)+'});\n');
18
+ console.log(JSON.stringify({endpoint,log,script,pid:process.pid}));
@@ -0,0 +1,31 @@
1
+ // Eval manually; counts completed Kitty <img> loads, not physical display flips.
2
+ window.startGottyFpsDebug = ({endpoint, duration=600000}={}) => {
3
+ window.stopGottyFpsDebug?.(); window.stopKittyFPS?.();
4
+ const box=document.createElement('div');
5
+ box.style.cssText='position:fixed;top:0;right:0;z-index:2147483647;background:#000b;color:cyan;padding:4px 8px;font:16px monospace;pointer-events:none';
6
+ box.textContent='Kitty: 0 FPS';document.body.append(box);
7
+ const session=Date.now().toString(36),ids=new WeakMap();let id=0,previous=0,started=performance.now(),last=started,frames=[],busy=false,dropped=0,sequence=0,stopped=false;
8
+ const records=[];window.__gottyFpsRecords=records;
9
+ const load=e=>{const el=e.target;if(!el.matches?.('img.kitty-image__content'))return;
10
+ const now=performance.now();if(!ids.has(el))ids.set(el,++id);
11
+ if(frames.length<2000)frames.push({t:now,id:ids.get(el),gap:previous?now-previous:null,w:el.naturalWidth,h:el.naturalHeight});else dropped++;
12
+ previous=now;
13
+ };
14
+ document.addEventListener('load',load,true);
15
+ const flush=()=>{
16
+ const now=performance.now(),batch=frames;frames=[];
17
+ const gaps=batch.map(f=>f.gap).filter(g=>g!==null).sort((a,b)=>a-b);
18
+ const row={session,sequence:sequence++,time:Date.now(),timeOrigin:performance.timeOrigin,elapsed:now-last,fps:batch.length*1000/(now-last),hidden:document.hidden,frames:batch,dropped,
19
+ p95:gaps[Math.floor(gaps.length*.95)]??null,p99:gaps[Math.floor(gaps.length*.99)]??null,max:gaps.at(-1)??null};last=now;
20
+ box.textContent=`Kitty: ${row.fps.toFixed(1)} FPS`;
21
+ records.push(row);if(records.length>600)records.shift();window.__gottyFpsLatest=row;
22
+ if(endpoint&&!busy){busy=true;fetch(endpoint,{method:'POST',headers:{'Content-Type':'text/plain'},body:JSON.stringify(row),signal:AbortSignal.timeout(5000)})
23
+ .then(r=>{if(!r.ok)throw Error('HTTP '+r.status);window.__gottyFpsDeliveryError=null;})
24
+ .catch(e=>{window.__gottyFpsDeliveryError=String(e);dropped+=batch.length;}).finally(()=>{busy=false;});}
25
+ else if(endpoint)dropped+=batch.length;
26
+ };
27
+ const timer=setInterval(flush,1000);
28
+ const expiry=setTimeout(()=>window.stopGottyFpsDebug?.(),Math.max(1000,Math.min(duration,600000)));
29
+ window.stopGottyFpsDebug=()=>{if(stopped)return;stopped=true;document.removeEventListener('load',load,true);clearInterval(timer);clearTimeout(expiry);box.remove();};
30
+ return 'Kitty FPS enabled (image loads; auto-stop in at most 10 minutes)';
31
+ };
@@ -0,0 +1,35 @@
1
+ (() => {
2
+ window.stopKittyFPS?.();
3
+
4
+ const box = document.createElement("div");
5
+ box.style.cssText =
6
+ "position:fixed;top:0;right:0;z-index:2147483647;" +
7
+ "background:#000b;color:cyan;padding:4px 8px;" +
8
+ "font:16px monospace;pointer-events:none";
9
+ box.textContent = "Kitty: 0 FPS";
10
+ document.body.append(box);
11
+
12
+ let count = 0, last = performance.now();
13
+ const loaded = e => {
14
+ if (e.target.matches?.("img.kitty-image__content"))
15
+ count++;
16
+ };
17
+ document.addEventListener("load", loaded, true);
18
+
19
+ const timer = setInterval(() => {
20
+ const now = performance.now();
21
+ box.textContent =
22
+ `Kitty: ${(count * 1000 / (now - last)).toFixed(1)}
23
+ FPS`;
24
+ count = 0;
25
+ last = now;
26
+ }, 1000);
27
+
28
+ window.stopKittyFPS = () => {
29
+ clearInterval(timer);
30
+ document.removeEventListener("load", loaded, true);
31
+ box.remove();
32
+ delete window.stopKittyFPS;
33
+ };
34
+ return "Kitty FPS enabled";
35
+ })()
@@ -0,0 +1,24 @@
1
+ // Eval manually. Counts delivery, bitmap decoding, image loads and animation ticks.
2
+ // Observational only: no frame dropping, focus changes or socket reconnects.
3
+ window.startKittyBackgroundDebug = (endpoint) => {
4
+ window.stopKittyBackgroundDebug?.();
5
+ const parse=JSON.parse,bitmap=window.createImageBitmap,history=[];
6
+ let received=0,bytes=0,decoded=0,loads=0,ticks=0,pending=0,maxPending=0,decodeMs=0,last=performance.now(),busy=false,stopped=false,raf;
7
+ const wrappedParse=function(...args){const v=parse.apply(this,args);if(v?.kind==='placement'&&v.image){received++;bytes+=v.image.data?.length||0;}return v;};
8
+ const wrappedBitmap=async function(...args){const start=performance.now();pending++;maxPending=Math.max(maxPending,pending);try{return await bitmap.apply(this,args)}finally{pending--;decoded++;decodeMs+=performance.now()-start;}};
9
+ JSON.parse=wrappedParse;if(bitmap)window.createImageBitmap=wrappedBitmap;
10
+ const load=e=>{if(e.target.matches?.('img.kitty-image__content'))loads++;};
11
+ document.addEventListener('load',load,true);
12
+ const tick=()=>{ticks++;if(!stopped)raf=requestAnimationFrame(tick)};raf=requestAnimationFrame(tick);
13
+ function flush(reason='interval'){
14
+ const now=performance.now(),row={kind:'background-profile',time:Date.now(),elapsed:now-last,reason,hidden:document.hidden,received,bytes,decoded,loads,ticks,pending,maxPending,decodeMs};
15
+ last=now;received=bytes=decoded=loads=ticks=decodeMs=0;maxPending=pending;
16
+ history.push(row);if(history.length>600)history.shift();window.__kittyBackgroundLatest=row;
17
+ if(endpoint&&!busy){busy=true;fetch(endpoint,{method:'POST',headers:{'Content-Type':'text/plain'},body:JSON.stringify(row),signal:AbortSignal.timeout(5000)}).catch(()=>{}).finally(()=>busy=false);}
18
+ }
19
+ const visibility=()=>flush('visibilitychange');document.addEventListener('visibilitychange',visibility);
20
+ const timer=setInterval(flush,1000),expiry=setTimeout(()=>window.stopKittyBackgroundDebug(),600000);
21
+ window.__kittyBackgroundHistory=history;
22
+ window.stopKittyBackgroundDebug=()=>{stopped=true;if(JSON.parse===wrappedParse)JSON.parse=parse;if(window.createImageBitmap===wrappedBitmap)window.createImageBitmap=bitmap;document.removeEventListener('load',load,true);document.removeEventListener('visibilitychange',visibility);cancelAnimationFrame(raf);clearInterval(timer);clearTimeout(expiry);};
23
+ return 'background diagnostic enabled';
24
+ };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buninu",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Buninu is a portable, self-bootstrapping Unix-like userspace built on Bun that runs across operating systems",
5
5
  "type": "module",
6
6
  "main": "./bin/init.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buninu-linux",
3
- "version": "0.17.60",
3
+ "version": "0.18.62",
4
4
  "description": "A Linux distribution running Bun as PID 1 with the Buninu userspace.",
5
5
  "type": "module",
6
6
  "main": "./index.js",
package/scripts/arch.js CHANGED
@@ -16,8 +16,8 @@ export const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), "..");
16
16
 
17
17
  const pins = {
18
18
  x86_64: {
19
- linuxVirtSha256: "3ff658cf5aada14687238527f48526cef7a75ac8c673fc597f3a65791c86a25e",
20
- linuxLtsSha256: "f205a980e1d683414f5557d26366bee1e52cf128613aa74ef371b3f11b019085",
19
+ linuxVirtSha256: "e121b547d9b86194a641718b52ae1c3e605085c59f31525b1649a59b3d4db0c7",
20
+ linuxLtsSha256: "2e03d0d13959a369764d8d24d51844634d1fd6973a523f4e49166a300c782a06",
21
21
  muslSha256: "573712e2f49c15bfc20a2699f204acdfc74c772722b15e7353d768057fae0e71",
22
22
  stubSha256: "8e64a5a3afee5f930e6e6716be726dc6d405530ac7f8fa5be6251dae68671ec9",
23
23
  libstdcppSha256: "14c987b556f5385a5db18376e788c75f37d85321b8dc1920d926ea7daac1d6f6",
@@ -32,8 +32,8 @@ const pins = {
32
32
  serialMajor: 4,
33
33
  },
34
34
  aarch64: {
35
- linuxVirtSha256: "b931f14a93634469cc8930afe5d6837361325456b620b94e970bb981d841c99c",
36
- linuxLtsSha256: "4d398f1d401597da5e92fa7c3c1af93c4a9988b3747222c0173bfe3ff4cfe4a2",
35
+ linuxVirtSha256: "3a9a0f16adaabee0e4e05e373682249e2a99ab000faebf6aaaf682ae575c661d",
36
+ linuxLtsSha256: "29d6cbf6f432667d5d17080037b3cfd4d28d71c3b0ecb4ad7e53438abf9f24e7",
37
37
  muslSha256: "5e9674b7f41152fe2119093b5cb4c13eaaadb19c2d5422b2d7267913e663ee6e",
38
38
  stubSha256: "a1823d2d7082db555d528f82c1809f276c818aeb5b40f198f4565f104e055c38",
39
39
  libstdcppSha256: "2302e766d4e4926038ec166ecb85837ee884576115236ddb565e3a5fca4a11d7",
@@ -68,7 +68,7 @@ export const config = ({
68
68
  // The Alpine repository keeps only the newest build of a package, so a pin
69
69
  // stops downloading when Alpine bumps it; the hashes then fail loudly.
70
70
  const alpineBase = `${mirror}/v3.24/main/${arch}`;
71
- const kernelVersion = "6.18.60";
71
+ const kernelVersion = "6.18.62";
72
72
  const kernelRelease = `${kernelVersion}-0-${flavor}`;
73
73
  const bunVersion = "1.4.2";
74
74
  const downloadsDir = join(rootDir, "downloads", arch);
package/scripts/arch.sh CHANGED
@@ -44,7 +44,7 @@ fi
44
44
  alpine_mirror=${ALPINE_MIRROR:-https://dl-cdn.alpinelinux.org/alpine}
45
45
  alpine_base="$alpine_mirror/v3.24/main/$arch"
46
46
 
47
- kernel_version=6.18.60
47
+ kernel_version=6.18.62
48
48
  kernel_release="$kernel_version-0-$linux_flavor"
49
49
  linux_package="linux-$linux_flavor-$kernel_version-r0.apk"
50
50
  musl_package=musl-1.2.6-r2.apk
@@ -63,8 +63,8 @@ musl_libc="libc.musl-$arch.so.1"
63
63
 
64
64
  case $arch in
65
65
  x86_64)
66
- linux_virt_sha256=3ff658cf5aada14687238527f48526cef7a75ac8c673fc597f3a65791c86a25e
67
- linux_lts_sha256=f205a980e1d683414f5557d26366bee1e52cf128613aa74ef371b3f11b019085
66
+ linux_virt_sha256=e121b547d9b86194a641718b52ae1c3e605085c59f31525b1649a59b3d4db0c7
67
+ linux_lts_sha256=2e03d0d13959a369764d8d24d51844634d1fd6973a523f4e49166a300c782a06
68
68
  musl_sha256=573712e2f49c15bfc20a2699f204acdfc74c772722b15e7353d768057fae0e71
69
69
  stub_sha256=8e64a5a3afee5f930e6e6716be726dc6d405530ac7f8fa5be6251dae68671ec9
70
70
  libstdcpp_sha256=14c987b556f5385a5db18376e788c75f37d85321b8dc1920d926ea7daac1d6f6
@@ -82,8 +82,8 @@ case $arch in
82
82
  serial_major=4
83
83
  ;;
84
84
  aarch64)
85
- linux_virt_sha256=b931f14a93634469cc8930afe5d6837361325456b620b94e970bb981d841c99c
86
- linux_lts_sha256=4d398f1d401597da5e92fa7c3c1af93c4a9988b3747222c0173bfe3ff4cfe4a2
85
+ linux_virt_sha256=3a9a0f16adaabee0e4e05e373682249e2a99ab000faebf6aaaf682ae575c661d
86
+ linux_lts_sha256=29d6cbf6f432667d5d17080037b3cfd4d28d71c3b0ecb4ad7e53438abf9f24e7
87
87
  musl_sha256=5e9674b7f41152fe2119093b5cb4c13eaaadb19c2d5422b2d7267913e663ee6e
88
88
  stub_sha256=a1823d2d7082db555d528f82c1809f276c818aeb5b40f198f4565f104e055c38
89
89
  libstdcpp_sha256=2302e766d4e4926038ec166ecb85837ee884576115236ddb565e3a5fca4a11d7
package/scripts/fetch.js CHANGED
@@ -76,34 +76,69 @@ export const readTar = (path) => {
76
76
  const links = new Map();
77
77
  let longName = null;
78
78
  let longLink = null;
79
- for (let offset = 0; offset + 512 <= data.length;) {
79
+ let extended = {}, global = {};
80
+ const invalid = (reason) => { throw new Error(`${path}: ${reason}`); };
81
+ const number = (header, start, length) => {
82
+ if (header[start] & 0x80) {
83
+ let value = BigInt(header[start] & 0x7f);
84
+ for (let i = start + 1; i < start + length; i++) value = value * 256n + BigInt(header[i]);
85
+ if (value > BigInt(Number.MAX_SAFE_INTEGER)) invalid("unsafe tar numeric field");
86
+ return Number(value);
87
+ }
88
+ const digits = header.toString("ascii", start, start + length).replace(/\0.*$/s, "").trim();
89
+ if (digits && !/^[0-7]+$/.test(digits)) invalid("invalid tar numeric field");
90
+ return digits ? Number.parseInt(digits, 8) : 0;
91
+ };
92
+ let offset = 0;
93
+ for (; offset + 512 <= data.length;) {
80
94
  const header = data.subarray(offset, offset + 512);
81
- if (header.every((byte) => byte === 0)) { offset += 512; continue; }
95
+ if (header.every((byte) => byte === 0)) {
96
+ offset += 512; longName = longLink = null; extended = {}; global = {}; continue;
97
+ }
98
+ let checksum = 0;
99
+ for (let i = 0; i < 512; i++) checksum += i >= 148 && i < 156 ? 32 : header[i];
100
+ if (checksum !== number(header, 148, 8)) invalid("invalid tar header checksum");
82
101
  const field = (start, length) => header.toString("utf8", start, start + length).replace(/\0.*$/s, "");
83
- const size = Number.parseInt(field(124, 12).trim() || "0", 8);
84
102
  const type = String.fromCharCode(header[156] || 0x30);
103
+ const metadata = ["x", "X", "g", "L", "K"].includes(type);
104
+ const attrs = { ...global, ...extended };
105
+ if (!metadata && attrs.size !== undefined && !/^[0-9]+$/.test(attrs.size)) invalid("invalid pax size");
106
+ const size = !metadata && attrs.size !== undefined ? Number(attrs.size) : number(header, 124, 12);
107
+ const padded = Math.ceil(size / 512) * 512;
108
+ if (!Number.isSafeInteger(size) || size < 0 || !Number.isSafeInteger(padded)
109
+ || padded > data.length - offset - 512) invalid("invalid or truncated tar member size");
85
110
  const body = data.subarray(offset + 512, offset + 512 + size);
86
- offset += 512 + Math.ceil(size / 512) * 512;
87
- if (type === "x") {
88
- // pax records: "LENGTH key=value\n"
89
- for (const record of body.toString("utf8").split("\n")) {
90
- const match = /^\d+ path=(.*)$/.exec(record);
91
- if (match) longName = match[1];
92
- const link = /^\d+ linkpath=(.*)$/.exec(record);
93
- if (link) longLink = link[1];
111
+ offset += 512 + padded;
112
+ if (type === "x" || type === "X" || type === "g") {
113
+ // PAX lengths count bytes, not UTF-16 characters. Reject malformed records.
114
+ const attrs = type === "g" ? global : extended;
115
+ for (let at = 0; at < body.length;) {
116
+ const space = body.indexOf(32, at);
117
+ if (space < 0) invalid("invalid pax record length");
118
+ const digits = body.toString("ascii", at, space);
119
+ const length = /^[0-9]+$/.test(digits) ? Number(digits) : NaN;
120
+ if (!Number.isSafeInteger(length) || length <= space - at + 1
121
+ || length > body.length - at || body[at + length - 1] !== 10) invalid("invalid pax record length");
122
+ const record = body.toString("utf8", space + 1, at + length - 1);
123
+ const equals = record.indexOf("=");
124
+ if (equals > 0) Object.defineProperty(attrs, record.slice(0, equals), {
125
+ value: record.slice(equals + 1), writable: true, enumerable: true, configurable: true,
126
+ });
127
+ at += length;
94
128
  }
95
129
  continue;
96
130
  }
97
131
  if (type === "L") { longName = body.toString("utf8").replace(/\0.*$/s, ""); continue; }
98
132
  if (type === "K") { longLink = body.toString("utf8").replace(/\0.*$/s, ""); continue; }
99
- if (type === "g") continue;
100
- const prefix = field(345, 155);
101
- const name = longName ?? (prefix ? `${prefix}/${field(0, 100)}` : field(0, 100));
133
+ const prefix = field(257, 5) === "ustar" ? field(345, 155) : "";
134
+ const name = attrs.path ?? longName ?? (prefix ? `${prefix}/${field(0, 100)}` : field(0, 100));
102
135
  longName = null;
103
136
  if (type === "0" || type === "\0") files.set(name.replace(/^\.\//, ""), body);
104
- if (type === "1" || type === "2") links.set(name.replace(/^\.\//, ""), { target: longLink ?? field(157, 100), hard: type === "1" });
137
+ if (type === "1" || type === "2") links.set(name.replace(/^\.\//, ""), { target: attrs.linkpath ?? longLink ?? field(157, 100), hard: type === "1" });
105
138
  longLink = null;
139
+ extended = {};
106
140
  }
141
+ if (offset !== data.length) invalid("truncated tar header");
107
142
  return {
108
143
  // Enumerate regular files without exposing the Map's mutation methods.
109
144
  entries() { return files.entries(); },
@@ -1,20 +1,20 @@
1
1
  # npm packages
2
- # Latest versions checked on 2026-10-11; SHA-256 pins verified against registry SHA-512 integrity.
3
- # buninu-browser 0.9.0, buninu-land 0.4.0, and casty 1.6.1 verified on 2026-11-02.
2
+ # SHA-256 pins verified against registry SHA-512 integrity.
3
+ # buninu-browser 0.9.2, buninu-land 0.4.1, and casty 1.6.2 verified on 2026-11-03.
4
4
  # jspulse 0.2.3 verified on 2026-10-22.
5
5
  # jspinyin 0.2.0 verified on 2026-10-22.
6
6
 
7
- "buninu-browser@0.9.0":
8
- - https://registry.npmjs.org/buninu-browser/-/buninu-browser-0.9.0.tgz
9
- - sha256:18702e6211db9a5aeb1584f924a040ce6c7d77778b730c0374a659ddf1cdfb20
7
+ "buninu-browser@0.9.2":
8
+ - https://registry.npmjs.org/buninu-browser/-/buninu-browser-0.9.2.tgz
9
+ - sha256:faba0d55272882da02cc5993047d831dae11845137b53b7f5c7642e99567391a
10
10
 
11
- "buninu-land@0.4.0":
12
- - https://registry.npmjs.org/buninu-land/-/buninu-land-0.4.0.tgz
13
- - sha256:92c5177058fb52a9961fea8196937fdb4c75ef0d472f2c0d0d07fab03ea42116
11
+ "buninu-land@0.4.1":
12
+ - https://registry.npmjs.org/buninu-land/-/buninu-land-0.4.1.tgz
13
+ - sha256:18281a77725cdce53cd26aa447c657c530616e0f0bcb44819352d3420023a34a
14
14
 
15
- "@drxiaozhi/casty@1.6.1":
16
- - https://registry.npmjs.org/@drxiaozhi/casty/-/casty-1.6.1.tgz
17
- - sha256:d7ce0fd5fd478340ac32cbd164bd008b97e0a0fe75a3366921da0aa2c55d46bc
15
+ "@drxiaozhi/casty@1.6.2":
16
+ - https://registry.npmjs.org/@drxiaozhi/casty/-/casty-1.6.2.tgz
17
+ - sha256:8aa8e59998aa82ef0e28ca172828eba116e0f19d890490fc19d7ca4dd295a77c
18
18
 
19
19
  "@drxiaozhi/jspulse@0.2.3":
20
20
  - https://registry.npmjs.org/@drxiaozhi/jspulse/-/jspulse-0.2.3.tgz