chamomile-petals 0.2.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b0e639488f319cee4214dadaf0a18d780c364f1e2dd0eb105ad8a02396c10476
4
- data.tar.gz: 312869e2b9d7f6e1cbeb10a88144adbd576ff53979e4aa6c3668846614e5d9d2
3
+ metadata.gz: dcf679005bf4865920fbb09cc137ba946ca9c3633737bad6b3073cfcbcfc44bd
4
+ data.tar.gz: 11b2fe6705a11e6b0f0c7963b0710386be9de7ffbd8440a13d7a3bee80c0fdae
5
5
  SHA512:
6
- metadata.gz: 83bd46a6443068228e48ac8084480044cb6656376120f9ed096ad803887d8c50bfe103143c988bb628576a2d58d20a93aaab5f448f1f2c7a6d19b98aa020150e
7
- data.tar.gz: d8f90b12a4a67f5a07e04870b83f004fff7d385de2face4bd4fb7008e0be3e906c2f9ce8fb1bedc469ea3be0003585964e39e53f912696148743b41eb4b7ff5e
6
+ metadata.gz: 455583595d0ddd88c90cd76aa77c632a29a8f37d5118d0822cf0ad7f5695b82bd94bc1d32958cb1fbaaa0a4845f8f9fabb23d1693bc27cc4479e750b462179e8
7
+ data.tar.gz: 9d57cc03e4d7fa75dec8f5b3c372d07eba43bb12831b120e2173dac90d7fd5d8f370aec024089a824fac50fbc72de957849404dce6fdcbfdd59ee5c7d6975bfb
data/lib/petals.rb CHANGED
@@ -1,29 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "chamomile"
4
- require_relative "petals/version"
5
- require_relative "petals/key_binding"
6
- require_relative "petals/spinner/types"
7
- require_relative "petals/spinner"
8
- require_relative "petals/text_input/key_map"
9
- require_relative "petals/text_input"
10
- require_relative "petals/stopwatch"
11
- require_relative "petals/timer"
12
- require_relative "petals/paginator/key_map"
13
- require_relative "petals/paginator"
14
- require_relative "petals/cursor"
15
- require_relative "petals/help"
16
- require_relative "petals/progress"
17
- require_relative "petals/viewport/key_map"
18
- require_relative "petals/viewport"
19
- require_relative "petals/file_picker/key_map"
20
- require_relative "petals/file_picker"
21
- require_relative "petals/table/key_map"
22
- require_relative "petals/table"
23
- require_relative "petals/text_area/key_map"
24
- require_relative "petals/text_area"
25
- require_relative "petals/list/key_map"
26
- require_relative "petals/list"
27
- require_relative "petals/render_cache"
28
- require_relative "petals/log_view"
29
- require_relative "petals/command_palette"
4
+
5
+ warn "[DEPRECATION] The `chamomile-petals` gem is deprecated. " \
6
+ "All components are now part of `chamomile` (v1.0+). " \
7
+ "Replace `require \"petals\"` with `require \"chamomile\"` " \
8
+ "and change `Petals::` to `Chamomile::`."
9
+
10
+ Petals = Chamomile unless defined?(Petals)
metadata CHANGED
@@ -1,99 +1,50 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: chamomile-petals
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jack Killilea
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-03-12 00:00:00.000000000 Z
11
+ date: 2026-03-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: chamomile
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - "~>"
18
- - !ruby/object:Gem::Version
19
- version: '0.2'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - "~>"
25
- - !ruby/object:Gem::Version
26
- version: '0.2'
27
- - !ruby/object:Gem::Dependency
28
- name: rspec
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - "~>"
32
- - !ruby/object:Gem::Version
33
- version: '3.12'
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - "~>"
39
- - !ruby/object:Gem::Version
40
- version: '3.12'
41
- - !ruby/object:Gem::Dependency
42
- name: rubocop
43
15
  requirement: !ruby/object:Gem::Requirement
44
16
  requirements:
45
17
  - - "~>"
46
18
  - !ruby/object:Gem::Version
47
19
  version: '1.0'
48
- type: :development
20
+ type: :runtime
49
21
  prerelease: false
50
22
  version_requirements: !ruby/object:Gem::Requirement
51
23
  requirements:
52
24
  - - "~>"
53
25
  - !ruby/object:Gem::Version
54
26
  version: '1.0'
55
- description: Spinner, TextInput, and more composable widgets for Chamomile TUI apps
27
+ description: This gem is deprecated. All components are now part of the chamomile
28
+ gem (v1.0+). This shim pulls in chamomile and aliases Petals to Chamomile for backward
29
+ compatibility.
56
30
  email:
57
31
  executables: []
58
32
  extensions: []
59
33
  extra_rdoc_files: []
60
34
  files:
61
35
  - lib/petals.rb
62
- - lib/petals/command_palette.rb
63
- - lib/petals/cursor.rb
64
- - lib/petals/file_picker.rb
65
- - lib/petals/file_picker/key_map.rb
66
- - lib/petals/help.rb
67
- - lib/petals/key_binding.rb
68
- - lib/petals/list.rb
69
- - lib/petals/list/key_map.rb
70
- - lib/petals/log_view.rb
71
- - lib/petals/paginator.rb
72
- - lib/petals/paginator/key_map.rb
73
- - lib/petals/progress.rb
74
- - lib/petals/render_cache.rb
75
- - lib/petals/spinner.rb
76
- - lib/petals/spinner/types.rb
77
- - lib/petals/stopwatch.rb
78
- - lib/petals/table.rb
79
- - lib/petals/table/key_map.rb
80
- - lib/petals/text_area.rb
81
- - lib/petals/text_area/key_map.rb
82
- - lib/petals/text_input.rb
83
- - lib/petals/text_input/key_map.rb
84
- - lib/petals/timer.rb
85
- - lib/petals/version.rb
86
- - lib/petals/viewport.rb
87
- - lib/petals/viewport/key_map.rb
88
- homepage: https://github.com/chamomile-rb/petals
36
+ homepage: https://github.com/chamomile-rb/chamomile
89
37
  licenses:
90
38
  - MIT
91
39
  metadata:
92
40
  rubygems_mfa_required: 'true'
93
- homepage_uri: https://github.com/chamomile-rb/petals
94
- source_code_uri: https://github.com/chamomile-rb/petals
95
- changelog_uri: https://github.com/chamomile-rb/petals/blob/master/CHANGELOG.md
96
- post_install_message:
41
+ homepage_uri: https://github.com/chamomile-rb/chamomile
42
+ source_code_uri: https://github.com/chamomile-rb/chamomile
43
+ changelog_uri: https://github.com/chamomile-rb/chamomile/blob/master/CHANGELOG.md
44
+ post_install_message: |
45
+ [DEPRECATION] chamomile-petals is deprecated.
46
+ All components are now part of the `chamomile` gem (v1.0+).
47
+ Replace `gem "petals"` with `gem "chamomile"` in your Gemfile.
97
48
  rdoc_options: []
98
49
  require_paths:
99
50
  - lib
@@ -111,5 +62,5 @@ requirements: []
111
62
  rubygems_version: 3.5.11
112
63
  signing_key:
113
64
  specification_version: 4
114
- summary: Reusable TUI components for the Chamomile framework
65
+ summary: "[DEPRECATED] Use chamomile instead"
115
66
  test_files: []
@@ -1,114 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Petals
4
- # A fuzzy-search command palette overlay.
5
- # Renders as a centered modal over existing content.
6
- #
7
- # Usage:
8
- # @palette = Petals::CommandPalette.new(
9
- # items: [
10
- # { label: "Run migrations", action: :run_migrate, key: "db:migrate" },
11
- # { label: "Start server", action: :server_start, key: "server" },
12
- # ]
13
- # )
14
- # # In update:
15
- # when KeyMsg
16
- # if @palette.visible?
17
- # result = @palette.handle_key(msg)
18
- # return result[:action] if result
19
- # end
20
- class CommandPalette
21
- Item = Data.define(:label, :action, :key)
22
-
23
- attr_reader :query, :cursor
24
-
25
- def initialize(items:, placeholder: "Type to filter...")
26
- @all_items = items.map { |i| Item.new(**i) }
27
- @placeholder = placeholder
28
- @query = ""
29
- @cursor = 0
30
- @visible = false
31
- end
32
-
33
- def show
34
- @visible = true
35
- @query = ""
36
- @cursor = 0
37
- end
38
-
39
- def hide
40
- @visible = false
41
- end
42
-
43
- def visible? = @visible
44
-
45
- def handle_key(msg)
46
- return nil unless @visible
47
-
48
- case msg.key
49
- when :escape
50
- hide
51
- nil
52
- when :enter
53
- selected = filtered_items[@cursor]
54
- hide
55
- selected
56
- when :up, "k"
57
- @cursor = [@cursor - 1, 0].max
58
- nil
59
- when :down, "j"
60
- max = filtered_items.size - 1
61
- @cursor = (@cursor + 1).clamp(0, [max, 0].max)
62
- nil
63
- when :backspace
64
- @query = @query[0..-2] || ""
65
- @cursor = 0
66
- nil
67
- else
68
- if msg.key.is_a?(String) && msg.key.length == 1
69
- @query += msg.key
70
- @cursor = 0
71
- end
72
- nil
73
- end
74
- end
75
-
76
- def render(width:, height:)
77
- return "" unless @visible
78
-
79
- items = filtered_items
80
- modal_width = [width - 8, 60].min
81
- modal_height = [items.size + 4, height - 4, 16].min
82
-
83
- lines = []
84
- query_display = @query.empty? ? "\e[38;5;240m#{@placeholder}\e[0m" : @query
85
- lines << " > #{query_display}"
86
- lines << ("\u2500" * (modal_width - 2))
87
-
88
- visible_items = items.first(modal_height - 4)
89
- visible_items.each_with_index do |item, i|
90
- prefix = i == @cursor ? "\u25b6 " : " "
91
- line = "#{prefix}#{item.label}"
92
- lines << (i == @cursor ? "\e[7m#{line}\e[0m" : line)
93
- end
94
-
95
- lines << ("\u2500" * (modal_width - 2))
96
- lines << "\e[38;5;240m \u2191\u2193 navigate Enter select Esc cancel\e[0m"
97
-
98
- box_lines = lines.map { |l| "\u2502 #{l.ljust(modal_width - 4)} \u2502" }
99
- top = "┌#{"\u2500" * (modal_width - 2)}┐"
100
- bottom = "└#{"\u2500" * (modal_width - 2)}┘"
101
-
102
- ([top] + box_lines + [bottom]).join("\n")
103
- end
104
-
105
- def filtered_items
106
- return @all_items if @query.empty?
107
-
108
- q = @query.downcase
109
- @all_items.select do |item|
110
- item.label.downcase.include?(q) || item.key.to_s.downcase.include?(q)
111
- end
112
- end
113
- end
114
- end
data/lib/petals/cursor.rb DELETED
@@ -1,96 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Petals
4
- CursorBlinkMsg = Data.define(:id, :tag)
5
-
6
- # Virtual text cursor with blink modes (blink, static, hide).
7
- class Cursor
8
- MODE_BLINK = :blink
9
- MODE_STATIC = :static
10
- MODE_HIDE = :hide
11
-
12
- BLINK_SPEED = 0.53
13
-
14
- @next_id = 0
15
- @id_mutex = Mutex.new
16
- @id_pid = Process.pid
17
-
18
- def self.next_id
19
- @id_mutex.synchronize do
20
- if Process.pid != @id_pid
21
- @id_pid = Process.pid
22
- @next_id = 0
23
- @id_mutex = Mutex.new
24
- end
25
- @next_id += 1
26
- "#{@id_pid}-cur-#{@next_id}"
27
- end
28
- end
29
-
30
- attr_reader :id, :mode, :blink_speed
31
- attr_accessor :char, :blinked
32
-
33
- def initialize(mode: MODE_BLINK, blink_speed: BLINK_SPEED)
34
- @id = self.class.next_id
35
- @mode = mode
36
- @blink_speed = blink_speed
37
- @char = " "
38
- @blinked = false
39
- @focused = false
40
- @tag = 0
41
- end
42
-
43
- def focus
44
- @focused = true
45
- @blinked = false
46
- @mode == MODE_BLINK ? blink_cmd : nil
47
- end
48
-
49
- def blur
50
- @focused = false
51
- @blinked = true
52
- @tag += 1
53
- nil
54
- end
55
-
56
- def focused?
57
- @focused
58
- end
59
-
60
- def mode=(new_mode)
61
- @mode = new_mode
62
- @tag += 1
63
- @blinked = new_mode == MODE_HIDE || !@focused
64
- @mode == MODE_BLINK && @focused ? blink_cmd : nil
65
- end
66
-
67
- def blink_cmd
68
- captured_id = @id
69
- captured_tag = @tag
70
- speed = @blink_speed
71
- -> {
72
- sleep(speed)
73
- CursorBlinkMsg.new(id: captured_id, tag: captured_tag)
74
- }
75
- end
76
-
77
- def handle(msg)
78
- return unless msg.is_a?(CursorBlinkMsg)
79
- return unless @mode == MODE_BLINK && @focused
80
- return unless msg.id == @id && msg.tag == @tag
81
-
82
- @blinked = !@blinked
83
- @tag += 1
84
- blink_cmd
85
- end
86
-
87
- alias update handle
88
-
89
- def view
90
- return @char if @mode == MODE_HIDE
91
- return @char if @blinked
92
-
93
- "\e[7m#{@char}\e[0m"
94
- end
95
- end
96
- end
@@ -1,17 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Petals
4
- class FilePicker
5
- DEFAULT_KEY_MAP = KeyBinding.normalize({
6
- up: [[:up, []], ["k", []]],
7
- down: [[:down, []], ["j", []]],
8
- open: [[:right, []], ["l", []], [:enter, []]],
9
- back: [[:left, []], ["h", []], [:backspace, []]],
10
- toggle_hidden: [[".", []]],
11
- page_up: [[:page_up, []], ["b", [:ctrl]]],
12
- page_down: [[:page_down, []], ["f", [:ctrl]]],
13
- goto_top: [["g", []], [:home, []]],
14
- goto_bottom: [["G", [:shift]], [:end_key, []]],
15
- })
16
- end
17
- end
@@ -1,278 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Petals
4
- FilePickerReadDirMsg = Data.define(:id, :entries)
5
-
6
- # Filesystem browser with async directory reading, navigation, and filtering.
7
- class FilePicker
8
- @next_id = 0
9
- @id_mutex = Mutex.new
10
- @id_pid = Process.pid
11
-
12
- def self.next_id
13
- @id_mutex.synchronize do
14
- if Process.pid != @id_pid
15
- @id_pid = Process.pid
16
- @next_id = 0
17
- @id_mutex = Mutex.new
18
- end
19
- @next_id += 1
20
- "#{@id_pid}-fp-#{@next_id}"
21
- end
22
- end
23
-
24
- attr_reader :id, :current_directory, :selected_path
25
- attr_accessor :key_map, :allowed_types, :show_permissions, :show_size,
26
- :show_hidden, :dir_allowed, :file_allowed, :height, :cursor_char
27
-
28
- def initialize(directory: Dir.pwd, key_map: DEFAULT_KEY_MAP, height: 10,
29
- allowed_types: [], show_permissions: false, show_size: false,
30
- show_hidden: false, dir_allowed: false, file_allowed: true,
31
- cursor_char: ">")
32
- @id = self.class.next_id
33
- @current_directory = File.expand_path(directory)
34
- @key_map = key_map
35
- @height = height
36
- @allowed_types = allowed_types
37
- @show_permissions = show_permissions
38
- @show_size = show_size
39
- @show_hidden = show_hidden
40
- @dir_allowed = dir_allowed
41
- @file_allowed = file_allowed
42
- @cursor_char = cursor_char
43
- @cursor = 0
44
- @entries = []
45
- @offset = 0
46
- @selected_path = nil
47
- @disabled_selected_path = nil
48
- @dir_stack = []
49
- end
50
-
51
- def init_cmd
52
- read_dir_cmd(@current_directory)
53
- end
54
-
55
- def handle(msg)
56
- case msg
57
- when FilePickerReadDirMsg
58
- return unless msg.id == @id
59
-
60
- @entries = msg.entries
61
- @cursor = @cursor.clamp(0, [entries_size - 1, 0].max)
62
- clamp_offset
63
- nil
64
- when Chamomile::KeyEvent
65
- handle_key(msg)
66
- end
67
- end
68
-
69
- alias update handle
70
-
71
- def view
72
- return " (empty)" if @entries.empty?
73
-
74
- visible = visible_entries
75
- lines = visible.each_with_index.map do |entry, i|
76
- idx = @offset + i
77
- prefix = idx == @cursor ? "#{@cursor_char} " : " "
78
- line = prefix + format_entry(entry)
79
- if idx == @cursor
80
- "\e[7m#{line}\e[0m"
81
- else
82
- line
83
- end
84
- end
85
- lines.join("\n")
86
- end
87
-
88
- def highlighted_path
89
- return nil if @entries.empty?
90
-
91
- entry = @entries[@cursor]
92
- return nil unless entry
93
-
94
- File.join(@current_directory, entry[:name])
95
- end
96
-
97
- def did_select_file?(msg)
98
- return [false, nil] unless msg.is_a?(Chamomile::KeyEvent)
99
-
100
- if @selected_path
101
- path = @selected_path
102
- @selected_path = nil
103
- [true, path]
104
- else
105
- [false, nil]
106
- end
107
- end
108
-
109
- def did_select_disabled_file?(msg)
110
- return [false, nil] unless msg.is_a?(Chamomile::KeyEvent)
111
-
112
- if @disabled_selected_path
113
- path = @disabled_selected_path
114
- @disabled_selected_path = nil
115
- [true, path]
116
- else
117
- [false, nil]
118
- end
119
- end
120
-
121
- private
122
-
123
- def handle_key(msg)
124
- kb = KeyBinding
125
-
126
- if kb.key_matches?(msg, @key_map, :up)
127
- move_cursor(-1)
128
- elsif kb.key_matches?(msg, @key_map, :down)
129
- move_cursor(1)
130
- elsif kb.key_matches?(msg, @key_map, :page_up)
131
- move_cursor(-@height)
132
- elsif kb.key_matches?(msg, @key_map, :page_down)
133
- move_cursor(@height)
134
- elsif kb.key_matches?(msg, @key_map, :goto_top)
135
- move_cursor(-entries_size)
136
- elsif kb.key_matches?(msg, @key_map, :goto_bottom)
137
- move_cursor(entries_size)
138
- elsif kb.key_matches?(msg, @key_map, :open)
139
- open_entry
140
- elsif kb.key_matches?(msg, @key_map, :back)
141
- go_back
142
- elsif kb.key_matches?(msg, @key_map, :toggle_hidden)
143
- @show_hidden = !@show_hidden
144
- read_dir_cmd(@current_directory)
145
- end
146
- end
147
-
148
- def move_cursor(delta)
149
- return if @entries.empty?
150
-
151
- @cursor = (@cursor + delta).clamp(0, entries_size - 1)
152
- clamp_offset
153
- nil
154
- end
155
-
156
- def open_entry
157
- return nil if @entries.empty?
158
-
159
- entry = @entries[@cursor]
160
- return nil unless entry
161
-
162
- path = File.join(@current_directory, entry[:name])
163
-
164
- if selectable?(entry)
165
- @selected_path = path
166
- elsif !entry[:directory] && @allowed_types.any?
167
- @disabled_selected_path = path
168
- end
169
-
170
- if entry[:directory]
171
- @dir_stack.push({ directory: @current_directory, cursor: @cursor, offset: @offset })
172
- @current_directory = path
173
- @cursor = 0
174
- @offset = 0
175
- return read_dir_cmd(path)
176
- end
177
-
178
- nil
179
- end
180
-
181
- def go_back
182
- parent = File.dirname(@current_directory)
183
- return nil if parent == @current_directory
184
-
185
- if @dir_stack.any?
186
- prev = @dir_stack.pop
187
- @current_directory = prev[:directory]
188
- @cursor = prev[:cursor]
189
- @offset = prev[:offset]
190
- else
191
- @current_directory = parent
192
- @cursor = 0
193
- @offset = 0
194
- end
195
-
196
- read_dir_cmd(@current_directory)
197
- end
198
-
199
- def read_dir_cmd(dir)
200
- captured_id = @id
201
- show_hidden = @show_hidden
202
- -> {
203
- entries = begin
204
- Dir.entries(dir)
205
- .reject { |e| [".", ".."].include?(e) }
206
- .reject { |e| !show_hidden && e.start_with?(".") }
207
- .map { |e| build_entry(dir, e) }
208
- .sort_by { |e| [e[:directory] ? 0 : 1, e[:name].downcase] }
209
- rescue SystemCallError
210
- []
211
- end
212
- FilePickerReadDirMsg.new(id: captured_id, entries: entries)
213
- }
214
- end
215
-
216
- def build_entry(dir, name)
217
- path = File.join(dir, name)
218
- stat = begin
219
- File.stat(path)
220
- rescue SystemCallError
221
- nil
222
- end
223
-
224
- {
225
- name: name,
226
- directory: stat&.directory? || false,
227
- size: stat&.size || 0,
228
- permissions: stat ? format("%o", stat.mode & 0o7777) : "0000",
229
- }
230
- end
231
-
232
- def selectable?(entry)
233
- if entry[:directory]
234
- @dir_allowed
235
- else
236
- return false unless @file_allowed
237
- return true if @allowed_types.empty?
238
-
239
- ext = File.extname(entry[:name]).downcase
240
- @allowed_types.any? { |t| t.downcase == ext || ".#{t.downcase}" == ext }
241
- end
242
- end
243
-
244
- def format_entry(entry)
245
- parts = []
246
- parts << (entry[:directory] ? "#{entry[:name]}/" : entry[:name])
247
- parts << human_size(entry[:size]) if @show_size && !entry[:directory]
248
- parts << entry[:permissions] if @show_permissions
249
- parts.join(" ")
250
- end
251
-
252
- def human_size(bytes)
253
- return "0B" if bytes.zero?
254
-
255
- units = %w[B KB MB GB TB]
256
- exp = (Math.log(bytes) / Math.log(1024)).floor
257
- exp = [exp, units.length - 1].min
258
- size = bytes.to_f / (1024**exp)
259
- exp.zero? ? "#{bytes}B" : format("%.1f%s", size, units[exp])
260
- end
261
-
262
- def entries_size
263
- @entries.length
264
- end
265
-
266
- def visible_entries
267
- @entries[@offset, @height] || []
268
- end
269
-
270
- def clamp_offset
271
- return if @entries.empty?
272
-
273
- @offset = @cursor if @cursor < @offset
274
- @offset = @cursor - @height + 1 if @cursor >= @offset + @height
275
- @offset = [0, @offset].max
276
- end
277
- end
278
- end