bubbles 0.0.5 → 0.1.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 (58) hide show
  1. checksums.yaml +5 -5
  2. data/LICENSE.txt +21 -0
  3. data/README.md +524 -80
  4. data/bubbles.gemspec +29 -21
  5. data/lib/bubbles/cursor.rb +169 -0
  6. data/lib/bubbles/file_picker.rb +397 -0
  7. data/lib/bubbles/help.rb +170 -0
  8. data/lib/bubbles/key.rb +96 -0
  9. data/lib/bubbles/list.rb +365 -0
  10. data/lib/bubbles/paginator.rb +158 -0
  11. data/lib/bubbles/progress.rb +276 -0
  12. data/lib/bubbles/spinner/spinners.rb +77 -0
  13. data/lib/bubbles/spinner.rb +122 -0
  14. data/lib/bubbles/stopwatch.rb +189 -0
  15. data/lib/bubbles/table.rb +248 -0
  16. data/lib/bubbles/text_area.rb +503 -0
  17. data/lib/bubbles/text_input.rb +543 -0
  18. data/lib/bubbles/timer.rb +196 -0
  19. data/lib/bubbles/version.rb +4 -1
  20. data/lib/bubbles/viewport.rb +296 -0
  21. data/lib/bubbles.rb +18 -35
  22. data/sig/bubbles/cursor.rbs +87 -0
  23. data/sig/bubbles/file_picker.rbs +138 -0
  24. data/sig/bubbles/help.rbs +88 -0
  25. data/sig/bubbles/key.rbs +63 -0
  26. data/sig/bubbles/list.rbs +138 -0
  27. data/sig/bubbles/paginator.rbs +90 -0
  28. data/sig/bubbles/progress.rbs +123 -0
  29. data/sig/bubbles/spinner/spinners.rbs +32 -0
  30. data/sig/bubbles/spinner.rbs +74 -0
  31. data/sig/bubbles/stopwatch.rbs +97 -0
  32. data/sig/bubbles/table.rbs +119 -0
  33. data/sig/bubbles/text_area.rbs +161 -0
  34. data/sig/bubbles/text_input.rbs +183 -0
  35. data/sig/bubbles/timer.rbs +107 -0
  36. data/sig/bubbles/version.rbs +5 -0
  37. data/sig/bubbles/viewport.rbs +125 -0
  38. data/sig/bubbles.rbs +4 -0
  39. metadata +66 -67
  40. data/.gitignore +0 -14
  41. data/.rspec +0 -2
  42. data/.travis.yml +0 -10
  43. data/Gemfile +0 -4
  44. data/LICENSE +0 -20
  45. data/Rakefile +0 -6
  46. data/bin/console +0 -14
  47. data/bin/setup +0 -8
  48. data/exe/bubbles +0 -5
  49. data/lib/bubbles/bubblicious_file.rb +0 -42
  50. data/lib/bubbles/command_queue.rb +0 -43
  51. data/lib/bubbles/common_uploader_interface.rb +0 -13
  52. data/lib/bubbles/config.rb +0 -149
  53. data/lib/bubbles/dir_watcher.rb +0 -53
  54. data/lib/bubbles/uploaders/local_dir.rb +0 -39
  55. data/lib/bubbles/uploaders/s3.rb +0 -36
  56. data/lib/bubbles/uploaders/s3_ensure_connection.rb +0 -26
  57. data/tmp/dummy_local_dir_uploader_dir/.gitkeep +0 -0
  58. data/tmp/dummy_processing_dir/.gitkeep +0 -0
@@ -0,0 +1,196 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ module Bubbles
5
+ # Timer is a countdown timer component.
6
+ #
7
+ # Example:
8
+ # timer = Bubbles::Timer.new(60.0) # 60 second countdown
9
+ #
10
+ # # In your model's init:
11
+ # def init
12
+ # [self, @timer.init]
13
+ # end
14
+ #
15
+ # # In your model's update:
16
+ # def update(message)
17
+ # case message
18
+ # when Bubbles::Timer::TickMessage, Bubbles::Timer::StartStopMessage
19
+ # @timer, command = @timer.update(message)
20
+ # [self, command]
21
+ # when Bubbles::Timer::TimeoutMessage
22
+ # # Timer finished!
23
+ # [self, nil]
24
+ # end
25
+ # end
26
+ #
27
+ class Timer
28
+ class TickMessage < Bubbletea::Message
29
+ attr_reader :id #: Integer
30
+ attr_reader :tag #: Integer
31
+ attr_reader :timeout #: bool
32
+
33
+ #: (id: Integer, tag: Integer, ?timeout: bool) -> void
34
+ def initialize(id:, tag:, timeout: false)
35
+ super()
36
+
37
+ @id = id
38
+ @tag = tag
39
+ @timeout = timeout
40
+ end
41
+ end
42
+
43
+ class TimeoutMessage < Bubbletea::Message
44
+ attr_reader :id #: Integer
45
+
46
+ #: (id: Integer) -> void
47
+ def initialize(id:)
48
+ super()
49
+
50
+ @id = id
51
+ end
52
+ end
53
+
54
+ class StartStopMessage < Bubbletea::Message
55
+ attr_reader :id #: Integer
56
+ attr_reader :running #: bool
57
+
58
+ #: (id: Integer, running: bool) -> void
59
+ def initialize(id:, running:)
60
+ super()
61
+
62
+ @id = id
63
+ @running = running
64
+ end
65
+ end
66
+
67
+ # @rbs self.@next_id: Integer
68
+ # @rbs self.@id_mutex: Mutex
69
+ @next_id = 0
70
+ @id_mutex = Mutex.new
71
+
72
+ class << self
73
+ #: () -> Integer
74
+ def next_id
75
+ @id_mutex.synchronize do
76
+ @next_id += 1
77
+ end
78
+ end
79
+ end
80
+
81
+ attr_reader :timeout #: Float
82
+ attr_reader :interval #: Float
83
+ attr_reader :id #: Integer
84
+
85
+ #: (Float, ?interval: Float) -> void
86
+ def initialize(timeout, interval: 1.0)
87
+ @timeout = timeout.to_f
88
+ @interval = interval.to_f
89
+ @id = self.class.next_id
90
+ @tag = 0
91
+ @running = false
92
+ end
93
+
94
+ #: () -> Bubbletea::Command?
95
+ def init
96
+ @running = true
97
+ tick
98
+ end
99
+
100
+ #: () -> bool
101
+ def running?
102
+ @running && @timeout.positive?
103
+ end
104
+
105
+ #: () -> bool
106
+ def timed_out?
107
+ @timeout <= 0
108
+ end
109
+
110
+ #: (Bubbletea::Message) -> [Timer, Bubbletea::Command?]
111
+ def update(message)
112
+ case message
113
+ when StartStopMessage
114
+ return [self, nil] if message.id.positive? && message.id != @id
115
+
116
+ @running = message.running
117
+ [self, @running ? tick : nil]
118
+
119
+ when TickMessage
120
+ return [self, nil] unless @running
121
+ return [self, nil] if message.id.positive? && message.id != @id
122
+ return [self, nil] if message.tag.positive? && message.tag != @tag
123
+
124
+ @timeout -= @interval
125
+ @tag += 1
126
+
127
+ [self, Bubbletea.batch(tick, timeout_command)]
128
+ else
129
+ [self, nil]
130
+ end
131
+ end
132
+
133
+ #: () -> String
134
+ def view
135
+ format_duration(@timeout)
136
+ end
137
+
138
+ #: () -> Bubbletea::Command
139
+ def start
140
+ start_stop(true)
141
+ end
142
+
143
+ #: () -> Bubbletea::Command
144
+ def stop
145
+ start_stop(false)
146
+ end
147
+
148
+ #: () -> Bubbletea::Command
149
+ def toggle
150
+ start_stop(!@running)
151
+ end
152
+
153
+ private
154
+
155
+ #: (bool) -> Bubbletea::Command
156
+ def start_stop(running)
157
+ current_id = @id
158
+ Bubbletea.send_message(StartStopMessage.new(id: current_id, running: running))
159
+ end
160
+
161
+ #: () -> Bubbletea::Command?
162
+ def tick
163
+ return nil unless running?
164
+
165
+ current_id = @id
166
+ current_tag = @tag
167
+
168
+ Bubbletea.tick(@interval) { TickMessage.new(id: current_id, tag: current_tag) }
169
+ end
170
+
171
+ #: () -> Bubbletea::Command?
172
+ def timeout_command
173
+ return nil unless timed_out?
174
+
175
+ current_id = @id
176
+ Bubbletea.send_message(TimeoutMessage.new(id: current_id))
177
+ end
178
+
179
+ #: (Float) -> String
180
+ def format_duration(seconds)
181
+ return "0s" if seconds <= 0
182
+
183
+ total_seconds = seconds.to_i
184
+ hours = total_seconds / 3600
185
+ minutes = (total_seconds % 3600) / 60
186
+ secs = total_seconds % 60
187
+
188
+ parts = [] #: Array[String]
189
+ parts << "#{hours}h" if hours.positive?
190
+ parts << "#{minutes}m" if minutes.positive? || hours.positive?
191
+ parts << "#{secs}s"
192
+
193
+ parts.join
194
+ end
195
+ end
196
+ end
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
1
4
  module Bubbles
2
- VERSION = "0.0.5"
5
+ VERSION = "0.1.0" #: String
3
6
  end
@@ -0,0 +1,296 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ module Bubbles
5
+ # Viewport is a scrollable content pane.
6
+ #
7
+ # Example:
8
+ # viewport = Bubbles::Viewport.new(width: 80, height: 24)
9
+ # viewport.content = "Long text content..."
10
+ #
11
+ # # In update:
12
+ # viewport, command = viewport.update(message)
13
+ #
14
+ # # In view:
15
+ # viewport.view
16
+ #
17
+ class Viewport
18
+ attr_accessor :width #: Integer
19
+ attr_accessor :height #: Integer
20
+ attr_accessor :style #: Lipgloss::Style?
21
+ attr_accessor :mouse_wheel_enabled #: bool
22
+ attr_accessor :mouse_wheel_delta #: Integer
23
+
24
+ attr_reader :y_offset #: Integer
25
+ attr_reader :x_offset #: Integer
26
+
27
+ #: (?width: Integer, ?height: Integer) -> void
28
+ def initialize(width: 80, height: 24)
29
+ @width = width
30
+ @height = height
31
+ @style = nil
32
+
33
+ @mouse_wheel_enabled = true
34
+ @mouse_wheel_delta = 3
35
+ @horizontal_step = 0
36
+
37
+ @y_offset = 0
38
+ @x_offset = 0
39
+
40
+ @lines = [] #: Array[String]
41
+ @longest_line_width = 0
42
+ end
43
+
44
+ #: () -> nil
45
+ def init
46
+ nil
47
+ end
48
+
49
+ #: () -> bool
50
+ def at_top?
51
+ @y_offset <= 0
52
+ end
53
+
54
+ #: () -> bool
55
+ def at_bottom?
56
+ @y_offset >= max_y_offset
57
+ end
58
+
59
+ #: () -> bool
60
+ def past_bottom?
61
+ @y_offset > max_y_offset
62
+ end
63
+
64
+ #: () -> Float -- scroll percentage (0.0 to 1.0)
65
+ def scroll_percent
66
+ return 1.0 if @height >= @lines.length
67
+
68
+ y = @y_offset.to_f
69
+ h = @height.to_f
70
+ t = @lines.length.to_f
71
+
72
+ v = y / (t - h)
73
+
74
+ v.clamp(0.0, 1.0).to_f
75
+ end
76
+
77
+ #: (String) -> void
78
+ def content=(content)
79
+ content = content.gsub("\r\n", "\n")
80
+ @lines = content.split("\n", -1)
81
+ @longest_line_width = @lines.map { |l| strip_ansi(l).length }.max || 0
82
+
83
+ goto_bottom if @y_offset > @lines.length - 1
84
+ end
85
+
86
+ #: () -> String
87
+ def content
88
+ @lines.join("\n")
89
+ end
90
+
91
+ #: (Integer) -> Integer
92
+ def y_offset=(offset)
93
+ @y_offset = offset.clamp(0, max_y_offset)
94
+ end
95
+
96
+ #: (Integer) -> Integer
97
+ def x_offset=(offset)
98
+ max_x = [@longest_line_width - @width, 0].max
99
+ @x_offset = offset.clamp(0, max_x)
100
+ end
101
+
102
+ #: (Integer) -> Integer
103
+ def horizontal_step=(step)
104
+ @horizontal_step = [step, 0].max
105
+ end
106
+
107
+ #: (Integer) -> Array[String]
108
+ def scroll_down(line_count)
109
+ return [] if at_bottom? || line_count.zero? || @lines.empty?
110
+
111
+ self.y_offset = @y_offset + line_count
112
+ visible_lines
113
+ end
114
+
115
+ #: (Integer) -> Array[String]
116
+ def scroll_up(line_count)
117
+ return [] if at_top? || line_count.zero? || @lines.empty?
118
+
119
+ self.y_offset = @y_offset - line_count
120
+ visible_lines
121
+ end
122
+
123
+ #: (Integer) -> void
124
+ def scroll_left(column_count)
125
+ self.x_offset = @x_offset - column_count
126
+ end
127
+
128
+ #: (Integer) -> void
129
+ def scroll_right(column_count)
130
+ self.x_offset = @x_offset + column_count
131
+ end
132
+
133
+ #: () -> Array[String]
134
+ def page_down
135
+ return [] if at_bottom?
136
+
137
+ scroll_down(@height)
138
+ end
139
+
140
+ #: () -> Array[String]
141
+ def page_up
142
+ return [] if at_top?
143
+
144
+ scroll_up(@height)
145
+ end
146
+
147
+ #: () -> Array[String]
148
+ def half_page_down
149
+ return [] if at_bottom?
150
+
151
+ scroll_down(@height / 2)
152
+ end
153
+
154
+ #: () -> Array[String]
155
+ def half_page_up
156
+ return [] if at_top?
157
+
158
+ scroll_up(@height / 2)
159
+ end
160
+
161
+ #: () -> Array[String]
162
+ def goto_top
163
+ return [] if at_top?
164
+
165
+ self.y_offset = 0
166
+ visible_lines
167
+ end
168
+
169
+ #: () -> Array[String]
170
+ def goto_bottom
171
+ self.y_offset = max_y_offset
172
+ visible_lines
173
+ end
174
+
175
+ #: () -> Integer
176
+ def total_line_count
177
+ @lines.length
178
+ end
179
+
180
+ #: () -> Integer
181
+ def visible_line_count
182
+ visible_lines.length
183
+ end
184
+
185
+ #: (Bubbletea::Message) -> [Viewport, Bubbletea::Command?]
186
+ def update(message)
187
+ case message
188
+ when Bubbletea::KeyMessage
189
+ handle_key(message)
190
+ when Bubbletea::MouseMessage
191
+ handle_mouse(message) if @mouse_wheel_enabled
192
+ end
193
+
194
+ [self, nil]
195
+ end
196
+
197
+ #: () -> String
198
+ def view
199
+ lines = visible_lines
200
+ content = lines.join("\n")
201
+
202
+ content += "\n" * (@height - lines.length) if lines.length < @height
203
+
204
+ if (style = @style)
205
+ style.width(@width).height(@height).render(content)
206
+ else
207
+ content
208
+ end
209
+ end
210
+
211
+ private
212
+
213
+ #: (Bubbletea::KeyMessage) -> void
214
+ def handle_key(message)
215
+ case message.to_s
216
+ when "pgdown", "ctrl+f"
217
+ page_down
218
+ when "pgup", "ctrl+b"
219
+ page_up
220
+ when "ctrl+d"
221
+ half_page_down
222
+ when "ctrl+u"
223
+ half_page_up
224
+ when "down", "j"
225
+ scroll_down(1)
226
+ when "up", "k"
227
+ scroll_up(1)
228
+ when "left", "h"
229
+ scroll_left(@horizontal_step) if @horizontal_step.positive?
230
+ when "right", "l"
231
+ scroll_right(@horizontal_step) if @horizontal_step.positive?
232
+ when "home", "g"
233
+ goto_top
234
+ when "end", "G"
235
+ goto_bottom
236
+ end
237
+ end
238
+
239
+ #: (Bubbletea::MouseMessage) -> void
240
+ def handle_mouse(message)
241
+ # Mouse wheel support would go here
242
+ # Depends on bubbletea-ruby mouse message support
243
+ end
244
+
245
+ #: () -> Integer
246
+ def max_y_offset
247
+ frame_size = if (style = @style) && style.respond_to?(:vertical_frame_size)
248
+ style.send(:vertical_frame_size).to_i
249
+ else
250
+ 0
251
+ end
252
+
253
+ [0, @lines.length - @height + frame_size].max
254
+ end
255
+
256
+ #: () -> Array[String]
257
+ def visible_lines
258
+ h = @height
259
+ if (style = @style) && style.respond_to?(:vertical_frame_size)
260
+ h -= style.send(:vertical_frame_size).to_i
261
+ end
262
+
263
+ return [] if @lines.empty?
264
+
265
+ top = [0, @y_offset].max
266
+ bottom = (@y_offset + h).clamp(top, @lines.length)
267
+ lines = @lines[top...bottom] || []
268
+
269
+ if @x_offset.positive? || @longest_line_width > @width
270
+ w = @width
271
+ if (style = @style) && style.respond_to?(:horizontal_frame_size)
272
+ w -= style.send(:horizontal_frame_size).to_i
273
+ end
274
+
275
+ lines = lines.map do |line|
276
+ cut_string(line, @x_offset, @x_offset + w)
277
+ end
278
+ end
279
+
280
+ lines
281
+ end
282
+
283
+ #: (String, Integer, Integer) -> String
284
+ def cut_string(string, start_column, end_column)
285
+ plain = strip_ansi(string)
286
+ return "" if start_column >= plain.length
287
+
288
+ plain[start_column...end_column] || ""
289
+ end
290
+
291
+ #: (String) -> String
292
+ def strip_ansi(string)
293
+ string.gsub(/\e\[[0-9;]*[A-Za-z]/, "")
294
+ end
295
+ end
296
+ end
data/lib/bubbles.rb CHANGED
@@ -1,39 +1,22 @@
1
- require 'fileutils'
2
- require 'pathname'
3
- require 'logger'
4
- require 'securerandom'
5
- require 'yaml'
6
- require 'json'
7
- require 'forwardable'
8
- require 'aws-sdk'
9
- require "bubbles/version"
10
- require "bubbles/config"
11
- require "bubbles/command_queue"
12
- require "bubbles/bubblicious_file"
13
- require "bubbles/dir_watcher"
14
- require 'bubbles/common_uploader_interface'
15
- require "bubbles/uploaders/s3"
16
- require "bubbles/uploaders/s3_ensure_connection"
17
- require "bubbles/uploaders/local_dir"
1
+ # frozen_string_literal: true
18
2
 
19
- module Bubbles
20
- extend self
21
-
22
- def config
23
- @config ||= Config.new
24
- end
3
+ require "bubbletea"
25
4
 
26
- def run
27
- command_queue = Bubbles::CommandQueue.new(config: config)
5
+ require_relative "bubbles/version"
6
+ require_relative "bubbles/spinner"
7
+ require_relative "bubbles/timer"
8
+ require_relative "bubbles/stopwatch"
9
+ require_relative "bubbles/key"
10
+ require_relative "bubbles/help"
11
+ require_relative "bubbles/paginator"
12
+ require_relative "bubbles/progress"
13
+ require_relative "bubbles/cursor"
14
+ require_relative "bubbles/text_input"
15
+ require_relative "bubbles/text_area"
16
+ require_relative "bubbles/viewport"
17
+ require_relative "bubbles/list"
18
+ require_relative "bubbles/table"
19
+ require_relative "bubbles/file_picker"
28
20
 
29
- command_queue << Bubbles::DirWatcher.new({
30
- config: config,
31
- command_queue: command_queue
32
- })
33
-
34
- loop do
35
- command_queue.call_next
36
- sleep config.sleep_interval
37
- end
38
- end
21
+ module Bubbles
39
22
  end
@@ -0,0 +1,87 @@
1
+ # Generated from lib/bubbles/cursor.rb with RBS::Inline
2
+
3
+ module Bubbles
4
+ # Cursor provides cursor functionality for text input components.
5
+ #
6
+ # Example:
7
+ # cursor = Bubbles::Cursor.new
8
+ # cursor.char = "a"
9
+ # cursor.focus
10
+ #
11
+ # # In update:
12
+ # cursor, command = cursor.update(message)
13
+ #
14
+ # # In view:
15
+ # cursor.view
16
+ class Cursor
17
+ MODE_BLINK: Symbol
18
+
19
+ MODE_STATIC: Symbol
20
+
21
+ MODE_HIDE: Symbol
22
+
23
+ DEFAULT_BLINK_SPEED: Float
24
+
25
+ class InitialBlinkMessage < Bubbletea::Message
26
+ end
27
+
28
+ class BlinkMessage < Bubbletea::Message
29
+ attr_reader id: Integer
30
+
31
+ attr_reader tag: Integer
32
+
33
+ # : (id: Integer, tag: Integer) -> void
34
+ def initialize: (id: Integer, tag: Integer) -> void
35
+ end
36
+
37
+ class BlinkCanceledMessage < Bubbletea::Message
38
+ end
39
+
40
+ # : () -> Integer
41
+ def self.next_id: () -> Integer
42
+
43
+ attr_reader id: Integer
44
+
45
+ attr_reader mode: Symbol
46
+
47
+ attr_accessor char: String
48
+
49
+ attr_accessor blink_speed: Float
50
+
51
+ attr_accessor style: Lipgloss::Style?
52
+
53
+ attr_accessor text_style: Lipgloss::Style?
54
+
55
+ # : () -> void
56
+ def initialize: () -> void
57
+
58
+ # : () -> bool
59
+ def blink?: () -> bool
60
+
61
+ # : () -> bool
62
+ def focused?: () -> bool
63
+
64
+ # : (Bubbletea::Message) -> [Cursor, Bubbletea::Command?]
65
+ def update: (Bubbletea::Message) -> [ Cursor, Bubbletea::Command? ]
66
+
67
+ # : (Symbol) -> Bubbletea::Command?
68
+ def set_mode: (Symbol) -> Bubbletea::Command?
69
+
70
+ # : () -> Bubbletea::Command?
71
+ def focus: () -> Bubbletea::Command?
72
+
73
+ # : () -> void
74
+ def blur: () -> void
75
+
76
+ # : () -> String
77
+ def view: () -> String
78
+
79
+ # : () -> Bubbletea::SendMessage
80
+ def self.blink: () -> Bubbletea::SendMessage
81
+
82
+ private
83
+
84
+ # : () -> Bubbletea::Command?
85
+ def blink_command: () -> Bubbletea::Command?
86
+ end
87
+ end