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
data/lib/fatty/action.rb
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fatty
|
|
4
|
+
class ActionError < StandardError; end
|
|
5
|
+
|
|
6
|
+
module Actions
|
|
7
|
+
@defs = {} # { Symbol => { owner:, on:, doc:, method: } }
|
|
8
|
+
|
|
9
|
+
def self.names
|
|
10
|
+
@defs.keys.sort
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.valid_names
|
|
14
|
+
names.map(&:to_s)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.catalog
|
|
18
|
+
names.map do |name|
|
|
19
|
+
defn = @defs[name]
|
|
20
|
+
{
|
|
21
|
+
name: name.to_s,
|
|
22
|
+
doc: defn[:doc],
|
|
23
|
+
on: defn[:on].to_s,
|
|
24
|
+
method: defn[:method].to_s,
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.catalog_by_target
|
|
30
|
+
@defs
|
|
31
|
+
.group_by { |_name, defn| defn[:on].to_s }
|
|
32
|
+
.sort
|
|
33
|
+
.to_h do |target, entries|
|
|
34
|
+
names = entries.map { |name, _defn| name.to_s }.sort
|
|
35
|
+
[target, names]
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.snapshot
|
|
40
|
+
@defs.dup
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.restore(snapshot)
|
|
44
|
+
@defs = snapshot
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.reset!
|
|
48
|
+
@defs.clear
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.defs
|
|
52
|
+
@defs
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.lookup(name)
|
|
56
|
+
@defs[name.to_sym]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.register(name, owner:, on:, method_name: name, doc: nil)
|
|
60
|
+
arg_str = "Action.register: name: #{name}, on: #{on}, method_name: #{method_name}, doc: #{doc}"
|
|
61
|
+
Fatty.debug("Action.register(#{arg_str})", tag: :action)
|
|
62
|
+
key = name.to_sym
|
|
63
|
+
raise ActionError, "action already registered for #{key}" if @defs.key?(key)
|
|
64
|
+
|
|
65
|
+
@defs[key] = {
|
|
66
|
+
owner: owner,
|
|
67
|
+
on: on.to_sym,
|
|
68
|
+
doc: doc,
|
|
69
|
+
method: method_name.to_sym,
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def self.call(name, env, *args, **kwargs)
|
|
74
|
+
arg_str = "name: #{name}, env: #{env}, args: #{args}, kwargs: #{kwargs}"
|
|
75
|
+
Fatty.debug("Action.call(#{arg_str})", tag: :action)
|
|
76
|
+
key = name.to_sym
|
|
77
|
+
defn = @defs[key] or raise ActionError, "Unknown action: #{key}"
|
|
78
|
+
|
|
79
|
+
target = env.public_send(defn[:on])
|
|
80
|
+
raise ActionError, "env.#{defn[:on]} is nil for action #{key}" unless target
|
|
81
|
+
|
|
82
|
+
# Inject count: from env.counter when the target accepts it.
|
|
83
|
+
if env.counter&.active? && !kwargs.key?(:count)
|
|
84
|
+
meth = target.method(defn[:method])
|
|
85
|
+
params = meth.parameters
|
|
86
|
+
|
|
87
|
+
accepts_count =
|
|
88
|
+
params.any? { |(kind, pname)| kind == :key && pname == :count } ||
|
|
89
|
+
params.any? { |(kind, _pname)| kind == :keyrest }
|
|
90
|
+
|
|
91
|
+
if accepts_count
|
|
92
|
+
kwargs = kwargs.merge(count: env.counter.consume(default: 1))
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
target.public_send(defn[:method], *args, **kwargs)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def self.registered?(name)
|
|
100
|
+
@defs.key?(name.to_sym)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fatty
|
|
4
|
+
# For holding the environment in which an action is executed
|
|
5
|
+
class ActionEnvironment
|
|
6
|
+
attr_accessor :session, :event, :counter, :buffer, :field, :pager
|
|
7
|
+
|
|
8
|
+
def initialize(
|
|
9
|
+
session: nil,
|
|
10
|
+
terminal: nil,
|
|
11
|
+
counter: nil,
|
|
12
|
+
event: nil,
|
|
13
|
+
buffer: nil,
|
|
14
|
+
field: nil,
|
|
15
|
+
pager: nil
|
|
16
|
+
)
|
|
17
|
+
@session = session
|
|
18
|
+
@terminal = terminal
|
|
19
|
+
@counter = counter
|
|
20
|
+
@event = event
|
|
21
|
+
@buffer = buffer
|
|
22
|
+
@field = field
|
|
23
|
+
@pager = pager
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def terminal
|
|
27
|
+
session&.terminal
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def to_s
|
|
31
|
+
parts = []
|
|
32
|
+
parts << "session: #{session}" if session
|
|
33
|
+
parts << "terminal: #{terminal}" if terminal
|
|
34
|
+
parts << "counter: #{counter}" if counter
|
|
35
|
+
parts << "event: #{event.to_s[0..90]}" if event
|
|
36
|
+
parts << "buffer: #{buffer}" if buffer
|
|
37
|
+
parts << "field: #{field}" if field
|
|
38
|
+
parts << "pager: #{pager}" if pager
|
|
39
|
+
parts.join('; ')
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fatty
|
|
4
|
+
module Actionable
|
|
5
|
+
def self.included(base)
|
|
6
|
+
base.extend(ClassMethods)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
module ClassMethods
|
|
10
|
+
# Optional override; by default infer slot from class name (InputBuffer -> :buffer)
|
|
11
|
+
def action_on(sym = nil)
|
|
12
|
+
@default_action_target = sym&.to_sym
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def default_action_target
|
|
16
|
+
@default_action_target || infer_action_target
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def desc(text)
|
|
20
|
+
@__next_action_doc = text.to_s
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def next_action_doc
|
|
24
|
+
doc = @__next_action_doc
|
|
25
|
+
@__next_action_doc = nil
|
|
26
|
+
doc
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Define instance method AND register as bindable action.
|
|
30
|
+
#
|
|
31
|
+
# Examples:
|
|
32
|
+
# action :bol { @cursor = 0 }
|
|
33
|
+
# action :insert { |str| ... }
|
|
34
|
+
# action :backward_char, to: :move_left # alias action name to a method
|
|
35
|
+
#
|
|
36
|
+
def action(name, on: default_action_target, to: name, doc: nil, &block)
|
|
37
|
+
name = name.to_sym
|
|
38
|
+
on = on.to_sym
|
|
39
|
+
to = to.to_sym
|
|
40
|
+
|
|
41
|
+
# Prefer desc() if present, else doc: kwarg
|
|
42
|
+
doc ||= next_action_doc
|
|
43
|
+
|
|
44
|
+
if block
|
|
45
|
+
# define the underlying method (usually same as name)
|
|
46
|
+
define_method(to, &block)
|
|
47
|
+
elsif name != to && !method_defined?(name)
|
|
48
|
+
# If this is an alias (action name differs from method), define the alias method
|
|
49
|
+
|
|
50
|
+
# Prefer a real alias if the target method already exists; otherwise define a delegator.
|
|
51
|
+
if method_defined?(to)
|
|
52
|
+
alias_method name, to
|
|
53
|
+
else
|
|
54
|
+
define_method(name) { |*args, &blk| public_send(to, *args, &blk) }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
Fatty::Actions.register(name, owner: self, on: on, method_name: to, doc: doc)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def infer_action_target
|
|
63
|
+
base = name.split("::").last
|
|
64
|
+
return :buffer if base == "InputBuffer"
|
|
65
|
+
return :field if base == "InputField"
|
|
66
|
+
return :terminal if base == "Terminal"
|
|
67
|
+
return :pager if base == "Pager"
|
|
68
|
+
|
|
69
|
+
base.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase.to_sym
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
data/lib/fatty/alert.rb
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fatty
|
|
4
|
+
class Alert
|
|
5
|
+
DETAIL_ORDER = %i[terminal key ctrl meta shift].freeze
|
|
6
|
+
|
|
7
|
+
attr_reader :level, :message, :details
|
|
8
|
+
|
|
9
|
+
# Return a new Alert object
|
|
10
|
+
#
|
|
11
|
+
# @param message [String]
|
|
12
|
+
# @param level [:info, :warn, :error]
|
|
13
|
+
# @param details [Hash|String]
|
|
14
|
+
# @param sticky [Boolean]
|
|
15
|
+
# @return [Alert]
|
|
16
|
+
def initialize(message:, level: :info, details: nil, sticky: false)
|
|
17
|
+
@message = message
|
|
18
|
+
@level = level.to_sym
|
|
19
|
+
@details = details
|
|
20
|
+
@sticky = !!sticky
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Return a new Alert object at level info
|
|
24
|
+
#
|
|
25
|
+
# @param msg [String]
|
|
26
|
+
# @return [Alert] with level info
|
|
27
|
+
def self.info(msg)
|
|
28
|
+
new(message: msg, level: :info)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Return a new Alert object at level warn
|
|
32
|
+
#
|
|
33
|
+
# @param msg [String]
|
|
34
|
+
# @return [Alert] with level warn
|
|
35
|
+
def self.warn(msg)
|
|
36
|
+
new(message: msg, level: :warn)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Return a new Alert object at level error
|
|
40
|
+
#
|
|
41
|
+
# @param msg [String]
|
|
42
|
+
# @return [Alert] with level error
|
|
43
|
+
def self.error(msg)
|
|
44
|
+
new(message: msg, level: :error)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Translate the "level" to a "role" used by the renderers. The returned
|
|
48
|
+
# roles are "composite" roles in the resolver in that they take the
|
|
49
|
+
# background ot the alert panel and apply a foreground color based on
|
|
50
|
+
# severity.
|
|
51
|
+
# @param level [:info, :warn, :error]
|
|
52
|
+
# @return [Symbol] composite or semantic role
|
|
53
|
+
# used by renderer (e.g., alert_good, :alert_info, :alert_warn, :alert_error)
|
|
54
|
+
def role
|
|
55
|
+
case level
|
|
56
|
+
when :good then :alert_good
|
|
57
|
+
when :warn then :alert_warn
|
|
58
|
+
when :error then :alert_error
|
|
59
|
+
else :alert_info
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Build a string version of the Alert suitable for display to the user.
|
|
64
|
+
def format
|
|
65
|
+
icon =
|
|
66
|
+
case level
|
|
67
|
+
when :warn then "⚠"
|
|
68
|
+
when :error then "✖"
|
|
69
|
+
else "ℹ"
|
|
70
|
+
end
|
|
71
|
+
msg = message.to_s
|
|
72
|
+
details_str = ""
|
|
73
|
+
if details.respond_to?(:empty?) ? !details.empty? : !!details
|
|
74
|
+
details_str = if details.is_a?(Hash)
|
|
75
|
+
" (" +
|
|
76
|
+
DETAIL_ORDER.filter_map { |k| "#{k}=#{details[k]}" if details.key?(k) }.join(" ") +
|
|
77
|
+
")"
|
|
78
|
+
else
|
|
79
|
+
" (#{details})"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
"#{icon} #{msg}#{details_str}"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Return whether this Alert is sticky, meaning that it should not be
|
|
86
|
+
# cleared until a key is presses or another Alert displayed
|
|
87
|
+
#
|
|
88
|
+
# @return [true, false] is this Alert sticky?
|
|
89
|
+
def sticky?
|
|
90
|
+
@sticky
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fatty
|
|
4
|
+
module Ansi
|
|
5
|
+
class Renderer
|
|
6
|
+
CSI = "\e["
|
|
7
|
+
|
|
8
|
+
def initialize(io: nil)
|
|
9
|
+
@io = io
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def render_line(row:, col:, width:, text:, role:, palette:)
|
|
13
|
+
spec = palette&.[](role)
|
|
14
|
+
return unless spec
|
|
15
|
+
|
|
16
|
+
msg = text.to_s.tr("\r\n", " ")
|
|
17
|
+
msg = Fatty::Ansi.truncate_visible(msg, width)
|
|
18
|
+
visible = Fatty::Ansi.visible_length(msg)
|
|
19
|
+
pad = width - visible
|
|
20
|
+
sgr = sgr_for_spec(spec)
|
|
21
|
+
padding = pad.positive? ? " " * pad : ""
|
|
22
|
+
write_ansi("#{CSI}#{row + 1};#{col + 1}H", sgr, msg, sgr, padding, reset)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def render_segments_line(row:, col:, width:, segments:, palette:, fill_role: :output)
|
|
26
|
+
rendered = +""
|
|
27
|
+
visible = 0
|
|
28
|
+
segments.each do |seg|
|
|
29
|
+
spec = palette&.[](seg[:role])
|
|
30
|
+
next unless spec
|
|
31
|
+
|
|
32
|
+
remaining = width - visible
|
|
33
|
+
break if remaining <= 0
|
|
34
|
+
|
|
35
|
+
text = Fatty::Ansi.truncate_visible(seg[:text].to_s, remaining)
|
|
36
|
+
next if text.empty?
|
|
37
|
+
|
|
38
|
+
visible += Fatty::Ansi.visible_length(text)
|
|
39
|
+
|
|
40
|
+
sgr =
|
|
41
|
+
if seg[:style]
|
|
42
|
+
sgr_for_style(seg[:style], fallback_spec: spec)
|
|
43
|
+
else
|
|
44
|
+
sgr_for_spec(spec)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
rendered << sgr
|
|
48
|
+
rendered << text
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
if visible < width
|
|
52
|
+
spec = palette&.[](fill_role)
|
|
53
|
+
rendered << sgr_for_spec(spec) if spec
|
|
54
|
+
rendered << (" " * (width - visible))
|
|
55
|
+
end
|
|
56
|
+
write_ansi("#{CSI}#{row + 1};#{col + 1}H", rendered, reset)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def write_ansi(*parts)
|
|
60
|
+
text = parts.join
|
|
61
|
+
|
|
62
|
+
if defined?(::Curses) && ::Curses.respond_to?(:putp)
|
|
63
|
+
::Curses.putp(text)
|
|
64
|
+
else
|
|
65
|
+
io.write(text)
|
|
66
|
+
io.flush
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
nil
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def io
|
|
73
|
+
@io || $stdout
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def save_cursor
|
|
79
|
+
"#{CSI}s"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def restore_cursor
|
|
83
|
+
"#{CSI}u"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def reset
|
|
87
|
+
"#{CSI}0m"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def move_to(row, col)
|
|
91
|
+
"#{CSI}#{row + 1};#{col + 1}H"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def sgr_for_spec(spec)
|
|
95
|
+
fg = spec[:fg_rgb]
|
|
96
|
+
bg = spec[:bg_rgb]
|
|
97
|
+
codes = []
|
|
98
|
+
|
|
99
|
+
Array(spec[:attrs]).each do |attr|
|
|
100
|
+
case attr.to_sym
|
|
101
|
+
when :bold
|
|
102
|
+
codes << "1"
|
|
103
|
+
when :dim
|
|
104
|
+
codes << "2"
|
|
105
|
+
when :underline
|
|
106
|
+
codes << "4"
|
|
107
|
+
when :reverse
|
|
108
|
+
if fg && bg
|
|
109
|
+
fg, bg = bg, fg
|
|
110
|
+
else
|
|
111
|
+
codes << "7"
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
parts = [reset]
|
|
117
|
+
parts << "#{CSI}#{codes.join(';')}m" unless codes.empty?
|
|
118
|
+
parts << "#{CSI}38;2;#{fg[0]};#{fg[1]};#{fg[2]}m" if fg
|
|
119
|
+
parts << "#{CSI}48;2;#{bg[0]};#{bg[1]};#{bg[2]}m" if bg
|
|
120
|
+
|
|
121
|
+
return reset if parts.empty?
|
|
122
|
+
|
|
123
|
+
parts.join
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def sgr_for_style(style, fallback_spec:)
|
|
127
|
+
fg = rgb_for_style_color(style.fg, fallback: fallback_spec[:fg_rgb])
|
|
128
|
+
bg = rgb_for_style_color(style.bg, fallback: fallback_spec[:bg_rgb])
|
|
129
|
+
|
|
130
|
+
codes = []
|
|
131
|
+
codes << "1" if style.bold
|
|
132
|
+
codes << "3" if style.italic
|
|
133
|
+
codes << "4" if style.underline
|
|
134
|
+
codes << "9" if style.strike
|
|
135
|
+
|
|
136
|
+
if style.reverse
|
|
137
|
+
if fg && bg
|
|
138
|
+
fg, bg = bg, fg
|
|
139
|
+
else
|
|
140
|
+
codes << "7"
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
parts = [reset]
|
|
145
|
+
parts << "#{CSI}#{codes.join(';')}m" unless codes.empty?
|
|
146
|
+
parts << "#{CSI}38;2;#{fg[0]};#{fg[1]};#{fg[2]}m" if fg
|
|
147
|
+
parts << "#{CSI}48;2;#{bg[0]};#{bg[1]};#{bg[2]}m" if bg
|
|
148
|
+
|
|
149
|
+
parts.join
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def rgb_for_style_color(color, fallback:)
|
|
153
|
+
case color
|
|
154
|
+
when Array
|
|
155
|
+
color
|
|
156
|
+
when Integer
|
|
157
|
+
if color.between?(0, 15)
|
|
158
|
+
Fatty::Color::ANSI_RGB[color]
|
|
159
|
+
else
|
|
160
|
+
Fatty::Color.xterm_rgb_for_index(color)
|
|
161
|
+
end
|
|
162
|
+
else
|
|
163
|
+
fallback
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|