├── MANIFEST.in ├── requirements.txt ├── bin └── keepcli ├── cover ├── simple_demo.gif └── browser_demo.gif ├── keepcli ├── __init__.py ├── version.py ├── kcliparser.py └── keep.py ├── .gitignore ├── CHANGES.md ├── setup.py ├── License.txt └── README.md /MANIFEST.in: -------------------------------------------------------------------------------- 1 | *.md 2 | *.txt 3 | cover/* 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | gkeepapi 2 | setuptools 3 | termcolor 4 | PyYAML 5 | -------------------------------------------------------------------------------- /bin/keepcli: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from keepcli import keep 3 | keep.cli() 4 | -------------------------------------------------------------------------------- /cover/simple_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgckind/keepcli/HEAD/cover/simple_demo.gif -------------------------------------------------------------------------------- /cover/browser_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgckind/keepcli/HEAD/cover/browser_demo.gif -------------------------------------------------------------------------------- /keepcli/__init__.py: -------------------------------------------------------------------------------- 1 | from .version import __version__ 2 | from .keep import * 3 | 4 | __all__ = ["keep", "version"] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | keepcli/keepcli 2 | *.pyc 3 | keepcli/__pycache__ 4 | keepcli/*.pyc 5 | keepcli.egg-info/ 6 | keepcli/update_version.sh 7 | RELEASE_NOTES 8 | HELP 9 | -------------------------------------------------------------------------------- /keepcli/version.py: -------------------------------------------------------------------------------- 1 | """keepcli version""" 2 | 3 | commit = '0478909' 4 | 5 | dev = 'dev-{}'.format(commit) 6 | version_tag = (1, 0, 1) 7 | __version__ = '.'.join(map(str, version_tag[:3])) 8 | 9 | if len(version_tag) > 3: 10 | __version__ = '%s-%s' % (__version__, version_tag[3]) 11 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Changes 2 | 3 | ## v1.0.1 4 | #### 2018-AUG-02 5 | - Add gif examples 6 | - Bugfixes 7 | - Add autocompletion to Move items to list 8 | - Delete all checked items with `deleteItem --all-checked` 9 | - Pin cards with `current pin` 10 | - add `elp` shorcut and `--pinned` option to show all pinned objects only 11 | 12 | 13 | ## v1.0.0 14 | #### 2018-JUL-23 15 | - Initial Release 16 | - Plenty of TODOs 17 | -------------------------------------------------------------------------------- /keepcli/kcliparser.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import sys 3 | from .version import __version__ 4 | 5 | 6 | class MyParser(argparse.ArgumentParser): 7 | def error(self, message): 8 | print('\n****************************************') 9 | sys.stderr.write('Error: %s \n' % message) 10 | print('\n****************************************') 11 | self.print_help() 12 | sys.exit(2) 13 | 14 | 15 | def get_args(): 16 | parser = MyParser( 17 | description='keepcli is a interactive command line tool for the unofficial Google Keep API') 18 | parser.add_argument("-v", "--version", action="store_true", 19 | help="print version number and exit") 20 | parser.add_argument("-o", "--offline", action="store_true", 21 | help="Run in offline mode (need to dump the data in advance)") 22 | args = parser.parse_args() 23 | 24 | if args.version: 25 | print('\nCurrent version: {}'.format(__version__)) 26 | sys.exit() 27 | 28 | return args 29 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | try: 4 | from setuptools import setup, find_packages 5 | except ImportError: 6 | from distutils.core import setup 7 | 8 | prjdir = os.path.dirname(__file__) 9 | __version__ = '' 10 | 11 | 12 | def read(filename): 13 | return open(os.path.join(prjdir, filename)).read() 14 | 15 | 16 | exec(open('keepcli/version.py').read()) 17 | 18 | try: 19 | with open('requirements.txt') as f: 20 | required = f.read().splitlines() 21 | except: 22 | required = [] 23 | 24 | try: 25 | pkgs = find_packages() 26 | except NameError: 27 | pkgs = ['keepcli'] 28 | 29 | try: 30 | import pypandoc 31 | long_description = pypandoc.convert('README.md', 'rst') 32 | except(IOError, ImportError): 33 | long_description = open('README.md').read() 34 | 35 | setup( 36 | name='keepcli', 37 | version=__version__, 38 | author='Matias Carrasco Kind', 39 | author_email='mgckind@gmail.com', 40 | scripts=['bin/keepcli'], 41 | packages=pkgs, 42 | license='LICENSE.txt', 43 | description='Simple unofficial Google Keep Interactive Command Line Interpreter', 44 | long_description=long_description, 45 | url='https://github.com/mgckind/keepcli', 46 | install_requires=required, 47 | ) 48 | -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | University of Illinois/NCSA Open Source License 2 | 3 | Copyright (c) 2018 University of Illinois at Urbana-Champaign 4 | All rights reserved. 5 | 6 | Developed by: Matias Carrasco Kind 7 | NCSA/UIUC 8 | https://github.com/mgckind/keepcli 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal with the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 11 | 12 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimers. 13 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimers in the documentation and/or other materials provided with the distribution. 14 | Neither the names of Matias Carrasco Kind, University of Illinois at Urbana-Champaign and NCSA, nor the names of its contributors may be used to endorse or promote products derived from this Software without specific prior written permission. 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # keepcli latest release License pypi version [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.2553684.svg)](https://doi.org/10.5281/zenodo.2553684) 2 | 3 | 4 | A simple and unofficial interactive command line interpreter client for [Google Keep](https://keep.google.com/). 5 | 6 | It uses the nice unofficial API client [gkeepapi](https://github.com/kiwiz/gkeepapi) 7 | 8 | As described also in that repository, *keepcli* is not supported nor endorsed by Google. 9 | 10 | ## Examples 11 | 12 | ![simple_demo](cover/simple_demo.gif) 13 | 14 | Let's see how everything is synchronized with the web interface. 15 | 16 | ![browser_demo](cover/browser_demo.gif) 17 | 18 | 19 | 20 | ## Development 21 | 22 | I wanted a simple cli tool that can easily be in sync with multiple devices and although there are alternatives like [todo.txt](http://todotxt.org/) which is an awesome tool, I prefered to keep my quick TODO's and notes in Google Keep, however there is not a public API for it nor a similar cli (given my shallow search). 23 | 24 | As a very active [Google Keep](https://keep.google.com/) user (among other Notes-taking tools) I developed this for my own needs during some free time, mainly to add, check/uncheck and move TODO items from various lists and add notes quickly when working from a terminal screen which is very often. It's been very productive to get things done and have multiple TODO list up to date and move items between them among others, it has a nice TAB autocompletion for some of the useful commands. Might be useful for others in a similar situation. I also keep track of dailiy activities, and things to do during the current and next week, moving items and pinning notes. 25 | 26 | Some offline features have not tested in too much detail and there might be some hidden issues, this is note a production-grade tool and is distributed 'as is'. 27 | 28 | I'll keep adding features as they are needed, based on my free time, contributions are welcome. 29 | 30 | ## Current version 31 | 32 | **keepcli 1.0.1** 33 | 34 | ## Installation 35 | 36 | Uses `python3` 37 | 38 | For development version: 39 | 40 | pip install git+https://github.com/mgckind/keepcli.git 41 | 42 | or latest version: 43 | 44 | pip install keepcli 45 | 46 | Its better to set up the App Password for Keep [here](https://myaccount.google.com/apppasswords) to authenticate. 47 | 48 | ## Usage 49 | 50 | 51 | From a terminal window: 52 | 53 | keepcli 54 | 55 | Inside `keepcli` you can type `help` to see a list of useful commands: 56 | 57 | keepcli [] ~> help 58 | 59 | ## Some Features 60 | 61 | - TAB autocompletion 62 | - Keep constant sync with Google Keep 63 | - Create list/note with `create` 64 | - Change card color 65 | - Pin/Unpin card 66 | - add/check/uncheck/delete items from a list 67 | - move items from a list to another 68 | - dump/load Google Keep entries for offline work 69 | - add text to Notes 70 | - useful shorcuts 71 | - show checked/unchecked items from lists 72 | 73 | ## Wishlist 74 | 75 | - Add labels 76 | - Add reminders 77 | - Add ability to change users 78 | - Add sub-items to list 79 | - Many more 80 | -------------------------------------------------------------------------------- /keepcli/keep.py: -------------------------------------------------------------------------------- 1 | import cmd 2 | import sys 3 | import os 4 | import getpass 5 | import pickle 6 | import argparse 7 | import gkeepapi 8 | import yaml 9 | import keepcli.kcliparser as kcliparser 10 | from keepcli.version import __version__ 11 | 12 | try: 13 | input = raw_input 14 | except NameError: 15 | pass 16 | 17 | 18 | def without_color(line, color, mode=0): 19 | """This function does nothing to the input line. 20 | Just a ancillary function when there is not output color""" 21 | return line 22 | 23 | 24 | try: 25 | from termcolor import colored as with_color 26 | 27 | def colored(line, color, mode=1): 28 | if mode == 1: 29 | return with_color(line, color) 30 | else: 31 | return line 32 | except ImportError: 33 | colored = without_color 34 | 35 | colors = { 36 | 'gray': 'grey', 37 | 'red': 'red', 38 | 'green': 'green', 39 | 'yellow': 'yellow', 40 | 'darkblue': 'blue', 41 | 'purple': 'magenta', 42 | 'blue': 'cyan', 43 | 'white': 'white'} 44 | 45 | colorsGK = { 46 | 'red': gkeepapi.node.ColorValue.Red, 47 | 'green': gkeepapi.node.ColorValue.Green, 48 | 'gray': gkeepapi.node.ColorValue.Gray, 49 | 'white': gkeepapi.node.ColorValue.White, 50 | 'yellow': gkeepapi.node.ColorValue.Yellow, 51 | } 52 | 53 | options_entries = ['all', 'notes', 'lists'] 54 | options_commands = ['note', 'list'] 55 | options_current = ['show', 'color', 'pin', 'unpin'] 56 | options_config = ['set'] 57 | true_options = ['true', 'yes', '1', 'y', 't'] 58 | 59 | 60 | def print_list(List, mode, only_unchecked=False): 61 | """ 62 | Prints out checked followed by unchecked items from a list sorted by time of creation 63 | 64 | Parameters 65 | ---------- 66 | List : gkeepapi.node.List 67 | The input list class 68 | mode : int 69 | mode to be used by colored to whether (mode=1) or not mode=0) use termcolor 70 | """ 71 | try: 72 | times_unchecked = [item.timestamps.created for item in List.unchecked] 73 | except: 74 | print('List printing is not supported without sync, please sync your data') 75 | return 76 | 77 | unchecked = [x for _, x in 78 | sorted(zip(times_unchecked, List.unchecked), key=lambda pair: pair[0])] 79 | times_checked = [item.timestamps.created for item in List.checked] 80 | checked = [x for _, x in sorted(zip(times_checked, List.checked), key=lambda pair: pair[0])] 81 | print('Unchecked items: {} out of {}'.format(len(unchecked), len(checked)+len(unchecked))) 82 | for i in unchecked: 83 | print(colored(i, "red", mode)) 84 | if only_unchecked: 85 | return 86 | for i in checked: 87 | print(colored(i, "green", mode)) 88 | 89 | 90 | def get_color(entry, mode, color_only=False): 91 | """ 92 | Get the color conversion from gkeeppii colors to termcolor colors 93 | 94 | Parameters 95 | ---------- 96 | entry : gkeepapi.node 97 | The Note class 98 | mode : int 99 | Whether to use colors (mode = 1) or not (mode = 0) 100 | color_only : bool, optional 101 | If True it just returns the colot to be used, if False it returns the colord title 102 | 103 | Returns 104 | ------- 105 | str 106 | Color string or a colored title depending on the value for color_only 107 | """ 108 | try: 109 | color = colors[entry.color.name.lower()] 110 | except KeyError: 111 | color = "white" 112 | if color_only: 113 | return color 114 | else: 115 | return colored(entry.title, color, mode) 116 | 117 | 118 | class GKeep(cmd.Cmd): 119 | """ The main cmd class""" 120 | def __init__(self, auth_file, conf_file, offline=False): 121 | # super().__init__() 122 | cmd.Cmd.__init__(self) 123 | self.do_clear(None) 124 | print('\nWelcome to keepcli {}, ' 125 | 'use help or ? to list possible commands.\n\n'.format(__version__)) 126 | self.offline = offline 127 | self.auth_file = auth_file 128 | self.conf_file = conf_file 129 | self.current = None 130 | self.update_config() 131 | self.kcli_path = os.path.dirname(self.auth_file) 132 | if self.offline: 133 | self.autosync = False 134 | self.prompt = 'keepcli [] ~> ' 135 | self.keep = gkeepapi.Keep() 136 | if not self.offline: 137 | try: 138 | with open(auth_file, 'r') as auth: 139 | conn = yaml.load(auth) 140 | except FileNotFoundError: 141 | conn = {} 142 | print('\nAuth file {} not found, will create one... ' 143 | '(Google App password is strongly recommended)\n'.format(auth_file)) 144 | conn['user'] = input('Enter username : ') 145 | conn['passwd'] = getpass.getpass(prompt='Enter password : ') 146 | print('\nLogging {} in...\n'.format(colored(conn['user'], 'green', self.termcolor))) 147 | try: 148 | self.connect = self.keep.login(conn['user'], conn['passwd']) 149 | with open(auth_file, 'w') as auth: 150 | yaml.dump(conn, auth, default_flow_style=False) 151 | self.username = conn['user'] 152 | except (gkeepapi.exception.LoginException, ValueError) as e: 153 | if e.__class__.__name__ == 'ValueError': 154 | print("\n Can't login and sync from empty content, please create a note online") 155 | else: 156 | print('\nUser/Password not valid (auth file : {})\n'.format(auth_file)) 157 | sys.exit(1) 158 | self.do_refresh(None, force_sync=True) 159 | else: 160 | print(colored('\nRunning Offline\n', "red", self.termcolor)) 161 | self.complete_ul = self.complete_useList 162 | self.complete_un = self.complete_useNote 163 | self.do_useNote(self.conf['current']) 164 | self.do_useList(self.conf['current']) 165 | self.doc_header = colored( 166 | ' *Other Commands*', "cyan", self.termcolor) + ' (type help ):' 167 | self.keep_header = colored( 168 | ' *Keep Commands*', "cyan", self.termcolor) + ' (type help ):' 169 | 170 | def update_config(self): 171 | """ Update config parameters into current session""" 172 | with open(self.conf_file, 'r') as confile: 173 | self.conf = yaml.load(confile) 174 | self.termcolor = 1 if self.conf['termcolor'] else 0 175 | self.autosync = True if self.conf['autosync'] else False 176 | 177 | def default(self, arg): 178 | print() 179 | print("Invalid command") 180 | print("Type 'help' or '?' to list available commands") 181 | print() 182 | 183 | 184 | def do_help(self, arg): 185 | """ 186 | List available commands with "help" or detailed help with "help cmd". 187 | 188 | Usage: 189 | ~> help 190 | """ 191 | if arg: 192 | try: 193 | func = getattr(self, 'help_' + arg) 194 | except AttributeError: 195 | try: 196 | doc = getattr(self, 'do_' + arg).__doc__ 197 | if doc: 198 | doc = str(doc) 199 | if doc.find('KEEP:') > -1: 200 | doc = doc.replace('KEEP:', '') 201 | self.stdout.write("%s\n" % str(doc)) 202 | return 203 | except AttributeError: 204 | pass 205 | self.stdout.write("%s\n" % str(self.nohelp % (arg,))) 206 | return 207 | func() 208 | else: 209 | self.do_clear(None) 210 | # self.stdout.write(str(self.intro) + "\n") 211 | print(colored('\n\n--------------- keepcli help ---------------\n\n', 'green', self.termcolor)) 212 | names = self.get_names() 213 | cmds_doc = [] 214 | cmds_undoc = [] 215 | cmds_keep = [] 216 | help = {} 217 | for name in names: 218 | if name[:5] == 'help_': 219 | help[name[5:]] = 1 220 | names.sort() 221 | # There can be duplicates if routines overridden 222 | prevname = '' 223 | for name in names: 224 | if name[:3] == 'do_': 225 | if name == prevname: 226 | continue 227 | prevname = name 228 | cmd = name[3:] 229 | if cmd in help: 230 | cmds_doc.append(cmd) 231 | del help[cmd] 232 | elif getattr(self, name).__doc__: 233 | doc = getattr(self, name).__doc__ 234 | if 'KEEP:' in doc: 235 | cmds_keep.append(cmd) 236 | else: 237 | cmds_doc.append(cmd) 238 | else: 239 | cmds_undoc.append(cmd) 240 | self.stdout.write("%s\n" % str(self.doc_leader)) 241 | self.print_topics(self.keep_header, cmds_keep, 80) 242 | self.print_topics(self.doc_header, cmds_doc, 80) 243 | # self.print_topics('Misc', list(help.keys()), 80) 244 | print() 245 | 246 | def print_topics(self, header, cmds, maxcol): 247 | if header is not None: 248 | if cmds: 249 | self.stdout.write("%s\n" % str(header)) 250 | if self.ruler: 251 | self.stdout.write("%s\n" % str(self.ruler * maxcol)) 252 | self.columnize(cmds, maxcol - 1) 253 | self.stdout.write("\n") 254 | 255 | def emptyline(self): 256 | """Do nothing when there is no input """ 257 | pass 258 | 259 | def do_version(self, arg): 260 | """ 261 | Print current keepcli version 262 | 263 | Usage: 264 | ~> version 265 | """ 266 | print('\nCurrent version: {}'.format(__version__)) 267 | 268 | def do_shortcuts(self, arg): 269 | """ 270 | Undocumented shortcuts used in keepcli. 271 | 272 | ul: useList --> select a list 273 | un: useNote --> select a note 274 | ai: addItem --> add item to a current List 275 | ai: addText --> add text to a current Note 276 | cs: current show --> shows current List/Note 277 | el: entries list --show --> show all unchecked items from all active lists 278 | elp: entries list --show --pinned --> show all unchecked items from all pinned lists 279 | """ 280 | self.do_help('shortcuts') 281 | 282 | def do_refresh(self, arg, force_sync=False): 283 | """ 284 | Sync and Refresh content from Google Keep 285 | 286 | Usage: 287 | ~> refresh 288 | """ 289 | sync = True if self.autosync else False 290 | if force_sync: 291 | sync = True 292 | if not self.offline: 293 | if sync: 294 | print('Syncing...') 295 | self.keep.sync() 296 | else: 297 | print(colored('Cannot sync while offline', 'red', self.termcolor)) 298 | self.entries = self.keep.all() 299 | self.titles = [] 300 | self.lists = [] 301 | self.notes = [] 302 | self.lists_obj = [] 303 | self.notes_obj = [] 304 | for n in self.entries: 305 | if not n.trashed: 306 | self.titles.append(n.title) 307 | if n.type.name == 'List': 308 | self.lists.append(n.title) 309 | self.lists_obj.append(n) 310 | if n.type.name == 'Note': 311 | self.notes.append(n.title) 312 | self.notes_obj.append(n) 313 | 314 | def do_sync(self, arg): 315 | """ 316 | Sync data with the server, it needs online access 317 | 318 | Usage: 319 | ~> sync 320 | """ 321 | self.do_refresh(None, force_sync=True) 322 | 323 | def do_whoami(self, arg): 324 | """ 325 | Print information about user 326 | 327 | Usage: 328 | ~> whoami 329 | """ 330 | print() 331 | allitem = sum([len(n.items) for n in self.lists_obj]) 332 | uncheck = sum([len(n.unchecked) for n in self.lists_obj]) 333 | print('User : {}'.format(self.username)) 334 | print('Entries : {} Notes and {} Lists'.format(len(self.notes), len(self.lists))) 335 | print('Uncheck Items: {} out of {}'.format(uncheck, allitem)) 336 | print() 337 | 338 | def do_cs(self, arg): 339 | self.do_current('show') 340 | 341 | def do_ai(self, arg): 342 | self.do_addItem(arg) 343 | 344 | def do_at(self, arg): 345 | self.do_addText(arg) 346 | 347 | def do_ul(self, arg): 348 | self.do_useList(arg) 349 | 350 | def do_un(self, arg): 351 | self.do_useNote(arg) 352 | 353 | def do_el(self, arg): 354 | self.do_entries('lists --show') 355 | 356 | def do_elp(self, arg): 357 | self.do_entries('lists --show --pinned') 358 | 359 | def do_exit(self, arg): 360 | """ 361 | Exit the program 362 | """ 363 | with open(self.conf_file, 'w') as conf: 364 | yaml.dump(self.conf, conf, default_flow_style=False) 365 | return True 366 | 367 | def do_config(self, arg): 368 | """ 369 | Print and set configuration options 370 | 371 | Usage: 372 | ~> config : shows current configuration 373 | ~> config set = : sets to and updates config file 374 | Ex: 375 | ~> config set termcolor=true : sets termcolor to true 376 | """ 377 | line = "".join(arg.split()) 378 | if arg == '': 379 | self.do_clear(None) 380 | print(colored('\n** Current configuration:\n', 'green', self.termcolor)) 381 | print('===============================') 382 | for item in self.conf.items(): 383 | print('{: <10} : {}'.format(*item)) 384 | print('===============================') 385 | print() 386 | self.do_help('config') 387 | if 'set' in line: 388 | action = line[line.startswith('set') and len('set'):].lstrip() 389 | action = "".join(action.split()) 390 | if '=' in action: 391 | key, value = action.split('=') 392 | elif ':' in action: 393 | key, value = action.split(':') 394 | else: 395 | print('format key = value') 396 | return 397 | value_b = True if value.lower() in true_options else False 398 | if key in self.conf: 399 | self.conf[key] = value_b 400 | with open(self.conf_file, 'w') as conf: 401 | yaml.dump(self.conf, conf, default_flow_style=False) 402 | self.update_config() 403 | else: 404 | print('{} is not a valid configuration option'.format(key)) 405 | 406 | def complete_config(self, text, line, start_index, end_index): 407 | if text: 408 | return [option for option in options_config if option.startswith(text)] 409 | else: 410 | return options_config 411 | 412 | def do_entries(self, arg): 413 | """ 414 | KEEP:Shows all lists and notes for the user 415 | 416 | Usage: 417 | ~> entries all : Included archived and deleted items 418 | ~> entries lists : Shows only lists 419 | ~> entries notes : Shows only notes 420 | 421 | Optional Arguments: 422 | --show : Shows all unchecked items for all Active lists 423 | --pinned : Shows only pinned entries 424 | 425 | Ex: 426 | ~> entries lists --show 427 | 428 | Note: 429 | Use shortcut el to replace entries lists --show 430 | ~> el 431 | Use shortcut elp to replace entries lists --show --pinned 432 | ~> elp 433 | """ 434 | self.do_clear(None) 435 | line = "".join(arg.split()) 436 | show = True if '--show' in line else False 437 | pinned_only = True if '--pinned' in line else False 438 | active = True 439 | notes = False 440 | lists = False 441 | if 'all' in line: 442 | active = False 443 | elif 'notes' in line: 444 | notes = True 445 | elif 'lists' in line: 446 | lists = True 447 | print() 448 | try: 449 | _ = self.entries 450 | except AttributeError: 451 | if self.offline: 452 | print('In offline mode, you need to load data first, use the load command') 453 | print() 454 | return 455 | pinned = [] 456 | unpinned = [] 457 | for n in self.entries: 458 | pinned.append(n) if n.pinned else unpinned.append(n) 459 | 460 | if len(pinned) > 0: 461 | print('* Pinned entries *: \n') 462 | for n in pinned: 463 | display = True 464 | if n.trashed: 465 | status = 'Deleted' 466 | if active or notes or lists: 467 | display = False 468 | else: 469 | status = 'Active' 470 | if notes and n.type.name == 'List': 471 | display = False 472 | if lists and n.type.name == 'Note': 473 | display = False 474 | data = {'title': get_color(n, self.termcolor), 'status': status, 'type': n.type.name} 475 | if n.type.name == 'List': 476 | data['type'] = colored(n.type.name, 'cyan', self.termcolor) 477 | if display: 478 | print('- {title: <30} {status: <10} [ {type} ]'.format(**data)) 479 | if show and lists and n.type.name == 'List': 480 | print_list(n, self.termcolor, only_unchecked=True) 481 | print() 482 | print() 483 | if len(unpinned) and not pinned_only > 0: 484 | print('* Unpinned entries *: \n') 485 | for n in unpinned: 486 | display = True 487 | if n.trashed: 488 | status = 'Deleted' 489 | if active or notes or lists: 490 | display = False 491 | else: 492 | status = 'Active' 493 | if notes and n.type.name == 'List': 494 | display = False 495 | if lists and n.type.name == 'Note': 496 | display = False 497 | data = {'title': get_color(n, self.termcolor), 'status': status, 'type': n.type.name} 498 | if n.type.name == 'List': 499 | data['type'] = colored(n.type.name, 'cyan', self.termcolor) 500 | if display: 501 | print('- {title: <30} {status: <10} [ {type} ]'.format(**data)) 502 | if show and lists and n.type.name == 'List': 503 | print_list(n, self.termcolor, only_unchecked=True) 504 | print() 505 | print() 506 | 507 | def complete_entries(self, text, line, start_index, end_index): 508 | if text: 509 | return [option for option in options_entries if option.startswith(text)] 510 | else: 511 | return options_entries 512 | 513 | def do_show(self, arg): 514 | """ 515 | KEEP:Print content os List/Note 516 | 517 | Usage: 518 | ~> show 519 | """ 520 | if arg == '' and self.current is None: 521 | self.do_help('show') 522 | return 523 | if arg == '' and self.current is not None: 524 | arg = self.current.title 525 | for n in self.entries: 526 | if arg == n.title: 527 | print() 528 | title = colored('============{:=<30}'.format(' '+n.title+' '), 529 | get_color(n, self.termcolor, True), self.termcolor) 530 | print(title) 531 | print() 532 | print(n.text) if n.type.name == 'Note' else print_list(n, self.termcolor) 533 | bottom = colored('============{:=<30}'.format(' '+n.title+' '), 534 | get_color(n, self.termcolor, True), self.termcolor) 535 | print(bottom) 536 | 537 | print() 538 | 539 | def complete_show(self, text, line, start_index, end_index): 540 | if text: 541 | return [option for option in self.titles if option.startswith(text)] 542 | else: 543 | return self.titles 544 | 545 | def do_delete(self, arg): 546 | """ 547 | KEEP:Delete entry based on its name. Works for lists and notes 548 | 549 | Usage: 550 | ~> delete 551 | """ 552 | if arg == '': 553 | self.do_help('delete') 554 | return 555 | for n in self.entries: 556 | if arg == n.title: 557 | print() 558 | question = '\nAre you sure you want to delete {} ?.\n'.format(n.title) 559 | question += 'This is irreversible [spell out yes]: ' 560 | question = colored(question, 'red', self.termcolor) 561 | if (input(question).lower() in ['yes']): 562 | print('{} Deleted'.format(n.title)) 563 | n.delete() 564 | self.do_refresh(None) 565 | print() 566 | 567 | def complete_delete(self, text, line, start_index, end_index): 568 | if text: 569 | return [option for option in self.titles if option.startswith(text)] 570 | else: 571 | return self.titles 572 | 573 | def do_current(self, arg): 574 | """ 575 | KEEP:Show current list or note being used 576 | 577 | Usage: 578 | ~> current : Prints current note/list 579 | ~> current show : Prints content of current note/list 580 | ~> current color : Change color card of entry 581 | ~> current pin : Pin current note/list 582 | ~> current unpin : Unpin current note/list 583 | 584 | Note: 585 | Use shortcut cs to current show 586 | ~> cs 587 | """ 588 | if self.current is None: 589 | print('Not Note or List is selected, use the command: useList or useNote') 590 | return 591 | print('\nCurrent entry: {}'.format(get_color(self.current, self.termcolor))) 592 | if 'show' in arg: 593 | self.do_show(self.current.title) 594 | elif 'pin' in arg: 595 | self.current.pinned = True 596 | self.do_refresh(None) 597 | elif 'unpin' in arg: 598 | self.current.pinned = False 599 | self.do_refresh(None) 600 | elif 'color' in arg: 601 | color = arg[arg.startswith('color') and len('color'):].lstrip() 602 | try: 603 | self.current.color = colorsGK[color] 604 | self.do_refresh(None) 605 | except: 606 | print('Color {} do not exist'.format(color)) 607 | else: 608 | print() 609 | self.do_help('current') 610 | 611 | def complete_current(self, text, line, start_index, end_index): 612 | if 'color' in line: 613 | if text: 614 | return [option for option in list(colorsGK.keys()) if option.startswith(text)] 615 | else: 616 | return list(colorsGK.keys()) 617 | else: 618 | if text: 619 | return [option for option in options_current if option.startswith(text)] 620 | else: 621 | return options_current 622 | 623 | def do_create(self, arg): 624 | """ 625 | KEEP:Create a note or a list 626 | 627 | Usage: 628 | ~> create note 629 | ~> create list <title> 630 | 631 | """ 632 | if arg == '': 633 | self.do_help('create') 634 | line = arg 635 | if line.startswith('note'): 636 | title = line[line.startswith('note') and len('note'):].lstrip() 637 | if title == '': 638 | print(colored('\nTitle cannot be empty\n', 'red', self.termcolor)) 639 | return 640 | print('Creating note: {}'.format(title)) 641 | self.keep.createNote(title) 642 | self.do_refresh(None) 643 | if line.startswith('list'): 644 | title = line[line.startswith('list') and len('list'):].lstrip() 645 | if title == '': 646 | print(colored('\nTitle cannot be empty\n', 'red', self.termcolor)) 647 | return 648 | print('Creating list: {}'.format(title)) 649 | self.keep.createList(title) 650 | self.do_refresh(None) 651 | 652 | def complete_create(self, text, line, start_index, end_index): 653 | if text: 654 | return [option for option in options_commands if option.startswith(text)] 655 | else: 656 | return options_commands 657 | 658 | def do_useList(self, arg): 659 | """ 660 | KEEP:Select a list to use, so items can be added, checked or unchecked 661 | 662 | Usage: 663 | ~> useList <title> 664 | 665 | Note: 666 | Use shortcut ul to select current list 667 | ~> ul <title> 668 | """ 669 | for n in self.entries: 670 | if arg == n.title and arg in self.lists: 671 | print() 672 | print('Current List set to: {}'.format(n.title)) 673 | self.current = n 674 | self.conf['current'] = n.title 675 | self.prompt = 'keepcli [{}] ~> '.format( 676 | colored(n.title[:15] + (n.title[15:] and '...'), 677 | get_color(n, self.termcolor, color_only=True), self.termcolor)) 678 | self.current_checked = [i.text for i in n.checked] 679 | self.current_unchecked = [i.text for i in n.unchecked] 680 | self.current_all_items = self.current_checked + self.current_unchecked 681 | 682 | def complete_useList(self, text, line, start_index, end_index): 683 | if text: 684 | return [option for option in self.lists if option.startswith(text)] 685 | else: 686 | return self.lists 687 | 688 | def do_useNote(self, arg): 689 | """ 690 | KEEP:Select a note to use, so text can be append to current text 691 | 692 | Usage: 693 | ~> useNote <title> 694 | 695 | Note: 696 | Use shortcut un to select current note 697 | ~> un <title> 698 | """ 699 | for n in self.entries: 700 | if arg == n.title and arg in self.notes: 701 | print() 702 | print('Current Note set to: {}'.format(n.title)) 703 | self.prompt = 'keepcli [{}] ~> '.format( 704 | colored(n.title[:15] + (n.title[15:] and '...'), 705 | get_color(n, self.termcolor, color_only=True), self.termcolor)) 706 | self.current = n 707 | self.conf['current'] = n.title 708 | 709 | def complete_useNote(self, text, line, start_index, end_index): 710 | if text: 711 | return [option for option in self.notes if option.startswith(text)] 712 | else: 713 | return self.notes 714 | 715 | def do_addText(self, arg): 716 | """ 717 | KEEP:Add text to the current note 718 | 719 | Usage: 720 | ~> addText <This is my example text> 721 | """ 722 | if self.current is None: 723 | print('Not Note or List is selected, use the command: useList or useNote') 724 | return 725 | if self.current.type.name == 'Note': 726 | self.current.text += '\n'+arg 727 | self.do_refresh(None) 728 | else: 729 | print('{} is not a Note'.format(self.current.title)) 730 | 731 | def do_checkItem(self, arg): 732 | """ 733 | KEEP:Mark an item as completed in a current list 734 | 735 | Usage: 736 | ~> checkItem <item in current list> 737 | """ 738 | checked = False 739 | if self.current is None: 740 | print('Not Note or List is selected, use the command: useList or useNote') 741 | return 742 | if self.current.type.name == 'List': 743 | if arg == '': 744 | self.do_help('checkItem') 745 | return 746 | for item in self.current.items: 747 | if arg == item.text: 748 | item.checked = True 749 | checked = True 750 | if checked: 751 | self.do_refresh(None) 752 | self.do_useList(self.current.title) 753 | else: 754 | print(colored('\nItem not found\n', 'red', self.termcolor)) 755 | return 756 | else: 757 | print('{} is not a List'.format(self.current.title)) 758 | 759 | def complete_checkItem(self, text, line, start_index, end_index): 760 | if text: 761 | temp = line[line.startswith('checkItem') and len('checkItem'):].lstrip() 762 | temp2 = temp.split()[-1] 763 | return [temp2 + option[option.startswith(temp) and len(temp):] 764 | for option in self.current_unchecked if option.startswith(temp)] 765 | else: 766 | temp = line[line.startswith('checkItem') and len('checkItem'):].lstrip() 767 | if temp == '': 768 | return self.current_unchecked 769 | else: 770 | options = [option[len(temp):] 771 | for option in self.current_unchecked if option.startswith(temp)] 772 | return options 773 | 774 | def do_deleteItem(self, arg): 775 | """ 776 | KEEP:Delete an item from a list (checked or unchecked), using --all-checked you can delete 777 | all checked items 778 | 779 | Usage: 780 | ~> deleteItem <item in current list> --> delete a single (un)checked item 781 | ~> deleteItem --all-checked --> delete all checked items 782 | """ 783 | if self.current is None: 784 | print(colored('Not Note or List is selected, use the command: useList or useNote', 785 | 'red', self.termcolor)) 786 | return 787 | if arg == '': 788 | self.do_help('deleteItem') 789 | return 790 | deleted = False 791 | delete_all_checked = False 792 | q = '\nAre you sure you want to delete all checked items?\n' 793 | q += 'This action is irreversible [spell out yes]: ' 794 | q = colored(q, 'red', self.termcolor) 795 | if self.current.type.name == 'List' and '--all-checked' in arg: 796 | if (input(q).lower() in ['yes']): 797 | delete_all_checked = True 798 | arg = '' 799 | else: 800 | return 801 | if self.current.type.name == 'List': 802 | for item in self.current.items: 803 | if delete_all_checked and item.checked: 804 | item.delete() 805 | deleted = True 806 | if arg == item.text: 807 | question = '\nAre you sure you want to delete {} ?.\n'.format(arg) 808 | question += 'This is irreversible [spell out yes]: ' 809 | question = colored(question, 'red', self.termcolor) 810 | if (input(question).lower() in ['yes']): 811 | item.delete() 812 | deleted = True 813 | break 814 | if deleted: 815 | self.do_refresh(None) 816 | self.do_useList(self.current.title) 817 | else: 818 | print('\n Item: [{}] does not exists\n'.format(arg)) 819 | else: 820 | print('{} is not a List'.format(self.current.title)) 821 | 822 | def complete_deleteItem(self, text, line, start_index, end_index): 823 | if text: 824 | if '--a' in line: 825 | return ['all-checked'] 826 | temp = line[line.startswith('deleteItem') and len('deleteItem'):].lstrip() 827 | temp2 = temp.split()[-1] 828 | return [temp2 + option[option.startswith(temp) and len(temp):] 829 | for option in self.current_all_items if option.startswith(temp)] 830 | else: 831 | temp = line[line.startswith('deleteItem') and len('deleteItem'):].lstrip() 832 | if temp == '': 833 | return self.current_all_items 834 | else: 835 | options = [option[len(temp):] 836 | for option in self.current_all_items if option.startswith(temp)] 837 | return options 838 | 839 | def do_uncheckItem(self, arg): 840 | """ 841 | KEEP:Mark an item as unchecked in a current list 842 | 843 | Usage: 844 | ~> uncheckItem <item in current list> 845 | """ 846 | unchecked = False 847 | if self.current is None: 848 | print('Not Note or List is selected, use the command: useList or useNote') 849 | return 850 | if self.current.type.name == 'List': 851 | for item in self.current.items: 852 | if arg == item.text: 853 | item.checked = False 854 | unchecked = True 855 | if unchecked: 856 | self.do_refresh(None) 857 | self.do_useList(self.current.title) 858 | else: 859 | print(colored('\nItem not found\n', 'red', self.termcolor)) 860 | return 861 | else: 862 | print('{} is not a List'.format(self.current.title)) 863 | 864 | def complete_uncheckItem(self, text, line, start_index, end_index): 865 | if text: 866 | temp = line[line.startswith('uncheckItem') and len('uncheckItem'):].lstrip() 867 | temp2 = temp.split()[-1] 868 | return [temp2 + option[option.startswith(temp) and len(temp):] 869 | for option in self.current_checked if option.startswith(temp)] 870 | else: 871 | temp = line[line.startswith('uncheckItem') and len('uncheckItem'):].lstrip() 872 | if temp == '': 873 | return self.current_checked 874 | else: 875 | options = [option[len(temp):] 876 | for option in self.current_checked if option.startswith(temp)] 877 | return options 878 | 879 | def do_addItem(self, arg): 880 | """ 881 | KEEP: Add a new item to current lists 882 | 883 | Usage: 884 | ~> addItem <item> 885 | 886 | Ex: 887 | ~> addItem get milk 888 | 889 | Note: 890 | You can also use the shortcut ai: 891 | ~> ai <item> 892 | """ 893 | if self.current is None: 894 | print('Not Note or List is selected, use the command: useList or useNote') 895 | return 896 | if self.current.type.name == 'List': 897 | new = arg.lstrip() 898 | if new == '': 899 | self.do_help('addItem') 900 | return 901 | self.current.add(new) 902 | self.do_refresh(None) 903 | if self.autosync: 904 | self.do_useList(self.current.title) 905 | else: 906 | print('{} is not a List'.format(self.current.title)) 907 | 908 | def do_moveItem(self, arg): 909 | """ 910 | KEEP:Move items from current list to another 911 | 912 | Usage: 913 | ~> moveItem <item> --list <destination_list> 914 | """ 915 | destination = None 916 | done = False 917 | if self.current is None: 918 | print('Not Note or List is selected, use the command: useList or useNote') 919 | return 920 | move_args = argparse.ArgumentParser(prog='', usage='', add_help=False) 921 | move_args.add_argument('item', action='store', default=None, nargs='+') 922 | move_args.add_argument('--list', help='Name of the destination list', 923 | action='store', default=None, nargs='+') 924 | 925 | try: 926 | args = move_args.parse_args(arg.split()) 927 | except: 928 | self.do_help('moveItem') 929 | return 930 | if args.list is None: 931 | print('You need to specify a list to move the item to with --list option') 932 | return 933 | else: 934 | new_arg = arg[:arg.index('--list')].rstrip() 935 | new_dest = arg[arg.index('--list')+6:].lstrip() 936 | for n in self.entries: 937 | if new_dest == n.title: 938 | destination = n 939 | if destination is None: 940 | print('List {} does not exist'.format(args.list)) 941 | self.do_entries('lists') 942 | return 943 | if self.current.type.name == 'List': 944 | for item in self.current.items: 945 | if new_arg == item.text: 946 | destination.add(item.text) 947 | item.delete() 948 | done = True 949 | break 950 | if not done: 951 | print('Item {} does not exist in list {}'.format(new_arg, self.current.title)) 952 | return 953 | self.do_refresh(None) 954 | self.do_useList(self.current.title) 955 | else: 956 | print('{} is not a List'.format(self.current.title)) 957 | 958 | def complete_moveItem(self, text, line, start_index, end_index): 959 | if text: 960 | if '--list' in line: 961 | return [option for option in self.lists if option.startswith(text)] 962 | temp = line[line.startswith('moveItem') and len('moveItem'):].lstrip() 963 | temp2 = temp.split()[-1] 964 | return [temp2 + option[option.startswith(temp) and len(temp):] 965 | for option in self.current_unchecked if option.startswith(temp)] 966 | else: 967 | if '--list' in line: 968 | return self.lists 969 | temp = line[line.startswith('moveItem') and len('moveItem'):].lstrip() 970 | if temp == '': 971 | return self.current_unchecked 972 | else: 973 | options = [option[len(temp):] 974 | for option in self.current_unchecked if option.startswith(temp)] 975 | return options 976 | 977 | def do_dump(self, arg): 978 | """ 979 | Pickle entries and current status for offline use 980 | 981 | Usage: 982 | ~> dump 983 | """ 984 | pickle.dump(self.keep, open(os.path.join(self.kcli_path, self.username+'.kci'), 'wb')) 985 | 986 | def do_load(self, arg): 987 | """ 988 | Load entries from a previously saved pickle. For offline use 989 | 990 | Usage: 991 | ~> load 992 | """ 993 | with open(self.auth_file, 'r') as auth: 994 | conn = yaml.load(auth) 995 | self.username = conn['user'] 996 | self.keep = pickle.load(open(os.path.join(self.kcli_path, self.username+'.kci'), 'rb')) 997 | self.do_refresh(None) 998 | 999 | def do_clear(self, line): 1000 | """ 1001 | Clears the screen. 1002 | 1003 | Usage: 1004 | ~> clean 1005 | """ 1006 | sys.stdout.flush() 1007 | # if line is None: 1008 | # return 1009 | try: 1010 | tmp = os.system('clear') 1011 | except: 1012 | try: 1013 | tmp = os.system('cls') 1014 | except: 1015 | pass 1016 | 1017 | 1018 | def write_conf(conf_file): 1019 | defaults = { 1020 | 'termcolor': True, 1021 | 'autosync': True, 1022 | 'current': '', 1023 | } 1024 | if not os.path.exists(conf_file): 1025 | with open(conf_file, 'w') as conf: 1026 | yaml.dump(defaults, conf, default_flow_style=False) 1027 | else: 1028 | with open(conf_file, 'r') as conf: 1029 | current = yaml.load(conf) 1030 | for k in defaults.keys(): 1031 | if current.get(k) is None: 1032 | current[k] = defaults[k] 1033 | with open(conf_file, 'w') as conf: 1034 | yaml.dump(current, conf, default_flow_style=False) 1035 | 1036 | 1037 | def cli(): 1038 | """ Main command line interface function""" 1039 | online = True if os.system("ping -c 1 " + 'google.com' + '> /dev/null 2>&1') is 0 else False 1040 | if not online: 1041 | print('You are offline, use the --offline option (and load your previously dumped data)') 1042 | return 1043 | kcli_path = os.path.join(os.environ["HOME"], ".keepcli/") 1044 | if not os.path.exists(kcli_path): 1045 | os.makedirs(kcli_path) 1046 | try: 1047 | auth_file = os.environ["KEEPCLI_AUTH"] 1048 | except KeyError: 1049 | auth_file = os.path.join(kcli_path, "auth.yaml") 1050 | conf_file = os.path.join(kcli_path, "config.yaml") 1051 | write_conf(conf_file) 1052 | args = kcliparser.get_args() 1053 | offline = True if args.offline else False 1054 | GKeep(auth_file=auth_file, conf_file=conf_file, offline=offline).cmdloop() 1055 | 1056 | 1057 | if __name__ == '__main__': 1058 | cli() 1059 | --------------------------------------------------------------------------------