434 lines
9.7 KiB
Lua
Executable File
434 lines
9.7 KiB
Lua
Executable File
#!/usr/bin/env lua
|
|
|
|
|
|
--___________--
|
|
--> GLOBALS <--
|
|
---------------
|
|
|
|
filepath = os.getenv('HOME') .. '/.notes/'
|
|
Note = {}
|
|
Note.__index = Note
|
|
setmetatable(Note, {__call = function(cls, ...) return cls:new() end})
|
|
Note.editor = os.getenv('VISUAL') or os.getenv('EDITOR') or 'vim'
|
|
current_notes = {}
|
|
term_width = 80
|
|
|
|
--______________--
|
|
--> Main Calls <--
|
|
------------------
|
|
|
|
function mainloop()
|
|
term_width = get_term_width()
|
|
local key = string.lower(getch())
|
|
io.write('\27[100D\27[1A\27[K')
|
|
if key == 'q' then os.exit(0)
|
|
elseif key == 'a' then add_note()
|
|
elseif key == 'l' then list_notes()
|
|
elseif key == 'v' then get_note_id('view',view_note)
|
|
elseif key == 'd' then get_note_id('delete', delete_note)
|
|
elseif key == 'e' then get_note_id('edit', edit_note)
|
|
end
|
|
end
|
|
|
|
function add_note()
|
|
local note = Note()
|
|
if note then
|
|
io.write('Save note (y/n)? ')
|
|
local subkey = string.lower(getch())
|
|
if subkey ~= 'y' then
|
|
return list_notes()
|
|
end
|
|
local s, e = note:save_local()
|
|
if s then
|
|
table.insert(current_notes, note)
|
|
list_notes()
|
|
else
|
|
print(e)
|
|
end
|
|
else
|
|
list_notes()
|
|
end
|
|
end
|
|
|
|
function view_note(num)
|
|
clear()
|
|
display_header()
|
|
|
|
print('Title : \27[3m' ..current_notes[num].title .. '\27[m')
|
|
print('Note id : ' .. tostring(num))
|
|
print('Priority : ' .. current_notes[num].priority .. '\n\n')
|
|
local wrapped_content = wrap_text(current_notes[num].content, term_width)
|
|
print(wrapped_content)
|
|
display_footer()
|
|
end
|
|
|
|
function edit_note(num)
|
|
-- TODO
|
|
-- add a check for save and revert if check fails
|
|
local content = current_notes[num].content
|
|
current_notes[num]:edit_content()
|
|
current_notes[num]:save_local()
|
|
list_notes()
|
|
end
|
|
|
|
function delete_note(num)
|
|
local s = '\nAre you sure you want to delete note %q (y/n)? '
|
|
s = string.format(s, current_notes[num].title)
|
|
s = wrap_text(s, term_width)
|
|
io.write(s)
|
|
res = string.lower(getch())
|
|
if not res or res ~= 'y' then return list_notes() end
|
|
local d = os.execute('rm ' .. filepath .. current_notes[num].time .. '.no')
|
|
table.remove(current_notes, num)
|
|
list_notes()
|
|
end
|
|
|
|
function order_notes()
|
|
-- pass
|
|
end
|
|
|
|
function show_help()
|
|
-- pass
|
|
end
|
|
|
|
function list_notes()
|
|
term_width = get_term_width()
|
|
clear()
|
|
display_header()
|
|
if #current_notes < 1 then
|
|
print('\n\n' .. string.rep(' ', 24) .. 'You have not taken any notes yet\n\n')
|
|
else
|
|
for i, note in ipairs(current_notes) do
|
|
-- TODO add priority to the list view, maybe sort?
|
|
print(string.format('%4d) %-' .. tostring(term_width - 19) .. 's %s', i, note.title ,os.date('%d %b %Y', note.time)))
|
|
end
|
|
end
|
|
display_footer()
|
|
end
|
|
|
|
function get_note_id(action, fn)
|
|
local num
|
|
while true do
|
|
io.write(string.format('\nWhich note would you like to %s? ', action))
|
|
num = tonumber(io.read())
|
|
if num and num > 0 and num <= #current_notes then break
|
|
elseif num then print('Entry must be a valid note id')
|
|
else return list_notes() end
|
|
end
|
|
return fn(num)
|
|
end
|
|
|
|
|
|
--___________--
|
|
--> HELPERS <--
|
|
---------------
|
|
|
|
function retrieve_from_remote()
|
|
--
|
|
end
|
|
|
|
function push_to_remote()
|
|
--
|
|
end
|
|
|
|
function get_notes()
|
|
local dir = io.popen('ls ' .. filepath)
|
|
while true do
|
|
local fname = dir:read()
|
|
if not fname then break end
|
|
local n = Note:create(fname)
|
|
if n then table.insert(current_notes, n) end
|
|
end
|
|
end
|
|
|
|
function display_header()
|
|
local pencil = [[
|
|
|
|
|
|
,----,---,---------------------------------------.
|
|
| |||||______________________________________/ `.
|
|
| ||||| _ . | >
|
|
:____;|_|;___|_|_|_______________________________\.`
|
|
]]
|
|
if term_width > 68 then
|
|
print(pencil)
|
|
end
|
|
print('\n' .. string.rep('*', term_width / 2 - 3) .. ' jott ' .. string.rep('*', term_width / 2 - 3) .. '\n\n' )
|
|
end
|
|
|
|
function display_footer()
|
|
local bar = '--- (L)ist, (V)iew, (A)dd, (E)dit, (D)elete ' .. string.rep('-', term_width - 64) .. ' (H)elp, (Q)uit --> '
|
|
local shortbar = string.rep('-', term_width - 20) .. ' (H)elp, (Q)uit --> '
|
|
local bar_to_use = term_width >= 70 and bar or shortbar
|
|
print('\n')
|
|
print(bar_to_use)
|
|
end
|
|
|
|
function clear()
|
|
io.write(string.char(27) .. '[1;1H ' .. string.char(27) .. '[0J')
|
|
end
|
|
|
|
|
|
--______________--
|
|
--> NOTE CLASS <--
|
|
------------------
|
|
|
|
function Note:create(name)
|
|
self = setmetatable({}, Note)
|
|
local fd
|
|
if name then
|
|
fd, err = self.parse_file(name)
|
|
if not fd then return false end
|
|
else
|
|
fd = {}
|
|
end
|
|
|
|
self.title = fd.title or ''
|
|
self.content = fd.content or ''
|
|
self.priority = fd.priority or ''
|
|
self.time = tonumber(fd.time) or os.time()
|
|
return self
|
|
end
|
|
|
|
function Note:set_title()
|
|
self.title = ''
|
|
while true do
|
|
io.write('Title : ')
|
|
self.title = io.read()
|
|
if #self.title > 60 then
|
|
print('Titles must be between 1 and 60 characters')
|
|
else
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
function Note:set_priority()
|
|
self.priority = nil
|
|
while not self.priority do
|
|
io.write('Priority (1-10): ')
|
|
self.priority = tonumber(io.read())
|
|
if not self.priority then
|
|
print('Priority must be between 1 and 10...')
|
|
end
|
|
end
|
|
end
|
|
|
|
function Note:set_content()
|
|
local filename = os.tmpname()
|
|
os.execute(self.editor .. " " .. filename)
|
|
if not file_exists(filename) then
|
|
return false
|
|
end
|
|
local file = io.open(filename, 'r')
|
|
local buffer
|
|
file:seek('set')
|
|
buffer = file:read('*a')
|
|
file:close()
|
|
local s, e = os.remove(filename)
|
|
if not s then
|
|
return false
|
|
end
|
|
self.content = buffer
|
|
end
|
|
|
|
function Note:edit_content()
|
|
local actual_fn = filepath .. tostring(self.time) .. '.no'
|
|
if not file_exists(actual_fn) then
|
|
return false
|
|
end
|
|
local temp_fn = os.tmpname()
|
|
local file = io.open(temp_fn, 'w')
|
|
file:seek('set')
|
|
file:write(self.content)
|
|
file:close()
|
|
os.execute(self.editor .. " " .. temp_fn)
|
|
file = io.open(temp_fn, 'r')
|
|
file:seek('set')
|
|
local buffer = file:read('*a')
|
|
file:close()
|
|
self.content = buffer
|
|
local s = os.remove(temp_fn)
|
|
if not s then
|
|
return false
|
|
end
|
|
return true
|
|
end
|
|
|
|
function Note.check_priority(num)
|
|
local num = tonumber(num)
|
|
if num and num > 0 and num < 11 then
|
|
return num
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
function Note:new()
|
|
local n = self.create()
|
|
if not n then return nil, 'There was an error adding your note :(' end
|
|
n:set_title()
|
|
if n.title == '' then return false end
|
|
n:set_priority()
|
|
n:set_content()
|
|
return n
|
|
end
|
|
|
|
function Note:save_local()
|
|
local filename = filepath .. tostring(self.time) .. '.no'
|
|
local f = assert(io.open(filename, 'w'))
|
|
if not f then
|
|
return nil, 'Could not open file: ' .. filename
|
|
end
|
|
local data = 'title|%s\ntime|%d\npriority|%s\n.\n%s'
|
|
local out = string.format(data, self.title, self.time, self.priority, self.content)
|
|
f:write(out)
|
|
f:flush()
|
|
f:close()
|
|
return true
|
|
end
|
|
|
|
function Note.parse_file(fname)
|
|
local fpath = filepath .. fname
|
|
local file = io.open(fpath, 'r')
|
|
local out = {}
|
|
local content = false
|
|
local c_out = {}
|
|
if not file then
|
|
return nil, 'Error opening file ' .. filepath
|
|
end
|
|
for line in file:lines() do
|
|
if line == '.' then
|
|
content = true
|
|
elseif not content then
|
|
local s = string.split(line, '|')
|
|
out[s[1]] = s[2]
|
|
else
|
|
table.insert(c_out, line)
|
|
end
|
|
end
|
|
out['content'] = table.concat(c_out,'\n')
|
|
|
|
return out
|
|
end
|
|
|
|
|
|
--_____________--
|
|
--> UTILITIES <--
|
|
-----------------
|
|
|
|
function build_path()
|
|
os.execute('mkdir ' .. filepath .. ' 2>/dev/null')
|
|
--os.execute('mkdir ' .. filepath .. 'content/' .. ' 2>/dev/null')
|
|
--os.execute('mkdir ' .. filepath .. 'meta/' .. ' 2>/dev/null')
|
|
end
|
|
|
|
function get_term_width()
|
|
local pop = io.popen('tput cols')
|
|
local cols = tonumber(pop:read())
|
|
pop:close()
|
|
return (cols and cols < 80) and cols or 80
|
|
end
|
|
|
|
-- function getch()
|
|
-- -- taken from: http://lua.2524044.n2.nabble.com/How-to-get-one-keystroke-without-hitting-Enter-td5858614.html
|
|
-- os.execute("stty cbreak </dev/tty >/dev/tty 2>&1")
|
|
-- local key = io.read(1)
|
|
-- os.execute("stty -cbreak </dev/tty >/dev/tty 2>&1");
|
|
-- print('\27[1D ')
|
|
|
|
-- return string.sub(key,1,1);
|
|
-- end
|
|
|
|
function getch()
|
|
os.execute("stty cbreak -echo")
|
|
local key = io.read(1)
|
|
if string.byte(key) == 27 then
|
|
io.read(2)
|
|
end
|
|
os.execute("stty -cbreak echo");
|
|
print('\27[1D ')
|
|
return key
|
|
end
|
|
|
|
function wrap_text(str,len)
|
|
local lines = string.split(str, '\n')
|
|
local out = {}
|
|
for i, line in ipairs(lines) do
|
|
if #line <= len then
|
|
table.insert(out,line)
|
|
else
|
|
local words = string.split(line, ' ')
|
|
local subout = ''
|
|
for i, word in ipairs(words) do
|
|
if #subout + #word <= len then
|
|
local spc = #subout > 0 and ' ' or ''
|
|
subout = subout .. spc .. word
|
|
if i == #words then
|
|
table.insert(out, subout)
|
|
end
|
|
else
|
|
table.insert(out, subout)
|
|
subout = word
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return table.concat(out, '\n')
|
|
end
|
|
|
|
function string.trim(str)
|
|
local pattern = '^%s*([%w%p ]-)%s*$'
|
|
return string.match(s, pattern)
|
|
end
|
|
|
|
function string.split(str, sep)
|
|
assert(str and sep, 'sting.split requires both an input string and a field separator string')
|
|
local start = 1
|
|
local t = {}
|
|
while true do
|
|
local out
|
|
local point = string.find(str, sep, start)
|
|
if point == start then
|
|
out = ''
|
|
elseif not point then
|
|
out = string.sub(str, start)
|
|
else
|
|
out = string.sub(str, start, point - 1)
|
|
end
|
|
table.insert(t, out)
|
|
start = point and point + 1 or nil
|
|
if not start then
|
|
break
|
|
end
|
|
end
|
|
return t
|
|
end
|
|
|
|
function file_exists(fn)
|
|
local file = io.open(fn, 'r')
|
|
if file ~= nil then
|
|
file:close()
|
|
return true
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
|
|
--________--
|
|
--> INIT <--
|
|
------------
|
|
|
|
function init()
|
|
term_width = get_term_width()
|
|
build_path()
|
|
get_notes()
|
|
list_notes()
|
|
while true do
|
|
mainloop()
|
|
end
|
|
end
|
|
|
|
init()
|