spotify_cli 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 +7 -0
- data/.DS_Store +0 -0
- data/.gitignore +10 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/README.md +101 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/bin/spotify +5 -0
- data/lib/dex/ui/README.md +20 -0
- data/lib/dex/ui/ansi.rb +48 -0
- data/lib/dex/ui/box.rb +15 -0
- data/lib/dex/ui/color.rb +57 -0
- data/lib/dex/ui/formatter.rb +155 -0
- data/lib/dex/ui/frame.rb +166 -0
- data/lib/dex/ui/glyph.rb +49 -0
- data/lib/dex/ui/progress.rb +19 -0
- data/lib/dex/ui/prompt.rb +121 -0
- data/lib/dex/ui/spinner.rb +168 -0
- data/lib/dex/ui/stdout_router.rb +186 -0
- data/lib/dex/ui/terminal.rb +18 -0
- data/lib/dex/ui.rb +83 -0
- data/lib/helpers/doc.rb +62 -0
- data/lib/spotify_cli/.DS_Store +0 -0
- data/lib/spotify_cli/api.rb +182 -0
- data/lib/spotify_cli/app.rb +101 -0
- data/lib/spotify_cli/version.rb +3 -0
- data/lib/spotify_cli.rb +38 -0
- data/spotify_cli.gemspec +36 -0
- metadata +118 -0
data/lib/dex/ui/frame.rb
ADDED
@@ -0,0 +1,166 @@
|
|
1
|
+
require 'dex/ui'
|
2
|
+
|
3
|
+
module Dex
|
4
|
+
module UI
|
5
|
+
module Frame
|
6
|
+
class << self
|
7
|
+
DEFAULT_FRAME_COLOR = Dex::UI.resolve_color(:cyan)
|
8
|
+
|
9
|
+
# Can be invoked in two ways: block and blockless
|
10
|
+
# In block form, the frame is closed automatically when the block returns
|
11
|
+
# In blockless form, caller MUST call Frame.close when the frame is
|
12
|
+
# logically done.
|
13
|
+
# blockless form is strongly discouraged in cases where block form can be
|
14
|
+
# made to work.
|
15
|
+
def open(
|
16
|
+
text,
|
17
|
+
color: DEFAULT_FRAME_COLOR,
|
18
|
+
failure_text: nil,
|
19
|
+
success_text: nil,
|
20
|
+
timing: nil
|
21
|
+
)
|
22
|
+
color = Dex::UI.resolve_color(color)
|
23
|
+
|
24
|
+
unless block_given?
|
25
|
+
if failure_text
|
26
|
+
raise ArgumentError, "failure_text is not compatible with blockless invocation"
|
27
|
+
elsif success_text
|
28
|
+
raise ArgumentError, "success_text is not compatible with blockless invocation"
|
29
|
+
elsif !timing.nil?
|
30
|
+
raise ArgumentError, "timing is not compatible with blockless invocation"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
timing = true if timing.nil?
|
35
|
+
|
36
|
+
t_start = Time.now.to_f
|
37
|
+
Dex::UI.raw do
|
38
|
+
puts edge(text, color: color, first: Dex::UI::Box::Heavy::TL)
|
39
|
+
end
|
40
|
+
FrameStack.push(color)
|
41
|
+
|
42
|
+
return unless block_given?
|
43
|
+
|
44
|
+
begin
|
45
|
+
success = false
|
46
|
+
success = yield
|
47
|
+
rescue
|
48
|
+
t_diff = timing ? (Time.now.to_f - t_start) : nil
|
49
|
+
close(failure_text, color: :red, elapsed: t_diff)
|
50
|
+
raise
|
51
|
+
else
|
52
|
+
t_diff = timing ? (Time.now.to_f - t_start) : nil
|
53
|
+
if success != false
|
54
|
+
close(success_text, color: color, elapsed: t_diff)
|
55
|
+
else
|
56
|
+
close(failure_text, color: :red, elapsed: t_diff)
|
57
|
+
end
|
58
|
+
success
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def close(text, color: DEFAULT_FRAME_COLOR, elapsed: nil)
|
63
|
+
color = Dex::UI.resolve_color(color)
|
64
|
+
|
65
|
+
FrameStack.pop
|
66
|
+
kwargs = {}
|
67
|
+
if elapsed
|
68
|
+
kwargs[:right_text] = "(#{elapsed.round(2)}s)"
|
69
|
+
end
|
70
|
+
Dex::UI.raw do
|
71
|
+
puts edge(text, color: color, first: Dex::UI::Box::Heavy::BL, **kwargs)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def divider(text, color: nil)
|
76
|
+
color = Dex::UI.resolve_color(color)
|
77
|
+
item = Dex::UI.resolve_color(FrameStack.pop)
|
78
|
+
|
79
|
+
Dex::UI.raw do
|
80
|
+
puts edge(text, color: (color || item), first: Dex::UI::Box::Heavy::DIV)
|
81
|
+
end
|
82
|
+
FrameStack.push(item)
|
83
|
+
end
|
84
|
+
|
85
|
+
def prefix(color: nil)
|
86
|
+
pfx = String.new
|
87
|
+
items = FrameStack.items
|
88
|
+
items[0..-2].each do |item|
|
89
|
+
pfx << Dex::UI.resolve_color(item).code << Dex::UI::Box::Heavy::VERT
|
90
|
+
end
|
91
|
+
if item = items.last
|
92
|
+
c = Thread.current[:dexui_frame_color_override] || color || item
|
93
|
+
pfx << Dex::UI.resolve_color(c).code \
|
94
|
+
<< Dex::UI::Box::Heavy::VERT << ' ' << Dex::UI::Color::RESET.code
|
95
|
+
end
|
96
|
+
pfx
|
97
|
+
end
|
98
|
+
|
99
|
+
def with_frame_color_override(color)
|
100
|
+
prev = Thread.current[:dexui_frame_color_override]
|
101
|
+
Thread.current[:dexui_frame_color_override] = color
|
102
|
+
yield
|
103
|
+
ensure
|
104
|
+
Thread.current[:dexui_frame_color_override] = prev
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def edge(text, color: raise, first: raise, right_text: nil)
|
110
|
+
color = Dex::UI.resolve_color(color)
|
111
|
+
text = Dex::UI.resolve_text("{{#{color.name}:#{text}}}")
|
112
|
+
|
113
|
+
prefix = String.new
|
114
|
+
FrameStack.items.each do |item|
|
115
|
+
prefix << Dex::UI.resolve_color(item).code << Dex::UI::Box::Heavy::VERT
|
116
|
+
end
|
117
|
+
prefix << color.code << first << (Dex::UI::Box::Heavy::HORZ * 2)
|
118
|
+
text ||= ''
|
119
|
+
unless text.empty?
|
120
|
+
prefix << ' ' << text << ' '
|
121
|
+
end
|
122
|
+
|
123
|
+
suffix = String.new
|
124
|
+
if right_text
|
125
|
+
suffix << ' ' << right_text << ' ' << color.code << (Dex::UI::Box::Heavy::HORZ * 2)
|
126
|
+
end
|
127
|
+
|
128
|
+
textwidth = Dex::UI::ANSI.printing_width(prefix + suffix)
|
129
|
+
termwidth = Dex::UI::Terminal.width
|
130
|
+
termwidth = 30 if termwidth < 30
|
131
|
+
|
132
|
+
if textwidth > termwidth
|
133
|
+
suffix = ''
|
134
|
+
prefix = prefix[0...termwidth]
|
135
|
+
textwidth = termwidth
|
136
|
+
end
|
137
|
+
padwidth = termwidth - textwidth
|
138
|
+
pad = Dex::UI::Box::Heavy::HORZ * padwidth
|
139
|
+
|
140
|
+
prefix + color.code + pad + suffix + Dex::UI::Color::RESET.code + "\n"
|
141
|
+
end
|
142
|
+
|
143
|
+
module FrameStack
|
144
|
+
ENVVAR = 'DEX_FRAME_STACK'
|
145
|
+
|
146
|
+
def self.items
|
147
|
+
ENV.fetch(ENVVAR, '').split(':').map(&:to_sym)
|
148
|
+
end
|
149
|
+
|
150
|
+
def self.push(item)
|
151
|
+
curr = items
|
152
|
+
curr << item.name
|
153
|
+
ENV[ENVVAR] = curr.join(':')
|
154
|
+
end
|
155
|
+
|
156
|
+
def self.pop
|
157
|
+
curr = items
|
158
|
+
ret = curr.pop
|
159
|
+
ENV[ENVVAR] = curr.join(':')
|
160
|
+
ret.nil? ? nil : ret.to_sym
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
data/lib/dex/ui/glyph.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'dex/ui'
|
2
|
+
|
3
|
+
module Dex
|
4
|
+
module UI
|
5
|
+
class Glyph
|
6
|
+
MAP = {}
|
7
|
+
|
8
|
+
attr_reader :handle, :codepoint, :color, :char, :to_s, :fmt
|
9
|
+
def initialize(handle, codepoint, color)
|
10
|
+
@handle = handle
|
11
|
+
@codepoint = codepoint
|
12
|
+
@color = color
|
13
|
+
@char = [codepoint].pack('U')
|
14
|
+
@to_s = color.code + char + Color::RESET.code
|
15
|
+
@fmt = "{{#{color.name}:#{char}}}"
|
16
|
+
|
17
|
+
MAP[handle] = self
|
18
|
+
end
|
19
|
+
|
20
|
+
STAR = new('*', 0x2b51, Color::YELLOW) # BLACK SMALL STAR
|
21
|
+
INFO = new('i', 0x1d4be, Color::BLUE) # MATHEMATICAL SCRIPT SMALL I
|
22
|
+
QUESTION = new('?', 0x003f, Color::BLUE) # QUESTION MARK
|
23
|
+
CHECK = new('v', 0x2713, Color::GREEN) # CHECK MARK
|
24
|
+
X = new('x', 0x2717, Color::RED) # BALLOT X
|
25
|
+
|
26
|
+
class InvalidGlyphHandle < ArgumentError
|
27
|
+
def initialize(handle)
|
28
|
+
@handle = handle
|
29
|
+
end
|
30
|
+
|
31
|
+
def message
|
32
|
+
keys = Glyph.available.join(',')
|
33
|
+
"invalid glyph handle: #{@handle} " \
|
34
|
+
"-- must be one of Dex::UI::Glyph.available (#{keys})"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.lookup(name)
|
39
|
+
MAP.fetch(name.to_s)
|
40
|
+
rescue KeyError
|
41
|
+
raise InvalidGlyphHandle, name
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.available
|
45
|
+
MAP.keys
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'dex/ui'
|
2
|
+
|
3
|
+
module Dex
|
4
|
+
module UI
|
5
|
+
module Progress
|
6
|
+
FILLED_BAR = Dex::UI::Glyph.new("◾", 0x2588, Color::CYAN)
|
7
|
+
UNFILLED_BAR = Dex::UI::Glyph.new("◽", 0x2588, Color::WHITE)
|
8
|
+
|
9
|
+
def self.progress(percent, width)
|
10
|
+
filled = (percent * width).ceil
|
11
|
+
unfilled = width - filled
|
12
|
+
Dex::UI.resolve_text [
|
13
|
+
(FILLED_BAR.to_s * filled),
|
14
|
+
(UNFILLED_BAR.to_s * unfilled)
|
15
|
+
].join
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'dex/ui'
|
2
|
+
require 'readline'
|
3
|
+
|
4
|
+
module Dex
|
5
|
+
module UI
|
6
|
+
module Prompt
|
7
|
+
class << self
|
8
|
+
def ask(question, options: nil, default: nil, is_file: nil, allow_empty: true)
|
9
|
+
if (default && !allow_empty) || (options && (default || is_file))
|
10
|
+
raise(ArgumentError, 'conflicting arguments')
|
11
|
+
end
|
12
|
+
|
13
|
+
if default
|
14
|
+
puts_question("#{question} (empty = #{default})")
|
15
|
+
else
|
16
|
+
puts_question(question)
|
17
|
+
end
|
18
|
+
|
19
|
+
if options
|
20
|
+
return ask_options(options)
|
21
|
+
end
|
22
|
+
|
23
|
+
loop do
|
24
|
+
line = readline(is_file: is_file)
|
25
|
+
|
26
|
+
if line.empty? && default
|
27
|
+
write_default_over_empty_input(default)
|
28
|
+
return default
|
29
|
+
end
|
30
|
+
|
31
|
+
if !line.empty? || allow_empty
|
32
|
+
return line
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def confirm(question)
|
38
|
+
puts_question(question + ' {{yellow:[y/n]}}')
|
39
|
+
|
40
|
+
loop do
|
41
|
+
line = readline(is_file: false)
|
42
|
+
char = line[0].downcase
|
43
|
+
return true if char == 'y'
|
44
|
+
return false if char == 'n'
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def write_default_over_empty_input(default)
|
51
|
+
Dex::UI.raw do
|
52
|
+
STDERR.puts(
|
53
|
+
Dex::UI::ANSI.cursor_up(1) +
|
54
|
+
"\r" +
|
55
|
+
Dex::UI::ANSI.cursor_forward(4) + # TODO: width
|
56
|
+
default +
|
57
|
+
Dex::UI::Color::RESET.code
|
58
|
+
)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def puts_question(str)
|
63
|
+
Dex::UI.with_frame_color(:blue) do
|
64
|
+
STDOUT.puts(Dex::UI.fmt('{{?}} ' + str))
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def ask_options(options)
|
69
|
+
puts_question("Your options are:")
|
70
|
+
options.each_with_index do |v, idx|
|
71
|
+
puts_question("#{idx + 1}) #{v}")
|
72
|
+
end
|
73
|
+
puts_question("Choose a number between 1 and #{options.length}")
|
74
|
+
|
75
|
+
buf = -1
|
76
|
+
available = (1..options.length).to_a
|
77
|
+
until available.include?(buf.to_i)
|
78
|
+
buf = readline(is_file: false)
|
79
|
+
|
80
|
+
if buf.nil?
|
81
|
+
STDERR.puts
|
82
|
+
next
|
83
|
+
end
|
84
|
+
|
85
|
+
if buf.is_a?(String)
|
86
|
+
buf = buf.chomp
|
87
|
+
end
|
88
|
+
buf = -1 if buf.empty?
|
89
|
+
buf = -1 if buf.to_i.to_s != buf
|
90
|
+
end
|
91
|
+
|
92
|
+
options[buf.to_i - 1]
|
93
|
+
end
|
94
|
+
|
95
|
+
def readline(is_file: false)
|
96
|
+
if is_file
|
97
|
+
Readline.completion_proc = Readline::FILENAME_COMPLETION_PROC
|
98
|
+
Readline.completion_append_character = ""
|
99
|
+
else
|
100
|
+
Readline.completion_proc = proc { |*| nil }
|
101
|
+
Readline.completion_append_character = " "
|
102
|
+
end
|
103
|
+
|
104
|
+
# because Readline is a C library, Dex::UI's hooks into $stdout don't
|
105
|
+
# work. We could work around this by having Dex::UI use a pipe and a
|
106
|
+
# thread to manage output, but the current strategy feels like a
|
107
|
+
# better tradeoff.
|
108
|
+
prefix = Dex::UI.with_frame_color(:blue) { Dex::UI::Frame.prefix }
|
109
|
+
prompt = prefix + Dex::UI.fmt('{{blue:> }}{{yellow:')
|
110
|
+
|
111
|
+
begin
|
112
|
+
Readline.readline(prompt, true).chomp
|
113
|
+
rescue Interrupt
|
114
|
+
Dex::UI.raw { STDERR.puts('^C' + Dex::UI::Color::RESET.code) }
|
115
|
+
raise
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
require 'dex/ui'
|
2
|
+
|
3
|
+
module Dex
|
4
|
+
module UI
|
5
|
+
module Spinner
|
6
|
+
PERIOD = 0.1 # seconds
|
7
|
+
|
8
|
+
begin
|
9
|
+
runes = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
10
|
+
colors = [Dex::UI::Color::CYAN.code] * 5 + [Dex::UI::Color::MAGENTA.code] * 5
|
11
|
+
raise unless runes.size == colors.size
|
12
|
+
GLYPHS = colors.zip(runes).map(&:join)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.spin(title, &block)
|
16
|
+
sg = SpinGroup.new
|
17
|
+
sg.add(title, &block)
|
18
|
+
sg.wait
|
19
|
+
end
|
20
|
+
|
21
|
+
class SpinGroup
|
22
|
+
def initialize
|
23
|
+
@m = Mutex.new
|
24
|
+
@consumed_lines = 0
|
25
|
+
@tasks = []
|
26
|
+
end
|
27
|
+
|
28
|
+
class Task
|
29
|
+
attr_reader :title, :exception, :success, :stdout, :stderr
|
30
|
+
|
31
|
+
def initialize(title, &block)
|
32
|
+
@title = title
|
33
|
+
@thread = Thread.new do
|
34
|
+
cap = Dex::UI::StdoutRouter::Capture.new(with_frame_inset: false, &block)
|
35
|
+
begin
|
36
|
+
cap.run
|
37
|
+
ensure
|
38
|
+
@stdout = cap.stdout
|
39
|
+
@stderr = cap.stderr
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
@done = false
|
44
|
+
@exception = nil
|
45
|
+
@success = false
|
46
|
+
end
|
47
|
+
|
48
|
+
def check
|
49
|
+
return true if @done
|
50
|
+
return false if @thread.alive?
|
51
|
+
|
52
|
+
@done = true
|
53
|
+
begin
|
54
|
+
status = @thread.join.status
|
55
|
+
@success = (status == false)
|
56
|
+
rescue => exc
|
57
|
+
@exception = exc
|
58
|
+
@success = false
|
59
|
+
end
|
60
|
+
|
61
|
+
@done
|
62
|
+
end
|
63
|
+
|
64
|
+
def render(index, force = true)
|
65
|
+
return full_render(index) if force
|
66
|
+
partial_render(index)
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def full_render(index)
|
72
|
+
inset + glyph(index) + Dex::UI::Color::RESET.code + ' ' + Dex::UI.resolve_text(title)
|
73
|
+
end
|
74
|
+
|
75
|
+
def partial_render(index)
|
76
|
+
Dex::UI::ANSI.cursor_forward(inset_width) + glyph(index) + Dex::UI::Color::RESET.code
|
77
|
+
end
|
78
|
+
|
79
|
+
def glyph(index)
|
80
|
+
if @done
|
81
|
+
@success ? Dex::UI::Glyph::CHECK.to_s : Dex::UI::Glyph::X.to_s
|
82
|
+
else
|
83
|
+
GLYPHS[index]
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def inset
|
88
|
+
@inset ||= Dex::UI::Frame.prefix
|
89
|
+
end
|
90
|
+
|
91
|
+
def inset_width
|
92
|
+
@inset_width ||= Dex::UI::ANSI.printing_width(inset)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def add(title, &block)
|
97
|
+
@m.synchronize do
|
98
|
+
@tasks << Task.new(title, &block)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def wait
|
103
|
+
idx = 0
|
104
|
+
|
105
|
+
loop do
|
106
|
+
all_done = true
|
107
|
+
|
108
|
+
@m.synchronize do
|
109
|
+
Dex::UI.raw do
|
110
|
+
@tasks.each.with_index do |task, int_index|
|
111
|
+
nat_index = int_index + 1
|
112
|
+
task_done = task.check
|
113
|
+
all_done = false unless task_done
|
114
|
+
|
115
|
+
if nat_index > @consumed_lines
|
116
|
+
print(task.render(idx, true) + "\n")
|
117
|
+
@consumed_lines += 1
|
118
|
+
else
|
119
|
+
offset = @consumed_lines - int_index
|
120
|
+
move_to = Dex::UI::ANSI.cursor_up(offset) + "\r"
|
121
|
+
move_from = "\r" + Dex::UI::ANSI.cursor_down(offset)
|
122
|
+
|
123
|
+
print(move_to + task.render(idx, idx.zero?) + move_from)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
break if all_done
|
130
|
+
|
131
|
+
idx = (idx + 1) % GLYPHS.size
|
132
|
+
sleep(PERIOD)
|
133
|
+
end
|
134
|
+
|
135
|
+
debrief
|
136
|
+
end
|
137
|
+
|
138
|
+
def debrief
|
139
|
+
@m.synchronize do
|
140
|
+
@tasks.each do |task|
|
141
|
+
next if task.success
|
142
|
+
|
143
|
+
e = task.exception
|
144
|
+
out = task.stdout
|
145
|
+
err = task.stderr
|
146
|
+
|
147
|
+
Dex::UI::Frame.open('Task Failed: ' + task.title, color: :red) do
|
148
|
+
if e
|
149
|
+
puts"#{e.class}: #{e.message}"
|
150
|
+
puts "\tfrom #{e.backtrace.join("\n\tfrom ")}"
|
151
|
+
end
|
152
|
+
|
153
|
+
Dex::UI::Frame.divider('STDOUT')
|
154
|
+
out = "(empty)" if out.strip.empty?
|
155
|
+
puts out
|
156
|
+
|
157
|
+
Dex::UI::Frame.divider('STDERR')
|
158
|
+
err = "(empty)" if err.strip.empty?
|
159
|
+
puts err
|
160
|
+
end
|
161
|
+
end
|
162
|
+
@tasks.all?(&:success)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
@@ -0,0 +1,186 @@
|
|
1
|
+
require 'dex/ui'
|
2
|
+
|
3
|
+
module Dex
|
4
|
+
module UI
|
5
|
+
module StdoutRouter
|
6
|
+
class << self
|
7
|
+
attr_accessor :duplicate_output_to
|
8
|
+
end
|
9
|
+
|
10
|
+
class Writer
|
11
|
+
def initialize(stream, name)
|
12
|
+
@stream = stream
|
13
|
+
@name = name
|
14
|
+
end
|
15
|
+
|
16
|
+
def write(*args)
|
17
|
+
if auto_frame_inset?
|
18
|
+
str = args[0].dup # unfreeze
|
19
|
+
str = str.force_encoding(Encoding::UTF_8)
|
20
|
+
str = apply_line_prefix(str, Dex::UI::Frame.prefix)
|
21
|
+
args[0] = str
|
22
|
+
else
|
23
|
+
@pending_newline = false
|
24
|
+
end
|
25
|
+
|
26
|
+
hook = Thread.current[:dexui_output_hook]
|
27
|
+
# hook return of false suppresses output.
|
28
|
+
if !hook || hook.call(args.first, @name) != false
|
29
|
+
args.first
|
30
|
+
@stream.write_without_dex_ui(*args)
|
31
|
+
if dup = StdoutRouter.duplicate_output_to
|
32
|
+
dup.write(*args)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def auto_frame_inset?
|
40
|
+
!Thread.current[:no_dexui_frame_inset]
|
41
|
+
end
|
42
|
+
|
43
|
+
def apply_line_prefix(str, prefix)
|
44
|
+
return '' if str.empty?
|
45
|
+
prefixed = String.new
|
46
|
+
str.force_encoding(Encoding::UTF_8).lines.each do |line|
|
47
|
+
if @pending_newline
|
48
|
+
prefixed << line
|
49
|
+
@pending_newline = false
|
50
|
+
else
|
51
|
+
prefixed << prefix << line
|
52
|
+
end
|
53
|
+
end
|
54
|
+
@pending_newline = !str.end_with?("\n")
|
55
|
+
prefixed
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class Capture
|
60
|
+
@m = Mutex.new
|
61
|
+
@active_captures = 0
|
62
|
+
@saved_stdin = nil
|
63
|
+
|
64
|
+
def self.with_stdin_masked
|
65
|
+
@m.synchronize do
|
66
|
+
if @active_captures.zero?
|
67
|
+
@saved_stdin = $stdin
|
68
|
+
$stdin, w = IO.pipe
|
69
|
+
$stdin.close
|
70
|
+
w.close
|
71
|
+
end
|
72
|
+
@active_captures += 1
|
73
|
+
end
|
74
|
+
|
75
|
+
yield
|
76
|
+
ensure
|
77
|
+
@m.synchronize do
|
78
|
+
@active_captures -= 1
|
79
|
+
if @active_captures.zero?
|
80
|
+
$stdin = @saved_stdin
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def initialize(with_frame_inset: true, &block)
|
86
|
+
@with_frame_inset = with_frame_inset
|
87
|
+
@block = block
|
88
|
+
end
|
89
|
+
|
90
|
+
attr_reader :stdout, :stderr
|
91
|
+
|
92
|
+
def run
|
93
|
+
StdoutRouter.assert_enabled!
|
94
|
+
|
95
|
+
out = StringIO.new
|
96
|
+
err = StringIO.new
|
97
|
+
|
98
|
+
prev_frame_inset = Thread.current[:no_dexui_frame_inset]
|
99
|
+
prev_hook = Thread.current[:dexui_output_hook]
|
100
|
+
|
101
|
+
self.class.with_stdin_masked do
|
102
|
+
Thread.current[:no_dexui_frame_inset] = !@with_frame_inset
|
103
|
+
Thread.current[:dexui_output_hook] = ->(data, stream) do
|
104
|
+
case stream
|
105
|
+
when :stdout then out.write(data)
|
106
|
+
when :stderr then err.write(data)
|
107
|
+
else raise
|
108
|
+
end
|
109
|
+
false # suppress writing to terminal
|
110
|
+
end
|
111
|
+
|
112
|
+
begin
|
113
|
+
@block.call
|
114
|
+
ensure
|
115
|
+
@stdout = out.string
|
116
|
+
@stderr = err.string
|
117
|
+
end
|
118
|
+
end
|
119
|
+
ensure
|
120
|
+
Thread.current[:dexui_output_hook] = prev_hook
|
121
|
+
Thread.current[:no_dexui_frame_inset] = prev_frame_inset
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
class << self
|
126
|
+
WRITE_WITHOUT_DEX_UI = :write_without_dex_ui
|
127
|
+
|
128
|
+
NotEnabled = Class.new(StandardError)
|
129
|
+
|
130
|
+
def assert_enabled!
|
131
|
+
raise NotEnabled unless enabled?
|
132
|
+
end
|
133
|
+
|
134
|
+
def with_enabled
|
135
|
+
enable
|
136
|
+
yield
|
137
|
+
ensure
|
138
|
+
disable
|
139
|
+
end
|
140
|
+
|
141
|
+
# TODO: remove this
|
142
|
+
def ensure_activated
|
143
|
+
enable unless enabled?
|
144
|
+
end
|
145
|
+
|
146
|
+
def enable
|
147
|
+
return false if enabled?($stdout) || enabled?($stderr)
|
148
|
+
activate($stdout, :stdout)
|
149
|
+
activate($stderr, :stderr)
|
150
|
+
true
|
151
|
+
end
|
152
|
+
|
153
|
+
def enabled?(stream = $stdout)
|
154
|
+
stream.respond_to?(WRITE_WITHOUT_DEX_UI)
|
155
|
+
end
|
156
|
+
|
157
|
+
def disable
|
158
|
+
return false unless enabled?($stdout) && enabled?($stderr)
|
159
|
+
deactivate($stdout)
|
160
|
+
deactivate($stderr)
|
161
|
+
true
|
162
|
+
end
|
163
|
+
|
164
|
+
private
|
165
|
+
|
166
|
+
def deactivate(stream)
|
167
|
+
sc = stream.singleton_class
|
168
|
+
sc.send(:remove_method, :write)
|
169
|
+
sc.send(:alias_method, :write, WRITE_WITHOUT_DEX_UI)
|
170
|
+
end
|
171
|
+
|
172
|
+
def activate(stream, streamname)
|
173
|
+
writer = StdoutRouter::Writer.new(stream, streamname)
|
174
|
+
|
175
|
+
raise if stream.respond_to?(WRITE_WITHOUT_DEX_UI)
|
176
|
+
stream.singleton_class.send(:alias_method, WRITE_WITHOUT_DEX_UI, :write)
|
177
|
+
stream.define_singleton_method(:write) do |*args|
|
178
|
+
writer.write(*args)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
Dex::UI::StdoutRouter.enable
|