fatty 0.99.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.
- checksums.yaml +7 -0
- data/.envrc +2 -0
- data/.simplecov +23 -0
- data/.yardopts +4 -0
- data/CHANGELOG.md +34 -0
- data/CHANGELOG.org +38 -0
- data/LICENSE.txt +21 -0
- data/README.md +31 -0
- data/README.org +166 -0
- data/Rakefile +15 -0
- data/TODO.org +163 -0
- data/examples/markdown/native-markdown.md +370 -0
- data/examples/markdown/ox-gfm-markdown.md +373 -0
- data/examples/markdown/ox-gfm-markdown.org +376 -0
- data/exe/fatty +275 -0
- data/fatty.gemspec +42 -0
- data/lib/fatty/accept_env.rb +32 -0
- data/lib/fatty/action.rb +103 -0
- data/lib/fatty/action_environment.rb +42 -0
- data/lib/fatty/actionable.rb +73 -0
- data/lib/fatty/alert.rb +93 -0
- data/lib/fatty/ansi/renderer.rb +168 -0
- data/lib/fatty/ansi.rb +352 -0
- data/lib/fatty/colors/color.rb +379 -0
- data/lib/fatty/colors/pairs.rb +73 -0
- data/lib/fatty/colors/palette.rb +73 -0
- data/lib/fatty/colors/rgb.txt +788 -0
- data/lib/fatty/colors.rb +5 -0
- data/lib/fatty/config.rb +86 -0
- data/lib/fatty/config_files/config.yml +50 -0
- data/lib/fatty/config_files/help.md +120 -0
- data/lib/fatty/config_files/help.org +124 -0
- data/lib/fatty/config_files/keybindings.yml +49 -0
- data/lib/fatty/config_files/keydefs.yml +23 -0
- data/lib/fatty/config_files/themes/mono.yml +76 -0
- data/lib/fatty/config_files/themes/nordic.yml +77 -0
- data/lib/fatty/config_files/themes/solarized_dark.yml +77 -0
- data/lib/fatty/config_files/themes/terminal.yml +90 -0
- data/lib/fatty/config_files/themes/wordperfect.yml +77 -0
- data/lib/fatty/config_files/themes/wordperfect_light.yml +77 -0
- data/lib/fatty/core_ext/string.rb +21 -0
- data/lib/fatty/core_ext.rb +3 -0
- data/lib/fatty/counter.rb +81 -0
- data/lib/fatty/curses/context.rb +279 -0
- data/lib/fatty/curses/curses_coder.rb +684 -0
- data/lib/fatty/curses/event_source.rb +230 -0
- data/lib/fatty/curses/key_decoder.rb +183 -0
- data/lib/fatty/curses/patch.rb +116 -0
- data/lib/fatty/curses/window_styling.rb +32 -0
- data/lib/fatty/curses.rb +16 -0
- data/lib/fatty/env.rb +100 -0
- data/lib/fatty/help.rb +41 -0
- data/lib/fatty/history/entry.rb +71 -0
- data/lib/fatty/history.rb +289 -0
- data/lib/fatty/input_buffer.rb +998 -0
- data/lib/fatty/input_field.rb +507 -0
- data/lib/fatty/key_event.rb +342 -0
- data/lib/fatty/key_map.rb +392 -0
- data/lib/fatty/keymaps/emacs.rb +189 -0
- data/lib/fatty/log_formats/json.rb +47 -0
- data/lib/fatty/log_formats/text.rb +67 -0
- data/lib/fatty/logger.rb +142 -0
- data/lib/fatty/markdown/ansi_renderer.rb +373 -0
- data/lib/fatty/markdown/render.rb +22 -0
- data/lib/fatty/markdown.rb +4 -0
- data/lib/fatty/menu_env.rb +22 -0
- data/lib/fatty/mouse_event.rb +32 -0
- data/lib/fatty/output_buffer.rb +78 -0
- data/lib/fatty/pager.rb +801 -0
- data/lib/fatty/prompt.rb +40 -0
- data/lib/fatty/renderer/curses.rb +697 -0
- data/lib/fatty/renderer/truecolor.rb +607 -0
- data/lib/fatty/renderer.rb +419 -0
- data/lib/fatty/screen.rb +96 -0
- data/lib/fatty/search.rb +43 -0
- data/lib/fatty/session/alert_session.rb +52 -0
- data/lib/fatty/session/input_session.rb +99 -0
- data/lib/fatty/session/isearch_session.rb +172 -0
- data/lib/fatty/session/keytest_session.rb +236 -0
- data/lib/fatty/session/modal_session.rb +61 -0
- data/lib/fatty/session/output_session.rb +105 -0
- data/lib/fatty/session/popup_session.rb +540 -0
- data/lib/fatty/session/prompt_session.rb +157 -0
- data/lib/fatty/session/search_session.rb +136 -0
- data/lib/fatty/session/shell_session.rb +566 -0
- data/lib/fatty/session.rb +173 -0
- data/lib/fatty/sessions.rb +14 -0
- data/lib/fatty/terminal/popup_owner.rb +26 -0
- data/lib/fatty/terminal/progress.rb +374 -0
- data/lib/fatty/terminal.rb +1067 -0
- data/lib/fatty/themes/loader.rb +136 -0
- data/lib/fatty/themes/manager.rb +71 -0
- data/lib/fatty/themes/registry.rb +64 -0
- data/lib/fatty/themes/resolver.rb +224 -0
- data/lib/fatty/themes/themes.rb +131 -0
- data/lib/fatty/themes.rb +6 -0
- data/lib/fatty/version.rb +5 -0
- data/lib/fatty/view/alert_view.rb +14 -0
- data/lib/fatty/view/cursor_view.rb +18 -0
- data/lib/fatty/view/input_view.rb +9 -0
- data/lib/fatty/view/output_view.rb +9 -0
- data/lib/fatty/view/status_view.rb +14 -0
- data/lib/fatty/view.rb +33 -0
- data/lib/fatty/viewport.rb +90 -0
- data/lib/fatty/views.rb +9 -0
- data/lib/fatty.rb +55 -0
- data/sig/fatty.rbs +4 -0
- metadata +250 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fatty
|
|
4
|
+
# Base class for stateful runtime components.
|
|
5
|
+
#
|
|
6
|
+
# Charm/Bubbletea-style contract:
|
|
7
|
+
#
|
|
8
|
+
# init(terminal:) => commands
|
|
9
|
+
# update(message) => commands
|
|
10
|
+
# view(screen:, renderer:, terminal:) => renders only (no return contract)
|
|
11
|
+
#
|
|
12
|
+
# Where `commands` is an Array (possibly empty). Terminal is responsible for
|
|
13
|
+
# executing commands after each update cycle.
|
|
14
|
+
class Session
|
|
15
|
+
include Actionable
|
|
16
|
+
|
|
17
|
+
attr_reader :terminal, :views, :keymap, :counter
|
|
18
|
+
|
|
19
|
+
def initialize(keymap: nil, views: [])
|
|
20
|
+
@keymap = keymap
|
|
21
|
+
@views = Array(views)
|
|
22
|
+
@counter = Counter.new
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def inspect
|
|
26
|
+
"#{self.class.name}:#{object_id}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def add_view(view)
|
|
30
|
+
@views << view
|
|
31
|
+
view
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Called once when the session becomes active (e.g. pushed).
|
|
35
|
+
# Subclasses may override to kick off timers/async work, etc.
|
|
36
|
+
def init(terminal:)
|
|
37
|
+
@terminal = terminal
|
|
38
|
+
[]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Handle a message and return commands.
|
|
42
|
+
def update(message)
|
|
43
|
+
Fatty.debug("#{self.class}#update(message -> #{message})", tag: :session)
|
|
44
|
+
|
|
45
|
+
commands =
|
|
46
|
+
case message[0]
|
|
47
|
+
when :key
|
|
48
|
+
ev = message[1]
|
|
49
|
+
action, args = resolve_action(ev)
|
|
50
|
+
|
|
51
|
+
Fatty.debug(
|
|
52
|
+
"#{self.class}#update: key ev=#{ev.inspect} action=#{action.inspect} args=#{args.inspect}",
|
|
53
|
+
tag: :session,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
if action
|
|
57
|
+
handle_action(action, args, event: ev)
|
|
58
|
+
else
|
|
59
|
+
update_key(ev)
|
|
60
|
+
end
|
|
61
|
+
when :cmd
|
|
62
|
+
Fatty.debug("#{self.class}#update: cmd message=#{message.inspect}", tag: :session)
|
|
63
|
+
update_cmd(message[1], message[2])
|
|
64
|
+
else
|
|
65
|
+
Fatty.warn("#{self.class}#update: unknown message[0]=#{message[0].inspect}", tag: :session)
|
|
66
|
+
[]
|
|
67
|
+
end
|
|
68
|
+
commands
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Save any state we want saved on quit, error, etc.
|
|
72
|
+
def persist!
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def tick
|
|
76
|
+
false
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def resolve_action(ev)
|
|
80
|
+
return [nil, []] unless keymap
|
|
81
|
+
|
|
82
|
+
keymap.resolve_action(ev, contexts: keymap_contexts)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Subclasses can override to vary contexts dynamically
|
|
86
|
+
# (paging/isearch/popup/etc). Must return an Array of symbols in
|
|
87
|
+
# precedence order.
|
|
88
|
+
def keymap_contexts
|
|
89
|
+
[:input]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Subclasses override this to react to resolved actions.
|
|
93
|
+
def handle_action(_action, _args, event:)
|
|
94
|
+
[]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
desc "Accumulate a count with a decimal digit"
|
|
98
|
+
action :count_digit, on: :session do |n|
|
|
99
|
+
counter.push_digit(n)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
desc "Clear the accumulated count"
|
|
103
|
+
action :count_clear, on: :session do
|
|
104
|
+
counter.clear!
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
desc "Universal argument (C-u)"
|
|
108
|
+
action :universal_argument, on: :session do
|
|
109
|
+
counter.universal_argument!
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Render the session.
|
|
113
|
+
#
|
|
114
|
+
# By default, renders all views belonging to the session, ordered by z-index.
|
|
115
|
+
# Subclasses can override, but should not mutate state here.
|
|
116
|
+
def view(screen:, renderer:)
|
|
117
|
+
views.sort_by(&:z).each do |v|
|
|
118
|
+
v.render(screen:, renderer:, terminal:, session: self)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def close
|
|
123
|
+
nil
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def handle_resize
|
|
127
|
+
[]
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
private
|
|
131
|
+
|
|
132
|
+
def update_key(_ev)
|
|
133
|
+
[]
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def update_cmd(_name, _payload)
|
|
137
|
+
[]
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def match_all_query_terms?(haystack, query)
|
|
141
|
+
Fatty::Search.match_all_terms?(haystack, query)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def safely_close_window(win)
|
|
145
|
+
return unless win
|
|
146
|
+
|
|
147
|
+
begin
|
|
148
|
+
win.erase
|
|
149
|
+
rescue RuntimeError => e
|
|
150
|
+
raise unless closed_window_error?(e)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
begin
|
|
154
|
+
win.noutrefresh if win.respond_to?(:noutrefresh)
|
|
155
|
+
rescue RuntimeError => e
|
|
156
|
+
raise unless closed_window_error?(e)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
begin
|
|
160
|
+
win.close
|
|
161
|
+
rescue RuntimeError => e
|
|
162
|
+
raise unless closed_window_error?(e)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
nil
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def closed_window_error?(error)
|
|
169
|
+
message = error.message
|
|
170
|
+
message.include?("closed window") || message.include?("already closed window")
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "session"
|
|
4
|
+
|
|
5
|
+
require_relative "session/alert_session"
|
|
6
|
+
require_relative "session/modal_session"
|
|
7
|
+
require_relative "session/popup_session"
|
|
8
|
+
require_relative "session/search_session"
|
|
9
|
+
require_relative "session/isearch_session"
|
|
10
|
+
require_relative "session/input_session"
|
|
11
|
+
require_relative "session/output_session"
|
|
12
|
+
require_relative "session/shell_session"
|
|
13
|
+
require_relative 'session/prompt_session'
|
|
14
|
+
require_relative 'session/keytest_session'
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fatty
|
|
4
|
+
class Terminal
|
|
5
|
+
class PopupOwner
|
|
6
|
+
attr_reader :on_result, :on_cancel
|
|
7
|
+
|
|
8
|
+
def initialize(on_result: nil, on_cancel: nil)
|
|
9
|
+
@on_result = on_result
|
|
10
|
+
@on_cancel = on_cancel
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def update(msg)
|
|
14
|
+
_cmd, name, payload = msg
|
|
15
|
+
|
|
16
|
+
case name
|
|
17
|
+
when :popup_result, :prompt_result
|
|
18
|
+
on_result&.call(payload)
|
|
19
|
+
when :popup_cancelled, :prompt_cancelled
|
|
20
|
+
on_cancel&.call
|
|
21
|
+
end
|
|
22
|
+
[]
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fatty
|
|
4
|
+
class Terminal
|
|
5
|
+
class Progress
|
|
6
|
+
PARTIAL_BLOCKS = ["", "▏", "▎", "▍", "▌", "▋", "▊", "▉"].freeze
|
|
7
|
+
FULL_BLOCK = "█"
|
|
8
|
+
EMPTY_BAR = "."
|
|
9
|
+
|
|
10
|
+
SHADE_EMPTY = "░"
|
|
11
|
+
SHADE_HALF = "▒"
|
|
12
|
+
SHADE_FULL = "▓"
|
|
13
|
+
|
|
14
|
+
BRAILLE_STEPS = ["⠀", "⣀", "⣄", "⣆", "⣇", "⣧", "⣷", "⣿"].freeze
|
|
15
|
+
|
|
16
|
+
attr_reader :terminal, :label, :total, :style, :role, :width
|
|
17
|
+
|
|
18
|
+
# The width parameter's purpose varies by style:
|
|
19
|
+
#
|
|
20
|
+
# :trail width = max visible width of the trail portion
|
|
21
|
+
# :bar width = max bar width
|
|
22
|
+
# :unicode_bar width = max bar width
|
|
23
|
+
# :braille_bar width = max bar width
|
|
24
|
+
# :spinner width ignored, unless later used for suffix/trail
|
|
25
|
+
# :count width ignored
|
|
26
|
+
# :percent width ignored
|
|
27
|
+
def initialize(terminal:, label:, total: nil, style: :percent, role: :info, width: 40)
|
|
28
|
+
@terminal = terminal
|
|
29
|
+
@label = label.to_s
|
|
30
|
+
@total = total&.to_i
|
|
31
|
+
@style = style.to_sym
|
|
32
|
+
@role = role
|
|
33
|
+
@width = width.to_i
|
|
34
|
+
@trail = []
|
|
35
|
+
@current = 0
|
|
36
|
+
@spinner_index = 0
|
|
37
|
+
|
|
38
|
+
validate_total_requirement!
|
|
39
|
+
|
|
40
|
+
refresh
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def update(current: nil, total: @total, label: @label, indicator: nil, render: false)
|
|
44
|
+
@current = current.to_i unless current.nil?
|
|
45
|
+
@total = total&.to_i
|
|
46
|
+
@label = label.to_s
|
|
47
|
+
|
|
48
|
+
if style == :spinner
|
|
49
|
+
advance_spinner
|
|
50
|
+
else
|
|
51
|
+
append_indicator(indicator)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
refresh
|
|
55
|
+
terminal.render_now if render
|
|
56
|
+
self
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def finish(message = nil, clear: false, role: @role, render: false, transient: true)
|
|
60
|
+
if clear
|
|
61
|
+
terminal.clear_status
|
|
62
|
+
else
|
|
63
|
+
text =
|
|
64
|
+
if message && !message.empty?
|
|
65
|
+
render_text(suffix: message)
|
|
66
|
+
else
|
|
67
|
+
render_text
|
|
68
|
+
end
|
|
69
|
+
terminal.set_status(text, role: role, transient: transient)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
terminal.render_now if render
|
|
73
|
+
self
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def clear
|
|
77
|
+
terminal.clear_status
|
|
78
|
+
self
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
attr_reader :current
|
|
84
|
+
|
|
85
|
+
def append_indicator(indicator)
|
|
86
|
+
return if indicator.nil?
|
|
87
|
+
|
|
88
|
+
item =
|
|
89
|
+
if indicator.is_a?(Hash) && indicator.key?(:text)
|
|
90
|
+
indicator
|
|
91
|
+
else
|
|
92
|
+
indicator.to_s
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
return if renderable_indicator_text(item).empty?
|
|
96
|
+
|
|
97
|
+
@trail << item
|
|
98
|
+
trim_trail if @width && @width > 0
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def renderable_indicator_text(item)
|
|
102
|
+
if item.is_a?(Hash) && item.key?(:text)
|
|
103
|
+
item[:text].to_s
|
|
104
|
+
else
|
|
105
|
+
item.to_s
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def refresh
|
|
110
|
+
terminal.set_status(render_text, role: role)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def render_text(suffix: nil)
|
|
114
|
+
case style
|
|
115
|
+
when :count
|
|
116
|
+
base = total ? "#{label} [#{current}/#{total}]" : "#{label} [#{current}]"
|
|
117
|
+
suffix_text(base, suffix)
|
|
118
|
+
when :simple_percent
|
|
119
|
+
base = total && total > 0 ? "#{label} #{percent}%" : label.to_s
|
|
120
|
+
suffix_text(base, suffix)
|
|
121
|
+
when :trail
|
|
122
|
+
render_trail_text(suffix: suffix)
|
|
123
|
+
when :bar
|
|
124
|
+
render_bar_text(suffix: suffix, mode: :solid)
|
|
125
|
+
when :unicode_bar
|
|
126
|
+
render_bar_text(suffix: suffix, mode: :unicode)
|
|
127
|
+
when :braille_bar
|
|
128
|
+
render_bar_text(suffix: suffix, mode: :braille)
|
|
129
|
+
when :spinner
|
|
130
|
+
render_spinner_text(suffix: suffix)
|
|
131
|
+
else
|
|
132
|
+
base = total && total > 0 ? "#{label} [#{current}/#{total}] #{percent}%" : "#{label} [#{current}]"
|
|
133
|
+
suffix_text(base, suffix)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def percent
|
|
138
|
+
((current.to_f / total.to_f) * 100).round
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def suffix_text(base, suffix)
|
|
142
|
+
extra = suffix.to_s
|
|
143
|
+
return base if extra.empty?
|
|
144
|
+
|
|
145
|
+
"#{base} #{extra}"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def render_trail_text(suffix: nil)
|
|
149
|
+
base =
|
|
150
|
+
if total && total > 0
|
|
151
|
+
"#{label} [#{current}/#{total}] #{percent}%"
|
|
152
|
+
else
|
|
153
|
+
"#{label} [#{current}]"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
trailer = suffix.to_s.empty? ? "" : " #{suffix}"
|
|
157
|
+
return "#{base}#{trailer}" if @trail.empty?
|
|
158
|
+
|
|
159
|
+
limit = trail_limit(base, trailer)
|
|
160
|
+
trail = limited_trail(limit)
|
|
161
|
+
[base, " ", *trail, trailer]
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def trail_limit(prefix, suffix = "")
|
|
165
|
+
cols =
|
|
166
|
+
if terminal.screen
|
|
167
|
+
terminal.screen.cols.to_i
|
|
168
|
+
else
|
|
169
|
+
80
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
available = cols - visible_length(prefix) - visible_length(suffix) - 4
|
|
173
|
+
available = 0 if available < 0
|
|
174
|
+
available
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def limited_trail(limit)
|
|
178
|
+
used = 0
|
|
179
|
+
selected = []
|
|
180
|
+
|
|
181
|
+
@trail.reverse_each do |item|
|
|
182
|
+
width = visible_length(item)
|
|
183
|
+
break if used + width > limit
|
|
184
|
+
|
|
185
|
+
selected.unshift(item)
|
|
186
|
+
used += width
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
selected
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def trim_trail
|
|
193
|
+
used = 0
|
|
194
|
+
selected = []
|
|
195
|
+
|
|
196
|
+
@trail.reverse_each do |item|
|
|
197
|
+
width = visible_length(item)
|
|
198
|
+
break if used + width > @width
|
|
199
|
+
|
|
200
|
+
selected.unshift(item)
|
|
201
|
+
used += width
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
@trail = selected
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def visible_length(value)
|
|
208
|
+
if value.is_a?(Hash) && value.key?(:text)
|
|
209
|
+
Fatty::Ansi.visible_length(value[:text].to_s)
|
|
210
|
+
else
|
|
211
|
+
Fatty::Ansi.visible_length(value.to_s)
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def render_bar_text(suffix: nil, mode: :solid)
|
|
216
|
+
base = label.to_s
|
|
217
|
+
info = progress_info_text
|
|
218
|
+
extra = suffix.to_s
|
|
219
|
+
trailer = extra.empty? ? "" : " #{extra}"
|
|
220
|
+
|
|
221
|
+
bar_width = bar_width_for(base, info, trailer)
|
|
222
|
+
bar =
|
|
223
|
+
case mode
|
|
224
|
+
when :unicode
|
|
225
|
+
unicode_bar(bar_width)
|
|
226
|
+
when :braille
|
|
227
|
+
braille_bar(bar_width)
|
|
228
|
+
else
|
|
229
|
+
solid_bar(bar_width)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
pieces = [base]
|
|
233
|
+
pieces << "[#{bar}]" unless bar.empty?
|
|
234
|
+
pieces << info unless info.empty?
|
|
235
|
+
|
|
236
|
+
text = pieces.join(" ")
|
|
237
|
+
text = "#{text}#{trailer}" unless trailer.empty?
|
|
238
|
+
text
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def render_spinner_text(suffix: nil)
|
|
242
|
+
frame = spinner_frames[@spinner_index % spinner_frames.length]
|
|
243
|
+
base =
|
|
244
|
+
if total && total > 0
|
|
245
|
+
"#{label} #{frame} #{percent}%"
|
|
246
|
+
else
|
|
247
|
+
"#{label} #{frame}"
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
suffix_text(base, suffix)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def progress_info_text
|
|
254
|
+
if total && total > 0
|
|
255
|
+
"#{percent}% [#{current}/#{total}]"
|
|
256
|
+
else
|
|
257
|
+
"[#{current}]"
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def bar_width_for(base, info, trailer)
|
|
262
|
+
cols =
|
|
263
|
+
if terminal.screen
|
|
264
|
+
terminal.screen.cols.to_i
|
|
265
|
+
else
|
|
266
|
+
80
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
reserved = visible_length(base)
|
|
270
|
+
reserved += 1 + visible_length(info) unless info.empty?
|
|
271
|
+
reserved += visible_length(trailer) unless trailer.empty?
|
|
272
|
+
|
|
273
|
+
# Space for:
|
|
274
|
+
# " ["
|
|
275
|
+
# "]"
|
|
276
|
+
# around the bar
|
|
277
|
+
available = cols - reserved - 4
|
|
278
|
+
available = 0 if available < 0
|
|
279
|
+
[available, @width].min
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def solid_bar(width)
|
|
283
|
+
return "" if width <= 0
|
|
284
|
+
|
|
285
|
+
ratio = progress_ratio
|
|
286
|
+
filled = (ratio * width).round
|
|
287
|
+
filled = 0 if filled < 0
|
|
288
|
+
filled = width if filled > width
|
|
289
|
+
|
|
290
|
+
empty = width - filled
|
|
291
|
+
(FULL_BLOCK * filled) + (EMPTY_BAR * empty)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def unicode_bar(width)
|
|
295
|
+
return "" if width <= 0
|
|
296
|
+
|
|
297
|
+
raw = progress_ratio * width
|
|
298
|
+
full = raw.floor
|
|
299
|
+
|
|
300
|
+
# Show a transition block whenever the bar is in progress but not complete,
|
|
301
|
+
# provided there is space for it.
|
|
302
|
+
half =
|
|
303
|
+
if progress_ratio.positive? && progress_ratio < 1.0 && full < width
|
|
304
|
+
1
|
|
305
|
+
else
|
|
306
|
+
0
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
bar = +""
|
|
310
|
+
bar << (SHADE_FULL * full)
|
|
311
|
+
bar << SHADE_HALF if half == 1
|
|
312
|
+
|
|
313
|
+
remaining = width - Fatty::Ansi.visible_length(bar)
|
|
314
|
+
remaining = 0 if remaining < 0
|
|
315
|
+
bar << (SHADE_EMPTY * remaining)
|
|
316
|
+
bar
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def braille_bar(width)
|
|
320
|
+
return "" if width <= 0
|
|
321
|
+
|
|
322
|
+
raw = progress_ratio * width
|
|
323
|
+
full = raw.floor
|
|
324
|
+
remainder = raw - full
|
|
325
|
+
|
|
326
|
+
partial_idx = (remainder * (BRAILLE_STEPS.length - 1)).round
|
|
327
|
+
partial_idx = 0 if partial_idx < 0
|
|
328
|
+
partial_idx = BRAILLE_STEPS.length - 1 if partial_idx >= BRAILLE_STEPS.length
|
|
329
|
+
|
|
330
|
+
bar = +""
|
|
331
|
+
bar << (BRAILLE_STEPS[-1] * full)
|
|
332
|
+
|
|
333
|
+
if progress_ratio.positive? && progress_ratio < 1.0 && full < width
|
|
334
|
+
partial =
|
|
335
|
+
if partial_idx.zero?
|
|
336
|
+
BRAILLE_STEPS[1]
|
|
337
|
+
else
|
|
338
|
+
BRAILLE_STEPS[partial_idx]
|
|
339
|
+
end
|
|
340
|
+
bar << partial
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
remaining = width - Fatty::Ansi.visible_length(bar)
|
|
344
|
+
remaining = 0 if remaining < 0
|
|
345
|
+
bar << (BRAILLE_STEPS[0] * remaining)
|
|
346
|
+
bar
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def progress_ratio
|
|
350
|
+
return 0.0 unless total && total > 0
|
|
351
|
+
|
|
352
|
+
ratio = current.to_f / total.to_f
|
|
353
|
+
ratio = 0.0 if ratio < 0.0
|
|
354
|
+
ratio = 1.0 if ratio > 1.0
|
|
355
|
+
ratio
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def validate_total_requirement!
|
|
359
|
+
return if style == :spinner
|
|
360
|
+
return if total && total > 0
|
|
361
|
+
|
|
362
|
+
raise ArgumentError, "progress style #{style.inspect} requires total:"
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def advance_spinner
|
|
366
|
+
@spinner_index = (@spinner_index + 1) % spinner_frames.length
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def spinner_frames
|
|
370
|
+
BRAILLE_STEPS[1..]
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
end
|