diff --git a/bin/cheat b/bin/cheat index 40ab7e5..22b5b28 100755 --- a/bin/cheat +++ b/bin/cheat @@ -36,9 +36,10 @@ Examples: # require the dependencies from __future__ import print_function -from cheat import sheets, sheet -from cheat.utils import colorize -from cheat.utils import die +from cheat.sheets import Sheets +from cheat.sheet import Sheet +from cheat.utils import Utils +from cheat.configuration import Configuration from docopt import docopt import os @@ -59,6 +60,11 @@ if __name__ == '__main__': # parse the command-line options options = docopt(__doc__, version='cheat 2.3.1') + config = Configuration() + sheets = Sheets(config.get_default_cheat_dir(),config.get_cheatpath()) + sheet = Sheet(config.get_default_cheat_dir(),config.get_cheatpath(),config.get_editor()) + utils = Utils(config.get_cheatcolors(),config.get_editor()) + # list directories if options['--directories']: print("\n".join(sheets.paths())) @@ -73,8 +79,8 @@ if __name__ == '__main__': # search among the cheatsheets elif options['--search']: - print(colorize(sheets.search(options[''])), end="") + print(utils.colorize(sheets.search(options[''])), end="") # print the cheatsheet else: - print(colorize(sheet.read(options[''])), end="") + print(utils.colorize(sheet.read(options[''])), end="") diff --git a/cheat/configuration.py b/cheat/configuration.py index 38e80e2..e89b849 100644 --- a/cheat/configuration.py +++ b/cheat/configuration.py @@ -1,5 +1,5 @@ import os -from cheat.utils import warn +from cheat.utils import Utils import json class Configuration: @@ -15,12 +15,12 @@ class Configuration: try: merged_config.update(self._read_configuration_file('/etc/cheat')) except Exception: - warn('error while parsing global configuration') + Utils.warn('error while parsing global configuration') try: merged_config.update(self._read_configuration_file(os.path.expanduser(os.path.join('~','.config','cheat','cheat')))) except Exception: - warn('error while parsing user configuration') + Utils.warn('error while parsing user configuration') merged_config.update(self._read_env_vars_config()) diff --git a/cheat/sheet.py b/cheat/sheet.py index ff1ce27..c9a0bb0 100644 --- a/cheat/sheet.py +++ b/cheat/sheet.py @@ -1,76 +1,85 @@ import os import shutil -from cheat import sheets -from cheat.utils import die, open_with_editor - -def copy(current_sheet_path, new_sheet_path): - """ Copies a sheet to a new path """ - - # attempt to copy the sheet to DEFAULT_CHEAT_DIR - try: - shutil.copy(current_sheet_path, new_sheet_path) - - # fail gracefully if the cheatsheet cannot be copied. This can happen if - # DEFAULT_CHEAT_DIR does not exist - except IOError: - die('Could not copy cheatsheet for editing.') +from cheat.sheets import Sheets +from cheat.utils import Utils -def create_or_edit(sheet): - """ Creates or edits a cheatsheet """ - - # if the cheatsheet does not exist - if not exists(sheet): - create(sheet) - - # if the cheatsheet exists but not in the default_path, copy it to the - # default path before editing - elif exists(sheet) and not exists_in_default_path(sheet): - copy(path(sheet), os.path.join(sheets.default_path(), sheet)) - edit(sheet) - - # if it exists and is in the default path, then just open it - else: - edit(sheet) +class Sheet: -def create(sheet): - """ Creates a cheatsheet """ - new_sheet_path = os.path.join(sheets.default_path(), sheet) - open_with_editor(new_sheet_path) + def __init__(self,default_cheat_dir,cheatpath,editor_exec): + self.sheets_instance = Sheets(default_cheat_dir,cheatpath) + self.utils_instance = Utils(None,editor_exec) -def edit(sheet): - """ Opens a cheatsheet for editing """ - open_with_editor(path(sheet)) + def copy(self,current_sheet_path, new_sheet_path): + """ Copies a sheet to a new path """ + + # attempt to copy the sheet to DEFAULT_CHEAT_DIR + try: + shutil.copy(current_sheet_path, new_sheet_path) + + # fail gracefully if the cheatsheet cannot be copied. This can happen if + # DEFAULT_CHEAT_DIR does not exist + except IOError: + Utils.die('Could not copy cheatsheet for editing.') -def exists(sheet): - """ Predicate that returns true if the sheet exists """ - return sheet in sheets.get() and os.access(path(sheet), os.R_OK) + def create_or_edit(self,sheet): + """ Creates or edits a cheatsheet """ + + # if the cheatsheet does not exist + if not self.exists(sheet): + self.create(sheet) + + # if the cheatsheet exists but not in the default_path, copy it to the + # default path before editing + elif self.exists(sheet) and not self.exists_in_default_path(sheet): + self.copy(self.path(sheet), os.path.join(self.sheets_instance.default_path(), sheet)) + self.edit(sheet) + + # if it exists and is in the default path, then just open it + else: + self.edit(sheet) -def exists_in_default_path(sheet): - """ Predicate that returns true if the sheet exists in default_path""" - default_path_sheet = os.path.join(sheets.default_path(), sheet) - return sheet in sheets.get() and os.access(default_path_sheet, os.R_OK) + def create(self,sheet): + """ Creates a cheatsheet """ + new_sheet_path = os.path.join(self.sheets_instance.default_path(), sheet) + self.utils_instance.open_with_editor(new_sheet_path) -def is_writable(sheet): - """ Predicate that returns true if the sheet is writeable """ - return sheet in sheets.get() and os.access(path(sheet), os.W_OK) + def edit(self,sheet): + """ Opens a cheatsheet for editing """ + self.utils_instance.open_with_editor(self.path(sheet)) -def path(sheet): - """ Returns a sheet's filesystem path """ - return sheets.get()[sheet] + def exists(self,sheet): + """ Predicate that returns true if the sheet exists """ + return sheet in self.sheets_instance.get() and os.access(self.path(sheet), os.R_OK) -def read(sheet): - """ Returns the contents of the cheatsheet as a String """ - if not exists(sheet): - die('No cheatsheet found for ' + sheet) + def exists_in_default_path(self,sheet): + """ Predicate that returns true if the sheet exists in default_path""" + default_path_sheet = os.path.join(self.sheets_instance.default_path(), sheet) + return sheet in self.sheets_instance.get() and os.access(default_path_sheet, os.R_OK) - with open(path(sheet)) as cheatfile: - return cheatfile.read() + + def is_writable(self,sheet): + """ Predicate that returns true if the sheet is writeable """ + return sheet in self.sheets_instance.get() and os.access(self.path(sheet), os.W_OK) + + + def path(self,sheet): + """ Returns a sheet's filesystem path """ + return self.sheets_instance.get()[sheet] + + + def read(self,sheet): + """ Returns the contents of the cheatsheet as a String """ + if not self.exists(sheet): + Utils.die('No cheatsheet found for ' + sheet) + + with open(self.path(sheet)) as cheatfile: + return cheatfile.read() diff --git a/cheat/sheets.py b/cheat/sheets.py index c106786..107bad3 100644 --- a/cheat/sheets.py +++ b/cheat/sheets.py @@ -1,94 +1,100 @@ import os -from cheat.utils import die -from cheat.utils import highlight from cheat.configuration import Configuration +from cheat.utils import Utils -def default_path(): - """ Returns the default cheatsheet path """ - - # determine the default cheatsheet dir - default_sheets_dir = Configuration().get_default_cheat_dir() or os.path.join('~', '.cheat') - default_sheets_dir = os.path.expanduser(os.path.expandvars(default_sheets_dir)) - - # create the DEFAULT_CHEAT_DIR if it does not exist - if not os.path.isdir(default_sheets_dir): - try: - # @kludge: unclear on why this is necessary - os.umask(0000) - os.mkdir(default_sheets_dir) - - except OSError: - die('Could not create DEFAULT_CHEAT_DIR') - - # assert that the DEFAULT_CHEAT_DIR is readable and writable - if not os.access(default_sheets_dir, os.R_OK): - die('The DEFAULT_CHEAT_DIR (' + default_sheets_dir +') is not readable.') - if not os.access(default_sheets_dir, os.W_OK): - die('The DEFAULT_CHEAT_DIR (' + default_sheets_dir +') is not writable.') - - # return the default dir - return default_sheets_dir +class Sheets: -def get(): - """ Assembles a dictionary of cheatsheets as name => file-path """ - cheats = {} - - # otherwise, scan the filesystem - for cheat_dir in reversed(paths()): - cheats.update( - dict([ - (cheat, os.path.join(cheat_dir, cheat)) - for cheat in os.listdir(cheat_dir) - if not cheat.startswith('.') - and not cheat.startswith('__') - ]) - ) - - return cheats + def __init__(self,default_cheat_dir,cheatpath): + self.default_cheat_dir = default_cheat_dir + self.cheatpath = cheatpath -def paths(): - """ Assembles a list of directories containing cheatsheets """ - sheet_paths = [ - default_path(), - '/usr/share/cheat', - ] + def default_path(self): + """ Returns the default cheatsheet path """ - # merge the CHEATPATH paths into the sheet_paths - if Configuration().get_cheatpath(): - for path in Configuration().get_cheatpath().split(os.pathsep): - if os.path.isdir(path): - sheet_paths.append(path) + # determine the default cheatsheet dir + default_sheets_dir = self.default_cheat_dir or os.path.join('~', '.cheat') + default_sheets_dir = os.path.expanduser(os.path.expandvars(default_sheets_dir)) - if not sheet_paths: - die('The DEFAULT_CHEAT_DIR dir does not exist or the CHEATPATH is not set.') + # create the DEFAULT_CHEAT_DIR if it does not exist + if not os.path.isdir(default_sheets_dir): + try: + # @kludge: unclear on why this is necessary + os.umask(0000) + os.mkdir(default_sheets_dir) - return sheet_paths + except OSError: + Utils.die('Could not create DEFAULT_CHEAT_DIR') + + # assert that the DEFAULT_CHEAT_DIR is readable and writable + if not os.access(default_sheets_dir, os.R_OK): + Utils.die('The DEFAULT_CHEAT_DIR (' + default_sheets_dir +') is not readable.') + if not os.access(default_sheets_dir, os.W_OK): + Utils.die('The DEFAULT_CHEAT_DIR (' + default_sheets_dir +') is not writable.') + + # return the default dir + return default_sheets_dir -def list(): - """ Lists the available cheatsheets """ - sheet_list = '' - pad_length = max([len(x) for x in get().keys()]) + 4 - for sheet in sorted(get().items()): - sheet_list += sheet[0].ljust(pad_length) + sheet[1] + "\n" - return sheet_list + def get(self): + """ Assembles a dictionary of cheatsheets as name => file-path """ + cheats = {} + + # otherwise, scan the filesystem + for cheat_dir in reversed(self.paths()): + cheats.update( + dict([ + (cheat, os.path.join(cheat_dir, cheat)) + for cheat in os.listdir(cheat_dir) + if not cheat.startswith('.') + and not cheat.startswith('__') + ]) + ) + + return cheats -def search(term): - """ Searches all cheatsheets for the specified term """ - result = '' - lowered_term = term.lower() + def paths(self): + """ Assembles a list of directories containing cheatsheets """ + sheet_paths = [ + self.default_path(), + '/usr/share/cheat', + ] - for cheatsheet in sorted(get().items()): - match = '' - for line in open(cheatsheet[1]): - if term in line: - match += ' ' + highlight(term, line) + # merge the CHEATPATH paths into the sheet_paths + if self.cheatpath: + for path in self.cheatpath.split(os.pathsep): + if os.path.isdir(path): + sheet_paths.append(path) - if match != '': - result += cheatsheet[0] + ":\n" + match + "\n" + if not sheet_paths: + Utils.die('The DEFAULT_CHEAT_DIR dir does not exist or the CHEATPATH is not set.') - return result + return sheet_paths + + + def list(self): + """ Lists the available cheatsheets """ + sheet_list = '' + pad_length = max([len(x) for x in self.get().keys()]) + 4 + for sheet in sorted(self.get().items()): + sheet_list += sheet[0].ljust(pad_length) + sheet[1] + "\n" + return sheet_list + + + def search(self,term): + """ Searches all cheatsheets for the specified term """ + result = '' + + for cheatsheet in sorted(self.get().items()): + match = '' + for line in open(cheatsheet[1]): + if term in line: + match += ' ' + line + + if match != '': + result += cheatsheet[0] + ":\n" + match + "\n" + + return result diff --git a/cheat/utils.py b/cheat/utils.py index c4202ec..17e4ba8 100644 --- a/cheat/utils.py +++ b/cheat/utils.py @@ -3,94 +3,99 @@ import os import subprocess import sys -from cheat.configuration import Configuration - -def highlight(needle, haystack): - """ Highlights a search term matched within a line """ - - # if a highlight color is not configured, exit early - if not 'CHEAT_HIGHLIGHT' in os.environ: - return haystack - - # otherwise, attempt to import the termcolor library - try: - from termcolor import colored - - # if the import fails, return uncolored text - except ImportError: - return haystack - - # if the import succeeds, colorize the needle in haystack - return haystack.replace(needle, colored(needle, os.environ.get('CHEAT_HIGHLIGHT'))); +class Utils: -def colorize(sheet_content): - """ Colorizes cheatsheet content if so configured """ + def __init__(self,cheatcolors,editor_executable): + self.displaycolors = cheatcolors + self.editor_executable = editor_executable - # only colorize if configured to do so, and if stdout is a tty - if not Configuration().get_cheatcolors() or not sys.stdout.isatty(): - return sheet_content - # don't attempt to colorize an empty cheatsheet - if not sheet_content.strip(): - return "" + def highlight(self, needle, haystack): + """ Highlights a search term matched within a line """ - # otherwise, attempt to import the pygments library - try: - from pygments import highlight - from pygments.lexers import get_lexer_by_name - from pygments.formatters import TerminalFormatter + # if a highlight color is not configured, exit early + if not 'CHEAT_HIGHLIGHT' in os.environ: + return haystack - # if the import fails, return uncolored text - except ImportError: - return sheet_content - - # otherwise, attempt to colorize - first_line = sheet_content.splitlines()[0] - lexer = get_lexer_by_name('bash') - - # apply syntax-highlighting if the first line is a code-fence - if first_line.startswith('```'): - sheet_content = '\n'.join(sheet_content.split('\n')[1:-2]) + # otherwise, attempt to import the termcolor library try: - lexer = get_lexer_by_name(first_line[3:]) - except Exception: - pass + from termcolor import colored - return highlight(sheet_content, lexer, TerminalFormatter()) + # if the import fails, return uncolored text + except ImportError: + return haystack + + # if the import succeeds, colorize the needle in haystack + return haystack.replace(needle, colored(needle, os.environ.get('CHEAT_HIGHLIGHT'))) -def die(message): - """ Prints a message to stderr and then terminates """ - warn(message) - exit(1) + def colorize(self,sheet_content): + """ Colorizes cheatsheet content if so configured """ + + # only colorize if configured to do so, and if stdout is a tty + if not self.displaycolors or not sys.stdout.isatty(): + return sheet_content + + # don't attempt to colorize an empty cheatsheet + if not sheet_content.strip(): + return "" + + # otherwise, attempt to import the pygments library + try: + from pygments import highlight + from pygments.lexers import get_lexer_by_name + from pygments.formatters import TerminalFormatter + + # if the import fails, return uncolored text + except ImportError: + return sheet_content + + # otherwise, attempt to colorize + first_line = sheet_content.splitlines()[0] + lexer = get_lexer_by_name('bash') + + # apply syntax-highlighting if the first line is a code-fence + if first_line.startswith('```'): + sheet_content = '\n'.join(sheet_content.split('\n')[1:-2]) + try: + lexer = get_lexer_by_name(first_line[3:]) + except Exception: + pass + + return highlight(sheet_content, lexer, TerminalFormatter()) -def editor(): - """ Determines the user's preferred editor """ - - # determine which editor to use - editor = Configuration().get_editor() - - # assert that the editor is set - if (not editor): - die( - 'You must set a CHEAT_EDITOR, VISUAL, or EDITOR environment ' - 'variable or setting in order to create/edit a cheatsheet.' - ) - - return editor + @staticmethod + def die(message): + """ Prints a message to stderr and then terminates """ + Utils.warn(message) + exit(1) -def open_with_editor(filepath): - """ Open `filepath` using the EDITOR specified by the environment variables """ - editor_cmd = editor().split() - try: - subprocess.call(editor_cmd + [filepath]) - except OSError: - die('Could not launch ' + editor()) + def editor(self): + """ Determines the user's preferred editor """ + + # assert that the editor is set + if (not self.editor_executable): + Utils.die( + 'You must set a CHEAT_EDITOR, VISUAL, or EDITOR environment ' + 'variable or setting in order to create/edit a cheatsheet.' + ) + + return self.editor_executable -def warn(message): - """ Prints a message to stderr """ - print((message), file=sys.stderr) + def open_with_editor(self,filepath): + """ Open `filepath` using the EDITOR specified by the environment variables """ + editor_cmd = self.editor().split() + try: + subprocess.call(editor_cmd + [filepath]) + except OSError: + Utils.die('Could not launch ' + self.editor()) + + + @staticmethod + def warn(message): + """ Prints a message to stderr """ + print((message), file=sys.stderr)