local Y=require("yaru.core") local gfx=require("yaru.gfx") local ui=require("yaru.ui") local fs=require("filesystem") local unicode=require("unicode") local usub,ulen=unicode.sub,unicode.len local M={} local KW={} for w in("and break do else elseif end false for function goto if in local nil not or repeat return then true until while"):gmatch("%S+")do KW[w]=true end local function lexLua(line,inComment) local out,i,n={},1,#line local function push(a,b,k)out[#out+1]={a,line:sub(a,b),k}end if inComment then local e=line:find("]]",1,true) if not e then push(1,n,"com")return out,true end push(1,e+1,"com");i=e+2 end while i<=n do local c=line:sub(i,i) if line:sub(i,i+3)=="--[["then local e=line:find("]]",i+4,true) if not e then push(i,n,"com")return out,true end push(i,e+1,"com");i=e+2 elseif line:sub(i,i+1)=="--"then push(i,n,"com")break elseif c=='"'or c=="'"then local j=i+1 while j<=n do local d=line:sub(j,j) if d=="\\"then j=j+1 elseif d==c then break end j=j+1 end push(i,math.min(j,n),"str");i=j+1 elseif line:sub(i,i+1)=="[["then local e=line:find("]]",i+2,true)or n-1 push(i,e+1,"str");i=e+2 elseif c:match("%d")then local e=select(2,line:find("^[%w%.]*",i)) push(i,e,"num");i=e+1 elseif c:match("[%a_]")then local e=select(2,line:find("^[%w_]*",i)) local word=line:sub(i,e) push(i,e,KW[word]and"kw"or"txt");i=e+1 else local e=select(2,line:find("^[^%w_\"'%-%[]*",i)) if e1 and ed.lines[#ed.lines]==""and data:sub(-1)=="\n"then ed.lines[#ed.lines]=nil end end if#ed.lines==0 then ed.lines[1]=""end ed.path,ed.dirty,ed.cx,ed.cy,ed.top,ed.left=p,false,1,1,1,0 ed.lua=p:match("%.lua$")~=nil ed.ro=fs.get(p)and fs.get(p).isReadOnly() setTitle() end local function save(p,after) p=p or ed.path if not p then Y.dialog{title="Save As",text="File name (full path)",entry="/home/untitled.txt",buttons={{"Cancel"}, {"Save",function(txt)if txt and txt~=""then save(txt,after)end end,"suggested"}}} return end local f,err=io.open(p,"w") if not f then return Y.notify("Could not save",tostring(err))end f:write(table.concat(ed.lines,"\n"),"\n") f:close() ed.path,ed.dirty=p,false ed.lua=p:match("%.lua$")~=nil setTitle() for _,w in ipairs(Y.windowsOf("files"))do if w.reload then w.reload()end end if after then after()end end win=Y.newWindow{app="editor",title="Text Editor",w=84,h=28,minW=40,minH=8,showTitle=true} if path then if fs.exists(path)then loadFile(path)else ed.path=path;ed.lua=path:match("%.lua$")~=nil;setTitle()end else setTitle()end local function line()return ed.lines[ed.cy]end local function clampX()ed.cx=math.max(1,math.min(ed.cx,ulen(line())+1))end local function modified()if not ed.dirty then ed.dirty=true;setTitle()end end local function insert(s) local l=line() local parts={} for p in(s.."\n"):gmatch("(.-)\n")do parts[#parts+1]=p end local before,after=usub(l,1,ed.cx-1),usub(l,ed.cx) if#parts==1 then ed.lines[ed.cy]=before..parts[1]..after ed.cx=ed.cx+ulen(parts[1]) else ed.lines[ed.cy]=before..parts[1] for i=2,#parts do table.insert(ed.lines,ed.cy+i-1,parts[i])end ed.cy=ed.cy+#parts-1 ed.cx=ulen(parts[#parts])+1 ed.lines[ed.cy]=ed.lines[ed.cy]..after end modified() end local function findNext(q,from) if not q or q==""then return end local n=#ed.lines for k=0,n do local y=(ed.cy-1+k)%n+1 local start=(k==0)and(from or ed.cx)or 1 local l=ed.lines[y] local bytePos=utf8.offset(l,start)or#l+1 local p=unicode.lower(l):find(unicode.lower(q),bytePos,true) if p then ed.cy=y ed.cx=ulen(l:sub(1,p-1))+1 return true end end Y.notify("Text Editor","“"..q.."” not found") end local GUT=5 local colors local function openDialog() Y.dialog{title="Open File",text="Full path",entry=ed.path and fs.path(ed.path)or"/home/",buttons={{"Cancel"}, {"Open",function(p)if p and p~=""then if not ed.dirty and#ed.lines==1 and ed.lines[1]==""then loadFile(p)else M.open(p)end end end,"suggested"}}} end function win.header(w,hw) ui.button(w,2,1,"Open ▾",openDialog,nil,8) ui.button(w,11,1,"+",function()M.open()end,"flat",3) ui.button(w,hw-7,1,"Save",function()save()end,nil,6) end function win.draw(w,cw,ch) local t=Y.theme colors=colors or{} local dark=t.dark local C={kw=dark and 0xF66151 or 0xA51D2D,str=dark and 0xE9AD0C or 0x986A44,com=dark and 0x77767B or 0x9A9996, num=dark and 0xC061CB or 0x813D9C,txt=t.fg} local rows=ch-1-(ed.find and 1 or 0) local tw=cw-GUT if ed.cy=ed.top+rows then ed.top=ed.cy-rows+1 end if ed.cx-1=ed.left+tw-1 then ed.left=ed.cx-tw+1 end gfx.fill(1,1,GUT,rows,t.sidebar) gfx.fill(GUT+1,1,tw,rows,t.view) local inCom=false if ed.lua then for y=1,ed.top-1 do local l=ed.lines[y] if l:find("--[[",1,true)or inCom then local _,c=lexLua(l,inCom);inCom=c end end end for r=1,rows do local y=ed.top+r-1 local l=ed.lines[y] if not l then break end local cur=y==ed.cy gfx.text(1,r,string.format("%4d",y),cur and t.fg or t.dim,cur and(dark and 0x383838 or 0xDEDDDA)or t.sidebar) if cur then gfx.fill(GUT+1,r,tw,1,dark and 0x2A2A2A or 0xF6F5F4)end local bg=cur and(dark and 0x2A2A2A or 0xF6F5F4)or t.view if ed.lua then local toks toks,inCom=lexLua(l,inCom) for _,tk in ipairs(toks)do local col=ulen(l:sub(1,tk[1]-1))-ed.left local s=tk[2] local sl=ulen(s) if col+sl>0 and coltw then s=usub(s,1,tw-col)end gfx.text(GUT+1+col,r,s,C[tk[3]],bg) end end else gfx.text(GUT+1,r,usub(l,ed.left+1,ed.left+tw),t.fg,bg) end if Y.focus==w and cur then local cc=usub(l,ed.cx,ed.cx) gfx.cell(GUT+ed.cx-ed.left,r,cc~=""and cc or" ",t.view,t.accent) end local ln=y ui.hit(w,GUT+1,r,tw,1,function(_,px)ed.cy=ln;ed.cx=px-GUT+ed.left;clampX()end) end ui.scrollbar(cw,1,rows,ed.top-1,rows,#ed.lines) if ed.find then local fy=ch-1 gfx.fill(1,fy,cw,1,t.sidebar) gfx.text(2,fy,"Find:",t.dim,t.sidebar) ui.entry(w,ed.find,8,fy,math.min(40,cw-20),true) ui.button(w,math.min(48,cw-11),fy,"Next",function()findNext(ed.find.text,ed.cx+1)end,nil,6) ui.button(w,cw-3,fy,"×",function()ed.find=nil end,"flat",3) end gfx.fill(1,ch,cw,1,t.sidebar) local st=string.format("Ln %d, Col %d",ed.cy,ed.cx) local mode=(ed.lua and"Lua"or"Plain Text")..(ed.ro and" (read-only)"or"") gfx.text(2,ch,ui.trunc(ed.path or"Untitled",cw-30),t.dim,t.sidebar) gfx.text(cw-unicode.len(st)-unicode.len(mode)-5,ch,mode.." "..st,t.dim,t.sidebar) end function win.scroll(w,x,y,dir) ed.top=math.max(1,math.min(#ed.lines,ed.top-dir*3)) ed.cy=math.max(ed.top,math.min(ed.cy,ed.top+w.h-4)) clampX() end function win.paste(w,s) if ed.find then ui.entryPaste(ed.find,s)return end insert((s:gsub("\r\n","\n"):gsub("\t"," "))) end function win.key(w,char,code) local K=Y.K local c,sh=Y.mods.ctrl(),Y.mods.shift() if ed.find and not c then local r=ui.entryKey(ed.find,char,code) if r=="enter"then findNext(ed.find.text,ed.cx+1) elseif r=="esc"then ed.find=nil end if r then return end end if c then if code==K.s then if sh then local p=ed.path;ed.path=nil;save(nil);if not ed.path then ed.path=p end else save()end elseif code==0x18 then openDialog() elseif code==0x31 then M.open() elseif code==0x21 then ed.find=ed.find or{text="",cur=0};ed.find.cur=ulen(ed.find.text) elseif code==0x22 then if ed.find then findNext(ed.find.text,ed.cx+1)end elseif code==K.c then Y.clipboard=line().."\n" elseif code==0x2D or code==0x25 then Y.clipboard=line().."\n" if#ed.lines>1 then table.remove(ed.lines,ed.cy)else ed.lines[1]=""end ed.cy=math.min(ed.cy,#ed.lines);clampX();modified() elseif code==K.v or code==0x16 then local s=Y.clipboard or"" if s:sub(-1)=="\n"then ed.cx=1;insert(s);ed.cx=1 else insert(s)end elseif code==K.home then ed.cy,ed.cx=1,1 elseif code==K["end"]then ed.cy=#ed.lines;ed.cx=ulen(line())+1 elseif code==K.d then table.insert(ed.lines,ed.cy+1,line());ed.cy=ed.cy+1;modified() end return end local l=line() if code==K.left then if ed.cx>1 then ed.cx=ed.cx-1 elseif ed.cy>1 then ed.cy=ed.cy-1;ed.cx=ulen(line())+1 end elseif code==K.right then if ed.cx<=ulen(l)then ed.cx=ed.cx+1 elseif ed.cy<#ed.lines then ed.cy=ed.cy+1;ed.cx=1 end elseif code==K.up then ed.cy=math.max(1,ed.cy-1);clampX() elseif code==K.down then ed.cy=math.min(#ed.lines,ed.cy+1);clampX() elseif code==K.pgup then ed.cy=math.max(1,ed.cy-(w.h-3));clampX() elseif code==K.pgdn then ed.cy=math.min(#ed.lines,ed.cy+(w.h-3));clampX() elseif code==K.home then local ind=ulen(l:match("^%s*")) ed.cx=(ed.cx==ind+1)and 1 or ind+1 elseif code==K["end"]then ed.cx=ulen(l)+1 elseif code==K.enter or code==0x9C then local ind=l:match("^%s*") if ed.lua and usub(l,1,ed.cx-1):match("[%(%{]%s*$")or usub(l,1,ed.cx-1):match("%f[%w]then%s*$")or usub(l,1,ed.cx-1):match("%f[%w]do%s*$") or usub(l,1,ed.cx-1):match("function[^%)]*%)%s*$")then ind=ind.." "end insert("\n"..ind) elseif code==K.back then if ed.cx>1 then local before=usub(l,1,ed.cx-1) local n=(before:match("^%s+$")and#before%2==0 and#before>=2)and 2 or 1 ed.lines[ed.cy]=usub(l,1,ed.cx-1-n)..usub(l,ed.cx) ed.cx=ed.cx-n modified() elseif ed.cy>1 then local prev=ed.lines[ed.cy-1] ed.cx=ulen(prev)+1 ed.lines[ed.cy-1]=prev..l table.remove(ed.lines,ed.cy) ed.cy=ed.cy-1 modified() end elseif code==K.del then if ed.cx<=ulen(l)then ed.lines[ed.cy]=usub(l,1,ed.cx-1)..usub(l,ed.cx+1);modified() elseif ed.cy<#ed.lines then ed.lines[ed.cy]=l..table.remove(ed.lines,ed.cy+1);modified()end elseif code==K.tab then insert(" ") elseif char and char>=32 and char~=127 then insert(unicode.char(char)) if ed.lua then local cl=line() local ind,word=cl:match("^(%s+)(%S+)$") if ind and#ind>=2 and ed.cx==ulen(cl)+1 and(word=="end"or word=="else"or word=="until"or word=="}")then local prev=ed.lines[ed.cy-1] local pind=prev and#prev:match("^%s*")or 0 if#ind>pind-2 then ed.lines[ed.cy]=cl:sub(3);ed.cx=ed.cx-2 end end end end end function win.onclose(w) if not ed.dirty then return true end Y.dialog{title="Save changes to “"..(ed.path and fs.name(ed.path)or"Untitled Document").."” before closing?", text="If you don't save, changes will be permanently lost.", buttons={{"Cancel"},{"Close without Saving",function()Y.closeWin(w,true)end,"destructive"}, {"Save",function()save(nil,function()Y.closeWin(w,true)end)end,"suggested"}}} return false end return win end return M