From a2dd66428daa115df8d332cda8ba39344436d13f Mon Sep 17 00:00:00 2001 From: lazmond3 Date: Sat, 3 Aug 2024 17:16:19 +0900 Subject: [PATCH 1/7] fzf history search feature --- mycli/key_bindings.py | 8 ++++ mycli/main.py | 4 +- mycli/packages/toolkit/__init__.py | 0 mycli/packages/toolkit/fzf.py | 45 ++++++++++++++++++ mycli/packages/toolkit/history.py | 75 ++++++++++++++++++++++++++++++ setup.py | 3 +- 6 files changed, 132 insertions(+), 3 deletions(-) create mode 100644 mycli/packages/toolkit/__init__.py create mode 100644 mycli/packages/toolkit/fzf.py create mode 100644 mycli/packages/toolkit/history.py diff --git a/mycli/key_bindings.py b/mycli/key_bindings.py index 443233fd..b084849d 100644 --- a/mycli/key_bindings.py +++ b/mycli/key_bindings.py @@ -3,6 +3,8 @@ from prompt_toolkit.filters import completion_is_selected, emacs_mode from prompt_toolkit.key_binding import KeyBindings +from .packages.toolkit.fzf import search_history + _logger = logging.getLogger(__name__) @@ -101,6 +103,12 @@ def _(event): cursorpos_abs -= 1 b.cursor_position = min(cursorpos_abs, len(b.text)) + @kb.add('c-r', filter=emacs_mode) + def _(event): + """Search history using fzf or default reverse incremental search.""" + _logger.debug('Detected key.') + search_history(event) + @kb.add('enter', filter=completion_is_selected) def _(event): """Makes the enter key work as the tab key only when showing the menu. diff --git a/mycli/main.py b/mycli/main.py index ce4dff7e..4c194ced 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -36,7 +36,6 @@ from prompt_toolkit.layout.processors import (HighlightMatchingBracketProcessor, ConditionalProcessor) from prompt_toolkit.lexers import PygmentsLexer -from prompt_toolkit.history import FileHistory from prompt_toolkit.auto_suggest import AutoSuggestFromHistory from .packages.special.main import NO_QUERY @@ -44,6 +43,7 @@ from .packages.tabular_output import sql_format from .packages import special from .packages.special.favoritequeries import FavoriteQueries +from .packages.toolkit.history import FileHistoryWithTimestamp from .sqlcompleter import SQLCompleter from .clitoolbar import create_toolbar_tokens_func from .clistyle import style_factory, style_factory_output @@ -626,7 +626,7 @@ def run_cli(self): history_file = os.path.expanduser( os.environ.get('MYCLI_HISTFILE', '~/.mycli-history')) if dir_path_exists(history_file): - history = FileHistory(history_file) + history = FileHistoryWithTimestamp(history_file) else: history = None self.echo( diff --git a/mycli/packages/toolkit/__init__.py b/mycli/packages/toolkit/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/mycli/packages/toolkit/fzf.py b/mycli/packages/toolkit/fzf.py new file mode 100644 index 00000000..36cb347a --- /dev/null +++ b/mycli/packages/toolkit/fzf.py @@ -0,0 +1,45 @@ +from shutil import which + +from pyfzf import FzfPrompt +from prompt_toolkit import search +from prompt_toolkit.key_binding.key_processor import KeyPressEvent + +from .history import FileHistoryWithTimestamp + + +class Fzf(FzfPrompt): + def __init__(self): + self.executable = which("fzf") + if self.executable: + super().__init__() + + def is_available(self) -> bool: + return self.executable is not None + + +def search_history(event: KeyPressEvent): + buffer = event.current_buffer + history = buffer.history + + fzf = Fzf() + + if fzf.is_available() and isinstance(history, FileHistoryWithTimestamp): + history_items_with_timestamp = history.load_history_with_timestamp() + + formatted_history_items = [] + original_history_items = [] + for item, timestamp in history_items_with_timestamp: + formatted_item = item.replace('\n', ' ') + timestamp = timestamp.split(".")[0] if "." in timestamp else timestamp + formatted_history_items.append(f"{timestamp} {formatted_item}") + original_history_items.append(item) + + result = fzf.prompt(formatted_history_items, fzf_options="--tiebreak=index") + + if result: + selected_index = formatted_history_items.index(result[0]) + buffer.text = original_history_items[selected_index] + buffer.cursor_position = len(buffer.text) + else: + # Fallback to default reverse incremental search + search.start_search(direction=search.SearchDirection.BACKWARD) diff --git a/mycli/packages/toolkit/history.py b/mycli/packages/toolkit/history.py new file mode 100644 index 00000000..d1717158 --- /dev/null +++ b/mycli/packages/toolkit/history.py @@ -0,0 +1,75 @@ +import os +from typing import Iterable, Union, List, Tuple + +from prompt_toolkit.history import FileHistory + +_StrOrBytesPath = Union[str, bytes, os.PathLike] + + +class FileHistoryWithTimestamp(FileHistory): + """ + :class:`.FileHistory` class that stores all strings in a file with timestamp. + """ + + def __init__(self, filename: _StrOrBytesPath) -> None: + self.filename = filename + super().__init__(filename) + + def load_history_strings(self) -> Iterable[str]: + strings: list[str] = [] + lines: list[str] = [] + + def add() -> None: + if lines: + string = "".join(lines)[:-1] + strings.append(string) + + if os.path.exists(self.filename): + with open(self.filename, "rb") as f: + for line_bytes in f: + line = line_bytes.decode("utf-8", errors="replace") + if line.startswith("+"): + lines.append(line[1:]) + else: + add() + lines = [] + + add() + + return reversed(strings) + + def load_history_with_timestamp(self) -> List[Tuple[str, str]]: + """ + Load history entries along with their timestamps. + + Returns: + List[Tuple[str, str]]: A list of tuples where each tuple contains + a history entry and its corresponding timestamp. + """ + history_with_timestamp: List[Tuple[str, str]] = [] + lines: List[str] = [] + timestamp: str = "" + + def add() -> None: + if lines: + # Join and drop trailing newline. + string = "".join(lines)[:-1] + history_with_timestamp.append((string, timestamp)) + + if os.path.exists(self.filename): + with open(self.filename, "rb") as f: + for line_bytes in f: + line = line_bytes.decode("utf-8", errors="replace") + + if line.startswith("#"): + # Extract timestamp + timestamp = line[2:].strip() + elif line.startswith("+"): + lines.append(line[1:]) + else: + add() + lines = [] + + add() + + return list(reversed(history_with_timestamp)) diff --git a/setup.py b/setup.py index 2c4f9e18..c7f93331 100755 --- a/setup.py +++ b/setup.py @@ -29,7 +29,8 @@ 'configobj >= 5.0.5', 'cli_helpers[styles] >= 2.2.1', 'pyperclip >= 1.8.1', - 'pyaes >= 1.6.1' + 'pyaes >= 1.6.1', + 'pyfzf >= 0.3.1', ] if sys.version_info.minor < 9: From a8495bdd822608fa507502cfe226358adcb72d2e Mon Sep 17 00:00:00 2001 From: lazmond3 Date: Sat, 3 Aug 2024 18:01:07 +0900 Subject: [PATCH 2/7] Add me to Contributors and write changelog.md --- changelog.md | 2 +- mycli/AUTHORS | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index fb32e5af..d279489b 100644 --- a/changelog.md +++ b/changelog.md @@ -23,7 +23,7 @@ Bug Fixes: 1.27.1 (2024/03/28) =================== - +* Added fzf-like history search functionality. The feature can switch between the old implementation and the new one based on the presence of the fzf binary. Bug Fixes: ---------- diff --git a/mycli/AUTHORS b/mycli/AUTHORS index baa9adaa..d5a9ce08 100644 --- a/mycli/AUTHORS +++ b/mycli/AUTHORS @@ -97,6 +97,7 @@ Contributors: * Zhanze Wang * Houston Wong * Mohamed Rezk + * Ryosuke Kazami Created by: From b21dfbfc39654cf08f8d00900fdeda1604b6974c Mon Sep 17 00:00:00 2001 From: lazmond3 Date: Wed, 7 Aug 2024 08:53:19 +0900 Subject: [PATCH 3/7] Remove redundant method definition load_history_strings --- mycli/packages/toolkit/history.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/mycli/packages/toolkit/history.py b/mycli/packages/toolkit/history.py index d1717158..75f4a5a2 100644 --- a/mycli/packages/toolkit/history.py +++ b/mycli/packages/toolkit/history.py @@ -15,29 +15,6 @@ def __init__(self, filename: _StrOrBytesPath) -> None: self.filename = filename super().__init__(filename) - def load_history_strings(self) -> Iterable[str]: - strings: list[str] = [] - lines: list[str] = [] - - def add() -> None: - if lines: - string = "".join(lines)[:-1] - strings.append(string) - - if os.path.exists(self.filename): - with open(self.filename, "rb") as f: - for line_bytes in f: - line = line_bytes.decode("utf-8", errors="replace") - if line.startswith("+"): - lines.append(line[1:]) - else: - add() - lines = [] - - add() - - return reversed(strings) - def load_history_with_timestamp(self) -> List[Tuple[str, str]]: """ Load history entries along with their timestamps. From d11dc90a422bf6065e94c855078a89ed38642191 Mon Sep 17 00:00:00 2001 From: lazmond3 Date: Tue, 10 Sep 2024 11:20:20 +0900 Subject: [PATCH 4/7] Fix ci: setuptools version fixed to <= 71.1.0 for testing --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 3f5fbdfb..603efa20 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -14,4 +14,4 @@ pyperclip>=1.8.1 importlib_resources>=5.0.0 pyaes>=1.6.1 sqlglot>=5.1.3 -setuptools +setuptools<=71.1.0 From 3acf2b00eb9c3715be764dc2f3341fb4d5b8fd31 Mon Sep 17 00:00:00 2001 From: Ryosuke Kazami <68408021+lazmond3@users.noreply.github.com> Date: Fri, 4 Oct 2024 17:01:54 +0900 Subject: [PATCH 5/7] Update changelog.md --- changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index d279489b..823673ba 100644 --- a/changelog.md +++ b/changelog.md @@ -23,7 +23,7 @@ Bug Fixes: 1.27.1 (2024/03/28) =================== -* Added fzf-like history search functionality. The feature can switch between the old implementation and the new one based on the presence of the fzf binary. +* Added fzf history search functionality. The feature can switch between the old implementation and the new one based on the presence of the fzf binary. Bug Fixes: ---------- From e766643982a6062043767904afdf6e893050a0cb Mon Sep 17 00:00:00 2001 From: lazmond3 Date: Thu, 31 Oct 2024 00:43:14 +0900 Subject: [PATCH 6/7] Fix changelog.md --- changelog.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 823673ba..ffe31314 100644 --- a/changelog.md +++ b/changelog.md @@ -11,6 +11,8 @@ Internal: Features: --------- +* Added fzf history search functionality. The feature can switch between the old implementation and the new one based on the presence of the fzf binary. + 1.27.2 (2024/04/03) =================== @@ -23,7 +25,6 @@ Bug Fixes: 1.27.1 (2024/03/28) =================== -* Added fzf history search functionality. The feature can switch between the old implementation and the new one based on the presence of the fzf binary. Bug Fixes: ---------- From 6b2838ef1c1a81999c4304f42acec5582c806e3d Mon Sep 17 00:00:00 2001 From: lazmond3 Date: Thu, 31 Oct 2024 00:44:15 +0900 Subject: [PATCH 7/7] Fix CI and solve #1146 --- mycli/packages/filepaths.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mycli/packages/filepaths.py b/mycli/packages/filepaths.py index 79fe26dc..a91055d2 100644 --- a/mycli/packages/filepaths.py +++ b/mycli/packages/filepaths.py @@ -100,7 +100,7 @@ def guess_socket_location(): for r, dirs, files in os.walk(directory, topdown=True): for filename in files: name, ext = os.path.splitext(filename) - if name.startswith("mysql") and ext in ('.socket', '.sock'): + if name.startswith("mysql") and name != "mysqlx" and ext in ('.socket', '.sock'): return os.path.join(r, filename) dirs[:] = [d for d in dirs if d.startswith("mysql")] return None