restiny 0.2.0__py3-none-any.whl → 0.2.1__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.
@@ -1,6 +1,9 @@
1
+ from dataclasses import dataclass
2
+ from http import HTTPStatus
3
+
1
4
  from textual import on
2
5
  from textual.app import ComposeResult
3
- from textual.reactive import Reactive
6
+ from textual.containers import VerticalScroll
4
7
  from textual.widgets import (
5
8
  ContentSwitcher,
6
9
  DataTable,
@@ -15,6 +18,16 @@ from restiny.enums import BodyRawLanguage
15
18
  from restiny.widgets import CustomTextArea
16
19
 
17
20
 
21
+ @dataclass
22
+ class ResponseAreaData:
23
+ status: HTTPStatus
24
+ size: int
25
+ elapsed_time: float | int
26
+ headers: dict
27
+ body_raw_language: BodyRawLanguage
28
+ body_raw: str
29
+
30
+
18
31
  # TODO: Implement 'Trace' tab pane
19
32
  class ResponseArea(Static):
20
33
  ALLOW_MAXIMIZE = True
@@ -38,8 +51,6 @@ class ResponseArea(Static):
38
51
 
39
52
  """
40
53
 
41
- has_response: bool = Reactive(False, layout=True, init=True)
42
-
43
54
  def compose(self) -> ComposeResult:
44
55
  with ContentSwitcher(id='response-switcher', initial='no-content'):
45
56
  yield Label(
@@ -49,7 +60,8 @@ class ResponseArea(Static):
49
60
 
50
61
  with TabbedContent(id='content'):
51
62
  with TabPane('Headers'):
52
- yield DataTable(show_cursor=False, id='headers')
63
+ with VerticalScroll():
64
+ yield DataTable(show_cursor=False, id='headers')
53
65
  with TabPane('Body'):
54
66
  yield Select(
55
67
  (
@@ -60,11 +72,11 @@ class ResponseArea(Static):
60
72
  ('XML', BodyRawLanguage.XML),
61
73
  ),
62
74
  allow_blank=False,
63
- tooltip='Body type',
64
- id='body-type',
75
+ tooltip='Syntax highlighting for the response body',
76
+ id='body-raw-language',
65
77
  )
66
78
  yield CustomTextArea.code_editor(
67
- id='body', read_only=True, classes='mt-1'
79
+ id='body-raw', read_only=True, classes='mt-1'
68
80
  )
69
81
 
70
82
  def on_mount(self) -> None:
@@ -73,25 +85,46 @@ class ResponseArea(Static):
73
85
  )
74
86
 
75
87
  self.headers_data_table = self.query_one('#headers', DataTable)
76
- self.body_type_select = self.query_one('#body-type', Select)
77
- self.body_text_area = self.query_one('#body', CustomTextArea)
88
+ self.body_raw_language_select = self.query_one(
89
+ '#body-raw-language', Select
90
+ )
91
+ self.body_raw_editor = self.query_one('#body-raw', CustomTextArea)
78
92
 
79
93
  self.headers_data_table.add_columns('Key', 'Value')
80
94
 
81
- @on(Select.Changed, '#body-type')
82
- def on_body_type_changed(self, message: Select.Changed) -> None:
83
- self.body_text_area.language = self.body_type_select.value
95
+ def set_data(self, data: ResponseAreaData | None) -> None:
96
+ self.border_title = self.BORDER_TITLE
97
+ self.border_subtitle = ''
98
+ self.headers_data_table.clear()
99
+ self.body_raw_language_select.value = BodyRawLanguage.PLAIN
100
+ self.body_raw_editor.clear()
84
101
 
85
- def watch_has_response(self, value: bool) -> None:
102
+ if data is None:
103
+ return
104
+
105
+ self.border_title = f'Response - {data.status} {data.status.phrase}'
106
+ self.border_subtitle = (
107
+ f'{data.size} bytes in {data.elapsed_time} seconds'
108
+ )
109
+ for header_key, header_value in data.headers.items():
110
+ self.headers_data_table.add_row(header_key, header_value)
111
+ self.body_raw_language_select.value = data.body_raw_language
112
+ self.body_raw_editor.text = data.body_raw
113
+
114
+ @property
115
+ def is_showing_response(self) -> bool:
116
+ if self._response_switcher.current == 'content':
117
+ return True
118
+ elif self._response_switcher.current == 'no-content':
119
+ return False
120
+
121
+ @is_showing_response.setter
122
+ def is_showing_response(self, value: bool) -> None:
86
123
  if value is True:
87
124
  self._response_switcher.current = 'content'
88
125
  elif value is False:
89
126
  self._response_switcher.current = 'no-content'
90
- self.reset_response()
91
127
 
92
- def reset_response(self) -> None:
93
- self.border_title = self.BORDER_TITLE
94
- self.border_subtitle = ''
95
- self.headers_data_table.clear()
96
- self.body_type_select.value = BodyRawLanguage.PLAIN
97
- self.body_text_area.clear()
128
+ @on(Select.Changed, '#body-raw-language')
129
+ def _on_body_raw_language_changed(self, message: Select.Changed) -> None:
130
+ self.body_raw_editor.language = self.body_raw_language_select.value
restiny/core/url_area.py CHANGED
@@ -3,7 +3,6 @@ from dataclasses import dataclass
3
3
  from textual import on
4
4
  from textual.app import ComposeResult
5
5
  from textual.message import Message
6
- from textual.reactive import Reactive
7
6
  from textual.widgets import Button, ContentSwitcher, Input, Select, Static
8
7
 
9
8
  from restiny.enums import HTTPMethod
@@ -29,8 +28,6 @@ class URLArea(Static):
29
28
  }
30
29
  """
31
30
 
32
- request_pending = Reactive(False)
33
-
34
31
  class SendRequest(Message):
35
32
  """
36
33
  Sent when the user send a request.
@@ -47,11 +44,15 @@ class URLArea(Static):
47
44
  def __init__(self) -> None:
48
45
  super().__init__()
49
46
 
47
+ def __init__(self, *args, **kwargs) -> None:
48
+ super().__init__(*args, **kwargs)
49
+ self._request_pending = False
50
+
50
51
  def compose(self) -> ComposeResult:
51
52
  yield Select.from_values(
52
53
  values=HTTPMethod.values(), allow_blank=False, id='method'
53
54
  )
54
- yield Input(placeholder='Enter URL', id='url')
55
+ yield Input(placeholder='Enter URL', select_on_focus=False, id='url')
55
56
  with ContentSwitcher(
56
57
  id='request-button-switcher', initial='send-request'
57
58
  ):
@@ -78,9 +79,28 @@ class URLArea(Static):
78
79
  self.send_request_button = self.query_one('#send-request', Button)
79
80
  self.cancel_request_button = self.query_one('#cancel-request', Button)
80
81
 
82
+ def get_data(self) -> URLAreaData:
83
+ return URLAreaData(
84
+ method=self.method_select.value,
85
+ url=self.url_input.value,
86
+ )
87
+
88
+ @property
89
+ def request_pending(self) -> bool:
90
+ return self._request_pending
91
+
92
+ @request_pending.setter
93
+ def request_pending(self, value: bool) -> None:
94
+ if value is True:
95
+ self._request_button_switcher.current = 'cancel-request'
96
+ elif value is False:
97
+ self._request_button_switcher.current = 'send-request'
98
+
99
+ self._request_pending = value
100
+
81
101
  @on(Button.Pressed, '#send-request')
82
- @on(Input.Submitted)
83
- def on_send_request(
102
+ @on(Input.Submitted, '#url')
103
+ def _on_send_request(
84
104
  self, message: Button.Pressed | Input.Submitted
85
105
  ) -> None:
86
106
  if self.request_pending:
@@ -88,22 +108,10 @@ class URLArea(Static):
88
108
 
89
109
  self.post_message(message=self.SendRequest())
90
110
 
91
- @on(Input.Submitted)
92
111
  @on(Button.Pressed, '#cancel-request')
93
- def on_cancel_request(self, message: Button.Pressed) -> None:
112
+ @on(Input.Submitted, '#url')
113
+ def _on_cancel_request(self, message: Button.Pressed) -> None:
94
114
  if not self.request_pending:
95
115
  return
96
116
 
97
117
  self.post_message(message=self.CancelRequest())
98
-
99
- def watch_request_pending(self, value: bool) -> None:
100
- if value is True:
101
- self._request_button_switcher.current = 'cancel-request'
102
- elif value is False:
103
- self._request_button_switcher.current = 'send-request'
104
-
105
- def get_data(self) -> URLAreaData:
106
- return URLAreaData(
107
- method=self.method_select.value,
108
- url=self.url_input.value,
109
- )
@@ -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)