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