]*>/i,m=>m+PRELUDE)}
defApp("tv",{name:"Terminal Velocity",icon:"game",cat:"Games",w:1040,h:700,single:true,startMax:true,desc:"the game",kw:"game terminal velocity republicka strategy rpg",
init(win,b){const f=h("iframe",{title:"Terminal Velocity",style:"flex:1;border:0;width:100%;height:100%;background:#0a0a0c",allow:"autoplay; clipboard-write"});f.srcdoc=gameHTML();b.append(f);win.frame=f;win.on.focus=()=>{try{f.contentWindow.focus()}catch(e){}};f.addEventListener("load",()=>{try{f.contentWindow.focus()}catch(e){}})}});
/* --- Terminal: its own small shell over the unified VFS --- */
function tokenize(line){const out=[];let cur="",q=null,has=false;for(let i=0;i"){if(cur||has){out.push(cur);cur="";has=false}if(c===">"&&line[i+1]===">"){out.push(">>");i++}else out.push(c)}else cur+=c}if(cur||has)out.push(cur);return out}
function globToRe(g){return new RegExp("^"+g.replace(/[.+^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*").replace(/\?/g,".")+"$","i")}
function expandGlob(a,cwd){if(!/[*?]/.test(a))return[a];const i=a.lastIndexOf("/"),dir=i>=0?a.slice(0,i+1):"",pat=a.slice(i+1);const d=V.get(dir||".",cwd);if(!d||!d.dir)return[a];const re=globToRe(pat);const r=V.ls(d).filter(n=>re.test(n.name)&&(pat[0]==="."||n.name[0]!==".")).map(n=>dir+n.name);return r.length?r:[a]}
const SH={};
const need=(n,m)=>{if(!n)throw new Error(m)};
SH.help=()=>`Terminal Velocity OS shell. Commands:
ls [-lahs] [path] cd [dir] pwd cat f... head/tail [-n N] f wc [-lwc] f
grep [-inrcl] pat [f...] find [path] [-name pat] [-type f|d] tree [path]
mkdir [-p] touch rm [-r] cp mv echo text [> file] stat du df
sort uniq tac (pipes | and redirects > >> work; globs * ? too)
open path open with the right app edit path open in Notepad
play path Media Player view path Image Viewer
export path download a file to your computer
apps list apps launch app start an app by id
theme [name] list/switch themes wallpaper [name] mount (local folder)
manifest archive status neofetch clear history date uname
tv start Terminal Velocity (the game) reboot
Tab completes. Up/Down is history. Ctrl+L clears.
`;
SH.pwd=(a,io)=>io.cwd;
SH.echo=a=>a.join(" ");
SH.clear=(a,io)=>{io.t.clear();return""};
SH.date=()=>new Date().toString();
SH.whoami=()=>"user";
SH.uname=()=>"TerminalVelocityOS "+OS.ver+" (browser) "+navigator.platform;
SH.history=(a,io)=>io.t.hist.map((l,i)=>String(i+1).padStart(4)+" "+l).join("\n");
SH.cd=(a,io)=>{const n=V.get(a[0]||"~",io.cwd);need(n,"cd: no such directory: "+a[0]);need(n.dir,"cd: not a directory: "+a[0]);io.t.cwd=V.path(n);io.t.prompt();return""};
SH.ls=(a,io)=>{const fl=a.filter(x=>x[0]==="-"&&x.length>1).join("");const L=fl.includes("l"),A=fl.includes("a"),Hh=fl.includes("h"),S=fl.includes("s");const ps=a.filter(x=>x[0]!=="-");if(!ps.length)ps.push(".");const out=[];
for(const p of ps){const n=V.get(p,io.cwd);if(!n){out.push("ls: cannot access '"+p+"': No such file or directory");continue}
let list=n.dir?V.ls(n):[n];if(!A)list=list.filter(x=>x.name[0]!==".");if(ps.length>1&&n.dir)out.push(p+":");
const sz=x=>x.dir?"-":(Hh||S?fmtBytes(x.size):String(x.size));
if(L)for(const x of list)out.push((x.dir?"d":"-")+(V.writable(x)?"rw-":"r--")+" "+sz(x).padStart(7)+" "+fmtDate(x.mt)+" "+x.name+(x.dir?"/":""));
else if(S)for(const x of list)out.push(sz(x).padStart(7)+" "+x.name+(x.dir?"/":""));
else out.push(list.map(x=>x.name+(x.dir?"/":"")).join(io.piped?"\n":" ")||"(empty)")}
return out.join("\n")};
async function catNodes(a,io){const o=[];if(!a.length&&io.in!=null)return io.in;for(const p of a){const n=V.get(p,io.cwd);need(n,"cat: "+p+": No such file");need(!n.dir,"cat: "+p+": Is a directory");if(n.src==="arch"&&V.kind(n)!=="text"&&V.kind(n)!=="md"&&V.kind(n)!=="html"&&n.size>2e6)throw new Error("cat: "+p+": big binary-ish archive file ("+fmtBytes(n.size)+"). Use open/play/view.");o.push(await V.read(n,"text"))}return o.join("")}
SH.cat=catNodes;
const nOpt=(a,d)=>{let n=d;const r=[];for(let i=0;i{const[n,r]=nOpt(a,10);return(await catNodes(r,io)).split("\n").slice(0,n).join("\n")};
SH.tail=async(a,io)=>{const[n,r]=nOpt(a,10);const l=(await catNodes(r,io)).replace(/\n$/,"").split("\n");return l.slice(-n).join("\n")};
SH.tac=async(a,io)=>(await catNodes(a,io)).replace(/\n$/,"").split("\n").reverse().join("\n");
SH.wc=async(a,io)=>{const fl=a.filter(x=>x[0]==="-").join(""),r=a.filter(x=>x[0]!=="-");const t=await catNodes(r,io);const l=t.split("\n").length-(t.endsWith("\n")?1:0),w=(t.match(/\S+/g)||[]).length,c=new Blob([t]).size;if(fl.includes("l"))return String(l);if(fl.includes("w"))return String(w);if(fl.includes("c"))return String(c);return`${l} ${w} ${c}`};
SH.sort=async(a,io)=>{const l=(await catNodes(a.filter(x=>x[0]!=="-"),io)).replace(/\n$/,"").split("\n").sort((x,y)=>x.localeCompare(y,undefined,{numeric:true}));if(a.includes("-r"))l.reverse();return l.join("\n")};
SH.uniq=async(a,io)=>{const l=(await catNodes(a,io)).replace(/\n$/,"").split("\n");return l.filter((x,i)=>i===0||x!==l[i-1]).join("\n")};
SH.grep=async(a,io)=>{let fl="";const r=[];for(const x of a)(/^-[a-z]+$/.test(x)?fl+=x.slice(1):r.push(x));need(r.length,"usage: grep [-inrcl] pattern [files]");const pat=r.shift();let re;try{re=new RegExp(pat,fl.includes("i")?"i":"")}catch(e){re=new RegExp(pat.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),fl.includes("i")?"i":"")}
const out=[];const doText=(t,nm)=>{let c=0;t.split("\n").forEach((ln,i)=>{if(re.test(ln)){c++;if(!fl.includes("c")&&!fl.includes("l"))out.push((nm?nm+":":"")+(fl.includes("n")?(i+1)+":":"")+ln)}});if(fl.includes("c"))out.push((nm?nm+":":"")+c);else if(fl.includes("l")&&c)out.push(nm)};
if(!r.length){need(io.in!=null,"grep: no input");doText(io.in,"");return out.join("\n")}
const files=[];const collect=(n,p)=>{if(!n.dir)files.push([n,p]);else if(fl.includes("r")){let c=0;for(const k of V.ls(n)){if(files.length>400)break;collect(k,p+"/"+k.name)}}};
for(const p of r){const n=V.get(p,io.cwd);need(n,"grep: "+p+": No such file");collect(n,p)}
for(const[n,p]of files){const k=V.kind(n);if(k!=="text"&&k!=="md"&&k!=="html"&&k!=="other")continue;if(n.src==="arch"&&n.size>1e6)continue;try{doText(await V.read(n,"text"),files.length>1||fl.includes("r")?p:"")}catch(e){out.push("grep: "+p+": "+e.message)}}
return out.join("\n")};
SH.find=(a,io)=>{let start=".",name=null,type=null,ic_=false;for(let i=0;i{if(out.length>=500)return;if(type==="f"&&m.dir)return;if(type==="d"&&!m.dir)return;if(name&&!name.test(m.name))return;out.push(m===n?start:(start==="."?".":start.replace(/\/$/,""))+V.path(m).slice(base.length))});
return out.join("\n")+(out.length>=500?"\n... (first 500 shown)":"")};
SH.tree=(a,io)=>{const n=V.get(a[0]||".",io.cwd);need(n,"tree: no such path");const o=[n.name||"/"];let c=0;const rec=(d,pre,dep)=>{if(dep>6)return;const k=V.ls(d).filter(x=>x.name[0]!==".");k.forEach((x,i)=>{if(c++>400)return;const last=i===k.length-1;o.push(pre+(last?"└── ":"├── ")+x.name+(x.dir?"/":""));if(x.dir)rec(x,pre+(last?" ":"│ "),dep+1)})};rec(n,"",0);if(c>400)o.push("... (truncated)");return o.join("\n")};
SH.mkdir=(a,io)=>{for(const p of a.filter(x=>x[0]!=="-"))V.mkdirp(p,io.cwd);return""};
SH.touch=async(a,io)=>{for(const p of a){const n=V.get(p,io.cwd);if(!n)await V.write(p,"",io.cwd);else if(!n.dir){n.mt=Date.now();V.save(n)}}return""};
SH.rm=async(a,io)=>{const rec=a.some(x=>/^-.*[rR]/.test(x));for(const p of a.filter(x=>x[0]!=="-")){const n=V.get(p,io.cwd);need(n,"rm: "+p+": No such file");await V.rm(n,rec)}return""};
SH.rmdir=SH.rm;
SH.mv=async(a,io)=>{need(a.length>=2,"usage: mv src dest");const d=a[a.length-1];for(const p of a.slice(0,-1)){const n=V.get(p,io.cwd);need(n,"mv: "+p+": No such file");await V.mv(n,V.norm(d,io.cwd))}return""};
SH.cp=async(a,io)=>{a=a.filter(x=>x[0]!=="-");need(a.length>=2,"usage: cp src dest");const d=a[a.length-1];for(const p of a.slice(0,-1)){const n=V.get(p,io.cwd);need(n,"cp: "+p+": No such file");await V.cp(n,V.norm(d,io.cwd))}return""};
SH.stat=(a,io)=>{const n=V.get(a[0]||".",io.cwd);need(n,"stat: no such file");const c=n.dir?V.count(n):null;return` path: ${V.path(n)}\n type: ${n.dir?"directory":V.kind(n)}\n size: ${n.dir?c.files+" files, "+fmtBytes(c.size):n.size+" ("+fmtBytes(n.size)+")"}\n store: ${n.src}${V.writable(n)?" (writable)":" (read-only)"}${n.url?"\n url: "+n.url:""}\n time: ${fmtDate(n.mt)}`};
SH.du=(a,io)=>{const n=V.get(a[0]||".",io.cwd);need(n,"du: no such path");const c=V.count(n);return`${fmtBytes(c.size)}\t${V.path(n)} (${c.files.toLocaleString()} files, ${c.dirs.toLocaleString()} dirs)`};
SH.df=async()=>{let u="",q="";try{const e=await navigator.storage.estimate();u=fmtBytes(e.usage);q=fmtBytes(e.quota)}catch(e){u="?";q="?"}const h_=V.count(V.user),A=V.count(V.arch);return`/archive ${A.files.toLocaleString()} files ${fmtBytes(A.size)} (read-only, remote)\n/home/user ${h_.files} files ${fmtBytes(h_.size)}\nbrowser storage used ${u} of ${q}`};
SH.open=(a,io)=>{need(a[0],"usage: open path");const n=V.get(a[0],io.cwd);need(n,"open: not found: "+a[0]);OS.openNode(n);return""};
SH.edit=(a,io)=>{const p=V.norm(a[0]||"/home/user/untitled.txt",io.cwd);OS.open("notepad",p);return""};
SH.play=(a,io)=>{need(a[0],"usage: play path");const n=V.get(a[0],io.cwd);need(n,"play: not found");OS.open("media",V.path(n));return""};
SH.view=(a,io)=>{need(a[0],"usage: view path");const n=V.get(a[0],io.cwd);need(n,"view: not found");OS.open("images",V.path(n));return""};
SH.export=async(a,io)=>{need(a[0],"usage: export path");const n=V.get(a[0],io.cwd);need(n&&!n.dir,"export: not a file");download(n.name,await V.read(n,"blob"));return"downloading "+n.name};
SH.apps=()=>Object.values(OS.apps).filter(a=>!a.hidden).map(a=>a.id.padEnd(10)+a.name.padEnd(22)+(a.desc||"")).join("\n");
SH.launch=(a)=>{need(a[0]&&OS.apps[a[0]],"launch: unknown app (see: apps)");OS.open(a[0],a[1]);return""};
SH.tv=()=>{OS.open("tv");return"launching Terminal Velocity..."};
SH.theme=(a)=>{if(!a[0])return THEME_ORDER.map(k=>(k===SET.theme?"* ":" ")+k.padEnd(11)+THEMES[k].n).join("\n");need(THEMES[a[0]],"theme: unknown. run `theme` to list");applyTheme(a[0]);return"theme: "+THEMES[a[0]].n};
SH.wallpaper=(a)=>{if(!a[0])return"wallpapers: auto "+WALLS.join(" ")+" (now: "+wallId()+")";need(a[0]==="auto"||WALLS.includes(a[0]),"wallpaper: unknown");SET.wall=a[0];saveSet();setWall();return"wallpaper: "+a[0]};
SH.mount=async()=>{const n=await V.mount();return"mounted at "+V.path(n)+" ("+V.count(n).files+" files)"};
SH.manifest=()=>MAN.json?`archive: ${MAN.files.toLocaleString()} files, ${fmtBytes(MAN.size)}\nsource : ${MAN.src}\ngenerated: ${MAN.gen||"?"}\nattempts:\n`+MAN.tried.map(t=>" "+(t.ok?"ok ":"fail ")+t.u+(t.why?" ("+t.why+")":"")).join("\n"):"no manifest mounted. Settings > Archive lets you load a manifest.json from a file or URL.\nattempts:\n"+MAN.tried.map(t=>" fail "+t.u+" ("+t.why+")").join("\n");
SH.reboot=()=>{setTimeout(()=>location.reload(),300);return"rebooting..."};
SH.neofetch=()=>{const A=MAN.files;return` ████████╗██╗ ██╗ user@terminal-velocity\n ╚══██╔══╝██║ ██║ ---------------------\n ██║ ██║ ██║ OS: Terminal Velocity OS ${OS.ver}\n ██║ ╚██╗ ██╔╝ Host: ${navigator.userAgent.split(") ")[0].split("(").pop()||"browser"}\n ██║ ╚████╔╝ Uptime: ${Math.round((performance.now()-OS.boot)/1000)}s\n ╚═╝ ╚═══╝ Theme: ${THEMES[SET.theme].n}\n Wallpaper: ${wallId()}\n Archive: ${A?A.toLocaleString()+" files":"not mounted"}\n Windows: ${OS.wins.length} Apps: ${Object.keys(OS.apps).length}\n Screen: ${innerWidth}x${innerHeight}\n License: CC0`};
SH.sleep=async a=>{await sleep((+a[0]||1)*1000);return""};
async function runLine(line,t){/* returns output string */
const toks=tokenize(line);if(!toks.length)return"";const stages=[[]];for(const x of toks){if(x==="|")stages.push([]);else stages[stages.length-1].push(x)}
let red=null,app=false;const last=stages[stages.length-1];const ri=last.findIndex(x=>x===">"||x===">>");if(ri>=0){app=last[ri]===">>";red=last[ri+1];last.splice(ri)}
let data=null;for(const st of stages){if(!st.length)throw new Error("syntax error near |");const cmd=st[0];let args=[];for(const x of st.slice(1))args.push(...expandGlob(x,t.cwd));const f=SH[cmd];if(!f)throw new Error(cmd+": command not found (try: help)");data=await f(args,{cwd:t.cwd,in:data,t,piped:st!==stages[stages.length-1]||!!red});if(data==null)data=""}
if(red){need(red,"syntax error: missing file after >");let o=data;if(app){const n=V.get(red,t.cwd);if(n&&!n.dir)o=(await V.read(n,"text"))+(data.endsWith("\n")?data:data+"\n")}else if(!o.endsWith("\n"))o+="\n";await V.write(red,o,t.cwd);return""}
return data}
defApp("terminal",{name:"Terminal",icon:"term",cat:"System",w:700,h:460,desc:"shell over the archive",kw:"shell console cmd bash ls cd",
init(win,b,arg){const t={cwd:"/home/user",hist:[],hi:0,clear(){out.innerHTML=""},prompt(){pr.textContent=t.cwd.replace(/^\/home\/user/,"~")+" $"}};if(typeof arg==="string"){const n=V.get(arg);if(n)t.cwd=n.dir?V.path(n):V.path(n.parent)}
const out=h("div",{style:"flex:1;overflow:auto;padding:8px 10px;white-space:pre-wrap;word-break:break-word;cursor:text"}),pr=h("span",{class:"ac",style:"white-space:nowrap"}),inp=h("input",{style:"flex:1;background:none;border:0;outline:0;min-width:0",autocomplete:"off",autocapitalize:"off",spellcheck:"false"}),row=h("div",{style:"display:flex;gap:8px;padding:6px 10px;border-top:1px solid var(--bd);background:var(--pn2)"},pr,inp);
b.append(out,row);b.addEventListener("pointerup",()=>{if(!getSelection().toString())inp.focus()});win.on.focus=()=>inp.focus();t.prompt();
const say=(s,c)=>{const d=h("div",{class:c||""},String(s).replace(/\n$/,""));out.append(d);out.scrollTop=out.scrollHeight};
say("Terminal Velocity OS shell. Type `help`. The archive is at /archive, your files at ~ (/home/user).","dim");win.t=t;
const run=async line=>{say(pr.textContent+" "+line,"ac");if(!line.trim())return;t.hist.push(line);t.hi=t.hist.length;try{const o=await runLine(line,t);if(o)say(o)}catch(e){say(e.message||String(e),"")|0;out.lastChild.style.color="var(--er)"}t.prompt();out.scrollTop=out.scrollHeight};
inp.addEventListener("keydown",e=>{if(e.key==="Enter"){const l=inp.value;inp.value="";run(l)}else if(e.key==="ArrowUp"){if(t.hi>0){t.hi--;inp.value=t.hist[t.hi];e.preventDefault()}}else if(e.key==="ArrowDown"){if(t.hik.startsWith(w))}else{const i=w.lastIndexOf("/"),dir=i>=0?w.slice(0,i+1):"",base=w.slice(i+1);const d=V.get(dir||".",t.cwd);if(d&&d.dir)c=V.ls(d).filter(n=>n.name.toLowerCase().startsWith(base.toLowerCase())).map(n=>dir+n.name+(n.dir?"/":""))}
if(c.length===1){const r=c[0].includes(" ")?'"'+c[0]+'"':c[0];inp.value=pre.slice(0,pre.length-w.length)+r+v.slice(pos)}else if(c.length>1){let p=c[0];for(const x of c)while(!x.toLowerCase().startsWith(p.toLowerCase()))p=p.slice(0,-1);if(p.length>w.length)inp.value=pre.slice(0,pre.length-w.length)+p+v.slice(pos);else say(c.slice(0,80).join(" "),"dim")}}});
setTimeout(()=>inp.focus(),30)}});
/* --- Files --- */
defApp("files",{name:"Files",icon:"folder",cat:"System",w:780,h:500,desc:"file manager",kw:"explorer finder folders archive browse",
init(win,b,arg){let cwd="/archive",hist=[],hi=-1,view=LS.get("tvos_fview","grid"),sel=new Set(),clip=null,filt="",lim=400;const S={};
const path=h("input",{class:"t grow",spellcheck:"false"}),flt=h("input",{class:"t",placeholder:"filter…",style:"width:130px"}),list=h("div",{class:"fl scr grow"}),status=h("div",{class:"row dim",style:"border-top:1px solid var(--bd);border-bottom:0;font-size:12px"}),side=h("div",{class:"side"});
const nav=(p,push=true)=>{const n=V.get(p);if(!n)return toast("No such folder: "+p,1);if(!n.dir){return OS.openNode(n)}cwd=V.path(n);if(push){hist=hist.slice(0,hi+1);hist.push(cwd);hi=hist.length-1}sel.clear();lim=400;flt.value="";filt="";render()};
const cur=()=>V.get(cwd);
const bt=(i,t,f)=>h("button",{class:"btn sm",title:t,onclick:f},ic(i));
const tools=h("div",{class:"row"},bt("back","Back",()=>{if(hi>0){hi--;nav(hist[hi],false)}}),bt("fwd","Forward",()=>{if(hi{const c=cur();c&&c.parent&&nav(V.path(c.parent))}),path,flt,bt("grid","Grid / list",()=>{view=view==="grid"?"list":"grid";LS.set("tvos_fview",view);render()}),bt("folderplus","New folder",()=>mk(true)),bt("refresh","Refresh",()=>render()));
b.append(tools,h("div",{style:"display:flex;flex:1;min-height:0"},side,h("div",{style:"flex:1;min-width:0;display:flex;flex-direction:column"},list,status)));
const places=()=>{side.innerHTML="";const P=[["Archive","/archive","globe"],["Home","/home/user","folder"],["Documents","/home/user/Documents","note"],["Music & Video","/home/user/Media","music"],["Temp","/tmp","file"],["System","/system","gear"]];for(const[n,p,i]of P)side.append(h("div",{class:cwd===p||cwd.startsWith(p+"/")&&p!=="/home/user"?"on":"",onclick:()=>{if(!V.get(p))V.mkdirp(p);nav(p)}},ic(i),n));for(const m of V.ls(V.mnt))side.append(h("div",{onclick:()=>nav("/mnt/"+m.name)},ic("usb"),m.name));side.append(h("div",{onclick:async()=>{try{const n=await V.mount();nav(V.path(n))}catch(e){toast(e.message,1)}}},ic("plus"),"Mount folder…"))};
const mk=async dir=>{const n=await dlgPrompt(dir?"New folder name:":"New file name:",dir?"New folder":"untitled.txt");if(!n)return;try{const p=cwd+"/"+n;dir?V.mkdirp(p):await V.write(p,"");render()}catch(e){toast(e.message,1)}};
const render=()=>{const c=cur();if(!c){cwd="/";return render()}path.value=cwd;let items=V.ls(c);if(filt)items=items.filter(n=>n.name.toLowerCase().includes(filt));const vis=items.filter(n=>n.name[0]!=="."||SET.dot);list.innerHTML="";list.className="fl scr grow"+(view==="list"?" lst":"");
if(!c.kids.size){list.append(h("div",{class:"pad dim",style:"grid-column:1/-1"},cwd==="/archive"?(MAN.json?"Empty manifest.":"No archive mounted. The manifest could not be loaded (see Settings > Archive to supply your own manifest.json)."):"This folder is empty. Right-click for options."))}
vis.slice(0,lim).forEach(n=>{const p=V.path(n);const it=h("div",{class:"it"+(n.dir?" d":"")+(sel.has(p)?" sel":""),title:n.name+(n.size?" · "+fmtBytes(n.size):""),onclick:e=>{if(isMob()&&!e.ctrlKey){OS.openNode(n);return}if(e.ctrlKey||e.metaKey){sel.has(p)?sel.delete(p):sel.add(p)}else{sel.clear();sel.add(p)}render()},ondblclick:()=>n.dir?nav(p):OS.openNode(n),oncontextmenu:e=>{e.preventDefault();e.stopPropagation();if(!sel.has(p)){sel.clear();sel.add(p);render()}itemMenu(e,n)}},ic(V.icon(n)),list.classList.contains("lst")?[h("span",{class:"n"},n.name),h("span",{class:"m"},n.dir?V.ls(n).length+" items":fmtBytes(n.size)),h("span",{class:"m",style:"width:120px"},n.src==="arch"?"":fmtDate(n.mt))]:h("div",{},n.name));list.append(it)});
if(vis.length>lim)list.append(h("button",{class:"btn",style:"grid-column:1/-1;margin:8px",onclick:()=>{lim+=1200;render()}},"Show "+Math.min(1200,vis.length-lim)+" more of "+(vis.length-lim).toLocaleString()+" remaining"));
status.textContent=vis.length.toLocaleString()+" items"+(sel.size?" · "+sel.size+" selected":"")+(V.writable(c)?"":" · read-only")+(MAN.json&&cwd.startsWith("/archive")?" · "+MAN.src:"");places()};
const selNodes=()=>[...sel].map(p=>V.get(p)).filter(Boolean);
const itemMenu=(e,n)=>{const ns=selNodes(),w=ns.every(x=>V.writable(x)&&x.src!=="arch");const m=[{t:"Open",ic:"eye",fn:()=>OS.openNode(n)}];if(!n.dir){const k=V.kind(n);m.push({t:"Open in Notepad",ic:"note",fn:()=>OS.openNode(n,"notepad")});if(k==="audio"||k==="video")m.push({t:"Play",ic:"play",fn:()=>OS.open("media",V.path(n))});if(k==="image")m.push({t:"View",ic:"image",fn:()=>OS.open("images",V.path(n))});if(k==="html")m.push({t:"Run as app",ic:"code",fn:()=>OS.open("runner",V.path(n))});m.push({t:"Download",ic:"dl",fn:async()=>{try{download(n.name,await V.read(n,"blob"))}catch(e){toast("Download failed: "+e.message+(n.src==="arch"?" (the archive host may block cross-site reads; use Open instead)":""),1)}}});if(n.url)m.push({t:"Open in browser tab",ic:"globe",fn:()=>window.open(n.url,"_blank")})}else{m.push({t:"Play folder",ic:"play",fn:()=>OS.open("media",{queue:V.ls(n).filter(x=>["audio","video"].includes(V.kind(x))).map(V.path)})})}
m.push("-",{t:"Copy",ic:"copy",fn:()=>{clip={nodes:ns,cut:false};toast("Copied "+ns.length)}},{t:"Cut",fn:()=>{clip={nodes:ns,cut:true}},dis:!w},{t:"Rename",ic:"edit",dis:!w||ns.length!==1,fn:async()=>{const nn=await dlgPrompt("Rename:",n.name);if(nn&&nn!==n.name)try{await V.mv(n,V.path(n.parent)+"/"+nn);render()}catch(er){toast(er.message,1)}}},{t:"Delete",ic:"trash",dis:!w,fn:async()=>{if(await dlgConfirm("Delete "+ns.length+" item(s)? This cannot be undone.","Delete","Delete")){try{for(const x of ns)await V.rm(x,true);sel.clear();render()}catch(er){toast(er.message,1)}}}},"-",{t:"Properties",fn:()=>dlgAlert(SH.stat([V.path(n)],{cwd:"/"}),n.name)});ctxMenu(e.clientX,e.clientY,m)};
list.addEventListener("contextmenu",e=>{e.preventDefault();const w=V.writable(cur());ctxMenu(e.clientX,e.clientY,[{t:"New folder",ic:"folderplus",dis:!w,fn:()=>mk(true)},{t:"New text file",ic:"note",dis:!w,fn:()=>mk(false)},{t:"Paste",ic:"copy",dis:!w||!clip,fn:async()=>{try{for(const x of clip.nodes)clip.cut?await V.mv(x,cwd):await V.cp(x,cwd);if(clip.cut)clip=null;render()}catch(er){toast(er.message,1)}}},"-",{t:"Upload files from computer…",ic:"dl",dis:!w,fn:async()=>{const fs=await pickDisk("",true);for(const f of fs)await V.write(cwd+"/"+f.name,f,null,f.type);render()}},{t:SET.dot?"Hide dotfiles":"Show dotfiles",fn:()=>{SET.dot=!SET.dot;render()}},{t:"Open Terminal here",ic:"term",fn:()=>OS.open("terminal",cwd)},{t:"Refresh",ic:"refresh",fn:render}])});
list.addEventListener("click",e=>{if(e.target===list){sel.clear();render()}});
path.addEventListener("keydown",e=>{if(e.key==="Enter")nav(path.value)});flt.addEventListener("input",()=>{filt=flt.value.toLowerCase();lim=400;render()});
b.addEventListener("keydown",e=>{if(e.key==="Delete"&&sel.size){const ns=selNodes();if(ns.every(x=>V.writable(x)))dlgConfirm("Delete "+ns.length+" item(s)?","Delete","Delete").then(async y=>{if(y){for(const x of ns)await V.rm(x,true);sel.clear();render()}})}else if(e.key==="Enter"&&sel.size===1)OS.openNode(selNodes()[0]);else if(e.key==="Backspace"&&e.target===b){const c=cur();c&&c.parent&&nav(V.path(c.parent))}});b.tabIndex=0;
const fsl=OS.on("fs",()=>{if(!win.dead&&cur())render()});win.on.close=()=>OS.off("fs",fsl);
win.nav=nav;V.mkdirp("/home/user/Documents");V.mkdirp("/home/user/Media");nav(typeof arg==="string"?arg:(MAN.json?"/archive":"/home/user"))},
onArg(win,arg){if(typeof arg==="string")win.nav(arg)}});
/* ===================== APPS 2: notepad, media, images, calc, reader, runner, archive ===================== */
/* --- generic path picker --- */
function pickPath(o){o=o||{};return dlg((b,done)=>{let cwd=o.start||(o.mode==="save"?"/home/user/Documents":MAN.json?"/archive":"/home/user");if(!V.get(cwd))cwd="/home/user";const nm=h("input",{class:"t",placeholder:"file name",value:o.name||""}),pa=h("input",{class:"t grow"}),ls=h("div",{class:"fl lst scr",style:"height:260px;border:1px solid var(--bd);border-radius:5px"});
const go=p=>{const n=V.get(p);if(n&&n.dir){cwd=V.path(n);draw()}};
const draw=()=>{pa.value=cwd;ls.innerHTML="";const c=V.get(cwd);if(c.parent)ls.append(h("div",{class:"it",onclick:()=>go(V.path(c.parent))},ic("up"),h("span",{class:"n"},"..")));let k=V.ls(c);if(o.exts)k=k.filter(n=>n.dir||o.exts.includes(V.kind(n)));k.slice(0,500).forEach(n=>ls.append(h("div",{class:"it",onclick:()=>{if(n.dir)go(V.path(n));else nm.value=n.name},ondblclick:()=>{if(!n.dir&&o.mode!=="save")done(V.path(n))}},ic(V.icon(n)),h("span",{class:"n"},n.name),h("span",{class:"m"},n.dir?"":fmtBytes(n.size)))))};
const ok=()=>{if(o.mode==="dir")return done(cwd);if(!nm.value.trim())return;done((cwd==="/"?"":cwd)+"/"+nm.value.trim())};
pa.addEventListener("keydown",e=>{if(e.key==="Enter")go(pa.value)});nm.addEventListener("keydown",e=>{if(e.key==="Enter")ok()});
b.append(h("h3",{},o.title||(o.mode==="save"?"Save as":o.mode==="dir"?"Choose folder":"Open file")),h("div",{class:"row",style:"padding:0;border:0"},pa),ls,o.mode==="dir"?"":nm,h("div",{class:"acts"},h("button",{class:"btn",onclick:()=>done(null)},"Cancel"),h("button",{class:"btn pri",onclick:ok},o.mode==="save"?"Save":o.mode==="dir"?"Choose this folder":"Open")));draw()})}
/* --- Notepad with real export --- */
defApp("notepad",{name:"Notepad",icon:"note",cat:"Accessories",w:760,h:520,single:false,desc:"text editor with real export",kw:"text editor write txt save export zip markdown",
init(win,b,arg){let tabs=[],act=null,tid=0,wrap=true,prev=false;const ed=h("textarea",{class:"t",spellcheck:"false",style:"flex:1;border:0;border-radius:0;resize:none;padding:10px;line-height:1.5;tab-size:2;white-space:pre-wrap;outline:0"}),pv=h("div",{class:"scr",style:"flex:1;padding:12px;display:none;border-left:1px solid var(--bd)"}),tabsEl=h("div",{class:"tabs"}),st=h("div",{class:"row dim",style:"border-top:1px solid var(--bd);border-bottom:0;font-size:12px"}),
fnd=h("input",{class:"t",placeholder:"find",style:"width:120px"}),rep=h("input",{class:"t",placeholder:"replace",style:"width:120px"}),fbar=h("div",{class:"row",style:"display:none"},fnd,h("button",{class:"btn sm",onclick:()=>findNext(1)},"Next"),h("button",{class:"btn sm",onclick:()=>findNext(-1)},"Prev"),rep,h("button",{class:"btn sm",onclick:replOne},"Replace"),h("button",{class:"btn sm",onclick:replAll},"All"),h("span",{class:"dim",id:"fcnt"}));
const T=()=>tabs.find(t=>t.id===act);
const newTab=(o)=>{const t=Object.assign({id:++tid,name:"untitled.txt",path:null,text:"",orig:"",dirty:false},o);tabs.push(t);sw(t.id);return t};
const sw=id=>{const c=T();if(c)c.text=ed.value;act=id;const t=T();ed.value=t.text;draw();upd();OS.title(win,"Notepad - "+t.name+(t.dirty?" •":""));ed.focus()};
const draw=()=>{tabsEl.innerHTML="";tabs.forEach(t=>tabsEl.append(h("div",{class:"tab"+(t.id===act?" on":""),onclick:()=>t.id!==act&&sw(t.id)},t.name+(t.dirty?" •":""),h("i",{onclick:e=>{e.stopPropagation();closeTab(t)}},"×"))))};
const closeTab=async t=>{if(t.id===act)t.text=ed.value;if(t.dirty&&t.text!==t.orig&&!await dlgConfirm("Discard unsaved changes to "+t.name+"?","Close tab","Discard"))return;const i=tabs.indexOf(t);tabs.splice(i,1);if(!tabs.length)newTab();else if(t.id===act)sw(tabs[Math.max(0,i-1)].id);else draw();persist()};
const upd=()=>{const t=T();if(!t)return;const v=ed.value,p=ed.selectionStart,ln=v.slice(0,p).split("\n"),words=(v.match(/\S+/g)||[]).length;st.textContent=`Ln ${ln.length}, Col ${ln[ln.length-1].length+1} · ${words} words · ${v.length} chars · ${t.path||"not saved to a file"}`;if(prev)pv.innerHTML=/\.html?$/.test(t.name)?"":md(v)};
let pt=0;const persist=()=>{clearTimeout(pt);pt=setTimeout(()=>{const c=T();if(c)c.text=ed.value;DB.set("np_session",{tabs:tabs.filter(t=>t.dirty||!t.path).map(t=>({name:t.name,path:t.path,text:t.text,orig:t.orig,dirty:t.dirty})),t:Date.now()})},500)};
ed.addEventListener("input",()=>{const t=T();t.dirty=t.text!==ed.value||t.dirty;t.dirty=ed.value!==t.orig;t.text=ed.value;draw();upd();OS.title(win,"Notepad - "+t.name+(t.dirty?" •":""));persist()});
ed.addEventListener("keyup",upd);ed.addEventListener("click",upd);
ed.addEventListener("keydown",e=>{if(e.key==="Tab"&&!e.ctrlKey){e.preventDefault();document.execCommand&&document.execCommand("insertText",false," ")}else if(e.ctrlKey&&e.key==="s"){e.preventDefault();e.shiftKey?saveAs():save()}else if(e.ctrlKey&&e.key==="f"){e.preventDefault();fbar.style.display="flex";fnd.focus()}else if(e.ctrlKey&&e.key==="n"){e.preventDefault();newTab()}else if(e.ctrlKey&&e.key==="o"){e.preventDefault();openV()}});
const load=async p=>{const n=V.get(p);if(!n||n.dir)return toast("Cannot open "+p,1);const ex=tabs.find(t=>t.path===V.path(n));if(ex)return sw(ex.id);let tx;try{tx=await V.read(n,"text")}catch(e){return toast("Cannot read "+n.name+": "+e.message,1)}const c=T();if(c&&!c.path&&!c.dirty&&!ed.value)tabs.splice(tabs.indexOf(c),1);newTab({name:n.name,path:V.path(n),text:tx,orig:tx})};
const save=async()=>{const t=T();t.text=ed.value;if(!t.path||!V.writable(V.get(t.path))&&V.get(t.path))return saveAs();try{await V.write(t.path,t.text);t.orig=t.text;t.dirty=false;draw();OS.title(win,"Notepad - "+t.name);toast("Saved "+t.path);persist()}catch(e){toast("Save failed: "+e.message,1)}};
const saveAs=async()=>{const t=T();t.text=ed.value;const p=await pickPath({mode:"save",name:t.name});if(!p)return;try{await V.write(p,t.text);t.path=p;t.name=p.split("/").pop();t.orig=t.text;t.dirty=false;draw();OS.title(win,"Notepad - "+t.name);toast("Saved "+p);persist()}catch(e){toast("Save failed: "+e.message,1)}};
const openV=async()=>{const p=await pickPath({title:"Open file"});if(p)load(p)};
const openDisk=async()=>{for(const f of await pickDisk(".txt,.md,.json,.csv,.html,.js,.css,.log,text/*",true)){const tx=await f.text();newTab({name:f.name,text:tx,orig:"",dirty:true})}};
const dlCur=()=>{const t=T();t.text=ed.value;download(t.name,t.text,"text/plain;charset=utf-8");toast("Downloading "+t.name)};
const toDisk=async()=>{const t=T();t.text=ed.value;if(window.showSaveFilePicker){try{const h_=await showSaveFilePicker({suggestedName:t.name});const w=await h_.createWritable();await w.write(t.text);await w.close();toast("Saved to your computer: "+h_.name);return}catch(e){if(e.name==="AbortError")return}}dlCur()};
const zipHome=async()=>{const c=T();if(c)c.text=ed.value;const files=[],enc=new TextEncoder();const walk=async(n,pre)=>{for(const k of V.ls(n)){if(k.dir)await walk(k,pre+k.name+"/");else{const buf=new Uint8Array(await V.read(k,"buf"));files.push({name:pre+k.name,data:buf,mt:k.mt})}}};await walk(V.user,"");for(const t of tabs)if(!t.path)files.push({name:"unsaved/"+t.name,data:enc.encode(t.text),mt:Date.now()});if(!files.length)return toast("Nothing to export yet.",1);download("terminal-velocity-os-files-"+new Date().toISOString().slice(0,10)+".zip",makeZip(files));toast("Exported "+files.length+" files as ZIP")};
const zipTabs=()=>{const c=T();c.text=ed.value;const enc=new TextEncoder();download("notes.zip",makeZip(tabs.map((t,i)=>({name:t.name,data:enc.encode(t.text),mt:Date.now()}))));toast("Exported "+tabs.length+" tab(s) as ZIP")};
let fpos=0;const findNext=d=>{const q=fnd.value;if(!q)return;const v=ed.value.toLowerCase(),ql=q.toLowerCase();let i=d>0?v.indexOf(ql,ed.selectionEnd):v.lastIndexOf(ql,ed.selectionStart-1);if(i<0)i=d>0?v.indexOf(ql):v.lastIndexOf(ql);if(i<0){$("#fcnt",fbar).textContent="not found";return}ed.focus();ed.setSelectionRange(i,i+q.length);$("#fcnt",fbar).textContent=(v.split(ql).length-1)+" matches";const lh=parseFloat(getComputedStyle(ed).lineHeight)||20;ed.scrollTop=Math.max(0,(ed.value.slice(0,i).split("\n").length-4)*lh)};
function replOne(){const q=fnd.value;if(!q)return;if(ed.value.slice(ed.selectionStart,ed.selectionEnd).toLowerCase()===q.toLowerCase()){const s=ed.selectionStart;ed.setRangeText(rep.value,s,ed.selectionEnd,"end");ed.dispatchEvent(new Event("input"))}findNext(1)}
function replAll(){const q=fnd.value;if(!q)return;const re=new RegExp(q.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),"gi");ed.value=ed.value.replace(re,rep.value);ed.dispatchEvent(new Event("input"))}
fnd.addEventListener("keydown",e=>{if(e.key==="Enter")findNext(e.shiftKey?-1:1);if(e.key==="Escape"){fbar.style.display="none";ed.focus()}});
const bt=(i,t,f,txt)=>h("button",{class:"btn sm",title:t,onclick:f},ic(i),txt||"");
const bar=h("div",{class:"row"},bt("plus","New (Ctrl+N)",()=>newTab(),"New"),bt("folder","Open from Terminal Velocity OS files",openV,"Open"),bt("dl","Open a file from your computer",openDisk,"From disk"),bt("save","Save (Ctrl+S)",save,"Save"),bt("save","Save as…",saveAs,"Save as"),h("button",{class:"btn sm",title:"Download this note to your computer",onclick:dlCur},ic("dl"),"Download"),h("button",{class:"btn sm",title:"Save to anywhere on your computer (file picker, Chromium) or download",onclick:toDisk},ic("usb"),"Save to computer"),h("button",{class:"btn sm",title:"Export every file in /home/user and unsaved tabs as one ZIP",onclick:zipHome},ic("zip"),"Export all (ZIP)"),h("button",{class:"btn sm",onclick:zipTabs,title:"ZIP of open tabs"},ic("zip"),"Tabs ZIP"),h("span",{class:"grow"}),bt("search","Find (Ctrl+F)",()=>{fbar.style.display=fbar.style.display==="none"?"flex":"none";fnd.focus()}),h("button",{class:"btn sm",onclick:e=>{wrap=!wrap;ed.style.whiteSpace=wrap?"pre-wrap":"pre";e.currentTarget.classList.toggle("on",wrap)}},"Wrap"),h("button",{class:"btn sm",onclick:e=>{prev=!prev;pv.style.display=prev?"block":"none";e.currentTarget.classList.toggle("on",prev);upd()}},"Preview"),bt("code","Run as HTML app",()=>{const t=T();OS.open("runner",{code:ed.value,name:t.name})},"Run"));
bar.lastChild.previousSibling.classList.add("on");
b.append(bar,tabsEl,fbar,h("div",{style:"flex:1;display:flex;min-height:0"},ed,pv),st);
win.onclose=async()=>{const c=T();if(c)c.text=ed.value;const d=tabs.filter(t=>t.dirty&&t.text!==t.orig);if(d.length&&!await dlgConfirm(d.length+" tab(s) have unsaved changes. Close anyway? (A draft is kept and restored next time.)","Notepad","Close")){return false}persist()};
win.openPath=load;(async()=>{const s=await DB.get("np_session");if(typeof arg==="string"){await load(arg)}else if(arg&&arg.code!=null){newTab({name:arg.name||"untitled.html",text:arg.code,orig:""})}else if(s&&s.tabs&&s.tabs.length&&!OS.wins.some(w=>w!==win&&w.app==="notepad")){s.tabs.forEach(t=>newTab(t));toast("Restored "+s.tabs.length+" unsaved note(s)")}if(!tabs.length)newTab();ed.focus()})()}});
/* --- Media Player --- */
defApp("media",{name:"Media Player",icon:"media",cat:"Media",w:820,h:520,desc:"audio & video, playlists, visualizer",kw:"music video mp3 mp4 player playlist play",
init(win,b,arg){let q=[],qi=-1,shuf=false,rep=0,noViz=false,actx=null,an=null,srcNode=null,raf=0;const v=h("video",{style:"width:100%;height:100%;background:#000;object-fit:contain",playsinline:true,preload:"metadata"}),cv=h("canvas",{style:"position:absolute;inset:0;width:100%;height:100%;pointer-events:none"}),stage=h("div",{style:"position:relative;flex:1;min-height:0;background:#000"},v,cv),plist=h("div",{class:"scr md-pl",style:"width:300px;flex:none;border-left:1px solid var(--bd)"}),
seek=h("input",{type:"range",min:0,max:1000,value:0,style:"flex:1;accent-color:var(--ac)"}),tm=h("span",{class:"dim",style:"min-width:90px;text-align:center"},"0:00 / 0:00"),vol=h("input",{type:"range",min:0,max:1,step:.01,value:.8,style:"width:80px;accent-color:var(--ac)"}),title=h("span",{class:"grow",style:"overflow-wrap:anywhere;line-height:1.3"},"Nothing playing");
const dlist=()=>{plist.innerHTML="";q.forEach((it,i)=>plist.append(h("div",{class:"si"+(i===qi?" sel":""),title:it.name,style:"align-items:flex-start",onclick:()=>play(i),oncontextmenu:e=>{e.preventDefault();ctxMenu(e.clientX,e.clientY,[{t:"Remove",ic:"trash",fn:()=>{q.splice(i,1);if(i{const n=V.get(p);if(!n||n.dir)return null;const k=V.kind(n);if(k!=="audio"&&k!=="video")return null;return{name:n.name,path:V.path(n),video:k==="video",node:n}};
const add=(paths,go)=>{const items=paths.map(mkItem).filter(Boolean);if(!items.length)return toast("No playable media there.",1);const first=q.length;q.push(...items);dlist();if(go||qi<0)play(first)};
async function play(i){if(i<0||i>=q.length)return;qi=i;const it=q[i];dlist();title.textContent=it.name;OS.title(win,"Media - "+it.name);let url;try{url=it.url||await V.read(it.node,"url")}catch(e){return toast(e.message,1)}
const tryUrl=(u,cors)=>{v.removeAttribute("crossorigin");if(cors&&!/^blob:/.test(u))v.crossOrigin="anonymous";v.src=u;v.play().catch(()=>{})};
v.onerror=()=>{if(v.crossOrigin&&!/^blob:/.test(url)){noViz=true;tryUrl(url,false)}else toast("Cannot play "+it.name+" (format unsupported or the host blocked it).",1)};tryUrl(url,!noViz&&!it.video)}
const next=d=>{if(!q.length)return;let i=shuf?Math.floor(Math.random()*q.length):qi+d;if(i>=q.length)i=rep?0:-1;if(i<0&&!shuf)i=rep?q.length-1:0;if(i>=0&&(i!==qi||rep===2||q.length===1))play(i)};
v.addEventListener("ended",()=>{if(rep===2)play(qi);else if(qi{if(!seek.dragging)seek.value=v.duration?v.currentTime/v.duration*1000:0;tm.textContent=fmtTime(v.currentTime)+" / "+fmtTime(v.duration)});
seek.addEventListener("input",()=>{seek.dragging=1;v.currentTime=seek.value/1000*(v.duration||0)});seek.addEventListener("change",()=>seek.dragging=0);
vol.addEventListener("input",()=>v.volume=vol.value);v.volume=.8;
v.addEventListener("play",()=>{startViz();pp.replaceChildren(ic("pause"))});v.addEventListener("pause",()=>pp.replaceChildren(ic("play")));
const startViz=()=>{if(!noViz&&!actx&&v.crossOrigin||(!noViz&&!actx&&q[qi]&&/^blob:|^data:/.test(v.src))){try{actx=new (window.AudioContext||window.webkitAudioContext)();an=actx.createAnalyser();an.fftSize=128;srcNode=actx.createMediaElementSource(v);srcNode.connect(an);an.connect(actx.destination)}catch(e){actx=null;noViz=true}}if(actx&&actx.state==="suspended")actx.resume();cancelAnimationFrame(raf);loop()};
const loop=()=>{raf=requestAnimationFrame(loop);if(win.dead||win.min)return;const c=cv.getContext("2d"),W=cv.width=cv.clientWidth,H=cv.height=cv.clientHeight;c.clearRect(0,0,W,H);const it=q[qi];if(it&&it.video&&!v.paused&&v.videoWidth)return;const n=48,bw=W/n,ac=css("ac"),a2=css("ac2");let d=null;if(an&&!noViz){d=new Uint8Array(an.frequencyBinCount);an.getByteFrequencyData(d)}const t=performance.now()/1000;
if(!it){c.fillStyle=ac;c.globalAlpha=.5;c.font="14px monospace";c.textAlign="center";c.fillText("TERMINAL VELOCITY MEDIA",W/2,H/2);return}
for(let i=0;ix))m=(d[Math.floor(i*d.length/n)]||0)/255;else m=v.paused?.03:(.25+.25*Math.sin(t*3+i*.5)+.2*Math.sin(t*5.3+i*1.3))*(.5+.5*Math.sin(t*50/60*6.283));m=Math.max(.02,Math.min(1,m));c.fillStyle=i%2?a2:ac;c.globalAlpha=.8;c.fillRect(i*bw+1,H-m*H*.8-10,bw-2,m*H*.8)}c.globalAlpha=1};
const pp=h("button",{class:"btn",onclick:()=>v.paused?v.play():v.pause()},ic("play")),bt=(i,t,f)=>h("button",{class:"btn",title:t,onclick:f},ic(i));
const spd=h("select",{class:"t",onchange:()=>v.playbackRate=+spd.value},...[.5,.75,1,1.25,1.5,2].map(x=>h("option",{value:x,selected:x===1},x+"x")));
const ctr=h("div",{class:"row"},bt("prev","Previous",()=>next(-1)),pp,bt("next","Next",()=>next(1)),seek,tm,ic("music"),vol,spd);
const sb=h("button",{class:"btn sm",onclick:e=>{shuf=!shuf;e.currentTarget.classList.toggle("on",shuf)}},"Shuffle"),rb=h("button",{class:"btn sm",onclick:e=>{rep=(rep+1)%3;e.currentTarget.textContent=["Repeat off","Repeat all","Repeat one"][rep];e.currentTarget.classList.toggle("on",rep>0)}},"Repeat off");
const top=h("div",{class:"row"},h("button",{class:"btn sm",onclick:async()=>{const p=await pickPath({title:"Add media"});if(p)add([p],true)}},ic("plus"),"Add from files"),h("button",{class:"btn sm",onclick:async()=>{const fs=await pickDisk("audio/*,video/*",true);if(!fs.length)return;for(const f of fs){q.push({name:f.name,url:URL.createObjectURL(f),video:f.type.startsWith("video")})}dlist();if(qi<0)play(0)}},ic("dl"),"From computer"),h("button",{class:"btn sm",onclick:async()=>{const p=await pickPath({mode:"dir",title:"Add every media file in a folder"});if(p){const d=V.get(p);add(V.ls(d).map(V.path),true)}}},ic("folder"),"Add folder"),sb,rb,h("button",{class:"btn sm",onclick:()=>{q=[];qi=-1;v.removeAttribute("src");v.load();dlist()}},"Clear"),h("span",{class:"grow"}),h("button",{class:"btn sm",onclick:()=>stage.requestFullscreen&&stage.requestFullscreen()},"Fullscreen"));
b.append(top,h("div",{class:"md-row",style:"flex:1;display:flex;min-height:0"},stage,plist),h("div",{class:"row",style:"border-bottom:0;border-top:1px solid var(--bd)"},title),ctr);
win.onArg=a=>{if(typeof a==="string")add([a],true);else if(a&&a.queue)add(a.queue,true)};win.on.close=()=>{cancelAnimationFrame(raf);v.pause();v.removeAttribute("src");actx&&actx.close&&actx.close()};
b.tabIndex=0;b.addEventListener("keydown",e=>{if(e.target.tagName==="INPUT"&&e.target.type!=="range")return;if(e.key===" "){e.preventDefault();v.paused?v.play():v.pause()}else if(e.key==="ArrowRight")v.currentTime+=5;else if(e.key==="ArrowLeft")v.currentTime-=5});
dlist();loop();if(typeof arg==="string")add([arg],true);else if(arg&&arg.queue)add(arg.queue,true)},
onArg(win,a){win.onArg(a)}});
/* --- Image viewer --- */
defApp("images",{name:"Image Viewer",icon:"image",cat:"Media",w:760,h:520,desc:"view images, zoom, slideshow",kw:"picture photo png jpg svg",
init(win,b,arg){let list=[],i=0,z=1,tx=0,ty=0,rot=0,fit=true,slide=0;const img=h("img",{style:"position:absolute;left:50%;top:50%;transform-origin:center;max-width:none;user-select:none;-webkit-user-drag:none",draggable:false}),st=h("div",{style:"flex:1;position:relative;overflow:hidden;background:repeating-conic-gradient(var(--pn2) 0% 25%,var(--bg) 0% 50%) 0 0/20px 20px;touch-action:none"},img),info=h("span",{class:"dim grow"},"No image");
const apply=()=>{img.style.transform=`translate(calc(-50% + ${tx}px),calc(-50% + ${ty}px)) rotate(${rot}deg) scale(${z})`};
const fitNow=()=>{if(!img.naturalWidth)return;const W=st.clientWidth,H=st.clientHeight,r=(rot/90)%2?[img.naturalHeight,img.naturalWidth]:[img.naturalWidth,img.naturalHeight];z=Math.min(1,W/r[0]*.96,H/r[1]*.96);tx=ty=0;apply()};
const show=async(k)=>{if(k<0||k>=list.length)return;i=k;const n=list[k];let u;try{u=await V.read(n,"url")}catch(e){return toast(e.message,1)}img.onload=()=>{rot=0;fitNow();info.textContent=`${n.name} · ${img.naturalWidth}×${img.naturalHeight} · ${fmtBytes(n.size)} · ${k+1}/${list.length}`};img.onerror=()=>{info.textContent="Cannot display "+n.name};img.src=u;OS.title(win,"Image - "+n.name)};
const open=p=>{const n=V.get(p);if(!n||n.dir)return;list=V.ls(n.parent).filter(x=>V.kind(x)==="image");i=list.indexOf(n);if(i<0){list=[n];i=0}show(i)};
st.addEventListener("wheel",e=>{e.preventDefault();const f=e.deltaY<0?1.15:1/1.15;z=Math.max(.05,Math.min(40,z*f));apply()},{passive:false});
st.addEventListener("pointerdown",e=>{const sx=e.clientX-tx,sy=e.clientY-ty;st.setPointerCapture(e.pointerId);const mv=ev=>{tx=ev.clientX-sx;ty=ev.clientY-sy;apply()},up=()=>{st.removeEventListener("pointermove",mv);st.removeEventListener("pointerup",up)};st.addEventListener("pointermove",mv);st.addEventListener("pointerup",up)});
st.addEventListener("dblclick",fitNow);win.on.resize=fitNow;
const bt=(t,f,txt)=>h("button",{class:"btn sm",onclick:f},txt);
b.append(h("div",{class:"row"},bt("",()=>show(i-1),"◀ Prev"),bt("",()=>show(i+1),"Next ▶"),bt("",()=>{z*=1.25;apply()},"+"),bt("",()=>{z/=1.25;apply()},"−"),bt("",fitNow,"Fit"),bt("",()=>{z=1;tx=ty=0;apply()},"1:1"),bt("",()=>{rot=(rot+90)%360;fitNow()},"Rotate"),bt("",async()=>{const p=await pickPath({title:"Open image",exts:["image"]});if(p)open(p)},"Open…"),bt("",()=>{const n=list[i];n&&V.read(n,"blob").then(bl=>download(n.name,bl)).catch(e=>toast(e.message,1))},"Download"),h("button",{class:"btn sm",onclick:e=>{if(slide){clearInterval(slide);slide=0;e.currentTarget.classList.remove("on")}else{slide=setInterval(()=>show((i+1)%list.length),3500);e.currentTarget.classList.add("on")}}},"Slideshow")),st,h("div",{class:"row",style:"border-bottom:0;border-top:1px solid var(--bd)"},info));
win.on.close=()=>clearInterval(slide);b.tabIndex=0;b.addEventListener("keydown",e=>{if(e.key==="ArrowRight")show(i+1);else if(e.key==="ArrowLeft")show(i-1)});win.open=open;if(typeof arg==="string")open(arg);else info.textContent="Open an image from Files, or press Open…"},
single:false,onArg(w,a){}});
/* --- Calculator --- */
function calcEval(src,deg,ans){const s=String(src).replace(/×/g,"*").replace(/÷/g,"/").replace(/−/g,"-").replace(/π/g,"pi").replace(/\s+/g,"").toLowerCase();let i=0;
const F={sin:x=>Math.sin(deg?x*Math.PI/180:x),cos:x=>Math.cos(deg?x*Math.PI/180:x),tan:x=>Math.tan(deg?x*Math.PI/180:x),asin:x=>{const r=Math.asin(x);return deg?r*180/Math.PI:r},acos:x=>{const r=Math.acos(x);return deg?r*180/Math.PI:r},atan:x=>{const r=Math.atan(x);return deg?r*180/Math.PI:r},sqrt:Math.sqrt,cbrt:Math.cbrt,ln:Math.log,log:Math.log10,log2:Math.log2,exp:Math.exp,abs:Math.abs,floor:Math.floor,ceil:Math.ceil,round:Math.round,fact:x=>{if(x<0||x>170||x%1)return NaN;let r=1;for(let k=2;k<=x;k++)r*=k;return r}};
const C={pi:Math.PI,e:Math.E,ans:ans||0,tau:Math.PI*2};
function ex(){let v=tm();while(s[i]==="+"||s[i]==="-"){const o=s[i++],r=tm();v=o==="+"?v+r:v-r}return v}
function tm(){let v=pw();while(s[i]==="*"||s[i]==="/"||s[i]==="%"){const o=s[i++],r=pw();v=o==="*"?v*r:o==="/"?v/r:v%r}return v}
function pw(){let b=un();if(s[i]==="^"){i++;return Math.pow(b,pw())}return b}
function un(){if(s[i]==="-"){i++;return-un()}if(s[i]==="+"){i++;return un()}return pf()}
function pf(){let v=at();while(s[i]==="!"){i++;v=F.fact(v)}return v}
function at(){if(s[i]==="("){i++;const v=ex();if(s[i]!==")")throw new Error("missing )");i++;return v}const m=/^(\d+\.?\d*|\.\d+)(e[+-]?\d+)?/.exec(s.slice(i));if(m){i+=m[0].length;return parseFloat(m[0])}const w=/^[a-z0-9]+/.exec(s.slice(i));if(w){const k=w[0];i+=k.length;if(s[i]==="("&&F[k]){i++;const a=ex();if(s[i]!==")")throw new Error("missing )");i++;return F[k](a)}if(k in C)return C[k];throw new Error("unknown: "+k)}throw new Error("syntax error")}
if(!s)return 0;const v=ex();if(i{try{const v=calcEval(inp.value,deg,ans);res.textContent=isFinite(v)?"= "+(+v.toPrecision(12)):"= undefined";return v}catch(e){res.textContent=inp.value?e.message:"";return undefined}};
const eq=()=>{const v=calc();if(v!==undefined&&isFinite(v)){hist.push(inp.value+" = "+ +v.toPrecision(12));hl.innerHTML="";hist.slice(-6).forEach(x=>hl.append(h("div",{},x)));ans=v;inp.value=String(+v.toPrecision(12));res.textContent=""}};
inp.addEventListener("input",calc);inp.addEventListener("keydown",e=>{if(e.key==="Enter"){e.preventDefault();eq()}else if(e.key==="Escape")inp.value=""});
const keys=[["sin(","cos(","tan(","sqrt(","C"],["ln(","log(","^","(",")"],["7","8","9","÷","⌫"],["4","5","6","×","π"],["1","2","3","−","ans"],["0",".","!","+","="]];
const g=h("div",{style:"display:grid;grid-template-columns:repeat(5,1fr);gap:4px;padding:8px;flex:1"});
for(const r of keys)for(const k of r)g.append(h("button",{class:"btn"+(k==="="?" pri":""),style:"justify-content:center;font-size:15px",onpointerdown:e=>e.preventDefault(),onclick:()=>{if(k==="C"){inp.value="";res.textContent=""}else if(k==="⌫")inp.value=inp.value.slice(0,-1);else if(k==="=")eq();else inp.value+=k;if(!coarse)inp.focus();calc()}},k));
b.append(h("div",{class:"row"},h("button",{class:"btn sm",onclick:e=>{deg=!deg;e.currentTarget.textContent=deg?"DEG":"RAD";calc()}},"RAD"),h("span",{class:"dim"},coarse?"tap the display to type":"type or tap · Enter =")),hl,h("div",{style:"padding:6px 8px 0"},inp),res,g);if(!coarse)setTimeout(()=>inp.focus(),30)}});
/* --- Reader (markdown / long text) --- */
defApp("reader",{name:"Reader",icon:"file",cat:"Media",w:700,h:560,desc:"read markdown & text comfortably",kw:"book read markdown txt article essay",
init(win,b,arg){let fs=LS.get("tvos_rfs",16),path=null;const body=h("div",{class:"scr",style:"flex:1;padding:18px 8%;font-family:Georgia,'Iowan Old Style',serif;line-height:1.7"}),sty=h("style");sty.textContent=`.rd h1,.rd h2,.rd h3{color:var(--ac);line-height:1.25}.rd code,.rd pre{background:var(--bg);padding:1px 5px;border-radius:4px}.rd pre{padding:10px;overflow:auto;white-space:pre-wrap}.rd blockquote{border-left:3px solid var(--ac);margin:8px 0;padding:2px 12px;color:var(--dim)}.rd p{margin:.7em 0}`;body.classList.add("rd");
const setFs=d=>{fs=Math.max(11,Math.min(30,fs+d));body.style.fontSize=fs+"px";LS.set("tvos_rfs",fs)};setFs(0);
const open=async p=>{const n=V.get(p);if(!n||n.dir)return;let t;try{t=await V.read(n,"text")}catch(e){body.textContent="Cannot read: "+e.message;return}path=V.path(n);OS.title(win,"Reader - "+n.name);body.innerHTML=/\.md|markdown$/i.test(n.name)?md(t):""+esc(t)+"
";body.scrollTop=LS.get("tvos_rpos_"+path,0)};
body.addEventListener("scroll",()=>{if(path)LS.set("tvos_rpos_"+path,body.scrollTop)});
b.append(sty,h("div",{class:"row"},h("button",{class:"btn sm",onclick:async()=>{const p=await pickPath({title:"Open text or markdown"});if(p)open(p)}},ic("folder"),"Open…"),h("button",{class:"btn sm",onclick:()=>setFs(-1)},"A−"),h("button",{class:"btn sm",onclick:()=>setFs(1)},"A+"),h("button",{class:"btn sm",onclick:()=>path&&OS.open("notepad",path)},"Edit in Notepad")),body);
win.openPath=open;body.append(h("div",{class:"dim"},"Open a .md or .txt file from the archive or your files."));if(typeof arg==="string")open(arg)},onArg(w,a){typeof a==="string"&&w.openPath(a)},single:false});
/* --- HTML App Runner (sandboxed) --- */
defApp("runner",{name:"App Runner",icon:"code",cat:"Tools",w:760,h:520,desc:"run HTML apps in a sandbox",kw:"html run sandbox code web app preview",
init(win,b,arg){const ta=h("textarea",{class:"t",spellcheck:"false",style:"width:100%;height:100%;border:0;border-radius:0;resize:none;padding:10px",placeholder:"Paste HTML here and press Run. It runs in a sandbox: scripts allowed, no access to your files, saves or this OS."}),fr=h("iframe",{sandbox:"allow-scripts allow-forms allow-modals allow-popups allow-pointer-lock",style:"flex:1;border:0;background:#fff;width:100%"}),pane=h("div",{style:"flex:1;min-height:0;display:flex;flex-direction:column"},fr);let srcMode=false;
const run=()=>{fr.srcdoc=ta.value};const setSrc=on=>{srcMode=on;pane.replaceChildren(on?ta:fr)};
b.append(h("div",{class:"row"},h("button",{class:"btn sm pri",onclick:()=>{setSrc(false);run()}},ic("play"),"Run"),h("button",{class:"btn sm",onclick:()=>setSrc(!srcMode)},"Source"),h("button",{class:"btn sm",onclick:async()=>{const p=await pickPath({title:"Open HTML"});if(p)load(p)}},ic("folder"),"Open…"),h("button",{class:"btn sm",onclick:()=>OS.open("notepad",{code:ta.value,name:"app.html"})},"Edit in Notepad"),h("button",{class:"btn sm",onclick:()=>{const u=URL.createObjectURL(new Blob([ta.value],{type:"text/html"}));window.open(u,"_blank")}},"Open in tab"),h("span",{class:"dim"},"sandboxed")),pane);
const load=async p=>{const n=V.get(p);if(!n||n.dir)return;try{ta.value=await V.read(n,"text")}catch(e){return toast(e.message,1)}OS.title(win,"App Runner - "+n.name);setSrc(false);run()};
win.load=load;if(typeof arg==="string")load(arg);else if(arg&&arg.code!=null){ta.value=arg.code;run()}else{ta.value="helloHello from the sandbox.