jott/jott

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()