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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +150 -37
- data/COMPARISON.md +101 -0
- data/DECISIONS.md +4266 -226
- data/README.md +44 -24
- data/TERMINOLOGY.md +22 -7
- data/book/03-layout.md +17 -10
- data/book/05-focus.md +67 -3
- data/book/06-theming.md +153 -7
- data/book/07-components.md +643 -67
- data/book/08-testing.md +94 -0
- data/book/09-styled-text.md +3 -3
- data/book/10-locale.md +216 -0
- data/book/README.md +14 -5
- data/examples/file_commander.rb +1 -1
- data/examples/sampler.rb +402 -62
- data/ideas/arrow-key-navigation.md +2 -2
- data/ideas/binder.md +177 -0
- data/ideas/composite-field.md +77 -0
- data/ideas/focus-accent.md +116 -0
- data/ideas/form-layout.md +151 -0
- data/ideas/hover/probe.rb +241 -0
- data/ideas/hover/probe_spec.rb +82 -0
- data/ideas/hover.md +909 -0
- data/ideas/modal-backdrop.md +24 -0
- data/ideas/new-components.md +49 -29
- data/lib/tuile/buffer.rb +51 -3
- data/lib/tuile/color.rb +143 -0
- data/lib/tuile/color_depth.rb +80 -0
- data/lib/tuile/component/abstract_string_field.rb +106 -58
- data/lib/tuile/component/abstract_wrapping_field.rb +243 -0
- data/lib/tuile/component/big_decimal_field.rb +52 -79
- data/lib/tuile/component/button.rb +3 -3
- data/lib/tuile/component/checkbox.rb +3 -3
- data/lib/tuile/component/checkbox_group.rb +36 -20
- data/lib/tuile/component/combo_box.rb +68 -33
- data/lib/tuile/component/confirm_window.rb +442 -0
- data/lib/tuile/component/date_field.rb +322 -0
- data/lib/tuile/component/float_field.rb +57 -82
- data/lib/tuile/component/has_bad_input.rb +88 -0
- data/lib/tuile/component/has_caption.rb +8 -0
- data/lib/tuile/component/has_content.rb +43 -11
- data/lib/tuile/component/has_placeholder.rb +62 -0
- data/lib/tuile/component/has_validation.rb +115 -0
- data/lib/tuile/component/has_value.rb +28 -1
- data/lib/tuile/component/info_window.rb +64 -16
- data/lib/tuile/component/integer_field.rb +51 -78
- data/lib/tuile/component/label.rb +6 -38
- data/lib/tuile/component/layout/box.rb +87 -19
- data/lib/tuile/component/layout.rb +13 -13
- data/lib/tuile/component/list.rb +11 -6
- data/lib/tuile/component/list_dropdown.rb +22 -10
- data/lib/tuile/component/log_text_view.rb +71 -0
- data/lib/tuile/component/log_window.rb +13 -48
- data/lib/tuile/component/menu_bar/cascade.rb +3 -3
- data/lib/tuile/component/menu_bar.rb +5 -5
- data/lib/tuile/component/notification.rb +16 -34
- data/lib/tuile/component/overlay.rb +209 -0
- data/lib/tuile/component/popup.rb +59 -187
- data/lib/tuile/component/progress_bar.rb +1 -1
- data/lib/tuile/component/radio_group.rb +39 -22
- data/lib/tuile/component/select.rb +26 -10
- data/lib/tuile/component/slot.rb +54 -0
- data/lib/tuile/component/tab_sheet.rb +0 -11
- data/lib/tuile/component/tabs.rb +5 -5
- data/lib/tuile/component/text_area.rb +14 -8
- data/lib/tuile/component/text_field.rb +42 -15
- data/lib/tuile/component/text_view.rb +25 -8
- data/lib/tuile/component/time_field.rb +454 -0
- data/lib/tuile/component/window.rb +48 -59
- data/lib/tuile/component.rb +580 -54
- data/lib/tuile/event_queue.rb +21 -1
- data/lib/tuile/fake_screen.rb +37 -3
- data/lib/tuile/final.rb +75 -0
- data/lib/tuile/keys.rb +7 -0
- data/lib/tuile/locale.rb +851 -0
- data/lib/tuile/screen.rb +251 -55
- data/lib/tuile/screen_pane.rb +50 -44
- data/lib/tuile/styled_string.rb +40 -7
- data/lib/tuile/terminal_background.rb +74 -16
- data/lib/tuile/testing.rb +198 -0
- data/lib/tuile/theme.rb +100 -10
- data/lib/tuile/version.rb +1 -1
- data/lib/tuile/vertical_scroll_bar.rb +88 -12
- data/lib/tuile.rb +1 -0
- data/sig/tuile.rbs +4545 -770
- metadata +25 -1
data/lib/tuile/event_queue.rb
CHANGED
|
@@ -247,6 +247,25 @@ module Tuile
|
|
|
247
247
|
end
|
|
248
248
|
end
|
|
249
249
|
|
|
250
|
+
# The terminal reported its background color — an OSC 11 reply, read
|
|
251
|
+
# back as a whole "key" by {Keys.getkey}.
|
|
252
|
+
#
|
|
253
|
+
# Only ever an answer to a {TerminalBackground::QUERY} someone wrote,
|
|
254
|
+
# and only the color: a {ColorSchemeEvent} is what prompts the re-probe
|
|
255
|
+
# and what carries the light/dark half. See {Screen#background_color}.
|
|
256
|
+
#
|
|
257
|
+
# @!attribute [r] color
|
|
258
|
+
# @return [Color] the reported background, 24-bit RGB.
|
|
259
|
+
class BackgroundColorEvent < Data.define(:color)
|
|
260
|
+
# @param key [String] key read via {Keys.getkey}.
|
|
261
|
+
# @return [BackgroundColorEvent, nil] nil when `key` is not an OSC 11
|
|
262
|
+
# background reply.
|
|
263
|
+
def self.parse(key)
|
|
264
|
+
result = TerminalBackground.parse(key)
|
|
265
|
+
result&.color && new(result.color)
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
250
269
|
# Emitted once when the queue is cleared, all messages are processed and the
|
|
251
270
|
# event loop will block waiting for more messages. Perfect time for
|
|
252
271
|
# repainting windows.
|
|
@@ -344,7 +363,8 @@ module Tuile
|
|
|
344
363
|
event = if key == Keys::PASTE_START
|
|
345
364
|
PasteEvent.new(Keys.read_paste)
|
|
346
365
|
else
|
|
347
|
-
MouseEvent.parse(key) || ColorSchemeEvent.parse(key) ||
|
|
366
|
+
MouseEvent.parse(key) || ColorSchemeEvent.parse(key) ||
|
|
367
|
+
BackgroundColorEvent.parse(key) || KeyEvent.new(key)
|
|
348
368
|
end
|
|
349
369
|
post event
|
|
350
370
|
end
|
data/lib/tuile/fake_screen.rb
CHANGED
|
@@ -27,7 +27,9 @@ module Tuile
|
|
|
27
27
|
super
|
|
28
28
|
@event_queue = FakeEventQueue.new
|
|
29
29
|
@size = Size.new(160, 50)
|
|
30
|
-
|
|
30
|
+
# super sized both to the test runner's TTY.
|
|
31
|
+
@buffer.resize(@size)
|
|
32
|
+
@pane.rect = Rect.new(0, 0, @size.width, @size.height)
|
|
31
33
|
@prints = []
|
|
32
34
|
end
|
|
33
35
|
|
|
@@ -81,12 +83,44 @@ module Tuile
|
|
|
81
83
|
@invalidated.clear
|
|
82
84
|
end
|
|
83
85
|
|
|
86
|
+
# Plays the terminal answering the OSC 11 re-probe, so a spec can
|
|
87
|
+
# exercise app code that derives colors from {#background_color}:
|
|
88
|
+
#
|
|
89
|
+
# Screen.instance.background_color = Color.rgb(30, 30, 46)
|
|
90
|
+
#
|
|
91
|
+
# Takes the same path a real reply does — a changed color fires
|
|
92
|
+
# {Component#on_theme_changed} across the tree and invalidates it.
|
|
93
|
+
# There is no such writer on {Screen}: the value is a report from the
|
|
94
|
+
# terminal, not a setting.
|
|
95
|
+
# @param color [Color]
|
|
96
|
+
# @return [void]
|
|
97
|
+
def background_color=(color)
|
|
98
|
+
on_background_color(color)
|
|
99
|
+
end
|
|
100
|
+
|
|
84
101
|
private
|
|
85
102
|
|
|
86
103
|
# No terminal probing in tests: skip {TerminalBackground.detect}
|
|
87
104
|
# (which would write an OSC 11 query to the test runner's TTY and
|
|
88
|
-
# steal its input) and pin the deterministic default.
|
|
105
|
+
# steal its input) and pin the deterministic default. The color is nil
|
|
106
|
+
# — the case every app must handle anyway — until a spec assigns one
|
|
107
|
+
# through {#background_color=}.
|
|
108
|
+
# @return [TerminalBackground::Result]
|
|
109
|
+
def detect_background = TerminalBackground::Result.new(scheme: :dark, color: nil)
|
|
110
|
+
|
|
111
|
+
# Pins the depth rather than reading the test runner's environment, so a
|
|
112
|
+
# spec asserting flushed bytes gets the same answer on a truecolor
|
|
113
|
+
# terminal, under `TERM=dumb` in CI, and inside tmux. A spec exercising
|
|
114
|
+
# degradation builds its own {Buffer} with the depth it wants.
|
|
89
115
|
# @return [Symbol]
|
|
90
|
-
def
|
|
116
|
+
def detect_color_depth = :truecolor
|
|
117
|
+
|
|
118
|
+
# Pins the conventions instead of probing, so a spec asserting a painted
|
|
119
|
+
# date gets the same answer under `LC_TIME=en_DK` as under `LANG=C` — and
|
|
120
|
+
# so no example pays for a `locale(1)` subprocess. A spec exercising
|
|
121
|
+
# detection assigns {Screen#locale=} or calls {Locale.from_keywords} with
|
|
122
|
+
# canned answers.
|
|
123
|
+
# @return [Locale]
|
|
124
|
+
def detect_locale = Locale::ISO
|
|
91
125
|
end
|
|
92
126
|
end
|
data/lib/tuile/final.rb
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tuile
|
|
4
|
+
# Ruby's missing `final` keyword. A class `extend`s this, marks the methods a
|
|
5
|
+
# subclass may not redefine, and calls {#verify_final!} from its own
|
|
6
|
+
# `initialize`:
|
|
7
|
+
#
|
|
8
|
+
# class Component
|
|
9
|
+
# extend Final
|
|
10
|
+
#
|
|
11
|
+
# final :children, :parent, :add_child
|
|
12
|
+
#
|
|
13
|
+
# def initialize = Component.verify_final!(self.class)
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# Class.new(Component) { def children = [] }.new # => Tuile::Error
|
|
17
|
+
#
|
|
18
|
+
# Marking a method says only *that* it is final. **Why** — the invariant an
|
|
19
|
+
# override would break, and the seam to use instead — belongs in that
|
|
20
|
+
# method's own rdoc, which is where the raise sends the reader.
|
|
21
|
+
#
|
|
22
|
+
# == Implementation details
|
|
23
|
+
#
|
|
24
|
+
# The check resolves each final method's `owner` instead of hooking
|
|
25
|
+
# `method_added`, which fires earlier but sees only `def` and
|
|
26
|
+
# `define_method` — an override arriving through an `include` or a `prepend`
|
|
27
|
+
# slips past it. Nothing fires late enough to place the check automatically,
|
|
28
|
+
# which is why the extending class owes the explicit {#verify_final!} call.
|
|
29
|
+
module Final
|
|
30
|
+
# Marks each of `names` non-overridable by subclasses. Declare them in one
|
|
31
|
+
# call near the top of the class; the names may be forward references,
|
|
32
|
+
# since nothing is resolved until {#verify_final!} runs.
|
|
33
|
+
#
|
|
34
|
+
# final :parent, :children, :add_child
|
|
35
|
+
#
|
|
36
|
+
# It *reads* better as a keyword on the definition (`final def foo`, which
|
|
37
|
+
# parses — `def` hands back its name), but don't: YARD has no handler for
|
|
38
|
+
# the macro, so a decorated `def` loses its parameter list and sord then
|
|
39
|
+
# generates `def foo: () -> void` into `sig/`, silently.
|
|
40
|
+
#
|
|
41
|
+
# @param names [Array<Symbol, Array<Symbol>>] method names.
|
|
42
|
+
# @return [Symbol, Array<Symbol>] the names — a bare Symbol when exactly
|
|
43
|
+
# one was given.
|
|
44
|
+
def final(*names)
|
|
45
|
+
names = names.flatten
|
|
46
|
+
(@final_methods ||= []).concat(names)
|
|
47
|
+
names.one? ? names.first : names
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @return [Array<Symbol>] the methods marked {#final} on this class.
|
|
51
|
+
def final_methods = @final_methods || []
|
|
52
|
+
|
|
53
|
+
# Raises unless `klass` inherits every {#final_methods} entry from this
|
|
54
|
+
# class. Memoized per class, so a construction-time call costs one hash
|
|
55
|
+
# lookup after the first instance.
|
|
56
|
+
#
|
|
57
|
+
# @param klass [Class] the class being instantiated.
|
|
58
|
+
# @raise [Error] if `klass` redefines a final method, by any route.
|
|
59
|
+
# @return [void]
|
|
60
|
+
def verify_final!(klass)
|
|
61
|
+
@final_verified ||= {}
|
|
62
|
+
return if @final_verified.key?(klass)
|
|
63
|
+
|
|
64
|
+
overridden = final_methods.reject { klass.instance_method(_1).owner == self }
|
|
65
|
+
unless overridden.empty?
|
|
66
|
+
raise Error, "#{klass} overrides #{overridden.join(", ")}, which #{overridden.one? ? "is" : "are"} final " \
|
|
67
|
+
"on #{self} and may not be redefined. See the rdoc of " \
|
|
68
|
+
"#{overridden.map { "#{self}##{_1}" }.join(", ")} for the invariant at stake and the seam " \
|
|
69
|
+
"to use instead."
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
@final_verified[klass] = true
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
data/lib/tuile/keys.rb
CHANGED
|
@@ -184,6 +184,13 @@ module Tuile
|
|
|
184
184
|
# sequences never start with `\e[?`, so this can't eat a regular key.
|
|
185
185
|
char += $stdin.read(1) while char.start_with?("\e[?") && !char.match?(/[\x40-\x7e]\z/)
|
|
186
186
|
|
|
187
|
+
# OSC replies (the ~22-byte background report, {TerminalBackground})
|
|
188
|
+
# outgrow the gulp too, and end in BEL or ST rather than at a fixed
|
|
189
|
+
# length. Byte-at-a-time is required, not merely tidy: ST *is* `\e\\`,
|
|
190
|
+
# so a gulping read would swallow it plus the keys typed behind it.
|
|
191
|
+
# Keyboard sequences never start with `\e]`, so this eats no real key.
|
|
192
|
+
char += $stdin.read(1) while char.start_with?("\e]") && !char.end_with?("\a", "\e\\")
|
|
193
|
+
|
|
187
194
|
char
|
|
188
195
|
end
|
|
189
196
|
|