#!/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 2>&1") -- local key = io.read(1) -- os.execute("stty -cbreak /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()