local component=require("component") local computer=require("computer") local event=require("event") local fs=require("filesystem") local unicode=require("unicode") local gfx=require("yaru.gfx") local T=require("yaru.theme") local ui=require("yaru.ui") local Y={running=false,windows={},ws=1,wsCount=4,notifs={},toasts={}, clipboard="",apps={},appOrder={},modules={},nextId=1,stats={}} package.loaded["yaru.core"]=Y local K={esc=0x01,tab=0x0F,enter=0x1C,lctrl=0x1D,rctrl=0x9D,lshift=0x2A,rshift=0x36, lalt=0x38,ralt=0xB8,f1=0x3B,f2=0x3C,f4=0x3E,f10=0x44,up=0xC8,down=0xD0, left=0xCB,right=0xCD,pgup=0xC9,pgdn=0xD1,home=0xC7,["end"]=0xCF,del=0xD3, back=0x0E,lsuper=0xDB,rsuper=0xDC,sysrq=0xB7,a=0x1E,d=0x20,e=0x12,h=0x23, l=0x26,t=0x14,v=0x2F,s=0x1F,c=0x2E,space=0x39} Y.K=K local mods={} local function ctrl()return mods[K.lctrl]or mods[K.rctrl]end local function alt()return mods[K.lalt]or mods[K.ralt]end local function shift()return mods[K.lshift]or mods[K.rshift]end local function super()return mods[K.lsuper]or mods[K.rsuper]end Y.mods={ctrl=ctrl,alt=alt,shift=shift,super=super} local W,H,DOCK=160,50,12 local gpu,screenAddr,kbAddr local now local CFG_PATH="/etc/yaru.cfg" local defaults={dark=true,accent="orange",wallpaper="noble",favorites={"files","terminal","editor","monitor","settings"}, clockSeconds=false,clockWeekday=true,clockReal=true,tz=0,dnd=false,beeps=true,showMemory=true, desktopIcons=true,autostart=true,autologin=false,password=nil,blank=0,resolution=nil,dockClickMinimize=true} function Y.loadConfig() local cfg={} local f=io.open(CFG_PATH,"r") if f then local ok,t=pcall(require("serialization").unserialize,f:read("*a")) f:close() if ok and type(t)=="table"then cfg=t end end for k,v in pairs(defaults)do if cfg[k]==nil then cfg[k]=v end end Y.cfg=cfg end function Y.saveConfig() local f=io.open(CFG_PATH,"w") if f then f:write(require("serialization").serialize(Y.cfg));f:close()end end function Y.applyTheme() Y.theme=T.build(Y.cfg.dark,Y.cfg.accent) gfx.setCustomPalette(Y.theme.palette) local wp=Y.cfg.wallpaper if type(wp)=="string"and wp:sub(1,1)=="/"and Y.setPictureWallpaper(wp)then else local wall=require("yaru.wall") if not wall.designs[wp]then Y.cfg.wallpaper="noble"end gfx.setWallpaper(wall.generate(Y.cfg.wallpaper,W,H),W) package.loaded["yaru.wall"]=nil end gfx.damageAll() end function Y.setPictureWallpaper(path) local img=require("yaru.image").load(path,true) if not img then return false end local iw,ih,d=img[1],img[2],img[3] local chars,cidx,out,row={},{},{},{} local byte=string.char for y=0,H-1 do local sy=y*ih//H for x=0,W-1 do local k=(sy*iw+x*iw//W+1)*3 local cp=d[k-2] local ci=cidx[cp] if not ci then if#chars>=255 then ci=cidx[32]or 1 else chars[#chars+1]=cp;ci=#chars;cidx[cp]=ci end end local bg=d[k] row[x+1]=byte(ci,gfx.color(d[k-1]),gfx.color(bg>=0 and bg or 0)) end out[y+1]=table.concat(row,"",1,W) end gfx.setWallpaper(table.concat(out),W,chars) return true end local timeBase,timeUp function Y.now() local up=computer.uptime() if not Y.cfg.clockReal then return os.time()end if not timeBase or up-timeUp>600 then local p="/tmp/.yaru_clock" local f=io.open(p,"w") local ms=0 if f then f:write("");f:close();ms=fs.lastModified(p)or 0 end if ms>0 then timeBase,timeUp=ms//1000,up else return os.time()end end return timeBase+math.floor(up-timeUp)+math.floor(Y.cfg.tz*3600) end now=Y.now function Y.date(fmt,t)return os.date("!"..fmt,t or now())end local builtin={ {"files","Files","folder"},{"terminal","Terminal","terminal"},{"editor","Text Editor","editor"}, {"monitor","System Monitor","monitor"},{"settings","Settings","settings"},{"calc","Calculator","calc"}, {"viewer","Image Viewer","viewer"},{"paint","Paint","paint"},{"notes","Notes","notes"}, {"clock","Clocks","clock"},{"disks","Disks","disks"},{"redstone","Redstone","redstone"}, {"chat","Messages","chat"},{"mines","Mines","mines"},{"g2048","2048","2048"}, {"explorer","Component Explorer","explorer"},{"backup","Backups","backup"},{"search","Search","search"}, {"energy","Power Statistics","energy"},{"storage","Storage","storage"},{"scanner","Terrain Scanner","scanner"}, {"scheduler","Scheduled Tasks","scheduler"},{"sudoku","Sudoku","sudoku"},{"tetris","Quadrapassel","tetris"}, {"player","Media Player","player"},{"composer","Composer","composer"},{"web","Web Browser","web"}, } local ICONDIR="/usr/share/yaru/icons/" Y.icons={files="folder",terminal="terminal",editor="editor",monitor="monitor",settings="settings", calc="calc",trash="trash",drive="hdd",floppy="floppy",home="folder",generic="app",script="script"} function Y.registerApps() Y.apps,Y.appOrder={},{} for _,b in ipairs(builtin)do local path="/usr/share/yaru/apps/"..b[1]..".lua" if fs.exists(path)then Y.apps[b[1]]={id=b[1],name=b[2],icon=b[3],path=path} Y.appOrder[#Y.appOrder+1]=b[1] end end local dir="/usr/share/applications" if fs.isDirectory(dir)then for name in fs.list(dir)do if name:match("%.lua$")then local ok,d=pcall(dofile,fs.concat(dir,name)) if ok and type(d)=="table"and d.name and d.exec then local id="user:"..name:gsub("%.lua$","") d.id,d.user=id,true Y.apps[id]=d Y.appOrder[#Y.appOrder+1]=id end end end end end local function appModule(id) local m=Y.modules[id] if not m then local a=Y.apps[id] local chunk,err=loadfile(a.path) if not chunk then Y.notify("Cannot start "..a.name,tostring(err))return end local ok,mod=xpcall(chunk,debug.traceback) if not ok then Y.notify("Cannot start "..a.name,tostring(mod))return end m=mod Y.modules[id]=m end return m end function Y.launch(id,...) local a=Y.apps[id] if not a then return end if a.user then if a.fullscreen then return Y.runFullscreen(a.exec)end return Y.launch("terminal",a.exec) end local m=appModule(id) if m then local ok,err=xpcall(m.open,debug.traceback,...) if not ok then Y.notify(a.name.." crashed",tostring(err):match("[^\n]*"))end end end local function maybeUnload(id) for _,w in ipairs(Y.windows)do if w.app==id then return end end local m=Y.modules[id] if m and not m.resident then Y.modules[id]=nil end end local function workArea()return DOCK+1,2,W-DOCK,H-1 end Y.workArea=workArea local function winDamage(w) gfx.damage(w.x,w.y-1,w.w+1,w.h+3) end function Y.redraw(w) if w then if not w.minimized and w.ws==Y.ws then winDamage(w)end else gfx.damageAll() end end local function clampWin(w) local ax,ay,aw,ah=workArea() w.w=math.max(w.minW or 20,math.min(w.w,aw)) w.h=math.max(w.minH or 6,math.min(w.h,ah)) w.x=math.max(1-w.w+8,math.min(w.x,W-6)) w.y=math.max(ay+1,math.min(w.y,H-1)) end function Y.newWindow(o) local w=o w.id=Y.nextId;Y.nextId=w.id+1 w.ws=Y.ws w.hits,w.hhits={},{} w.w,w.h=w.w or 70,w.h or 22 local ax,ay,aw,ah=workArea() local n=0 for _,o2 in ipairs(Y.windows)do if o2.ws==Y.ws then n=n+1 end end w.x=w.x or ax+(aw-w.w)//2+(n%5)*3-6 w.y=w.y or ay+math.max(0,(ah-w.h)//3)+(n%5) clampWin(w) table.insert(Y.windows,w) Y.focusWin(w) winDamage(w) return w end function Y.setPrecise(on) on=on and true or false if on==Y.precise or not screenAddr then return end local ok=pcall(component.invoke,screenAddr,"setPrecise",on) local okq,isP=pcall(component.invoke,screenAddr,"isPrecise") Y.precise=ok and okq and isP==true end function Y.focusWin(w) if Y.focus and Y.focus~=w then Y.redraw(Y.focus);if Y.focus.blur then pcall(Y.focus.blur,Y.focus)end end if w then for i,o in ipairs(Y.windows)do if o==w then table.remove(Y.windows,i)break end end table.insert(Y.windows,w) w.minimized=false if w.ws~=Y.ws then Y.switchWs(w.ws)end winDamage(w) if w.focusIn then pcall(w.focusIn,w)end end Y.focus=w Y.setPrecise(w and w.precise) Y.dockDirty() gfx.damage(1,1,W,1) end local function topWindow() for i=#Y.windows,1,-1 do local w=Y.windows[i] if w.ws==Y.ws and not w.minimized then return w end end end function Y.closeWin(w,force) if not force and w.onclose then local ok,veto=pcall(w.onclose,w) if ok and veto==false then return end end if w.cleanup then pcall(w.cleanup,w)end winDamage(w) for i,o in ipairs(Y.windows)do if o==w then table.remove(Y.windows,i)break end end if Y.focus==w then Y.focus=nil;Y.focusWin(topWindow())end maybeUnload(w.app) Y.dockDirty() end function Y.minimize(w) winDamage(w) w.minimized=true if Y.focus==w then Y.focus=nil;Y.focusWin(topWindow())end Y.dockDirty() end function Y.maximize(w,how) winDamage(w) local ax,ay,aw,ah=workArea() if how=="restore"or(not how and w.maxState)then if w.restore then w.x,w.y,w.w,w.h=table.unpack(w.restore)end w.maxState,w.restore=nil,nil else if not w.maxState then w.restore={w.x,w.y,w.w,w.h}end how=how or"max" if how=="max"then w.x,w.y,w.w,w.h=ax,ay,aw,ah elseif how=="left"then w.x,w.y,w.w,w.h=ax,ay,aw//2,ah else w.x,w.y,w.w,w.h=ax+aw//2,ay,aw-aw//2,ah end w.maxState=how end if w.resized then pcall(w.resized,w)end winDamage(w) end function Y.switchWs(n) if n<1 or n>Y.wsCount or n==Y.ws then return end for _,w in ipairs(Y.windows)do if w.sticky then w.ws=n end end Y.ws=n Y.focus=nil Y.focusWin(topWindow()) gfx.damageAll() end function Y.windowsOf(id) local r={} for _,w in ipairs(Y.windows)do if w.app==id then r[#r+1]=w end end return r end function Y.notify(title,body,opts) opts=opts or{} local n={title=tostring(title),body=body and tostring(body)or"",time=now(),icon=opts.icon,action=opts.action,actionLabel=opts.actionLabel} table.insert(Y.notifs,1,n) Y.notifs[21]=nil Y.unread=true if not Y.cfg.dnd or opts.urgent then n.expires=computer.uptime()+(opts.timeout or 5) Y.toasts={n} if Y.cfg.beeps then computer.beep(1000,0.05)end end gfx.damage(1,1,W,1) gfx.damage(W//2-30,2,62,7) end local image=require("yaru.image") local function drawIcon(x,y,name) local img=image.load(ICONDIR..(Y.icons[name]or name)..".pic")or image.load(ICONDIR.."app.pic") if img then gfx.image(x,y,img)end end Y.drawIcon=drawIcon function Y.wallpaper()gfx.drawWallpaper()end local function label(x,y,s,w) s=ui.trunc(s,w) local lx=x+(w-unicode.len(s))//2 gfx.blend(lx-1,y,unicode.len(s)+2,1,0,0.4) gfx.text(lx,y,s,0xFFFFFF) end Y.label=label local deskItems={} local function drawWallpaper() gfx.drawWallpaper() deskItems={} if Y.cfg.desktopIcons and not(Y.overlay and Y.overlay.opaque)then local x,y=DOCK+3,3 local function item(icon,name,kind,arg) drawIcon(x+1,y,icon) label(x-1,y+4,name,12) deskItems[#deskItems+1]={x-1,y,12,5,kind,arg} y=y+6 end item("home","Home","home") for _,d in ipairs(Y.drives or{})do item(d.floppy and"floppy"or"drive",d.label,"drive",d.path)end item("trash","Trash","trash") end end Y.deskItems=function()return deskItems end local function drawWindow(w) local t=Y.theme local focused=Y.focus==w local x,y=w.x,w.y local hb=focused and t.header or t.headerIn local rounded=not w.maxState if rounded then local a=focused and 0.5 or 0.3 gfx.blend(x+w.w,y,1,w.h+1,0,a) gfx.blend(x+1,y+w.h,w.w-1,1,0,a) gfx.half(x+1,y-1,w.w-2,hb,false) gfx.text(x,y-1,"⣠",hb);gfx.text(x+w.w-1,y-1,"⣄",hb) end gfx.fill(x,y,w.w,1,hb) local title=ui.trunc(w.title or"",w.w-22) if not w.header or w.showTitle then gfx.text(x+(w.w-unicode.len(title))//2,y,title,focused and t.headerFg or t.dim,hb)end w.hhits={} if w.header then gfx.pushClip(x,y,w.w-11,1) gfx.origin(x-1,y-1) local saved=w.hits w.hits=w.hhits pcall(w.header,w,w.w-11) w.hits=saved gfx.popClip() gfx.origin(0,0) end local bb=t.dark and 0x454545 or 0xDEDDDA local bfg=focused and t.headerFg or t.dim local bx=x+w.w-10 if not w.fixed then ui.pill(bx,y,3,bb);gfx.text(bx+1,y,"−",bfg,bb) ui.pill(bx+3,y,3,bb);gfx.text(bx+4,y,w.maxState and"◫"or"□",bfg,bb) end local cb=focused and t.accent or bb ui.pill(bx+6,y,3,cb);gfx.text(bx+7,y,"×",focused and 0xFFFFFF or bfg,cb) w.hits={} if gfx.pushClip(x,y+1,w.w,w.h-1)then gfx.origin(x-1,y) gfx.fill(1,1,w.w,w.h-1,t.bg) local ok,err=pcall(w.draw,w,w.w,w.h-1) if not ok then gfx.text(2,2,"draw error:",t.danger) gfx.text(2,3,ui.trunc(tostring(err),w.w-3),t.fg) end end gfx.popClip() gfx.origin(0,0) if rounded then local yb=y+w.h-1 gfx.text(x,yb,"⠙",gfx.bgAt(x,yb),gfx.bgAt(x-1,yb)) gfx.text(x+w.w-1,yb,"⠋",gfx.bgAt(x+w.w-1,yb),gfx.bgAt(x+w.w,yb)) if not w.fixed then gfx.text(x+w.w-2,yb,"◢",t.dim)end end end local dockItems={} function Y.dockDirty()gfx.damage(1,2,DOCK,H-1)end local function running(id) local n,vis=0,false for _,w in ipairs(Y.windows)do if w.app==id then n=n+1 end end return n end local function drawDock() local t=Y.theme gfx.blend(1,2,DOCK,H-1,0x000000,0.5) dockItems={} local y=3 local seen={} local function item(kind,id,icon,label,count) if y+4>H-5 then return end local focusedApp=Y.focus and Y.focus.app==id if focusedApp then gfx.blend(2,y,DOCK-2,4,0xFFFFFF,0.16) gfx.half(3,y-1,DOCK-4,0xFFFFFF,false,0.16) gfx.half(3,y+4,DOCK-4,0xFFFFFF,true,0.16) end drawIcon(3,y,icon) if count and count>0 then gfx.text(1,y+1,"•",t.accent) if count>1 then gfx.text(1,y+2,"•",t.accent)end end dockItems[#dockItems+1]={kind=kind,id=id,y=y,label=label} y=y+5 end for _,id in ipairs(Y.cfg.favorites)do local a=Y.apps[id] if a then seen[id]=true;item("app",id,a.icon or"generic",a.name,running(id))end end for _,w in ipairs(Y.windows)do if not seen[w.app]and Y.apps[w.app]then seen[w.app]=true local a=Y.apps[w.app] item("app",w.app,a.icon or"generic",a.name,running(w.app)) end end gfx.text(3,y-1,string.rep("─",DOCK-4),0x5E5C64) y=y+1 for _,d in ipairs(Y.drives or{})do item("drive",d.path,d.floppy and"floppy"or"drive",d.label)end item("trash","trash","trash","Trash") local gy=H-4 local ov=Y.overlay and Y.overlay.kind=="overview"and Y.overlay.mode=="apps" if ov then gfx.blend(2,gy,DOCK-2,3,0xFFFFFF,0.16)end for r=0,2 do gfx.text(4,gy+r,"● ● ●",0xFFFFFF)end dockItems[#dockItems+1]={kind="grid",y=gy} end local function clockText() local fmt=(Y.cfg.clockWeekday and"%a "or"").."%b %d %H:%M"..(Y.cfg.clockSeconds and":%S"or"") return Y.date(fmt) end local panelHits={} local function drawPanel() local t=Y.theme gfx.fill(1,1,W,1,t.panel) panelHits={} local x=2 for i=1,Y.wsCount do if i==Y.ws then ui.pill(x,1,4,0xFFFFFF);x=x+5 else gfx.text(x,1,"●",0x77767B,t.panel);x=x+2 end end panelHits[#panelHits+1]={1,x,"activities"} if Y.focus then local a=Y.apps[Y.focus.app] if a then gfx.text(x+2,1,a.name,t.panelFg,t.panel)end end local c=clockText() local cw=unicode.len(c)+(Y.unread and 2 or 0) local cx=(W-cw)//2 if Y.overlay and Y.overlay.kind=="calendar"then ui.pill(cx-2,1,cw+4,0x383838)end gfx.text(cx,1,c,t.panelFg) if Y.unread then gfx.text(cx+cw-1,1,"●",t.accent)end panelHits[#panelHits+1]={cx-2,cx+cw+2,"calendar"} local tray={} if Y.cfg.showMemory then local total,free=computer.totalMemory(),computer.freeMemory() tray[#tray+1]=string.format("▦ %d%%",math.floor((total-free)*100/total+.5)) end if component.isAvailable("internet")then tray[#tray+1]="⇅" elseif component.isAvailable("modem")then tray[#tray+1]="⌁"end tray[#tray+1]=Y.cfg.beeps and"♪"or"∅" local maxE=computer.maxEnergy() if maxE and maxE>0 then tray[#tray+1]=string.format("ϟ %d%%",math.floor(computer.energy()*100/maxE))end tray[#tray+1]="⌽" local s=table.concat(tray," ") local sw=unicode.len(s) local tx=W-sw-2 if Y.overlay and Y.overlay.kind=="quick"then ui.pill(tx-2,1,sw+4,0x383838)end gfx.text(tx,1,s,t.panelFg) panelHits[#panelHits+1]={tx-2,W,"quick"} end local function drawToasts() local t=Y.theme local n=Y.toasts[1] if not n or(Y.overlay and(Y.overlay.kind=="lock"or Y.overlay.opaque))then return end local w=56 local x=(W-w)//2 ui.card(x,3,w,3,t.menu) gfx.text(x+2,3,"●",t.accent,t.menu) gfx.text(x+4,3,ui.trunc(n.title,w-14),t.menuFg,t.menu) gfx.text(x+w-8,3,"now",t.dim,t.menu) gfx.text(x+w-3,3,"×",t.dim,t.menu) gfx.text(x+4,4,ui.trunc(n.body:gsub("\n"," "),w-6),t.dim,t.menu) if n.action then local l=n.actionLabel or"Open" ui.button(nil,x+w-unicode.len(l)-6,5,l,nil,nil) end Y.toastRect={x,2,w,5} end function Y.render() if Y.paused then return end if not gfx.isDamaged()then return end local t0=os.clock() gfx.resetClip() local lock=Y.overlay and Y.overlay.kind=="lock" if not lock then drawWallpaper() if not(Y.overlay and Y.overlay.opaque)then for _,w in ipairs(Y.windows)do if w.ws==Y.ws and not w.minimized and gfx.damaged(w.x,w.y-1,w.w+1,w.h+3)then gfx.origin(0,0) drawWindow(w) end end end gfx.origin(0,0) if gfx.damaged(1,2,DOCK,H-1)then drawDock()end if gfx.damaged(1,1,W,1)then drawPanel()end drawToasts() end if Y.overlay then gfx.origin(0,0) Y.overlay.hits={} local ok,err=pcall(Y.overlay.draw,Y.overlay) if not ok then Y.overlay=nil;Y.notify("Shell error",tostring(err))end end local calls=gfx.flush() Y.stats.lastFrameMs=(os.clock()-t0)*1000 Y.stats.lastRuns=calls end local lazyCache={} function Y.openOverlay(o) if Y.overlay and Y.overlay.close then pcall(Y.overlay.close,Y.overlay)end Y.overlay=o gfx.damageAll() end function Y.closeOverlay() if Y.overlay and Y.overlay.close then pcall(Y.overlay.close,Y.overlay)end Y.overlay=nil for k in pairs(lazyCache)do lazyCache[k]=nil end gfx.damageAll() end function Y.menu(x,y,items) local t=Y.theme local mw=10 for _,it in ipairs(items)do if type(it)=="table"then mw=math.max(mw,unicode.len(it[1])+(it.key and unicode.len(it.key)+3 or 0)+7)end end local mh=#items if x+mw>W then x=W-mw end if y+mh>H-1 then y=math.max(3,H-mh-1)end if y<3 then y=3 end local o={kind="menu",sel=0} local function activate(i) local it=items[i] if type(it)~="table"or it.disabled then return end Y.closeOverlay() if it[2]then local ok,e=pcall(it[2])if not ok then Y.notify("Error",e)end end end function o.draw() ui.card(x,y,mw,mh,t.menu) for i,it in ipairs(items)do local yy=y+i-1 if it=="-"then gfx.text(x+2,yy,string.rep("─",mw-4),t.sep,t.menu) else local bg=t.menu if i==o.sel then bg=t.menuHi;ui.select(x+1,yy,mw-2,bg)end local fg=it.disabled and t.dim or t.menuFg if it.check~=nil then gfx.text(x+2,yy,it.check and"✓"or" ",t.accent,bg)end gfx.text(x+4,yy,it[1],fg,bg) if it.key then gfx.text(x+mw-unicode.len(it.key)-3,yy,it.key,t.dim,bg)end o.hits[#o.hits+1]={x,yy,mw,1,function()activate(i)end} end end end function o.touch(_,px,py) Y.closeOverlay() return true end function o.key(_,char,code) if code==K.esc then Y.closeOverlay() elseif code==K.down or code==K.up then local d=code==K.down and 1 or-1 for _=1,#items do o.sel=(o.sel+d-1)%#items+1 if items[o.sel]~="-"and not items[o.sel].disabled then break end end gfx.damage(x,y-1,mw+1,mh+3) elseif code==K.enter then activate(o.sel)end end Y.openOverlay(o) return o end function Y.dialog(d) local t=Y.theme local lines={} for l in((d.text or"").."\n"):gmatch("(.-)\n")do lines[#lines+1]=l end local dw=math.max(40,unicode.len(d.title or"")+6) for _,l in ipairs(lines)do dw=math.max(dw,unicode.len(l)+6)end dw=math.min(dw,W-10) local dh=5+#lines+(d.entry and 2 or 0) local x,y=(W-dw)//2,(H-dh)//2 local o={kind="dialog",field=d.entry and{text=d.entry,cur=unicode.len(d.entry)}} local function press(b) Y.closeOverlay() if b and b[2]then local ok,e=pcall(b[2],o.field and o.field.text)if not ok then Y.notify("Error",e)end end end function o.draw() gfx.blend(1,2,W,H-1,0,0.5) ui.card(x,y,dw,dh,t.menu) local tt=d.title or"" gfx.text(x+(dw-unicode.len(tt))//2,y+1,tt,t.menuFg,t.menu) for i,l in ipairs(lines)do gfx.text(x+(dw-unicode.len(l))//2,y+2+i,l,t.dim,t.menu)end if o.field then ui.entry(nil,o.field,x+3,y+3+#lines,dw-6,true,nil,d.mask)end local bw=(dw-4)//#d.buttons for i,b in ipairs(d.buttons)do local bx=x+2+(i-1)*bw ui.button(nil,bx,y+dh-2,b[1],nil,b[3]or nil,bw-1) o.hits[#o.hits+1]={bx,y+dh-2,bw-1,1,function()press(b)end} end end function o.touch()return true end function o.key(_,char,code) if code==K.esc then press(d.cancel and{"",d.cancel}) elseif code==K.enter then for _,b in ipairs(d.buttons)do if b[3]=="suggested"or b[3]=="destructive"then return press(b)end end press(d.buttons[#d.buttons]) elseif o.field then ui.entryKey(o.field,char,code);gfx.damage(x,y-1,dw+1,dh+3)end end function o.paste(_,s)if o.field then ui.entryPaste(o.field,s);gfx.damage(x,y-1,dw+1,dh+3)end end Y.openOverlay(o) end function Y.lazy(name) local m=lazyCache[name] if not m then m=assert(loadfile("/usr/share/yaru/shell/"..name..".lua"))() lazyCache[name]=m end return m end function Y.runFullscreen(cmd) Y.paused=true local term=require("term") gfx.defaultPalette() local wasPrecise=Y.precise Y.setPrecise(false) gpu.setBackground(0);gpu.setForeground(0xFFFFFF) gpu.fill(1,1,W,H," ") term.setCursor(1,1) local ok,err=xpcall(require("shell").execute,debug.traceback,cmd) if not ok or err==false then io.write("\n[process exited] ",tostring(err or""),"\n")end io.write("\nPress any key to return to the desktop…") event.pull("key_down") Y.paused=false gpu.setResolution(W,H) gfx.setCustomPalette(Y.theme.palette,true) Y.setPrecise(wasPrecise) gfx.damageAll() end function Y.scanDrives() local boot=computer.getBootAddress() local tmp=computer.tmpAddress() Y.drives={} for proxy,path in fs.mounts()do if path:match("^/mnt/[^/]+$")and proxy.address~=boot and proxy.address~=tmp then local label=proxy.getLabel and proxy.getLabel()or nil Y.drives[#Y.drives+1]={path=path,label=label or path:sub(6),address=proxy.address} end end table.sort(Y.drives,function(a,b)return a.path=it.y and y<=it.y+3 then if it.kind=="grid"then Y.lazy("overview").open("apps") elseif it.kind=="app"then local wins=Y.windowsOf(it.id) if btn==1 then local fav=false for _,f in ipairs(Y.cfg.favorites)do if f==it.id then fav=true end end local items={{"New Window",function()Y.launch(it.id)end}} if#wins>0 then items[#items+1]={"Quit",function()for _,w in ipairs(wins)do Y.closeWin(w)end end}end items[#items+1]="-" items[#items+1]={fav and"Unpin"or"Pin to Dash",function() if fav then for i,f in ipairs(Y.cfg.favorites)do if f==it.id then table.remove(Y.cfg.favorites,i)break end end else table.insert(Y.cfg.favorites,it.id)end Y.saveConfig();Y.dockDirty() end} Y.menu(DOCK+1,it.y,items) elseif#wins==0 then Y.launch(it.id) else local top for _,w in ipairs(Y.windows)do if w.app==it.id then top=w end end if Y.focus and Y.focus.app==it.id and Y.cfg.dockClickMinimize then for _,w in ipairs(wins)do if not w.minimized then Y.minimize(w)end end else for _,w in ipairs(wins)do if w.minimized and w~=top then w.minimized=false;winDamage(w)end end Y.focusWin(top) end end elseif it.kind=="drive"then if btn==1 then Y.menu(DOCK+1,it.y,{{"Open",function()Y.launch("files",it.id)end}, {"Eject",function()fs.umount(it.id);Y.scanDrives();gfx.damageAll();Y.notify("Drive ejected",it.label.." can be removed safely")end}}) else Y.launch("files",it.id)end elseif it.kind=="trash"then Y.launch("files","trash://") end return end end end local function hitList(hits,x,y,btn,...) for i=#hits,1,-1 do local h=hits[i] if x>=h[1]and x=h[2]and y=w.x and x=w.y and y=r[1]and x=r[2]and y=r[1]+r[3]-3)then pcall(n.action)end return end end if y==1 then for _,h in ipairs(panelHits)do if x>=h[1]and x<=h[2]then if h[3]=="activities"then Y.lazy("overview").open("windows") elseif h[3]=="calendar"then Y.lazy("calendar").open() elseif h[3]=="quick"then Y.lazy("quick").open()end return end end return end if x<=DOCK then return dockTouch(x,y,btn)end local w=winAt(x,y) if not w then for _,it in ipairs(deskItems)do if x>=it[1]and x=it[2]and y=6 and rx<=8 then return Y.closeWin(w)end if not w.fixed and rx>=3 and rx<=5 then return Y.maximize(w)end if not w.fixed and rx>=0 and rx<=2 then return Y.minimize(w)end if hitList(w.hhits,lx,1,btn,w,lx,1,btn)then winDamage(w)return end local tnow=computer.uptime() if btn==0 and tnow-lastClick.t<0.4 and lastClick.y==y then lastClick.t=0;return Y.maximize(w)end lastClick.t,lastClick.y=tnow,y if btn==1 then return Y.menu(x,y+1,{ {"Minimize",function()Y.minimize(w)end}, {w.maxState and"Unmaximize"or"Maximize",function()Y.maximize(w)end}, "-", {"Always on Visible Workspace",function()w.sticky=not w.sticky end,check=w.sticky}, {"Move to Workspace Right",function()if w.ws1 then w.ws=w.ws-1;Y.focusWin(topWindow());gfx.damageAll()end end}, "-", {"Close",function()Y.closeWin(w)end,key="Alt+F4"}, }) end if btn==0 then drag={win=w,mode="move",dx=x-w.x,dy=y-w.y}end return end if not w.maxState and not w.fixed and x>=w.x+w.w-2 and y==w.y+w.h-1 then drag={win=w,mode="resize"} return end local tnow=computer.uptime() local dbl=tnow-lastClick.t<0.4 and lastClick.x==x and lastClick.y==y lastClick.t,lastClick.x,lastClick.y=dbl and 0 or tnow,x,y local hit,r=hitList(w.hits,lx,ly,btn,w,lx,ly,btn,dbl) if hit then winDamage(w)if r=="focus"then w.focusField=true end return end if w.touch then pcall(w.touch,w,lx,ly,btn,"touch",dbl);winDamage(w)end end local function onDrag(x,y,btn) if drag then local w=drag.win winDamage(w) if drag.mode=="move"then if w.maxState then local rw=w.restore and w.restore[3]or w.w Y.maximize(w,"restore") drag.dx=math.min(drag.dx,rw-10) end local ox,oy=w.x,w.y w.x,w.y=x-drag.dx,math.max(3,y-drag.dy) if not Y.overlay and Y.windows[#Y.windows]==w and not w.maxState then gfx.moveHint(ox,oy-1,w.w+1,w.h+3,w.x-ox,w.y-oy) end else w.w,w.h=math.max(w.minW or 20,x-w.x+1),math.max(w.minH or 6,y-w.y+1) if w.resized then pcall(w.resized,w)end end winDamage(w) return end local w=Y.focus if w and w.touch and w.ws==Y.ws and not w.minimized then pcall(w.touch,w,x-w.x+1,y-w.y,btn,"drag");winDamage(w) end end local function onDrop(x,y,btn) if drag then local w=drag.win if drag.mode=="move"then if y<=2 and w.y<=2 then Y.maximize(w,"max") elseif x<=DOCK+1 then Y.maximize(w,"left") elseif x>=W then Y.maximize(w,"right")end end clampWin(w) gfx.damageAll() drag=nil return end local w=Y.focus if w and w.touch then pcall(w.touch,w,x-w.x+1,y-w.y,btn,"drop");winDamage(w)end end local function onScroll(x,y,dir) local o=Y.overlay if o then if o.scroll then o.scroll(o,x,y,dir)end return end if y==1 and x<12 then return Y.switchWs(Y.ws-dir)end local w=winAt(x,y) if w and w.scroll then pcall(w.scroll,w,x-w.x+1,y-w.y,dir);winDamage(w)end end function Y.screenshot() local rows=gfx.dump() local dir="/home/Pictures" fs.makeDirectory(dir) local name=fs.concat(dir,"Screenshot "..Y.date("%Y-%m-%d %H-%M-%S")..".txt") local f=io.open(name,"w") if f then f:write(table.concat(rows,"\n"));f:close()end Y.notify("Screenshot captured","Saved to "..name,{action=function()Y.launch("editor",name)end,actionLabel="Open"}) end local function cycleWs(d,move) local n=Y.ws+d if n<1 or n>Y.wsCount then return end if move and Y.focus then Y.focus.ws=n end Y.switchWs(n) end local function globalKey(char,code) local c,a,s,sup=ctrl(),alt(),shift(),super() if sup and code~=K.lsuper and code~=K.rsuper then superTap=false end if code==K.sysrq then Y.screenshot()return true end if c and a and code==K.t then Y.launch("terminal")return true end if c and a and code==K.del then Y.lazy("quick").powerDialog()return true end if(c and a and(code==K.left or code==K.right))or(sup and(code==K.pgup or code==K.pgdn))then cycleWs((code==K.left or code==K.pgup)and-1 or 1,s)return true end if c and a and code==K.d or sup and code==K.d then local any=false for _,w in ipairs(Y.windows)do if w.ws==Y.ws and not w.minimized then any=true end end for _,w in ipairs(Y.windows)do if w.ws==Y.ws then w.minimized=any end end Y.focus=nil if not any then Y.focusWin(topWindow())end gfx.damageAll() return true end if a and code==K.f4 then if Y.focus then Y.closeWin(Y.focus)end return true end if a and code==K.f10 then if Y.focus then Y.maximize(Y.focus)end return true end if a and code==K.f1 then Y.lazy("overview").open("windows")return true end if a and code==K.f2 then Y.lazy("overview").open("run")return true end if(a or sup)and code==K.tab then Y.lazy("switcher").open(s and-1 or 1)return true end if c and code==K.tab and not a then Y.lazy("switcher").open(s and-1 or 1)return true end if sup then if code==K.up and Y.focus then Y.maximize(Y.focus,"max")return true end if code==K.down and Y.focus then if Y.focus.maxState then Y.maximize(Y.focus,"restore")else Y.minimize(Y.focus)end return true end if code==K.left and Y.focus then Y.maximize(Y.focus,"left")return true end if code==K.right and Y.focus then Y.maximize(Y.focus,"right")return true end if code==K.h and Y.focus then Y.minimize(Y.focus)return true end if code==K.a then Y.lazy("overview").open("apps")return true end if code==K.e then Y.launch("files","/home")return true end if code==K.l then Y.lazy("lock").open(Y.cfg.password~=nil)return true end if code==K.v then Y.lazy("calendar").open()return true end if code==K.s then Y.lazy("quick").open()return true end if code==K.t then Y.launch("terminal")return true end end end local function onKey(char,code,down) mods[code]=down or nil if not down then if(code==K.lsuper or code==K.rsuper)and superTap and not(Y.overlay and Y.overlay.kind=="lock")then superTap=false if Y.overlay and Y.overlay.kind=="overview"then Y.closeOverlay()else Y.lazy("overview").open("windows")end end if Y.overlay and Y.overlay.keyUp then Y.overlay.keyUp(Y.overlay,char,code)end if Y.focus and Y.focus.keyUp and not Y.overlay then pcall(Y.focus.keyUp,Y.focus,char,code)end return end Y.lastInput=computer.uptime() if code==K.lsuper or code==K.rsuper then superTap=true return end local o=Y.overlay if o and o.kind=="lock"then return o.key(o,char,code)end if globalKey(char,code)then return end if o then if o.key then o.key(o,char,code)end return end local w=Y.focus if w and w.key and not w.minimized then local ok,err=pcall(w.key,w,char,code) if not ok then Y.notify("Application error",err)end winDamage(w) end end local INPUT={key_down=1,key_up=1,touch=1,drag=1,drop=1,scroll=1,clipboard=1} local function handle(name,a1,a2,a3,a4,a5,...) if INPUT[name]and a1~=kbAddr and a1~=screenAddr then return end if not INPUT[name]then for _,w in ipairs(Y.windows)do if w.onSignal then local ok=pcall(w.onSignal,name,a1,a2,a3,a4,a5,...)if ok then winDamage(w)end end end end if name=="touch"or name=="drag"or name=="drop"or name=="scroll"then if Y.precise and(math.type(a2)=="float"or math.type(a3)=="float")then Y.touchFrac=a3-math.floor(a3) a2,a3=math.floor(a2)+1,math.floor(a3)+1 else Y.touchFrac=nil end if name=="drag"and not(Y.focus and Y.focus.precise)then local k=math.floor(a2)*1000+math.floor(a3) if k==Y.lastDragCell then return end Y.lastDragCell=k elseif name=="touch"or name=="drop"then Y.lastDragCell=nil end end if name=="touch"then Y.lastInput=computer.uptime()onTouch(math.floor(a2),math.floor(a3),a4) elseif name=="drag"then onDrag(math.floor(a2),math.floor(a3),a4) elseif name=="drop"then onDrop(math.floor(a2),math.floor(a3),a4) elseif name=="scroll"then onScroll(math.floor(a2),math.floor(a3),a4) elseif name=="key_down"then onKey(a2,a3,true) elseif name=="key_up"then onKey(a2,a3,false) elseif name=="clipboard"then local o=Y.overlay if o and o.paste then o.paste(o,a2) elseif Y.focus and Y.focus.paste then pcall(Y.focus.paste,Y.focus,a2);winDamage(Y.focus)end elseif name=="component_added"or name=="component_removed"then if a2=="filesystem"then local before=#(Y.drives or{}) Y.scanDrives() gfx.damageAll() if name=="component_added"and#Y.drives>before then local d=Y.drives[#Y.drives] for _,dd in ipairs(Y.drives)do if dd.address==a1 then d=dd end end Y.notify("Drive connected",d.label.." mounted at "..d.path,{action=function()Y.launch("files",d.path)end,actionLabel="Open"}) end elseif name=="component_added"and a2~="filesystem"then Y.notify("New device",a2 .." "..a1:sub(1,8)) end elseif name=="screen_resized"then local w,h=gpu.getResolution() if w~=W or h~=H then Y.setResolution(w,h,true)end elseif name=="yaru_notify"then Y.notify(a1,a2) elseif name=="yaru_open"then Y.launch(a1,a2) elseif name=="yaru_clipboard"then Y.clipboard=a1 or"" end end function Y.setResolution(w,h,already) if not already then gpu.setResolution(w,h)end W,H=gpu.getResolution() gfx.init(gpu,W,H) Y.applyTheme() for _,win in ipairs(Y.windows)do if win.maxState then local m=win.maxState;win.maxState=nil;Y.maximize(win,m)end clampWin(win) if win.resized then pcall(win.resized,win)end end end function Y.size()return W,H,DOCK end local function nextTimeout() local up=computer.uptime() local t=Y.cfg.clockSeconds and 1 or(60-now()%60) local n=Y.toasts[1] if n and n.expires then t=math.min(t,math.max(0,n.expires-up))end for _,w in ipairs(Y.windows)do if w.tickInterval and not w.minimized then t=math.min(t,math.max(0,(w.nextTick or 0)-up))end end local o=Y.overlay if o and o.tick then t=math.min(t,math.max(0,(o.nextTick or 0)-up))end return math.max(0.05,t) end local lastMinute local function periodic() local up=computer.uptime() local n=Y.toasts[1] if n and n.expires and up>=n.expires then Y.toasts={} gfx.damage(W//2-30,2,62,7) end local m=Y.cfg.clockSeconds and now()or now()//60 if m~=lastMinute then lastMinute=m;gfx.damage(1,1,W,1) if fs.exists("/etc/yaru-tasks.cfg")then pcall(require("yaru.tasks").tick,Y)end end for _,w in ipairs(Y.windows)do if w.tickInterval and not w.minimized and up>=(w.nextTick or 0)then w.nextTick=up+w.tickInterval pcall(w.tick,w) end end local o=Y.overlay if o and o.tick and up>=(o.nextTick or 0)then o.nextTick=up+(o.tickInterval or 0.5);o.tick(o)end if Y.cfg.blank and Y.cfg.blank>0 and up-(Y.lastInput or up)>Y.cfg.blank*60 and not(Y.overlay and Y.overlay.kind=="lock")then Y.lazy("lock").open(Y.cfg.password~=nil,true) end end function Y.run(opts) opts=opts or{} Y.loadConfig() gpu=component.gpu screenAddr=gpu.getScreen() Y.precise=false kbAddr=component.isAvailable("keyboard")and component.keyboard.address or"" Y.screenAddr,Y.kbAddr,Y.gpu=screenAddr,kbAddr,gpu if Y.cfg.resolution then pcall(gpu.setResolution,Y.cfg.resolution[1],Y.cfg.resolution[2])end W,H=gpu.getResolution() if W<50 or H<16 then error("screen too small for the desktop (need a tier 2+ screen and GPU)")end if gpu.getDepth()<4 then error("the desktop needs a tier 2+ GPU and screen (colour)")end gfx.init(gpu,W,H) Y.applyTheme() Y.registerApps() Y.scanDrives() Y.running=true Y.lastInput=computer.uptime() event.interruptHook=function() local p=require("process").findProcess() local id=p and p.data.yaruTerm return id~=nil and Y.focus~=nil and Y.focus.termId==id end if not opts.nologin and not(Y.cfg.autologin and not Y.cfg.password)then Y.lazy("lock").open(Y.cfg.password~=nil,false,true)end Y.render() if opts.onStarted then pcall(opts.onStarted)end if fs.exists("/etc/yaru-tasks.cfg")then pcall(require("yaru.tasks").tick,Y,true)end while Y.running do local sig=table.pack(event.pull(nextTimeout())) if sig[1]then local ok,err=xpcall(handle,debug.traceback,table.unpack(sig,1,sig.n)) if not ok then pcall(Y.notify,"Shell error",tostring(err):match("[^\n]*"));pcall(event.onError,err)end end periodic() local ok,err=xpcall(Y.render,debug.traceback) if not ok then pcall(event.onError,err);gfx.invalidate()end end for i=#Y.windows,1,-1 do Y.closeWin(Y.windows[i],true)end event.interruptHook=nil Y.overlay=nil Y.setPrecise(false) gfx.defaultPalette() gpu.setBackground(0);gpu.setForeground(0xFFFFFF) gpu.fill(1,1,W,H," ") require("term").setCursor(1,1) return Y.exitAction end function Y.quit(action)Y.exitAction=action;Y.running=false end return Y