local Y=require("yaru.core") local gfx=require("yaru.gfx") local ui=require("yaru.ui") local fs=require("filesystem") local unicode=require("unicode") local M={} local DIR="/home/Notes" local COLORS={0xFFDB80,0xB6DBFF,0xB6FFBF,0xFFB6BF,0xDBB6FF,0xF0F0F0} local function wrap(s,w) local out={} for para in(s.."\n"):gmatch("(.-)\n")do if para==""then out[#out+1]=""end while unicode.len(para)>0 do local line=unicode.sub(para,1,w) if unicode.len(para)>w then local cut=line:match("^.*()%s") if cut and cut>w//2 then line=line:sub(1,cut-1)end end out[#out+1]=line para=unicode.sub(para,unicode.len(line)+1):gsub("^%s+","") end end return out end function M.open() fs.makeDirectory(DIR) local st={notes={},sel=nil} local win=Y.newWindow{app="notes",title="Notes",w=92,h=30,minW=40,minH=12} local function reload() st.notes={} for n in fs.list(DIR)do if n:match("%.txt$")then local f=io.open(fs.concat(DIR,n)) local text=f and f:read("*a")or"" if f then f:close()end local color=tonumber(text:match("^#color (%x+)\n")or"",16) text=text:gsub("^#color %x+\n","") st.notes[#st.notes+1]={file=n,text=text,color=color or COLORS[1]} end end table.sort(st.notes,function(a,b)return a.file>b.file end) end local function save(n) local f=io.open(fs.concat(DIR,n.file),"w") if f then f:write(string.format("#color %06X\n",n.color),n.text);f:close()end end local function newNote() local name=string.format("%d.txt",math.floor(Y.now())) while fs.exists(fs.concat(DIR,name))do name=name:gsub("%.txt$","_.txt")end local n={file=name,text="",color=COLORS[(#st.notes%#COLORS)+1]} save(n) reload() for i,m in ipairs(st.notes)do if m.file==name then st.sel=i end end st.edit={text="",cur=0} end function win.header(w,hw) ui.button(w,2,1,"+ New Note",newNote,"suggested") end function win.draw(w,cw,ch) local t=Y.theme local NW,NH=28,9 local cols=math.max(1,(cw-2)//(NW+2)) if#st.notes==0 then local s="No notes yet. Press “+ New Note”." gfx.text((cw-unicode.len(s))//2,ch//2,s,t.dim) end for i,n in ipairs(st.notes)do local c,r=(i-1)%cols,(i-1)//cols local x,y=2+c*(NW+2),2+r*(NH+2) if y+NH>ch+1 then break end local editing=st.sel==i ui.card(x,y,NW,NH,n.color,true) local text=editing and st.edit.text or n.text local lines=wrap(text,NW-2) for li=1,math.min(#lines,NH-1)do gfx.text(x+1,y+li-1,lines[li],0x2D2D2D,n.color)end if editing then local last=lines[#lines]or"" local ly=math.min(#lines,NH-1) gfx.cell(x+1+unicode.len(last),y+math.max(ly,1)-1,"▏",0x2D2D2D,n.color) end for k,col in ipairs(COLORS)do gfx.text(x+k*2-1,y+NH-1,"●",col==n.color and 0x2D2D2D or col,n.color) ui.hit(w,x+k*2-1,y+NH-1,1,1,function()n.color=col;save(n)end) end gfx.text(x+NW-2,y+NH-1,"×",0x5A5A5A,n.color) ui.hit(w,x+NW-3,y+NH-1,3,1,function() Y.dialog{title="Delete this note?",buttons={{"Cancel"},{"Delete",function() fs.remove(fs.concat(DIR,n.file));st.sel=nil;reload()end,"destructive"}}} end) local idx=i ui.hit(w,x,y,NW,NH-1,function() if st.sel and st.notes[st.sel]then st.notes[st.sel].text=st.edit.text;save(st.notes[st.sel])end st.sel=idx st.edit={text=n.text,cur=unicode.len(n.text)} end) end end function win.key(w,char,code) if not st.sel or not st.notes[st.sel]then return end local e=st.edit local K=Y.K if code==K.enter then e.text=e.text.."\n" elseif code==K.back then e.text=unicode.sub(e.text,1,-2) elseif code==K.esc then st.notes[st.sel].text=e.text;save(st.notes[st.sel]);st.sel=nil return elseif char and char>=32 then e.text=e.text..unicode.char(char)end st.notes[st.sel].text=e.text st.dirty=true end function win.paste(w,s)if st.sel then st.edit.text=st.edit.text..s;st.notes[st.sel].text=st.edit.text;st.dirty=true end end win.tickInterval=3 function win.tick()if st.dirty and st.sel and st.notes[st.sel]then save(st.notes[st.sel]);st.dirty=false end end function win.cleanup()if st.sel and st.notes[st.sel]then save(st.notes[st.sel])end end reload() return win end return M