tuile 0.10.0 → 0.12.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +109 -64
- data/DECISIONS.md +1299 -22
- data/README.md +18 -13
- data/TERMINOLOGY.md +61 -0
- data/book/02-repaint.md +1 -1
- data/book/03-layout.md +154 -9
- data/book/05-focus.md +2 -0
- data/book/06-theming.md +1 -1
- data/book/07-components.md +202 -37
- data/book/README.md +3 -1
- data/examples/file_commander.rb +5 -4
- data/examples/sampler.rb +320 -133
- data/ideas/arrow-key-navigation.md +205 -0
- data/ideas/new-components.md +26 -12
- data/lib/tuile/buffer.rb +7 -7
- data/lib/tuile/component/big_decimal_field.rb +199 -0
- data/lib/tuile/component/button.rb +1 -1
- data/lib/tuile/component/checkbox.rb +11 -10
- data/lib/tuile/component/checkbox_group.rb +31 -26
- data/lib/tuile/component/combo_box.rb +18 -33
- data/lib/tuile/component/float_field.rb +161 -0
- data/lib/tuile/component/info_window.rb +1 -1
- data/lib/tuile/component/label.rb +14 -14
- data/lib/tuile/component/layout/box.rb +316 -0
- data/lib/tuile/component/layout/horizontal.rb +40 -0
- data/lib/tuile/component/layout/vertical.rb +41 -0
- data/lib/tuile/component/layout.rb +149 -1
- data/lib/tuile/component/list.rb +291 -216
- data/lib/tuile/component/list_dropdown.rb +82 -24
- data/lib/tuile/component/notification.rb +317 -0
- data/lib/tuile/component/picker_window.rb +3 -3
- data/lib/tuile/component/popup.rb +8 -10
- data/lib/tuile/component/progress_bar.rb +1 -1
- data/lib/tuile/component/radio_group.rb +32 -30
- data/lib/tuile/component/select.rb +251 -0
- data/lib/tuile/component/text_area/wrapped_text.rb +320 -0
- data/lib/tuile/component/text_area.rb +79 -273
- data/lib/tuile/component/text_field.rb +1 -1
- data/lib/tuile/component/text_view.rb +191 -177
- data/lib/tuile/component/window.rb +8 -8
- data/lib/tuile/component.rb +5 -5
- data/lib/tuile/screen.rb +1 -1
- data/lib/tuile/styled_string.rb +25 -15
- data/lib/tuile/version.rb +1 -1
- data/lib/tuile/vertical_scroll_bar.rb +6 -6
- data/lib/tuile.rb +4 -0
- data/sig/tuile.rbs +1670 -406
- metadata +11 -1
data/book/07-components.md
CHANGED
|
@@ -175,7 +175,25 @@ qty.on_value_change = ->(n) { recompute(n) } # n is an Integer, or nil
|
|
|
175
175
|
qty.value = 3
|
|
176
176
|
```
|
|
177
177
|
|
|
178
|
-
|
|
178
|
+
{Tuile::Component::FloatField} is the same field one type over — it also
|
|
179
|
+
accepts a single decimal point, and its value is a `Float`. That naming is
|
|
180
|
+
a small rule worth knowing, because it tells you what you're getting: a
|
|
181
|
+
typed field is named after the Ruby class of its value, so `IntegerField`
|
|
182
|
+
hands back an `Integer` and `FloatField` a `Float` — a binary double, which
|
|
183
|
+
makes it exactly the wrong field for money. It parses generously while you
|
|
184
|
+
type: a buffer of `1.` already reads as `1.0`, so reaching for the decimal
|
|
185
|
+
point doesn't blink the value to `nil` and back in the listener you wired.
|
|
186
|
+
|
|
187
|
+
Money gets {Tuile::Component::BigDecimalField}, the same field once more with
|
|
188
|
+
an exact decimal inside — type `0.1` and it is `0.1`, not the `0.1000…0055`
|
|
189
|
+
a binary double stores. It is strict about how that exactness is preserved:
|
|
190
|
+
assigning a `Float` raises rather than quietly converting, because by the time
|
|
191
|
+
`19.99` reaches the setter it is already not `19.99`. This is the one
|
|
192
|
+
component with a dependency Tuile itself doesn't carry — `bigdecimal` has
|
|
193
|
+
been a *bundled* gem since Ruby 3.4, so an app that uses this field names it
|
|
194
|
+
in its own `Gemfile`, and an app that doesn't never loads it.
|
|
195
|
+
|
|
196
|
+
The combo box and the two numeric fields are built the same way, and it's
|
|
179
197
|
worth seeing why: each *wraps* a text field rather than *being* one. A
|
|
180
198
|
subclass would inherit the text field's `String`-typed value and wear it
|
|
181
199
|
on its face right next to the real typed one — two conflicting answers to
|
|
@@ -201,11 +219,38 @@ read-only or required flag yet. Room left for that layer to grow into.
|
|
|
201
219
|
|
|
202
220
|
## Choosing from a set
|
|
203
221
|
|
|
204
|
-
{Tuile::Component::List} is the workhorse: a scrollable column of
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
222
|
+
{Tuile::Component::List} is the workhorse: a scrollable column of *items*
|
|
223
|
+
— objects of whatever type your app deals in — one row each. You give it
|
|
224
|
+
the items and a `renderer` that turns one item into a row, and it does the
|
|
225
|
+
rest: ellipsizing a row too wide for the viewport (spans preserved),
|
|
226
|
+
scrolling, and handing your callbacks back **the item itself** rather than
|
|
227
|
+
the text it drew for it.
|
|
228
|
+
|
|
229
|
+
```ruby
|
|
230
|
+
list = Component::List.new
|
|
231
|
+
list.items = User.all
|
|
232
|
+
list.renderer = ->(u) { "#{u.name} #{u.email}" }
|
|
233
|
+
list.cursor = Component::List::Cursor.new
|
|
234
|
+
list.on_item_chosen = ->(_index, user) { open(user) }
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
That's the same bargain the value seam struck earlier in this chapter: the
|
|
238
|
+
component speaks in your objects, and nothing has to map a row of text
|
|
239
|
+
back to the thing it stood for. When your items *are* the text, skip the
|
|
240
|
+
renderer entirely — `list.lines = entries` takes strings (or
|
|
241
|
+
{Tuile::StyledString}s, or anything with a `to_s`), splits them on
|
|
242
|
+
newlines, and shows each as its own row.
|
|
243
|
+
|
|
244
|
+
The renderer runs when a row is *painted*, and only for the rows actually
|
|
245
|
+
on screen: a hundred-thousand-item list renders the twenty you can see.
|
|
246
|
+
That's what makes a long list cheap, and it comes with one rule — keep the
|
|
247
|
+
renderer a pure function of its item. It may be called on any frame, so it
|
|
248
|
+
is the wrong place to reach for a database; do that work when you build
|
|
249
|
+
the items.
|
|
250
|
+
|
|
251
|
+
What makes the list flexible beyond that is that its *cursor behavior is a
|
|
252
|
+
pluggable object* rather than a boolean. Assign one of three
|
|
253
|
+
{Tuile::Component::List::Cursor} variants to fit the interaction:
|
|
209
254
|
|
|
210
255
|
- **`Cursor::None`** (the default) — no cursor at all. The list is a
|
|
211
256
|
read-only scroll region: a log, a static report.
|
|
@@ -216,29 +261,24 @@ variants to fit the interaction:
|
|
|
216
261
|
lines. For a list where only some rows are selectable (headers
|
|
217
262
|
interspersed with items, say), it skips the rest.
|
|
218
263
|
|
|
219
|
-
Two callbacks cover the events you care about
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
(`scrollbar_visibility`).
|
|
228
|
-
|
|
229
|
-
```ruby
|
|
230
|
-
list = Component::List.new
|
|
231
|
-
list.lines = entries
|
|
232
|
-
list.cursor = Component::List::Cursor.new
|
|
233
|
-
list.on_item_chosen = ->(index, line) { open(entries[index]) }
|
|
234
|
-
```
|
|
264
|
+
Two callbacks cover the events you care about, and both are handed the
|
|
265
|
+
`(index, item)` pair. `on_item_chosen` fires when the user commits to the
|
|
266
|
+
cursor's row — Enter or a left-click — and is the "open this" signal.
|
|
267
|
+
`on_cursor_changed` fires when the highlighted row *changes*, which is
|
|
268
|
+
exactly what you wire to keep a details pane in sync with the selection.
|
|
269
|
+
For a tailing list — a live log — set `auto_scroll`; it pins to the bottom
|
|
270
|
+
as items arrive, but politely stops yanking you down the moment you scroll
|
|
271
|
+
up to read history, and resumes once you scroll back (`following?` tells
|
|
272
|
+
you which). A scrollbar is one assignment (`scrollbar_visibility`).
|
|
235
273
|
|
|
236
274
|
When the set is long and the user roughly knows what they want, a plain
|
|
237
275
|
list makes them scroll for it. {Tuile::Component::ComboBox} is the answer:
|
|
238
276
|
a text field with a dropdown that filters as you type. Hand it `items` (of
|
|
239
277
|
any type) and, when their `to_s` isn't what you want shown, an
|
|
240
278
|
`item_label` strategy to render each one; type to narrow, arrow to move,
|
|
241
|
-
Enter or click to accept.
|
|
279
|
+
Enter or click to accept. (The domain widgets all call that strategy
|
|
280
|
+
`item_label`, where a bare list calls it `renderer` — a label is text the
|
|
281
|
+
widget then decorates, a row is the whole rendering.) It's the value seam doing real work — its
|
|
242
282
|
`value` is the selected *item*, the object and not its label, so a combo
|
|
243
283
|
over `User`s hands back a `User`. The field's text is merely a transient
|
|
244
284
|
query: it reverts to the selection's label when you dismiss the dropdown,
|
|
@@ -272,15 +312,22 @@ cb.toggle # unchecks it, firing the listener with false
|
|
|
272
312
|
|
|
273
313
|
Two of its choices are worth understanding, because they're really
|
|
274
314
|
statements about how Tuile widgets behave in general. The first: **Enter
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
315
|
+
toggles it, just as Space does.** Space is the native gesture for flipping
|
|
316
|
+
a checkbox, and for a while Enter was deliberately left alone — a checkbox
|
|
317
|
+
has no action to confirm. What settled it is the checkbox group further
|
|
318
|
+
down this chapter: a checkable row inside a list flips on Enter, because
|
|
319
|
+
Enter is how a list chooses the row under its cursor. Had the standalone
|
|
320
|
+
widget stayed silent, the same `[ ] Verbose` would have responded to Enter
|
|
321
|
+
in a group and ignored it in a form, which is a distinction the person at
|
|
322
|
+
the keyboard has no way to see.
|
|
323
|
+
|
|
324
|
+
The consequence is worth stating plainly, because it's the general rule
|
|
325
|
+
hiding behind the specific choice: a focused checkbox *consumes* Enter, so
|
|
326
|
+
a form's submit button on an ancestor won't see it. That's not a
|
|
327
|
+
regression from some guarantee — the framework never kept Enter clear, and
|
|
328
|
+
chapter 5's table shows why it can't: a text area claims Enter for a
|
|
329
|
+
newline, a button claims it to activate itself. Whether Enter reaches your
|
|
330
|
+
form always depends on the widget that has focus.
|
|
284
331
|
|
|
285
332
|
The second is about *where the widget actually is*. A form column will
|
|
286
333
|
happily hand a checkbox forty columns for a caption that needs twenty-two,
|
|
@@ -331,10 +378,11 @@ coerced), and let the widget's own toggling build the new sets for you.
|
|
|
331
378
|
Here the cursor and the selection are genuinely two different things — the
|
|
332
379
|
cursor says *where you are*, the checkmarks say *what you picked* — and
|
|
333
380
|
that shape is exactly what a list already provides. So a checkbox group
|
|
334
|
-
doesn't paint rows itself; it holds a {Tuile::Component::List}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
381
|
+
doesn't paint rows itself; it holds a {Tuile::Component::List} of the
|
|
382
|
+
items, supplies the renderer that puts a `[x]` or `[ ]` in front of each
|
|
383
|
+
label, and gets the cursor, the scrolling, the scrollbar and the per-row
|
|
384
|
+
mouse handling for free, in the same "wrap a generic component to make a
|
|
385
|
+
domain one" way the combo box wraps a text field. That inheritance goes further than
|
|
338
386
|
convenience: a click anywhere on a row toggles it, and Enter toggles the
|
|
339
387
|
cursor's row, because those are the list's own gestures for choosing an
|
|
340
388
|
item.
|
|
@@ -412,6 +460,76 @@ one cell, and keeps the set of characters riding on that bet small enough
|
|
|
412
460
|
to enumerate, so a new widget reaches for ASCII and offers the pretty
|
|
413
461
|
glyph only where someone can opt in knowing their terminal.
|
|
414
462
|
|
|
463
|
+
A radio group spends a row per option, permanently. When the form has six
|
|
464
|
+
of these and a terminal has twenty-four rows, that arithmetic stops
|
|
465
|
+
working, and {Tuile::Component::Select} is the same single answer on *one*
|
|
466
|
+
row: the selected label plus a `▾`, with the options appearing only while
|
|
467
|
+
you're choosing between them.
|
|
468
|
+
|
|
469
|
+
```ruby
|
|
470
|
+
level = Component::Select.new(items: %w[debug info warn error], value: "warn")
|
|
471
|
+
level.on_value_change = ->(l) { logger.level = l }
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
Enter, Space or Down opens the dropdown, the arrows move the highlight,
|
|
475
|
+
Enter or Space commits, ESC closes it having changed nothing. `value` is
|
|
476
|
+
the selected item as always, `nil` while nothing is selected — and that
|
|
477
|
+
`nil` is a perfectly ordinary state here, which is why there's no
|
|
478
|
+
placeholder text: an optional enum field simply shows a blank face.
|
|
479
|
+
|
|
480
|
+
So when do you reach for which? The temptation is to decide by item count,
|
|
481
|
+
and that's the wrong axis. Ask instead **who wrote the labels**:
|
|
482
|
+
|
|
483
|
+
| The options are… | Widget | Why |
|
|
484
|
+
|---|---|---|
|
|
485
|
+
| a developer-authored enum, on one form row | `Select` | one row; borrows *n* transiently |
|
|
486
|
+
| the same enum, worth comparing side by side | `RadioGroup` | spends *n* rows permanently |
|
|
487
|
+
| supplied by the app, open-ended, labels you don't control | `ComboBox` | filtering *is* the navigation |
|
|
488
|
+
| an enum, several of which apply | `CheckboxGroup` | a frozen `Set` value |
|
|
489
|
+
|
|
490
|
+
A select is for a closed set you knew when you wrote the code — log level,
|
|
491
|
+
sort order, line endings, Yes/No/Ask. A combo box is for countries, users,
|
|
492
|
+
branches: data. A twelve-value enum is still a select, and a three-row
|
|
493
|
+
country list loaded from a database is still a combo box, because next
|
|
494
|
+
release it's two hundred rows and the widget you chose shouldn't have to
|
|
495
|
+
change. Count is a symptom; authorship is the criterion.
|
|
496
|
+
|
|
497
|
+
Which brings up the property that really separates the two, and it's not
|
|
498
|
+
the filtering. **A select claims no printable key but Space.** Every other
|
|
499
|
+
letter and digit bubbles straight past it, up the focus chain, to your
|
|
500
|
+
application — so a form's `s`-to-save, or a layout's `1`/`2`/`3` jumps
|
|
501
|
+
between panes, keep working while focus sits in a select. A combo box can
|
|
502
|
+
never offer that: its field must eat every printable, because every
|
|
503
|
+
printable is potentially part of the query. Add the fact that a select has
|
|
504
|
+
no caret, and the two together are the whole case for the component. A
|
|
505
|
+
caret is the strongest promise a terminal can make about what a widget
|
|
506
|
+
does, and spending it on "you may type free text here" over a four-value
|
|
507
|
+
enum is a lie the user then has to discover.
|
|
508
|
+
|
|
509
|
+
Space is the one exception, and it's a safe one precisely because Space was
|
|
510
|
+
never yours to begin with: every activatable widget in Tuile already claims
|
|
511
|
+
it — a button, a checkbox, a radio group. Home and End, by contrast, are
|
|
512
|
+
declined, so they stay available for you to bind app-wide.
|
|
513
|
+
|
|
514
|
+
You may be waiting for type-ahead — press `f` and jump to the first item
|
|
515
|
+
starting with `f`, the way desktop lists do. It isn't there, deliberately.
|
|
516
|
+
The single-key version is silently wrong: with Finland, Fiji and Jamaica in
|
|
517
|
+
the list, typing `fij` selects *Jamaica*, because each key is a fresh
|
|
518
|
+
one-character match. The fix everyone reaches for next is a small
|
|
519
|
+
accumulating buffer that clears after a second of idleness — and that
|
|
520
|
+
buffer *is* the combo box's query with the display removed. If you're
|
|
521
|
+
holding query state, showing it is strictly better than hiding it, and
|
|
522
|
+
showing it is a combo box. On a terminal it's worse still: the timer leans
|
|
523
|
+
on inter-keystroke gaps, and gaps are exactly what a laggy SSH link or a
|
|
524
|
+
paste destroys.
|
|
525
|
+
|
|
526
|
+
One small nicety worth noticing: the dropdown is never narrower than the
|
|
527
|
+
select itself, and grows past it when a label needs the room — so its edges
|
|
528
|
+
line up with the face you clicked, and the labels are never the thing that
|
|
529
|
+
gets ellipsized. It opens below the select, flips above near the bottom of
|
|
530
|
+
the screen, slides left rather than running off the right edge, and grows a
|
|
531
|
+
scrollbar when there are more options than it can show.
|
|
532
|
+
|
|
415
533
|
For a discrete action rather than a selection, {Tuile::Component::Button}
|
|
416
534
|
is a one-row `[ caption ]` that fires `on_click` on Enter, Space, or a
|
|
417
535
|
left-click, highlighting its background while focused. It's a tab stop, so
|
|
@@ -497,7 +615,7 @@ popups are for.
|
|
|
497
615
|
|
|
498
616
|
The bottom border has two mutually exclusive uses, and the distinction is
|
|
499
617
|
the top-down-layout principle from chapter 3 made concrete. `footer_text=`
|
|
500
|
-
embeds decoration into the border
|
|
618
|
+
embeds decoration into the border row — chrome, mirroring the caption on
|
|
501
619
|
top, not focusable. `footer=` mounts a *real focusable component* spanning
|
|
502
620
|
the full inner width — the search-field-in-the-border case. A footer
|
|
503
621
|
component present takes the row and hides the text; neither drives the
|
|
@@ -544,6 +662,53 @@ A nested TextField still swallows printable keys first, so typing `q` into
|
|
|
544
662
|
a field inside a popup doesn't dismiss it — the popup's own `q` handler sits
|
|
545
663
|
on the ancestor, and only sees keys the field declined.
|
|
546
664
|
|
|
665
|
+
## Notifications
|
|
666
|
+
|
|
667
|
+
{Tuile::Component::Notification} is the one overlay you don't assemble at
|
|
668
|
+
all. It's the TTY toast: a message in the top-right corner that shows up,
|
|
669
|
+
holds for three seconds, and removes itself.
|
|
670
|
+
|
|
671
|
+
```ruby
|
|
672
|
+
Component::Notification.show("Saved")
|
|
673
|
+
Component::Notification.show("Disk almost full", color: Color::RED)
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
That class method is the *only* way in — `new` is private. The reason is
|
|
677
|
+
worth understanding, because it's the design in one line: there is never
|
|
678
|
+
more than one notification box on screen. `show` looks for the live one in
|
|
679
|
+
the popups stack and appends to it, so a burst of messages stacks as
|
|
680
|
+
entries inside a single frame:
|
|
681
|
+
|
|
682
|
+
```
|
|
683
|
+
┌──────────────┐
|
|
684
|
+
│Job 1 finished│ ← goes in 3 s
|
|
685
|
+
│Job 2 finished│ ← then this one
|
|
686
|
+
│Job 3 finished│
|
|
687
|
+
└──────────────┘
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
They then leave **one at a time**, oldest first, three seconds apart. This
|
|
691
|
+
is the interesting half of the design. Five notifications raised in the
|
|
692
|
+
same instant would, given five independent timers, appear and vanish
|
|
693
|
+
together — a flash you have no chance of reading. Draining them one per
|
|
694
|
+
tick means the burst takes fifteen seconds to clear and you read it in
|
|
695
|
+
peace. A message arriving mid-cycle just waits its turn rather than
|
|
696
|
+
restarting the clock, which is also what stops a steady trickle of
|
|
697
|
+
notifications from keeping the box alive forever.
|
|
698
|
+
|
|
699
|
+
Everything else follows from "a toast must not interrupt": it's a non-modal
|
|
700
|
+
popup, so it takes no focus, receives no keys (not even the `q` a normal
|
|
701
|
+
popup would claim), and blocks no click outside its own box. You keep
|
|
702
|
+
typing into whatever you were typing into, and the notification appears and
|
|
703
|
+
leaves around you. A left-click on the box dismisses the whole thing early.
|
|
704
|
+
|
|
705
|
+
Two limits are worth knowing before you reach them. A long message wraps to
|
|
706
|
+
at most three rows and is then ellipsized — the box is capped at 40 % of
|
|
707
|
+
the screen — and at most five messages are held, after which the newest is
|
|
708
|
+
dropped and reported to `Tuile.logger`. Both are deliberate: a notification
|
|
709
|
+
is a glance, not a document, and an app with more to say than five short
|
|
710
|
+
lines wants a LogWindow, which is next.
|
|
711
|
+
|
|
547
712
|
## Batteries-included windows
|
|
548
713
|
|
|
549
714
|
The last three components are conveniences: common Window-plus-content
|
data/book/README.md
CHANGED
|
@@ -52,7 +52,9 @@ one, not to fill an outline.
|
|
|
52
52
|
the design. Top-down, absolute, integer coordinates; a parent
|
|
53
53
|
assigns its children's `rect` and components never negotiate a size.
|
|
54
54
|
The C64 argument for *why simple layouting is enough* on a character
|
|
55
|
-
grid, `Layout::Absolute` and the `rect=` override, `
|
|
55
|
+
grid, `Layout::Absolute` and the `rect=` override, the `Vertical` /
|
|
56
|
+
`Horizontal` box layouts and their three constraints (`Fixed` /
|
|
57
|
+
`Percent` / `Expand`) as sugar over that same rule, `Fraction` for
|
|
56
58
|
sizing a popup against the screen, and resize as a discrete
|
|
57
59
|
recompute. Geometry primitives (`Point` / `Size` / `Rect`) live here.
|
|
58
60
|
4. **[The event loop and background work](04-event-loop.md).** The
|
data/examples/file_commander.rb
CHANGED
|
@@ -33,6 +33,7 @@ module FileCommanderExample
|
|
|
33
33
|
def initialize(start_dir)
|
|
34
34
|
super()
|
|
35
35
|
self.cursor = Tuile::Component::List::Cursor.new
|
|
36
|
+
self.renderer = ->(entry) { Rainbow(entry[:display]).color(TYPE_COLORS[entry[:type]]) }
|
|
36
37
|
@cwd = File.expand_path(start_dir)
|
|
37
38
|
@on_cwd_changed = nil
|
|
38
39
|
load_entries
|
|
@@ -60,8 +61,8 @@ module FileCommanderExample
|
|
|
60
61
|
|
|
61
62
|
private
|
|
62
63
|
|
|
63
|
-
def descend(_index,
|
|
64
|
-
target = File.expand_path(File.join(@cwd,
|
|
64
|
+
def descend(_index, entry)
|
|
65
|
+
target = File.expand_path(File.join(@cwd, entry[:name]))
|
|
65
66
|
change_to(target) if File.directory?(target)
|
|
66
67
|
end
|
|
67
68
|
|
|
@@ -75,7 +76,7 @@ module FileCommanderExample
|
|
|
75
76
|
@cwd = path
|
|
76
77
|
load_entries
|
|
77
78
|
self.cursor = Tuile::Component::List::Cursor.new
|
|
78
|
-
self.
|
|
79
|
+
self.scroll_top_row = 0
|
|
79
80
|
@on_cwd_changed&.call
|
|
80
81
|
rescue SystemCallError => e
|
|
81
82
|
@cwd = previous
|
|
@@ -89,7 +90,7 @@ module FileCommanderExample
|
|
|
89
90
|
{ name: name, type: classify(path), display: is_dir ? "#{name}/" : name, dir_first: is_dir ? 0 : 1 }
|
|
90
91
|
end
|
|
91
92
|
entries.sort_by! { |e| [e[:dir_first], e[:name].downcase] }
|
|
92
|
-
self.
|
|
93
|
+
self.items = entries
|
|
93
94
|
end
|
|
94
95
|
|
|
95
96
|
# Classify by symlink first so a symlink-to-dir still reads as a link.
|