mirror of
https://github.com/nvbn/thefuck.git
synced 2025-03-20 01:28:56 +00:00
Merge d43bff511cd5acb31eb0de9b075a154cd87f5119 into c7e7e1d884d3bb241ea6448f72a989434c2a35ec
This commit is contained in:
commit
a79c64fccb
@ -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):
|
||||||
|
@ -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)
|
||||||
|
@ -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]
|
||||||
|
@ -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):
|
||||||
|
@ -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'))
|
||||||
|
@ -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)
|
||||||
|
@ -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',
|
||||||
|
@ -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')
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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())
|
||||||
|
Loading…
x
Reference in New Issue
Block a user