local Y=require("yaru.core") local gfx=require("yaru.gfx") local ui=require("yaru.ui") local T=require("yaru.theme") local computer=require("computer") local component=require("component") local fs=require("filesystem") local unicode=require("unicode") local M={} local PAGES={ {"appearance","◐ Appearance"},{"display","▭ Displays"},{"sound","♪ Sound"}, {"notifications","• Notifications"},{"power","ϟ Power"},{"privacy","◉ Privacy & Login"}, {"datetime","◷ Date & Time"},{"keyboard","⌨ Keyboard"},{"about","ℹ About"}, } local function save()Y.saveConfig()end local function row(w,x,y,width,label,sub) local t=Y.theme ui.round(x,y,width,sub and 2 or 1,t.card) gfx.text(x+1,y,ui.trunc(label,width-12),t.fg,t.card) if sub then gfx.text(x+1,y+1,ui.trunc(sub,width-12),t.dim,t.card)end end local function fitResolution() local gpu=Y.gpu local mw,mh=gpu.maxResolution() local ok,aw,ah=pcall(component.invoke,gpu.getScreen(),"getAspectRatio") if not ok or not aw then return mw,mh end local ratio=(aw*2-0.5)/(ah-0.25) local w,h if mw/mh>ratio then h=mh;w=math.floor(mh*ratio+.5)else w=mw;h=math.floor(mw/ratio+.5)end return math.max(50,math.min(mw,w)),math.max(16,math.min(mh,h)) end local pages={} function pages.appearance(w,x,y,pw) local t=Y.theme gfx.text(x,y,"Style",t.fg);y=y+1 for i,v in ipairs({{"Default",false},{"Dark",true}})do local bx=x+(i-1)*21 local sel=Y.cfg.dark==v[2] ui.round(bx,y,19,5,sel and t.accent or t.sep) local pb=v[2]and 0x242424 or 0xFAFAFA ui.round(bx+1,y,17,4,pb) ui.round(bx+4,y+1,11,3,v[2]and 0x383838 or 0xFFFFFF) gfx.fill(bx+4,y+1,11,1,v[2]and 0x303030 or 0xEBEBEB) ui.pill(bx+11,y+1,3,Y.theme.accent) gfx.text(bx+(19-#v[1])//2,y+4,v[1],sel and 0xFFFFFF or t.fg,sel and t.accent or t.sep) ui.hit(w,bx,y,19,5,function()Y.cfg.dark=v[2];save();Y.applyTheme()end) end y=y+7 gfx.text(x,y,"Color",t.fg);y=y+1 for i,a in ipairs(T.accents)do local bx=x+(i-1)*5 local sel=Y.cfg.accent==a[1] ui.pill(bx,y,3,a[2]) if sel then gfx.text(bx+1,y,"✓",0xFFFFFF,a[2])end ui.hit(w,bx,y,3,1,function()Y.cfg.accent=a[1];save();Y.applyTheme()end) end gfx.text(x,y+1,"Accent: "..Y.cfg.accent,t.dim) y=y+3 gfx.text(x,y,"Background",t.fg);y=y+1 local wall=require("yaru.wall") for i,id in ipairs(wall.order)do local d=wall.designs[id] local col,rw=(i-1)%4,(i-1)//4 local bx,by=x+col*17,y+rw*5 local sel=Y.cfg.wallpaper==id if sel then ui.pill(bx-1,by+3,17,t.accent)end local ramp=d.ramp for k=0,14 do local c1=ramp[math.min(#ramp,1+k*#ramp//15)] local c2=ramp[math.min(#ramp,1+(k+3)*#ramp//15)] gfx.fill(bx+k,by,1,1,c1,c2,"▄") gfx.fill(bx+k,by+1,1,1,c2,ramp[math.min(#ramp,1+(k+6)*#ramp//15)],"▄") gfx.fill(bx+k,by+2,1,1,ramp[math.min(#ramp,1+(k+6)*#ramp//15)],ramp[math.min(#ramp,1+(k+9)*#ramp//15)],"▄") end local label=(sel and"✓ "or"")..d.name gfx.text(bx+(15-unicode.len(label))//2,by+3,label,sel and 0xFFFFFF or t.dim,sel and t.accent or nil) ui.hit(w,bx,by,15,4,function()Y.cfg.wallpaper=id;save();Y.applyTheme()end) end y=y+11 local pic=type(Y.cfg.wallpaper)=="string"and Y.cfg.wallpaper:sub(1,1)=="/" ui.button(w,x,y-1,pic and("Picture: "..require("filesystem").name(Y.cfg.wallpaper))or"Picture…",function() Y.dialog{title="Picture Background",text="Path to a .pic image (e.g. /home/Pictures/Road.pic)",entry=pic and Y.cfg.wallpaper or"/home/Pictures/", buttons={{"Cancel"},{"Set",function(p)if p and p~=""then Y.cfg.wallpaper=p;save();Y.applyTheme()end end,"suggested"}}} end,pic and"suggested"or nil) y=y+2 row(w,x,y,pw,"Desktop icons","Show Home and drives on the desktop") ui.switch(w,x+pw-6,y,Y.cfg.desktopIcons,function()Y.cfg.desktopIcons=not Y.cfg.desktopIcons;save();Y.redraw()end) y=y+3 row(w,x,y,pw,"Dock: click to minimize","Clicking a focused app's icon minimizes it") ui.switch(w,x+pw-6,y,Y.cfg.dockClickMinimize,function()Y.cfg.dockClickMinimize=not Y.cfg.dockClickMinimize;save()end) end function pages.display(w,x,y,pw) local t=Y.theme local gpu=Y.gpu local cw,ch=gpu.getResolution() local mw,mh=gpu.maxResolution() local fw,fh=fitResolution() gfx.text(x,y,"Built-in Display",t.fg) gfx.text(x,y+1,string.format("Current %dx%d • maximum %dx%d • colour depth %d bit",cw,ch,mw,mh,gpu.getDepth()),t.dim) y=y+3 gfx.text(x,y,"Resolution",t.fg);y=y+1 local opts={{fw,fh,"Fit to screen (no borders)"},{mw,mh,"Maximum"}} for _,r in ipairs({{120,40},{100,32},{80,25}})do if r[1]<=mw and r[2]<=mh then opts[#opts+1]={r[1],r[2],""}end end for _,o in ipairs(opts)do local on=o[1]==cw and o[2]==ch ui.radio(w,x,y,on,string.format("%dx%d %s",o[1],o[2],o[3]),function() if o[1]<50 or o[2]<16 then return Y.notify("Displays","The desktop needs at least 50x16")end Y.cfg.resolution={o[1],o[2]};save() Y.setResolution(o[1],o[2]) end) y=y+1 end y=y+1 ui.button(w,x,y,"Reset to Default",function()Y.cfg.resolution=nil;save();Y.setResolution(mw,mh)end) y=y+2 gfx.text(x,y,"Lower resolutions redraw faster: fewer cells per frame.",t.dim) gfx.text(x,y+1,"Tier 3 GPU + tier 3 screen recommended (160x50, 256 colours).",t.dim) y=y+3 local frame=Y.stats.lastFrameMs gfx.text(x,y,string.format("Last frame: %.1f ms Lua, %d GPU runs",frame or 0,Y.stats.lastRuns or 0),t.dim) end function pages.sound(w,x,y,pw) local t=Y.theme row(w,x,y,pw,"System sounds","Beeps for notifications and errors (computer speaker)") ui.switch(w,x+pw-6,y,Y.cfg.beeps,function()Y.cfg.beeps=not Y.cfg.beeps;save()end) y=y+3 ui.button(w,x,y,"Test Sound",function()computer.beep(880,0.1);computer.beep(1320,0.1)end) y=y+2 local nb=component.isAvailable("note_block")or component.isAvailable("iron_noteblock") local sound=component.isAvailable("sound") gfx.text(x,y,"Output devices",t.fg);y=y+1 gfx.text(x,y,"• Computer speaker (beep)",t.dim);y=y+1 if nb then gfx.text(x,y,"• Note block / iron note block",t.dim);y=y+1 end if sound then gfx.text(x,y,"• Sound card",t.dim);y=y+1 end end function pages.notifications(w,x,y,pw) local t=Y.theme row(w,x,y,pw,"Do Not Disturb","Only urgent notifications show banners") ui.switch(w,x+pw-6,y,Y.cfg.dnd,function()Y.cfg.dnd=not Y.cfg.dnd;save();Y.redraw()end) y=y+3 ui.button(w,x,y,"Send Test Notification",function()Y.notify("Test notification","Hello from Settings")end) ui.button(w,x+26,y,"Clear History ("..#Y.notifs..")",function()Y.notifs={};Y.unread=false;Y.redraw()end) y=y+2 gfx.text(x,y,"From a shell or program: notify-send \"Title\" \"Body\"",t.dim) gfx.text(x,y+1,"or computer.pushSignal(\"yaru_notify\", title, body)",t.dim) end function pages.power(w,x,y,pw) local t=Y.theme local e,me=computer.energy(),computer.maxEnergy() gfx.text(x,y,"Energy",t.fg) gfx.text(x,y+1,string.format("%.0f / %.0f (%d%%)",e,me,me>0 and math.floor(e*100/me)or 0),t.dim) gfx.fill(x,y+2,pw,1,t.sep) if me>0 then gfx.fill(x,y+2,math.floor(pw*e/me),1,t.success)end y=y+4 gfx.text(x,y,"Screen Blank (locks the session)",t.fg);y=y+1 for i,m in ipairs({0,1,5,10,15})do ui.radio(w,x+(i-1)*12,y,(Y.cfg.blank or 0)==m,m==0 and"Never"or(m.." min"),function()Y.cfg.blank=m;save()end) end y=y+2 gfx.text(x,y,"Power saving (always on):",t.fg);y=y+1 for _,s in ipairs({"• the desktop sleeps in event.pull until input, a timer, or the next clock minute", "• only changed screen cells are sent to the GPU; unfocused terminals stop blinking", "• app code is unloaded when its last window closes; shell popups are loaded on demand"})do gfx.text(x,y,ui.trunc(s,pw),t.dim);y=y+1 end y=y+1 ui.button(w,x,y,"Restart…",function()Y.lazy("quick").powerDialog()end) end function pages.privacy(w,x,y,pw) local t=Y.theme row(w,x,y,pw,"Start desktop at boot","Hold any key during boot for the text shell") ui.switch(w,x+pw-6,y,Y.cfg.autostart,function()Y.cfg.autostart=not Y.cfg.autostart;save()end) y=y+3 row(w,x,y,pw,"Automatic login","Skip the login screen when no password is set") ui.switch(w,x+pw-6,y,Y.cfg.autologin,function()Y.cfg.autologin=not Y.cfg.autologin;save()end) y=y+3 row(w,x,y,pw,"Password",Y.cfg.password and"Set (SHA-256, salted)"or"Not set: anyone can unlock") y=y+3 ui.button(w,x,y,Y.cfg.password and"Change Password…"or"Set Password…",function() Y.dialog{title="New Password",entry="",mask=true,buttons={{"Cancel"},{"Save",function(pw1) if not pw1 or pw1==""then return end Y.dialog{title="Confirm Password",entry="",mask=true,buttons={{"Cancel"},{"Save",function(pw2) if pw1~=pw2 then return Y.notify("Password","Passwords do not match")end Y.cfg.password=require("yaru.sha256").make(pw1);save() Y.notify("Password","Password set") end,"suggested"}}} end,"suggested"}}} end,"suggested") if Y.cfg.password then ui.button(w,x+22,y,"Remove Password",function()Y.cfg.password=nil;save()end,"destructive") end y=y+2 ui.button(w,x,y,"Lock Screen (Super+L)",function()Y.lazy("lock").open(Y.cfg.password~=nil)end) y=y+2 gfx.text(x,y,"Note: anyone with physical access can boot another disk; the password protects",t.dim) gfx.text(x,y+1,"an unattended session, not the data on the drive.",t.dim) end function pages.datetime(w,x,y,pw) local t=Y.theme gfx.text(x,y,Y.date("%A, %B %d %Y %H:%M:%S"),t.fg) y=y+2 row(w,x,y,pw,"Real-world time","Use the host's clock (via file timestamps) instead of Minecraft time") ui.switch(w,x+pw-6,y,Y.cfg.clockReal,function()Y.cfg.clockReal=not Y.cfg.clockReal;save();Y.redraw()end) y=y+3 row(w,x,y,pw,"Time zone",string.format("UTC%+g",Y.cfg.tz or 0)) ui.button(w,x+pw-12,y,"−",function()Y.cfg.tz=math.max(-12,(Y.cfg.tz or 0)-0.5);save();Y.redraw()end,nil,4) ui.button(w,x+pw-7,y,"+",function()Y.cfg.tz=math.min(14,(Y.cfg.tz or 0)+0.5);save();Y.redraw()end,nil,4) y=y+3 row(w,x,y,pw,"Weekday in top bar") ui.switch(w,x+pw-6,y,Y.cfg.clockWeekday,function()Y.cfg.clockWeekday=not Y.cfg.clockWeekday;save();Y.redraw()end) y=y+2 row(w,x,y,pw,"Seconds in top bar","Wakes the desktop every second") ui.switch(w,x+pw-6,y,Y.cfg.clockSeconds,function()Y.cfg.clockSeconds=not Y.cfg.clockSeconds;save();Y.redraw()end) end local SHORTCUTS={ {"Super","Activities overview"},{"Super+A","Show applications"},{"Alt+F2","Run a command"}, {"Alt+Tab / Super+Tab / Ctrl+Tab","Switch windows"},{"Alt+F4","Close window"},{"Alt+F10 / Super+↑","Maximize"}, {"Super+← / →","Tile left / right"},{"Super+↓ / Super+H","Restore / minimize"},{"Super+D / Ctrl+Alt+D","Show desktop"}, {"Ctrl+Alt+← / →, Super+PgUp/PgDn","Switch workspace (+Shift: move window)"},{"Ctrl+Alt+T / Super+T","Terminal"}, {"Super+E","Home folder"},{"Super+V","Notifications & calendar"},{"Super+S","Quick settings"}, {"Super+L","Lock screen"},{"Ctrl+Alt+Del","Log out / power off"},{"Print Screen","Screenshot (text) to ~/Pictures"}, {"Ctrl+Shift+V / Ctrl+Shift+T","Terminal: paste / new window"}, } function pages.keyboard(w,x,y,pw,ph) local t=Y.theme gfx.text(x,y,"Keyboard Shortcuts",t.fg) gfx.text(x,y+1,"Tip: the host OS may take Alt+Tab or Super; use Ctrl+Tab / Ctrl+Alt instead.",t.dim) y=y+3 for i,s in ipairs(SHORTCUTS)do if y>=ph then break end gfx.fill(x,y,pw,1,i%2==0 and t.bg or t.card) gfx.text(x+1,y,ui.trunc(s[2],pw//2-2),t.fg) gfx.text(x+pw//2,y,ui.trunc(s[1],pw-pw//2-1),t.dim) y=y+1 end end function pages.about(w,x,y,pw) local t=Y.theme local logo={" ▄▄▄ "," █ ○ █ "," ▀▀▀ "} for i,l in ipairs(logo)do gfx.text(x+(pw-7)//2,y+i-1,l,t.accent)end y=y+4 local host=os.getenv("HOSTNAME")or"openos" local lines={} local function add(k,v)lines[#lines+1]={k,v}end add("Device Name",host) add("OS",(_OSVERSION or"OpenOS").." + Yaru desktop") add("Architecture",computer.getArchitecture and computer.getArchitecture()or"Lua") local tot,free=computer.totalMemory(),computer.freeMemory() add("Memory",string.format("%s total, %s free",ui.size(tot),ui.size(free))) local gpu=Y.gpu local mw,mh=gpu.maxResolution() add("Graphics",string.format("GPU tier %d (%dx%d, %d-bit)",mw>=160 and 3 or mw>=80 and 2 or 1,mw,mh,gpu.getDepth())) local root=fs.get("/") if root then add("Disk",string.format("%s of %s used%s",ui.size(root.spaceUsed()),ui.size(root.spaceTotal()),root.isReadOnly()and" (read-only)"or""))end local n=0 for _ in component.list()do n=n+1 end add("Components",tostring(n)) add("Uptime",string.format("%d min",computer.uptime()//60)) add("Address",computer.address():sub(1,13).."…") for _,l in ipairs(lines)do gfx.fill(x,y,pw,1,t.card) gfx.text(x+1,y,l[1],t.fg,t.card) gfx.text(x+pw-1-unicode.len(l[2]),y,ui.trunc(l[2],pw-16),t.dim,t.card) y=y+1 end y=y+1 ui.button(w,x,y,"Rename Device…",function() Y.dialog{title="Rename Device",entry=host,buttons={{"Cancel"},{"Rename",function(txt) if not txt or txt==""or txt:find("[%s/]")then return end local f=io.open("/etc/hostname","w")if f then f:write(txt);f:close()end os.setenv("HOSTNAME",txt) Y.notify("Device renamed","New terminals will show "..txt) end,"suggested"}}} end) ui.button(w,x+18,y,"System Monitor",function()Y.launch("monitor")end) end function M.open(page) local existing=Y.windowsOf("settings")[1] local st if existing then st=existing.state if page then st.page=page end Y.focusWin(existing) return existing end st={page=page or"appearance"} local win=Y.newWindow{app="settings",title="Settings",w=100,h=36,minW=70,minH=18,state=st} local SIDE=22 function win.draw(w,cw,ch) local t=Y.theme gfx.fill(1,1,SIDE,ch,t.sidebar) for i,p in ipairs(PAGES)do local sel=st.page==p[1] local yy=i*2 if sel then ui.select(2,yy,SIDE-2,t.dark and 0x454545 or 0xDEDDDA)end gfx.text(4,yy,p[2],t.fg) ui.hit(w,1,yy,SIDE,1,function()st.page=p[1]end) end local x=SIDE+3 local pw=math.min(cw-SIDE-4,72) for _,p in ipairs(PAGES)do if p[1]==st.page then w.title=p[2]:gsub("^%S+ ","")end end w.tickInterval=(st.page=="datetime"or st.page=="power")and 1 or nil local fn=pages[st.page]or pages.appearance fn(w,x,2,pw,ch) end function win.key(w,char,code) local K=Y.K local idx=1 for i,p in ipairs(PAGES)do if p[1]==st.page then idx=i end end if code==K.down then idx=math.min(#PAGES,idx+1)elseif code==K.up then idx=math.max(1,idx-1)end st.page=PAGES[idx][1] end function win.tick(w)if st.page=="datetime"or st.page=="power"then Y.redraw(w)end end return win end return M