From 15372fcb90730ea6b8755f7242e0b8bba005668f Mon Sep 17 00:00:00 2001 From: Pablo Santiago Blum de Aguiar Date: Sun, 17 Dec 2017 02:53:30 -0200 Subject: [PATCH 1/4] #N/A: Support editing command --- tests/test_types.py | 12 ++++++++++++ tests/test_ui.py | 32 ++++++++++++++++++++++++++------ thefuck/const.py | 5 ++++- thefuck/logs.py | 16 +++++++++++++--- thefuck/shells/fish.py | 7 +++++++ thefuck/shells/generic.py | 7 +++++++ thefuck/types.py | 8 ++++++++ thefuck/ui.py | 18 ++++++++++++------ 8 files changed, 89 insertions(+), 16 deletions(-) diff --git a/tests/test_types.py b/tests/test_types.py index f946a8b..dcea1b0 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -43,6 +43,18 @@ class TestCorrectedCommand(object): out, _ = capsys.readouterr() assert out[:-1] == printed + def test_run_with_edit(self, capsys, monkeypatch, mocker): + script = "git branch" + edit_tpl = 'editor "{}"' + monkeypatch.setattr( + 'thefuck.types.shell.edit_command', + lambda script: edit_tpl.format(script), + ) + command = CorrectedCommand(script, None, 1000).edit() + command.run(Command(script, '')) + out, _ = capsys.readouterr() + assert out[:-1] == edit_tpl.format(script) + class TestRule(object): def test_from_path(self, mocker): diff --git a/tests/test_ui.py b/tests/test_ui.py index 1e53965..e88d060 100644 --- a/tests/test_ui.py +++ b/tests/test_ui.py @@ -16,12 +16,15 @@ def patch_get_key(monkeypatch): return patch -def test_read_actions(patch_get_key): +@pytest.mark.parametrize("shell_can_edit", [True, False]) +def test_read_actions(shell_can_edit, patch_get_key): patch_get_key([ # Enter: '\n', # Enter: '\r', + # Edit: + const.KEY_BACKSPACE, 'd', # Ignored: 'x', 'y', # Up: @@ -30,11 +33,17 @@ def test_read_actions(patch_get_key): const.KEY_DOWN, 'j', # Ctrl+C: const.KEY_CTRL_C, 'q']) - assert (list(islice(ui.read_actions(), 8)) - == [const.ACTION_SELECT, const.ACTION_SELECT, - const.ACTION_PREVIOUS, const.ACTION_PREVIOUS, - const.ACTION_NEXT, const.ACTION_NEXT, - const.ACTION_ABORT, const.ACTION_ABORT]) + expected_actions = [const.ACTION_SELECT, const.ACTION_SELECT, + const.ACTION_PREVIOUS, const.ACTION_PREVIOUS, + const.ACTION_NEXT, const.ACTION_NEXT, + const.ACTION_ABORT, const.ACTION_ABORT] + number_of_items = 8 + if shell_can_edit: + expected_actions.insert(2, const.ACTION_EDIT) + expected_actions.insert(2, const.ACTION_EDIT) + number_of_items = 10 + assert (list(islice(ui.read_actions(shell_can_edit), number_of_items)) + == expected_actions) def test_command_selector(): @@ -106,3 +115,14 @@ class TestSelectCommand(object): u'{mark}\x1b[1K\rcd [enter/↑/↓/ctrl+c]\n' ).format(mark=const.USER_COMMAND_MARK) assert capsys.readouterr() == ('', stderr) + + def test_with_edit(self, capsys, patch_get_key, commands, monkeypatch): + monkeypatch.setattr('thefuck.ui.shell.can_edit', lambda: True) + patch_get_key([const.KEY_BACKSPACE, '\n']) + command = ui.select_command(iter(commands)) + assert command == commands[0] + assert command.should_edit is True + stderr = ( + u'{mark}\x1b[1K\rls [enter/edit/↑/↓/ctrl+c]\n' + ).format(mark=const.USER_COMMAND_MARK) + assert capsys.readouterr() == ('', stderr) diff --git a/thefuck/const.py b/thefuck/const.py index d272f1b..10fea85 100644 --- a/thefuck/const.py +++ b/thefuck/const.py @@ -14,15 +14,18 @@ KEY_DOWN = _GenConst('↓') KEY_CTRL_C = _GenConst('Ctrl+C') KEY_CTRL_N = _GenConst('Ctrl+N') KEY_CTRL_P = _GenConst('Ctrl+P') +KEY_BACKSPACE = _GenConst('Backspace') KEY_MAPPING = {'\x0e': KEY_CTRL_N, '\x03': KEY_CTRL_C, - '\x10': KEY_CTRL_P} + '\x10': KEY_CTRL_P, + '\x7f': KEY_BACKSPACE} ACTION_SELECT = _GenConst('select') ACTION_ABORT = _GenConst('abort') ACTION_PREVIOUS = _GenConst('previous') ACTION_NEXT = _GenConst('next') +ACTION_EDIT = _GenConst('edit') ALL_ENABLED = _GenConst('All rules enabled') DEFAULT_RULES = [ALL_ENABLED] diff --git a/thefuck/logs.py b/thefuck/logs.py index e064de6..0ba46ba 100644 --- a/thefuck/logs.py +++ b/thefuck/logs.py @@ -56,10 +56,19 @@ def show_corrected_command(corrected_command): reset=color(colorama.Style.RESET_ALL))) -def confirm_text(corrected_command): +def edit_part(show_edit): + if show_edit: + return '/{yellow}e{bold}d{normal}it'.format( + yellow=color(colorama.Fore.YELLOW), + bold=color(colorama.Style.BRIGHT), + normal=color(colorama.Style.NORMAL)) + return '' + + +def confirm_text(corrected_command, show_edit): sys.stderr.write( (u'{prefix}{clear}{bold}{script}{reset}{side_effect} ' - u'[{green}enter{reset}/{blue}↑{reset}/{blue}↓{reset}' + u'[{green}enter{reset}{edit}{reset}/{blue}↑{reset}/{blue}↓{reset}' u'/{red}ctrl+c{reset}]').format( prefix=const.USER_COMMAND_MARK, script=corrected_command.script, @@ -69,7 +78,8 @@ def confirm_text(corrected_command): green=color(colorama.Fore.GREEN), red=color(colorama.Fore.RED), reset=color(colorama.Style.RESET_ALL), - blue=color(colorama.Fore.BLUE))) + blue=color(colorama.Fore.BLUE), + edit=edit_part(show_edit))) def debug(msg): diff --git a/thefuck/shells/fish.py b/thefuck/shells/fish.py index 5147819..bf91934 100644 --- a/thefuck/shells/fish.py +++ b/thefuck/shells/fish.py @@ -127,3 +127,10 @@ class Fish(Generic): history.write(entry.encode('utf-8')) else: history.write(entry) + + def can_edit(self): + return True + + def edit_command(self, command): + """Return the shell editable command""" + return u'commandline -r "{}"'.format(command) diff --git a/thefuck/shells/generic.py b/thefuck/shells/generic.py index aa81e2a..2b44a74 100644 --- a/thefuck/shells/generic.py +++ b/thefuck/shells/generic.py @@ -121,6 +121,13 @@ class Generic(object): """ + def can_edit(self): + return False + + def edit_command(self, command): + """Return the shell editable command""" + return command + def get_builtin_commands(self): """Returns shells builtin commands.""" return ['alias', 'bg', 'bind', 'break', 'builtin', 'case', 'cd', diff --git a/thefuck/types.py b/thefuck/types.py index 8c5770f..92e64d1 100644 --- a/thefuck/types.py +++ b/thefuck/types.py @@ -209,6 +209,7 @@ class CorrectedCommand(object): self.script = script self.side_effect = side_effect self.priority = priority + self.should_edit = False def __eq__(self, other): """Ignores `priority` field.""" @@ -232,6 +233,9 @@ class CorrectedCommand(object): of running fuck in case fixed command fails again. """ + if self.should_edit: + self.script = shell.edit_command(self.script) + if settings.repeat: repeat_fuck = '{} --repeat {}--force-command {}'.format( get_alias(), @@ -241,6 +245,10 @@ class CorrectedCommand(object): else: return self.script + def edit(self): + self.should_edit = True + return self + def run(self, old_cmd): """Runs command from rule for passed command. diff --git a/thefuck/ui.py b/thefuck/ui.py index 9c05db3..573fb36 100644 --- a/thefuck/ui.py +++ b/thefuck/ui.py @@ -3,21 +3,24 @@ import sys from .conf import settings from .exceptions import NoRuleMatched +from .shells import shell from .system import get_key from .utils import get_alias from . import logs, const -def read_actions(): +def read_actions(can_edit): """Yields actions for pressed keys.""" while True: key = get_key() - # Handle arrows, j/k (qwerty), and n/e (colemak) + # Handle arrows, edit, j/k (qwerty), and n/e (colemak) if key in (const.KEY_UP, const.KEY_CTRL_N, 'k', 'e'): yield const.ACTION_PREVIOUS elif key in (const.KEY_DOWN, const.KEY_CTRL_P, 'j', 'n'): yield const.ACTION_NEXT + elif can_edit and key in (const.KEY_BACKSPACE, 'd'): + yield const.ACTION_EDIT elif key in (const.KEY_CTRL_C, 'q'): yield const.ACTION_ABORT elif key in ('\n', '\r'): @@ -78,18 +81,21 @@ def select_command(corrected_commands): logs.show_corrected_command(selector.value) return selector.value - logs.confirm_text(selector.value) + logs.confirm_text(selector.value, shell.can_edit()) - for action in read_actions(): + for action in read_actions(shell.can_edit()): if action == const.ACTION_SELECT: sys.stderr.write('\n') return selector.value + elif action == const.ACTION_EDIT: + sys.stderr.write('\n') + return selector.value.edit() elif action == const.ACTION_ABORT: logs.failed('\nAborted') return elif action == const.ACTION_PREVIOUS: selector.previous() - logs.confirm_text(selector.value) + logs.confirm_text(selector.value, shell.can_edit()) elif action == const.ACTION_NEXT: selector.next() - logs.confirm_text(selector.value) + logs.confirm_text(selector.value, shell.can_edit()) From f96406cd71a0e03e398915700ebd2c969d0bf5ca Mon Sep 17 00:00:00 2001 From: Josh Date: Sun, 1 Mar 2020 12:16:56 -0600 Subject: [PATCH 2/4] #N/A: Edit commands using the default editor This PR to implement a feature that would allow you to edit the suggested command in the editor that is set in $EDITOR --- thefuck/shells/bash.py | 3 +++ thefuck/shells/generic.py | 20 ++++++++++++++++++-- thefuck/shells/zsh.py | 3 +++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/thefuck/shells/bash.py b/thefuck/shells/bash.py index fa7a207..7309300 100644 --- a/thefuck/shells/bash.py +++ b/thefuck/shells/bash.py @@ -65,6 +65,9 @@ class Bash(Generic): return dict(self._parse_alias(alias) for alias in raw_aliases if alias and '=' in alias) + def can_edit(self): + return True + def _get_history_file_name(self): return os.environ.get("HISTFILE", os.path.expanduser('~/.bash_history')) diff --git a/thefuck/shells/generic.py b/thefuck/shells/generic.py index 2b44a74..2c81860 100644 --- a/thefuck/shells/generic.py +++ b/thefuck/shells/generic.py @@ -125,8 +125,24 @@ class Generic(object): return False def edit_command(self, command): - """Return the shell editable command""" - return command + """Spawn default editor (or `vi` if not set) and edit command in a buffer""" + # Create a temporary file and write some default text + # mktemp somewhere + file_path = "The Fuck: Command Edit" + with open(file_path, "w") as file_handle: + file_handle.write(command) + + editor = os.getenv("EDITOR", "vi") + + os.system(u"{} '{}' >/dev/tty".format(editor, file_path)) + + data = None + with open(file_path, "r") as file_handle: + data = file_handle.read() + + os.remove(file_path) + + return data def get_builtin_commands(self): """Returns shells builtin commands.""" diff --git a/thefuck/shells/zsh.py b/thefuck/shells/zsh.py index e1fdf20..4a2da32 100644 --- a/thefuck/shells/zsh.py +++ b/thefuck/shells/zsh.py @@ -64,6 +64,9 @@ class Zsh(Generic): value = value[1:-1] return name, value + def can_edit(self): + return True + @memoize def get_aliases(self): raw_aliases = os.environ.get('TF_SHELL_ALIASES', '').split('\n') From 3c1fa55ee1aaf2117000279c504170d8e9566664 Mon Sep 17 00:00:00 2001 From: Josh Martin Date: Sun, 5 Apr 2020 22:39:47 -0400 Subject: [PATCH 3/4] Switched to using tempfiles still need to test this --- thefuck/shells/generic.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/thefuck/shells/generic.py b/thefuck/shells/generic.py index 2c81860..be2a88b 100644 --- a/thefuck/shells/generic.py +++ b/thefuck/shells/generic.py @@ -2,6 +2,8 @@ import io import os import shlex import six +import tempfile +from subprocess import call from collections import namedtuple from ..logs import warn from ..utils import memoize @@ -128,21 +130,16 @@ class Generic(object): """Spawn default editor (or `vi` if not set) and edit command in a buffer""" # Create a temporary file and write some default text # mktemp somewhere - file_path = "The Fuck: Command Edit" - with open(file_path, "w") as file_handle: - file_handle.write(command) - + edited_command = None editor = os.getenv("EDITOR", "vi") - os.system(u"{} '{}' >/dev/tty".format(editor, file_path)) + with tempfile.TemporaryFile(prefix="the_fuck/command_edit__") as fp: + # open named temp file in default editor + fp.write(b'{}'.format(command)) + call([editor, fp.name]) + edited_command = fp.read() - data = None - with open(file_path, "r") as file_handle: - data = file_handle.read() - - os.remove(file_path) - - return data + return edited_command def get_builtin_commands(self): """Returns shells builtin commands.""" From d43bff511cd5acb31eb0de9b075a154cd87f5119 Mon Sep 17 00:00:00 2001 From: Josh Martin Date: Mon, 6 Apr 2020 18:38:33 -0400 Subject: [PATCH 4/4] Tested edit command --- thefuck/shells/generic.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/thefuck/shells/generic.py b/thefuck/shells/generic.py index be2a88b..d542bfe 100644 --- a/thefuck/shells/generic.py +++ b/thefuck/shells/generic.py @@ -3,7 +3,6 @@ import os import shlex import six import tempfile -from subprocess import call from collections import namedtuple from ..logs import warn from ..utils import memoize @@ -130,16 +129,25 @@ class Generic(object): """Spawn default editor (or `vi` if not set) and edit command in a buffer""" # Create a temporary file and write some default text # mktemp somewhere - edited_command = None + editor = os.getenv("EDITOR", "vi") - with tempfile.TemporaryFile(prefix="the_fuck/command_edit__") as fp: - # open named temp file in default editor - fp.write(b'{}'.format(command)) - call([editor, fp.name]) - edited_command = fp.read() + tf = tempfile.NamedTemporaryFile( + prefix="the_fuck-command_edit__", + suffix=".tmp", + delete=False) + tf.write(command.encode('utf8')) + tf.close() - return edited_command + os.system(u"{} '{}' >/dev/tty".format(editor, tf.name)) + + tf = open(tf.name, 'r') + edited_message = tf.read() + tf.close() + + os.unlink(tf.name) + + return edited_message def get_builtin_commands(self): """Returns shells builtin commands."""