diff --git a/mock/local_test.py b/mock/local_test.py
index 9e20232..12e4c38 100644
--- a/mock/local_test.py
+++ b/mock/local_test.py
@@ -1,61 +1,102 @@
-import math
+__all__ = ["AutoCompleter"]
+
+from prompt_toolkit import PromptSession, HTML
+from prompt_toolkit.completion import Completer, Completion
+from prompt_toolkit.document import Document
+from prompt_toolkit.history import History, ThreadedHistory, FileHistory, InMemoryHistory
+from prompt_toolkit.key_binding import KeyBindings
-def estimate_nth_prime_upper_bound(n: int):
- if n < 6:
- return 15
-
- log_n = math.log(n)
- log_log_n = math.log(log_n)
-
- if n < 100:
- return int(n * (log_n + log_log_n) * 1.5)
- elif n < 1000:
- return int(n * (log_n + log_log_n) * 1.3)
- elif n >= 8009824:
- return int(n * (log_n + log_log_n - 1 + 1.8 * log_log_n / log_n))
- else:
- return int(n * (log_n + log_log_n - 1 + 2.0 * log_log_n / log_n))
+class HistoryCompleter(Completer):
+ def __init__(self, history_container: History, static_commands: set[str]) -> None:
+ self.history_container: History = history_container
+ self.static_commands: set[str] = static_commands
+
+ def get_completions(self, document: Document, complete_event):
+ text: str = document.text_before_cursor
+ history_items: set[str] = set(self.history_container.load_history_strings())
+ all_candidates: set[str] = history_items.union(self.static_commands)
+ matches: list[str] = sorted(cmd for cmd in all_candidates if cmd.startswith(text))
+
+ if not matches:
+ return
+
+ for match in matches:
+ yield Completion(
+ match,
+ start_position=-len(text),
+ display=match
+ )
+
+ @staticmethod
+ def _find_common_prefix(matches: list[str]) -> str:
+ if not matches:
+ return ""
+ common: str = matches[0]
+ for match in matches[1:]:
+ i: int = 0
+ while i < len(common) and i < len(match) and common[i] == match[i]:
+ i += 1
+ common = common[:i]
+ return common
-def odd_dig_primes(n: int) -> list[int]:
- nums = {k: True for k in range(2, n+1)}
-
- for num, is_checkable in nums.items():
- if not is_checkable:
- continue
-
- if nums[2]:
- nums[2] = False
-
- for x in range(num * num, n, num):
- nums[x] = False
-
- primes = len([x for x in nums.items() if x[1]])
- max_prime = max([x[0] for x in nums.items() if x[1]])
-
- upper_bound = estimate_nth_prime_upper_bound(primes+1)
- print(upper_bound)
- nums2 = {k: True for k in range(2, upper_bound)}
+class AutoCompleter:
+ def __init__(
+ self,
+ history_filename: str | None = None,
+ autocomplete_button: str = "tab"
+ ) -> None:
+ self.history_filename: str | None = history_filename
+ self.autocomplete_button: str = autocomplete_button
+ self._session: PromptSession | None = None
- for num, is_checkable in nums2.items():
- if not is_checkable:
- continue
-
- if nums2[2]:
- nums2[2] = False
-
- for x in range(num * num, upper_bound, num):
- nums2[x] = False
-
- print([x for x in nums2.items() if x[1]])
+ def initial_setup(self, all_commands: set[str]) -> None:
+ kb = KeyBindings()
- next_prime_after_max = [x[0] for x in nums2.items() if x[1]][-1]
+ def _(event):
+ buff = event.app.current_buffer
- return [
- primes,
- max_prime,
- next_prime_after_max
- ]
-
-print(odd_dig_primes(13))
\ No newline at end of file
+ if buff.complete_state:
+ buff.complete_next()
+ else:
+ completions = list(buff.completer.get_completions(buff.document, None))
+ if len(completions) == 1:
+ buff.apply_completion(completions[0])
+ else:
+ buff.start_completion(select_first=False)
+
+ kb.add(self.autocomplete_button)(_)
+
+ if self.history_filename:
+ history = FileHistory(self.history_filename)
+ history = ThreadedHistory(history)
+ else:
+ history = InMemoryHistory()
+
+ self._session = PromptSession(
+ history=history,
+ completer=HistoryCompleter(history, all_commands),
+ complete_while_typing=False,
+ key_bindings=kb,
+ )
+
+ def prompt(self, prompt_text: str | HTML = ">>> ") -> str:
+ if self._session is None:
+ raise RuntimeError("Call initial_setup() before using prompt()")
+ return self._session.prompt(
+ HTML(f"{prompt_text}") if isinstance(prompt_text, str) else prompt_text
+ )
+
+
+if __name__ == "__main__":
+ test_commands: set[str] = {"start", "qwertyu", "stop", "exit"}
+ hist_file: str = "history.txt"
+
+ ac: AutoCompleter = AutoCompleter(autocomplete_button='tab')
+ ac.initial_setup(test_commands)
+
+ while True:
+ inp: str = ac.prompt(">>> ").strip()
+ if inp == "exit":
+ break
diff --git a/pyproject.toml b/pyproject.toml
index eeabf62..0c68235 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -11,6 +11,7 @@ dependencies = [
"art (>=6.4,<7.0)",
"pyreadline3>=3.5.4; sys_platform == 'win32'",
"dishka>=1.7.2",
+ "prompt-toolkit>=3.0.52",
]
[dependency-groups]
diff --git a/src/argenta/app/autocompleter/entity.py b/src/argenta/app/autocompleter/entity.py
index a50bad0..db0f6f1 100644
--- a/src/argenta/app/autocompleter/entity.py
+++ b/src/argenta/app/autocompleter/entity.py
@@ -1,97 +1,89 @@
__all__ = ["AutoCompleter"]
-import os
-import readline
-from typing import Never
+from prompt_toolkit import PromptSession, HTML
+from prompt_toolkit.completion import Completer, Completion
+from prompt_toolkit.document import Document
+from prompt_toolkit.history import History, ThreadedHistory, FileHistory, InMemoryHistory
+from prompt_toolkit.key_binding import KeyBindings
+
+
+class HistoryCompleter(Completer):
+ def __init__(self, history_container: History, static_commands: set[str]) -> None:
+ self.history_container: History = history_container
+ self.static_commands: set[str] = static_commands
+
+ def get_completions(self, document: Document, complete_event):
+ text: str = document.text_before_cursor
+ history_items: set[str] = set(self.history_container.load_history_strings())
+ all_candidates: set[str] = history_items.union(self.static_commands)
+ matches: list[str] = sorted(cmd for cmd in all_candidates if cmd.startswith(text))
+
+ if not matches:
+ return
+
+ for match in matches:
+ yield Completion(
+ match,
+ start_position=-len(text),
+ display=match
+ )
+
+ @staticmethod
+ def _find_common_prefix(matches: list[str]) -> str:
+ if not matches:
+ return ""
+ common: str = matches[0]
+ for match in matches[1:]:
+ i: int = 0
+ while i < len(common) and i < len(match) and common[i] == match[i]:
+ i += 1
+ common = common[:i]
+ return common
class AutoCompleter:
def __init__(
- self, history_filename: str | None = None, autocomplete_button: str = "tab"
+ self,
+ history_filename: str | None = None,
+ autocomplete_button: str = "tab"
) -> None:
- """
- Public. Configures and implements auto-completion of input command
- :param history_filename: the name of the file for saving the history of the autocompleter
- :param autocomplete_button: the button for auto-completion
- :return: None
- """
self.history_filename: str | None = history_filename
self.autocomplete_button: str = autocomplete_button
-
- def _complete(self, text: str, state: int) -> str | None:
- """
- Private. Auto-completion function
- :param text: part of the command being entered
- :param state: the current cursor position is relative to the beginning of the line
- :return: the desired candidate as str or None
- """
- matches: list[str] = sorted(
- cmd for cmd in _get_history_items() if cmd.startswith(text)
- )
- if len(matches) > 1:
- common_prefix = matches[0]
- for match in matches[1:]:
- i = 0
- while (
- i < len(common_prefix)
- and i < len(match)
- and common_prefix[i] == match[i]
- ):
- i += 1
- common_prefix = common_prefix[:i]
- if state == 0:
- readline.insert_text(common_prefix[len(text) :])
- readline.redisplay()
- return None
- elif len(matches) == 1:
- return matches[0] if state == 0 else None
- else:
- return None
+ self._session: PromptSession | None = None
def initial_setup(self, all_commands: set[str]) -> None:
- """
- Private. Initial setup function
- :param all_commands: Registered commands for adding them to the autocomplete history
- :return: None
- """
- if self.history_filename:
- if os.path.exists(self.history_filename):
- readline.read_history_file(self.history_filename)
+ kb = KeyBindings()
+
+ def _(event):
+ buff = event.app.current_buffer
+
+ if buff.complete_state:
+ buff.complete_next()
else:
- for line in all_commands:
- readline.add_history(line)
+ completions = list(buff.completer.get_completions(buff.document, None))
+ if len(completions) == 1:
+ buff.apply_completion(completions[0])
+ else:
+ buff.start_completion(select_first=False)
- if not self.history_filename:
- for line in all_commands:
- readline.add_history(line)
+ kb.add(self.autocomplete_button)(_)
- readline.set_completer(self._complete)
- readline.set_completer_delims(readline.get_completer_delims().replace(" ", ""))
- readline.parse_and_bind(f"{self.autocomplete_button}: complete")
-
- def exit_setup(self, all_commands: set[str]) -> None:
- """
- Private. Exit setup function
- :return: None
- """
if self.history_filename:
- readline.write_history_file(self.history_filename)
- with open(self.history_filename, "r") as history_file:
- raw_history = history_file.read()
- pretty_history: list[str] = []
- for line in set(raw_history.strip().split("\n")):
- if line.split()[0] in all_commands:
- pretty_history.append(line)
- with open(self.history_filename, "w") as history_file:
- _ = history_file.write("\n".join(pretty_history))
+ history = FileHistory(self.history_filename)
+ history = ThreadedHistory(history)
+ else:
+ history = InMemoryHistory()
+ self._session = PromptSession(
+ history=history,
+ completer=HistoryCompleter(history, all_commands),
+ complete_while_typing=False,
+ key_bindings=kb,
+ )
-def _get_history_items() -> list[str] | list[Never]:
- """
- Private. Returns a list of all commands entered by the user
- :return: all commands entered by the user as list[str] | list[Never]
- """
- return [
- readline.get_history_item(i)
- for i in range(1, readline.get_current_history_length() + 1)
- ]
+ def prompt(self, prompt_text: str | HTML = ">>> ") -> str:
+ if self._session is None:
+ raise RuntimeError("Call initial_setup() before using prompt()")
+ return self._session.prompt(
+ HTML(f"{prompt_text}") if isinstance(prompt_text, str) else prompt_text
+ )
\ No newline at end of file
diff --git a/uv.lock b/uv.lock
index d5fb31f..fe65840 100644
--- a/uv.lock
+++ b/uv.lock
@@ -44,6 +44,7 @@ source = { editable = "." }
dependencies = [
{ name = "art" },
{ name = "dishka" },
+ { name = "prompt-toolkit" },
{ name = "pyreadline3", marker = "sys_platform == 'win32'" },
{ name = "rich" },
]
@@ -75,6 +76,7 @@ typecheckers = [
requires-dist = [
{ name = "art", specifier = ">=6.4,<7.0" },
{ name = "dishka", specifier = ">=1.7.2" },
+ { name = "prompt-toolkit", specifier = ">=3.0.52" },
{ name = "pyreadline3", marker = "sys_platform == 'win32'", specifier = ">=3.5.4" },
{ name = "rich", specifier = ">=14.0.0,<15.0.0" },
]
@@ -589,6 +591,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
+[[package]]
+name = "prompt-toolkit"
+version = "3.0.52"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "wcwidth" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" },
+]
+
[[package]]
name = "pycodestyle"
version = "2.14.0"
@@ -1023,6 +1037,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" },
]
+[[package]]
+name = "wcwidth"
+version = "0.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/38/75/2144b65e4fba12a2d9868e9a3f99db7fa0760670d064603634bef9ff1709/wcwidth-0.3.0.tar.gz", hash = "sha256:af1a2fb0b83ef4a7fc0682a4c95ca2576e14d0280bca2a9e67b7dc9f2733e123", size = 172238, upload-time = "2026-01-21T17:44:09.508Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/18/0e/a5f0257ab47492b7afb5fb60347d14ba19445e2773fc8352d4be6bd2f6f8/wcwidth-0.3.0-py3-none-any.whl", hash = "sha256:073a1acb250e4add96cfd5ef84e0036605cd6e0d0782c8c15c80e42202348458", size = 85520, upload-time = "2026-01-21T17:44:08.002Z" },
+]
+
[[package]]
name = "websockets"
version = "15.0.1"