riffer-rig 0.1.2 → 0.2.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 +18 -0
- data/lib/riffer/rig/cli.rb +4 -7
- data/lib/riffer/rig/repl.rb +39 -5
- data/lib/riffer/rig/ui/animator.rb +56 -11
- data/lib/riffer/rig/ui/banner.rb +7 -15
- data/lib/riffer/rig/ui/renderer.rb +31 -2
- data/lib/riffer/rig/ui/smoother.rb +92 -0
- data/lib/riffer/rig/version.rb +1 -1
- metadata +2 -3
- data/lib/riffer/rig/ui/pixels.rb +0 -47
- data/lib/riffer/rig/ui/riffy.rb +0 -81
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f87fb650798f95679ff56654620a0fcb3cc383d8ce650849d9e33382a7837a6f
|
|
4
|
+
data.tar.gz: e8784d13f472d45c5ffb7f1d269c5ab8ff7d0b41a62cfae446ceaee2345413b9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '09252fc5c265fdc91be5fc8753b3407560c3be9f94af9616b2d3ae9e8e3c28f100374bb1014d7b8038bd51abc5f86875e6116fc874733ba58dda87254b15e3cc'
|
|
7
|
+
data.tar.gz: 7f1cc7dae6f6ccf69cf370296458d1eef837111f01041adae31a59e48df6719903c1c43630cf0248ba4b7ba140d420f663a585dcb13f5c945d5f105eaa2b8fc9
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.0](https://github.com/bottrall/riffer-rig/compare/v0.1.2...v0.2.0) (2026-09-08)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### ⚠ BREAKING CHANGES
|
|
7
|
+
|
|
8
|
+
* remove the Riffy mascot entirely ([#151](https://github.com/bottrall/riffer-rig/issues/151))
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* remove the Riffy mascot entirely ([#151](https://github.com/bottrall/riffer-rig/issues/151)) ([af17f99](https://github.com/bottrall/riffer-rig/commit/af17f9907e8d43a01f966e1a0979f868a8d50466))
|
|
13
|
+
* **ui:** keep the thinking indicator alive during reasoning ([#156](https://github.com/bottrall/riffer-rig/issues/156)) ([4ff67cc](https://github.com/bottrall/riffer-rig/commit/4ff67cc6a2e133324b98dbdf40f63c500ee00a4c))
|
|
14
|
+
* **ui:** smooth streamed text output at 60fps ([#153](https://github.com/bottrall/riffer-rig/issues/153)) ([42dc781](https://github.com/bottrall/riffer-rig/commit/42dc781b3fc4a942e0eaca755b9e0b4180659506))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Bug Fixes
|
|
18
|
+
|
|
19
|
+
* **ui:** drop 'code' subtitle from banner ([#157](https://github.com/bottrall/riffer-rig/issues/157)) ([dfd2e73](https://github.com/bottrall/riffer-rig/commit/dfd2e73d6b71f10adbf1dfa2ae659aad85ed547e))
|
|
20
|
+
|
|
3
21
|
## [0.1.2](https://github.com/bottrall/riffer-rig/compare/v0.1.1...v0.1.2) (2026-09-07)
|
|
4
22
|
|
|
5
23
|
|
data/lib/riffer/rig/cli.rb
CHANGED
|
@@ -30,8 +30,9 @@ module Riffer::Rig::CLI
|
|
|
30
30
|
reveal_banner(theme, animator, model)
|
|
31
31
|
|
|
32
32
|
tally = Riffer::Rig::TokenTally.new(pricing: Riffer::Rig::Settings.pricing_for(model))
|
|
33
|
-
|
|
34
|
-
Riffer::Rig::
|
|
33
|
+
smoother = Riffer::Rig::UI::Smoother.new(io: output, theme:)
|
|
34
|
+
renderer = Riffer::Rig::UI::Renderer.new(io: output, theme:, tally:, smoother:)
|
|
35
|
+
Riffer::Rig::REPL.new(agent:, renderer:, animator:, theme:, smoother:, input:, output:).run
|
|
35
36
|
0
|
|
36
37
|
end
|
|
37
38
|
|
|
@@ -81,11 +82,7 @@ module Riffer::Rig::CLI
|
|
|
81
82
|
loaded = [Riffer::Rig::CodingAgent::GLOBAL_AGENTS_FILE, File.join(Dir.pwd, 'AGENTS.md')].select { |path| File.file?(path) }
|
|
82
83
|
context = loaded.empty? ? 'none' : loaded.join(', ')
|
|
83
84
|
|
|
84
|
-
|
|
85
|
-
frames = glints.map do |glint_col|
|
|
86
|
-
Riffer::Rig::UI::Banner.lines(theme, model: model, cwd: Dir.pwd, context: context, skills: count_skills, version: Riffer::Rig::VERSION, glint_col: glint_col)
|
|
87
|
-
end
|
|
88
|
-
animator.reveal(frames)
|
|
85
|
+
animator.reveal([Riffer::Rig::UI::Banner.lines(theme, model: model, cwd: Dir.pwd, context: context, skills: count_skills, version: Riffer::Rig::VERSION)])
|
|
89
86
|
end
|
|
90
87
|
|
|
91
88
|
def count_skills
|
data/lib/riffer/rig/repl.rb
CHANGED
|
@@ -4,14 +4,15 @@ class Riffer::Rig::REPL
|
|
|
4
4
|
EXIT_COMMANDS = ['/exit', '/quit'].freeze
|
|
5
5
|
SKILL_COMMAND = %r{\A/skill:([a-z0-9]+(?:-[a-z0-9]+)*)(?:\s+(.*))?\z}m
|
|
6
6
|
|
|
7
|
-
def initialize(agent:, renderer:, input: $stdin, output: $stdout, theme: Riffer::Rig::UI::Theme.for(output), animator: Riffer::Rig::UI::Animator.new(io: output, theme:))
|
|
7
|
+
def initialize(agent:, renderer:, input: $stdin, output: $stdout, theme: Riffer::Rig::UI::Theme.for(output), animator: Riffer::Rig::UI::Animator.new(io: output, theme:), smoother: Riffer::Rig::UI::Smoother.new(io: output, theme:))
|
|
8
8
|
@agent = agent
|
|
9
9
|
@renderer = renderer
|
|
10
10
|
@animator = animator
|
|
11
|
+
@smoother = smoother
|
|
11
12
|
@theme = theme
|
|
12
13
|
@input = input
|
|
13
14
|
@output = output
|
|
14
|
-
@agent.session.on_message { |message|
|
|
15
|
+
@agent.session.on_message { |message| render_tool_result(message) }
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
def run
|
|
@@ -39,16 +40,38 @@ class Riffer::Rig::REPL
|
|
|
39
40
|
private
|
|
40
41
|
|
|
41
42
|
def run_turn(prompt)
|
|
42
|
-
@animator.
|
|
43
|
+
@animator.start
|
|
44
|
+
@smoother.start
|
|
43
45
|
@agent.stream(prompt).each do |event|
|
|
44
|
-
|
|
46
|
+
case event
|
|
47
|
+
when Riffer::StreamEvents::ReasoningDelta
|
|
48
|
+
@animator.start(:reasoning)
|
|
49
|
+
when Riffer::StreamEvents::ReasoningDone
|
|
50
|
+
@animator.start
|
|
51
|
+
when Riffer::StreamEvents::ToolCallDelta, Riffer::StreamEvents::FinishReasonDone
|
|
52
|
+
# Round bookkeeping: nothing renders, so the indicator just carries on
|
|
53
|
+
# (or stays parked while the smoother finishes typing earlier text).
|
|
54
|
+
next
|
|
55
|
+
when Riffer::StreamEvents::ToolCallDone, Riffer::StreamEvents::SkillActivation, Riffer::StreamEvents::TokenUsageDone
|
|
56
|
+
# The spinner shares its line with what's about to print, and tool
|
|
57
|
+
# execution plus the next model invocation emit no events — stop it for
|
|
58
|
+
# the render, then bring it straight back to cover the silent stretch.
|
|
59
|
+
@animator.stop
|
|
60
|
+
@renderer.render(event)
|
|
61
|
+
@animator.start
|
|
62
|
+
next
|
|
63
|
+
else
|
|
64
|
+
@animator.stop
|
|
65
|
+
end
|
|
45
66
|
@renderer.render(event)
|
|
46
67
|
end
|
|
68
|
+
@smoother.finish
|
|
47
69
|
@output.puts
|
|
48
70
|
rescue StandardError => e
|
|
49
71
|
@output.puts("\nError: #{e.message}")
|
|
50
72
|
ensure
|
|
51
|
-
@
|
|
73
|
+
@smoother.finish
|
|
74
|
+
@animator.stop
|
|
52
75
|
end
|
|
53
76
|
|
|
54
77
|
def run_skill_command(name, args)
|
|
@@ -83,4 +106,15 @@ class Riffer::Rig::REPL
|
|
|
83
106
|
def skill_block(name, body)
|
|
84
107
|
"<skill name=\"#{name}\">\n#{body}\n</skill>"
|
|
85
108
|
end
|
|
109
|
+
|
|
110
|
+
# Tool results can land mid-animation (tool execution emits no stream events,
|
|
111
|
+
# so the indicator is up); stop it around the line so its next frame doesn't
|
|
112
|
+
# erase what we printed.
|
|
113
|
+
def render_tool_result(message)
|
|
114
|
+
return unless message.is_a?(Riffer::Messages::Tool)
|
|
115
|
+
|
|
116
|
+
@animator.stop
|
|
117
|
+
@renderer.render_tool_result(message)
|
|
118
|
+
@animator.start
|
|
119
|
+
end
|
|
86
120
|
end
|
|
@@ -5,11 +5,24 @@
|
|
|
5
5
|
class Riffer::Rig::UI::Animator
|
|
6
6
|
REVEAL_FRAME_SECONDS = 0.05
|
|
7
7
|
SPINNER_FRAME_SECONDS = 0.12
|
|
8
|
+
EQ_LEVELS = '▁▂▃▄▅▆▇█'.chars.freeze
|
|
9
|
+
EQ_BARS = 7
|
|
10
|
+
NEUTRAL_LABEL = 'riffing…'
|
|
11
|
+
REASONING_PHRASES = [
|
|
12
|
+
'contemplating…', 'pondering…', 'mulling it over…', 'reasoning…',
|
|
13
|
+
'connecting the dots…', 'herding thoughts…', 'consulting the muse…',
|
|
14
|
+
'doing some deep listening…', 'warming up…', 'tuning up…',
|
|
15
|
+
'in the woodshed…', 'vamping…', 'finding the key…', 'counting it off…',
|
|
16
|
+
'jamming internally…'
|
|
17
|
+
].freeze
|
|
18
|
+
REASONING_TICK_RANGE = (1..5)
|
|
8
19
|
|
|
9
20
|
def initialize(io: $stdout, theme: Riffer::Rig::UI::Theme.for(io))
|
|
10
21
|
@io = io
|
|
11
22
|
@theme = theme
|
|
12
23
|
@thread = nil
|
|
24
|
+
@mode = :neutral
|
|
25
|
+
@phrase = nil
|
|
13
26
|
end
|
|
14
27
|
|
|
15
28
|
# When not on a TTY, prints the final frame once instead of animating.
|
|
@@ -28,22 +41,20 @@ class Riffer::Rig::UI::Animator
|
|
|
28
41
|
end
|
|
29
42
|
end
|
|
30
43
|
|
|
31
|
-
def
|
|
44
|
+
def start(mode = :neutral)
|
|
32
45
|
return unless enabled?
|
|
33
46
|
|
|
34
|
-
@
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
until @stop
|
|
38
|
-
@io.print("\r #{Riffer::Rig::UI::Riffy.equalizer(@theme, tick)}\e[K")
|
|
39
|
-
@io.flush
|
|
40
|
-
sleep(SPINNER_FRAME_SECONDS)
|
|
41
|
-
tick += 1
|
|
42
|
-
end
|
|
47
|
+
if @thread
|
|
48
|
+
@mode = mode
|
|
49
|
+
return
|
|
43
50
|
end
|
|
51
|
+
|
|
52
|
+
@mode = mode
|
|
53
|
+
@stop = false
|
|
54
|
+
@thread = Thread.new { animate }
|
|
44
55
|
end
|
|
45
56
|
|
|
46
|
-
def
|
|
57
|
+
def stop
|
|
47
58
|
return unless @thread
|
|
48
59
|
|
|
49
60
|
@stop = true
|
|
@@ -53,8 +64,42 @@ class Riffer::Rig::UI::Animator
|
|
|
53
64
|
@io.flush
|
|
54
65
|
end
|
|
55
66
|
|
|
67
|
+
def equalizer(tick, label = NEUTRAL_LABEL)
|
|
68
|
+
bars = Array.new(EQ_BARS) do |i|
|
|
69
|
+
height = (Math.sin((tick + i) * 0.6).abs * (EQ_LEVELS.length - 1)).round
|
|
70
|
+
bar = EQ_LEVELS[height]
|
|
71
|
+
i.even? ? @theme.cyan(bar) : @theme.magenta(bar)
|
|
72
|
+
end
|
|
73
|
+
"#{bars.join} #{@theme.grey(label)}"
|
|
74
|
+
end
|
|
75
|
+
|
|
56
76
|
private
|
|
57
77
|
|
|
78
|
+
def animate
|
|
79
|
+
tick = 0
|
|
80
|
+
roll_at = 0.0 # force an immediate phrase roll when entering reasoning mode
|
|
81
|
+
until @stop
|
|
82
|
+
roll_at = roll_phrase(roll_at)
|
|
83
|
+
@io.print("\r #{equalizer(tick, label)}\e[K")
|
|
84
|
+
@io.flush
|
|
85
|
+
sleep(SPINNER_FRAME_SECONDS)
|
|
86
|
+
tick += 1
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Re-rolls both phrase and duration whenever the reasoning tick expires.
|
|
91
|
+
def roll_phrase(roll_at)
|
|
92
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
93
|
+
return roll_at unless @mode == :reasoning && now >= roll_at
|
|
94
|
+
|
|
95
|
+
@phrase = REASONING_PHRASES.sample
|
|
96
|
+
now + rand(REASONING_TICK_RANGE)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def label
|
|
100
|
+
@mode == :reasoning ? @phrase : NEUTRAL_LABEL
|
|
101
|
+
end
|
|
102
|
+
|
|
58
103
|
def enabled?
|
|
59
104
|
@theme.enabled && @io.respond_to?(:tty?) && @io.tty?
|
|
60
105
|
end
|
data/lib/riffer/rig/ui/banner.rb
CHANGED
|
@@ -23,27 +23,19 @@ module Riffer::Rig::UI::Banner
|
|
|
23
23
|
|
|
24
24
|
INFO_LABEL_WIDTH = 8
|
|
25
25
|
INDENT = ' '
|
|
26
|
-
BEAR_GAP = ' '
|
|
27
26
|
|
|
28
|
-
def call(theme, model:, cwd:, context:, skills:, version
|
|
29
|
-
lines(theme, model: model, cwd: cwd, context: context, skills: skills, version: version
|
|
27
|
+
def call(theme, model:, cwd:, context:, skills:, version:)
|
|
28
|
+
lines(theme, model: model, cwd: cwd, context: context, skills: skills, version: version).join("\n")
|
|
30
29
|
end
|
|
31
30
|
|
|
32
|
-
def lines(theme, model:, cwd:, context:, skills:, version
|
|
33
|
-
[''] + art(theme
|
|
31
|
+
def lines(theme, model:, cwd:, context:, skills:, version:)
|
|
32
|
+
[''] + art(theme) + [''] +
|
|
34
33
|
info(theme, model: model, cwd: cwd, context: context, skills: skills, version: version) + ['']
|
|
35
34
|
end
|
|
36
35
|
|
|
37
|
-
def art(theme
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
right += ['', "#{theme.grey('· code ·')} #{theme.cyan("♪ let's riff ♪")}"]
|
|
41
|
-
|
|
42
|
-
pad = (bear.length - right.length) / 2
|
|
43
|
-
bear.each_index.map do |i|
|
|
44
|
-
side = right[i - pad] if (pad...(pad + right.length)).cover?(i)
|
|
45
|
-
"#{INDENT}#{bear[i]}#{BEAR_GAP}#{side}".rstrip
|
|
46
|
-
end
|
|
36
|
+
def art(theme)
|
|
37
|
+
art = WORDMARK.each_index.map { |i| "#{INDENT}#{theme.paint(WORDMARK[i], ROW_COLOURS[i])}" }
|
|
38
|
+
art + ["#{INDENT}#{theme.cyan("♪ let's riff ♪")}"]
|
|
47
39
|
end
|
|
48
40
|
|
|
49
41
|
def info(theme, model:, cwd:, context:, skills:, version:)
|
|
@@ -5,23 +5,28 @@ require 'json'
|
|
|
5
5
|
class Riffer::Rig::UI::Renderer
|
|
6
6
|
RESULT_PREVIEW_LIMIT = 200
|
|
7
7
|
|
|
8
|
-
def initialize(io: $stdout, theme: Riffer::Rig::UI::Theme.for(io), tally: nil)
|
|
8
|
+
def initialize(io: $stdout, theme: Riffer::Rig::UI::Theme.for(io), tally: nil, smoother: nil)
|
|
9
9
|
@io = io
|
|
10
10
|
@theme = theme
|
|
11
11
|
@tally = tally
|
|
12
|
+
@smoother = smoother
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
def render(event)
|
|
15
16
|
case event
|
|
16
17
|
when Riffer::StreamEvents::TextDelta
|
|
17
|
-
|
|
18
|
+
smoother << event.content
|
|
18
19
|
when Riffer::StreamEvents::ToolCallDone
|
|
20
|
+
drain_smoother
|
|
19
21
|
@io.puts("\n#{@theme.cyan("⚙ #{event.name}(#{format_arguments(event.arguments)})")}")
|
|
20
22
|
when Riffer::StreamEvents::SkillActivation
|
|
23
|
+
drain_smoother
|
|
21
24
|
@io.puts("\n#{@theme.magenta("✦ skill: #{event.name}")}")
|
|
22
25
|
when Riffer::StreamEvents::Interrupt
|
|
26
|
+
drain_smoother
|
|
23
27
|
@io.puts(@theme.dim("[interrupted: #{event.reason}]"))
|
|
24
28
|
when Riffer::StreamEvents::TokenUsageDone
|
|
29
|
+
drain_smoother
|
|
25
30
|
render_token_usage(event.token_usage)
|
|
26
31
|
end
|
|
27
32
|
end
|
|
@@ -35,6 +40,30 @@ class Riffer::Rig::UI::Renderer
|
|
|
35
40
|
|
|
36
41
|
private
|
|
37
42
|
|
|
43
|
+
def smoother
|
|
44
|
+
@smoother || PassThroughSmoother.new(@io)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Stand-in when no smoother is injected, so a bare Renderer still prints
|
|
48
|
+
# synchronously.
|
|
49
|
+
class PassThroughSmoother
|
|
50
|
+
def initialize(io) = @io = io
|
|
51
|
+
|
|
52
|
+
def <<(content)
|
|
53
|
+
@io.print(content)
|
|
54
|
+
@io.flush
|
|
55
|
+
self
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def drain = nil
|
|
59
|
+
|
|
60
|
+
def finish = nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def drain_smoother
|
|
64
|
+
smoother.drain
|
|
65
|
+
end
|
|
66
|
+
|
|
38
67
|
def render_token_usage(usage)
|
|
39
68
|
return unless @tally
|
|
40
69
|
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Paces streamed text onto a 60fps cadence so API bursts read as continuous
|
|
4
|
+
# typing. On a non-TTY output it is transparent: writes pass through
|
|
5
|
+
# synchronously and no thread ever runs.
|
|
6
|
+
class Riffer::Rig::UI::Smoother
|
|
7
|
+
TICK_SECONDS = 1.0 / 60.0
|
|
8
|
+
|
|
9
|
+
# A frame removes only a sixtieth of the remaining backlog — an exponential
|
|
10
|
+
# decay whose tail keeps text flowing for a second or more, so the reveal
|
|
11
|
+
# never visibly stops between chunks. The carry accrues sub-character
|
|
12
|
+
# releases so the effective rate can settle at the provider's throughput;
|
|
13
|
+
# the floor is fractional (10 chars/s) so it can't over-drain a backlog
|
|
14
|
+
# that's trickling in slower than 60 chars/s.
|
|
15
|
+
BACKLOG_FRACTION_PER_TICK = Rational(1, 60)
|
|
16
|
+
MIN_CHARS_PER_TICK = Rational(1, 6)
|
|
17
|
+
|
|
18
|
+
def initialize(io: $stdout, theme: Riffer::Rig::UI::Theme.for(io), clock: Kernel)
|
|
19
|
+
@io = io
|
|
20
|
+
@theme = theme
|
|
21
|
+
@clock = clock
|
|
22
|
+
@backlog = +''
|
|
23
|
+
@carry = Rational(0)
|
|
24
|
+
@mutex = Mutex.new
|
|
25
|
+
@thread = nil
|
|
26
|
+
@stop = false
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def start
|
|
30
|
+
return unless enabled?
|
|
31
|
+
return if @thread
|
|
32
|
+
|
|
33
|
+
@stop = false
|
|
34
|
+
@thread = Thread.new do
|
|
35
|
+
until @stop
|
|
36
|
+
tick
|
|
37
|
+
@clock.sleep(TICK_SECONDS)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def <<(content)
|
|
43
|
+
if enabled?
|
|
44
|
+
@mutex.synchronize { @backlog << content }
|
|
45
|
+
else
|
|
46
|
+
@io.print(content)
|
|
47
|
+
@io.flush
|
|
48
|
+
end
|
|
49
|
+
self
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The write sits inside the mutex so a concurrent tick can't reorder a drain.
|
|
53
|
+
def tick
|
|
54
|
+
@mutex.synchronize do
|
|
55
|
+
return if @backlog.empty?
|
|
56
|
+
|
|
57
|
+
@carry += [@backlog.length * BACKLOG_FRACTION_PER_TICK, MIN_CHARS_PER_TICK].max
|
|
58
|
+
count = @carry.floor
|
|
59
|
+
return if count.zero?
|
|
60
|
+
|
|
61
|
+
@carry -= count
|
|
62
|
+
@io.print(@backlog.slice!(0, count))
|
|
63
|
+
@io.flush
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Called before non-delta output so printed order matches stream order.
|
|
68
|
+
def drain
|
|
69
|
+
@mutex.synchronize do
|
|
70
|
+
return if @backlog.empty?
|
|
71
|
+
|
|
72
|
+
@io.print(@backlog.slice!(0, @backlog.length))
|
|
73
|
+
@io.flush
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Idempotent: the REPL's ensure path runs it even after a happy-path finish.
|
|
78
|
+
def finish
|
|
79
|
+
if @thread
|
|
80
|
+
@stop = true
|
|
81
|
+
@thread.join
|
|
82
|
+
@thread = nil
|
|
83
|
+
end
|
|
84
|
+
drain
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def enabled?
|
|
90
|
+
@theme.enabled && @io.respond_to?(:tty?) && @io.tty?
|
|
91
|
+
end
|
|
92
|
+
end
|
data/lib/riffer/rig/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: riffer-rig
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jake Bottrall
|
|
@@ -105,9 +105,8 @@ files:
|
|
|
105
105
|
- lib/riffer/rig/ui/animator.rb
|
|
106
106
|
- lib/riffer/rig/ui/banner.rb
|
|
107
107
|
- lib/riffer/rig/ui/palette.rb
|
|
108
|
-
- lib/riffer/rig/ui/pixels.rb
|
|
109
108
|
- lib/riffer/rig/ui/renderer.rb
|
|
110
|
-
- lib/riffer/rig/ui/
|
|
109
|
+
- lib/riffer/rig/ui/smoother.rb
|
|
111
110
|
- lib/riffer/rig/ui/theme.rb
|
|
112
111
|
- lib/riffer/rig/version.rb
|
|
113
112
|
homepage: https://github.com/bottrall/riffer-rig
|
data/lib/riffer/rig/ui/pixels.rb
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Each character cell stacks two vertical pixels via half-block glyphs (▀ top,
|
|
4
|
-
# ▄ bottom): the foreground colours the top pixel and the background the bottom,
|
|
5
|
-
# doubling vertical resolution. A disabled theme falls back to plain block
|
|
6
|
-
# glyphs so piped/non-colour output stays escape-free.
|
|
7
|
-
module Riffer::Rig::UI::Pixels
|
|
8
|
-
extend self
|
|
9
|
-
|
|
10
|
-
RESET = "\e[0m"
|
|
11
|
-
|
|
12
|
-
def render(sprite, palette, theme)
|
|
13
|
-
sprite.each_slice(2).map do |top, bottom|
|
|
14
|
-
render_row(top, bottom || [], palette, theme)
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
private
|
|
19
|
-
|
|
20
|
-
def render_row(top, bottom, palette, theme)
|
|
21
|
-
width = [top.length, bottom.length].max
|
|
22
|
-
(0...width).map { |x| cell(palette[top[x]], palette[bottom[x]], theme) }.join
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def cell(top_rgb, bottom_rgb, theme)
|
|
26
|
-
return ' ' if top_rgb.nil? && bottom_rgb.nil?
|
|
27
|
-
|
|
28
|
-
unless theme.enabled
|
|
29
|
-
return '█' if top_rgb && bottom_rgb
|
|
30
|
-
return '▀' if top_rgb
|
|
31
|
-
|
|
32
|
-
return '▄'
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
if top_rgb && bottom_rgb
|
|
36
|
-
"#{fg(top_rgb)}#{bg(bottom_rgb)}▀#{RESET}"
|
|
37
|
-
elsif top_rgb
|
|
38
|
-
"#{fg(top_rgb)}▀#{RESET}"
|
|
39
|
-
else
|
|
40
|
-
"#{fg(bottom_rgb)}▄#{RESET}"
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def fg(rgb) = "\e[38;2;#{rgb[0]};#{rgb[1]};#{rgb[2]}m"
|
|
45
|
-
|
|
46
|
-
def bg(rgb) = "\e[48;2;#{rgb[0]};#{rgb[1]};#{rgb[2]}m"
|
|
47
|
-
end
|
data/lib/riffer/rig/ui/riffy.rb
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# riffer-rig's mascot sprite, authored as a character grid (see LEGEND).
|
|
4
|
-
module Riffer::Rig::UI::Riffy
|
|
5
|
-
extend self
|
|
6
|
-
|
|
7
|
-
LEGEND = {
|
|
8
|
-
'.' => nil,
|
|
9
|
-
'D' => :outline, # deep purple outline / shadow / nose
|
|
10
|
-
'B' => :body, # purple fur
|
|
11
|
-
'M' => :shade, # mid purple (inner ear, jaw shadow, leg lines)
|
|
12
|
-
'L' => :light, # light lilac (snout, paw pads)
|
|
13
|
-
'K' => :shades, # near-black sunglasses
|
|
14
|
-
'W' => :glint # lens glint (intro only)
|
|
15
|
-
}.freeze
|
|
16
|
-
|
|
17
|
-
PALETTE = {
|
|
18
|
-
outline: [74, 46, 122],
|
|
19
|
-
body: [157, 92, 232],
|
|
20
|
-
shade: [120, 66, 190],
|
|
21
|
-
light: [212, 180, 250],
|
|
22
|
-
shades: [12, 10, 20],
|
|
23
|
-
glint: [240, 240, 255]
|
|
24
|
-
}.freeze
|
|
25
|
-
|
|
26
|
-
GRID = [
|
|
27
|
-
'...DDDD......DDDD...',
|
|
28
|
-
'..DBBBBD....DBBBBD..',
|
|
29
|
-
'..DBMMBD....DBMMBD..',
|
|
30
|
-
'..DBMMBD....DBMMBD..',
|
|
31
|
-
'..DBBDDDDDDDDDDBBD..',
|
|
32
|
-
'...DBBBBBBBBBBBBD...',
|
|
33
|
-
'...DKKKKKKKKKKKKD...',
|
|
34
|
-
'...DKKKKKBBKKKKKD...',
|
|
35
|
-
'...DBKKKBBBBKKKBD...',
|
|
36
|
-
'...DBBBBBDDBBBBBD...',
|
|
37
|
-
'...DBBBBLLLLBBBBD...',
|
|
38
|
-
'...DBBBMLLLLMBBBD...',
|
|
39
|
-
'....DBBBBBBBBBBD....',
|
|
40
|
-
'....DDMMMMMMMMDD....',
|
|
41
|
-
'.....DBBBBBBBBD.....',
|
|
42
|
-
'....DBBBBBBBBBBD....',
|
|
43
|
-
'...DBBBBBBBBBBBBD...',
|
|
44
|
-
'..DBBBBBBBBBBBBBBD..',
|
|
45
|
-
'.DBBBBBBBBBBBBBBBBD.',
|
|
46
|
-
'.DBBBBDBBDDBBDBBBBD.',
|
|
47
|
-
'.DLLLDDLLDDLLDDLLLD.'
|
|
48
|
-
].freeze
|
|
49
|
-
|
|
50
|
-
WIDTH = GRID.first.length
|
|
51
|
-
HEIGHT = (GRID.length + 1) / 2
|
|
52
|
-
|
|
53
|
-
LENS_ROWS = [6, 7, 8].freeze
|
|
54
|
-
GLINT_COLS = [4, 5, 6, 7, 8, 11, 12, 13, 14, 15].freeze
|
|
55
|
-
|
|
56
|
-
EQ_LEVELS = '▁▂▃▄▅▆▇█'.chars.freeze
|
|
57
|
-
EQ_BARS = 7
|
|
58
|
-
|
|
59
|
-
def render(theme, glint_col: nil)
|
|
60
|
-
sprite = GRID.map { |row| row.chars.map { |char| LEGEND.fetch(char) } }
|
|
61
|
-
apply_glint(sprite, glint_col) if glint_col
|
|
62
|
-
Riffer::Rig::UI::Pixels.render(sprite, PALETTE, theme)
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def equalizer(theme, tick)
|
|
66
|
-
bars = Array.new(EQ_BARS) do |i|
|
|
67
|
-
height = (Math.sin((tick + i) * 0.6).abs * (EQ_LEVELS.length - 1)).round
|
|
68
|
-
bar = EQ_LEVELS[height]
|
|
69
|
-
i.even? ? theme.cyan(bar) : theme.magenta(bar)
|
|
70
|
-
end
|
|
71
|
-
"#{bars.join} #{theme.grey('riffing…')}"
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
private
|
|
75
|
-
|
|
76
|
-
def apply_glint(sprite, column)
|
|
77
|
-
LENS_ROWS.each do |row|
|
|
78
|
-
sprite[row][column] = :glint if sprite[row][column] == :shades
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
end
|