local Y=require("yaru.core") local gfx=require("yaru.gfx") local ui=require("yaru.ui") local fs=require("filesystem") local component=require("component") local unicode=require("unicode") local W=require("ocweb") local M={} local function colors(t) return{text=t.fg,heading=t.dark and 0xC061CB or 0x813D9C,link=t.dark and 0x62A0EA or 0x1A5FB4, dim=t.dim,accent=t.accent,code=t.dark and 0x8FF0A4 or 0x26A269,strong=t.dark and 0xFFFFFF or 0x000000, em=t.dark and 0xF9F06B or 0x986A44} end function M.open(url) local inet=component.isAvailable("internet")and component.internet or nil local st={addr={text="",cur=0},editing=false,findBox=nil} local win=Y.newWindow{app="web",title="Web Browser",w=110,h=36,minW=50,minH=12} local S S=W.session{inet=inet,width=win.w-3,colors=colors(Y.theme), onChange=function() win.title=S.title or"Web Browser" if S.job or S.proc then win.tickInterval=0.05 end Y.redraw(win) end, download=function(u,body,ct) fs.makeDirectory("/home/Downloads") local name=(u:match("([^/?#]+)[?#]?[^/]*$")or"download"):gsub("[^%w%._%-]","_") if name==""then name="download"end local path="/home/Downloads/"..name local f=io.open(path,"wb") if f then f:write(body or"");f:close()end Y.notify("Download finished",path.." ("..W.size(#(body or""))..")",{actionLabel="Show", action=function()Y.launch("files","/home/Downloads")end}) end} local function follow(href) if not href then return end local target=W.resolve(S.url,href) st.editing=false S.navigate(target) end local function focusAddress() st.editing=true st.addr.text=S.url st.addr.cur=unicode.len(st.addr.text) end function win.header(w,hw) local x=2 x=x+ui.button(w,x,1,"‹",function()S.goBack()end,#S.back>0 and nil or"flat",3)+1 x=x+ui.button(w,x,1,"›",function()S.goForward()end,#S.fwd>0 and nil or"flat",3)+1 x=x+ui.button(w,x,1,S.job and"×"or"↻",function()if S.job then S.stop()else S.reload()end end,nil,3)+1 x=x+ui.button(w,x,1,"⌂",function()S.navigate("about:home")end,nil,3)+1 local aw=hw-x-9 ui.entry(w,st.editing and st.addr or{text=S.url,cur=0},x,1,aw,st.editing,"Search or enter address") ui.hit(w,x,1,aw,1,function()if not st.editing then focusAddress()end end) ui.button(w,x+aw+1,1,S.isBookmarked()and"◆"or"◇",function() local on=S.toggleBookmark() Y.notify("Bookmarks",(on and"Added "or"Removed ")..(S.title or S.url)) end,"flat",3) ui.button(w,x+aw+4,1,"☰",function() Y.menu(w.x+hw-22,w.y+2,{ {"Bookmarks",function()S.navigate("about:bookmarks")end,key="Ctrl+B"}, {"History",function()S.navigate("about:history")end,key="Ctrl+H"}, "-", {"Find in Page…",function()st.findBox={text="",cur=0}end,key="Ctrl+F"}, {S.source and"View Page"or"View Source",function()S.toggleSource()end,key="Ctrl+U"}, {"Save Page As…",function()win.savePage()end,key="Ctrl+S"}, {"Copy Address",function()Y.clipboard=S.url;Y.notify("Web Browser","Address copied")end}, "-", {"Text Only (no styles/pictures)",function()S.setTextOnly(not S.textOnly);S.layout()end,check=S.textOnly}, {"Full Rendering (server)",function() if not S.server then return win.serverDialog()end S.setServer(S.server,not S.renderMode) S.navigate(S.url,false) end,check=S.renderMode}, {"Render Server…",function()win.serverDialog()end}, }) end,"flat",3) end function win.serverDialog() Y.dialog{title="Render server",text="Chromium on your PC renders pages with CSS, JavaScript and pictures\n(tools/ocweb_server.py). Address, e.g. http://192.168.1.103:8765", entry=S.server or"http://",buttons={{"Cancel"},{"Off",function()S.setServer(S.server or"",false);S.navigate(S.url,false)end}, {"Use",function(v)if v and v:match("^https?://")then S.setServer(v,true);S.sid=nil;S.navigate(S.url,false)end end,"suggested"}}} end function win.savePage() Y.dialog{title="Save Page",entry="/home/Downloads/"..((S.title or"page"):gsub("[^%w%-_]","_"):sub(1,24))..".html", buttons={{"Cancel"},{"Save",function(p) if not p or p==""then return end fs.makeDirectory(fs.path(p)) local f=io.open(p,"w") if f then f:write(S.body or"");f:close();Y.notify("Web Browser","Saved "..p)end end,"suggested"}}} end function win.draw(w,cw,ch) local t=Y.theme S.colors=colors(t) S.resize(cw-2) if S.proc or S.job then w.tickInterval=0.05 end local ph=ch-1-(st.findBox and 1 or 0) st.ph=ph gfx.fill(1,1,cw,ph,t.view) S.cols,S.rows=cw-1,ph local page=S.page local fr=S.frame if fr and S.renderMode and not S.job and(fr.cols~=S.cols or fr.rows~=S.rows)then S.rScroll(fr.scroll) end if fr and S.renderMode then if st.gridOf~=fr or st.gridTheme~=t then local g,rgb={},W.cubeRGB local cache={} local function idx(i)local c=cache[i]if not c then c=gfx.color(rgb(i));cache[i]=c end return c end for i,c in ipairs(fr.cells)do if c[1]then g[i]=c[1]|(idx(c[2])<<21)|(idx(c[3])<<29) elseif c[2]==c[3]then g[i]=32|(idx(c[2])<<21)|(idx(c[2])<<29) else g[i]=0x2580|(idx(c[2])<<21)|(idx(c[3])<<29)end end st.grid,st.gridOf,st.gridTheme=g,fr,t end gfx.blit(st.grid,fr.cols,fr.rows,1,1) if st.typing then local inp=st.typing local w2=math.max(inp.w,unicode.len(st.typed)+2) ui.round(inp.x,inp.y,math.min(w2,cw-inp.x),1,0xFFFFFF) local shown=inp.type=="password"and string.rep("•",unicode.len(st.typed))or st.typed gfx.text(inp.x+1,inp.y,ui.trunc(shown,w2-2).."▏",0x000000,0xFFFFFF) end ui.hit(w,1,1,cw-1,ph,function(_,px,py,btn) if btn==1 then for _,l in ipairs(fr.links)do if l.href~=""and px>=l.x and px=l.y and py=inp.x and px=inp.y and py>16,(c>>8)&255,c&255 if math.max(r,g,b)-math.min(r,g,b)>18 then v=W.cubeRGB(16+((r*5+127)//255)*40+((g*7+127)//255)*5+((b*4+127)//255)) else v=c end snapCache[c]=v return v end local maxScroll=math.max(0,sp.height-ph) S.scroll=math.max(0,math.min(S.scroll or 0,maxScroll)) local top,bottom=S.scroll+1,S.scroll+ph gfx.fill(1,1,cw-1,ph,C(sp.bg)) gfx.pushClip(1,1,cw-1,ph) for _,op in ipairs(sp.ops)do local y,h=op[3],op[1]=="text"and 1 or op[5] if y+h-1>=top and y<=bottom then local vy=y-S.scroll if op[1]=="fill"then gfx.fill(op[2],vy,op[4],op[5],C(op[6])) elseif op[1]=="text"then gfx.text(op[2],vy,op[4],C(op[5]),C(op[6])) elseif op[1]=="img"then local pic=S.picture(op[6],op[4],op[5]) if pic then local g={} local IM=require("ocimg") for cy=0,op[5]-1 do for cx=0,op[4]-1 do local tpx,bpx=IM.pixel(pic,cx,cy*2),IM.pixel(pic,cx,cy*2+1) if not tpx or not bpx then local under=gfx.bgAt(op[2]+cx,vy+cy) tpx,bpx=tpx or under,bpx or under end g[cy*op[4]+cx+1]=(tpx==bpx and 32 or 0x2580)|(gfx.color(C(tpx))<<21)|(gfx.color(C(bpx))<<29) end end gfx.blit(g,op[4],op[5],op[2],vy) elseif not op.cover then gfx.fill(op[2],vy,op[4],op[5],t.dark and 0x2D2D2D or 0xDEDDDA) if op[7]and op[7]~=""then gfx.text(op[2],vy,ui.trunc("▣ "..op[7],op[4]),t.dim)end end end end end gfx.popClip() for _,l in ipairs(sp.links)do if l.y+l.h-1>=top and l.y<=bottom then local href=l.href ui.hit(w,l.x,l.y-S.scroll,l.w,l.h,function(_,_,_,btn) if btn==1 then Y.menu(w.x+l.x,w.y+l.y-S.scroll+1,{{"Open Link",function()follow(href)end}, {"Open in New Window",function()Y.launch("web",W.resolve(S.url,href))end}, {"Copy Link Address",function()Y.clipboard=W.resolve(S.url,href)end}}) else follow(href)end end) end end for _,inp in ipairs(sp.inputs)do if inp.y>=top and inp.y<=bottom then ui.hit(w,inp.x,inp.y-S.scroll,inp.w,1,function() local el=inp.el if inp.kind=="submit"then if inp.form then S.submit(inp.form,el)end elseif inp.kind=="checkbox"or inp.kind=="radio"then el.attrs.checked=not el.attrs.checked and"checked"or nil S.layout() elseif inp.kind~="select"then st.formEdit={inp=inp,text=el.attrs.value or""} end end) end end if st.formEdit then local fe=st.formEdit local vy=fe.inp.y-S.scroll if vy>=1 and vy<=ph then local ww=math.max(fe.inp.w,10) gfx.fill(fe.inp.x,vy,ww,1,0xFFFFFF) local shown=fe.inp.kind=="password"and string.rep("•",unicode.len(fe.text))or fe.text gfx.text(fe.inp.x,vy,unicode.sub(shown,-(ww-1)).."▏",0x000000,0xFFFFFF) end end ui.scrollbar(cw,1,ph,S.scroll,ph,sp.height) elseif page then local maxScroll=math.max(0,#page.lines-ph) S.scroll=math.max(0,math.min(S.scroll or 0,maxScroll)) local selLink=page.links[S.sel or 0] for row=1,ph do local li=S.scroll+row local line=page.lines[li] if not line then break end if st.findLine==li then gfx.fill(1,row,cw-1,1,t.dark and 0x454545 or 0xF9F06B)end local x=2+(line.indent or 0) for _,seg in ipairs(line.segs)do local fg=seg.fg or t.fg local bg=nil if seg.href and selLink and selLink.line==li and selLink.href==seg.href then bg=t.accent;fg=0xFFFFFF end gfx.text(x,row,seg.text,fg,bg) local len=unicode.len(seg.text) if seg.href then local href=seg.href ui.hit(w,x,row,len,1,function(_,_,_,btn) if btn==1 then Y.menu(w.x+x,w.y+row+1,{{"Open Link",function()follow(href)end}, {"Open in New Window",function()Y.launch("web",W.resolve(S.url,href))end}, {"Copy Link Address",function()Y.clipboard=W.resolve(S.url,href)end}}) else follow(href)end end) end x=x+len end end ui.scrollbar(cw,1,ph,S.scroll,ph,#page.lines) end if st.findBox then gfx.fill(1,ch-1,cw,1,t.sidebar) gfx.text(2,ch-1,"Find:",t.dim,t.sidebar) ui.entry(w,st.findBox,8,ch-1,40,true) ui.button(w,50,ch-1,"Next",function()win.findNext()end,nil,6) ui.button(w,cw-3,ch-1,"×",function()st.findBox,st.findLine=nil,nil end,"flat",3) end gfx.fill(1,ch,cw,1,t.sidebar) local status=S.status or"" if selLink then status="→ "..W.resolve(S.url,selLink.href)end if st.typing then status="Typing into "..st.typing.type.." · Enter: send · Tab: next field · Esc: cancel"end gfx.text(2,ch,ui.trunc(status,cw-20),t.dim,t.sidebar) if S.job then local p=S.job.progress ui.progress(cw-17,ch,16,p or((os.clock()*2)%1),t.accent) end end function win.findNext() if not st.findBox then return end local i=S.find(st.findBox.text,st.findLine or S.scroll) if i then st.findLine=i;S.scroll=math.max(0,i-3) else Y.notify("Web Browser","“"..st.findBox.text.."” not found")end end function win.tick(w) if S.job or S.proc then S.step()end if not S.job and st.queue and#st.queue>0 then table.remove(st.queue,1)()end win.tickInterval=(S.job or S.proc or(st.queue and#st.queue>0))and 0.05 or nil Y.redraw(w) end local function scrollBy(n) local total=S.styled and not S.source and S.styled.height or(S.page and#S.page.lines) if not total then return end S.scroll=math.max(0,math.min(math.max(0,total-(st.ph or 20)),(S.scroll or 0)+n)) end local function selectLink(dir) local links=S.page and S.page.links or{} if#links==0 then return end S.sel=((S.sel or 0)-1+dir)%#links+1 local li=links[S.sel].line if li<=S.scroll then S.scroll=li-1 elseif li>S.scroll+(st.ph or 20)then S.scroll=li-(st.ph or 20)end end function win.key(w,char,code) local K=Y.K local ctrl,alt=Y.mods.ctrl(),Y.mods.alt() if st.editing then local r=ui.entryKey(st.addr,char,code) if r=="enter"then st.editing=false;S.navigate(st.addr.text) elseif r=="esc"then st.editing=false end return end if st.formEdit then local fe=st.formEdit if code==K.enter or code==K.tab then fe.inp.el.attrs.value=fe.text st.formEdit=nil if code==K.enter and fe.inp.form then S.submit(fe.inp.form) else if code==K.tab then local found for _,inp in ipairs(S.styled and S.styled.inputs or{})do if found and inp.kind~="submit"and inp.kind~="checkbox"and inp.kind~="radio"and inp.kind~="select"then st.formEdit={inp=inp,text=inp.el.attrs.value or""} break end if inp==fe.inp then found=true end end end S.layout() end elseif code==K.esc then st.formEdit=nil elseif code==K.back then fe.text=unicode.sub(fe.text,1,-2) elseif char and char>=32 then fe.text=fe.text..unicode.char(char)end return end if st.typing then if code==K.enter or code==K.tab then local key=code==K.enter and"Enter"or"Tab" local text=st.typed st.typing=nil st.queue=st.queue or{} if text~=""then st.queue[#st.queue+1]=function()S.rType(text)end end st.queue[#st.queue+1]=function()S.rKey(key)end win.tickInterval=0.05 elseif code==K.esc then st.typing=nil elseif code==K.back then st.typed=unicode.sub(st.typed,1,-2) elseif char and char>=32 then st.typed=st.typed..unicode.char(char)end return end if S.renderMode and S.frame and not st.findBox and not ctrl and not alt then local fr=S.frame if code==K.pgdn or char==32 then return S.rScroll(fr.scroll+fr.rows-2) elseif code==K.pgup then return S.rScroll(fr.scroll-fr.rows+2) elseif code==K.down then return S.rScroll(fr.scroll+3) elseif code==K.up then return S.rScroll(fr.scroll-3) elseif code==K.home then return S.rScroll(0) elseif code==K["end"]then return S.rScroll(fr.total) elseif code==K.back then return S.goBack() elseif code==K.enter or code==K.tab or code==K.esc then return S.rKey(code==K.enter and"Enter"or code==K.tab and"Tab"or"Escape")end end if st.findBox and not ctrl then local r=ui.entryKey(st.findBox,char,code) if r=="enter"then win.findNext()elseif r=="esc"then st.findBox,st.findLine=nil,nil end if r then return end end if ctrl then if code==K.l then focusAddress() elseif code==0x21 then st.findBox=st.findBox or{text="",cur=0} elseif code==0x20 then S.toggleBookmark() elseif code==0x16 then S.toggleSource() elseif code==K.s then win.savePage() elseif code==0x30 then S.navigate("about:bookmarks") elseif code==K.h then S.navigate("about:history")end return end if alt and code==K.left then S.goBack()return end if alt and code==K.right then S.goForward()return end if code==0x3F then S.reload() elseif code==K.esc then S.stop() elseif code==K.up then scrollBy(-1)elseif code==K.down then scrollBy(1) elseif code==K.pgup then scrollBy(-(st.ph or 20)+1) elseif code==K.pgdn or char==32 then scrollBy((st.ph or 20)-1) elseif code==K.home then S.scroll=0 elseif code==K["end"]then scrollBy(1e9) elseif code==K.tab then selectLink(Y.mods.shift()and-1 or 1) elseif code==K.enter then local l=S.page and S.page.links[S.sel or 0]if l then follow(l.href)end elseif code==K.back then S.goBack()end end function win.paste(w,s) if st.formEdit then st.formEdit.text=st.formEdit.text..s return end if st.typing then st.typed=st.typed..s return end if st.editing then ui.entryPaste(st.addr,s)elseif st.findBox then ui.entryPaste(st.findBox,s) else focusAddress();st.addr.text,st.addr.cur=s,unicode.len(s)end end function win.scroll(w,x,y,dir) if S.renderMode and S.frame then if not S.job then S.rScroll(S.frame.scroll-dir*math.max(3,S.frame.rows//3))end return end scrollBy(-dir*3) end function win.cleanup() S.stop() S.styled,S.page,S.frame,S.imgCache,S.imgData=nil,nil,nil,nil,nil if#Y.windowsOf("web")<=1 then for _,m in ipairs({"ocweb","ocdom","occss","oclayout","ocimg"})do package.loaded[m]=nil end end end win.session=S S.cols,S.rows=win.w-1,win.h-2 S.navigate(url or"about:home",false) win.tickInterval=0.05 return win end return M