pytermgui.widgets.interactive.input_field

This module contains the InputField class.

View Source
  0"""This module contains the `InputField` class."""
  1
  2from __future__ import annotations
  3from typing import Any
  4
  5import string
  6
  7from ...ansi_interface import MouseAction, MouseEvent
  8from ...input import keys
  9from ...enums import HorizontalAlignment
 10from ...regex import real_length
 11from .. import styles as w_styles
 12from ..base import Label
 13
 14
 15class InputField(Label):
 16    """An element to display user input
 17
 18    This class does NOT read input. To use this widget, send it
 19    user data gathered by `pytermgui.input.getch` or other means.
 20
 21    Args:
 22        value: The default value of this InputField.
 23        prompt: Text to display to the left of the field.
 24        expect: Type object that all input should match. This type
 25              is called on each new key, and if a `ValueError` is
 26              raised the key is discarded. The `value` attribute
 27              is also converted using this type.
 28
 29    Example of usage:
 30
 31    ```python3
 32    import pytermgui as ptg
 33
 34    field = ptg.InputField()
 35
 36    root = ptg.Container(
 37        "[210 bold]This is an InputField!",
 38        field,
 39    )
 40
 41    while True:
 42        key = getch()
 43
 44        # Send key to field
 45        field.handle_key(key)
 46        root.print()
 47    ```
 48    """
 49
 50    styles = w_styles.StyleManager(
 51        value=w_styles.FOREGROUND,
 52        cursor=w_styles.MarkupFormatter("[inverse]{item}"),
 53        fill=w_styles.MarkupFormatter("[@243]{item}"),
 54    )
 55
 56    is_bindable = True
 57
 58    def __init__(
 59        self,
 60        value: str = "",
 61        prompt: str = "",
 62        expect: type | None = None,
 63        **attrs: Any,
 64    ) -> None:
 65        """Initialize object"""
 66
 67        super().__init__(prompt + value, **attrs)
 68
 69        self.parent_align = HorizontalAlignment.LEFT
 70
 71        self.value = value
 72        self.prompt = prompt
 73        self.cursor = real_length(self.value)
 74        self.expect = expect
 75
 76    @property
 77    def selectables_length(self) -> int:
 78        """Get length of selectables in object"""
 79
 80        return 1
 81
 82    @property
 83    def cursor(self) -> int:
 84        """Get cursor"""
 85
 86        return self._cursor
 87
 88    @cursor.setter
 89    def cursor(self, value: int) -> None:
 90        """Set cursor as an always-valid value"""
 91
 92        self._cursor = max(0, min(value, real_length(str(self.value))))
 93
 94    def handle_key(self, key: str) -> bool:
 95        """Handle keypress, return True if success, False if failure"""
 96
 97        def _run_callback() -> None:
 98            """Call callback if `keys.ANY_KEY` is bound"""
 99
100            if keys.ANY_KEY in self._bindings:
101                method, _ = self._bindings[keys.ANY_KEY]
102                method(self, key)
103
104        if self.execute_binding(key):
105            return True
106
107        if key == keys.TAB:
108            return False
109
110        if key == keys.BACKSPACE and self.cursor > 0:
111            self.value = str(self.value)
112            left = self.value[: self.cursor - 1]
113            right = self.value[self.cursor :]
114            self.value = left + right
115
116            self.cursor -= 1
117
118            _run_callback()
119
120        elif key in [keys.LEFT, keys.CTRL_B]:
121            self.cursor -= 1
122
123        elif key in [keys.RIGHT, keys.CTRL_F]:
124            self.cursor += 1
125
126        # Ignore unhandled non-printing keys
127        elif key == keys.ENTER or key not in string.printable:
128            return False
129
130        # Add character
131        else:
132            if self.expect is not None:
133                try:
134                    self.expect(key)
135                except ValueError:
136                    return False
137
138            self.value = str(self.value)
139
140            left = self.value[: self.cursor] + key
141            right = self.value[self.cursor :]
142
143            self.value = left + right
144            self.cursor += len(key)
145            _run_callback()
146
147        if self.expect is not None and self.value != "":
148            self.value = self.expect(self.value)
149
150        return True
151
152    def handle_mouse(self, event: MouseEvent) -> bool:
153        """Handle mouse events"""
154
155        # Ignore mouse release events
156        if event.action is MouseAction.RELEASE:
157            return True
158
159        # Set cursor to mouse location
160        if event.action is MouseAction.LEFT_CLICK:
161            self.cursor = event.position[0] - self.pos[0]
162            return True
163
164        return super().handle_mouse(event)
165
166    def get_lines(self) -> list[str]:
167        """Get lines of object"""
168
169        # Cache value to be reset later
170        old = self.value
171
172        # Stringify value in case `expect` is set
173        self.value = str(self.value)
174
175        # Create sides separated by cursor
176        left = self.styles.fill(self.value[: self.cursor])
177        right = self.styles.fill(self.value[self.cursor + 1 :])
178
179        # Assign cursor character
180        if self.selected_index is None:
181            if len(self.value) <= self.cursor:
182                cursor_char = ""
183
184            else:
185                cursor_char = self.styles.fill(self.value[self.cursor])
186
187        # These errors are really weird, as there is nothing different with
188        # styles.cursor compared to others, which pass just fine.
189        elif len(self.value) > self.cursor:
190            cursor_char = self.styles.cursor(  # pylint: disable=not-callable
191                self.value[self.cursor]
192            )
193
194        else:
195            cursor_char = self.styles.cursor(" ")  # pylint: disable=not-callable
196
197        # Set new value, get lines using it
198        self.value = self.prompt
199
200        if len(self.prompt) > 0:
201            self.value += " "
202
203        self.value += left + cursor_char + right
204
205        lines = super().get_lines()
206
207        # Set old value
208        self.value = old
209
210        return [
211            line + self.styles.fill((self.width - real_length(line)) * " ")
212            for line in lines
213        ]
#   class InputField(pytermgui.widgets.base.Label):
View Source
 16class InputField(Label):
 17    """An element to display user input
 18
 19    This class does NOT read input. To use this widget, send it
 20    user data gathered by `pytermgui.input.getch` or other means.
 21
 22    Args:
 23        value: The default value of this InputField.
 24        prompt: Text to display to the left of the field.
 25        expect: Type object that all input should match. This type
 26              is called on each new key, and if a `ValueError` is
 27              raised the key is discarded. The `value` attribute
 28              is also converted using this type.
 29
 30    Example of usage:
 31
 32    ```python3
 33    import pytermgui as ptg
 34
 35    field = ptg.InputField()
 36
 37    root = ptg.Container(
 38        "[210 bold]This is an InputField!",
 39        field,
 40    )
 41
 42    while True:
 43        key = getch()
 44
 45        # Send key to field
 46        field.handle_key(key)
 47        root.print()
 48    ```
 49    """
 50
 51    styles = w_styles.StyleManager(
 52        value=w_styles.FOREGROUND,
 53        cursor=w_styles.MarkupFormatter("[inverse]{item}"),
 54        fill=w_styles.MarkupFormatter("[@243]{item}"),
 55    )
 56
 57    is_bindable = True
 58
 59    def __init__(
 60        self,
 61        value: str = "",
 62        prompt: str = "",
 63        expect: type | None = None,
 64        **attrs: Any,
 65    ) -> None:
 66        """Initialize object"""
 67
 68        super().__init__(prompt + value, **attrs)
 69
 70        self.parent_align = HorizontalAlignment.LEFT
 71
 72        self.value = value
 73        self.prompt = prompt
 74        self.cursor = real_length(self.value)
 75        self.expect = expect
 76
 77    @property
 78    def selectables_length(self) -> int:
 79        """Get length of selectables in object"""
 80
 81        return 1
 82
 83    @property
 84    def cursor(self) -> int:
 85        """Get cursor"""
 86
 87        return self._cursor
 88
 89    @cursor.setter
 90    def cursor(self, value: int) -> None:
 91        """Set cursor as an always-valid value"""
 92
 93        self._cursor = max(0, min(value, real_length(str(self.value))))
 94
 95    def handle_key(self, key: str) -> bool:
 96        """Handle keypress, return True if success, False if failure"""
 97
 98        def _run_callback() -> None:
 99            """Call callback if `keys.ANY_KEY` is bound"""
100
101            if keys.ANY_KEY in self._bindings:
102                method, _ = self._bindings[keys.ANY_KEY]
103                method(self, key)
104
105        if self.execute_binding(key):
106            return True
107
108        if key == keys.TAB:
109            return False
110
111        if key == keys.BACKSPACE and self.cursor > 0:
112            self.value = str(self.value)
113            left = self.value[: self.cursor - 1]
114            right = self.value[self.cursor :]
115            self.value = left + right
116
117            self.cursor -= 1
118
119            _run_callback()
120
121        elif key in [keys.LEFT, keys.CTRL_B]:
122            self.cursor -= 1
123
124        elif key in [keys.RIGHT, keys.CTRL_F]:
125            self.cursor += 1
126
127        # Ignore unhandled non-printing keys
128        elif key == keys.ENTER or key not in string.printable:
129            return False
130
131        # Add character
132        else:
133            if self.expect is not None:
134                try:
135                    self.expect(key)
136                except ValueError:
137                    return False
138
139            self.value = str(self.value)
140
141            left = self.value[: self.cursor] + key
142            right = self.value[self.cursor :]
143
144            self.value = left + right
145            self.cursor += len(key)
146            _run_callback()
147
148        if self.expect is not None and self.value != "":
149            self.value = self.expect(self.value)
150
151        return True
152
153    def handle_mouse(self, event: MouseEvent) -> bool:
154        """Handle mouse events"""
155
156        # Ignore mouse release events
157        if event.action is MouseAction.RELEASE:
158            return True
159
160        # Set cursor to mouse location
161        if event.action is MouseAction.LEFT_CLICK:
162            self.cursor = event.position[0] - self.pos[0]
163            return True
164
165        return super().handle_mouse(event)
166
167    def get_lines(self) -> list[str]:
168        """Get lines of object"""
169
170        # Cache value to be reset later
171        old = self.value
172
173        # Stringify value in case `expect` is set
174        self.value = str(self.value)
175
176        # Create sides separated by cursor
177        left = self.styles.fill(self.value[: self.cursor])
178        right = self.styles.fill(self.value[self.cursor + 1 :])
179
180        # Assign cursor character
181        if self.selected_index is None:
182            if len(self.value) <= self.cursor:
183                cursor_char = ""
184
185            else:
186                cursor_char = self.styles.fill(self.value[self.cursor])
187
188        # These errors are really weird, as there is nothing different with
189        # styles.cursor compared to others, which pass just fine.
190        elif len(self.value) > self.cursor:
191            cursor_char = self.styles.cursor(  # pylint: disable=not-callable
192                self.value[self.cursor]
193            )
194
195        else:
196            cursor_char = self.styles.cursor(" ")  # pylint: disable=not-callable
197
198        # Set new value, get lines using it
199        self.value = self.prompt
200
201        if len(self.prompt) > 0:
202            self.value += " "
203
204        self.value += left + cursor_char + right
205
206        lines = super().get_lines()
207
208        # Set old value
209        self.value = old
210
211        return [
212            line + self.styles.fill((self.width - real_length(line)) * " ")
213            for line in lines
214        ]

An element to display user input

This class does NOT read input. To use this widget, send it user data gathered by pytermgui.input.getch or other means.

Args
  • value: The default value of this InputField.
  • prompt: Text to display to the left of the field.
  • expect: Type object that all input should match. This type is called on each new key, and if a ValueError is raised the key is discarded. The value attribute is also converted using this type.

Example of usage:

import pytermgui as ptg

field = ptg.InputField()

root = ptg.Container(
    "[210 bold]This is an InputField!",
    field,
)

while True:
    key = getch()

    # Send key to field
    field.handle_key(key)
    root.print()
#   InputField( value: str = '', prompt: str = '', expect: type | None = None, **attrs: Any )
View Source
59    def __init__(
60        self,
61        value: str = "",
62        prompt: str = "",
63        expect: type | None = None,
64        **attrs: Any,
65    ) -> None:
66        """Initialize object"""
67
68        super().__init__(prompt + value, **attrs)
69
70        self.parent_align = HorizontalAlignment.LEFT
71
72        self.value = value
73        self.prompt = prompt
74        self.cursor = real_length(self.value)
75        self.expect = expect

Initialize object

#   styles = {'value': StyleCall(obj=None, method=<function <lambda>>), 'cursor': StyleCall(obj=None, method=MarkupFormatter(markup='[inverse]{item}', ensure_strip=False, _markup_cache={})), 'fill': StyleCall(obj=None, method=MarkupFormatter(markup='[@243]{item}', ensure_strip=False, _markup_cache={}))}

Default styles for this class

#   is_bindable = True

Allow binding support

#   cursor: int

Get cursor

#   selectables_length: int

Get length of selectables in object

#   def handle_key(self, key: str) -> bool:
View Source
 95    def handle_key(self, key: str) -> bool:
 96        """Handle keypress, return True if success, False if failure"""
 97
 98        def _run_callback() -> None:
 99            """Call callback if `keys.ANY_KEY` is bound"""
100
101            if keys.ANY_KEY in self._bindings:
102                method, _ = self._bindings[keys.ANY_KEY]
103                method(self, key)
104
105        if self.execute_binding(key):
106            return True
107
108        if key == keys.TAB:
109            return False
110
111        if key == keys.BACKSPACE and self.cursor > 0:
112            self.value = str(self.value)
113            left = self.value[: self.cursor - 1]
114            right = self.value[self.cursor :]
115            self.value = left + right
116
117            self.cursor -= 1
118
119            _run_callback()
120
121        elif key in [keys.LEFT, keys.CTRL_B]:
122            self.cursor -= 1
123
124        elif key in [keys.RIGHT, keys.CTRL_F]:
125            self.cursor += 1
126
127        # Ignore unhandled non-printing keys
128        elif key == keys.ENTER or key not in string.printable:
129            return False
130
131        # Add character
132        else:
133            if self.expect is not None:
134                try:
135                    self.expect(key)
136                except ValueError:
137                    return False
138
139            self.value = str(self.value)
140
141            left = self.value[: self.cursor] + key
142            right = self.value[self.cursor :]
143
144            self.value = left + right
145            self.cursor += len(key)
146            _run_callback()
147
148        if self.expect is not None and self.value != "":
149            self.value = self.expect(self.value)
150
151        return True

Handle keypress, return True if success, False if failure

#   def handle_mouse(self, event: pytermgui.ansi_interface.MouseEvent) -> bool:
View Source
153    def handle_mouse(self, event: MouseEvent) -> bool:
154        """Handle mouse events"""
155
156        # Ignore mouse release events
157        if event.action is MouseAction.RELEASE:
158            return True
159
160        # Set cursor to mouse location
161        if event.action is MouseAction.LEFT_CLICK:
162            self.cursor = event.position[0] - self.pos[0]
163            return True
164
165        return super().handle_mouse(event)

Handle mouse events

#   def get_lines(self) -> list[str]:
View Source
167    def get_lines(self) -> list[str]:
168        """Get lines of object"""
169
170        # Cache value to be reset later
171        old = self.value
172
173        # Stringify value in case `expect` is set
174        self.value = str(self.value)
175
176        # Create sides separated by cursor
177        left = self.styles.fill(self.value[: self.cursor])
178        right = self.styles.fill(self.value[self.cursor + 1 :])
179
180        # Assign cursor character
181        if self.selected_index is None:
182            if len(self.value) <= self.cursor:
183                cursor_char = ""
184
185            else:
186                cursor_char = self.styles.fill(self.value[self.cursor])
187
188        # These errors are really weird, as there is nothing different with
189        # styles.cursor compared to others, which pass just fine.
190        elif len(self.value) > self.cursor:
191            cursor_char = self.styles.cursor(  # pylint: disable=not-callable
192                self.value[self.cursor]
193            )
194
195        else:
196            cursor_char = self.styles.cursor(" ")  # pylint: disable=not-callable
197
198        # Set new value, get lines using it
199        self.value = self.prompt
200
201        if len(self.prompt) > 0:
202            self.value += " "
203
204        self.value += left + cursor_char + right
205
206        lines = super().get_lines()
207
208        # Set old value
209        self.value = old
210
211        return [
212            line + self.styles.fill((self.width - real_length(line)) * " ")
213            for line in lines
214        ]

Get lines of object