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,90 @@
|
|
|
1
|
+
# lib/fatty/config_files/themes/terminal.yml
|
|
2
|
+
name: terminal
|
|
3
|
+
inherit: null
|
|
4
|
+
|
|
5
|
+
output:
|
|
6
|
+
fg: default
|
|
7
|
+
bg: default
|
|
8
|
+
|
|
9
|
+
input:
|
|
10
|
+
fg: default
|
|
11
|
+
bg: default
|
|
12
|
+
|
|
13
|
+
input_suggestion:
|
|
14
|
+
fg: default
|
|
15
|
+
bg: default
|
|
16
|
+
attrs: [dim]
|
|
17
|
+
|
|
18
|
+
cursor:
|
|
19
|
+
fg: default
|
|
20
|
+
bg: default
|
|
21
|
+
attrs: [reverse]
|
|
22
|
+
|
|
23
|
+
region:
|
|
24
|
+
fg: default
|
|
25
|
+
bg: default
|
|
26
|
+
attrs: [reverse]
|
|
27
|
+
|
|
28
|
+
info:
|
|
29
|
+
fg: default
|
|
30
|
+
bg: default
|
|
31
|
+
|
|
32
|
+
good:
|
|
33
|
+
fg: default
|
|
34
|
+
bg: default
|
|
35
|
+
attrs: [bold]
|
|
36
|
+
|
|
37
|
+
warn:
|
|
38
|
+
fg: default
|
|
39
|
+
bg: default
|
|
40
|
+
attrs: [reverse]
|
|
41
|
+
|
|
42
|
+
error:
|
|
43
|
+
fg: default
|
|
44
|
+
bg: default
|
|
45
|
+
attrs: [bold, reverse]
|
|
46
|
+
|
|
47
|
+
pager_status:
|
|
48
|
+
fg: default
|
|
49
|
+
bg: default
|
|
50
|
+
attrs: [reverse]
|
|
51
|
+
|
|
52
|
+
search_input:
|
|
53
|
+
fg: default
|
|
54
|
+
bg: default
|
|
55
|
+
attrs: [reverse]
|
|
56
|
+
|
|
57
|
+
match_current:
|
|
58
|
+
fg: default
|
|
59
|
+
bg: default
|
|
60
|
+
attrs: [reverse]
|
|
61
|
+
|
|
62
|
+
match_other:
|
|
63
|
+
fg: default
|
|
64
|
+
bg: default
|
|
65
|
+
attrs: [underline]
|
|
66
|
+
|
|
67
|
+
popup:
|
|
68
|
+
fg: default
|
|
69
|
+
bg: default
|
|
70
|
+
|
|
71
|
+
popup_selection:
|
|
72
|
+
fg: default
|
|
73
|
+
bg: default
|
|
74
|
+
attrs: [reverse]
|
|
75
|
+
|
|
76
|
+
popup_input:
|
|
77
|
+
fg: default
|
|
78
|
+
bg: default
|
|
79
|
+
attrs: [reverse]
|
|
80
|
+
|
|
81
|
+
popup_frame:
|
|
82
|
+
fg: default
|
|
83
|
+
bg: default
|
|
84
|
+
border: single
|
|
85
|
+
corners: square
|
|
86
|
+
|
|
87
|
+
popup_counts:
|
|
88
|
+
fg: default
|
|
89
|
+
bg: default
|
|
90
|
+
attrs: [bold]
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
name: wordperfect
|
|
2
|
+
inherit: null
|
|
3
|
+
|
|
4
|
+
output:
|
|
5
|
+
fg: white
|
|
6
|
+
bg: navy
|
|
7
|
+
|
|
8
|
+
input:
|
|
9
|
+
fg: white
|
|
10
|
+
bg: navy
|
|
11
|
+
attrs: [bold]
|
|
12
|
+
|
|
13
|
+
input_suggestion:
|
|
14
|
+
fg: lightgray
|
|
15
|
+
bg: navy
|
|
16
|
+
|
|
17
|
+
cursor:
|
|
18
|
+
fg: white
|
|
19
|
+
bg: red
|
|
20
|
+
|
|
21
|
+
region:
|
|
22
|
+
fg: navy
|
|
23
|
+
bg: yellow
|
|
24
|
+
|
|
25
|
+
good:
|
|
26
|
+
fg: green
|
|
27
|
+
bg: navy
|
|
28
|
+
|
|
29
|
+
info:
|
|
30
|
+
fg: black
|
|
31
|
+
bg: navy
|
|
32
|
+
|
|
33
|
+
warn:
|
|
34
|
+
fg: black
|
|
35
|
+
bg: magenta
|
|
36
|
+
|
|
37
|
+
error:
|
|
38
|
+
fg: white
|
|
39
|
+
bg: red
|
|
40
|
+
|
|
41
|
+
pager_status:
|
|
42
|
+
fg: black
|
|
43
|
+
bg: lightgreen
|
|
44
|
+
|
|
45
|
+
search_input:
|
|
46
|
+
fg: black
|
|
47
|
+
bg: cyan
|
|
48
|
+
|
|
49
|
+
match_current:
|
|
50
|
+
fg: black
|
|
51
|
+
bg: red
|
|
52
|
+
|
|
53
|
+
match_other:
|
|
54
|
+
fg: grey
|
|
55
|
+
bg: pink
|
|
56
|
+
|
|
57
|
+
popup:
|
|
58
|
+
fg: white
|
|
59
|
+
bg: navy
|
|
60
|
+
|
|
61
|
+
popup_input:
|
|
62
|
+
fg: white
|
|
63
|
+
bg: navy
|
|
64
|
+
|
|
65
|
+
popup_counts:
|
|
66
|
+
fg: navy
|
|
67
|
+
bg: red
|
|
68
|
+
|
|
69
|
+
popup_selection:
|
|
70
|
+
fg: navy
|
|
71
|
+
bg: yellow
|
|
72
|
+
|
|
73
|
+
popup_frame:
|
|
74
|
+
fg: navy
|
|
75
|
+
bg: yellow
|
|
76
|
+
border: single
|
|
77
|
+
corners: rounded
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
name: wordperfect_light
|
|
2
|
+
inherit: wordperfect
|
|
3
|
+
|
|
4
|
+
output:
|
|
5
|
+
fg: white
|
|
6
|
+
bg: lightblue
|
|
7
|
+
|
|
8
|
+
# input:
|
|
9
|
+
# fg: white
|
|
10
|
+
# bg: navy
|
|
11
|
+
# attrs: [bold]
|
|
12
|
+
|
|
13
|
+
# input_suggestion:
|
|
14
|
+
# fg: lightgray
|
|
15
|
+
# bg: navy
|
|
16
|
+
|
|
17
|
+
# cursor:
|
|
18
|
+
# fg: white
|
|
19
|
+
# bg: red
|
|
20
|
+
|
|
21
|
+
# region:
|
|
22
|
+
# fg: navy
|
|
23
|
+
# bg: yellow
|
|
24
|
+
|
|
25
|
+
# good:
|
|
26
|
+
# fg: green
|
|
27
|
+
# bg: navy
|
|
28
|
+
|
|
29
|
+
# info:
|
|
30
|
+
# fg: black
|
|
31
|
+
# bg: navy
|
|
32
|
+
|
|
33
|
+
# warn:
|
|
34
|
+
# fg: black
|
|
35
|
+
# bg: magenta
|
|
36
|
+
|
|
37
|
+
# error:
|
|
38
|
+
# fg: white
|
|
39
|
+
# bg: red
|
|
40
|
+
|
|
41
|
+
# pager_status:
|
|
42
|
+
# fg: black
|
|
43
|
+
# bg: lightgreen
|
|
44
|
+
|
|
45
|
+
# search_input:
|
|
46
|
+
# fg: black
|
|
47
|
+
# bg: cyan
|
|
48
|
+
|
|
49
|
+
# match_current:
|
|
50
|
+
# fg: black
|
|
51
|
+
# bg: red
|
|
52
|
+
|
|
53
|
+
# match_other:
|
|
54
|
+
# fg: grey
|
|
55
|
+
# bg: pink
|
|
56
|
+
|
|
57
|
+
# popup:
|
|
58
|
+
# fg: white
|
|
59
|
+
# bg: navy
|
|
60
|
+
|
|
61
|
+
# popup_input:
|
|
62
|
+
# fg: white
|
|
63
|
+
# bg: navy
|
|
64
|
+
|
|
65
|
+
# popup_counts:
|
|
66
|
+
# fg: navy
|
|
67
|
+
# bg: red
|
|
68
|
+
|
|
69
|
+
# popup_selection:
|
|
70
|
+
# fg: navy
|
|
71
|
+
# bg: yellow
|
|
72
|
+
|
|
73
|
+
# popup_frame:
|
|
74
|
+
# fg: navy
|
|
75
|
+
# bg: yellow
|
|
76
|
+
# border: single
|
|
77
|
+
# corners: rounded
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# These can be used to add "roles" to strings so that they can be styled
|
|
4
|
+
# according to the theme.
|
|
5
|
+
class String
|
|
6
|
+
def fatty_good
|
|
7
|
+
{ text: self, role: :good }
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def fatty_info
|
|
11
|
+
{ text: self, role: :info }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def fatty_warn
|
|
15
|
+
{ text: self, role: :warn }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def fatty_error
|
|
19
|
+
{ text: self, role: :error }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fatty
|
|
4
|
+
# Counter accumulates and otherwise managed a numeric prefix count (e.g.,
|
|
5
|
+
# "12").
|
|
6
|
+
#
|
|
7
|
+
# Intended usage:
|
|
8
|
+
# counter.push_digit(1)
|
|
9
|
+
# counter.push_digit(2)
|
|
10
|
+
# n = counter.consume(default: 1) # => 12 (and clears)
|
|
11
|
+
#
|
|
12
|
+
class Counter
|
|
13
|
+
MAX_DIGITS = 6
|
|
14
|
+
|
|
15
|
+
def initialize
|
|
16
|
+
@digits = +""
|
|
17
|
+
@replace_next = false
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def to_s
|
|
21
|
+
"Counter: #{@digits}; #{@replace_next}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def active?
|
|
25
|
+
!@digits.empty?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def clear!
|
|
29
|
+
@digits.clear
|
|
30
|
+
@replace_next = false
|
|
31
|
+
self
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def digits
|
|
35
|
+
@digits.dup
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def push_digit(n)
|
|
39
|
+
if @replace_next
|
|
40
|
+
@digits.clear
|
|
41
|
+
@replace_next = false
|
|
42
|
+
end
|
|
43
|
+
if @digits.length < MAX_DIGITS
|
|
44
|
+
@digits << n.to_i.to_s
|
|
45
|
+
end
|
|
46
|
+
self
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def set(n)
|
|
50
|
+
@digits = n.to_i.to_s
|
|
51
|
+
@replace_next = false
|
|
52
|
+
self
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def universal_argument!
|
|
56
|
+
if active?
|
|
57
|
+
set(value * 4)
|
|
58
|
+
else
|
|
59
|
+
set(4)
|
|
60
|
+
@replace_next = true # next digit replaces the "4"
|
|
61
|
+
end
|
|
62
|
+
self
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def value
|
|
66
|
+
if active?
|
|
67
|
+
@digits.to_i
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def consume(default: nil)
|
|
72
|
+
n = value
|
|
73
|
+
if n.nil?
|
|
74
|
+
default
|
|
75
|
+
else
|
|
76
|
+
clear!
|
|
77
|
+
n
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "curses"
|
|
4
|
+
|
|
5
|
+
module Fatty
|
|
6
|
+
module Curses
|
|
7
|
+
# Context represents the active curses environment.
|
|
8
|
+
#
|
|
9
|
+
# It owns:
|
|
10
|
+
# - curses initialization and shutdown
|
|
11
|
+
# - terminal mode configuration
|
|
12
|
+
# - window lifecycle
|
|
13
|
+
#
|
|
14
|
+
# Context does NOT:
|
|
15
|
+
# - read input
|
|
16
|
+
# - decode keys
|
|
17
|
+
# - render UI
|
|
18
|
+
#
|
|
19
|
+
# Those responsibilities belong to EventSource and Renderer.
|
|
20
|
+
#
|
|
21
|
+
# Context exists so that all curses state is centralized and never leaks
|
|
22
|
+
# into Sessions, Views, or Terminal.
|
|
23
|
+
class Context
|
|
24
|
+
DEFAULT_ESC_DELAY = 25
|
|
25
|
+
|
|
26
|
+
attr_reader :input_win, :output_win, :status_win, :alert_win
|
|
27
|
+
attr_reader :rows, :cols, :palette, :truecolor
|
|
28
|
+
|
|
29
|
+
def initialize
|
|
30
|
+
@started = false
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def start
|
|
34
|
+
return self if @started
|
|
35
|
+
|
|
36
|
+
::Curses.init_screen
|
|
37
|
+
configure_escape_delay!
|
|
38
|
+
MouseConstants.ensure!
|
|
39
|
+
|
|
40
|
+
::Curses.raw
|
|
41
|
+
::Curses.noecho
|
|
42
|
+
::Curses.curs_set(1)
|
|
43
|
+
::Curses.stdscr.keypad(true)
|
|
44
|
+
::Curses.mousemask(::Curses::ALL_MOUSE_EVENTS)
|
|
45
|
+
enable_bracketed_paste!
|
|
46
|
+
setup_colors
|
|
47
|
+
@truecolor = truecolor_enabled?
|
|
48
|
+
@started = true
|
|
49
|
+
self
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def configure_escape_delay!
|
|
53
|
+
delay =
|
|
54
|
+
if ENV["ESCDELAY"]
|
|
55
|
+
ENV["ESCDELAY"].to_i
|
|
56
|
+
else
|
|
57
|
+
Fatty::Config.config.dig(:esc_delay)&.to_i
|
|
58
|
+
end
|
|
59
|
+
delay = DEFAULT_ESC_DELAY if delay.nil? || delay <= 0
|
|
60
|
+
if ::Curses.respond_to?(:set_escdelay)
|
|
61
|
+
::Curses.set_escdelay(delay)
|
|
62
|
+
else
|
|
63
|
+
ENV["ESCDELAY"] = delay.to_s
|
|
64
|
+
end
|
|
65
|
+
Fatty.info("ESC delay set to #{delay} ms", tag: :input)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def setup_colors
|
|
69
|
+
return unless ::Curses.has_colors?
|
|
70
|
+
|
|
71
|
+
reset_ansi_pairs!
|
|
72
|
+
::Curses.start_color
|
|
73
|
+
::Curses.use_default_colors if ::Curses.respond_to?(:use_default_colors)
|
|
74
|
+
|
|
75
|
+
theme_spec = Fatty::Themes::Manager.roles(Fatty::Themes::Manager.current) || {}
|
|
76
|
+
palette = Fatty::Colors::Palette.compile(
|
|
77
|
+
theme_spec,
|
|
78
|
+
available_colors: ::Curses.colors,
|
|
79
|
+
)
|
|
80
|
+
apply_palette(palette)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Allocate or reallocate windows using Screen layout.
|
|
84
|
+
def apply_layout(screen)
|
|
85
|
+
ensure_started!
|
|
86
|
+
|
|
87
|
+
@rows = screen.rows
|
|
88
|
+
@cols = screen.cols
|
|
89
|
+
|
|
90
|
+
close_windows
|
|
91
|
+
|
|
92
|
+
out = screen.output_rect
|
|
93
|
+
sts = screen.status_rect
|
|
94
|
+
inp = screen.input_rect
|
|
95
|
+
alr = screen.alert_rect
|
|
96
|
+
|
|
97
|
+
@output_win = ::Curses::Window.new(out.rows, out.cols, out.row, out.col)
|
|
98
|
+
@status_win = ::Curses::Window.new(sts.rows, sts.cols, sts.row, sts.col)
|
|
99
|
+
@input_win = ::Curses::Window.new(inp.rows, inp.cols, inp.row, inp.col)
|
|
100
|
+
@alert_win = ::Curses::Window.new(alr.rows, alr.cols, alr.row, alr.col)
|
|
101
|
+
|
|
102
|
+
# We do our own viewport/paging; allowing curses to scroll introduces
|
|
103
|
+
# “mystery” blank lines if a newline slips into output
|
|
104
|
+
@output_win.scrollok(true)
|
|
105
|
+
@input_win.keypad(true)
|
|
106
|
+
self
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def close
|
|
110
|
+
close_windows
|
|
111
|
+
disable_bracketed_paste! if @started
|
|
112
|
+
::Curses.close_screen if @started
|
|
113
|
+
@started = false
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Map a Fatty::Ansi::Style to a curses attribute.
|
|
117
|
+
#
|
|
118
|
+
# - If style has no explicit fg/bg, we keep the themed role pair.
|
|
119
|
+
# - If style specifies fg/bg, we allocate/init a curses pair on demand.
|
|
120
|
+
#
|
|
121
|
+
# Note: this is intentionally independent of theme roles; it is for SGR
|
|
122
|
+
# output runs inside the output pane.
|
|
123
|
+
def truecolor_enabled?
|
|
124
|
+
cfg = Fatty::Config.config
|
|
125
|
+
|
|
126
|
+
setting =
|
|
127
|
+
if cfg.key?(:truecolor)
|
|
128
|
+
cfg[:truecolor]
|
|
129
|
+
else
|
|
130
|
+
"auto"
|
|
131
|
+
end
|
|
132
|
+
@truecolor =
|
|
133
|
+
case setting.to_s.downcase
|
|
134
|
+
when "true", "yes", "on", "1"
|
|
135
|
+
true
|
|
136
|
+
when "false", "no", "off", "0"
|
|
137
|
+
false
|
|
138
|
+
else
|
|
139
|
+
truecolor_env?
|
|
140
|
+
end
|
|
141
|
+
Fatty.info(
|
|
142
|
+
"truecolor=#{@truecolor} setting=#{setting.inspect} " \
|
|
143
|
+
"TERM=#{ENV['TERM'].inspect} COLORTERM=#{ENV['COLORTERM'].inspect} " \
|
|
144
|
+
"TERM_PROGRAM=#{ENV['TERM_PROGRAM'].inspect} TMUX=#{ENV.key?('TMUX')}",
|
|
145
|
+
tag: :themes,
|
|
146
|
+
)
|
|
147
|
+
@truecolor
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def truecolor_env?
|
|
151
|
+
colorterm = ENV["COLORTERM"].to_s
|
|
152
|
+
term = ENV["TERM"].to_s
|
|
153
|
+
term_program = ENV["TERM_PROGRAM"].to_s
|
|
154
|
+
|
|
155
|
+
colorterm.match?(/truecolor|24bit/i) ||
|
|
156
|
+
term.match?(/truecolor|24bit|direct/i) ||
|
|
157
|
+
term.match?(/kitty|wezterm|alacritty|ghostty|foot/i) ||
|
|
158
|
+
term_program.match?(/kitty|wezterm|alacritty|ghostty|iTerm/i)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Map a Fatty::Ansi::Style to a curses attribute.
|
|
162
|
+
#
|
|
163
|
+
# - If style has no explicit fg/bg, keep the themed role pair.
|
|
164
|
+
# - If style specifies fg/bg, allocate/init a curses pair on demand.
|
|
165
|
+
#
|
|
166
|
+
# This is intentionally independent of theme roles; it is for SGR output
|
|
167
|
+
# runs inside the output pane.
|
|
168
|
+
def ansi_attr(style, fallback_role: :output)
|
|
169
|
+
base_pair_id = Fatty::Colors::Pairs::ROLE_TO_PAIR.fetch(fallback_role)
|
|
170
|
+
base_attr = ::Curses.color_pair(base_pair_id)
|
|
171
|
+
|
|
172
|
+
has_explicit = !(style.fg.nil? && style.bg.nil?)
|
|
173
|
+
attr =
|
|
174
|
+
if has_explicit
|
|
175
|
+
pair_id = ansi_pair_id(style.fg, style.bg, fallback_pair_id: base_pair_id)
|
|
176
|
+
::Curses.color_pair(pair_id)
|
|
177
|
+
else
|
|
178
|
+
base_attr
|
|
179
|
+
end
|
|
180
|
+
attr |= ::Curses::A_BOLD if style.bold
|
|
181
|
+
attr |= ::Curses::A_UNDERLINE if style.underline
|
|
182
|
+
attr |= ::Curses::A_REVERSE if style.reverse
|
|
183
|
+
if style.italic && defined?(::Curses::A_ITALIC)
|
|
184
|
+
attr |= ::Curses::A_ITALIC
|
|
185
|
+
end
|
|
186
|
+
if style.strike && defined?(::Curses::A_HORIZONTAL)
|
|
187
|
+
attr |= ::Curses::A_HORIZONTAL
|
|
188
|
+
end
|
|
189
|
+
attr
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def apply_palette(palette)
|
|
193
|
+
if ::Curses.has_colors?
|
|
194
|
+
::Curses.start_color
|
|
195
|
+
::Curses.use_default_colors if ::Curses.respond_to?(:use_default_colors)
|
|
196
|
+
palette.each_value do |entry|
|
|
197
|
+
next unless entry[:pair]
|
|
198
|
+
|
|
199
|
+
::Curses.init_pair(entry[:pair], entry[:fg], entry[:bg])
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
@palette = palette
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
private
|
|
206
|
+
|
|
207
|
+
def ensure_started!
|
|
208
|
+
return if @started
|
|
209
|
+
|
|
210
|
+
raise "Curses::Context not started"
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def close_windows
|
|
214
|
+
@output_win&.close
|
|
215
|
+
@input_win&.close
|
|
216
|
+
@alert_win&.close
|
|
217
|
+
@status_win&.close
|
|
218
|
+
|
|
219
|
+
@output_win = nil
|
|
220
|
+
@input_win = nil
|
|
221
|
+
@alert_win = nil
|
|
222
|
+
@status_win = nil
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Generate a map from [fg, bg] pairs to Cuses pair ids.
|
|
226
|
+
def ansi_pair_id(fg, bg, fallback_pair_id:)
|
|
227
|
+
has_colors = ::Curses.has_colors?
|
|
228
|
+
return fallback_pair_id unless has_colors
|
|
229
|
+
|
|
230
|
+
@ansi_pairs ||= {}
|
|
231
|
+
@ansi_pair_limit ||= begin
|
|
232
|
+
::Curses.color_pairs
|
|
233
|
+
rescue StandardError
|
|
234
|
+
256
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
key = [fg || -1, bg || -1]
|
|
238
|
+
cached = @ansi_pairs[key]
|
|
239
|
+
return cached if cached
|
|
240
|
+
|
|
241
|
+
# Leave low pair ids for stable theme roles.
|
|
242
|
+
start_id = 64
|
|
243
|
+
@ansi_next_pair_id ||= start_id
|
|
244
|
+
@ansi_next_pair_id = start_id if @ansi_next_pair_id < start_id
|
|
245
|
+
|
|
246
|
+
if @ansi_next_pair_id >= @ansi_pair_limit
|
|
247
|
+
@ansi_pairs[key] = fallback_pair_id
|
|
248
|
+
else
|
|
249
|
+
pair_id = @ansi_next_pair_id
|
|
250
|
+
@ansi_next_pair_id += 1
|
|
251
|
+
|
|
252
|
+
fg_i = fg.nil? ? -1 : fg.to_i
|
|
253
|
+
bg_i = bg.nil? ? -1 : bg.to_i
|
|
254
|
+
::Curses.init_pair(pair_id, fg_i, bg_i)
|
|
255
|
+
@ansi_pairs[key] = pair_id
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
@ansi_pairs[key]
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def reset_ansi_pairs!
|
|
262
|
+
@ansi_pairs = {}
|
|
263
|
+
@ansi_next_pair_id = 1
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def enable_bracketed_paste!
|
|
267
|
+
$stdout.write("\e[?2004h")
|
|
268
|
+
$stdout.flush
|
|
269
|
+
nil
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def disable_bracketed_paste!
|
|
273
|
+
$stdout.write("\e[?2004l")
|
|
274
|
+
$stdout.flush
|
|
275
|
+
nil
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|