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
@@ -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) || KeyEvent.new(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
@@ -27,7 +27,9 @@ module Tuile
27
27
  super
28
28
  @event_queue = FakeEventQueue.new
29
29
  @size = Size.new(160, 50)
30
- @buffer.resize(@size) # super sized it to the test runner's TTY
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 detect_scheme = :dark
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
@@ -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