1
0
mirror of https://github.com/nvbn/thefuck.git synced 2025-03-20 09:39:01 +00:00

Merge d43bff511cd5acb31eb0de9b075a154cd87f5119 into c7e7e1d884d3bb241ea6448f72a989434c2a35ec

This commit is contained in:
Josh Martin 2024-05-11 07:31:10 +08:00 committed by GitHub
commit a79c64fccb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 116 additions and 16 deletions

View File

@ -43,6 +43,18 @@ class TestCorrectedCommand(object):
out, _ = capsys.readouterr() out, _ = capsys.readouterr()
assert out == printed assert out == 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): class TestRule(object):
def test_from_path_rule_exception(self, mocker): def test_from_path_rule_exception(self, mocker):

View File

@ -16,12 +16,15 @@ def patch_get_key(monkeypatch):
return patch 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([ patch_get_key([
# Enter: # Enter:
'\n', '\n',
# Enter: # Enter:
'\r', '\r',
# Edit:
const.KEY_BACKSPACE, 'd',
# Ignored: # Ignored:
'x', 'y', 'x', 'y',
# Up: # Up:
@ -30,11 +33,17 @@ def test_read_actions(patch_get_key):
const.KEY_DOWN, 'j', const.KEY_DOWN, 'j',
# Ctrl+C: # Ctrl+C:
const.KEY_CTRL_C, 'q']) const.KEY_CTRL_C, 'q'])
assert (list(islice(ui.read_actions(), 8)) expected_actions = [const.ACTION_SELECT, const.ACTION_SELECT,
== [const.ACTION_SELECT, const.ACTION_SELECT, const.ACTION_PREVIOUS, const.ACTION_PREVIOUS,
const.ACTION_PREVIOUS, const.ACTION_PREVIOUS, const.ACTION_NEXT, const.ACTION_NEXT,
const.ACTION_NEXT, const.ACTION_NEXT, const.ACTION_ABORT, const.ACTION_ABORT]
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(): def test_command_selector():
@ -106,3 +115,14 @@ class TestSelectCommand(object):
u'{mark}\x1b[1K\rcd [enter/↑/↓/ctrl+c]\n' u'{mark}\x1b[1K\rcd [enter/↑/↓/ctrl+c]\n'
).format(mark=const.USER_COMMAND_MARK) ).format(mark=const.USER_COMMAND_MARK)
assert capsys.readouterr() == ('', stderr) 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)

View File

@ -14,15 +14,18 @@ KEY_DOWN = _GenConst('↓')
KEY_CTRL_C = _GenConst('Ctrl+C') KEY_CTRL_C = _GenConst('Ctrl+C')
KEY_CTRL_N = _GenConst('Ctrl+N') KEY_CTRL_N = _GenConst('Ctrl+N')
KEY_CTRL_P = _GenConst('Ctrl+P') KEY_CTRL_P = _GenConst('Ctrl+P')
KEY_BACKSPACE = _GenConst('Backspace')
KEY_MAPPING = {'\x0e': KEY_CTRL_N, KEY_MAPPING = {'\x0e': KEY_CTRL_N,
'\x03': KEY_CTRL_C, '\x03': KEY_CTRL_C,
'\x10': KEY_CTRL_P} '\x10': KEY_CTRL_P,
'\x7f': KEY_BACKSPACE}
ACTION_SELECT = _GenConst('select') ACTION_SELECT = _GenConst('select')
ACTION_ABORT = _GenConst('abort') ACTION_ABORT = _GenConst('abort')
ACTION_PREVIOUS = _GenConst('previous') ACTION_PREVIOUS = _GenConst('previous')
ACTION_NEXT = _GenConst('next') ACTION_NEXT = _GenConst('next')
ACTION_EDIT = _GenConst('edit')
ALL_ENABLED = _GenConst('All rules enabled') ALL_ENABLED = _GenConst('All rules enabled')
DEFAULT_RULES = [ALL_ENABLED] DEFAULT_RULES = [ALL_ENABLED]

View File

@ -56,10 +56,19 @@ def show_corrected_command(corrected_command):
reset=color(colorama.Style.RESET_ALL))) 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( sys.stderr.write(
(u'{prefix}{clear}{bold}{script}{reset}{side_effect} ' (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( u'/{red}ctrl+c{reset}]').format(
prefix=const.USER_COMMAND_MARK, prefix=const.USER_COMMAND_MARK,
script=corrected_command.script, script=corrected_command.script,
@ -69,7 +78,8 @@ def confirm_text(corrected_command):
green=color(colorama.Fore.GREEN), green=color(colorama.Fore.GREEN),
red=color(colorama.Fore.RED), red=color(colorama.Fore.RED),
reset=color(colorama.Style.RESET_ALL), reset=color(colorama.Style.RESET_ALL),
blue=color(colorama.Fore.BLUE))) blue=color(colorama.Fore.BLUE),
edit=edit_part(show_edit)))
def debug(msg): def debug(msg):

View File

@ -65,6 +65,9 @@ class Bash(Generic):
return dict(self._parse_alias(alias) return dict(self._parse_alias(alias)
for alias in raw_aliases if alias and '=' in alias) for alias in raw_aliases if alias and '=' in alias)
def can_edit(self):
return True
def _get_history_file_name(self): def _get_history_file_name(self):
return os.environ.get("HISTFILE", return os.environ.get("HISTFILE",
os.path.expanduser('~/.bash_history')) os.path.expanduser('~/.bash_history'))

View File

@ -127,3 +127,10 @@ class Fish(Generic):
history.write(entry.encode('utf-8')) history.write(entry.encode('utf-8'))
else: else:
history.write(entry) 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)

View File

@ -2,6 +2,7 @@ import io
import os import os
import shlex import shlex
import six import six
import tempfile
from collections import namedtuple from collections import namedtuple
from ..logs import warn from ..logs import warn
from ..utils import memoize from ..utils import memoize
@ -121,6 +122,33 @@ class Generic(object):
""" """
def can_edit(self):
return False
def edit_command(self, 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
editor = os.getenv("EDITOR", "vi")
tf = tempfile.NamedTemporaryFile(
prefix="the_fuck-command_edit__",
suffix=".tmp",
delete=False)
tf.write(command.encode('utf8'))
tf.close()
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): def get_builtin_commands(self):
"""Returns shells builtin commands.""" """Returns shells builtin commands."""
return ['alias', 'bg', 'bind', 'break', 'builtin', 'case', 'cd', return ['alias', 'bg', 'bind', 'break', 'builtin', 'case', 'cd',

View File

@ -64,6 +64,9 @@ class Zsh(Generic):
value = value[1:-1] value = value[1:-1]
return name, value return name, value
def can_edit(self):
return True
@memoize @memoize
def get_aliases(self): def get_aliases(self):
raw_aliases = os.environ.get('TF_SHELL_ALIASES', '').split('\n') raw_aliases = os.environ.get('TF_SHELL_ALIASES', '').split('\n')

View File

@ -212,6 +212,7 @@ class CorrectedCommand(object):
self.script = script self.script = script
self.side_effect = side_effect self.side_effect = side_effect
self.priority = priority self.priority = priority
self.should_edit = False
def __eq__(self, other): def __eq__(self, other):
"""Ignores `priority` field.""" """Ignores `priority` field."""
@ -235,6 +236,9 @@ class CorrectedCommand(object):
of running fuck in case fixed command fails again. of running fuck in case fixed command fails again.
""" """
if self.should_edit:
self.script = shell.edit_command(self.script)
if settings.repeat: if settings.repeat:
repeat_fuck = '{} --repeat {}--force-command {}'.format( repeat_fuck = '{} --repeat {}--force-command {}'.format(
get_alias(), get_alias(),
@ -244,6 +248,10 @@ class CorrectedCommand(object):
else: else:
return self.script return self.script
def edit(self):
self.should_edit = True
return self
def run(self, old_cmd): def run(self, old_cmd):
"""Runs command from rule for passed command. """Runs command from rule for passed command.

View File

@ -3,21 +3,24 @@
import sys import sys
from .conf import settings from .conf import settings
from .exceptions import NoRuleMatched from .exceptions import NoRuleMatched
from .shells import shell
from .system import get_key from .system import get_key
from .utils import get_alias from .utils import get_alias
from . import logs, const from . import logs, const
def read_actions(): def read_actions(can_edit):
"""Yields actions for pressed keys.""" """Yields actions for pressed keys."""
while True: while True:
key = get_key() 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'): if key in (const.KEY_UP, const.KEY_CTRL_N, 'k', 'e'):
yield const.ACTION_PREVIOUS yield const.ACTION_PREVIOUS
elif key in (const.KEY_DOWN, const.KEY_CTRL_P, 'j', 'n'): elif key in (const.KEY_DOWN, const.KEY_CTRL_P, 'j', 'n'):
yield const.ACTION_NEXT 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'): elif key in (const.KEY_CTRL_C, 'q'):
yield const.ACTION_ABORT yield const.ACTION_ABORT
elif key in ('\n', '\r'): elif key in ('\n', '\r'):
@ -78,18 +81,21 @@ def select_command(corrected_commands):
logs.show_corrected_command(selector.value) logs.show_corrected_command(selector.value)
return 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: if action == const.ACTION_SELECT:
sys.stderr.write('\n') sys.stderr.write('\n')
return selector.value return selector.value
elif action == const.ACTION_EDIT:
sys.stderr.write('\n')
return selector.value.edit()
elif action == const.ACTION_ABORT: elif action == const.ACTION_ABORT:
logs.failed('\nAborted') logs.failed('\nAborted')
return return
elif action == const.ACTION_PREVIOUS: elif action == const.ACTION_PREVIOUS:
selector.previous() selector.previous()
logs.confirm_text(selector.value) logs.confirm_text(selector.value, shell.can_edit())
elif action == const.ACTION_NEXT: elif action == const.ACTION_NEXT:
selector.next() selector.next()
logs.confirm_text(selector.value) logs.confirm_text(selector.value, shell.can_edit())