db_sucker 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/CHANGELOG.md +45 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +193 -0
  7. data/Rakefile +1 -0
  8. data/VERSION +1 -0
  9. data/bin/db_sucker +12 -0
  10. data/bin/db_sucker.sh +14 -0
  11. data/db_sucker.gemspec +29 -0
  12. data/doc/config_example.rb +53 -0
  13. data/doc/container_example.yml +150 -0
  14. data/lib/db_sucker/adapters/mysql2.rb +103 -0
  15. data/lib/db_sucker/application/colorize.rb +28 -0
  16. data/lib/db_sucker/application/container/accessors.rb +60 -0
  17. data/lib/db_sucker/application/container/ssh.rb +225 -0
  18. data/lib/db_sucker/application/container/validations.rb +53 -0
  19. data/lib/db_sucker/application/container/variation/accessors.rb +45 -0
  20. data/lib/db_sucker/application/container/variation/helpers.rb +21 -0
  21. data/lib/db_sucker/application/container/variation/worker_api.rb +65 -0
  22. data/lib/db_sucker/application/container/variation.rb +60 -0
  23. data/lib/db_sucker/application/container.rb +70 -0
  24. data/lib/db_sucker/application/container_collection.rb +47 -0
  25. data/lib/db_sucker/application/core.rb +222 -0
  26. data/lib/db_sucker/application/dispatch.rb +364 -0
  27. data/lib/db_sucker/application/evented_resultset.rb +149 -0
  28. data/lib/db_sucker/application/fake_channel.rb +22 -0
  29. data/lib/db_sucker/application/output_helper.rb +197 -0
  30. data/lib/db_sucker/application/sklaven_treiber/log_spool.rb +57 -0
  31. data/lib/db_sucker/application/sklaven_treiber/worker/accessors.rb +105 -0
  32. data/lib/db_sucker/application/sklaven_treiber/worker/core.rb +168 -0
  33. data/lib/db_sucker/application/sklaven_treiber/worker/helpers.rb +144 -0
  34. data/lib/db_sucker/application/sklaven_treiber/worker/io/base.rb +240 -0
  35. data/lib/db_sucker/application/sklaven_treiber/worker/io/file_copy.rb +81 -0
  36. data/lib/db_sucker/application/sklaven_treiber/worker/io/file_gunzip.rb +58 -0
  37. data/lib/db_sucker/application/sklaven_treiber/worker/io/file_import_sql.rb +80 -0
  38. data/lib/db_sucker/application/sklaven_treiber/worker/io/file_shasum.rb +49 -0
  39. data/lib/db_sucker/application/sklaven_treiber/worker/io/pv_wrapper.rb +73 -0
  40. data/lib/db_sucker/application/sklaven_treiber/worker/io/sftp_download.rb +57 -0
  41. data/lib/db_sucker/application/sklaven_treiber/worker/io/throughput.rb +219 -0
  42. data/lib/db_sucker/application/sklaven_treiber/worker/routines.rb +313 -0
  43. data/lib/db_sucker/application/sklaven_treiber/worker.rb +48 -0
  44. data/lib/db_sucker/application/sklaven_treiber.rb +281 -0
  45. data/lib/db_sucker/application/slot_pool.rb +137 -0
  46. data/lib/db_sucker/application/tie.rb +25 -0
  47. data/lib/db_sucker/application/window/core.rb +185 -0
  48. data/lib/db_sucker/application/window/dialog.rb +142 -0
  49. data/lib/db_sucker/application/window/keypad/core.rb +85 -0
  50. data/lib/db_sucker/application/window/keypad.rb +174 -0
  51. data/lib/db_sucker/application/window/prompt.rb +124 -0
  52. data/lib/db_sucker/application/window.rb +329 -0
  53. data/lib/db_sucker/application.rb +168 -0
  54. data/lib/db_sucker/patches/beta-warning.rb +374 -0
  55. data/lib/db_sucker/patches/developer.rb +29 -0
  56. data/lib/db_sucker/patches/net-sftp.rb +20 -0
  57. data/lib/db_sucker/patches/thread-count.rb +30 -0
  58. data/lib/db_sucker/version.rb +4 -0
  59. data/lib/db_sucker.rb +81 -0
  60. metadata +217 -0
@@ -0,0 +1,48 @@
1
+ module DbSucker
2
+ class Application
3
+ class SklavenTreiber
4
+ class Worker
5
+ SlotPoolNotInitializedError = Class.new(::RuntimeError)
6
+ ChannelFailRetryError = Class.new(::RuntimeError)
7
+
8
+ include Core
9
+ include Accessors
10
+ include Helpers
11
+ include Routines
12
+
13
+ attr_reader :exception, :ctn, :var, :table, :thread, :monitor, :step, :perform, :should_cancel, :sklaventreiber, :timings, :sshing
14
+ OutputHelper.hook(self)
15
+
16
+ def initialize sklaventreiber, ctn, var, table
17
+ @sklaventreiber = sklaventreiber
18
+ @ctn = ctn
19
+ @var = var
20
+ @table = table
21
+ @monitor = Monitor.new
22
+ @timings = {}
23
+ @deferred = false
24
+ @spinner_frames = sklaventreiber.window.try(:spinner_frames).try(:dup) || []
25
+ @current_perform = :unknown
26
+ @perform = %w[].tap do |perform|
27
+ perform << "r_dump_file"
28
+ perform << "r_calculate_raw_hash" if ctn.integrity?
29
+ perform << "r_compress_file"
30
+ perform << "r_calculate_compressed_hash" if ctn.integrity?
31
+ perform << "l_download_file"
32
+ perform << "l_verify_compressed_hash" if ctn.integrity?
33
+ perform << "l_copy_file" if var.copies_file? && var.copies_file_compressed?
34
+ if var.requires_uncompression?
35
+ perform << "l_decompress_file"
36
+ perform << "l_verify_raw_hash" if ctn.integrity?
37
+ perform << "l_copy_file" if var.copies_file? && !var.copies_file_compressed?
38
+ perform << "l_import_file" if var.data["database"]
39
+ end
40
+ end
41
+
42
+ @state = :pending
43
+ @status = ["waiting...", "gray"]
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,281 @@
1
+ module DbSucker
2
+ class Application
3
+ class SklavenTreiber
4
+ attr_reader :app, :trxid, :window, :data, :status, :monitor, :workers, :poll, :throughput, :slot_pools
5
+
6
+ def initialize app, trxid
7
+ @app = app
8
+ @trxid = trxid
9
+ @status = ["initializing", "gray"]
10
+ @monitor = Monitor.new
11
+ @workers = []
12
+ @threads = []
13
+ @slot_pools = {}
14
+ @sleep_before_exit = 0
15
+ @throughput = Worker::IO::Throughput.new(self)
16
+
17
+ @data = {
18
+ database: nil,
19
+ tables_transfer: nil,
20
+ tables_transfer_list: [],
21
+ tables_total: nil,
22
+ tables_done: 0,
23
+ }
24
+ end
25
+
26
+ def sync
27
+ @monitor.synchronize { yield }
28
+ end
29
+
30
+ def pause_worker worker
31
+ sync { worker.pause }
32
+ end
33
+
34
+ def unpause_worker worker
35
+ sync { worker.unpause }
36
+ end
37
+
38
+ def pause_all_workers
39
+ sync { @workers.each {|wrk| pause_worker(wrk) } }
40
+ end
41
+
42
+ def unpause_all_workers
43
+ sync { @workers.each {|wrk| unpause_worker(wrk) } }
44
+ end
45
+
46
+ def spooled
47
+ stdout_was = app.opts[:stdout]
48
+ app.opts[:stdout] = SklavenTreiber::LogSpool.new(stdout_was) if app.opts[:window_enabled]
49
+ yield if block_given?
50
+ ensure
51
+ app.opts[:stdout].spooldown do |meth, args, time|
52
+ stdout_was.send(meth, *args)
53
+ end if app.opts[:stdout].respond_to?(:spooldown)
54
+ app.opts[:stdout] = stdout_was
55
+ end
56
+
57
+ def whip_it! ctn, var
58
+ @ctn, @var = ctn, var
59
+
60
+ _start_ssh_poll
61
+ _init_window
62
+ _check_remote_tmp_directory
63
+ _select_tables
64
+ _initialize_slot_pools
65
+ _initialize_workers
66
+ @ctn.pv_utility # lazy load
67
+ @poll[:force] = false
68
+ @throughput.start_loop
69
+
70
+ @sleep_before_exit = 3 if @window
71
+ _run_consumers
72
+ ensure
73
+ app.sandboxed do
74
+ @status = ["terminating (canceling workers)", "red"]
75
+ @workers.each {|w| catch(:abort_execution) { w.cancel! } }
76
+ end
77
+ app.sandboxed do
78
+ @status = ["terminating (SSH poll)", "red"]
79
+ if @poll
80
+ @poll[:force] = false
81
+ @poll.join
82
+ end
83
+ end
84
+ @status = ["terminated", "red"]
85
+ sleep @sleep_before_exit
86
+ app.sandboxed { @window.try(:stop) }
87
+ app.sandboxed { @ctn.try(:sftp_end) }
88
+ app.sandboxed { @throughput.try(:stop_loop) }
89
+ app.sandboxed { @slot_pools.each{|n, p| p.close! } }
90
+ app.sandboxed do
91
+ app.puts @window.try(:_render_final_results)
92
+ end
93
+ @ctn, @var = nil, nil
94
+ end
95
+
96
+ def _init_window
97
+ return unless app.opts[:window_enabled]
98
+ @window = Window.new(app, self)
99
+ @window.init!
100
+ @window.start
101
+ end
102
+
103
+ def _check_remote_tmp_directory
104
+ @status = ["checking remote temp directory", "blue"]
105
+ @ctn.sftp_begin
106
+ @ctn.sftp_start do |sftp|
107
+ # check tmp directory
108
+ app.debug "Checking remote temp directory #{app.c @ctn.tmp_path, :magenta}"
109
+ begin
110
+ sftp.dir.glob("#{@ctn.tmp_path}", "**/*")
111
+ rescue Net::SFTP::StatusException => ex
112
+ if ex.message["no such file"]
113
+ app.abort "Temp directory `#{@ctn.tmp_path}' does not exist on the remote side!", 2
114
+ else
115
+ raise
116
+ end
117
+ end
118
+ end
119
+ end
120
+
121
+ def _select_tables
122
+ @status = ["selecting tables for transfer", "blue"]
123
+ ttt, at = @var.tables_to_transfer
124
+
125
+ # apply only/except filters provided via command line
126
+ if @app.opts[:suck_only].any? && @app.opts[:suck_except].any?
127
+ raise OptionParser::InvalidArgument, "only one of `--only' or `--except' option can be provided at the same time"
128
+ elsif @app.opts[:suck_only].any?
129
+ unless (r = @app.opts[:suck_only] - at).empty?
130
+ raise Container::TableNotFoundError, "table(s) `#{r * ", "}' for the database `#{@ctn.source["database"]}' could not be found (provided via --only, variation `#{@ctn.name}/#{@var.name}' in `#{@ctn.src}')"
131
+ end
132
+ ttt = @app.opts[:suck_only]
133
+ elsif @app.opts[:suck_except].any?
134
+ unless (r = @app.opts[:suck_except] - at).empty?
135
+ raise Container::TableNotFoundError, "table(s) `#{r * ", "}' for the database `#{@ctn.source["database"]}' could not be found (provided via --except, variation `#{@ctn.name}/#{@var.name}' in `#{@ctn.src}')"
136
+ end
137
+ ttt = ttt - @app.opts[:suck_except]
138
+ end
139
+
140
+ @data[:database] = @ctn.source["database"]
141
+ @data[:tables_transfer] = ttt.length
142
+ @data[:tables_transfer_list] = ttt
143
+ @data[:window_col1] = ttt.map(&:length).max
144
+ @data[:tables_total] = at.length
145
+ end
146
+
147
+ def _initialize_slot_pools
148
+ app.opts[:slot_pools].each do |name, slots|
149
+ @slot_pools[name] = SlotPool.new(slots, name)
150
+ end
151
+ end
152
+
153
+ def _initialize_workers
154
+ @status = ["initializing workers 0/#{@data[:tables_transfer]}", "blue"]
155
+
156
+ @data[:tables_transfer_list].each_with_index do |table, index|
157
+ @status = ["initializing workers #{index+1}/#{@data[:tables_transfer]}", "blue"]
158
+ @workers << Worker.new(self, @ctn, @var, table)
159
+ end
160
+ end
161
+
162
+ def _start_ssh_poll
163
+ wait_lock = Queue.new
164
+ @poll = app.spawn_thread(:sklaventreiber_ssh_poll) do |thr|
165
+ thr[:force] = true
166
+ thr[:iteration] = 0
167
+ thr[:errors] = 0
168
+ wait_lock << true
169
+ begin
170
+ @ctn.loop_ssh(0.1) {
171
+ thr[:iteration] += 1
172
+ thr[:last_iteration] = Time.current
173
+ thr[:force] || @workers.select{|w| !w.done? || w.sshing }.any?
174
+ }
175
+ rescue Container::SSH::ChannelOpenFailedError
176
+ thr[:errors] += 1
177
+ sleep 0.5
178
+ retry
179
+ end
180
+
181
+ if thr[:errors].zero?
182
+ app.debug "SSH error count (#{thr[:errors]})"
183
+ elsif thr[:errors] > 25
184
+ app.warning "SSH error count (#{thr[:errors]}) is high! Verify remote MaxSessions setting or lower concurrent worker count."
185
+ else
186
+ app.warning "SSH errors occured (#{thr[:errors]})! Verify remote MaxSessions setting or lower concurrent worker count."
187
+ end
188
+ end
189
+ wait_lock.pop
190
+ sleep 0.01 until @poll[:iteration] && @poll[:iteration] > 0
191
+ end
192
+
193
+ def _run_consumers
194
+ cnum = [app.opts[:consumers], @data[:tables_transfer]].min
195
+ @data[:window_col2] = cnum.to_s.length
196
+ if cnum <= 1
197
+ _run_in_main_thread
198
+ else
199
+ _run_in_threads(cnum)
200
+ end
201
+ end
202
+
203
+ def _run_in_main_thread
204
+ @status = ["running in main thread...", "green"]
205
+
206
+ # control thread
207
+ ctrlthr = app.spawn_thread(:sklaventreiber_worker_ctrl) do |thr|
208
+ loop do
209
+ _control_thread
210
+ break if thr[:stop]
211
+ thr.wait(0.1)
212
+ end
213
+ end
214
+
215
+ begin
216
+ Thread.current[:managed_worker] = :main
217
+ _queueoff
218
+ ensure
219
+ ctrlthr[:stop] = true
220
+ ctrlthr.signal.join
221
+ end
222
+ end
223
+
224
+ def _run_in_threads(cnum)
225
+ @status = ["starting consumer 0/#{cnum}", "blue"]
226
+
227
+ # initializing consumer threads
228
+ cnum.times do |wi|
229
+ @status = ["starting consumer #{wi+1}/#{cnum}", "blue"]
230
+ @threads << app.spawn_thread(:sklaventreiber_worker) {|thr|
231
+ begin
232
+ thr[:managed_worker] = wi
233
+ thr.wait(0.1) until thr[:start] || $core_runtime_exiting
234
+ _queueoff
235
+ rescue Interrupt
236
+ end
237
+ }
238
+ end
239
+
240
+ # start consumer threads
241
+ @status = ["running", "green"]
242
+ @threads.each{|t| t[:start] = true; t.signal }
243
+
244
+ # master thread (control)
245
+ while @threads.any?(&:alive?)
246
+ _control_thread
247
+ Thread.current.wait(0.1)
248
+ end
249
+ @threads.each(&:join)
250
+ end
251
+
252
+ def _queueoff
253
+ loop do
254
+ return if $core_runtime_exiting
255
+ worker = false
256
+ sync do
257
+ pending = @workers.select(&:pending?)
258
+ return unless pending.any?
259
+ worker = pending.first.aquire(Thread.current)
260
+ end
261
+ if worker
262
+ begin
263
+ worker.run
264
+ ensure
265
+ sync { @data[:tables_done] += 1 }
266
+ end
267
+ end
268
+ end
269
+ end
270
+
271
+ def _control_thread
272
+ if $core_runtime_exiting && $core_runtime_exiting < 100
273
+ $core_runtime_exiting += 100
274
+ app.sandboxed { @workers.each {|w| catch(:abort_execution) { w.cancel! } } }
275
+ app.sandboxed { @slot_pools.each{|n, p| p.softclose! } }
276
+ app.wakeup_handlers
277
+ end
278
+ end
279
+ end
280
+ end
281
+ end
@@ -0,0 +1,137 @@
1
+ module DbSucker
2
+ class Application
3
+ class SlotPool
4
+ SlotAllocationError = Class.new(::RuntimeError)
5
+ PoolAlreadyClosedError = Class.new(SlotAllocationError)
6
+
7
+ def initialize slots = 1, name = nil
8
+ @name = name
9
+ @slots = slots
10
+ @monitor = Monitor.new
11
+ @closed = false
12
+ @softclosed = false
13
+ @waiting = []
14
+ @active = []
15
+ @signal = @monitor.new_cond
16
+ @closed_signal = @monitor.new_cond
17
+ end
18
+
19
+ def self.expose what, &how
20
+ define_method(what) do |*args, &block|
21
+ sync { instance_exec(*args, &how) }
22
+ end
23
+ end
24
+
25
+ def sync &block
26
+ @monitor.synchronize(&block)
27
+ end
28
+
29
+ expose(:name) { @name }
30
+ expose(:active) { @active }
31
+ expose(:active?) { @active.any? }
32
+ expose(:waiting) { @waiting }
33
+ expose(:waiting?) { @waiting.any? }
34
+ expose(:closed?) { @closed }
35
+ expose(:softclosed?) { @softclosed }
36
+ expose(:slots) { @slots }
37
+ expose(:available_slots) { @slots ? @slots - @active.length : 1.0/0 }
38
+ expose(:slots?) { @slots ? available_slots > 0 : true }
39
+
40
+ def close
41
+ sync do
42
+ @closed = true
43
+ @signal.broadcast
44
+ end
45
+ true
46
+ end
47
+
48
+ def close!
49
+ sync do
50
+ close
51
+ @closed_signal.wait if active?
52
+ end
53
+ end
54
+
55
+ def softclose!
56
+ @softclosed = true
57
+ dequeue_waiting!
58
+ end
59
+
60
+ def dequeue_waiting!
61
+ sync do
62
+ while @waiting.any?
63
+ _wthr, _tthr = @waiting.shift
64
+ _wthr.signal
65
+ end
66
+ @signal.broadcast
67
+ end
68
+ end
69
+
70
+ def qindex thr = nil
71
+ thr ||= Thread.current
72
+ sync do
73
+ index = @waiting.find_index {|wthr, tthr| tthr == thr }
74
+ index ? index + 1 : false
75
+ end
76
+ end
77
+
78
+ def puts *a
79
+ Thread.main[:app].puts(*a)
80
+ end
81
+
82
+ def aquired? tthr = nil
83
+ @active.include?(tthr || Thread.current)
84
+ end
85
+
86
+ def wait_aquired tthr = nil
87
+ tthr ||= Thread.current
88
+ #tthr.wait(0.1) until qindex(tthr)
89
+ loop do
90
+ sync do
91
+ #puts "<#{Time.current.to_f}-#{tthr[:current_task]}> wait for index"
92
+ #puts "<#{Time.current.to_f}-#{tthr[:current_task]}> has #{available_slots} slots"
93
+ while slots? && @waiting.any?
94
+ _wthr, _tthr = @waiting.shift
95
+ #puts "<#{Time.current.to_f}-#{_tthr[:current_task]}> running now"
96
+ @active.push(_tthr) unless @softclosed
97
+ _tthr.signal
98
+ _wthr.signal
99
+ end
100
+ unless qindex(tthr)
101
+ #puts "<#{Time.current.to_f}-#{tthr[:current_task]}> return"
102
+ return
103
+ end
104
+ #puts "<#{Time.current.to_f}-#{tthr[:current_task]}> wait"
105
+ @signal.wait #(1)
106
+ #puts "<#{Time.current.to_f}-#{tthr[:current_task]}> wait DONE"
107
+ end
108
+ end
109
+ end
110
+
111
+ def aquire tthr = nil
112
+ wthr = Thread.current
113
+ tthr ||= Thread.current
114
+ sync do
115
+ raise PoolAlreadyClosedError, "slot pool has already been closed, cannot aquire slot" if closed?
116
+ @waiting << [wthr, tthr]
117
+ #puts "<#{Time.current.to_f}-#{tthr[:current_task]}> broadcasting signal after adding new waiter"
118
+ @signal.broadcast # signal polling threads
119
+ end
120
+ tthr.signal # signal target thread to continue and poll
121
+ wthr.wait # suspend thread until we aquired it
122
+ true
123
+ end
124
+
125
+ def release tthr = nil
126
+ sync do
127
+ tthr ||= Thread.current
128
+ ai = @active.delete(tthr)
129
+ return unless ai
130
+ #puts "<#{Time.current.to_f}-#{tthr[:current_task]}> broadcasting signal"
131
+ @signal.broadcast
132
+ @closed_signal.broadcast if @active.empty? && closed?
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,25 @@
1
+ module DbSucker
2
+ class Application
3
+ class Tie
4
+ def self.descendants
5
+ @descendants ||= []
6
+ end
7
+
8
+ # Descendant tracking for inherited classes.
9
+ def self.inherited(descendant)
10
+ descendants << descendant
11
+ end
12
+
13
+ def self.hook_all! app
14
+ descendants.uniq.each do |klass|
15
+ app.debug "[AppTie] Loading apptie `#{klass.name}'"
16
+ klass.hook!(app)
17
+ end
18
+ end
19
+
20
+ def self.hook! app
21
+ raise NotImplementedError, "AppTies must implement class method `.hook!(app)'!"
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,185 @@
1
+ module DbSucker
2
+ class Application
3
+ class Window
4
+ module Core
5
+ UnknownSpinnerError = Class.new(::RuntimeError)
6
+ SPINNERS = {
7
+ arrows: "←↖↑↗→↘↓↙",
8
+ blocks: "▁▃▄▅▆▇█▇▆▅▄▃",
9
+ blocks2: "▉▊▋▌▍▎▏▎▍▌▋▊▉",
10
+ blocks3: "▖▘▝▗",
11
+ blocks4: "▌▀▐▄",
12
+ forks: "┤┘┴└├┌┬┐",
13
+ triangles: "◢◣◤◥",
14
+ trbl_square: "◰◳◲◱",
15
+ trbl_circle: "◴◷◶◵",
16
+ circle_half: "◐◓◑◒",
17
+ circle_quarter: "◜◝◞◟",
18
+ circle_quarter2: "╮╯╰╭",
19
+ unix: "|/-\\",
20
+ bomb: ".oO@*",
21
+ eye: "◡⊙◠",
22
+ diamond: "◇◈◆",
23
+ }
24
+
25
+ def start
26
+ @keypad.start_loop
27
+ start_window_loop
28
+ end
29
+
30
+ def stop
31
+ stop_window_loop
32
+ @keypad.stop_loop
33
+ close_screen
34
+ app.debug "Leaving curses screen mode"
35
+ end
36
+
37
+ def start_window_loop
38
+ @loop = app.spawn_thread(:window_draw_loop) do |thr|
39
+ loop do
40
+ break if thr[:stop] && (@view == :status || @force_kill)
41
+ refresh_screen if app.opts[:window_draw]
42
+ thr.wait(app.opts[:window_refresh_delay])
43
+ end
44
+ end
45
+ end
46
+
47
+ def stop_window_loop
48
+ return unless @loop
49
+ @loop[:stop] = true
50
+ @loop.signal.join
51
+ end
52
+
53
+ def choose_spinner
54
+ spinner = app.opts[:window_spinner]
55
+ spinner = SPINNERS.keys.sample if spinner == :random
56
+ if s = SPINNERS[spinner]
57
+ @spinner_frames = s.split("").reverse.freeze
58
+ else
59
+ raise UnknownSpinnerError, "The spinner `#{spinner}' does not exist, use :random or one of: #{SPINNERS.keys * ", "}"
60
+ end
61
+ end
62
+
63
+ def init!
64
+ app.debug "Entering curses screen mode"
65
+ init_screen
66
+ nl
67
+ if @app.opts[:window_keypad]
68
+ raw
69
+ nonl
70
+ noecho
71
+ cbreak
72
+ stdscr.keypad = true
73
+ set_cursor 0
74
+ end
75
+
76
+ # colors
77
+ start_color
78
+ use_default_colors
79
+ [:COLOR_BLACK, :COLOR_RED, :COLOR_GREEN, :COLOR_YELLOW, :COLOR_BLUE, :COLOR_MAGENTA, :COLOR_CYAN, :COLOR_WHITE].each do |cl|
80
+ c = Window.const_get(cl)
81
+ init_pair(c, c, -1)
82
+ end
83
+ init_pair(Window::COLOR_GRAY, 0, -1)
84
+ end
85
+
86
+ def set_cursor visibility
87
+ curs_set(visibility)
88
+ end
89
+
90
+ def force_cursor line, col = 0
91
+ if line.nil?
92
+ @force_cursor = nil
93
+ else
94
+ @force_cursor = [line, col]
95
+ end
96
+ end
97
+
98
+ def update
99
+ clear
100
+ @x_offset = 0
101
+ @line = -1
102
+ yield if block_given?
103
+ next_line
104
+ setpos(*@force_cursor) if @force_cursor
105
+ refresh
106
+ end
107
+
108
+ def line l = 1
109
+ setpos(l - 1, @x_offset)
110
+ end
111
+
112
+ def next_line
113
+ @line += 1
114
+ setpos(@line, @x_offset)
115
+ end
116
+
117
+ def change_view new_view
118
+ view_was = @view
119
+ if block_given?
120
+ begin
121
+ @view = new_view
122
+ yield
123
+ ensure
124
+ @view = view_was
125
+ end
126
+ else
127
+ @view = new_view
128
+ view_was
129
+ end
130
+ end
131
+
132
+ def progress_bar perc, opts = {}
133
+ opts = opts.reverse_merge({
134
+ width: cols - stdscr.curx - 3,
135
+ prog_open: "[",
136
+ prog_open_color: "yellow",
137
+ prog_done: "=",
138
+ prog_done_color: "green",
139
+ prog_current: ">",
140
+ prog_current_color: "yellow",
141
+ prog_remain: ".",
142
+ prog_remain_color: "gray",
143
+ prog_close: "]",
144
+ prog_close_color: "yellow",
145
+ })
146
+ pdone = (opts[:width].to_d * (perc.to_d / 100.to_d)).ceil.to_i
147
+ prem = opts[:width] - pdone
148
+ pcur = 0
149
+ if perc < 100
150
+ pdone.zero? ? (prem -= 1) : (pdone -= 1)
151
+ pcur += 1
152
+ end
153
+
154
+ send(opts[:prog_open_color], " #{opts[:prog_open]}")
155
+ send(opts[:prog_done_color], "".ljust(pdone, opts[:prog_done])) unless pdone.zero?
156
+ send(opts[:prog_current_color], "".ljust(pcur, opts[:prog_current])) unless pcur.zero?
157
+ send(opts[:prog_remain_color], "".ljust(prem, opts[:prog_remain])) unless prem.zero?
158
+ send(opts[:prog_close_color], "#{opts[:prog_close]}")
159
+ end
160
+
161
+ def dialog &block
162
+ Dialog.new(self, &block)
163
+ end
164
+
165
+ def dialog! &block
166
+ dialog(&block).render!
167
+ end
168
+
169
+ # colors
170
+ [:red, :blue, :yellow, :cyan, :magenta, :gray, :green, :white].each do |c|
171
+ define_method(c) do |*args, &block|
172
+ color = Window.const_get "COLOR_#{c.to_s.upcase}"
173
+ attron(color_pair(color)|Window::A_NORMAL) do
174
+ if block
175
+ block.call
176
+ else
177
+ args.each {|a| addstr(a) }
178
+ end
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end