dopairb 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7e71c4e0e91c35575f8ac0ebc641873bc4fc5af8dfc78b3ab4b17aa2b9ab3057
4
+ data.tar.gz: df276b5e5997edf7eaf128f022c6475a39b8db2949317e64db718bc8c91bf15a
5
+ SHA512:
6
+ metadata.gz: 1fed2568ae4137a3da8d88c4d7ce74cf79ec6fba2f82d5d823a5cae1e063f91d35363910f7b68e3e154de0491b249e6125c1eea72c45fd6d399ed0b86244c055
7
+ data.tar.gz: 7a117f3e7a8ae10726a56b3469c8580fe7e68e0a1cad2a4c1b145ea8e480ff35d1a90ccd14e37a249e3078da1d095998b1c55c0e14ddd56ed1ae3e3917493387
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Koichi Sasada
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,190 @@
1
+ # dopairb
2
+
3
+ An interactive Ruby shell that answers every keystroke, evaluation, result and exception with loud terminal effects.
4
+ It is built as an IRB extension. The design spec is [spec.md](spec.md) (in Japanese).
5
+
6
+ ```
7
+ dopairb(main):001> (1..100).sum COMBO 00 SCORE 33 [⣿⣿⣀⣀⣀⣀⣀⣀] x1.5
8
+ ⠂⠌⠟⠃ <- sparks, afterglow and HUD on every keystroke
9
+
10
+ █████ ███ ████ ████ █████ █ █ ███ █████ █
11
+ █ █ █ █ █ █ █ █ █ █ █ <- big moments: drop, impact, shockwave, fireworks
12
+ ████ █ ████ ███ █ █████ █ █ █
13
+ ✦ FIRST HIT! ✦ +252 <- optional one-line badge left behind
14
+ => 5050 <- IRB's normal result output, untouched
15
+ ```
16
+
17
+ ## Install
18
+
19
+ dopairb is not on rubygems.org yet. Install it from the repository:
20
+
21
+ ```console
22
+ $ git clone https://github.com/ko1/dopairb
23
+ $ cd dopairb
24
+ $ gem build dopairb.gemspec && gem install ./dopairb-*.gem
25
+ ```
26
+
27
+ To try it without installing, run `ruby exe/dopairb` in the checkout.
28
+
29
+ ## Usage
30
+
31
+ ```console
32
+ $ dopairb # start it (IRB options are passed through)
33
+ $ dopairb --calm # quieter: low intensity, no flash, shorter effects
34
+ $ dopairb --max # everything at maximum; the biggest moments go full screen
35
+ $ dopairb --party # max + full-screen flash + bell
36
+ $ dopairb --no-flash # suppress flashing, available from the very first start
37
+ $ DOPAIRB="intensity=low,flash=off" dopairb
38
+ ```
39
+
40
+ To enable it in your regular `irb`, add this to `~/.irbrc`:
41
+
42
+ ```ruby
43
+ require "dopairb"
44
+ Dopairb.enable # also e.g. Dopairb.enable(flash: :off, intensity: :low)
45
+ ```
46
+
47
+ Inside a session, the `dopa` command changes settings:
48
+
49
+ ```
50
+ dopa show settings and session stats
51
+ dopa off|low|normal|max
52
+ dopa calm / dopa party
53
+ dopa demo play every effect once (does not change your score)
54
+ dopa flash=off duration=0.5 ...
55
+ ```
56
+
57
+ ### Settings
58
+
59
+ | Setting | Values | Default | Meaning |
60
+ | --- | --- | --- | --- |
61
+ | `intensity` | off / low / normal / max | normal | Overall amount. `off` behaves exactly like plain IRB |
62
+ | `motion` | on / off | on | `off` disables animation (badges only) |
63
+ | `flash` | off / soft / full | soft | `soft` flashes only the effect area, `full` inverts the whole screen |
64
+ | `sound` | on / off | off | Terminal bell on big moments |
65
+ | `hud` | on / off | on | COMBO / SCORE / CHARGE on the right of the prompt line |
66
+ | `duration` | 0.1–5 | 1.0 | Length multiplier for every effect |
67
+ | `color` | auto / truecolor / 256 / 16 / none | auto | `none` when `NO_COLOR` is set |
68
+ | `trail` | none / big / all | big | Which effects leave a one-line badge |
69
+ | `charge` | 0–0.3 s | 0.12 | Wind-up before a fast result lands; 0 disables it |
70
+ | `intro` | on / off | on | Title animation at startup |
71
+ | `keys` | on / off | on | Per-keystroke effects |
72
+
73
+ ## Effects
74
+
75
+ Effects come in four strengths: **small** (every key), **medium** (syntactic moments), **large** (evaluation finished) and **huge** (records and comebacks).
76
+ When several fire at once, only the biggest plays. The smaller ones are folded into its subtitle rather than queued.
77
+
78
+ | Moment | Effect |
79
+ | --- | --- |
80
+ | Typing | A spark trail right of the cursor, a brief glow on the typed character, embers falling below, a x1.0–x3.0 multiplier |
81
+ | Resuming after a pause | Reignites with `CHARGE!` |
82
+ | Backspace / Delete | The deleted character shatters and falls. The next keystroke shows `RECOVERY` |
83
+ | Cursor movement | A light streak in the direction of travel |
84
+ | Closing a bracket | The matching pair pulses, `NICE!` |
85
+ | Closing a string or block | The range is highlighted, `end` shows `SEALED!` |
86
+ | Accepting a completion | A light sweeps across the completed part, `<< LOCK ON` |
87
+ | History recall | The line appears behind a scanline, `<< REWIND` |
88
+ | Continuing a multi-line input | `CHARGE LV2`… |
89
+ | Paste | One `PASTE x120` popup, no per-character effects |
90
+ | Enter | A hot band sweeps across the input line ("EXECUTE"). Evaluations longer than 0.25 s show `CHARGING`, and after 3 s a calm elapsed-time readout |
91
+ | Ordinary value | A comet flies from `=>` and lands, `+SCORE` `COMBO n` |
92
+ | `nil` | A swing and a miss (`~~~`) and a puff of smoke |
93
+ | `true` / `false` | A `YES!` / `NO!` stamp slams down |
94
+ | Big numbers (≥10,000) | Giant digits count up, then a shockwave |
95
+ | Long String / Array / Hash | Characters pour in / elements pop into place one by one |
96
+ | `def` / `class` / `module` | Unlocks like `NEW ABILITY` |
97
+ | 20+ lines of stdout | `OUTPUT RAIN` digital rain |
98
+ | SyntaxError | The input line shakes and cracks, `SYNTAX BREAK`, with the error position |
99
+ | NameError | A spotlight on that name in your input, `UNKNOWN SYMBOL` |
100
+ | NoMethodError | The link between receiver and method snaps |
101
+ | TypeError / ArgumentError | The two sides collide and explode (`TYPE CLASH` / `ARGUMENT CLASH`) |
102
+ | Other exceptions | A red flash and a glitching `FAILED` |
103
+ | Ctrl-C | The stored charge scatters, `INTERRUPTED` |
104
+ | First eval / COMBO 5, 10, 25… / eval count 10, 50, 100… | `FIRST HIT!` / `COMBO 10!` / `100 EVALS` |
105
+ | Numeric record (more than doubled) | `NEW RECORD` |
106
+ | Success right after an error | `FIXED!` (after one error) / `COMEBACK!` (after a streak), both huge |
107
+ | Exit | A result screen that counts up, left behind as a plain table |
108
+
109
+ Error effects name the kind of error. They never make fun of the failure.
110
+
111
+ ## Readable, never broken
112
+
113
+ - IRB evaluates and prints as usual. `=> value`, exception messages and backtraces come from IRB itself.
114
+ Effects are drawn in a temporary area and cleared. Only your input, the program's output and the result (plus an optional one-line badge) stay in the scrollback.
115
+ - dopairb never calls your `#inspect` / `#to_s` for its effects. It classifies values only with `Module#===` and `bind_call` on built-in methods.
116
+ - When your program writes to stdout or stderr during an evaluation, the waiting display and any effect get out of the way immediately. While a `system` / `spawn` / `IO.popen` child might write straight to the terminal, nothing is decorated.
117
+ - Pressing a key during an effect ends it at once, and the key goes to Reline as usual. Per-keystroke effects pause while you paste.
118
+ - Non-interactive runs (pipes, redirects), `TERM=dumb` and `intensity=off` emit no control sequences at all. `NO_COLOR` emits no color codes.
119
+ - On narrow terminals, big-font banners fall back to spaced-out one-line text, then to badges only. On terminals where East Asian Ambiguous characters are two columns wide, box-drawing and block characters are replaced with ASCII or braille.
120
+ - An exception inside an effect never reaches the REPL. Set `DOPAIRB_DEBUG=path` to log it.
121
+
122
+ ## Implementation
123
+
124
+ dopairb extends IRB and has no evaluation engine of its own. All dependencies on IRB and Reline internals live in two files.
125
+
126
+ - `lib/dopairb/reline_adapter.rb` (Reline 0.5–0.7, tested with 0.6.3)
127
+ - Public API: `Reline.add_dialog_proc` (three dialogs: HUD, spark trail, effect strip) and `output_modifier_proc` (glow on the input).
128
+ - Internals:
129
+ - `LineEditor#input_key`: before/after comparison of each key.
130
+ - `#handle_signal`: Reline calls it every 10 ms while waiting for input, so it serves as the animation tick.
131
+ - `#render`.
132
+ - `#render_finished`: records how the submitted input looks on screen.
133
+ - `Core#readmultiline`.
134
+ - `lib/dopairb/irb_adapter.rb` (IRB 1.14–1.x, tested with 1.18.0)
135
+ - Public API: `IRB::Command.register(:dopa)` and `IRB.conf[:AT_EXIT]`.
136
+ - Internals: `IRB::Context#evaluate` (around each statement) and `IRB::WorkSpace#filter_backtrace` (hides dopairb's own frames).
137
+
138
+ The rest is terminal-independent logic:
139
+
140
+ - `Game`: COMBO / CHARGE / SCORE.
141
+ - `Probe`: safe classification of values and exceptions.
142
+ - `InputFx`: per-keystroke effects.
143
+ - `Director` and the scenes in `scenes/`.
144
+ - `Canvas`: a cell grid with a braille sub-pixel layer.
145
+ - `Stage`: borrows a few rows at the cursor, draws, and restores the input line exactly.
146
+
147
+ ### Open questions in the spec (§11) and what was decided
148
+
149
+ - The HUD is not pinned to the top of the screen. It sits at the right end of the prompt line while typing and disappears when the input is submitted.
150
+ - Output printed during evaluation (`puts` etc.) is not decorated. Its lines are only counted, to trigger `OUTPUT RAIN`.
151
+ - Scores live for the session only and are not saved.
152
+ - Effects use Unicode. Any glyph that is not one column wide is swapped for ASCII or braille at runtime.
153
+ - Start it with the `dopairb` command, or call `Dopairb.enable` in `.irbrc`.
154
+ - IRB sessions entered via `binding.irb` get the same Context hook. The result screen appears only when the outermost session ends.
155
+
156
+ ## Performance
157
+
158
+ These are rough numbers from a local machine, not a formal benchmark.
159
+ Processing one key (`LineEditor#update` + `render`) takes about 3 ms in plain IRB and about 5.6 ms in dopairb.
160
+ With keys typed 30 ms apart, both follow the input within 1 ms.
161
+ While an effect is running, the screen is redrawn at up to 30 fps, about 1 ms per frame.
162
+
163
+ ## Development
164
+
165
+ ```console
166
+ $ bundle exec rake test # unit tests + every scene x width x color depth + PTY integration tests
167
+ ```
168
+
169
+ The integration tests (`test/test_integration.rb`) start dopairb on a pseudo terminal. They rebuild the screen with a small VT100 model (`test/support/vt.rb`) and check the acceptance criteria of spec §10:
170
+
171
+ - results and error messages remain as plain text
172
+ - fast typing and pastes do not change the input
173
+ - Ctrl-C works
174
+ - narrow terminals work
175
+ - `NO_COLOR` emits no color codes
176
+ - non-interactive runs contain no control sequences
177
+ - `intensity=off` behaves as plain IRB
178
+
179
+ They require `setsid`.
180
+
181
+ ## Known limitations
182
+
183
+ - Output that does not come from a child process (a C extension writing to fd 1 directly, `$stdout.reopen`, …) cannot be detected and may mix with the waiting display during an evaluation.
184
+ - When a multi-line input grows at the bottom of the screen, the effect strip below the input disappears (the HUD and spark trail remain). The strip is also hidden while completion candidates are shown.
185
+ - When IRB shows a long result in a pager (`less`), the effect finishes before the pager starts.
186
+ - If the terminal is resized in the middle of an effect, the rest of that effect may be garbled. The next effect uses the new width.
187
+
188
+ ## License
189
+
190
+ MIT
data/exe/dopairb ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/dopairb/cli"
5
+
6
+ Dopairb::CLI.start(ARGV)
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dopairb
4
+ # A w x h grid of cells with an optional braille sub-pixel layer
5
+ # (2x4 dots per cell). Rendered to SGR-colored strings, one per row.
6
+ class Canvas
7
+ BRAILLE = [[0x01, 0x08], [0x02, 0x10], [0x04, 0x20], [0x40, 0x80]].freeze
8
+ CONT = :cont
9
+
10
+ attr_reader :w, :h
11
+
12
+ def initialize(w, h)
13
+ @w = w
14
+ @h = h
15
+ @ch = Array.new(h) { Array.new(w) }
16
+ @fg = Array.new(h) { Array.new(w) }
17
+ @bg = Array.new(h) { Array.new(w) }
18
+ @bold = Array.new(h) { Array.new(w, false) }
19
+ @dots = Array.new(h) { Array.new(w, 0) }
20
+ @dotc = Array.new(h) { Array.new(w) }
21
+ end
22
+
23
+ def put(x, y, str, fg = nil, bg: nil, bold: false)
24
+ x = x.round
25
+ y = y.round
26
+ return x if y < 0 || y >= @h
27
+ str.each_char do |c|
28
+ cw = c.ord < 0x80 ? 1 : Term.char_width(c)
29
+ if cw <= 0
30
+ next
31
+ elsif cw == 2
32
+ if x >= 0 && x + 1 < @w
33
+ set(x, y, c, fg, bg, bold)
34
+ set(x + 1, y, CONT, fg, bg, bold)
35
+ end
36
+ elsif x >= 0 && x < @w
37
+ set(x, y, c, fg, bg, bold)
38
+ end
39
+ x += cw
40
+ end
41
+ x
42
+ end
43
+
44
+ def char_at(x, y)
45
+ return nil unless x >= 0 && y >= 0 && x < @w && y < @h
46
+ @ch[y][x]
47
+ end
48
+
49
+ def put_center(y, str, fg = nil, bg: nil, bold: false)
50
+ put((@w - Term.str_width(str)) / 2, y, str, fg, bg: bg, bold: bold)
51
+ end
52
+
53
+ def bg(x, y, color)
54
+ x = x.round
55
+ y = y.round
56
+ return unless x >= 0 && y >= 0 && x < @w && y < @h
57
+ @bg[y][x] = color
58
+ end
59
+
60
+ def fill_bg(color, x0 = 0, y0 = 0, w = @w, h = @h)
61
+ (y0...(y0 + h)).each do |y|
62
+ next if y < 0 || y >= @h
63
+ (x0...(x0 + w)).each do |x|
64
+ @bg[y][x] = color if x >= 0 && x < @w
65
+ end
66
+ end
67
+ end
68
+
69
+ # Blend the existing background towards color by k (0..1). Unset bg is black.
70
+ def tint_bg(color, k, x0 = 0, y0 = 0, w = @w, h = @h)
71
+ (y0...(y0 + h)).each do |y|
72
+ next if y < 0 || y >= @h
73
+ (x0...(x0 + w)).each do |x|
74
+ next if x < 0 || x >= @w
75
+ @bg[y][x] = Color.mix(@bg[y][x] || [0, 0, 0], color, k)
76
+ end
77
+ end
78
+ end
79
+
80
+ # Sub-pixel dot; px in 0...(w*2), py in 0...(h*4)
81
+ def dot(px, py, color)
82
+ px = px.floor
83
+ py = py.floor
84
+ return if px < 0 || py < 0
85
+ x = px >> 1
86
+ y = py >> 2
87
+ return if x >= @w || y >= @h
88
+ @dots[y][x] |= BRAILLE[py & 3][px & 1]
89
+ @dotc[y][x] = color
90
+ end
91
+
92
+ def line(x0, y0, x1, y1, color)
93
+ steps = [((x1 - x0) * 2).abs, ((y1 - y0) * 4).abs, 1].max.ceil
94
+ (0..steps).each do |i|
95
+ t = i.fdiv(steps)
96
+ dot((x0 + (x1 - x0) * t) * 2, (y0 + (y1 - y0) * t) * 4, color)
97
+ end
98
+ end
99
+
100
+ # Ellipse in cell units; ry is visually halved by the cell aspect.
101
+ def ring(cx, cy, r, color, density: 1.0)
102
+ return if r <= 0
103
+ n = [(r * 12 * density).ceil, 8].max
104
+ n.times do |i|
105
+ a = i * 2 * Math::PI / n
106
+ dot((cx + Math.cos(a) * r) * 2, (cy + Math.sin(a) * r * 0.5) * 4, color)
107
+ end
108
+ end
109
+
110
+ def render(depth, shake: 0)
111
+ Array.new(@h) { |y| render_row(y, depth, shake) }
112
+ end
113
+
114
+ private
115
+
116
+ def set(x, y, c, fg, bg, bold)
117
+ @ch[y][x] = c
118
+ @fg[y][x] = fg
119
+ @bg[y][x] = bg if bg
120
+ @bold[y][x] = bold
121
+ end
122
+
123
+ def render_row(y, depth, shake)
124
+ chs = @ch[y]
125
+ last = @w - 1
126
+ last -= 1 while last >= 0 && chs[last].nil? && @dots[y][last] == 0 && @bg[y][last].nil?
127
+ out = +""
128
+ cur = nil
129
+ if shake > 0
130
+ out << (" " * shake)
131
+ end
132
+ start = shake < 0 ? -shake : 0
133
+ limit = shake > 0 ? [last, @w - 1 - shake].min : last
134
+ x = start
135
+ while x <= limit
136
+ c = chs[x]
137
+ if c == CONT
138
+ x += 1
139
+ next
140
+ end
141
+ fg = @fg[y][x]
142
+ bold = @bold[y][x]
143
+ if c.nil?
144
+ if @dots[y][x] != 0
145
+ c = Term.utf8? ? (0x2800 + @dots[y][x]).chr(Encoding::UTF_8) : "."
146
+ fg = @dotc[y][x]
147
+ bold = false
148
+ else
149
+ c = " "
150
+ end
151
+ end
152
+ style = [fg, @bg[y][x], bold]
153
+ if style != cur
154
+ out << (depth == :none && !bold && @bg[y][x].nil? ? "\e[0m" : Color.sgr(fg, @bg[y][x], bold, depth))
155
+ cur = style
156
+ end
157
+ out << c
158
+ x += 1
159
+ end
160
+ out << "\e[0m" if cur
161
+ out
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dopairb
4
+ # Shown on the line below the input while an evaluation keeps running:
5
+ # an energetic charge at first, then a calm elapsed-time readout.
6
+ class ChargeLine
7
+ DELAY = 0.25
8
+ CALM_AFTER = 3.0
9
+
10
+ attr_reader :owner
11
+
12
+ def initialize(config, charge: 0.0)
13
+ @config = config
14
+ @charge = charge
15
+ @q = Thread::Queue.new
16
+ @drawn = false
17
+ @dead = false
18
+ @shown = false
19
+ @owner = nil
20
+ end
21
+
22
+ def shown? = @shown
23
+
24
+ def start
25
+ @t0 = now
26
+ @thread = Thread.new do
27
+ Thread.current.report_on_exception = false
28
+ @owner = Thread.current
29
+ run
30
+ end
31
+ self
32
+ end
33
+
34
+ def stop
35
+ @q << :stop
36
+ @thread&.join(0.5)
37
+ Term::LOCK.synchronize do
38
+ clear
39
+ @dead = true
40
+ Term.overlay = nil if Term.overlay.equal?(self)
41
+ end
42
+ end
43
+
44
+ # user output is coming (called under Term::LOCK)
45
+ def yield_to_output
46
+ clear
47
+ @dead = true
48
+ Term.overlay = nil if Term.overlay.equal?(self)
49
+ end
50
+
51
+ private
52
+
53
+ def run
54
+ return if @q.pop(timeout: DELAY)
55
+ Term::LOCK.synchronize do
56
+ return if @dead || OutputTap.output?
57
+ Term.overlay = self
58
+ end
59
+ depth = Term.depth(@config)
60
+ spin = %w[⣾ ⣽ ⣻ ⢿ ⡿ ⣟ ⣯ ⣷].map { |g| Term.glyph(g, "|") }
61
+ spin = %w[| / - \\] if spin.uniq.size == 1
62
+ i = 0
63
+ loop do
64
+ el = now - @t0
65
+ Term::LOCK.synchronize do
66
+ return if @dead
67
+ Term.out.write("\r\e[2K#{line(el, i, spin, depth)}\e[0m\r")
68
+ @drawn = true
69
+ @shown = true
70
+ end
71
+ i += 1
72
+ break if @q.pop(timeout: el < CALM_AFTER ? 1 / 15.0 : 0.5)
73
+ end
74
+ end
75
+
76
+ def line(el, i, spin, depth)
77
+ cols = Term.cols - 2
78
+ if el < CALM_AFTER || !@config.motion
79
+ k = (el - DELAY) / (CALM_AFTER - DELAY)
80
+ cells = [[cols - 30, 24].min, 4].max
81
+ filled = (cells * k.clamp(0, 1)).round
82
+ bar = +""
83
+ cells.times do |x|
84
+ on = x < filled
85
+ col = on ? Color.ramp(Color::FIRE, 0.5 - 0.4 * ((x - i) % 6) / 6.0) : [60, 60, 70]
86
+ bar << Color.sgr(col, nil, on, depth) << Term.glyph(on ? "⣿" : "⣀", on ? "#" : ".")
87
+ end
88
+ head = Color.sgr(Color.rainbow(el), nil, true, depth)
89
+ "#{head}#{spin[i % spin.size]} CHARGING\e[0m #{bar}\e[0m #{Color.sgr([255, 220, 150], nil, true, depth)}#{format('%.1fs', el)}"[0, cols * 30]
90
+ else
91
+ "#{Color.sgr([110, 130, 170], nil, false, depth)}#{spin[i % spin.size]} running #{clock(el)} (Ctrl-C to interrupt)"
92
+ end
93
+ end
94
+
95
+ def clear
96
+ return unless @drawn
97
+ Term.out.write("\r\e[2K")
98
+ @drawn = false
99
+ end
100
+
101
+ def clock(el)
102
+ el < 60 ? format("%.1fs", el) : format("%dm%02ds", el / 60, el % 60)
103
+ end
104
+
105
+ def now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
106
+ end
107
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+ require_relative "../dopairb"
5
+
6
+ module Dopairb
7
+ # `dopairb [dopairb options] [--] [irb options]`
8
+ module CLI
9
+ module_function
10
+
11
+ def parser(settings)
12
+ OptionParser.new do |o|
13
+ o.require_exact = true
14
+ o.banner = "Usage: dopairb [options] [-- irb options]"
15
+ o.separator ""
16
+ o.separator "IRB with fireworks. Every other option is handed to IRB."
17
+ o.separator ""
18
+ o.on("--intensity=LEVEL", Config::INTENSITIES.map(&:to_s), "off / low / normal (default) / max") { |v| settings << [:intensity, v] }
19
+ o.on("--calm", "low intensity, no flash, shorter effects") { settings.concat([[:intensity, :low], [:flash, :off], [:duration, 0.7]]) }
20
+ o.on("--max", "everything at maximum") { settings << [:intensity, :max] }
21
+ o.on("--party", "max + full-screen flash + bell") { settings.concat([[:intensity, :max], [:flash, :full], [:sound, true]]) }
22
+ o.on("--[no-]flash[=MODE]", "flash: off / soft (default) / full") { |v| settings << [:flash, v.nil? ? :soft : (v == false ? :off : v)] }
23
+ o.on("--[no-]motion", "animations (default on)") { |v| settings << [:motion, v] }
24
+ o.on("--[no-]sound", "terminal bell on big moments (default off)") { |v| settings << [:sound, v] }
25
+ o.on("--[no-]hud", "COMBO / SCORE next to the prompt (default on)") { |v| settings << [:hud, v] }
26
+ o.on("--[no-]keys", "per-keystroke effects (default on)") { |v| settings << [:keys, v] }
27
+ o.on("--[no-]intro", "title animation (default on)") { |v| settings << [:intro, v] }
28
+ o.on("--duration=X", Float, "effect length multiplier") { |v| settings << [:duration, v] }
29
+ o.on("--set=K=V,...", "any setting, same format as $DOPAIRB") { |v| settings << [:string, v] }
30
+ o.on("--version", "print version") do
31
+ puts "dopairb #{VERSION} (irb #{IRB::VERSION}, reline #{Reline::VERSION})"
32
+ exit
33
+ end
34
+ o.on("-h", "--help", "this help") do
35
+ puts o
36
+ puts
37
+ puts "Settings (also via DOPAIRB=\"key=value,...\" or `dopa key=value` inside the session):"
38
+ puts Config.new.describe
39
+ exit
40
+ end
41
+ end
42
+ end
43
+
44
+ def split_args(argv)
45
+ return [argv[0...argv.index("--")], argv[(argv.index("--") + 1)..]] if argv.include?("--")
46
+ argv.partition { |a| ours?(a) }
47
+ end
48
+
49
+ def ours?(arg)
50
+ return true if %w[-h --help --version].include?(arg)
51
+ return false unless arg.start_with?("--")
52
+ parser([]).parse([arg])
53
+ true
54
+ rescue OptionParser::InvalidOption, OptionParser::AmbiguousOption
55
+ false
56
+ rescue OptionParser::ParseError
57
+ true
58
+ end
59
+
60
+ def start(argv = ARGV)
61
+ ours, irb_args = split_args(argv.dup)
62
+ settings = []
63
+ parser(settings).parse!(ours)
64
+ settings.each do |k, v|
65
+ k == :string ? Dopairb.config.apply_string(v) : Dopairb.config.set(k, v)
66
+ end
67
+ run_irb(irb_args)
68
+ rescue OptionParser::ParseError, ArgumentError => e
69
+ warn "dopairb: #{e.message}"
70
+ exit 2
71
+ end
72
+
73
+ def run_irb(irb_args)
74
+ STDOUT.sync = true
75
+ $0 = "dopairb"
76
+ IRB.setup(nil, argv: irb_args)
77
+ Dopairb.enable
78
+ IRB.conf[:PROMPT][:DOPAIRB] = {
79
+ PROMPT_I: "dopairb(%m):%03n> ",
80
+ PROMPT_S: "dopairb(%m):%03n%l ",
81
+ PROMPT_C: "dopairb(%m):%03n* ",
82
+ RETURN: "=> %s\n",
83
+ }
84
+ IRB.conf[:PROMPT_MODE] = :DOPAIRB if IRB.conf[:PROMPT_MODE] == :DEFAULT
85
+ irb = IRB::Irb.new
86
+ Dopairb.session.intro
87
+ irb.run(IRB.conf)
88
+ end
89
+ end
90
+ end