From 43dacdcece2325e73d05888c14d834e74bca18a2 Mon Sep 17 00:00:00 2001 From: Joshua Paling Date: Wed, 19 Oct 2016 22:11:51 +1100 Subject: [PATCH] use curses for in-place updates on status --- README.md | 2 + lib/safe_update.rb | 1 + lib/safe_update/outdated_gem.rb | 22 ++++---- lib/safe_update/presenter.rb | 93 +++++++++++++++++++++++++++++++++ lib/safe_update/updater.rb | 19 +++---- safe_update.gemspec | 2 + spec/presenter_test.rb | 57 ++++++++++++++++++++ 7 files changed, 173 insertions(+), 23 deletions(-) create mode 100644 lib/safe_update/presenter.rb create mode 100644 spec/presenter_test.rb diff --git a/README.md b/README.md index 2c344eb..79da53e 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,8 @@ I've knocked this up really quickly, and it's pretty MVP-ish. Ideas for future i After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. +To test the presenter that displays progress to the terminal: `ruby ./spec/presenter_test.rb` + To install this gem (as in, your development copy of the code) onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). diff --git a/lib/safe_update.rb b/lib/safe_update.rb index 1e889bf..7d9ab39 100644 --- a/lib/safe_update.rb +++ b/lib/safe_update.rb @@ -10,6 +10,7 @@ require 'safe_update/outdated_gem' require 'safe_update/bundle_outdated_parser' require 'safe_update/git_repo' +require 'safe_update/presenter' module SafeUpdate end diff --git a/lib/safe_update/outdated_gem.rb b/lib/safe_update/outdated_gem.rb index 31f45ab..f5f7093 100644 --- a/lib/safe_update/outdated_gem.rb +++ b/lib/safe_update/outdated_gem.rb @@ -1,21 +1,24 @@ module SafeUpdate class OutdatedGem - attr_reader :gem_name, :newest, :installed, :requested + STATUS_PENDING = 'pending' + STATUS_UPDATING = 'updating' + STATUS_TESTING = 'testing' + STATUS_UPDATED = 'updated' + STATUS_UNCHANGED = 'unchanged' + STATUS_TESTS_FAIL = 'tests_fail' + + attr_reader :gem_name, :newest, :installed, :requested, :current_status def initialize(opts = {}) @gem_name = opts[:gem_name] @newest = opts[:newest] @installed = opts[:installed] @requested = opts[:requested] @git_repo = opts[:git_repo] || GitRepo.new + @current_status = STATUS_PENDING end def attempt_update(test_command = nil) - puts '-------------' - puts "OUTDATED GEM: #{@gem_name}" - puts " Newest: #{@newest}. " - puts "Installed: #{@installed}." - puts "Running `bundle update #{@gem_name}`..." - + @current_status = STATUS_UPDATING `bundle update #{@gem_name}` # sometimes the gem may be outdated, but it's matching the @@ -26,16 +29,15 @@ def attempt_update(test_command = nil) end if test_command - puts "Running tests with: #{test_command}" + @current_status = STATUS_TESTING result = system(test_command) if result != true - puts "tests failed - this gem won't be updated (test result: #{$?.to_i})" + @current_status = STATUS_TESTS_FAIL @git_repo.discard_local_changes return end end - puts "committing changes (message: '#{commit_message}')..." @git_repo.commit_gemfile_lock(commit_message) end diff --git a/lib/safe_update/presenter.rb b/lib/safe_update/presenter.rb new file mode 100644 index 0000000..443ec2a --- /dev/null +++ b/lib/safe_update/presenter.rb @@ -0,0 +1,93 @@ +require 'curses' + +Curses.noecho +Curses.init_screen + +module SafeUpdate + class Presenter + # outdated_gems is an array of instances of SafeUpdate::OutdatedGem + + SPINNER_STATES = ['|', '/', '-', '\\'] + + def initialize + @tick = 1 + @running = true + end + + def call(outdated_gems) + @outdated_gems = outdated_gems + while @running do + @tick += 1 + update_screen + sleep 0.3 + end + end + + def stop + @running = false + print_final_output + end + + private + + def print_final_output + Curses.close_screen + puts title + puts header + @outdated_gems.each do |outdated_gem| + puts present_single_gem(outdated_gem) + end + end + + def update_screen + Curses.setpos(0, 0) + Curses.addstr(title) + Curses.refresh + + Curses.setpos(1, 0) + Curses.addstr(header) + Curses.refresh + + @outdated_gems.each_with_index do |outdated_gem, i| + Curses.setpos(i + 2, 0) + line = present_single_gem(outdated_gem) + Curses.addstr(line) + Curses.refresh + end + end + + def title + '=> Updating your gems... safely ' + current_spinner_state + end + + def current_spinner_state + div, remainder = @tick.divmod(SPINNER_STATES.length) + SPINNER_STATES[remainder] + end + + def header + return [ + fixed_length_string('GEM', 10), + fixed_length_string('INSTALLED', 10), + fixed_length_string('REQUESTED', 10), + fixed_length_string('NEWEST', 7), + fixed_length_string('STATUS', 10) + ].join(' | ') + end + + def present_single_gem(outdated_gem) + return [ + fixed_length_string(outdated_gem.gem_name, 10), + fixed_length_string(outdated_gem.installed, 10), + fixed_length_string(outdated_gem.requested || ' -', 10), + fixed_length_string(outdated_gem.newest, 7), + fixed_length_string(outdated_gem.current_status, 10) + ].join(' | ') + end + + # inspired by http://stackoverflow.com/questions/14714936/fix-ruby-string-to-n-characters + def fixed_length_string(str, length) + "%-#{length}.#{length}s" % str + end + end +end diff --git a/lib/safe_update/updater.rb b/lib/safe_update/updater.rb index d81b3cd..a5f96dd 100644 --- a/lib/safe_update/updater.rb +++ b/lib/safe_update/updater.rb @@ -15,6 +15,11 @@ def run(push: nil, test_command: nil) push_interval = push.to_i if run_git_push puts 'Finding outdated gems...' + outdated_gems = BundleOutdatedParser.new.call + + presenter = SafeUpdate::Presenter.new + Thread.new { presenter.call(outdated_gems) } + outdated_gems.to_enum.with_index(1) do |outdated_gem, index| outdated_gem.attempt_update(test_command) @git_repo.push if run_git_push && index % push_interval == 0 @@ -23,19 +28,7 @@ def run(push: nil, test_command: nil) # run it once at the very end, so the final commit can be tested in CI @git_repo.push if run_git_push - display_finished_message - end - - private - - def outdated_gems - BundleOutdatedParser.new.call - end - - def display_finished_message - puts '-------------' - puts '-------------' - puts 'FINISHED' + presenter.stop end end end diff --git a/safe_update.gemspec b/safe_update.gemspec index 8774eb4..effcf7c 100644 --- a/safe_update.gemspec +++ b/safe_update.gemspec @@ -21,4 +21,6 @@ Gem::Specification.new do |spec| spec.add_development_dependency "bundler", "~> 1.11" spec.add_development_dependency "rake", "~> 10.0" spec.add_development_dependency "rspec", "~> 3.0" + + spec.add_runtime_dependency 'curses', '~> 1.0.1' end diff --git a/spec/presenter_test.rb b/spec/presenter_test.rb new file mode 100644 index 0000000..6ab9eff --- /dev/null +++ b/spec/presenter_test.rb @@ -0,0 +1,57 @@ +$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) +require 'safe_update' + +outdated_gems = [] +outdated_gems << SafeUpdate::OutdatedGem.new( + gem_name: 'rails', + newest: '1.2.3', + installed: '1.2.1', + requested: '~> 1.2.0', +) + +outdated_gems << SafeUpdate::OutdatedGem.new( + gem_name: 'rspec', + newest: '1.2.3', + installed: '1.2.1', + requested: '~> 1.2.0', +) + +outdated_gems << SafeUpdate::OutdatedGem.new( + gem_name: 'byebug', + newest: '1.2.3', + installed: '1.2.1', + requested: '~> 1.2.0', +) + +outdated_gems << SafeUpdate::OutdatedGem.new( + gem_name: 'bullet', + newest: '3.2.1', + installed: '1.2.1', +) + +states = [ + SafeUpdate::OutdatedGem::STATUS_PENDING, + SafeUpdate::OutdatedGem::STATUS_UPDATING, + SafeUpdate::OutdatedGem::STATUS_TESTING, + SafeUpdate::OutdatedGem::STATUS_UPDATED, + SafeUpdate::OutdatedGem::STATUS_UNCHANGED, + SafeUpdate::OutdatedGem::STATUS_TESTS_FAIL, +] + +outdated_gems.map do |outdated_gem| + outdated_gem.instance_variable_set( + :@current_status, + states.sample + ) +end + +presenter = SafeUpdate::Presenter.new +Thread.new { presenter.call(outdated_gems) } +20.times do + sleep 0.25 + outdated_gems.sample.instance_variable_set( + :@current_status, + states.sample + ) +end +presenter.stop