No bookmarks yet. Press Ctrl+D on a page.
")end add("Type an address or words to search (DuckDuckGo).
") if#S.bookmarks>0 then add("Ctrl+L address · Enter open · Tab next link · Alt+Left/Right back/forward · F5 reload · Ctrl+F find · Ctrl+D bookmark · Ctrl+U view source · Ctrl+S save page · Space/PgDn scroll · Esc stop
]]) end add("") return table.concat(h) end local cfgMode=loadList("mode") S.textOnly=cfgMode.textOnly or false function S.setTextOnly(v)S.textOnly=v and true or false;saveList("mode",{textOnly=S.textOnly})end S.imgCache,S.imgNat,S.imgData={},{},{} local budget=0.04 local sliceStart=0 local okc,computerLib=pcall(require,"computer") local function freeMem() if okc and computerLib and computerLib.freeMemory then return computerLib.freeMemory()end return math.huge end local lowCount=0 local function slice() if os.clock()-sliceStart>budget then if freeMem()<100*1024 then lowCount=lowCount+1 if lowCount>25 then error("not enough memory for the styled view",0)end else lowCount=0 end coroutine.yield() end end S.cssCache={} local function fetchInside(url,maxBytes) if not inet then return nil end local job=W.fetch(inet,url,{maxBytes=maxBytes}) while true do local st=job:step() if st=="done"then return job.body,job.ct end if st=="error"then return nil end coroutine.yield() end end local function styledPipeline(html,url) local D,CSS,LAY=require("ocdom"),require("occss"),require("oclayout") S.status="Parsing…" if collectgarbage then collectgarbage()end local free=freeMem() local maxNodes=free==math.huge and 8000 or math.max(500,math.min(8000,(free-300*1024)//900)) LAY.maxRows=math.max(150,math.min(3000,maxNodes//2)) local dom=D.parse(html,W.decodeEntities,maxNodes,slice) local vw=require("oclayout").viewport(S.cols) local rules={} local base=function(u)return W.resolve(url,u)end local sheets,total=0,0 D.walk(dom,function(n) if n.tag=="title"and n.children[1]then S.title=n.children[1].text:gsub("%s+"," ")end end) local list,present={},nil D.walk(dom,function(n) if n.css then list[#list+1]={css=n.css} elseif n.tag=="link"and(n.attrs.rel or""):lower():find("stylesheet")and n.attrs.href then local media=(n.attrs.media or"all"):lower() if not media:find("print")or media:find("screen")then list[#list+1]={href=base(n.attrs.href)}end end end) for _,it in ipairs(list)do local css,sheetUrl=it.css,url if it.href and sheets<6 and total<400*1024 then S.status="Loading style "..(it.href:match("[^/?]+[^/]*$")or it.href).."…" if o.onChange then o.onChange("status")end css=S.cssCache[it.href] if not css then css=fetchInside(it.href,300*1024) if css and#css<120*1024 then S.cssCache[it.href]=css end end sheets=sheets+1 sheetUrl=it.href if css then total=total+#css end end if css then present=present or CSS.present(dom) CSS.parse(W.sanitize(css),vw,rules,function(u)return W.resolve(sheetUrl,u)end,present) css=nil slice() end end S.status=string.format("Styling (%d rules)…",#rules) if o.onChange then o.onChange("status")end CSS.cascade(dom,rules,vw,slice) rules=nil LAY.natural=function(src)return S.imgNat[src]end local function doLayout() S.status="Layout…" local page=LAY.layout(dom,S.cols,base,slice) local used={} for _,op in ipairs(page.images)do used[op[6].."|"..op[4].."x"..op[5]]=true end for k in pairs(S.imgCache)do if not used[k]then S.imgCache[k]=nil end end S.styled=page S.styledDom=dom S.scroll=math.min(S.scroll or 0,math.max(0,page.height-1)) if o.onChange then o.onChange("page")end return page end local page=doLayout() local I=require("ocimg") local rawBudget=160*1024 local function decodeFor(pg,src,data) for _,op in ipairs(pg.images)do if op[6]==src then local key=src.."|"..op[4].."x"..op[5] if S.imgCache[key]==nil then local img=I.decode(data,op[4],op[5]*2,slice) S.imgCache[key]=img or false if o.onChange then o.onChange("page")end end end end end local seen,n,guessed={},0,false for _,op in ipairs(page.images)do local src=op[6] if src and src~=""and not seen[src]and not src:find("^data:")and n<30 then seen[src]=true n=n+1 S.status=string.format("Picture %d of %d…",n,#page.images) if o.onChange then o.onChange("status")end local data=fetchInside(src,400*1024) if data then local w,h=I.size(data) if w then if not S.imgNat[src]then guessed=true end S.imgNat[src]={w,h} decodeFor(page,src,data) if#data<=rawBudget then S.imgData[src]=data;rawBudget=rawBudget-#data end end end data=nil end end if guessed then page=doLayout() for src in pairs(seen)do local missing=false for _,op in ipairs(page.images)do if op[6]==src and S.imgCache[src.."|"..op[4].."x"..op[5]]==nil then missing=true end end if missing then local data=S.imgData[src]or fetchInside(src,400*1024) if data then decodeFor(page,src,data)end end end end S.status=string.format("%s · %d pictures%s",S.httpInfo or"",n,dom.truncated and" · long page cut (memory)"or"") if o.onChange then o.onChange("page")end end function S.picture(src,w,h) local c=S.imgCache[src.."|"..w.."x"..h] return c or nil end function S.layout() local body,isHtml=S.body or"",S.isHtml if S.source then isHtml=false end S.page=W.layout(body,S.width,S.colors,isHtml) S.title=(not S.source and S.page.title)or S.url S.proc=nil if isHtml and not S.textOnly and not S.source then local body2,url=body,S.url S.proc=coroutine.create(function()styledPipeline(body2,url)end) end end local function show(body,ct,url) S.frame=nil S.styled=nil S.cssCache,S.imgData,S.imgCache={},{},{} S.body=body S.ct=ct or"text/html" S.isHtml=S.ct:find("html")or body:sub(1,512):lower():find("200 do table.remove(S.globalHistory,1)end saveList("history",S.globalHistory) end if o.onChange then o.onChange("page")end end function S.stop() if S.job then S.job:cancel();S.job=nil;S.status="Stopped"end if S.proc then S.proc=nil;S.status="Stopped"end end function S.navigate(input,push) local url=W.normalize(input,o.searchUrl) if not url then return end if push~=false and S.url and S.url~=url then table.insert(S.back,S.url) S.fwd={} end S.stop() S.source=false if url:match("^about:")then S.status=url return show(aboutPage(url),"text/html",url) end if not url:match("^https?:")then S.status="Unsupported address: "..url;if o.onChange then o.onChange("status")end return end if S.renderMode then S.frame=S.frame return S.rOpen(url,push) end if S.body and url:gsub("#.*$","")==(S.url or""):gsub("#.*$","")and url:find("#")then S.url=url local a=S.page.anchors[url:match("#(.+)$")] if a then S.scroll=a-1 end if o.onChange then o.onChange("page")end return end if not inet then S.status="No internet card";return show(aboutPage("about:home"),"text/html","about:home")end S.job=W.fetch(inet,url) S.loadingUrl=url S.status="Connecting to "..(W.parse(url).host or url).."…" if o.onChange then o.onChange("status")end end function S.goBack() if S.renderMode and S.sid and S.frame then return S.rNav("back")end local u=table.remove(S.back)if u then table.insert(S.fwd,S.url)S.navigate(u,false)end end function S.goForward() if S.renderMode and S.sid and S.frame then return S.rNav("forward")end local u=table.remove(S.fwd)if u then table.insert(S.back,S.url)S.navigate(u,false)end end function S.reload() if S.renderMode and S.sid and S.frame then return S.rNav("reload")end S.navigate(S.url,false) end function S.step() if S.proc and not S.job then sliceStart=os.clock() local ok,err=coroutine.resume(S.proc) if not ok then S.status="Styled view failed: "..tostring(err).." (text view shown)";S.proc,S.styled=nil,nil end if S.proc and coroutine.status(S.proc)=="dead"then S.proc=nil end return true end local job=S.job if not job then return false end local st=job:step() if st=="done"and job.render then S.job=nil local f,err=W.parseFrame(job.body or"") if not f then S.status="Render server: "..tostring(err) if err and err:find("unknown session")then S.sid=nil end else S.sid,S.frame=f.sid,f if f.url~=S.url and S.url and not S.url:match("^about:")and job.url:find("/open?",1,true)==nil then table.insert(S.back,S.url) end S.url,S.title=f.url,(f.title~=""and f.title or f.url) S.status=string.format("Rendered · %s · rows %d-%d of %d",W.size(#job.body),f.scroll+1,f.scroll+f.rows,f.total) S.page=nil if not S.url:match("^about:")then table.insert(S.globalHistory,{url=S.url,title=S.title}) while#S.globalHistory>200 do table.remove(S.globalHistory,1)end saveList("history",S.globalHistory) end end if o.onChange then o.onChange("page")end return true elseif st=="error"and job.render then S.job=nil S.status="Render server unreachable: "..tostring(job.err).." (menu → Render server)" if o.onChange then o.onChange("status")end return true elseif st=="done"then S.job=nil local ct=job.ct or"" local text=ct==""or ct:find("text")or ct:find("html")or ct:find("json")or ct:find("xml")or ct:find("javascript") if not text and o.download then o.download(job.url,job.body,ct) S.status="Downloaded "..job.url return true end local body=W.decodeBody(job.body or"",ct) S.status=string.format("HTTP %s · %s%s",tostring(job.code),ui_size_(#(job.body or"")),job.truncated and" (truncated)"or"") S.httpInfo=S.status show(body,ct,job.url) return true elseif st=="error"then S.job=nil S.status="Error: "..tostring(job.err) show(""..esc(S.loadingUrl or"").. "
"..esc(tostring(job.err)).."
","text/html",S.loadingUrl or"about:home") return true end S.status=string.format("Loading %s… %s",W.parse(job.url).host or"",job.received>0 and ui_size_(job.received)or"") return true end local cfgServer=loadList("server") S.server=cfgServer.url S.renderMode=cfgServer.on and S.server~=nil function S.setServer(url,on) S.server=url~=""and url or nil S.renderMode=(on and S.server~=nil)and true or false saveList("server",{url=S.server,on=S.renderMode}) end S.cols,S.rows=80,24 local function rq(cmd,params) local q={} for k,v in pairs(params or{})do q[#q+1]=k.."="..W.urlencode(tostring(v))end if S.sid then q[#q+1]="sid="..S.sid end return S.server:gsub("/+$","").."/"..cmd.."?"..table.concat(q,"&") end local function renderJob(cmd,params,label) if not inet then S.status="No internet card";return end if S.job then S.job:cancel()end S.job=W.fetch(inet,rq(cmd,params),{maxBytes=2*1024*1024,timeout=40}) S.job.render=true S.status=label or"Rendering…" if o.onChange then o.onChange("status")end end function S.rOpen(url,push) if push~=false and S.url and S.url~=url then table.insert(S.back,S.url);S.fwd={}end S.loadingUrl=url renderJob("open",{url=url,cols=S.cols,rows=S.rows},"Rendering "..(W.parse(url).host or url).."…") end function S.rClick(x,y)renderJob("click",{x=x,y=y},"Click…")end function S.rType(text)renderJob("type",{text=text},"Typing…")end function S.rKey(key)renderJob("key",{key=key},key.."…")end function S.rScroll(row) if not S.frame then return end row=math.max(0,math.min(row,math.max(0,S.frame.total-S.rows))) renderJob("frame",{row=row,cols=S.cols,rows=S.rows},"Scrolling…") end function S.rNav(cmd)renderJob(cmd,{},cmd.."…")end function S.submit(form,submitter) local D=require("ocdom") local fields={} D.walk(form,function(n) local a=n.attrs or{} if not n.tag or not a.name or a.disabled then return end if n.tag=="input"then local t=(a.type or"text"):lower() if t=="checkbox"or t=="radio"then if a.checked then fields[#fields+1]={a.name,a.value or"on"}end elseif t=="submit"or t=="button"or t=="image"or t=="reset"or t=="file"then if n==submitter then fields[#fields+1]={a.name,a.value or""}end else fields[#fields+1]={a.name,a.value or""}end elseif n.tag=="textarea"then fields[#fields+1]={a.name,a.value or D.text(n)} elseif n.tag=="select"then local val D.walk(n,function(o2)if o2.tag=="option"and(not val or o2.attrs.selected)then val=o2.attrs.value or D.text(o2)end end) fields[#fields+1]={a.name,val or""} elseif n.tag=="button"and n==submitter then fields[#fields+1]={a.name,a.value or""}end end) local q={} for _,f in ipairs(fields)do q[#q+1]=W.urlencode(f[1]).."="..W.urlencode(f[2])end local query=table.concat(q,"&") local action=W.resolve(S.url,form.attrs.action or S.url) if(form.attrs.method or"get"):lower()=="post"then if not inet then return end table.insert(S.back,S.url);S.fwd={} S.stop() S.job=W.fetch(inet,action,{post=query}) S.loadingUrl=action S.status="Sending form…" if o.onChange then o.onChange("status")end else S.navigate(action:gsub("%?.*$","").."?"..query) end end function S.isBookmarked()for i,b in ipairs(S.bookmarks)do if b.url==S.url then return i end end end function S.toggleBookmark() local i=S.isBookmarked() if i then table.remove(S.bookmarks,i)else table.insert(S.bookmarks,1,{url=S.url,title=S.title})end saveList("bookmarks",S.bookmarks) return not i end function S.toggleSource()S.source=not S.source;S.layout();S.scroll=0 end function S.find(text,from) if not S.page or text==""then return end local q=unicode.lower(text) local n=#S.page.lines for k=1,n do local i=(from+k-1)%n+1 if unicode.lower(W.lineText(S.page.lines[i])):find(q,1,true)then return i end end end function S.resize(width) if width~=S.width then S.width=width S.cols=width+1 if S.body then S.layout()end end end return S end local R_LV,G_LV,B_LV={},{},{} for i=0,5 do R_LV[i]=math.floor(i*255/5+0.5)end for i=0,7 do G_LV[i]=math.floor(i*255/7+0.5)end for i=0,4 do B_LV[i]=math.floor(i*255/4+0.5)end function W.cubeRGB(i) i=i-16 if i<0 then i=0 end return R_LV[i//40]<<16|G_LV[(i//5)%8]<<8|B_LV[i%5] end function W.parseFrame(data) local nl=data:find("\n",1,true) if data:sub(1,3)=="ERR"then return nil,data:sub(5)end if not nl or data:sub(1,4)~="SID "then return nil,"not a render server answer"end local sid=data:sub(5,nl-1) local p=nl+1 if data:sub(p,p+3)~="OCF2"then return nil,"bad frame"end p=p+4 local f={sid=sid} f.cols,f.rows,f.total,f.scroll,f.nlinks,f.ninputs,p=string.unpack(">I2I2I2I2I2I2",data,p) local function str() local s s,p=string.unpack(">s2",data,p) return s end f.url,f.title=str(),str() local cells={} local byte,sub=string.byte,string.sub for i=1,f.cols*f.rows do local t,b,fg,n=byte(data,p,p+3) p=p+4 if n and n>0 then cells[i]={utf8.codepoint(sub(data,p,p+n-1)),fg,t} p=p+n else cells[i]={false,t,b} end end f.cells=cells f.links,f.inputs={},{} for i=1,f.nlinks do local x,y,w,h x,y,w,h,p=string.unpack(">I2I2I2I2",data,p) f.links[i]={x=x,y=y,w=w,h=h,href=str(),text=str()} end for i=1,f.ninputs do local x,y,w,h x,y,w,h,p=string.unpack(">I2I2I2I2",data,p) f.inputs[i]={x=x,y=y,w=w,h=h,type=str(),value=str(),placeholder=str()} end return f end function W.lineText(line) local t={} for _,s in ipairs(line.segs)do t[#t+1]=s.text end return string.rep(" ",line.indent or 0)..table.concat(t) end return W