Remove Whirly and show concurrent downloads.

This commit is contained in:
Markus Reiter 2024-07-15 16:31:21 -04:00
parent 3a51f55572
commit 6434533ff5
No known key found for this signature in database
GPG Key ID: 245293B51702655B
12 changed files with 128 additions and 2128 deletions

View File

@ -84,4 +84,3 @@ gem "plist"
gem "ruby-macho"
gem "sorbet-runtime"
gem "warning"
gem "whirly"

View File

@ -80,10 +80,36 @@ module Homebrew
end
end
class Spinner
FRAMES = [
"",
"",
"",
"",
"",
"",
].freeze
sig { void }
def initialize
@start = Time.now
@i = 0
end
sig { returns(String) }
def to_s
now = Time.now
if @start + 0.1 < now
@start = now
@i = (@i + 1) % FRAMES.count
end
FRAMES.fetch(@i)
end
end
sig { override.void }
def run
require "whirly"
Formulary.enable_factory_cache!
bucket = if args.deps?
@ -194,24 +220,83 @@ module Homebrew
end
end
downloads.each do |downloadable, promise|
message = "#{downloadable.download_type.capitalize} #{downloadable.name}"
if concurrency > 1
Whirly.start spinner: "arc", status: message
else
puts message
if concurrency == 1
downloads.each_value do |promise|
promise.wait!
rescue ChecksumMismatchError => e
opoo "#{downloadable.download_type.capitalize} reports different checksum: #{e.expected}"
Homebrew.failed = true if downloadable.is_a?(Resource::Patch)
end
else
promise.wait!
spinner = Spinner.new
Whirly.configure stop: "#{Tty.green}✔︎#{Tty.reset}"
Whirly.stop if args.concurrency
rescue ChecksumMismatchError => e
Whirly.configure stop: "#{Tty.red}#{Tty.reset}"
Whirly.stop if args.concurrency
remaining_downloads = downloads.dup
opoo "#{downloadable.download_type.capitalize} reports different checksum: #{e.expected}"
Homebrew.failed = true if downloadable.is_a?(Resource::Patch)
previous_pending_line_count = 0
begin
print Tty.hide_cursor
output_message = lambda do |downloadable, promise|
status = case promise.state
when :fulfilled
"#{Tty.green}✔︎#{Tty.reset}"
when :rejected
"#{Tty.red}#{Tty.reset}"
when :pending
spinner
else
raise promise.state
end
message = "#{downloadable.download_type.capitalize} #{downloadable.name}"
puts "#{status} #{message}"
if promise.rejected? && (e = promise.reason).is_a?(ChecksumMismatchError)
opoo "#{downloadable.download_type.capitalize} reports different checksum: #{e.expected}"
Homebrew.failed = true if downloadable.is_a?(Resource::Patch)
next 2
end
1
end
until remaining_downloads.empty?
begin
finished_downloads = {}
finished_states = [:fulfilled, :rejected]
remaining_downloads.each do |downloadable, promise|
break unless finished_states.include?(promise.state)
finished_downloads[downloadable] = remaining_downloads.delete(downloadable)
end
finished_downloads.each do |downloadable, promise|
previous_pending_line_count -= 1
output_message.call(downloadable, promise)
end
previous_pending_line_count = 0
remaining_downloads.each do |downloadable, promise|
break if previous_pending_line_count >= (Tty.height - 1)
previous_pending_line_count += output_message.call(downloadable, promise)
end
if previous_pending_line_count.positive?
$stdout.print "\033[#{previous_pending_line_count}A"
$stdout.flush
end
rescue Interrupt
print "\n" * previous_pending_line_count
raise
end
end
ensure
print Tty.show_cursor
end
end
download_queue.shutdown

View File

@ -1,103 +0,0 @@
# typed: true
# DO NOT EDIT MANUALLY
# This is an autogenerated file for types exported from the `whirly` gem.
# Please instead update this file by running `bin/tapioca gem whirly`.
# source://whirly//lib/whirly/version.rb#3
module Whirly
class << self
# save options and preprocess, set defaults if value is still unknown
#
# source://whirly//lib/whirly.rb#114
def configure(**options); end
# frames can be generated from enumerables or procs
#
# source://whirly//lib/whirly.rb#85
def configure_frames(spinner); end
# set spinner directly or lookup
#
# source://whirly//lib/whirly.rb#55
def configure_spinner(spinner_option); end
# @return [Boolean]
#
# source://whirly//lib/whirly.rb#49
def configured?; end
# @return [Boolean]
#
# source://whirly//lib/whirly.rb#45
def enabled?; end
# source://whirly//lib/whirly.rb#240
def initialize_color; end
# source://whirly//lib/whirly.rb#249
def next_color; end
# Returns the value of attribute options.
#
# source://whirly//lib/whirly.rb#43
def options; end
# source://whirly//lib/whirly.rb#215
def render(next_frame = T.unsafe(nil)); end
# source://whirly//lib/whirly.rb#225
def render_prefix; end
# source://whirly//lib/whirly.rb#233
def render_suffix; end
# source://whirly//lib/whirly.rb#194
def reset; end
# source://whirly//lib/whirly.rb#132
def start(**options); end
# Returns the value of attribute status.
#
# source://whirly//lib/whirly.rb#42
def status; end
# Sets the attribute status
#
# @param value the value to set the attribute status to.
#
# source://whirly//lib/whirly.rb#42
def status=(_arg0); end
# source://whirly//lib/whirly.rb#180
def stop(stop_frame = T.unsafe(nil)); end
# - - -
#
# source://whirly//lib/whirly.rb#203
def unrender; end
end
end
# source://whirly//lib/whirly.rb#14
Whirly::CLI_COMMANDS = T.let(T.unsafe(nil), Hash)
# source://whirly//lib/whirly.rb#19
Whirly::DEFAULT_OPTIONS = T.let(T.unsafe(nil), Hash)
# source://whirly//lib/whirly.rb#35
Whirly::SOFT_DEFAULT_OPTIONS = T.let(T.unsafe(nil), Hash)
# source://whirly//lib/whirly/spinners.rb#2
module Whirly::Spinners; end
# source://whirly//lib/whirly/spinners/cli.rb#5
Whirly::Spinners::CLI = T.let(T.unsafe(nil), Hash)
# source://whirly//lib/whirly/spinners/whirly.rb#5
Whirly::Spinners::WHIRLY = T.let(T.unsafe(nil), Hash)
# source://whirly//lib/whirly/version.rb#4
Whirly::VERSION = T.let(T.unsafe(nil), String)

View File

@ -51,11 +51,36 @@ module Tty
string.gsub(/\033\[\d+(;\d+)*m/, "")
end
sig { returns(String) }
def hide_cursor
"\033[?25l"
end
sig { returns(String) }
def show_cursor
"\033[?25h"
end
sig { returns(T.nilable([Integer, Integer])) }
def size
`/bin/stty size 2>/dev/null`.split&.map(&:to_i)
end
sig { returns(Integer) }
def height
@height ||= begin
height, = size
height, = `/usr/bin/tput lines 2>/dev/null`.split if height.zero?
height ||= 40
height.to_i
end
end
sig { returns(Integer) }
def width
@width ||= begin
_, width = `/bin/stty size 2>/dev/null`.split
width, = `/usr/bin/tput cols 2>/dev/null`.split if width.to_i.zero?
_, width = size
width, = `/usr/bin/tput cols 2>/dev/null`.split if width.zero?
width ||= 80
width.to_i
end

View File

@ -1,20 +0,0 @@
Copyright (c) 2016 Jan Lelis, https://janlelis.com
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,274 +0,0 @@
{
"roman_numerals": {
"interval": 90,
"mode": "swing",
"frames": [
"",
"Ⅱ",
"Ⅲ",
"Ⅳ",
"",
"Ⅵ",
"Ⅶ",
"Ⅷ",
"Ⅸ",
""
]
},
"double_mark": {
"interval": 120,
"mode": "random",
"frames": [
"⁇",
"⁈",
"⁉",
"‼"
]
},
"heart_exclamation": {
"interval": 45,
"frames": [
"❢",
"❣"
]
},
"pencil": {
"interval": 200,
"frames": [
"✏",
"✎"
]
},
"bars": {
"interval": 80,
"mode": "swing",
"frames": [
"𝍠",
"𝍡",
"𝍢",
"𝍣",
"𝍤"
]
},
"dice": {
"interval": 100,
"mode": "random",
"frames": [
"⚀",
"⚁",
"⚂",
"⚃",
"⚄",
"⚅"
]
},
"hanoi": {
"interval": 150,
"mode": "swing",
"frames": [
"𝍥",
"𝍦",
"𝍧",
"𝍨"
]
},
"vertical_bars": {
"interval": 80,
"mode": "swing",
"frames": [
"𝍩",
"𝍪",
"𝍫",
"𝍬",
"𝍭"
]
},
"whirly": {
"interval": 200,
"mode": "random",
"frames": [
"😀",
"😁",
"😂",
"😃",
"😄",
"😅",
"😆",
"😇",
"😈",
"😉",
"😊",
"😋",
"😌",
"😍",
"😎",
"😏",
"😐",
"😑",
"😒",
"😓",
"😔",
"😕",
"😖",
"😗",
"😘",
"😙",
"😚",
"😛",
"😜",
"😝",
"😞",
"😟",
"😠",
"😡",
"😢",
"😣",
"😤",
"😥",
"😦",
"😧",
"😨",
"😩",
"😪",
"😫",
"😬",
"😭",
"😮",
"😯",
"😰",
"😱",
"😲",
"😳",
"😴",
"😵",
"😶",
"🙁",
"🙂",
"🙃",
"🙄",
"😷",
"🤐",
"🤑",
"🤒",
"🤓",
"🤔",
"🤕",
"🤖",
"🤗"
]
},
"cat": {
"interval": 200,
"mode": "random",
"frames": [
"😸",
"😹",
"😺",
"😻",
"😼",
"😽",
"😾",
"😿",
"🙀"
]
},
"card": {
"interval": 90,
"stop": "🂠",
"frames": [
"🃁", "🃂", "🃃", "🃄", "🃅", "🃆", "🃇", "🃈", "🃉", "🃊", "🃋", "🃌", "🃍", "🃎",
"🂱", "🂲", "🂳", "🂴", "🂵", "🂶", "🂷", "🂸", "🂹", "🂺", "🂻", "🂼", "🂽", "🂾",
"🂡", "🂢", "🂣", "🂤", "🂥", "🂦", "🂧", "🂨", "🂩", "🂪", "🂫", "🂬", "🂭", "🂮",
"🃑", "🃒", "🃓", "🃔", "🃕", "🃖", "🃗", "🃘", "🃙", "🃚", "🃛", "🃜", "🃝", "🃞"
]
},
"cloud": {
"interval": 140,
"frames": [
"🌥",
"🌦",
"🌧",
"🌨",
"🌩",
"🌪"
]
},
"photo": {
"interval": 200,
"frames": [
"📷",
"📸"
]
},
"banknote": {
"interval": 100,
"frames": [
"💴",
"💵",
"💶",
"💷"
]
},
"white_square": {
"interval": 100,
"mode": "swing",
"frames": [
"🞓",
"🞒",
"🞑",
"🞐",
"🞏",
"🞎",
"🞔"
]
},
"xberg": {
"interval": 150,
"mode": "random",
"frames": [
"",
"⛰",
"⛰",
"⛰",
"⛰",
"⛰",
"⛰"
]
},
"circled_letter": {
"interval": 120,
"mode": "random",
"frames": [
"Ⓐ", "Ⓑ", "Ⓒ", "Ⓓ", "Ⓔ", "Ⓕ", "Ⓖ", "Ⓗ", "Ⓘ",
"Ⓙ", "Ⓚ", "Ⓛ", "Ⓜ", "Ⓝ", "Ⓞ", "Ⓟ", "Ⓠ", "Ⓡ",
"Ⓢ", "Ⓣ", "Ⓤ", "Ⓥ", "Ⓦ", "Ⓧ", "Ⓨ", "Ⓩ"
]
},
"circled_number": {
"interval": 120,
"mode": "random",
"frames": [
"①", "②", "③", "④", "⑤", "⑥", "⑦", "⑧", "⑨"
]
},
"letter_with_parens": {
"interval": 150,
"mode": "random",
"frames": [
"🄐", "🄑", "🄒", "🄓", "🄔", "🄕", "🄖", "🄗", "🄘",
"🄙", "🄚", "🄛", "🄜", "🄝", "🄞", "🄟", "🄠", "🄡",
"🄢", "🄣", "🄤", "🄥", "🄦", "🄧", "🄨", "🄩"
]
},
"starlike": {
"interval": 120,
"mode": "random",
"frames": [
"✩", "✪", "✫", "✬", "✭", "✮", "✯", "✰",
"✱", "✲", "✳", "✴", "✵", "✶", "✷", "✸",
"✹", "✺", "✻", "✼", "✽", "✾", "✿", "❀",
"❁", "❂", "❃", "❄", "❅", "❆", "❇", "❈",
"❉", "❊"
]
}
}

View File

@ -1,263 +0,0 @@
require_relative "whirly/version"
require_relative "whirly/spinners"
require "unicode/display_width"
begin
require "paint"
rescue LoadError
end
module Whirly
@configured = false
CLI_COMMANDS = {
hide_cursor: "\x1b[?25l",
show_cursor: "\x1b[?25h",
}.freeze
DEFAULT_OPTIONS = {
ambiguous_character_width: 1,
ansi_escape_mode: "restore",
append_newline: true,
color: !!defined?(Paint),
color_change_rate: 30,
hide_cursor: true,
non_tty: false,
position: "normal",
remove_after_stop: false,
spinner: "whirly",
spinner_packs: [:whirly, :cli],
status: nil,
stream: $stdout,
}.freeze
SOFT_DEFAULT_OPTIONS = {
interval: 100,
mode: "linear",
stop: nil,
}.freeze
class << self
attr_accessor :status
attr_reader :options
def enabled?
!!(defined?(@enabled) && @enabled)
end
def configured?
!!(@configured)
end
end
# set spinner directly or lookup
def self.configure_spinner(spinner_option)
case spinner_option
when Hash
spinner = spinner_option.dup
when Enumerable
spinner = { "frames" => spinner_option.dup }
when Proc
spinner = { "proc" => spinner_option.dup }
else
spinner = nil
catch(:found){
@options[:spinner_packs].each{ |spinner_pack|
spinners = Whirly::Spinners.const_get(spinner_pack.to_s.upcase)
if spinners[spinner_option]
spinner = spinners[spinner_option].dup
throw(:found)
end
}
}
end
# validate spinner
if !spinner || (!spinner["frames"] && !spinner["proc"])
raise(ArgumentError, "Whirly: Invalid spinner given")
end
spinner
end
# frames can be generated from enumerables or procs
def self.configure_frames(spinner)
if spinner["frames"]
case spinner["mode"]
when "swing"
frames = (spinner["frames"].to_a + spinner["frames"].to_a[1..-2].reverse).cycle
when "random"
frame_pool = spinner["frames"].to_a
frames = ->(){ frame_pool.sample }
when "reverse"
frames = spinner["frames"].to_a.reverse.cycle
else
frames = spinner["frames"].cycle
end
elsif spinner["proc"]
frames = spinner["proc"].dup
else
raise(ArgumentError, "Whirly: Invalid spinner given")
end
if frames.is_a? Proc
class << frames
alias next call
end
end
frames
end
# save options and preprocess, set defaults if value is still unknown
def self.configure(**options)
if !defined?(@configured) || !@configured || !defined?(@options) || !@options
@options = DEFAULT_OPTIONS.dup
@configured = true
end
@options.merge!(options)
spinner = configure_spinner(@options[:spinner])
spinner_overwrites = {}
spinner_overwrites["mode"] = @options[:mode] if @options.key?(:mode)
@frames = configure_frames(spinner.merge(spinner_overwrites))
@interval = (@options[:interval] || spinner["interval"] || SOFT_DEFAULT_OPTIONS[:interval]) * 0.001
@stop = @options[:stop] || spinner["stop"]
@status = @options[:status]
end
def self.start(**options)
# optionally overwrite configuration on start
configure(**options)
# only enable once
return false if defined?(@enabled) && @enabled
# set status to enabled
@enabled = true
# only do something if we are on a real terminal (or forced)
return false unless @options[:stream].tty? || @options[:non_tty]
# ensure cursor is visible after exit the program (only register for the very first time)
if (!defined?(@at_exit_handler_registered) || !@at_exit_handler_registered) && @options[:hide_cursor]
@at_exit_handler_registered = true
stream = @options[:stream]
at_exit{ stream.print CLI_COMMANDS[:show_cursor] }
end
# init color
initialize_color if @options[:color]
# hide cursor
@options[:stream].print CLI_COMMANDS[:hide_cursor] if @options[:hide_cursor]
# start spinner loop
@thread = Thread.new do
@current_frame = nil
while true # it's just a spinner, no exact timing here
next_color if @color
render
sleep(@interval)
end
end
# idiomatic block syntax support
if block_given?
begin
yield
ensure
Whirly.stop
end
end
true
end
def self.stop(stop_frame = nil)
return false unless @enabled
@enabled = false
return false unless @options[:stream].tty? || @options[:non_tty]
@thread.terminate if @thread
render(stop_frame || @stop) if stop_frame || @stop
unrender if @options[:remove_after_stop]
@options[:stream].puts if @options[:append_newline]
@options[:stream].print CLI_COMMANDS[:show_cursor] if @options[:hide_cursor]
true
end
def self.reset
at_exit_handler_registered = defined?(@at_exit_handler_registered) && @at_exit_handler_registered
instance_variables.each{ |iv| remove_instance_variable(iv) }
@at_exit_handler_registered = at_exit_handler_registered
@configured = false
end
# - - -
def self.unrender
return unless @current_frame
case @options[:ansi_escape_mode]
when "restore"
@options[:stream].print(render_prefix + (
' ' * (Unicode::DisplayWidth.of(@current_frame, @options[:ambiguous_character_width]) + 1)
) + render_suffix)
when "line"
@options[:stream].print "\e[1K"
end
end
def self.render(next_frame = nil)
unrender
@current_frame = next_frame || @frames.next
@current_frame = Paint[@current_frame, @color] if @options[:color]
@current_frame += " #{@status}" if @status
@options[:stream].print(render_prefix + @current_frame.to_s + render_suffix)
end
def self.render_prefix
res = ""
res << "\n" if @options[:position] == "below"
res << "\e7" if @options[:ansi_escape_mode] == "restore"
res << "\e[G" if @options[:ansi_escape_mode] == "line"
res
end
def self.render_suffix
res = ""
res << "\e8" if @options[:ansi_escape_mode] == "restore"
res << "\e[1A" if @options[:position] == "below"
res
end
def self.initialize_color
if !defined?(Paint)
warn "Whirly warning: Using colors requires the paint gem"
else
@color = "%.6x" % rand(16777216)
@color_directions = (0..2).map{ |e| rand(3) - 1 }
end
end
def self.next_color
@color = @color.scan(/../).map.with_index{ |c, i|
color_change = rand(@options[:color_change_rate]) * @color_directions[i]
nc = c.to_i(16) + color_change
if nc <= 0
nc = 0
@color_directions[i] = rand(3) - 1
elsif nc >= 255
nc = 255
@color_directions[i] = rand(3) - 1
end
"%.2x" % nc
}.join
end
end

View File

@ -1,7 +0,0 @@
module Whirly
module Spinners
end
end
require_relative "spinners/whirly"
require_relative "spinners/cli"

View File

@ -1,7 +0,0 @@
require "json"
module Whirly
module Spinners
CLI = JSON.load(File.read(File.dirname(__FILE__) + "/../../../data/cli-spinners.json")).freeze
end
end

View File

@ -1,15 +0,0 @@
require "json"
module Whirly
module Spinners
WHIRLY = {
"random_dots" => { "proc" => ->(){ [ 0x2800 + rand(256)].pack("U") }, "interval" => 100 },
"mahjong" => { "proc" => ->(){ [0x1F000 + rand(44)].pack("U") }, "interval" => 200 },
"domino" => { "proc" => ->(){ [0x1F030 + rand(50)].pack("U") }, "interval" => 200 },
"vertical_domino" => { "proc" => ->(){ [0x1F062 + rand(50)].pack("U") }, "interval" => 200 }
}
WHIRLY.merge! JSON.load(File.read(File.dirname(__FILE__) + "/../../../data/whirly-static-spinners.json"))
WHIRLY.freeze
end
end

View File

@ -1,5 +0,0 @@
# frozen_string_literal: true
module Whirly
VERSION = "0.3.0"
end