├── 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
[](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 | 
13 |
14 | Let's see how everything is synchronized with the web interface.
15 |
16 | 
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
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
664 |
665 | Note:
666 | Use shortcut ul to select current list
667 | ~> ul
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
694 |
695 | Note:
696 | Use shortcut un to select current note
697 | ~> un
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
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 -
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
- --> 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
-
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
-
885 |
886 | Ex:
887 | ~> addItem get milk
888 |
889 | Note:
890 | You can also use the shortcut ai:
891 | ~> ai
-
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
- --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 |
--------------------------------------------------------------------------------