377 lines
12 KiB
Python
377 lines
12 KiB
Python
import datetime
|
|
import shutil
|
|
import sqlite3
|
|
import sys
|
|
import textwrap
|
|
|
|
try:
|
|
import colorama
|
|
colorama.init()
|
|
HAS_COLORAMA = True
|
|
except:
|
|
HAS_COLORAMA = False
|
|
|
|
SQL_ID = 0
|
|
SQL_TODOTABLE = 1
|
|
SQL_CREATED = 2
|
|
SQL_MESSAGE = 3
|
|
|
|
HELP_FULL = [
|
|
('> toddo', 'Display the todos from the current table'),
|
|
('> toddo all', 'Display the todos from all tables'),
|
|
('> toddo 4', 'Display the todo with ID 4'),
|
|
('> toddo add', 'Add a new todo via multi-line typing prompt'),
|
|
('> toddo add "message"', 'Add a new todo with this message'),
|
|
('> toddo remove 8', 'Remove the todo with ID 8'),
|
|
('> toddo table', 'Display the name of the current table'),
|
|
('> toddo table name', 'Switch to the table named "name"')
|
|
]
|
|
|
|
HELP_NOENTRIES = '''Your todo list is empty!
|
|
Use `toddo add` or `toddo add "message"` to make entries.'''
|
|
|
|
HELP_NOACTIVE = '''Table `%s` has no entries!
|
|
Use `toddo all` to see if there are entries for other tables.'''
|
|
|
|
HELP_REMOVE = '''Provide an ID number to remove.'''
|
|
|
|
# The newline at the top of this message is intentional
|
|
DISPLAY_INDIVIDUAL = '''
|
|
ID: _id_
|
|
Table: _table_
|
|
Created: _human_
|
|
Message: _message_'''
|
|
|
|
class ToddoExc(Exception):
|
|
pass
|
|
|
|
class Toddo():
|
|
def __init__(self, dbname='C:/git/else/toddo/toddo.db'):
|
|
self.dbname = dbname
|
|
self._sql = None
|
|
self._cur = None
|
|
|
|
@property
|
|
def sql(self):
|
|
if self._sql is None:
|
|
self._sql = sqlite3.connect(self.dbname)
|
|
return self._sql
|
|
|
|
@property
|
|
def cur(self):
|
|
if self._cur is None:
|
|
self._cur = self.sql.cursor()
|
|
self._cur.execute('CREATE TABLE IF NOT EXISTS meta(key TEXT, val TEXT)')
|
|
self._cur.execute('CREATE TABLE IF NOT EXISTS todos(id INT, todotable TEXT, created INT, message TEXT)')
|
|
self._cur.execute('CREATE INDEX IF NOT EXISTS todoindex on todos(id)')
|
|
return self._cur
|
|
|
|
def _install_default_lastid(self):
|
|
self.cur.execute('SELECT val FROM meta WHERE key == "lastid"')
|
|
f = cur.fetchone()
|
|
if f is not None:
|
|
return int(f[0])
|
|
self.cur.execute('INSERT INTO meta VALUES("lastid", 1)')
|
|
self.sql.commit()
|
|
return 1
|
|
|
|
def _install_default_todotable(self):
|
|
self.cur.execute('SELECT val FROM meta WHERE key == "todotable"')
|
|
f = cur.fetchone()
|
|
if f is not None:
|
|
return f[0]
|
|
self.cur.execute('INSERT INTO meta VALUES("todotable", "default")')
|
|
self.sql.commit()
|
|
return 'default'
|
|
|
|
def add_todo(self, message=None):
|
|
'''
|
|
Create new entry in the database on the active todotable.
|
|
'''
|
|
if message is None:
|
|
message = multi_line_input()
|
|
message = str(message)
|
|
if message is '':
|
|
raise ToddoExc('Todos cannot be blank.')
|
|
|
|
todoid = self.increment_lastid()
|
|
todotable = self.get_todotable()
|
|
created = int(datetime.datetime.now(datetime.timezone.utc).timestamp())
|
|
|
|
self.cur.execute('INSERT INTO todos VALUES(?, ?, ?, ?)', [todoid, todotable, created, message])
|
|
self.sql.commit()
|
|
return todoid
|
|
|
|
def remove_todo(self, idnumber):
|
|
'''
|
|
Drop todo from the database.
|
|
'''
|
|
idnumber = int(idnumber)
|
|
self.cur.execute('SELECT * FROM todos WHERE id=?', [idnumber])
|
|
todo = self.cur.fetchone()
|
|
if todo is None:
|
|
raise ToddoExc('Todo %d does not exist.' % idnumber)
|
|
activetable = self.get_todotable()
|
|
requestedtable = todo[SQL_TODOTABLE]
|
|
if requestedtable.lower() != activetable.lower():
|
|
raise ToddoExc('Todo %d is not part of the active table `%s`. It belongs to `%s`.' % (idnumber, activetable, requestedtable))
|
|
print(self.display_one_todo(idnumber))
|
|
self.cur.execute('DELETE FROM todos WHERE id=?', [idnumber])
|
|
self.sql.commit()
|
|
return idnumber
|
|
|
|
def display_one_todo(self, idnumber):
|
|
'''
|
|
Make a nice display that shows a todo's entire contents.
|
|
'''
|
|
self.cur.execute('SELECT * FROM todos WHERE id=?', [idnumber])
|
|
todo = self.cur.fetchone()
|
|
if todo is None:
|
|
raise ToddoExc('Todo %d does not exist.' % idnumber)
|
|
|
|
message = todo[SQL_MESSAGE]
|
|
messageleft = len('Message: ')
|
|
width = shutil.get_terminal_size()[0] - (messageleft + 1)
|
|
message = nicewrap(message, width, messageleft)
|
|
|
|
output = DISPLAY_INDIVIDUAL
|
|
output = output.replace('_id_', str(todo[SQL_ID]))
|
|
output = output.replace('_table_', todo[SQL_TODOTABLE])
|
|
output = output.replace('_human_', human(todo[SQL_CREATED]))
|
|
output = output.replace('_message_', message)
|
|
|
|
return output
|
|
|
|
def display_active_todos(self):
|
|
'''
|
|
Pass the active table name into display_todos_from_table
|
|
'''
|
|
todotable = self.get_todotable()
|
|
return self.display_todos_from_table(todotable)
|
|
|
|
def display_todos_from_table(self, todotable):
|
|
'''
|
|
Make a nice display from the database.
|
|
'''
|
|
if todotable is None:
|
|
self.cur.execute('SELECT * FROM todos ORDER BY id ASC')
|
|
else:
|
|
self.cur.execute('SELECT * FROM todos WHERE todotable=? ORDER BY id ASC', [todotable])
|
|
todos = self.cur.fetchall()
|
|
if len(todos) == 0:
|
|
return None
|
|
|
|
todos = [list(x) for x in todos]
|
|
|
|
longest_id = max(len(str(x[SQL_ID])) for x in todos)
|
|
longest_table = max(len(str(x[SQL_TODOTABLE])) for x in todos)
|
|
|
|
display = []
|
|
for todo in todos:
|
|
todoid = str(todo[SQL_ID])
|
|
todoid = (' '*(longest_id-len(todoid))) + todoid
|
|
|
|
timestamp = human(todo[SQL_CREATED])
|
|
|
|
todotable = todo[SQL_TODOTABLE]
|
|
todotable = (' '*(longest_table-len(todotable))) + todotable
|
|
|
|
message = todo[SQL_MESSAGE]
|
|
if '\n' in message:
|
|
message = message.split('\n')[0] + ' ...'
|
|
|
|
terminal_width = shutil.get_terminal_size()[0]
|
|
total = '%s : %s : %s' % (timestamp, todoid, message)
|
|
space_remaining = terminal_width - len(total)
|
|
if len(total) > terminal_width:
|
|
total = total[:(terminal_width-(len(total)+4))] + '...'
|
|
display.append(total)
|
|
|
|
return '\n'.join(display)
|
|
|
|
def get_todotable(self):
|
|
self.cur.execute('SELECT val FROM meta WHERE key="todotable"')
|
|
todotable = self.cur.fetchone()
|
|
if todotable is None:
|
|
self._install_default_todotable()
|
|
todotable = 'default'
|
|
else:
|
|
todotable = todotable[0]
|
|
return todotable
|
|
|
|
def increment_lastid(self, increment=False):
|
|
'''
|
|
Increment the lastid in the meta table, THEN return it.
|
|
'''
|
|
self.cur.execute('SELECT val FROM meta WHERE key="lastid"')
|
|
lastid = self.cur.fetchone()
|
|
if lastid is None:
|
|
self._install_default_lastid()
|
|
return 1
|
|
else:
|
|
lastid = int(lastid[0]) + 1
|
|
self.cur.execute('UPDATE meta SET val=? WHERE key="lastid"', [lastid])
|
|
return lastid
|
|
|
|
def switch_todotable(self, newtable=None):
|
|
'''
|
|
Update the meta with `newtable` as the new active todotable.
|
|
'''
|
|
self.cur.execute('SELECT val FROM meta WHERE key="todotable"')
|
|
activetable = self.cur.fetchone()
|
|
if not activetable:
|
|
activetable = self._install_default_todotable()
|
|
else:
|
|
activetable = activetable[0]
|
|
if newtable is None:
|
|
return activetable
|
|
self.cur.execute('UPDATE meta SET val=? WHERE key="todotable"', [newtable])
|
|
self.sql.commit()
|
|
return newtable
|
|
|
|
def colorama_print(text):
|
|
alternator = False
|
|
terminal_size = shutil.get_terminal_size()[0]
|
|
for line in text.split('\n'):
|
|
line = line.ljust(terminal_size, ' ')
|
|
if HAS_COLORAMA:
|
|
if alternator:
|
|
sys.stdout.write(colorama.Fore.BLACK)
|
|
sys.stdout.write(colorama.Back.WHITE)
|
|
else:
|
|
sys.stdout.write(colorama.Fore.WHITE)
|
|
sys.stdout.write(colorama.Back.BLACK)
|
|
alternator = not alternator
|
|
# \r because the ljust puts us on the next line, no need for \n
|
|
print(line, end='\r', flush=True)
|
|
if HAS_COLORAMA:
|
|
sys.stdout.write(colorama.Back.RESET)
|
|
sys.stdout.write(colorama.Fore.RESET)
|
|
sys.stdout.flush()
|
|
|
|
def human(timestamp):
|
|
timestamp = datetime.datetime.utcfromtimestamp(timestamp)
|
|
timestamp = datetime.datetime.strftime(timestamp, '%d %b %Y %H:%M')
|
|
return timestamp
|
|
|
|
def multi_line_input():
|
|
print('Submit a ctrl+z to finish typing.')
|
|
userinput = ''
|
|
ctrlz = '\x1a'
|
|
while True:
|
|
try:
|
|
additional = input('- ')
|
|
except EOFError:
|
|
# If you only enter a ctrlz
|
|
return userinput
|
|
|
|
if ctrlz in additional:
|
|
additional = additional.split(ctrlz)[0]
|
|
userinput += additional
|
|
break
|
|
|
|
userinput += additional + '\n'
|
|
|
|
return userinput.strip()
|
|
|
|
def nicewrap(message, width, paddingleft):
|
|
# http://stackoverflow.com/a/26538082
|
|
message = message.split('\n')
|
|
message = [
|
|
textwrap.wrap(
|
|
line,
|
|
width,
|
|
break_long_words=True,
|
|
replace_whitespace=False,
|
|
)
|
|
for line in message
|
|
]
|
|
message = ['\n'.join(line) for line in message]
|
|
message = '\n'.join(message)
|
|
|
|
message = message.strip()
|
|
message = message.replace('\n', '\n' + (' '*paddingleft))
|
|
return message
|
|
|
|
def fullhelp():
|
|
longestleft = max(len(x[0]) for x in HELP_FULL)
|
|
width = shutil.get_terminal_size()[0] - 1
|
|
message = []
|
|
for item in HELP_FULL:
|
|
pad = width - (longestleft+ 3)
|
|
item = '%s : %s' % (item[0] + (' '*(longestleft - len(item[0]))), nicewrap(item[1], pad, longestleft + 3))
|
|
message.append(item)
|
|
message = '\n'.join(message)
|
|
return message
|
|
|
|
|
|
if __name__ == '__main__':
|
|
toddo = Toddo()
|
|
|
|
# Look, no more IndexErrors
|
|
sys.argv += [None]*10
|
|
|
|
if isinstance(sys.argv[1], str):
|
|
sys.argv[1] = sys.argv[1].lower()
|
|
|
|
if sys.argv[1] is None:
|
|
message = toddo.display_active_todos()
|
|
if message is None:
|
|
table = toddo.get_todotable()
|
|
print(HELP_NOACTIVE % table)
|
|
else:
|
|
colorama_print(message)
|
|
|
|
elif sys.argv[1] == 'all':
|
|
message = toddo.display_todos_from_table(None)
|
|
if message is None:
|
|
print(HELP_NOENTRIES)
|
|
else:
|
|
colorama_print(message)
|
|
|
|
elif sys.argv[1] == 'add':
|
|
args = list(filter(None, sys.argv))
|
|
args = args[2:]
|
|
args = ' '.join(args)
|
|
if args == '':
|
|
args = None
|
|
message = toddo.add_todo(args)
|
|
if isinstance(message, int):
|
|
print('Added %d' % message)
|
|
|
|
elif sys.argv[1] == 'remove':
|
|
idnumber = sys.argv[2]
|
|
if idnumber is None or not idnumber.replace(',', '').isdigit():
|
|
print(HELP_REMOVE)
|
|
else:
|
|
message = []
|
|
ids = [int(x) for x in idnumber.split(',')]
|
|
for x in ids:
|
|
try:
|
|
t = toddo.remove_todo(x)
|
|
message.append('Removed %d' % t)
|
|
except ToddoExc as e:
|
|
message.append(e.args[0])
|
|
print('\n'.join(message))
|
|
|
|
elif sys.argv[1] == 'table':
|
|
currenttable = toddo.get_todotable()
|
|
message = toddo.switch_todotable(sys.argv[2])
|
|
if currenttable == message:
|
|
print('You are on table `%s`' % message)
|
|
else:
|
|
print('Switched to table `%s`' % message)
|
|
|
|
elif sys.argv[1].isdigit():
|
|
try:
|
|
message = toddo.display_one_todo(int(sys.argv[1]))
|
|
print(message)
|
|
except ToddoExc as e:
|
|
print(e.args[0])
|
|
|
|
elif sys.argv[1] == 'help':
|
|
print(fullhelp())
|
|
|
|
else:
|
|
print('Command not recognized.')
|
|
print(fullhelp())
|