tuile 0.13.0 → 0.15.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.
Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +150 -37
  3. data/COMPARISON.md +101 -0
  4. data/DECISIONS.md +4266 -226
  5. data/README.md +44 -24
  6. data/TERMINOLOGY.md +22 -7
  7. data/book/03-layout.md +17 -10
  8. data/book/05-focus.md +67 -3
  9. data/book/06-theming.md +153 -7
  10. data/book/07-components.md +643 -67
  11. data/book/08-testing.md +94 -0
  12. data/book/09-styled-text.md +3 -3
  13. data/book/10-locale.md +216 -0
  14. data/book/README.md +14 -5
  15. data/examples/file_commander.rb +1 -1
  16. data/examples/sampler.rb +402 -62
  17. data/ideas/arrow-key-navigation.md +2 -2
  18. data/ideas/binder.md +177 -0
  19. data/ideas/composite-field.md +77 -0
  20. data/ideas/focus-accent.md +116 -0
  21. data/ideas/form-layout.md +151 -0
  22. data/ideas/hover/probe.rb +241 -0
  23. data/ideas/hover/probe_spec.rb +82 -0
  24. data/ideas/hover.md +909 -0
  25. data/ideas/modal-backdrop.md +24 -0
  26. data/ideas/new-components.md +49 -29
  27. data/lib/tuile/buffer.rb +51 -3
  28. data/lib/tuile/color.rb +143 -0
  29. data/lib/tuile/color_depth.rb +80 -0
  30. data/lib/tuile/component/abstract_string_field.rb +106 -58
  31. data/lib/tuile/component/abstract_wrapping_field.rb +243 -0
  32. data/lib/tuile/component/big_decimal_field.rb +52 -79
  33. data/lib/tuile/component/button.rb +3 -3
  34. data/lib/tuile/component/checkbox.rb +3 -3
  35. data/lib/tuile/component/checkbox_group.rb +36 -20
  36. data/lib/tuile/component/combo_box.rb +68 -33
  37. data/lib/tuile/component/confirm_window.rb +442 -0
  38. data/lib/tuile/component/date_field.rb +322 -0
  39. data/lib/tuile/component/float_field.rb +57 -82
  40. data/lib/tuile/component/has_bad_input.rb +88 -0
  41. data/lib/tuile/component/has_caption.rb +8 -0
  42. data/lib/tuile/component/has_content.rb +43 -11
  43. data/lib/tuile/component/has_placeholder.rb +62 -0
  44. data/lib/tuile/component/has_validation.rb +115 -0
  45. data/lib/tuile/component/has_value.rb +28 -1
  46. data/lib/tuile/component/info_window.rb +64 -16
  47. data/lib/tuile/component/integer_field.rb +51 -78
  48. data/lib/tuile/component/label.rb +6 -38
  49. data/lib/tuile/component/layout/box.rb +87 -19
  50. data/lib/tuile/component/layout.rb +13 -13
  51. data/lib/tuile/component/list.rb +11 -6
  52. data/lib/tuile/component/list_dropdown.rb +22 -10
  53. data/lib/tuile/component/log_text_view.rb +71 -0
  54. data/lib/tuile/component/log_window.rb +13 -48
  55. data/lib/tuile/component/menu_bar/cascade.rb +3 -3
  56. data/lib/tuile/component/menu_bar.rb +5 -5
  57. data/lib/tuile/component/notification.rb +16 -34
  58. data/lib/tuile/component/overlay.rb +209 -0
  59. data/lib/tuile/component/popup.rb +59 -187
  60. data/lib/tuile/component/progress_bar.rb +1 -1
  61. data/lib/tuile/component/radio_group.rb +39 -22
  62. data/lib/tuile/component/select.rb +26 -10
  63. data/lib/tuile/component/slot.rb +54 -0
  64. data/lib/tuile/component/tab_sheet.rb +0 -11
  65. data/lib/tuile/component/tabs.rb +5 -5
  66. data/lib/tuile/component/text_area.rb +14 -8
  67. data/lib/tuile/component/text_field.rb +42 -15
  68. data/lib/tuile/component/text_view.rb +25 -8
  69. data/lib/tuile/component/time_field.rb +454 -0
  70. data/lib/tuile/component/window.rb +48 -59
  71. data/lib/tuile/component.rb +580 -54
  72. data/lib/tuile/event_queue.rb +21 -1
  73. data/lib/tuile/fake_screen.rb +37 -3
  74. data/lib/tuile/final.rb +75 -0
  75. data/lib/tuile/keys.rb +7 -0
  76. data/lib/tuile/locale.rb +851 -0
  77. data/lib/tuile/screen.rb +251 -55
  78. data/lib/tuile/screen_pane.rb +50 -44
  79. data/lib/tuile/styled_string.rb +40 -7
  80. data/lib/tuile/terminal_background.rb +74 -16
  81. data/lib/tuile/testing.rb +198 -0
  82. data/lib/tuile/theme.rb +100 -10
  83. data/lib/tuile/version.rb +1 -1
  84. data/lib/tuile/vertical_scroll_bar.rb +88 -12
  85. data/lib/tuile.rb +1 -0
  86. data/sig/tuile.rbs +4545 -770
  87. metadata +25 -1
data/book/08-testing.md CHANGED
@@ -95,6 +95,84 @@ lives in the buffer; cursor behavior lives in `prints`. Keeping the two
95
95
  apart is deliberate, and mixing them up is the most common way a first
96
96
  Tuile test goes wrong.
97
97
 
98
+ ## Finding the component to drive
99
+
100
+ Asserting on the buffer needs a `rect`; driving a component needs the
101
+ component itself. For a two-line test you have it already — you just built
102
+ it. The awkward case is the one that shows up as soon as an app grows: the
103
+ widget you want to poke is four levels down inside something a *builder
104
+ method* assembled, and the test never held a reference to it.
105
+
106
+ {Tuile::Testing} is the answer. `Testing.get` walks the tree and returns the
107
+ one component matching a spec:
108
+
109
+ ```ruby
110
+ Testing.get(Component::Button, caption: "Save").handle_key(Keys::ENTER)
111
+ Testing.get(id: :amount).value = 42
112
+ ```
113
+
114
+ The spec is a class, an `id`, a caption, a block, or any combination of
115
+ them — never a path through the hierarchy, which would break every time you
116
+ nested one more layout. The class slot also takes a *mixin*, which is where
117
+ the `Has*` family from chapter 7 pays off a second time:
118
+ `Testing.find(Component::HasBadInput)` finds every field in the tree whose
119
+ parse can fail, whatever their classes.
120
+
121
+ The `id` in that second line is a plain `Symbol` tag you set on any
122
+ component, purely so a test can ask for it back:
123
+
124
+ ```ruby
125
+ amount = Component::IntegerField.new
126
+ amount.id = :amount # nothing paints this, and nothing reads it
127
+ ```
128
+
129
+ `get` is strict on purpose: it raises unless *exactly one* component
130
+ matches. That is the whole feature. The obvious thing to write by hand is a
131
+ walk that takes the first match —
132
+
133
+ ```ruby
134
+ combo = nil
135
+ window.on_tree { |c| combo ||= c if c.is_a?(Component::ComboBox) }
136
+ ```
137
+
138
+ — and the day the pane grows a second `ComboBox`, that silently re-points
139
+ your test at a different widget. Nothing fails; the assertions just start
140
+ describing something else. `get` calls that ambiguity what it is, and the
141
+ failure comes with a dump of the tree it searched:
142
+
143
+ ```
144
+ expected 1 Component::ComboBox, found 2
145
+ searched:
146
+ #<ScreenPane rect=(0,0 160x50)>
147
+ #<Window rect=(0,0 40x10) caption="Settings">
148
+ → #<ComboBox rect=(1,1 38x1) value=nil>
149
+ → #<ComboBox rect=(1,2 38x1) value="dark">
150
+ ```
151
+
152
+ Which usually tells you the fix immediately: narrow the search. Every
153
+ lookup takes `in:` to scope it to a subtree, and by default searches the
154
+ whole screen — popups included, since chapter 1's pane holds them under the
155
+ same root as the content.
156
+
157
+ ```ruby
158
+ Testing.get(Component::ComboBox, in: sampler.demo_window)
159
+ ```
160
+
161
+ Its sibling `Testing.find` returns *all* matches as an array, and takes an
162
+ optional `count:` — an Integer for exactly, a Range for a bound — so an
163
+ assertion about how many of something exists is one call:
164
+
165
+ ```ruby
166
+ Testing.find(Component::Checkbox, in: form, count: 3) # raises unless 3
167
+ ```
168
+
169
+ Two habits worth forming. Call these qualified, as `Testing.get(...)`:
170
+ `find` and `get` are the most collision-prone names in a spec suite, so
171
+ Tuile deliberately neither installs them on `Component` nor asks you to mix
172
+ the module in. And remember this is *additive* — it makes driving a tree
173
+ terser, and changes nothing about the assertion channel. What a component
174
+ *shows* is still asserted on the buffer.
175
+
98
176
  ## Driving the system
99
177
 
100
178
  There are two altitudes at which you feed input, and picking the right one
@@ -108,9 +186,25 @@ key, so you assert on that too:
108
186
 
109
187
  ```ruby
110
188
  list.handle_key(Keys::DOWN_ARROW) # exercises the cursor directly
189
+ ```
190
+
191
+ **A mouse test needs the component mounted, where a key test doesn't.** A
192
+ click doesn't only *do* something, it also *focuses* — and
193
+ {Tuile::Screen#focused=} refuses a component that isn't on the pane, so
194
+ `handle_mouse` on a component you never attached raises "is not attached to
195
+ this screen". Give it a tree first:
196
+
197
+ ```ruby
198
+ screen.content = list # a click focuses; focus needs a tree
199
+ list.rect = Rect.new(0, 0, 10, 5)
111
200
  list.handle_mouse(MouseEvent.new(:left, 5, 2))
112
201
  ```
113
202
 
203
+ That applies to containers too, and to more of them than you might expect:
204
+ a click descends to every child whose rect contains the point, so testing a
205
+ window's footer by clicking it exercises the window, the footer's slot and
206
+ the footer, all of which want to be attached.
207
+
114
208
  **High: go through the pane.** {Tuile::ScreenPane#handle_key} runs the
115
209
  dispatch rung from chapter 5 that routing is actually about: delivery to
116
210
  {Tuile::Screen#focused}, then the bubble up its ancestor chain to the scope
@@ -29,7 +29,7 @@ tangle has to be re-untangled on every edit.
29
29
  {Tuile::StyledString} untangles it once, structurally. A styled string is
30
30
  a sequence of **spans**, each a maximal run of characters that share one
31
31
  complete {Tuile::StyledString::Style} — foreground, background, bold,
32
- italic, underline, strikethrough. The spans are non-overlapping and tile
32
+ italic, underline, strikethrough, inverse. The spans are non-overlapping and tile
33
33
  the whole string: every character belongs to exactly one span, and that
34
34
  span's `style` *is* the character's style. There are no overlay layers to
35
35
  merge, no running state to reconstruct. "What's the style at column 5?"
@@ -100,8 +100,8 @@ by default.**
100
100
 
101
101
  Strict means it recognizes exactly the SGR codes that map to a
102
102
  {Tuile::StyledString::Style}'s attributes — the foreground and background
103
- colors, bold, italic, underline, strikethrough — and *raises* on anything
104
- else. An unmodeled attribute like blink or reverse video, an unknown SGR
103
+ colors, bold, italic, underline, strikethrough, inverse — and *raises* on
104
+ anything else. An unmodeled attribute like blink or conceal, an unknown SGR
105
105
  code, a non-SGR escape like a cursor move or an OSC sequence: all of them
106
106
  are a {Tuile::StyledString::ParseError}, not a shrug. The reason is a
107
107
  contract worth protecting: `parse(to_ansi(x)) == x`. If parsing silently
data/book/10-locale.md ADDED
@@ -0,0 +1,216 @@
1
+ # 10. Locale: the conventions, not the words
2
+
3
+ Chapter 6 told one story about adapting to the user's environment: the
4
+ terminal reports whether it is light or dark, Tuile picks a matching
5
+ {Tuile::Theme}, and the whole UI restyles. This chapter tells the same
6
+ story about a different fact — not what the terminal looks like, but how
7
+ the *person* in front of it expects a date and a number to be spelled.
8
+
9
+ The two are shaped alike on purpose. A frozen value type, detected once
10
+ at startup, held on the screen, read at use time, replaceable at
11
+ runtime, with a hook for anything that baked the old answer in. If you
12
+ have read chapter 6, you already know the shape; what is worth your time
13
+ here is the *boundary* — because this is the feature most likely to grow
14
+ into something Tuile is not.
15
+
16
+ ## The one rule: conventions, never prose
17
+
18
+ > A {Tuile::Locale} holds formatting conventions — how a value is
19
+ > rendered and parsed. It never holds prose.
20
+
21
+ That sentence is the whole design. It is what makes `Locale` a bag of
22
+ about eight members rather than an internationalization subsystem, and
23
+ it is the test a ninth member has to pass.
24
+
25
+ The distinction is easy to feel once you see the two halves side by
26
+ side. "Dates in this session are spelled `dd.mm.yyyy`" is a convention:
27
+ it is a *rule* about rendering, it applies to every date the app will
28
+ ever show, and Tuile can obey it without knowing anything about your
29
+ app. "The date you typed is not valid" is prose: it is one sentence, in
30
+ one language, that belongs to one widget, and translating it is a job
31
+ with a catalogue, interpolation, and pluralization behind it. Tuile does
32
+ the first and stays out of the second — a component's wording stays the
33
+ component's own, settable where it matters.
34
+
35
+ And the line is not Tuile's invention. POSIX drew it first: `LC_MESSAGES`
36
+ carries the language your programs speak, `LC_TIME` and `LC_NUMERIC`
37
+ carry the formatting conventions. Tuile reads the formatting categories
38
+ and never the message one. That is why a session can coherently ask for
39
+ an English UI *and* ISO dates *and* a decimal comma — three categories,
40
+ three independent answers, which is exactly what one of your author's
41
+ machines is configured to want.
42
+
43
+ ## What is in one
44
+
45
+ ```ruby
46
+ locale = Tuile::Screen.instance.locale
47
+
48
+ locale.date_formats # => ["%Y-%m-%d"] strftime patterns, primary first
49
+ locale.time_formats # => ["%H:%M:%S"] ditto, at full precision
50
+ locale.calendar_start # => Date::GREGORIAN
51
+ locale.first_weekday # => 1 Monday, in Date#wday numbering
52
+ locale.month_names[9] # => "September"
53
+ locale.abbr_day_names[1] # => "Mon"
54
+ locale.decimal_separator # => "."
55
+ ```
56
+
57
+ Two things about those last few are worth pausing on.
58
+
59
+ **The name tables are keyed by the `Date` accessor that reads them.**
60
+ `month_names[date.month]`, `day_names[date.wday]` — so you never do
61
+ arithmetic to look a name up. That is why the two have different shapes:
62
+ `Date#month` is 1-based, so the month tables are `Hash`es keyed `1..12`;
63
+ `Date#wday` is 0-based, so the day tables are plain seven-element
64
+ arrays. It looks like an inconsistency and is the opposite of one. A
65
+ 0-based month array would answer `month_names[9] # => "October"` — a
66
+ *plausible wrong answer*, silently, in the calendar header of a
67
+ month-view someone ships. The `Hash` makes that mistake impossible to
68
+ express instead of merely documented against.
69
+
70
+ **`date_formats` is a list, and the first entry is special.** Parsing
71
+ tries them in order and the first whole match wins; the primary —
72
+ `formats.first` — is also what gets *written* when a value is assigned or
73
+ a loosely typed buffer is canonicalized. Lenient in, strict out. Which
74
+ means only the primary has to survive a round-trip; a later entry only
75
+ ever parses, so it is allowed to be lossy. Chapter 7 has the field-side
76
+ story; {Tuile::Component::DateField} has the details.
77
+
78
+ **`time_formats` keeps its seconds, and the field is what drops them.**
79
+ This is the one member whose detected value is deliberately *more* than
80
+ any field shows. glibc's `t_fmt` — where the conventions come from — is a
81
+ **clock display** format, so it carries seconds nearly everywhere:
82
+ `%H:%M:%S` under C, `%H.%M.%S` under Finnish. Honoring that in a form
83
+ would put `:00` in front of every user who never asked for it, which is
84
+ what WinForms and Ant Design both actually ship.
85
+
86
+ So the seconds are dropped — but *in the field*, not here, and the
87
+ distinction is the whole point. Dropping them is a **policy**, and a
88
+ lossy one: a status-bar clock or a log timestamp legitimately wants
89
+ `13:45:30`, and if the locale had already thrown the seconds away, it
90
+ would have no way to ask for them back. A convention that carries less
91
+ than the system said is not a convention any more. So the locale keeps
92
+ the full spelling, {Tuile::Component::TimeField} reduces it to whatever
93
+ its `step` calls for, and a consumer that wants the other answer still
94
+ has one.
95
+
96
+ What *does* happen at the boundary is expansion: libc's shorthands are
97
+ rewritten into their parts (`%T` → `%H:%M:%S`, `%r` → `%I:%M:%S %p`) so
98
+ no consumer has to know them. That is a representation change and
99
+ nothing more, which is the line between what normalizes here and what
100
+ does not.
101
+
102
+ ## Detection: ask the system, and only when it was asked
103
+
104
+ Ruby exposes no locale data at all. There is no `nl_langinfo` binding,
105
+ nothing on `Date`, nothing in `Etc`; `Date::MONTHNAMES` is frozen
106
+ English under every `LC_ALL` you set, and `strftime("%x")` is a fixed
107
+ American `%m/%d/%y` that merely *looks* like a locale channel. So
108
+ {Tuile::Locale.system} shells out to `locale -k` — one subprocess of
109
+ about a millisecond, whose answers are already strftime patterns, which
110
+ is Tuile's own vocabulary rather than a second grammar to translate.
111
+
112
+ The interesting part is not the subprocess. It is the gate in front of
113
+ it.
114
+
115
+ The C/POSIX default date format is American. That means "the user said
116
+ nothing" and "the user wants `%m/%d/%y`" are *indistinguishable* from
117
+ the answer — so a container with no `LANG` set would confidently show
118
+ American dates to a Norwegian. Tuile therefore detects only when the
119
+ environment actually said something: if the POSIX chain for a category
120
+ is empty, or names `C` or `POSIX`, the probe's answer for that category
121
+ is thrown away and {Tuile::Locale::ISO} stands.
122
+
123
+ And the gate is *per category*, which is the part that surprises people.
124
+ Gating once on "any locale variable is set" is wrong on a real machine:
125
+ a session exporting only `LC_NUMERIC=de_DE.UTF-8` would open the gate,
126
+ and then `d_fmt` — resolving through an unset `LC_TIME` and an unset
127
+ `LANG` — comes back American. So the date conventions are kept only if
128
+ `LC_ALL` / `LC_TIME` / `LANG` speaks, and the numeric one only if
129
+ `LC_ALL` / `LC_NUMERIC` / `LANG` does. One subprocess, two gates.
130
+
131
+ Everything else about detection is defensive, because none of it can be
132
+ allowed to fail loudly at startup: `locale(1)`'s exit status is useless
133
+ in both directions (a bad locale name exits 0 and quietly returns C; an
134
+ unknown keyword exits 1 while printing every good key), so the status is
135
+ ignored and each value is validated on its own. Anything that does not
136
+ hold up falls back to its `ISO` member individually — not
137
+ all-or-nothing. A missing binary, a Windows box, a musl container: `ISO`,
138
+ no exception raised.
139
+
140
+ ## The floor is ISO 8601, and that is an argument
141
+
142
+ {Tuile::Locale::ISO} is the only constant Tuile ships. There is no
143
+ `EN_US`, no `DE`, and there will not be: two presets are a catalogue, a
144
+ catalogue implies completeness, and completeness is a promise about the
145
+ world's date conventions that a TUI toolkit has no business making. You
146
+ build your own from `ISO`:
147
+
148
+ ```ruby
149
+ screen.locale = Tuile::Locale::ISO.with(date_formats: ["%d.%m.%Y", "%Y-%m-%d"])
150
+ ```
151
+
152
+ Every member is validated in the constructor, and `Data#with` re-runs the
153
+ constructor — so an invalid `Locale` is unreachable by either route.
154
+
155
+ `ISO` is a defensible floor rather than a default someone picked, because
156
+ three of its members cite the same standard: ISO 8601 dates, an ISO 8601
157
+ week starting Monday, and the proleptic Gregorian calendar ISO 8601
158
+ mandates. The names are Ruby's own frozen English tables, re-keyed but
159
+ not authored — so Tuile ships zero locale data of its own, even in the
160
+ fallback. And the decimal separator is `"."` not because ISO says so (ISO
161
+ 31-0 rather prefers the comma) but because `Float#to_s` writes a dot and
162
+ a field's `value=` goes through it: any other floor would make a numeric
163
+ field disagree with itself before you configured anything.
164
+
165
+ ## Fields follow the session unless you say otherwise
166
+
167
+ A {Tuile::Component::DateField} takes both of its conventions from
168
+ `Screen#locale` until you assign them. So the common cases are one line
169
+ each, and they compose:
170
+
171
+ ```ruby
172
+ screen.locale = Locale::ISO.with(date_formats: ["%d.%m.%Y"]) # every field
173
+ field.formats = "%Y-%m-%d" # this one only
174
+ field.formats = nil # follow again
175
+ ```
176
+
177
+ `nil` restoring inheritance is the same shape `placeholder=` and
178
+ `bg_color=` already use, and it is what keeps the two decisions
179
+ independent: overriding one field is not opting it out of the session
180
+ forever.
181
+
182
+ ## When the locale changes under you
183
+
184
+ {Tuile::Screen#locale=} fires {Tuile::Component#on_locale_changed}
185
+ across the attached tree and then invalidates all of it — the same
186
+ machinery {Tuile::Screen#theme=} uses, for the same reason.
187
+
188
+ Because everything repaints, anything your code *pulls* at paint or parse
189
+ time needs no hook at all. A date field parses its buffer on read; a
190
+ calendar grid would look month names up while painting. Both simply come
191
+ out right on the next frame.
192
+
193
+ The hook is for state you *pushed* somewhere when you last read the
194
+ conventions. A date field's typing hint is the worked example: it lives
195
+ in its editor's `placeholder`, written when the formats were last set, so
196
+ a repaint alone would faithfully repaint the stale `dd.mm.yyyy`. The
197
+ field overrides `on_locale_changed` to re-derive it — and, while it is
198
+ there, to rewrite a buffer that still parses into the new primary format.
199
+ Your own code does the same for a date you rendered into a
200
+ {Tuile::Component::Label}, either by overriding the hook or by assigning
201
+ the listener:
202
+
203
+ ```ruby
204
+ label.on_locale_changed = -> { label.text = due.strftime(screen.locale.date_formats.first) }
205
+ ```
206
+
207
+ One consequence to accept rather than defend against: a field holding a
208
+ half-typed buffer when the grammar changes under it may stop parsing, and
209
+ will then read as bad input. That is correct — the text is still there,
210
+ the user can see it, and they can fix it. A locale change is a
211
+ once-a-session event, not something worth contorting a field to survive.
212
+
213
+ The other rule inherited from chapter 6 applies unchanged: **read the
214
+ locale at use time; never cache it in an ivar.** A cached one strands on
215
+ the old conventions the moment someone assigns a new locale, and nothing
216
+ raises to tell you.
data/book/README.md CHANGED
@@ -29,9 +29,10 @@ theming (including live OS light/dark flips). Chapters 7–8 close out
29
29
  **narratively** — a tour of the shipped component toolbox framed around
30
30
  when and why to reach for each, and how to test a Tuile app end to end.
31
31
  Those two lean on the rdoc for the exact APIs; the guide keeps to the
32
- walkthroughs and use-cases. Chapter 9 is a **deep dive** on
33
- `Tuile::StyledString`, the text primitive under everything the framework
34
- draws — read it when you start rendering your own styled content.
32
+ walkthroughs and use-cases. Chapters 9–10 are **deep dives** on one
33
+ piece each, to read when you need them: `Tuile::StyledString`, the text
34
+ primitive under everything the framework draws, and `Tuile::Locale`, the
35
+ formatting conventions a date or a number is spelled by.
35
36
 
36
37
  The book grows organically — a chapter exists when a concept has earned
37
38
  one, not to fill an outline.
@@ -80,10 +81,18 @@ one, not to fill an outline.
80
81
  window conveniences — framed around *when and why* you reach for each.
81
82
  Signatures stay in the rdoc.
82
83
  8. **[Testing a Tuile app](08-testing.md).** The testing approach:
83
- `FakeScreen`, asserting against the painted buffer, driving
84
- invalidation, and PTY-based end-to-end tests of runnable scripts.
84
+ `FakeScreen`, asserting against the painted buffer, locating the
85
+ component to drive with `Tuile::Testing`, driving invalidation, and
86
+ PTY-based end-to-end tests of runnable scripts.
85
87
  9. **[Styled text](09-styled-text.md).** A deep dive on
86
88
  `Tuile::StyledString`, the span-based "text plus styling" value type
87
89
  under everything Tuile draws: why spans instead of a `String` full of
88
90
  escape codes, the style-aware algebra (slice/wrap/concat by display
89
91
  column), minimal-diff rendering, and the strict-vs-lenient parser.
92
+ 10. **[Locale](10-locale.md).** The other environment fact, shaped like
93
+ chapter 6's theme: `Tuile::Locale` holds *formatting conventions and
94
+ never prose*, detected from `locale(1)` only when the environment
95
+ actually asked, with `Locale::ISO` as the floor. Why the name tables
96
+ are keyed by the `Date` accessor that reads them, how a field follows
97
+ the session until you override it, and what `on_locale_changed` is
98
+ for.
@@ -82,7 +82,7 @@ module FileCommanderExample
82
82
  @on_cwd_changed&.call
83
83
  rescue SystemCallError => e
84
84
  @cwd = previous
85
- Tuile::Component::InfoWindow.open("Cannot open", [path, e.message])
85
+ Tuile::Component::InfoWindow.open("Cannot open", "#{path}\n#{e.message}")
86
86
  end
87
87
 
88
88
  def load_entries