|
7 | 7 | """ |
8 | 8 |
|
9 | 9 | import asyncio |
| 10 | +from enum import Enum |
10 | 11 | import inspect |
11 | 12 | import logging |
12 | 13 | import os |
| 14 | +import re |
13 | 15 | import struct |
14 | 16 | from pathlib import Path |
15 | 17 |
|
|
18 | 20 | from bleak.backends.device import BLEDevice |
19 | 21 | from bleak.backends.scanner import AdvertisementData |
20 | 22 | from prompt_toolkit import PromptSession |
| 23 | +from prompt_toolkit.completion import Completer, Completion, FuzzyCompleter |
| 24 | +from prompt_toolkit.document import Document |
21 | 25 | from prompt_toolkit.history import FileHistory |
22 | 26 | from prompt_toolkit.patch_stdout import StdoutProxy, patch_stdout |
23 | 27 |
|
|
42 | 46 |
|
43 | 47 | # The first groups is any type from bytecodes that inherits from int (includes |
44 | 48 | # enums/flags) or bytes. |
45 | | -_eval_pool.update( |
46 | | - { |
47 | | - k: v |
48 | | - for k, v in bytecodes.__dict__.items() |
49 | | - if inspect.isclass(v) |
50 | | - and v.__module__ == bytecodes.__name__ |
51 | | - and (issubclass(v, int) or issubclass(v, bytes)) |
52 | | - } |
53 | | -) |
| 49 | +_PARAMETER_TYPES = { |
| 50 | + k: v |
| 51 | + for k, v in bytecodes.__dict__.items() |
| 52 | + if inspect.isclass(v) |
| 53 | + and v.__module__ == bytecodes.__name__ |
| 54 | + and (issubclass(v, int) or issubclass(v, bytes)) |
| 55 | +} |
| 56 | + |
| 57 | +_eval_pool.update(_PARAMETER_TYPES) |
54 | 58 |
|
55 | 59 | # The second group are all of the non-abstract message types from the messages module. |
56 | | -_eval_pool.update( |
57 | | - { |
58 | | - k: v |
59 | | - for k, v in messages.__dict__.items() |
60 | | - if inspect.isclass(v) |
61 | | - and issubclass(v, AbstractMessage) |
62 | | - and not inspect.isabstract(v) |
63 | | - } |
64 | | -) |
| 60 | +_MESSAGE_KINDS = { |
| 61 | + k: v |
| 62 | + for k, v in messages.__dict__.items() |
| 63 | + if inspect.isclass(v) |
| 64 | + and issubclass(v, AbstractMessage) |
| 65 | + and not inspect.isabstract(v) |
| 66 | +} |
| 67 | + |
| 68 | +_eval_pool.update(_MESSAGE_KINDS) |
| 69 | + |
| 70 | + |
| 71 | +class _CommandCompleter(Completer): |
| 72 | + """ |
| 73 | + Custom completer for command prompt. |
| 74 | + """ |
| 75 | + |
| 76 | + # matches words with dots in them, e.g. "Enum.MEMBER" |
| 77 | + _MATCH_DOT = re.compile(r"[a-zA-Z0-9_\.]+") |
| 78 | + |
| 79 | + def get_completions(self, document: Document, complete_event): |
| 80 | + if document.get_word_before_cursor() == ".": |
| 81 | + # if this is a dotted word, look up the enum member |
| 82 | + cls = _PARAMETER_TYPES.get( |
| 83 | + document.get_word_before_cursor(pattern=self._MATCH_DOT).split(".")[0] |
| 84 | + ) |
| 85 | + if cls and issubclass(cls, Enum): |
| 86 | + for m in cls: |
| 87 | + yield Completion(m.name) |
| 88 | + elif document.find_enclosing_bracket_left("(", ")") is not None: |
| 89 | + # if we are inside of "(...)", list the enums and other parameter types |
| 90 | + for p in _PARAMETER_TYPES.keys(): |
| 91 | + yield Completion(p) |
| 92 | + elif document.get_word_under_cursor() == "": |
| 93 | + # if we are at the beginning of the line, list the commands |
| 94 | + for m in _MESSAGE_KINDS.keys(): |
| 95 | + yield Completion(m) |
65 | 96 |
|
66 | 97 |
|
67 | 98 | async def repl() -> None: |
68 | 99 | """ |
69 | 100 | Provides an interactive REPL for sending and receiving LWP3 messages. |
70 | 101 | """ |
71 | 102 | os.makedirs(history_file.parent, exist_ok=True) |
72 | | - session = PromptSession(history=FileHistory(history_file)) |
| 103 | + |
| 104 | + session = PromptSession( |
| 105 | + history=FileHistory(history_file), |
| 106 | + completer=FuzzyCompleter(_CommandCompleter()), |
| 107 | + ) |
73 | 108 |
|
74 | 109 | def match_lwp3_uuid(dev: BLEDevice, adv: AdvertisementData) -> None: |
75 | 110 | if LWP3_HUB_SERVICE_UUID.lower() not in adv.service_uuids: |
|
0 commit comments