src/pathpicker/output.py (134 lines of code) (raw):

# Copyright (c) Facebook, Inc. and its affiliates. # # This source code is licensed under the MIT license found in the # LICENSE file in the root directory of this source tree. import os import pickle from typing import List, Tuple from pathpicker import logger, state_files from pathpicker.line_format import LineMatch RED_COLOR = "\033[0;31m" NO_COLOR = "\033[0m" INVALID_FILE_WARNING = """ Warning! Some invalid or unresolvable files were detected. """ GIT_ABBREVIATION_WARNING = """ It looks like one of these is a git abbreviated file with a triple dot path (.../). Try to turn off git's abbreviation with --numstat so we get actual paths (not abbreviated versions which cannot be resolved. """ CONTINUE_WARNING = "Are you sure you want to continue? Ctrl-C to quit" # The two main entry points into this module: # def exec_composed_command(command: str, line_objs: List[LineMatch]) -> None: if not command: edit_files(line_objs) return logger.add_event("command_on_num_files", len(line_objs)) command = compose_command(command, line_objs) append_alias_expansion() append_if_invalid(line_objs) append_friendly_command(command) append_exit() def edit_files(line_objs: List[LineMatch]) -> None: logger.add_event("editing_num_files", len(line_objs)) files_and_line_numbers = [ (line_obj.get_path(), line_obj.get_line_num()) for line_obj in line_objs ] command = join_files_into_command(files_and_line_numbers) append_if_invalid(line_objs) append_to_file(command) append_exit() # Private helpers def append_if_invalid(line_objs: List[LineMatch]) -> None: # lastly lets check validity and actually output an # error if any files are invalid invalid_lines = [line for line in line_objs if not line.is_resolvable()] if not invalid_lines: return append_error(INVALID_FILE_WARNING) if any(map(LineMatch.is_git_abbreviated_path, invalid_lines)): append_error(GIT_ABBREVIATION_WARNING) append_to_file(f'read -p "{CONTINUE_WARNING}" -r') def output_selection(line_objs: List[LineMatch]) -> None: file_path = state_files.get_selection_file_path() indices = [line.index for line in line_objs] file = open(file_path, "wb") pickle.dump(indices, file) file.close() def get_editor_and_path() -> Tuple[str, str]: editor_path = ( os.environ.get("FPP_EDITOR") or os.environ.get("VISUAL") or os.environ.get("EDITOR") ) if editor_path: editor = os.path.basename(editor_path) logger.add_event(f"using_editor_{editor}") return editor, editor_path return "vim", "vim" def join_files_into_command(files_and_line_numbers: List[Tuple[str, int]]) -> str: editor, editor_path = get_editor_and_path() cmd = editor_path + " " if editor == "vim -p": first_file_path, first_line_num = files_and_line_numbers[0] cmd += f" +{first_line_num} {first_file_path}" for (file_path, line_num) in files_and_line_numbers[1:]: cmd += f' +"tabnew +{line_num} {file_path}"' elif editor in ["vim", "mvim", "nvim"] and not os.environ.get("FPP_DISABLE_SPLIT"): first_file_path, first_line_num = files_and_line_numbers[0] cmd += f" +{first_line_num} {first_file_path}" for (file_path, line_num) in files_and_line_numbers[1:]: cmd += f' +"vsp +{line_num} {file_path}"' else: for (file_path, line_num) in files_and_line_numbers: editor_without_args = editor.split()[0] if ( editor_without_args in ["vi", "nvim", "nano", "joe", "emacs", "emacsclient", "micro"] and line_num != 0 ): cmd += f" +{line_num} '{file_path}'" elif editor_without_args in ["subl", "sublime", "atom"] and line_num != 0: cmd += f" '{file_path}:{line_num}'" elif line_num != 0 and os.environ.get("FPP_LINENUM_SEP"): cmd += f" '{file_path}{os.environ.get('FPP_LINENUM_SEP')}{line_num}'" else: cmd += f" '{file_path}'" return cmd def compose_cd_command(_command: str, line_objs: List[LineMatch]) -> str: file_path = os.path.expanduser(line_objs[0].get_dir()) file_path = os.path.abspath(file_path) # now copy it into clipboard for cdp-ing # TODO -- this is pretty specific to # pcottles workflow return f'echo "{file_path}" > ~/.dircopy' def is_cd_command(command: str) -> bool: return command[0:3] in ["cd ", "cd"] def compose_command(command: str, line_objs: List[LineMatch]) -> str: if is_cd_command(command): return compose_cd_command(command, line_objs) return compose_file_command(command, line_objs) def compose_file_command(command: str, line_objs: List[LineMatch]) -> str: command = command.encode().decode("utf-8") paths = [f"'{line_obj.get_path()}'" for line_obj in line_objs] path_str = " ".join(paths) if "$F" in command: command = command.replace("$F", path_str) else: command = f"{command} {path_str}" return command def output_nothing() -> None: append_to_file('echo "nothing to do!"; exit 1') def clear_file() -> None: write_to_file("") def append_alias_expansion() -> None: # zsh by default expands aliases when running in interactive mode # (see ../fpp). bash (on this author's Yosemite box) seems to have # alias expansion off when run with -i present and -c absent, # despite documentation hinting otherwise. # # so here we must ask bash to turn on alias expansion. shell = os.environ.get("SHELL") if shell is None or "fish" not in shell: append_to_file( """ if type shopt > /dev/null; then shopt -s expand_aliases fi """ ) def append_friendly_command(command: str) -> None: header = 'echo "executing command:"\necho "' + command.replace('"', '\\"') + '"' append_to_file(header) append_to_file(command) def append_error(text: str) -> None: append_to_file(f'printf "{RED_COLOR}{text}{NO_COLOR}\n"') def append_to_file(command: str) -> None: file = open(state_files.get_script_output_file_path(), "a") file.write(command + "\n") file.close() logger.output() def append_exit() -> None: # The `$SHELL` environment variable points to the default shell, # not the current shell. But they are often the same. And there # is no other simple and reliable way to detect the current shell. shell = os.environ["SHELL"] # ``csh``, fish`` and, ``rc`` uses ``$status`` instead of ``$?``. if shell.endswith("csh") or shell.endswith("fish") or shell.endswith("rc"): exit_status = "$status" # Otherwise we assume a Bournal-like shell, e.g. bash and zsh. else: exit_status = "$?" append_to_file(f"exit {exit_status};") def write_to_file(command: str) -> None: file = open(state_files.get_script_output_file_path(), "w") file.write(command + "\n") file.close() logger.output()