spotify_cli 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|