restiny 0.2.0__py3-none-any.whl → 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of restiny might be problematic. Click here for more details.

@@ -6,20 +6,20 @@ from textual import on
6
6
  from textual.app import ComposeResult
7
7
  from textual.containers import VerticalScroll
8
8
  from textual.message import Message
9
+ from textual.widget import Widget
9
10
  from textual.widgets import (
10
11
  Button,
11
12
  ContentSwitcher,
12
13
  Input,
13
14
  RadioButton,
14
15
  RadioSet,
15
- Static,
16
16
  Switch,
17
17
  )
18
18
 
19
19
  from restiny.widgets.path_chooser import PathChooser
20
20
 
21
21
 
22
- class DynamicField(Static):
22
+ class DynamicField(Widget):
23
23
  @abstractmethod
24
24
  def compose(self) -> ComposeResult: ...
25
25
 
@@ -130,6 +130,8 @@ class TextDynamicField(DynamicField):
130
130
 
131
131
  DEFAULT_CSS = """
132
132
  TextDynamicField {
133
+ width: 100%;
134
+ height: auto;
133
135
  layout: grid;
134
136
  grid-size: 4 1;
135
137
  grid-columns: auto 1fr 2fr auto; /* Set 1:2 ratio between Inputs */
@@ -140,23 +142,35 @@ class TextDynamicField(DynamicField):
140
142
  self, enabled: bool, key: str, value: str, *args, **kwargs
141
143
  ) -> None:
142
144
  super().__init__(*args, **kwargs)
143
-
144
- # Store initial values temporarily; applied after mounting.
145
- self._enabled = enabled
146
- self._key = key
147
- self._value = value
145
+ self._initial_enabled = enabled
146
+ self._initial_key = key
147
+ self._initial_value = value
148
148
 
149
149
  def compose(self) -> ComposeResult:
150
- yield Switch(value=self._enabled, tooltip='Send this field?')
151
- yield Input(value=self._key, placeholder='Key', id='key')
152
- yield Input(value=self._value, placeholder='Value', id='value')
153
- yield Button(label='', tooltip='Remove field')
150
+ yield Switch(
151
+ value=self._initial_enabled,
152
+ tooltip='Send this field?',
153
+ id='enabled',
154
+ )
155
+ yield Input(
156
+ value=self._initial_key,
157
+ placeholder='Key',
158
+ select_on_focus=False,
159
+ id='key',
160
+ )
161
+ yield Input(
162
+ value=self._initial_value,
163
+ placeholder='Value',
164
+ select_on_focus=False,
165
+ id='value',
166
+ )
167
+ yield Button(label='➖', tooltip='Remove field', id='remove')
154
168
 
155
- def on_mount(self) -> None:
156
- self.enabled_switch: Switch = self.query_one(Switch)
157
- self.key_input: Input = self.query_one('#key', Input)
158
- self.value_input: Input = self.query_one('#value', Input)
159
- self.remove_button: Button = self.query_one(Button)
169
+ async def on_mount(self) -> None:
170
+ self.enabled_switch = self.query_one('#enabled', Switch)
171
+ self.key_input = self.query_one('#key', Input)
172
+ self.value_input = self.query_one('#value', Input)
173
+ self.remove_button = self.query_one('#remove', Button)
160
174
 
161
175
  @property
162
176
  def enabled(self) -> bool:
@@ -190,14 +204,15 @@ class TextDynamicField(DynamicField):
190
204
  def is_empty(self) -> bool:
191
205
  return not self.is_filled
192
206
 
193
- @on(Switch.Changed)
207
+ @on(Switch.Changed, '#enabled')
194
208
  def on_enabled_or_disabled(self, message: Switch.Changed) -> None:
195
209
  if message.value is True:
196
210
  self.post_message(self.Enabled(field=self))
197
211
  elif message.value is False:
198
212
  self.post_message(message=self.Disabled(field=self))
199
213
 
200
- @on(Input.Changed)
214
+ @on(Input.Changed, '#key')
215
+ @on(Input.Changed, '#value')
201
216
  def on_input_changed(self, message: Input.Changed) -> None:
202
217
  self.enabled_switch.value = True
203
218
 
@@ -206,12 +221,12 @@ class TextDynamicField(DynamicField):
206
221
  elif self.is_filled:
207
222
  self.post_message(message=self.Filled(field=self))
208
223
 
209
- @on(Button.Pressed)
224
+ @on(Button.Pressed, '#remove')
210
225
  def on_remove_requested(self, message: Button.Pressed) -> None:
211
226
  self.post_message(self.RemoveRequested(field=self))
212
227
 
213
228
 
214
- class _ValueMode(StrEnum):
229
+ class _ValueKind(StrEnum):
215
230
  TEXT = 'text'
216
231
  FILE = 'file'
217
232
 
@@ -240,66 +255,72 @@ class TextOrFileDynamicField(DynamicField):
240
255
  enabled: bool = False,
241
256
  key: str = '',
242
257
  value: str | Path | None = '',
243
- value_mode: _ValueMode = 'text',
258
+ value_kind: _ValueKind = _ValueKind.TEXT,
244
259
  *args,
245
260
  **kwargs,
246
261
  ) -> None:
247
262
  super().__init__(*args, **kwargs)
248
- self._enabled = enabled
249
- self._key = key
250
- self._value = value
251
- self._value_mode = value_mode
263
+ self._initial_enabled = enabled
264
+ self._initial_key = key
265
+ self._initial_value = value
266
+ self._initial_value_kind = value_kind
252
267
 
253
268
  def compose(self) -> ComposeResult:
254
- with RadioSet(id='value-mode', compact=True):
269
+ with RadioSet(id='value-kind', compact=True):
255
270
  yield RadioButton(
256
- label=_ValueMode.TEXT,
257
- value=bool(self._value_mode == _ValueMode.TEXT),
258
- id='value-mode-text',
271
+ label=_ValueKind.TEXT,
272
+ value=bool(self._initial_value_kind == _ValueKind.TEXT),
273
+ id='value-kind-text',
259
274
  )
260
275
  yield RadioButton(
261
- label=_ValueMode.FILE,
262
- value=bool(self._value_mode == _ValueMode.FILE),
263
- id='value-mode-file',
276
+ label=_ValueKind.FILE,
277
+ value=bool(self._initial_value_kind == _ValueKind.FILE),
278
+ id='value-kind-file',
264
279
  )
265
280
  yield Switch(
266
- value=self._enabled,
281
+ value=self._initial_enabled,
267
282
  tooltip='Send this field?',
268
283
  id='enabled',
269
284
  )
270
- yield Input(value=self._key, placeholder='Key', id='key')
285
+ yield Input(
286
+ value=self._initial_key,
287
+ placeholder='Key',
288
+ select_on_focus=False,
289
+ id='key',
290
+ )
271
291
  with ContentSwitcher(
272
292
  initial='value-text'
273
- if self._value_mode == _ValueMode.TEXT
293
+ if self._initial_value_kind == _ValueKind.TEXT
274
294
  else 'value-file',
275
- id='value-mode-switcher',
295
+ id='value-kind-switcher',
276
296
  ):
277
297
  yield Input(
278
- value=self._value
279
- if self._value_mode == _ValueMode.TEXT
298
+ value=self._initial_value
299
+ if self._initial_value_kind == _ValueKind.TEXT
280
300
  else '',
281
301
  placeholder='Value',
302
+ select_on_focus=False,
282
303
  id='value-text',
283
304
  )
284
305
  yield PathChooser.file(
285
- path=self._value
286
- if self._value_mode == _ValueMode.FILE
306
+ path=self._initial_value
307
+ if self._initial_value_kind == _ValueKind.FILE
287
308
  else None,
288
309
  id='value-file',
289
310
  )
290
311
  yield Button(label='➖', tooltip='Remove field', id='remove')
291
312
 
292
313
  def on_mount(self) -> None:
293
- self.value_mode_switcher = self.query_one(
294
- '#value-mode-switcher', ContentSwitcher
314
+ self.value_kind_switcher = self.query_one(
315
+ '#value-kind-switcher', ContentSwitcher
295
316
  )
296
317
 
297
- self.value_mode_radioset = self.query_one('#value-mode', RadioSet)
298
- self.value_mode_text_radio_button = self.query_one(
299
- '#value-mode-text', RadioButton
318
+ self.value_kind_radioset = self.query_one('#value-kind', RadioSet)
319
+ self.value_kind_text_radio_button = self.query_one(
320
+ '#value-kind-text', RadioButton
300
321
  )
301
- self.value_mode_file_radio_button = self.query_one(
302
- '#value-mode-file', RadioButton
322
+ self.value_kind_file_radio_button = self.query_one(
323
+ '#value-kind-file', RadioButton
303
324
  )
304
325
  self.enabled_switch = self.query_one('#enabled', Switch)
305
326
  self.key_input = self.query_one('#key', Input)
@@ -325,42 +346,42 @@ class TextOrFileDynamicField(DynamicField):
325
346
 
326
347
  @property
327
348
  def value(self) -> str | Path | None:
328
- if self.value_mode == _ValueMode.TEXT:
349
+ if self.value_kind == _ValueKind.TEXT:
329
350
  return self.value_text_input.value
330
- elif self.value_mode == _ValueMode.FILE:
351
+ elif self.value_kind == _ValueKind.FILE:
331
352
  return self.value_file_input.path
332
353
 
333
354
  @value.setter
334
- def value(self, value: str | Path) -> None:
355
+ def value(self, value: str | Path | None) -> None:
335
356
  if isinstance(value, str):
336
357
  self.value_text_input.value = value
337
- elif isinstance(value, Path):
358
+ elif isinstance(value, Path) or value is None:
338
359
  self.value_file_input.path = value
339
360
 
340
361
  @property
341
- def value_mode(self) -> _ValueMode:
342
- return _ValueMode(self.value_mode_radioset.pressed_button.label)
343
-
344
- @value_mode.setter
345
- def value_mode(self, value: _ValueMode) -> None:
346
- if value == _ValueMode.TEXT:
347
- self.value_mode_switcher.current = 'value-text'
348
- self.value_mode_text_radio_button.value = True
349
- elif value == _ValueMode.FILE:
350
- self.value_mode_switcher.current = 'value-file'
351
- self.value_mode_file_radio_button.value = True
362
+ def value_kind(self) -> _ValueKind:
363
+ return _ValueKind(self.value_kind_radioset.pressed_button.label)
364
+
365
+ @value_kind.setter
366
+ def value_kind(self, value: _ValueKind) -> None:
367
+ if value == _ValueKind.TEXT:
368
+ self.value_kind_switcher.current = 'value-text'
369
+ self.value_kind_text_radio_button.value = True
370
+ elif value == _ValueKind.FILE:
371
+ self.value_kind_switcher.current = 'value-file'
372
+ self.value_kind_file_radio_button.value = True
352
373
 
353
374
  @property
354
375
  def is_filled(self) -> bool:
355
376
  if len(self.key_input.value) > 0:
356
377
  return True
357
378
  elif (
358
- self.value_mode == _ValueMode.TEXT
379
+ self.value_kind == _ValueKind.TEXT
359
380
  and len(self.value_text_input.value) > 0
360
381
  ):
361
382
  return True
362
383
  elif (
363
- self.value_mode == _ValueMode.FILE
384
+ self.value_kind == _ValueKind.FILE
364
385
  and self.value_file_input.path is not None
365
386
  ):
366
387
  return True
@@ -371,9 +392,9 @@ class TextOrFileDynamicField(DynamicField):
371
392
  def is_empty(self) -> bool:
372
393
  return not self.is_filled
373
394
 
374
- @on(RadioSet.Changed, '#value-mode')
375
- def on_value_mode_changed(self, message: RadioSet.Changed) -> None:
376
- self.value_mode = _ValueMode(message.pressed.label)
395
+ @on(RadioSet.Changed, '#value-kind')
396
+ def on_value_kind_changed(self, message: RadioSet.Changed) -> None:
397
+ self.value_kind = _ValueKind(message.pressed.label)
377
398
 
378
399
  @on(Switch.Changed, '#enabled')
379
400
  def on_enabled_or_disabled(self, message: Switch.Changed) -> None:
@@ -400,11 +421,18 @@ class TextOrFileDynamicField(DynamicField):
400
421
  self.post_message(self.RemoveRequested(field=self))
401
422
 
402
423
 
403
- class DynamicFields(Static):
424
+ class DynamicFields(Widget):
404
425
  """
405
426
  Enableable and removable fields
406
427
  """
407
428
 
429
+ DEFAULT_CSS = """
430
+ DynamicFields {
431
+ width: auto;
432
+ height: 1fr;
433
+ }
434
+ """
435
+
408
436
  class FieldEmpty(Message):
409
437
  """
410
438
  Sent when one of the fields becomes empty.
@@ -447,15 +475,12 @@ class DynamicFields(Static):
447
475
  self._fields = fields
448
476
 
449
477
  def compose(self) -> ComposeResult:
450
- yield VerticalScroll()
478
+ with VerticalScroll(can_focus=False):
479
+ yield from self._fields
451
480
 
452
- async def on_mount(self) -> None:
481
+ def on_mount(self) -> None:
453
482
  self.fields_container = self.query_one(VerticalScroll)
454
483
 
455
- # Set initial_fields
456
- for field in self._fields:
457
- await self.add_field(field=field)
458
-
459
484
  @property
460
485
  def fields(self) -> list[DynamicField]:
461
486
  return list(self.query(DynamicField))
@@ -469,7 +494,7 @@ class DynamicFields(Static):
469
494
  return [field for field in self.fields if field.is_filled]
470
495
 
471
496
  @property
472
- def values(self) -> list[dict[str, str | bool]]:
497
+ def values(self) -> list[dict[str, str | bool | Path | None]]:
473
498
  return [
474
499
  {
475
500
  'enabled': field.enabled,
@@ -479,62 +504,63 @@ class DynamicFields(Static):
479
504
  for field in self.fields
480
505
  ]
481
506
 
507
+ async def add_field(self, field: DynamicField) -> None:
508
+ await self.fields_container.mount(field)
509
+
510
+ def remove_field(self, field: DynamicField) -> None:
511
+ field.add_class('hidden')
512
+ field.remove()
513
+
482
514
  @on(DynamicField.Empty)
483
- async def on_field_is_empty(self, message: DynamicField.Empty) -> None:
484
- await self.remove_field(field=message.control)
515
+ def _on_field_is_empty(self, message: DynamicField.Empty) -> None:
516
+ self._focus_neighbor_field_then_remove(field=message.field)
485
517
  self.post_message(
486
- message=self.FieldEmpty(fields=self, field=message.control)
518
+ message=self.FieldEmpty(fields=self, field=message.field)
487
519
  )
488
520
 
489
521
  @on(DynamicField.Filled)
490
- async def on_field_is_filled(self, message: DynamicField.Filled) -> None:
522
+ async def _on_field_is_filled(self, message: DynamicField.Filled) -> None:
491
523
  if len(self.empty_fields) == 0:
492
- last_field = self.fields[-1]
493
- if isinstance(last_field, TextDynamicField):
524
+ field = message.field
525
+ if isinstance(field, TextDynamicField):
494
526
  await self.add_field(
495
527
  TextDynamicField(enabled=False, key='', value='')
496
528
  )
497
- elif isinstance(last_field, TextOrFileDynamicField):
529
+ elif isinstance(field, TextOrFileDynamicField):
498
530
  await self.add_field(
499
531
  TextOrFileDynamicField(
500
532
  enabled=False,
501
533
  key='',
502
534
  value='',
503
- value_mode=_ValueMode.TEXT,
535
+ value_kind=_ValueKind.TEXT,
504
536
  )
505
537
  )
506
538
 
507
539
  self.post_message(
508
- message=self.FieldFilled(fields=self, field=message.control)
540
+ message=self.FieldFilled(fields=self, field=message.field)
509
541
  )
510
542
 
511
543
  @on(DynamicField.RemoveRequested)
512
- async def on_field_remove_requested(
544
+ def _on_field_remove_requested(
513
545
  self, message: DynamicField.RemoveRequested
514
546
  ) -> None:
515
- await self.remove_field(field=message.control)
547
+ self._focus_neighbor_field_then_remove(field=message.field)
516
548
 
517
- async def add_field(self, field: DynamicField) -> None:
518
- await self.fields_container.mount(field)
519
-
520
- async def remove_field(self, field: DynamicField) -> None:
549
+ def _focus_neighbor_field_then_remove(self, field: DynamicField) -> None:
521
550
  if len(self.fields) == 1:
522
551
  self.app.bell()
523
552
  return
524
- elif self.fields[-1] is field: # Last field
553
+ elif field is self.fields[-1]:
525
554
  self.app.bell()
526
555
  return
527
556
 
528
- if self.fields[0] is field: # First field
529
- self.app.screen.focus_next()
530
- self.app.screen.focus_next()
531
- self.app.screen.focus_next()
532
- self.app.screen.focus_next()
533
- elif self.fields[-2] is field: # Penultimate field
534
- self.app.screen.focus_previous()
535
- self.app.screen.focus_previous()
536
- self.app.screen.focus_previous()
537
- self.app.screen.focus_previous()
557
+ field_index = self.fields.index(field)
538
558
 
539
- field.add_class('hidden')
540
- await field.remove() # Maybe the `await` is unnecessary
559
+ neighbor_field = None
560
+ if field_index == 0:
561
+ neighbor_field = self.fields[field_index + 1]
562
+ else:
563
+ neighbor_field = self.fields[field_index - 1]
564
+
565
+ self.app.set_focus(neighbor_field.query_one(Input))
566
+ self.remove_field(field=field)
@@ -0,0 +1,159 @@
1
+ from enum import StrEnum
2
+
3
+ from textual import on
4
+ from textual.app import ComposeResult
5
+ from textual.containers import Horizontal
6
+ from textual.message import Message
7
+ from textual.widget import Widget
8
+ from textual.widgets import Button, Input
9
+
10
+
11
+ class _Icon(StrEnum):
12
+ SHOW = ' 🔓 '
13
+ HIDE = ' 🔒 '
14
+
15
+
16
+ class _Tooltip(StrEnum):
17
+ SHOW = 'Show'
18
+ HIDE = 'Hide'
19
+
20
+
21
+ class PasswordInput(Widget):
22
+ DEFAULT_CSS = """
23
+ PasswordInput {
24
+ width: 1fr;
25
+ height: auto;
26
+ }
27
+
28
+ PasswordInput > Horizontal {
29
+ width: auto;
30
+ height: auto;
31
+ }
32
+
33
+ PasswordInput Input {
34
+ width: 1fr;
35
+ margin-right: 0;
36
+ border-right: none;
37
+ }
38
+
39
+ PasswordInput Input:focus {
40
+ border-right: none;
41
+ }
42
+
43
+
44
+ PasswordInput Button {
45
+ width: auto;
46
+ margin-left: 0;
47
+ border-left: none;
48
+ }
49
+
50
+ """
51
+
52
+ class Changed(Message):
53
+ """
54
+ Sent when value changed.
55
+ """
56
+
57
+ def __init__(self, input: 'PasswordInput', value: str):
58
+ super().__init__()
59
+ self.input = input
60
+ self.value = value
61
+
62
+ @property
63
+ def control(self) -> 'PasswordInput':
64
+ return self.input
65
+
66
+ class Shown(Message):
67
+ """
68
+ Sent when the value becomes visible.
69
+ """
70
+
71
+ def __init__(self, input: 'PasswordInput') -> None:
72
+ super().__init__()
73
+ self.input = input
74
+
75
+ @property
76
+ def control(self) -> 'PasswordInput':
77
+ return self.input
78
+
79
+ class Hidden(Message):
80
+ """
81
+ Sent when the value becomes hidden.
82
+ """
83
+
84
+ def __init__(self, input: 'PasswordInput') -> None:
85
+ super().__init__()
86
+ self.input = input
87
+
88
+ @property
89
+ def control(self) -> 'PasswordInput':
90
+ return self.input
91
+
92
+ def __init__(self, *args, **kwargs) -> None:
93
+ super().__init__(
94
+ id=kwargs.pop('id', None), classes=kwargs.pop('classes', None)
95
+ )
96
+ kwargs.pop('password', None)
97
+ self._input_args = args
98
+ self._input_kwargs = kwargs
99
+
100
+ def compose(self) -> ComposeResult:
101
+ with Horizontal():
102
+ yield Input(
103
+ *self._input_args,
104
+ **self._input_kwargs,
105
+ password=True,
106
+ id='value',
107
+ )
108
+ yield Button(
109
+ _Icon.SHOW, tooltip=_Tooltip.SHOW, id='toggle-visibility'
110
+ )
111
+
112
+ def on_mount(self) -> None:
113
+ self.value_input = self.query_one('#value', Input)
114
+ self.toggle_visibility_button = self.query_one(
115
+ '#toggle-visibility', Button
116
+ )
117
+
118
+ def show(self) -> None:
119
+ self.value_input.password = False
120
+ self.toggle_visibility_button.label = _Icon.HIDE
121
+ self.toggle_visibility_button.tooltip = _Tooltip.HIDE
122
+ self.post_message(message=self.Hidden(input=self))
123
+
124
+ def hide(self) -> None:
125
+ self.value_input.password = True
126
+ self.toggle_visibility_button.label = _Icon.SHOW
127
+ self.toggle_visibility_button.tooltip = _Tooltip.SHOW
128
+ self.post_message(message=self.Shown(input=self))
129
+
130
+ @property
131
+ def value(self) -> str:
132
+ return self.value_input.value
133
+
134
+ @value.setter
135
+ def value(self, value: str) -> None:
136
+ self.value_input.value = value
137
+
138
+ @property
139
+ def shown(self) -> bool:
140
+ return self.value_input.password is False
141
+
142
+ @property
143
+ def hidden(self) -> bool:
144
+ return not self.shown
145
+
146
+ @on(Input.Changed, '#value')
147
+ def _on_value_changed(self, message: Input.Changed) -> None:
148
+ self.post_message(
149
+ message=self.Changed(input=self, value=message.value)
150
+ )
151
+
152
+ @on(Button.Pressed, '#toggle-visibility')
153
+ def _on_toggle_visibility(self, message: Button.Pressed) -> None:
154
+ if self.value_input.password is False:
155
+ self.hide()
156
+ elif self.value_input.password is True:
157
+ self.show()
158
+
159
+ self.value_input.focus()