Skip to content

Commit

Permalink
use curses for in-place updates on status
Browse files Browse the repository at this point in the history
  • Loading branch information
joshuapaling committed Oct 19, 2016
1 parent a3dacbc commit 43dacdc
Show file tree
Hide file tree
Showing 7 changed files with 173 additions and 23 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
1 change: 1 addition & 0 deletions lib/safe_update.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
22 changes: 12 additions & 10 deletions lib/safe_update/outdated_gem.rb
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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

Expand Down
93 changes: 93 additions & 0 deletions lib/safe_update/presenter.rb
Original file line number Diff line number Diff line change
@@ -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
19 changes: 6 additions & 13 deletions lib/safe_update/updater.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
2 changes: 2 additions & 0 deletions safe_update.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -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
57 changes: 57 additions & 0 deletions spec/presenter_test.rb
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 43dacdc

Please sign in to comment.