bubbles 0.1.0 → 0.1.1
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/README.md +80 -3
- data/lib/bubbles/ansi.rb +71 -0
- data/lib/bubbles/cryptic_spinner.rb +274 -0
- data/lib/bubbles/help.rb +1 -6
- data/lib/bubbles/version.rb +1 -1
- data/lib/bubbles/viewport.rb +2 -15
- data/lib/bubbles.rb +2 -0
- data/sig/bubbles/ansi.rbs +23 -0
- data/sig/bubbles/cryptic_spinner.rbs +143 -0
- data/sig/bubbles/help.rbs +0 -3
- data/sig/bubbles/viewport.rbs +0 -6
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9d17ffabaffefe00745e04715fe4ccb25ee222a93ea59356fae2ac37240a051a
|
|
4
|
+
data.tar.gz: 93aab8f7b28a74febd9080f65669b81eaab50122cc0c0d53e60ebf277d0be627
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8e1cfd76acbc352629a1b29f0402b9bbc0690ebcbef3fe22c09bef5cfc30e39f1e7818e943f17f62333845453a02cc22675d07b68e0fbe0f93b6773ae711c90e
|
|
7
|
+
data.tar.gz: 93219fc0ed7b7a48fd899f7df71b5df6f92b31bdd5d93666a4f6fdedf29fde180fe1e62bc004ba8575f56f93f672e50de4c5a9e29297b754a5246e5a0e4b886e
|
data/README.md
CHANGED
|
@@ -29,6 +29,7 @@ gem install bubbles
|
|
|
29
29
|
| Component | Description |
|
|
30
30
|
|-----------|-------------|
|
|
31
31
|
| [Spinner](#spinner) | Loading spinners with multiple styles |
|
|
32
|
+
| [CrypticSpinner](#crypticspinner) | Animated gradient spinner with cryptic characters |
|
|
32
33
|
| [Progress](#progress) | Animated progress bars |
|
|
33
34
|
| [Timer](#timer) | Countdown timer |
|
|
34
35
|
| [Stopwatch](#stopwatch) | Elapsed time counter |
|
|
@@ -53,7 +54,7 @@ gem install bubbles
|
|
|
53
54
|
require "bubbles"
|
|
54
55
|
|
|
55
56
|
spinner = Bubbles::Spinner.new
|
|
56
|
-
spinner.spinner = Bubbles::Spinners::
|
|
57
|
+
spinner.spinner = Bubbles::Spinners::DOT
|
|
57
58
|
```
|
|
58
59
|
|
|
59
60
|
**In your update method:**
|
|
@@ -85,6 +86,71 @@ Bubbles::Spinners::HAMBURGER
|
|
|
85
86
|
Bubbles::Spinners::ELLIPSIS
|
|
86
87
|
```
|
|
87
88
|
|
|
89
|
+
### CrypticSpinner
|
|
90
|
+
|
|
91
|
+
**Animated gradient spinner with cryptic characters (inspired by [Charm CLI](https://github.com/charmbracelet/crush)):**
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
spinner = Bubbles::CrypticSpinner.new(
|
|
95
|
+
size: 15,
|
|
96
|
+
label: "Thinking",
|
|
97
|
+
cycle_colors: true
|
|
98
|
+
)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**In your update method:**
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
case message
|
|
105
|
+
when Bubbles::CrypticSpinner::TickMessage
|
|
106
|
+
spinner, command = spinner.update(message)
|
|
107
|
+
end
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**In your view method:**
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
spinner.view
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Custom colors (using CharmTone palette):**
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
spinner = Bubbles::CrypticSpinner.new(
|
|
120
|
+
size: 15,
|
|
121
|
+
label: "Processing",
|
|
122
|
+
color_a: "#6B50FF", # Charple (purple)
|
|
123
|
+
color_b: "#FF60FF", # Dolly (pink)
|
|
124
|
+
label_color: "#DFDBDD",
|
|
125
|
+
cycle_colors: true
|
|
126
|
+
)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Multi-row (matrix style):**
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
spinner = Bubbles::CrypticSpinner.new(
|
|
133
|
+
size: 40,
|
|
134
|
+
rows: 5,
|
|
135
|
+
label: "Decrypting",
|
|
136
|
+
color_a: "#00ff00",
|
|
137
|
+
color_b: "#003300",
|
|
138
|
+
cycle_colors: true
|
|
139
|
+
)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**Options:**
|
|
143
|
+
|
|
144
|
+
| Option | Default | Description |
|
|
145
|
+
|--------|---------|-------------|
|
|
146
|
+
| `size` | 10 | Number of cycling characters |
|
|
147
|
+
| `rows` | 1 | Number of rows (for matrix effect) |
|
|
148
|
+
| `label` | "" | Text label after the animation |
|
|
149
|
+
| `color_a` | "#6B50FF" | Start color of gradient |
|
|
150
|
+
| `color_b` | "#FF60FF" | End color of gradient |
|
|
151
|
+
| `label_color` | "#DFDBDD" | Color of the label text |
|
|
152
|
+
| `cycle_colors` | false | Animate gradient movement |
|
|
153
|
+
|
|
88
154
|
### Progress
|
|
89
155
|
|
|
90
156
|
**Animated progress bar:**
|
|
@@ -507,7 +573,7 @@ class MyApp
|
|
|
507
573
|
|
|
508
574
|
def initialize
|
|
509
575
|
@spinner = Bubbles::Spinner.new
|
|
510
|
-
@spinner.spinner = Bubbles::Spinners::
|
|
576
|
+
@spinner.spinner = Bubbles::Spinners::DOT
|
|
511
577
|
end
|
|
512
578
|
|
|
513
579
|
def init
|
|
@@ -555,6 +621,7 @@ bundle exec rake test
|
|
|
555
621
|
|
|
556
622
|
```bash
|
|
557
623
|
./demo/spinner
|
|
624
|
+
./demo/cryptic_spinner
|
|
558
625
|
./demo/progress
|
|
559
626
|
./demo/textinput
|
|
560
627
|
./demo/textarea
|
|
@@ -579,4 +646,14 @@ The gem is available as open source under the terms of the MIT License.
|
|
|
579
646
|
|
|
580
647
|
## Acknowledgments
|
|
581
648
|
|
|
582
|
-
This gem is a Ruby implementation of [charmbracelet/bubbles](https://github.com/charmbracelet/bubbles), part of the excellent [Charm](https://charm.sh) ecosystem.
|
|
649
|
+
This gem is a Ruby implementation of [charmbracelet/bubbles](https://github.com/charmbracelet/bubbles), part of the excellent [Charm](https://charm.sh) ecosystem. Charm Ruby is not affiliated with or endorsed by Charmbracelet, Inc.
|
|
650
|
+
|
|
651
|
+
---
|
|
652
|
+
|
|
653
|
+
Part of [Charm Ruby](https://charm-ruby.dev).
|
|
654
|
+
|
|
655
|
+
<a href="https://charm-ruby.dev"><img alt="Charm Ruby" src="https://marcoroth.dev/images/heros/glamorous-christmas.png" width="400"></a>
|
|
656
|
+
|
|
657
|
+
[Lipgloss](https://github.com/marcoroth/lipgloss-ruby) • [Bubble Tea](https://github.com/marcoroth/bubbletea-ruby) • [Bubbles](https://github.com/marcoroth/bubbles-ruby) • [Glamour](https://github.com/marcoroth/glamour-ruby) • [Huh?](https://github.com/marcoroth/huh-ruby) • [Harmonica](https://github.com/marcoroth/harmonica-ruby) • [Bubblezone](https://github.com/marcoroth/bubblezone-ruby) • [Gum](https://github.com/marcoroth/gum-ruby) • [ntcharts](https://github.com/marcoroth/ntcharts-ruby)
|
|
658
|
+
|
|
659
|
+
The terminal doesn't have to be boring.
|
data/lib/bubbles/ansi.rb
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
module Bubbles
|
|
5
|
+
module ANSI
|
|
6
|
+
PATTERN = /\e\[[0-9;]*[A-Za-z]/ #: Regexp
|
|
7
|
+
|
|
8
|
+
# Extracts a visible character range from a string while preserving ANSI escape codes.
|
|
9
|
+
# ANSI codes before the start position are carried forward and applied to the result.
|
|
10
|
+
# A reset sequence is appended if the result contains any ANSI codes.
|
|
11
|
+
#
|
|
12
|
+
# @rbs string String -- the input string potentially containing ANSI codes
|
|
13
|
+
# @rbs start_column Integer -- the starting visible character position (0-indexed)
|
|
14
|
+
# @rbs end_column Integer -- the ending visible character position (exclusive)
|
|
15
|
+
# @rbs return String -- the extracted substring with ANSI codes preserved
|
|
16
|
+
#
|
|
17
|
+
def self.cut_string(string, start_column, end_column)
|
|
18
|
+
result = +""
|
|
19
|
+
active_codes = +""
|
|
20
|
+
visible_position = 0
|
|
21
|
+
scanner_position = 0
|
|
22
|
+
|
|
23
|
+
while scanner_position < string.length
|
|
24
|
+
if string[scanner_position..] =~ /\A(#{PATTERN})/
|
|
25
|
+
ansi_sequence = ::Regexp.last_match(1)
|
|
26
|
+
next unless ansi_sequence
|
|
27
|
+
|
|
28
|
+
if visible_position < start_column
|
|
29
|
+
if ansi_sequence == "\e[0m" # rubocop:disable Metrics/BlockNesting
|
|
30
|
+
active_codes = +""
|
|
31
|
+
else
|
|
32
|
+
active_codes << ansi_sequence
|
|
33
|
+
end
|
|
34
|
+
elsif visible_position < end_column
|
|
35
|
+
result << ansi_sequence
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
scanner_position += ansi_sequence.length
|
|
39
|
+
else
|
|
40
|
+
char = string[scanner_position]
|
|
41
|
+
next unless char
|
|
42
|
+
|
|
43
|
+
if visible_position >= start_column && visible_position < end_column
|
|
44
|
+
result << active_codes unless active_codes.empty?
|
|
45
|
+
active_codes = +""
|
|
46
|
+
result << char
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
visible_position += 1
|
|
50
|
+
scanner_position += 1
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
stripped = strip(result)
|
|
55
|
+
return "" if stripped.empty?
|
|
56
|
+
|
|
57
|
+
result << "\e[0m" unless result == stripped
|
|
58
|
+
|
|
59
|
+
result
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Removes all ANSI escape codes from a string.
|
|
63
|
+
#
|
|
64
|
+
# @rbs string String -- the input string
|
|
65
|
+
# @rbs return String -- the string with all ANSI codes removed
|
|
66
|
+
#
|
|
67
|
+
def self.strip(string)
|
|
68
|
+
string.gsub(PATTERN, "")
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
require "lipgloss"
|
|
5
|
+
|
|
6
|
+
module Bubbles
|
|
7
|
+
# CrypticSpinner is an animated activity indicator that displays cycling
|
|
8
|
+
# random characters with gradient colors. Inspired by the Charm CLI crush
|
|
9
|
+
# animation.
|
|
10
|
+
#
|
|
11
|
+
# Example:
|
|
12
|
+
# spinner = Bubbles::CrypticSpinner.new(
|
|
13
|
+
# size: 15,
|
|
14
|
+
# rows: 1,
|
|
15
|
+
# label: "Loading",
|
|
16
|
+
# color_a: "#ff0000",
|
|
17
|
+
# color_b: "#0000ff",
|
|
18
|
+
# cycle_colors: true
|
|
19
|
+
# )
|
|
20
|
+
#
|
|
21
|
+
# Multi-row example (matrix style):
|
|
22
|
+
# spinner = Bubbles::CrypticSpinner.new(
|
|
23
|
+
# size: 30,
|
|
24
|
+
# rows: 5,
|
|
25
|
+
# cycle_colors: true
|
|
26
|
+
# )
|
|
27
|
+
#
|
|
28
|
+
# # In your model's init:
|
|
29
|
+
# def init
|
|
30
|
+
# [self, @spinner.tick]
|
|
31
|
+
# end
|
|
32
|
+
#
|
|
33
|
+
# # In your model's update:
|
|
34
|
+
# def update(message)
|
|
35
|
+
# case message
|
|
36
|
+
# when Bubbles::CrypticSpinner::TickMessage
|
|
37
|
+
# @spinner, command = @spinner.update(message)
|
|
38
|
+
# [self, command]
|
|
39
|
+
# end
|
|
40
|
+
# end
|
|
41
|
+
#
|
|
42
|
+
# # In your model's view:
|
|
43
|
+
# def view
|
|
44
|
+
# @spinner.view
|
|
45
|
+
# end
|
|
46
|
+
#
|
|
47
|
+
class CrypticSpinner
|
|
48
|
+
AVAILABLE_CHARS = "0123456789abcdefABCDEF~!@#$%^&*()+=_".chars.freeze #: Array[String]
|
|
49
|
+
INITIAL_CHAR = "." #: String
|
|
50
|
+
ELLIPSIS_FRAMES = [".", "..", "...", ""].freeze #: Array[String]
|
|
51
|
+
|
|
52
|
+
FPS = 20 #: Integer
|
|
53
|
+
FRAME_DURATION = 1.0 / FPS #: Float
|
|
54
|
+
MAX_BIRTH_OFFSET = 1.0 #: Float
|
|
55
|
+
ELLIPSIS_ANIM_SPEED = 8 #: Integer
|
|
56
|
+
|
|
57
|
+
DEFAULT_SIZE = 10 #: Integer
|
|
58
|
+
DEFAULT_ROWS = 1 #: Integer
|
|
59
|
+
DEFAULT_COLOR_A = "#6B50FF" #: String
|
|
60
|
+
DEFAULT_COLOR_B = "#FF60FF" #: String
|
|
61
|
+
DEFAULT_LABEL_COLOR = "#DFDBDD" #: String
|
|
62
|
+
|
|
63
|
+
class TickMessage < Bubbletea::Message
|
|
64
|
+
attr_reader :id #: Integer
|
|
65
|
+
attr_reader :tag #: Integer
|
|
66
|
+
|
|
67
|
+
#: (id: Integer, tag: Integer) -> void
|
|
68
|
+
def initialize(id:, tag:)
|
|
69
|
+
super()
|
|
70
|
+
@id = id
|
|
71
|
+
@tag = tag
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @rbs self.@next_id: Integer
|
|
76
|
+
# @rbs self.@id_mutex: Mutex
|
|
77
|
+
@next_id = 0
|
|
78
|
+
@id_mutex = Mutex.new
|
|
79
|
+
|
|
80
|
+
class << self
|
|
81
|
+
#: () -> Integer
|
|
82
|
+
def next_id
|
|
83
|
+
@id_mutex.synchronize do
|
|
84
|
+
@next_id += 1
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
attr_reader :id #: Integer
|
|
90
|
+
attr_reader :size #: Integer
|
|
91
|
+
attr_reader :rows #: Integer
|
|
92
|
+
attr_accessor :label
|
|
93
|
+
attr_reader :label_color #: String
|
|
94
|
+
attr_reader :color_a #: String
|
|
95
|
+
attr_reader :color_b #: String
|
|
96
|
+
attr_reader :cycle_colors #: bool
|
|
97
|
+
|
|
98
|
+
#: (
|
|
99
|
+
#: ?size: Integer,
|
|
100
|
+
#: ?rows: Integer,
|
|
101
|
+
#: ?label: String,
|
|
102
|
+
#: ?label_color: String,
|
|
103
|
+
#: ?color_a: String,
|
|
104
|
+
#: ?color_b: String,
|
|
105
|
+
#: ?cycle_colors: bool
|
|
106
|
+
#: ) -> void
|
|
107
|
+
def initialize(
|
|
108
|
+
size: DEFAULT_SIZE,
|
|
109
|
+
rows: DEFAULT_ROWS,
|
|
110
|
+
label: "",
|
|
111
|
+
label_color: DEFAULT_LABEL_COLOR,
|
|
112
|
+
color_a: DEFAULT_COLOR_A,
|
|
113
|
+
color_b: DEFAULT_COLOR_B,
|
|
114
|
+
cycle_colors: false
|
|
115
|
+
)
|
|
116
|
+
@id = self.class.next_id
|
|
117
|
+
@tag = 0
|
|
118
|
+
@size = size
|
|
119
|
+
@rows = rows
|
|
120
|
+
@label = label
|
|
121
|
+
@label_color = label_color
|
|
122
|
+
@color_a = color_a
|
|
123
|
+
@color_b = color_b
|
|
124
|
+
@cycle_colors = cycle_colors
|
|
125
|
+
|
|
126
|
+
@step = 0
|
|
127
|
+
@ellipsis_step = 0
|
|
128
|
+
@start_time = Time.now
|
|
129
|
+
@initialized = false
|
|
130
|
+
|
|
131
|
+
@birth_offsets = Array.new(@rows) do |row|
|
|
132
|
+
Array.new(@size) { (rand * MAX_BIRTH_OFFSET) + (row * 0.1) }
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
@gradient = generate_gradient
|
|
136
|
+
|
|
137
|
+
prerender_frames
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
#: () -> [CrypticSpinner, Bubbletea::Command]
|
|
141
|
+
def init
|
|
142
|
+
[self, tick]
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
#: (Bubbletea::Message) -> [CrypticSpinner, Bubbletea::Command?]
|
|
146
|
+
def update(message)
|
|
147
|
+
case message
|
|
148
|
+
when TickMessage
|
|
149
|
+
return [self, nil] if message.id.positive? && message.id != @id
|
|
150
|
+
return [self, nil] if message.tag.positive? && message.tag != @tag
|
|
151
|
+
|
|
152
|
+
@step = (@step + 1) % @cycling_frames.length
|
|
153
|
+
@tag += 1
|
|
154
|
+
|
|
155
|
+
if @initialized && !@label.empty?
|
|
156
|
+
@ellipsis_step = (@ellipsis_step + 1) % (ELLIPSIS_ANIM_SPEED * ELLIPSIS_FRAMES.length)
|
|
157
|
+
elsif !@initialized && (Time.now - @start_time) >= MAX_BIRTH_OFFSET
|
|
158
|
+
@initialized = true
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
[self, tick]
|
|
162
|
+
else
|
|
163
|
+
[self, nil]
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
#: () -> String
|
|
168
|
+
def view
|
|
169
|
+
elapsed = Time.now - @start_time
|
|
170
|
+
lines = [] #: Array[String]
|
|
171
|
+
|
|
172
|
+
@rows.times do |row|
|
|
173
|
+
line = String.new
|
|
174
|
+
|
|
175
|
+
@size.times do |i|
|
|
176
|
+
line << if !@initialized && elapsed < @birth_offsets[row][i]
|
|
177
|
+
@initial_frames[@step][row][i]
|
|
178
|
+
else
|
|
179
|
+
@cycling_frames[@step][row][i]
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
if row == @rows - 1 && !@label.empty?
|
|
184
|
+
line << " "
|
|
185
|
+
line << render_label
|
|
186
|
+
|
|
187
|
+
if @initialized
|
|
188
|
+
ellipsis_index = @ellipsis_step / ELLIPSIS_ANIM_SPEED
|
|
189
|
+
line << render_ellipsis(ELLIPSIS_FRAMES[ellipsis_index])
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
lines << line
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
lines.join("\n")
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
#: () -> Bubbletea::Command
|
|
200
|
+
def tick
|
|
201
|
+
current_id = @id
|
|
202
|
+
current_tag = @tag
|
|
203
|
+
|
|
204
|
+
Bubbletea.tick(FRAME_DURATION) { TickMessage.new(id: current_id, tag: current_tag) }
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
#: () -> Integer
|
|
208
|
+
def width
|
|
209
|
+
w = @size
|
|
210
|
+
|
|
211
|
+
w += 1 + @label.length + (ELLIPSIS_FRAMES.max_by(&:length) || "").length unless @label.empty?
|
|
212
|
+
|
|
213
|
+
w
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
#: () -> Integer
|
|
217
|
+
def height
|
|
218
|
+
@rows
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
#: (String) -> void
|
|
222
|
+
|
|
223
|
+
private
|
|
224
|
+
|
|
225
|
+
#: () -> Array[String]
|
|
226
|
+
def generate_gradient
|
|
227
|
+
num_colors = @cycle_colors ? @size * 3 : @size
|
|
228
|
+
|
|
229
|
+
Lipgloss::ColorBlend.blends(@color_a, @color_b, num_colors, mode: :hcl)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
#: () -> void
|
|
233
|
+
def prerender_frames
|
|
234
|
+
num_frames = @cycle_colors ? @size * 2 : 10
|
|
235
|
+
|
|
236
|
+
@initial_frames = Array.new(num_frames) do |frame_index|
|
|
237
|
+
Array.new(@rows) do |row|
|
|
238
|
+
offset = @cycle_colors ? frame_index + row : row
|
|
239
|
+
|
|
240
|
+
Array.new(@size) do |char_index|
|
|
241
|
+
color_index = (char_index + offset) % @gradient.length
|
|
242
|
+
style = Lipgloss::Style.new.foreground(@gradient[color_index])
|
|
243
|
+
style.render(INITIAL_CHAR)
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
@cycling_frames = Array.new(num_frames) do |frame_index|
|
|
249
|
+
Array.new(@rows) do |row|
|
|
250
|
+
offset = @cycle_colors ? frame_index + row : row
|
|
251
|
+
|
|
252
|
+
Array.new(@size) do |char_index|
|
|
253
|
+
color_index = (char_index + offset) % @gradient.length
|
|
254
|
+
char = AVAILABLE_CHARS.sample
|
|
255
|
+
style = Lipgloss::Style.new.foreground(@gradient[color_index])
|
|
256
|
+
style.render(char)
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
#: () -> String
|
|
263
|
+
def render_label
|
|
264
|
+
style = Lipgloss::Style.new.foreground(@label_color)
|
|
265
|
+
style.render(@label)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
#: (String) -> String
|
|
269
|
+
def render_ellipsis(ellipsis)
|
|
270
|
+
style = Lipgloss::Style.new.foreground(@label_color)
|
|
271
|
+
style.render(ellipsis)
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
end
|
data/lib/bubbles/help.rb
CHANGED
|
@@ -152,7 +152,7 @@ module Bubbles
|
|
|
152
152
|
lines = text.split("\n")
|
|
153
153
|
|
|
154
154
|
lines.map do |line|
|
|
155
|
-
plain =
|
|
155
|
+
plain = ANSI.strip(line)
|
|
156
156
|
|
|
157
157
|
if plain.length > @width
|
|
158
158
|
line[0, @width]
|
|
@@ -161,10 +161,5 @@ module Bubbles
|
|
|
161
161
|
end
|
|
162
162
|
end.join("\n")
|
|
163
163
|
end
|
|
164
|
-
|
|
165
|
-
#: (String) -> String
|
|
166
|
-
def strip_ansi(str)
|
|
167
|
-
str.gsub(/\e\[[0-9;]*[A-Za-z]/, "")
|
|
168
|
-
end
|
|
169
164
|
end
|
|
170
165
|
end
|
data/lib/bubbles/version.rb
CHANGED
data/lib/bubbles/viewport.rb
CHANGED
|
@@ -78,7 +78,7 @@ module Bubbles
|
|
|
78
78
|
def content=(content)
|
|
79
79
|
content = content.gsub("\r\n", "\n")
|
|
80
80
|
@lines = content.split("\n", -1)
|
|
81
|
-
@longest_line_width = @lines.map { |l|
|
|
81
|
+
@longest_line_width = @lines.map { |l| ANSI.strip(l).length }.max || 0
|
|
82
82
|
|
|
83
83
|
goto_bottom if @y_offset > @lines.length - 1
|
|
84
84
|
end
|
|
@@ -273,24 +273,11 @@ module Bubbles
|
|
|
273
273
|
end
|
|
274
274
|
|
|
275
275
|
lines = lines.map do |line|
|
|
276
|
-
cut_string(line, @x_offset, @x_offset + w)
|
|
276
|
+
ANSI.cut_string(line, @x_offset, @x_offset + w)
|
|
277
277
|
end
|
|
278
278
|
end
|
|
279
279
|
|
|
280
280
|
lines
|
|
281
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
282
|
end
|
|
296
283
|
end
|
data/lib/bubbles.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "bubbletea"
|
|
4
4
|
|
|
5
5
|
require_relative "bubbles/version"
|
|
6
|
+
require_relative "bubbles/ansi"
|
|
6
7
|
require_relative "bubbles/spinner"
|
|
7
8
|
require_relative "bubbles/timer"
|
|
8
9
|
require_relative "bubbles/stopwatch"
|
|
@@ -17,6 +18,7 @@ require_relative "bubbles/viewport"
|
|
|
17
18
|
require_relative "bubbles/list"
|
|
18
19
|
require_relative "bubbles/table"
|
|
19
20
|
require_relative "bubbles/file_picker"
|
|
21
|
+
require_relative "bubbles/cryptic_spinner"
|
|
20
22
|
|
|
21
23
|
module Bubbles
|
|
22
24
|
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Generated from lib/bubbles/ansi.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module Bubbles
|
|
4
|
+
module ANSI
|
|
5
|
+
PATTERN: Regexp
|
|
6
|
+
|
|
7
|
+
# Extracts a visible character range from a string while preserving ANSI escape codes.
|
|
8
|
+
# ANSI codes before the start position are carried forward and applied to the result.
|
|
9
|
+
# A reset sequence is appended if the result contains any ANSI codes.
|
|
10
|
+
#
|
|
11
|
+
# @rbs string String -- the input string potentially containing ANSI codes
|
|
12
|
+
# @rbs start_column Integer -- the starting visible character position (0-indexed)
|
|
13
|
+
# @rbs end_column Integer -- the ending visible character position (exclusive)
|
|
14
|
+
# @rbs return String -- the extracted substring with ANSI codes preserved
|
|
15
|
+
def self.cut_string: (String string, Integer start_column, Integer end_column) -> String
|
|
16
|
+
|
|
17
|
+
# Removes all ANSI escape codes from a string.
|
|
18
|
+
#
|
|
19
|
+
# @rbs string String -- the input string
|
|
20
|
+
# @rbs return String -- the string with all ANSI codes removed
|
|
21
|
+
def self.strip: (String string) -> String
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Generated from lib/bubbles/cryptic_spinner.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module Bubbles
|
|
4
|
+
# CrypticSpinner is an animated activity indicator that displays cycling
|
|
5
|
+
# random characters with gradient colors. Inspired by the Charm CLI crush
|
|
6
|
+
# animation.
|
|
7
|
+
#
|
|
8
|
+
# Example:
|
|
9
|
+
# spinner = Bubbles::CrypticSpinner.new(
|
|
10
|
+
# size: 15,
|
|
11
|
+
# rows: 1,
|
|
12
|
+
# label: "Loading",
|
|
13
|
+
# color_a: "#ff0000",
|
|
14
|
+
# color_b: "#0000ff",
|
|
15
|
+
# cycle_colors: true
|
|
16
|
+
# )
|
|
17
|
+
#
|
|
18
|
+
# Multi-row example (matrix style):
|
|
19
|
+
# spinner = Bubbles::CrypticSpinner.new(
|
|
20
|
+
# size: 30,
|
|
21
|
+
# rows: 5,
|
|
22
|
+
# cycle_colors: true
|
|
23
|
+
# )
|
|
24
|
+
#
|
|
25
|
+
# # In your model's init:
|
|
26
|
+
# def init
|
|
27
|
+
# [self, @spinner.tick]
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
# # In your model's update:
|
|
31
|
+
# def update(message)
|
|
32
|
+
# case message
|
|
33
|
+
# when Bubbles::CrypticSpinner::TickMessage
|
|
34
|
+
# @spinner, command = @spinner.update(message)
|
|
35
|
+
# [self, command]
|
|
36
|
+
# end
|
|
37
|
+
# end
|
|
38
|
+
#
|
|
39
|
+
# # In your model's view:
|
|
40
|
+
# def view
|
|
41
|
+
# @spinner.view
|
|
42
|
+
# end
|
|
43
|
+
class CrypticSpinner
|
|
44
|
+
AVAILABLE_CHARS: Array[String]
|
|
45
|
+
|
|
46
|
+
INITIAL_CHAR: String
|
|
47
|
+
|
|
48
|
+
ELLIPSIS_FRAMES: Array[String]
|
|
49
|
+
|
|
50
|
+
FPS: Integer
|
|
51
|
+
|
|
52
|
+
FRAME_DURATION: Float
|
|
53
|
+
|
|
54
|
+
MAX_BIRTH_OFFSET: Float
|
|
55
|
+
|
|
56
|
+
ELLIPSIS_ANIM_SPEED: Integer
|
|
57
|
+
|
|
58
|
+
DEFAULT_SIZE: Integer
|
|
59
|
+
|
|
60
|
+
DEFAULT_ROWS: Integer
|
|
61
|
+
|
|
62
|
+
DEFAULT_COLOR_A: String
|
|
63
|
+
|
|
64
|
+
DEFAULT_COLOR_B: String
|
|
65
|
+
|
|
66
|
+
DEFAULT_LABEL_COLOR: String
|
|
67
|
+
|
|
68
|
+
class TickMessage < Bubbletea::Message
|
|
69
|
+
attr_reader id: Integer
|
|
70
|
+
|
|
71
|
+
attr_reader tag: Integer
|
|
72
|
+
|
|
73
|
+
# : (id: Integer, tag: Integer) -> void
|
|
74
|
+
def initialize: (id: Integer, tag: Integer) -> void
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
self.@id_mutex: Mutex
|
|
78
|
+
|
|
79
|
+
self.@next_id: Integer
|
|
80
|
+
|
|
81
|
+
# : () -> Integer
|
|
82
|
+
def self.next_id: () -> Integer
|
|
83
|
+
|
|
84
|
+
attr_reader id: Integer
|
|
85
|
+
|
|
86
|
+
attr_reader size: Integer
|
|
87
|
+
|
|
88
|
+
attr_reader rows: Integer
|
|
89
|
+
|
|
90
|
+
attr_accessor label: untyped
|
|
91
|
+
|
|
92
|
+
attr_reader label_color: String
|
|
93
|
+
|
|
94
|
+
attr_reader color_a: String
|
|
95
|
+
|
|
96
|
+
attr_reader color_b: String
|
|
97
|
+
|
|
98
|
+
attr_reader cycle_colors: bool
|
|
99
|
+
|
|
100
|
+
# : (
|
|
101
|
+
# : ?size: Integer,
|
|
102
|
+
# : ?rows: Integer,
|
|
103
|
+
# : ?label: String,
|
|
104
|
+
# : ?label_color: String,
|
|
105
|
+
# : ?color_a: String,
|
|
106
|
+
# : ?color_b: String,
|
|
107
|
+
# : ?cycle_colors: bool
|
|
108
|
+
# : ) -> void
|
|
109
|
+
def initialize: (?size: untyped, ?rows: untyped, ?label: untyped, ?label_color: untyped, ?color_a: untyped, ?color_b: untyped, ?cycle_colors: untyped) -> untyped
|
|
110
|
+
|
|
111
|
+
# : () -> [CrypticSpinner, Bubbletea::Command]
|
|
112
|
+
def init: () -> [ CrypticSpinner, Bubbletea::Command ]
|
|
113
|
+
|
|
114
|
+
# : (Bubbletea::Message) -> [CrypticSpinner, Bubbletea::Command?]
|
|
115
|
+
def update: (Bubbletea::Message) -> [ CrypticSpinner, Bubbletea::Command? ]
|
|
116
|
+
|
|
117
|
+
# : () -> String
|
|
118
|
+
def view: () -> String
|
|
119
|
+
|
|
120
|
+
# : () -> Bubbletea::Command
|
|
121
|
+
def tick: () -> Bubbletea::Command
|
|
122
|
+
|
|
123
|
+
# : () -> Integer
|
|
124
|
+
def width: () -> Integer
|
|
125
|
+
|
|
126
|
+
# : () -> Integer
|
|
127
|
+
def height: () -> Integer
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
|
|
131
|
+
# : () -> Array[String]
|
|
132
|
+
def generate_gradient: () -> Array[String]
|
|
133
|
+
|
|
134
|
+
# : () -> void
|
|
135
|
+
def prerender_frames: () -> void
|
|
136
|
+
|
|
137
|
+
# : () -> String
|
|
138
|
+
def render_label: () -> String
|
|
139
|
+
|
|
140
|
+
# : (String) -> String
|
|
141
|
+
def render_ellipsis: (String) -> String
|
|
142
|
+
end
|
|
143
|
+
end
|
data/sig/bubbles/help.rbs
CHANGED
data/sig/bubbles/viewport.rbs
CHANGED
|
@@ -115,11 +115,5 @@ module Bubbles
|
|
|
115
115
|
|
|
116
116
|
# : () -> Array[String]
|
|
117
117
|
def visible_lines: () -> Array[String]
|
|
118
|
-
|
|
119
|
-
# : (String, Integer, Integer) -> String
|
|
120
|
-
def cut_string: (String, Integer, Integer) -> String
|
|
121
|
-
|
|
122
|
-
# : (String) -> String
|
|
123
|
-
def strip_ansi: (String) -> String
|
|
124
118
|
end
|
|
125
119
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bubbles
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Marco Roth
|
|
@@ -63,6 +63,8 @@ files:
|
|
|
63
63
|
- README.md
|
|
64
64
|
- bubbles.gemspec
|
|
65
65
|
- lib/bubbles.rb
|
|
66
|
+
- lib/bubbles/ansi.rb
|
|
67
|
+
- lib/bubbles/cryptic_spinner.rb
|
|
66
68
|
- lib/bubbles/cursor.rb
|
|
67
69
|
- lib/bubbles/file_picker.rb
|
|
68
70
|
- lib/bubbles/help.rb
|
|
@@ -80,6 +82,8 @@ files:
|
|
|
80
82
|
- lib/bubbles/version.rb
|
|
81
83
|
- lib/bubbles/viewport.rb
|
|
82
84
|
- sig/bubbles.rbs
|
|
85
|
+
- sig/bubbles/ansi.rbs
|
|
86
|
+
- sig/bubbles/cryptic_spinner.rbs
|
|
83
87
|
- sig/bubbles/cursor.rbs
|
|
84
88
|
- sig/bubbles/file_picker.rbs
|
|
85
89
|
- sig/bubbles/help.rbs
|