chore(python): reorder dependency-heavy code
What changed, and why it matters
This is a routine code reorganization in the Python trezorlib library. It moves two helper classes into a new module and moves a user-interface file into the command-line interface package. No security-sensitive behavior is changed, no bugs are fixed, and no vulnerabilities are introduced.
No security action required; treat as normal maintenance.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit is a pure refactor: (1) EnumAdapter and TupleAdapter are relocated from trezorlib.tools to a new trezorlib.construct_helpers module, with all imports updated across firmware parsing and internal modules; (2) trezorlib/ui.py is moved to trezorlib/cli/ui.py because the core library no longer depends on it, and CLI modules now import it from the new location. The file contents are identical except for import path adjustments.
Changed components
python/src/trezorlib/tools.pypython/src/trezorlib/construct_helpers.pypython/src/trezorlib/ui.pypython/src/trezorlib/cli/ui.pypython/src/trezorlib/cli/__init__.pypython/src/trezorlib/cli/device.pyInspect captured patch +285 / −265
diff --git a/python/src/trezorlib/_internal/emu_ble.py b/python/src/trezorlib/_internal/emu_ble.py
index ef8b1bb9..17e24cdd 100644
--- a/python/src/trezorlib/_internal/emu_ble.py
+++ b/python/src/trezorlib/_internal/emu_ble.py
@@ -25,8 +25,8 @@ from typing import TYPE_CHECKING, Iterable, Tuple
import construct as c
from construct_classes import Struct
+from ..construct_helpers import EnumAdapter
from ..log import DUMP_PACKETS
-from ..tools import EnumAdapter
from ..transport import Timeout, Transport, TransportException
from ..transport.udp import UdpTransport
diff --git a/python/src/trezorlib/_internal/translations.py b/python/src/trezorlib/_internal/translations.py
index c897f4cd..d8f58f0c 100644
--- a/python/src/trezorlib/_internal/translations.py
+++ b/python/src/trezorlib/_internal/translations.py
@@ -27,10 +27,10 @@ import construct as c
from construct_classes import Struct, subcon
from typing_extensions import Self, TypedDict
+from ..construct_helpers import EnumAdapter, TupleAdapter
from ..debuglink import LayoutType
from ..firmware.models import Model
from ..models import TrezorModel
-from ..tools import EnumAdapter, TupleAdapter
# All sections need to be aligned to 2 bytes for the offset tables using u16 to work properly
ALIGNMENT = 2
diff --git a/python/src/trezorlib/cli/__init__.py b/python/src/trezorlib/cli/__init__.py
index 21bf34f1..ba62cc75 100644
--- a/python/src/trezorlib/cli/__init__.py
+++ b/python/src/trezorlib/cli/__init__.py
@@ -33,11 +33,11 @@ from pathlib import Path
import click
from typing_extensions import Self
-from .. import exceptions, messages, protocol_v1, transport, ui
+from .. import exceptions, messages, protocol_v1, transport
from ..client import AppManifest, PassphraseSetting, Session, TrezorClient, get_client
from ..thp import client as thp_client
from ..transport import Transport
-from . import credentials
+from . import credentials, ui
LOG = logging.getLogger(__name__)
diff --git a/python/src/trezorlib/cli/device.py b/python/src/trezorlib/cli/device.py
index 5deebb76..2a1b1ff5 100644
--- a/python/src/trezorlib/cli/device.py
+++ b/python/src/trezorlib/cli/device.py
@@ -24,9 +24,9 @@ import typing as t
import click
import requests
-from .. import authentication, debuglink, device, exceptions, messages, ui
+from .. import authentication, debuglink, device, exceptions, messages
from ..tools import format_path
-from . import ChoiceType, with_session
+from . import ChoiceType, ui, with_session
if t.TYPE_CHECKING:
from ..client import Session
diff --git a/python/src/trezorlib/cli/ui.py b/python/src/trezorlib/cli/ui.py
new file mode 100644
index 00000000..b45d5663
--- /dev/null
+++ b/python/src/trezorlib/cli/ui.py
@@ -0,0 +1,224 @@
+# This file is part of the Trezor project.
+#
+# Copyright (C) SatoshiLabs and contributors
+#
+# This library is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3
+# as published by the Free Software Foundation.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the License along with this library.
+# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
+
+from __future__ import annotations
+
+import sys
+import typing as t
+
+import click
+from mnemonic import Mnemonic
+
+from .. import device, messages
+from ..client import MAX_PIN_LENGTH
+from ..exceptions import Cancelled
+from ..messages import PinMatrixRequestType, WordRequestType
+
+PIN_MATRIX_DESCRIPTION = """
+Use the numeric keypad or lowercase letters to describe number positions.
+
+The layout is:
+
+ 7 8 9 e r t
+ 4 5 6 -or- d f g
+ 1 2 3 c v b
+""".strip()
+
+RECOVERY_MATRIX_DESCRIPTION = """
+Use the numeric keypad to describe positions.
+For the word list use only left and right keys.
+Use backspace to correct an entry.
+
+The keypad layout is:
+ 7 8 9 7 | 9
+ 4 5 6 4 | 6
+ 1 2 3 1 | 3
+""".strip()
+
+PIN_GENERIC = None
+PIN_CURRENT = PinMatrixRequestType.Current
+PIN_NEW = PinMatrixRequestType.NewFirst
+PIN_CONFIRM = PinMatrixRequestType.NewSecond
+WIPE_CODE_NEW = PinMatrixRequestType.WipeCodeFirst
+WIPE_CODE_CONFIRM = PinMatrixRequestType.WipeCodeSecond
+
+# Workaround for limitation of Git Bash
+# getpass function does not work correctly on Windows when not using a real terminal
+# (the hidden input is not allowed and it also freezes the script completely)
+# Details: https://bugs.python.org/issue44762
+CAN_HANDLE_HIDDEN_INPUT = sys.stdin and sys.stdin.isatty()
+
+
+def echo(*args: t.Any, **kwargs: t.Any) -> None:
+ return click.echo(*args, err=True, **kwargs)
+
+
+def prompt(text: str, *, hide_input: bool = False, **kwargs: t.Any) -> t.Any:
+ # Disallowing hidden input and warning user when it would cause issues
+ if not CAN_HANDLE_HIDDEN_INPUT and hide_input:
+ hide_input = False
+ text += " (WARNING: will be displayed!)"
+ return click.prompt(text, hide_input=hide_input, err=True, **kwargs)
+
+
+class ClickUI:
+ def __init__(
+ self, always_prompt: bool = False, passphrase_on_host: bool = False
+ ) -> None:
+ self.pinmatrix_shown = False
+ self.last_prompt_shown = ""
+ self.always_prompt = always_prompt
+ self.passphrase_on_host = passphrase_on_host
+
+ def _prompt_for_button(self, br: messages.ButtonRequest) -> str:
+ if br.code == messages.ButtonRequestType.PassphraseEntry:
+ return "Please enter passphrase on your Trezor device."
+ if br.code == messages.ButtonRequestType.PinEntry:
+ return "Please enter PIN on your Trezor device."
+
+ return "Please confirm action on your Trezor device."
+
+ def button_request(self, br: messages.ButtonRequest) -> None:
+ prompt = self._prompt_for_button(br)
+ if prompt != self.last_prompt_shown:
+ echo(prompt)
+ if not self.always_prompt:
+ self.last_prompt_shown = prompt
+
+ def get_pin(self, request: messages.PinMatrixRequest) -> str:
+ code = request.type
+ if code == PIN_CURRENT:
+ desc = "current PIN"
+ elif code == PIN_NEW:
+ desc = "new PIN"
+ elif code == PIN_CONFIRM:
+ desc = "new PIN again"
+ elif code == WIPE_CODE_NEW:
+ desc = "new wipe code"
+ elif code == WIPE_CODE_CONFIRM:
+ desc = "new wipe code again"
+ else:
+ desc = "PIN"
+
+ if not self.pinmatrix_shown:
+ echo(PIN_MATRIX_DESCRIPTION)
+ if not self.always_prompt:
+ self.pinmatrix_shown = True
+
+ while True:
+ try:
+ pin = prompt(f"Please enter {desc}", hide_input=True)
+ except click.Abort:
+ raise Cancelled from None
+
+ # translate letters to numbers if letters were used
+ if all(d in "cvbdfgert" for d in pin):
+ pin = pin.translate(str.maketrans("cvbdfgert", "123456789"))
+
+ if any(d not in "123456789" for d in pin):
+ echo(
+ "The value may only consist of digits 1 to 9 or letters cvbdfgert."
+ )
+ elif len(pin) > MAX_PIN_LENGTH:
+ echo(f"The value must be at most {MAX_PIN_LENGTH} digits in length.")
+ else:
+ return pin
+
+
+class ScriptUI:
+ """Interface to be used by scripts, not directly by user.
+
+ Communicates with a client application using print() and input().
+
+ Lot of `ClickUI` logic is outsourced to the client application, which
+ is responsible for supplying the PIN and passphrase.
+
+ Reference client implementation can be found under `tools/trezorctl_script_client.py`.
+ """
+
+ @staticmethod
+ def button_request(br: messages.ButtonRequest) -> None:
+ code = br.code.name if br.code else None
+ print(f"?BUTTON code={code} pages={br.pages} name={br.name}")
+
+ @staticmethod
+ def get_pin(request: messages.PinMatrixRequest) -> str:
+ code = request.type
+ if code is None:
+ print("?PIN")
+ else:
+ print(f"?PIN code={code.name}")
+
+ pin = input()
+ if pin == "CANCEL":
+ raise Cancelled from None
+ elif not pin.startswith(":"):
+ raise RuntimeError("Sent PIN must start with ':'")
+ else:
+ pin = pin[1:]
+ return pin
+
+
+def mnemonic_words(
+ expand: bool = False, language: str = "english"
+) -> t.Callable[[WordRequestType], str]:
+ if expand:
+ wordlist = Mnemonic(language).wordlist
+ else:
+ wordlist = []
+
+ def expand_word(word: str) -> str:
+ if not expand:
+ return word
+ if word in wordlist:
+ return word
+ matches = [w for w in wordlist if w.startswith(word)]
+ if len(matches) == 1:
+ return matches[0]
+ echo("Choose one of: " + ", ".join(matches))
+ raise KeyError(word)
+
+ def get_word(type: WordRequestType) -> str:
+ assert type == WordRequestType.Plain
+ while True:
+ try:
+ word = prompt("Enter one word of mnemonic")
+ return expand_word(word)
+ except KeyError:
+ pass
+ except click.Abort:
+ raise Cancelled from None
+
+ return get_word
+
+
+def matrix_words(type: WordRequestType) -> str:
+ while True:
+ try:
+ ch = click.getchar()
+ except (KeyboardInterrupt, EOFError):
+ raise Cancelled from None
+
+ if ch in "\x04\x1b":
+ # Ctrl+D, Esc
+ raise Cancelled
+ if ch in "\x08\x7f":
+ # Backspace, Del
+ return device.RECOVERY_BACK
+ if type == WordRequestType.Matrix6 and ch in "147369":
+ return ch
+ if type == WordRequestType.Matrix9 and ch in "123456789":
+ return ch
diff --git a/python/src/trezorlib/construct_helpers.py b/python/src/trezorlib/construct_helpers.py
new file mode 100644
index 00000000..f4e166ad
--- /dev/null
+++ b/python/src/trezorlib/construct_helpers.py
@@ -0,0 +1,50 @@
+# This file is part of the Trezor project.
+#
+# Copyright (C) SatoshiLabs and contributors
+#
+# This library is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3
+# as published by the Free Software Foundation.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the License along with this library.
+# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
+
+from __future__ import annotations
+
+import typing as t
+from enum import Enum
+
+import construct
+
+
+class EnumAdapter(construct.Adapter):
+ def __init__(self, subcon: construct.Adapter, enum: type[Enum]) -> None:
+ self.enum = enum
+ super().__init__(subcon)
+
+ def _encode(self, obj: t.Any, context: t.Any, path: t.Any) -> t.Any:
+ if isinstance(obj, self.enum):
+ return obj.value
+ return obj
+
+ def _decode(self, obj: t.Any, context: t.Any, path: t.Any) -> t.Any:
+ try:
+ return self.enum(obj)
+ except ValueError:
+ return obj
+
+
+class TupleAdapter(construct.Adapter):
+ def __init__(self, *subcons: construct.Adapter) -> None:
+ super().__init__(construct.Sequence(*subcons))
+
+ def _encode(self, obj: t.Any, context: t.Any, path: t.Any) -> t.Any:
+ return obj
+
+ def _decode(self, obj: t.Any, context: t.Any, path: t.Any) -> t.Any:
+ return tuple(obj)
diff --git a/python/src/trezorlib/definitions.py b/python/src/trezorlib/definitions.py
index f7f60e33..ce047fb5 100644
--- a/python/src/trezorlib/definitions.py
+++ b/python/src/trezorlib/definitions.py
@@ -24,8 +24,8 @@ import requests
from construct_classes import Struct, subcon
from . import cosi, merkle_tree
+from .construct_helpers import EnumAdapter
from .messages import DefinitionType
-from .tools import EnumAdapter
LOG = logging.getLogger(__name__)
diff --git a/python/src/trezorlib/firmware/core.py b/python/src/trezorlib/firmware/core.py
index 219d9116..732bb27d 100644
--- a/python/src/trezorlib/firmware/core.py
+++ b/python/src/trezorlib/firmware/core.py
@@ -25,7 +25,7 @@ import construct as c
from construct_classes import Struct, subcon
from .. import cosi, merkle_tree
-from ..tools import EnumAdapter, TupleAdapter
+from ..construct_helpers import EnumAdapter, TupleAdapter
from . import consts, models, util
from .models import Model
from .vendor import VendorHeader
diff --git a/python/src/trezorlib/firmware/secmon.py b/python/src/trezorlib/firmware/secmon.py
index 651f798d..ca02fcb7 100644
--- a/python/src/trezorlib/firmware/secmon.py
+++ b/python/src/trezorlib/firmware/secmon.py
@@ -21,7 +21,7 @@ from copy import copy
import construct as c
from construct_classes import Struct, subcon
-from ..tools import EnumAdapter, TupleAdapter
+from ..construct_helpers import EnumAdapter, TupleAdapter
from . import util
from .models import Model
diff --git a/python/src/trezorlib/firmware/vendor.py b/python/src/trezorlib/firmware/vendor.py
index f96be5e1..202e99ab 100644
--- a/python/src/trezorlib/firmware/vendor.py
+++ b/python/src/trezorlib/firmware/vendor.py
@@ -24,8 +24,8 @@ import construct as c
from construct_classes import Struct, subcon
from .. import cosi
+from ..construct_helpers import EnumAdapter, TupleAdapter
from ..toif import ToifStruct
-from ..tools import EnumAdapter, TupleAdapter
from . import util
from .models import Model
diff --git a/python/src/trezorlib/toif.py b/python/src/trezorlib/toif.py
index b93af7e5..1076bd09 100644
--- a/python/src/trezorlib/toif.py
+++ b/python/src/trezorlib/toif.py
@@ -23,7 +23,7 @@ from typing import Sequence, Tuple
import construct as c
from typing_extensions import Literal
-from .tools import EnumAdapter
+from .construct_helpers import EnumAdapter
try:
# Explanation of having to use "Image.Image" in typing:
diff --git a/python/src/trezorlib/tools.py b/python/src/trezorlib/tools.py
index 135d950d..c8c0940d 100644
--- a/python/src/trezorlib/tools.py
+++ b/python/src/trezorlib/tools.py
@@ -23,9 +23,7 @@ import struct
import typing as t
import unicodedata
from contextlib import AbstractContextManager
-from enum import Enum
-import construct
import typing_extensions as tx
from . import messages
@@ -324,34 +322,6 @@ def descriptor_checksum(desc: str) -> str:
return "".join(ret)
-class EnumAdapter(construct.Adapter):
- def __init__(self, subcon: construct.Adapter, enum: type[Enum]) -> None:
- self.enum = enum
- super().__init__(subcon)
-
- def _encode(self, obj: t.Any, context: t.Any, path: t.Any) -> t.Any:
- if isinstance(obj, self.enum):
- return obj.value
- return obj
-
- def _decode(self, obj: t.Any, context: t.Any, path: t.Any) -> t.Any:
- try:
- return self.enum(obj)
- except ValueError:
- return obj
-
-
-class TupleAdapter(construct.Adapter):
- def __init__(self, *subcons: construct.Adapter) -> None:
- super().__init__(construct.Sequence(*subcons))
-
- def _encode(self, obj: t.Any, context: t.Any, path: t.Any) -> t.Any:
- return obj
-
- def _decode(self, obj: t.Any, context: t.Any, path: t.Any) -> t.Any:
- return tuple(obj)
-
-
def enter_context(context_func: ContextFunc[CM, P, R]) -> ContextFunc[CM, P, R]:
"""Generic wrapper around any function or method that accepts a context manager
as its first argument.
diff --git a/python/src/trezorlib/ui.py b/python/src/trezorlib/ui.py
deleted file mode 100644
index 7194d3ff..00000000
--- a/python/src/trezorlib/ui.py
+++ /dev/null
@@ -1,224 +0,0 @@
-# This file is part of the Trezor project.
-#
-# Copyright (C) SatoshiLabs and contributors
-#
-# This library is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License version 3
-# as published by the Free Software Foundation.
-#
-# This library is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Lesser General Public License for more details.
-#
-# You should have received a copy of the License along with this library.
-# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
-
-from __future__ import annotations
-
-import sys
-import typing as t
-
-import click
-from mnemonic import Mnemonic
-
-from . import device, messages
-from .client import MAX_PIN_LENGTH
-from .exceptions import Cancelled
-from .messages import PinMatrixRequestType, WordRequestType
-
-PIN_MATRIX_DESCRIPTION = """
-Use the numeric keypad or lowercase letters to describe number positions.
-
-The layout is:
-
- 7 8 9 e r t
- 4 5 6 -or- d f g
- 1 2 3 c v b
-""".strip()
-
-RECOVERY_MATRIX_DESCRIPTION = """
-Use the numeric keypad to describe positions.
-For the word list use only left and right keys.
-Use backspace to correct an entry.
-
-The keypad layout is:
- 7 8 9 7 | 9
- 4 5 6 4 | 6
- 1 2 3 1 | 3
-""".strip()
-
-PIN_GENERIC = None
-PIN_CURRENT = PinMatrixRequestType.Current
-PIN_NEW = PinMatrixRequestType.NewFirst
-PIN_CONFIRM = PinMatrixRequestType.NewSecond
-WIPE_CODE_NEW = PinMatrixRequestType.WipeCodeFirst
-WIPE_CODE_CONFIRM = PinMatrixRequestType.WipeCodeSecond
-
-# Workaround for limitation of Git Bash
-# getpass function does not work correctly on Windows when not using a real terminal
-# (the hidden input is not allowed and it also freezes the script completely)
-# Details: https://bugs.python.org/issue44762
-CAN_HANDLE_HIDDEN_INPUT = sys.stdin and sys.stdin.isatty()
-
-
-def echo(*args: t.Any, **kwargs: t.Any) -> None:
- return click.echo(*args, err=True, **kwargs)
-
-
-def prompt(text: str, *, hide_input: bool = False, **kwargs: t.Any) -> t.Any:
- # Disallowing hidden input and warning user when it would cause issues
- if not CAN_HANDLE_HIDDEN_INPUT and hide_input:
- hide_input = False
- text += " (WARNING: will be displayed!)"
- return click.prompt(text, hide_input=hide_input, err=True, **kwargs)
-
-
-class ClickUI:
- def __init__(
- self, always_prompt: bool = False, passphrase_on_host: bool = False
- ) -> None:
- self.pinmatrix_shown = False
- self.last_prompt_shown = ""
- self.always_prompt = always_prompt
- self.passphrase_on_host = passphrase_on_host
-
- def _prompt_for_button(self, br: messages.ButtonRequest) -> str:
- if br.code == messages.ButtonRequestType.PassphraseEntry:
- return "Please enter passphrase on your Trezor device."
- if br.code == messages.ButtonRequestType.PinEntry:
- return "Please enter PIN on your Trezor device."
-
- return "Please confirm action on your Trezor device."
-
- def button_request(self, br: messages.ButtonRequest) -> None:
- prompt = self._prompt_for_button(br)
- if prompt != self.last_prompt_shown:
- echo(prompt)
- if not self.always_prompt:
- self.last_prompt_shown = prompt
-
- def get_pin(self, request: messages.PinMatrixRequest) -> str:
- code = request.type
- if code == PIN_CURRENT:
- desc = "current PIN"
- elif code == PIN_NEW:
- desc = "new PIN"
- elif code == PIN_CONFIRM:
- desc = "new PIN again"
- elif code == WIPE_CODE_NEW:
- desc = "new wipe code"
- elif code == WIPE_CODE_CONFIRM:
- desc = "new wipe code again"
- else:
- desc = "PIN"
-
- if not self.pinmatrix_shown:
- echo(PIN_MATRIX_DESCRIPTION)
- if not self.always_prompt:
- self.pinmatrix_shown = True
-
- while True:
- try:
- pin = prompt(f"Please enter {desc}", hide_input=True)
- except click.Abort:
- raise Cancelled from None
-
- # translate letters to numbers if letters were used
- if all(d in "cvbdfgert" for d in pin):
- pin = pin.translate(str.maketrans("cvbdfgert", "123456789"))
-
- if any(d not in "123456789" for d in pin):
- echo(
- "The value may only consist of digits 1 to 9 or letters cvbdfgert."
- )
- elif len(pin) > MAX_PIN_LENGTH:
- echo(f"The value must be at most {MAX_PIN_LENGTH} digits in length.")
- else:
- return pin
-
-
-class ScriptUI:
- """Interface to be used by scripts, not directly by user.
-
- Communicates with a client application using print() and input().
-
- Lot of `ClickUI` logic is outsourced to the client application, which
- is responsible for supplying the PIN and passphrase.
-
- Reference client implementation can be found under `tools/trezorctl_script_client.py`.
- """
-
- @staticmethod
- def button_request(br: messages.ButtonRequest) -> None:
- code = br.code.name if br.code else None
- print(f"?BUTTON code={code} pages={br.pages} name={br.name}")
-
- @staticmethod
- def get_pin(request: messages.PinMatrixRequest) -> str:
- code = request.type
- if code is None:
- print("?PIN")
- else:
- print(f"?PIN code={code.name}")
-
- pin = input()
- if pin == "CANCEL":
- raise Cancelled from None
- elif not pin.startswith(":"):
- raise RuntimeError("Sent PIN must start with ':'")
- else:
- pin = pin[1:]
- return pin
-
-
-def mnemonic_words(
- expand: bool = False, language: str = "english"
-) -> t.Callable[[WordRequestType], str]:
- if expand:
- wordlist = Mnemonic(language).wordlist
- else:
- wordlist = []
-
- def expand_word(word: str) -> str:
- if not expand:
- return word
- if word in wordlist:
- return word
- matches = [w for w in wordlist if w.startswith(word)]
- if len(matches) == 1:
- return matches[0]
- echo("Choose one of: " + ", ".join(matches))
- raise KeyError(word)
-
- def get_word(type: WordRequestType) -> str:
- assert type == WordRequestType.Plain
- while True:
- try:
- word = prompt("Enter one word of mnemonic")
- return expand_word(word)
- except KeyError:
- pass
- except click.Abort:
- raise Cancelled from None
-
- return get_word
-
-
-def matrix_words(type: WordRequestType) -> str:
- while True:
- try:
- ch = click.getchar()
- except (KeyboardInterrupt, EOFError):
- raise Cancelled from None
-
- if ch in "\x04\x1b":
- # Ctrl+D, Esc
- raise Cancelled
- if ch in "\x08\x7f":
- # Backspace, Del
- return device.RECOVERY_BACK
- if type == WordRequestType.Matrix6 and ch in "147369":
- return ch
- if type == WordRequestType.Matrix9 and ch in "123456789":
- return ch
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.