db_sucker 3.0.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.
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