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.
Files changed (108) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc +2 -0
  3. data/.simplecov +23 -0
  4. data/.yardopts +4 -0
  5. data/CHANGELOG.md +34 -0
  6. data/CHANGELOG.org +38 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +31 -0
  9. data/README.org +166 -0
  10. data/Rakefile +15 -0
  11. data/TODO.org +163 -0
  12. data/examples/markdown/native-markdown.md +370 -0
  13. data/examples/markdown/ox-gfm-markdown.md +373 -0
  14. data/examples/markdown/ox-gfm-markdown.org +376 -0
  15. data/exe/fatty +275 -0
  16. data/fatty.gemspec +42 -0
  17. data/lib/fatty/accept_env.rb +32 -0
  18. data/lib/fatty/action.rb +103 -0
  19. data/lib/fatty/action_environment.rb +42 -0
  20. data/lib/fatty/actionable.rb +73 -0
  21. data/lib/fatty/alert.rb +93 -0
  22. data/lib/fatty/ansi/renderer.rb +168 -0
  23. data/lib/fatty/ansi.rb +352 -0
  24. data/lib/fatty/colors/color.rb +379 -0
  25. data/lib/fatty/colors/pairs.rb +73 -0
  26. data/lib/fatty/colors/palette.rb +73 -0
  27. data/lib/fatty/colors/rgb.txt +788 -0
  28. data/lib/fatty/colors.rb +5 -0
  29. data/lib/fatty/config.rb +86 -0
  30. data/lib/fatty/config_files/config.yml +50 -0
  31. data/lib/fatty/config_files/help.md +120 -0
  32. data/lib/fatty/config_files/help.org +124 -0
  33. data/lib/fatty/config_files/keybindings.yml +49 -0
  34. data/lib/fatty/config_files/keydefs.yml +23 -0
  35. data/lib/fatty/config_files/themes/mono.yml +76 -0
  36. data/lib/fatty/config_files/themes/nordic.yml +77 -0
  37. data/lib/fatty/config_files/themes/solarized_dark.yml +77 -0
  38. data/lib/fatty/config_files/themes/terminal.yml +90 -0
  39. data/lib/fatty/config_files/themes/wordperfect.yml +77 -0
  40. data/lib/fatty/config_files/themes/wordperfect_light.yml +77 -0
  41. data/lib/fatty/core_ext/string.rb +21 -0
  42. data/lib/fatty/core_ext.rb +3 -0
  43. data/lib/fatty/counter.rb +81 -0
  44. data/lib/fatty/curses/context.rb +279 -0
  45. data/lib/fatty/curses/curses_coder.rb +684 -0
  46. data/lib/fatty/curses/event_source.rb +230 -0
  47. data/lib/fatty/curses/key_decoder.rb +183 -0
  48. data/lib/fatty/curses/patch.rb +116 -0
  49. data/lib/fatty/curses/window_styling.rb +32 -0
  50. data/lib/fatty/curses.rb +16 -0
  51. data/lib/fatty/env.rb +100 -0
  52. data/lib/fatty/help.rb +41 -0
  53. data/lib/fatty/history/entry.rb +71 -0
  54. data/lib/fatty/history.rb +289 -0
  55. data/lib/fatty/input_buffer.rb +998 -0
  56. data/lib/fatty/input_field.rb +507 -0
  57. data/lib/fatty/key_event.rb +342 -0
  58. data/lib/fatty/key_map.rb +392 -0
  59. data/lib/fatty/keymaps/emacs.rb +189 -0
  60. data/lib/fatty/log_formats/json.rb +47 -0
  61. data/lib/fatty/log_formats/text.rb +67 -0
  62. data/lib/fatty/logger.rb +142 -0
  63. data/lib/fatty/markdown/ansi_renderer.rb +373 -0
  64. data/lib/fatty/markdown/render.rb +22 -0
  65. data/lib/fatty/markdown.rb +4 -0
  66. data/lib/fatty/menu_env.rb +22 -0
  67. data/lib/fatty/mouse_event.rb +32 -0
  68. data/lib/fatty/output_buffer.rb +78 -0
  69. data/lib/fatty/pager.rb +801 -0
  70. data/lib/fatty/prompt.rb +40 -0
  71. data/lib/fatty/renderer/curses.rb +697 -0
  72. data/lib/fatty/renderer/truecolor.rb +607 -0
  73. data/lib/fatty/renderer.rb +419 -0
  74. data/lib/fatty/screen.rb +96 -0
  75. data/lib/fatty/search.rb +43 -0
  76. data/lib/fatty/session/alert_session.rb +52 -0
  77. data/lib/fatty/session/input_session.rb +99 -0
  78. data/lib/fatty/session/isearch_session.rb +172 -0
  79. data/lib/fatty/session/keytest_session.rb +236 -0
  80. data/lib/fatty/session/modal_session.rb +61 -0
  81. data/lib/fatty/session/output_session.rb +105 -0
  82. data/lib/fatty/session/popup_session.rb +540 -0
  83. data/lib/fatty/session/prompt_session.rb +157 -0
  84. data/lib/fatty/session/search_session.rb +136 -0
  85. data/lib/fatty/session/shell_session.rb +566 -0
  86. data/lib/fatty/session.rb +173 -0
  87. data/lib/fatty/sessions.rb +14 -0
  88. data/lib/fatty/terminal/popup_owner.rb +26 -0
  89. data/lib/fatty/terminal/progress.rb +374 -0
  90. data/lib/fatty/terminal.rb +1067 -0
  91. data/lib/fatty/themes/loader.rb +136 -0
  92. data/lib/fatty/themes/manager.rb +71 -0
  93. data/lib/fatty/themes/registry.rb +64 -0
  94. data/lib/fatty/themes/resolver.rb +224 -0
  95. data/lib/fatty/themes/themes.rb +131 -0
  96. data/lib/fatty/themes.rb +6 -0
  97. data/lib/fatty/version.rb +5 -0
  98. data/lib/fatty/view/alert_view.rb +14 -0
  99. data/lib/fatty/view/cursor_view.rb +18 -0
  100. data/lib/fatty/view/input_view.rb +9 -0
  101. data/lib/fatty/view/output_view.rb +9 -0
  102. data/lib/fatty/view/status_view.rb +14 -0
  103. data/lib/fatty/view.rb +33 -0
  104. data/lib/fatty/viewport.rb +90 -0
  105. data/lib/fatty/views.rb +9 -0
  106. data/lib/fatty.rb +55 -0
  107. data/sig/fatty.rbs +4 -0
  108. metadata +250 -0
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module Fatty
6
+ module Themes
7
+ module Loader
8
+ RESERVED_KEYS = %i[name inherit markdown].freeze
9
+
10
+ ROLE_ALIASES = {
11
+ status_good: :good,
12
+ status_info: :info,
13
+ status_warn: :warn,
14
+ status_error: :error,
15
+
16
+ search: :search_input,
17
+ search_highlight: :match_current,
18
+ search_highlight_secondary: :match_other,
19
+ }.freeze
20
+
21
+ def self.load_dir(path, registry:)
22
+ Dir.glob(File.join(path.to_s, "*.{yml,yaml}")).sort.each do |file|
23
+ load_file(file, registry: registry)
24
+ end
25
+ registry
26
+ end
27
+
28
+ def self.load_file(path, registry:)
29
+ data = YAML.safe_load_file(
30
+ path,
31
+ permitted_classes: [Symbol],
32
+ aliases: false,
33
+ symbolize_names: true,
34
+ )
35
+
36
+ unless data.is_a?(Hash)
37
+ registry.warnings << "Theme file did not contain a mapping: #{path}"
38
+ return
39
+ end
40
+
41
+ registry.add(normalize(data), source: path)
42
+ rescue Psych::Exception => e
43
+ message = "Theme YAML error in #{path}: #{e.class}: #{e.message}"
44
+ registry.warnings << message
45
+ Fatty.warn(message, tag: :theme) if Fatty.respond_to?(:warn)
46
+ nil
47
+ rescue StandardError => e
48
+ message = "Theme load error in #{path}: #{e.class}: #{e.message}"
49
+ registry.warnings << message
50
+ Fatty.warn(message, tag: :theme) if Fatty.respond_to?(:warn)
51
+ nil
52
+ end
53
+
54
+ def self.normalize(data)
55
+ out = {
56
+ name: normalize_optional_symbol(data[:name]),
57
+ inherit: normalize_optional_symbol(data[:inherit]),
58
+ roles: {},
59
+ }
60
+
61
+ data.each do |key, value|
62
+ k = key.to_sym
63
+ next if RESERVED_KEYS.include?(k)
64
+
65
+ out[:roles][normalize_role_name(k)] = normalize_spec(value)
66
+ end
67
+
68
+ normalize_markdown_roles(data[:markdown]).each do |role, spec|
69
+ out[:roles][role] ||= spec
70
+ end
71
+
72
+ out
73
+ end
74
+
75
+ def self.normalize_role_name(name)
76
+ sym = name.to_sym
77
+ ROLE_ALIASES.fetch(sym, sym)
78
+ end
79
+
80
+ def self.normalize_markdown_roles(value)
81
+ return {} unless value.is_a?(Hash)
82
+
83
+ value.each_with_object({}) do |(key, spec), roles|
84
+ role = :"markdown_#{normalize_role_name(key)}"
85
+ roles[role] = normalize_spec(spec)
86
+ end
87
+ end
88
+
89
+ def self.normalize_spec(spec_hash)
90
+ return {} unless spec_hash.is_a?(Hash)
91
+
92
+ normalized =
93
+ spec_hash.each_with_object({}) do |(k, v), h|
94
+ key = k.to_sym
95
+ h[key] =
96
+ case key
97
+ when :attr
98
+ v
99
+ when :attrs
100
+ normalize_attrs(v)
101
+ when :border, :corners
102
+ normalize_optional_symbol(v)
103
+ else
104
+ v
105
+ end
106
+ end
107
+
108
+ if normalized.key?(:attr)
109
+ attr = normalized.delete(:attr)
110
+ attrs = normalized.key?(:attrs) ? normalized[:attrs] : []
111
+ normalized[:attrs] = normalize_attrs(attrs + Array(attr))
112
+ end
113
+
114
+ normalized
115
+ end
116
+
117
+ def self.normalize_attrs(value)
118
+ Array(value).map(&:to_sym)
119
+ end
120
+
121
+ def self.normalize_optional_symbol(value)
122
+ return if value.nil?
123
+
124
+ text = value.to_s.strip
125
+ return if text.empty?
126
+
127
+ case text.downcase
128
+ when "null", "nil", "none"
129
+ nil
130
+ else
131
+ text.to_sym
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fatty
4
+ module Themes
5
+ module Manager
6
+ FALLBACK_THEME = :terminal
7
+
8
+ def self.registry
9
+ @registry ||=
10
+ begin
11
+ reg = Registry.new
12
+ Loader.load_dir(Fatty::Config.user_themes_dir, registry: reg)
13
+ reg
14
+ end
15
+ end
16
+
17
+ def self.load!
18
+ registry.clear
19
+ Loader.load_dir(Fatty::Config.user_themes_dir, registry: registry)
20
+ registry
21
+ end
22
+
23
+ def self.theme_names
24
+ registry.names.sort
25
+ end
26
+
27
+ def self.current
28
+ return @current if @current
29
+
30
+ theme = Fatty::Config.config[:theme]
31
+
32
+ @current =
33
+ if theme && !theme.to_s.strip.empty?
34
+ theme.to_sym
35
+ else
36
+ FALLBACK_THEME
37
+ end
38
+ end
39
+
40
+ def self.set(theme)
41
+ t = theme.to_sym
42
+ if theme_names.include?(t)
43
+ @current = t
44
+ else
45
+ Fatty.warn("Unknown theme: #{theme}, falling back to #{FALLBACK_THEME}", tag: :theme)
46
+ @current = FALLBACK_THEME
47
+ end
48
+ end
49
+
50
+ def self.cycle
51
+ names = theme_names
52
+ return set(FALLBACK_THEME) if names.empty?
53
+
54
+ idx = names.index(current) || 0
55
+ set(names[(idx + 1) % names.length])
56
+ end
57
+
58
+ def self.fetch(name)
59
+ Resolver.resolve(registry, name)
60
+ end
61
+
62
+ def self.roles(name = current)
63
+ fetch(name)[:roles]
64
+ end
65
+
66
+ def self.markdown(name = current)
67
+ fetch(name)[:markdown]
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fatty
4
+ module Themes
5
+ class Registry
6
+ attr_reader :warnings
7
+
8
+ def initialize
9
+ @defs = {}
10
+ @warnings = []
11
+ end
12
+
13
+ def clear
14
+ @defs.clear
15
+ @warnings.clear
16
+ nil
17
+ end
18
+
19
+ def add(defn, source: nil)
20
+ name = normalize_name(defn[:name])
21
+ if name
22
+ @defs[name] = defn.merge(name: name, source: source)
23
+ else
24
+ warn("Theme missing name#{source_suffix(source)}")
25
+ end
26
+ nil
27
+ end
28
+
29
+ def names
30
+ @defs.keys
31
+ end
32
+
33
+ def include?(name)
34
+ @defs.key?(normalize_name(name))
35
+ end
36
+
37
+ def fetch(name)
38
+ @defs[normalize_name(name)]
39
+ end
40
+
41
+ def each(&block)
42
+ @defs.each_value(&block)
43
+ end
44
+
45
+ private
46
+
47
+ def normalize_name(name)
48
+ return if name.nil?
49
+
50
+ text = name.to_s.strip
51
+ text.empty? ? nil : text.to_sym
52
+ end
53
+
54
+ def warn(message)
55
+ @warnings << message
56
+ Fatty.warn(message, tag: :theme) if Fatty.respond_to?(:warn)
57
+ end
58
+
59
+ def source_suffix(source)
60
+ source ? " in #{source}" : ""
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,224 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fatty
4
+ module Themes
5
+ class ResolveError < StandardError; end
6
+ class MissingThemeError < ResolveError; end
7
+ class InheritanceCycleError < ResolveError; end
8
+
9
+ # The Resolver module is responsible for transforming a raw theme loaded
10
+ # by the Loader into a fully-resolved "theme_spec".
11
+ #
12
+ # Resolution includes:
13
+ # - applying inheritance between themes
14
+ # - applying inheritance between roles
15
+ # - filling in default attributes
16
+ # - synthesizing "composite" roles used by the alert panel
17
+ #
18
+ # Composite alert roles (e.g., :alert_info, :alert_warn, etc.) combine the
19
+ # background and structural properties of the :alert role with the foreground
20
+ # and attributes of the semantic roles (:good, :info, :warn, :error).
21
+ #
22
+ # The result of resolution is a Hash keyed by role names, where each value
23
+ # is a symbolic color specification (typically using color names and attrs).
24
+ # We refer to this Hash as a "theme_spec".
25
+ #
26
+ # A theme_spec is not directly renderable. Before rendering, it must be
27
+ # "compiled" into a palette by Fatty::Colors::Palette.apply!. The palette
28
+ # maps each role to concrete rendering data (RGB values for truecolor or
29
+ # Curses color pairs, along with attributes).
30
+ #
31
+ # Renderer classes consume only the compiled palette and never operate
32
+ # directly on the theme_spec.
33
+ # module Resolver
34
+ module Resolver
35
+ DEFAULT_ROLE_SPECS = {
36
+ region: { attrs: [:reverse] },
37
+ cursor: { attrs: [:reverse] },
38
+ input_suggestion: { attrs: [:dim] },
39
+ pager_status: { attrs: [:reverse] },
40
+ search_input: { attrs: [:reverse] },
41
+ match_current: { attrs: [:reverse] },
42
+ match_other: { attrs: [:underline] },
43
+ popup_counts: { attrs: [:bold] },
44
+ alert: { attrs: [:bold] },
45
+ markdown_table_cell: {},
46
+ markdown_underline: { attrs: [:underline] },
47
+ markdown_hrule: { attrs: [:dim] },
48
+ }.freeze
49
+
50
+ ROLE_PARENTS = {
51
+ input: :output,
52
+ input_suggestion: :input,
53
+
54
+ region: :output,
55
+ cursor: :input,
56
+
57
+ popup: :output,
58
+ popup_frame: :popup,
59
+ popup_input: :input,
60
+ popup_selection: :region,
61
+ popup_counts: :popup,
62
+
63
+ search_input: :popup,
64
+
65
+ match_current: :region,
66
+ match_other: :region,
67
+
68
+ status: :output,
69
+ alert: :output,
70
+ info: :output,
71
+ good: :info,
72
+ warn: :info,
73
+ error: :warn,
74
+
75
+ pager_status: :status,
76
+
77
+ markdown_h1: :output,
78
+ markdown_h2: :markdown_h1,
79
+ markdown_h3: :markdown_h2,
80
+
81
+ markdown_code: :output,
82
+ markdown_code_gutter: :markdown_code,
83
+
84
+ markdown_strong: :output,
85
+ markdown_emphasis: :output,
86
+
87
+ markdown_link: :output,
88
+ markdown_url: :markdown_link,
89
+
90
+ markdown_quote_gutter: :output,
91
+ markdown_highlight: :output,
92
+ markdown_table_header: :markdown_strong,
93
+ markdown_table_cell: :output,
94
+ markdown_underline: :output,
95
+ markdown_hrule: :output,
96
+ }.freeze
97
+
98
+ def self.empty_theme
99
+ {
100
+ name: nil,
101
+ inherit: nil,
102
+ roles: {},
103
+ source: nil,
104
+ }
105
+ end
106
+
107
+ def self.resolve(registry, name)
108
+ raw = merge_theme_chain(registry, name.to_sym, stack: [])
109
+ raw[:roles] = resolve_role_inheritance(raw[:roles])
110
+ raw[:roles] = add_composite_roles(raw[:roles])
111
+ raw[:name] = name.to_sym
112
+ raw
113
+ end
114
+
115
+ def self.merge_theme_chain(registry, name, stack:)
116
+ defn = registry.fetch(name)
117
+ raise MissingThemeError, "Theme not found: #{name}" unless defn
118
+
119
+ if stack.include?(name)
120
+ cycle = (stack + [name]).join(" -> ")
121
+ raise InheritanceCycleError, "Theme inheritance cycle: #{cycle}"
122
+ end
123
+
124
+ parent =
125
+ if defn[:inherit]
126
+ merge_theme_chain(registry, defn[:inherit], stack: stack + [name])
127
+ else
128
+ empty_theme
129
+ end
130
+
131
+ merged = deep_merge(parent, defn)
132
+ merged[:name] = name
133
+ merged[:inherit] = defn[:inherit]
134
+ merged[:source] = defn[:source]
135
+ merged
136
+ end
137
+
138
+ def self.resolve_role_inheritance(roles)
139
+ resolved = {}
140
+ names = (
141
+ ROLE_PARENTS.keys +
142
+ ROLE_PARENTS.values +
143
+ DEFAULT_ROLE_SPECS.keys +
144
+ roles.keys
145
+ ).compact.uniq
146
+
147
+ names.each do |name|
148
+ resolve_role(name, roles, resolved, stack: [])
149
+ end
150
+
151
+ resolved
152
+ end
153
+
154
+ def self.resolve_role(name, roles, resolved, stack:)
155
+ return resolved[name] if resolved.key?(name)
156
+
157
+ if stack.include?(name)
158
+ raise InheritanceCycleError, "Role inheritance cycle: #{(stack + [name]).join(' -> ')}"
159
+ end
160
+
161
+ spec = roles[name] || DEFAULT_ROLE_SPECS.fetch(name, {})
162
+ parent_name =
163
+ if spec.key?(:inherit)
164
+ spec[:inherit]&.to_sym
165
+ else
166
+ ROLE_PARENTS[name]
167
+ end
168
+
169
+ parent_spec =
170
+ if parent_name
171
+ resolve_role(parent_name, roles, resolved, stack: stack + [name])
172
+ else
173
+ {}
174
+ end
175
+
176
+ resolved[name] = deep_merge_hash(parent_spec, spec.reject { |k, _| k == :inherit })
177
+ end
178
+
179
+ def self.deep_merge(parent, child)
180
+ out = parent.dup
181
+
182
+ out[:roles] = deep_merge_hash(parent[:roles] || {}, child[:roles] || {})
183
+
184
+ child.each do |key, value|
185
+ next if key == :roles
186
+
187
+ out[key] = value
188
+ end
189
+
190
+ out
191
+ end
192
+
193
+ def self.deep_merge_hash(parent, child)
194
+ parent.merge(child) do |_key, old_value, new_value|
195
+ if old_value.is_a?(Hash) && new_value.is_a?(Hash)
196
+ deep_merge_hash(old_value, new_value)
197
+ else
198
+ new_value
199
+ end
200
+ end
201
+ end
202
+
203
+ def self.add_composite_roles(roles)
204
+ roles = roles.dup
205
+
206
+ roles[:alert_info] = composite_role(roles[:alert] || {}, roles[:info] || {})
207
+ roles[:alert_good] = composite_role(roles[:alert] || {}, roles[:good] || {})
208
+ roles[:alert_warn] = composite_role(roles[:alert] || {}, roles[:warn] || {})
209
+ roles[:alert_error] = composite_role(roles[:alert] || {}, roles[:error] || {})
210
+ roles
211
+ end
212
+ private_class_method :add_composite_roles
213
+
214
+ def self.composite_role(base, accent)
215
+ {
216
+ fg: accent[:fg] || accent["fg"] || base[:fg] || base["fg"],
217
+ bg: base[:bg] || base["bg"] || accent[:bg] || accent["bg"],
218
+ attrs: accent[:attrs] || accent["attrs"] || base[:attrs] || base["attrs"],
219
+ }.compact
220
+ end
221
+ private_class_method :composite_role
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fatty
4
+ module Colors
5
+ module Themes
6
+ # Theme values may be:
7
+ # - ANSI names (e.g., "yellow", "bright_white")
8
+ # - Aliases supported by Fatty::Color (e.g., "navy", "dark_blue")
9
+ # - Integers 0..255
10
+ # - Hex strings "#RRGGBB"
11
+ # - X11 color names (if present in your bundled rgb.txt)
12
+ #
13
+ # Roles are intentionally scoped (popup_selection vs region, etc.)
14
+ THEMES = {
15
+ wordperfect: {
16
+ output: { fg: "white", bg: "navy" },
17
+ input: { fg: "white", bg: "navy" },
18
+ input_suggestion: { fg: "lightgray", bg: "navy" },
19
+ cursor: { fg: "white", bg: "red" },
20
+
21
+ region: { fg: "navy", bg: "yellow" },
22
+
23
+ status_good: { fg: "green", bg: "navy" },
24
+ status_info: { fg: "white", bg: "navy" },
25
+ status_warn: { fg: "black", bg: "magenta" },
26
+ status_error: { fg: "white", bg: "red" },
27
+
28
+ pager_status: { fg: "black", bg: "lightgreen" },
29
+
30
+ search_highlight: { fg: "black", bg: "red" },
31
+ search_highlight_secondary: { fg: "grey", bg: "pink" },
32
+ search: { fg: "black", bg: "cyan" },
33
+
34
+ popup: { fg: "white", bg: "navy" },
35
+ popup_selection: { fg: "navy", bg: 'yellow' },
36
+ popup_input: { fg: "white", bg: "dark_blue" },
37
+ popup_frame: { fg: "white", bg: "navy" },
38
+ popup_counts: { fg: "navy", bg: "white" },
39
+ },
40
+
41
+ nordic: {
42
+ output: { fg: "#d8dee9", bg: "#2e3440" },
43
+ input: { fg: "#eceff4", bg: "#3b4252" },
44
+ input_suggestion: { fg: "#81a1c1", bg: "#3b4252" },
45
+ cursor: { fg: "#2e3440", bg: "#88c0d0" },
46
+
47
+ region: { fg: "#2e3440", bg: "#88c0d0" },
48
+
49
+ status_good: { fg: "green", bg: "#2e3440" },
50
+ status_info: { fg: "#d8dee9", bg: "#2e3440" },
51
+ status_warn: { fg: "#2e3440", bg: "#ebcb8b" },
52
+ status_error: { fg: "#eceff4", bg: "#bf616a" },
53
+
54
+ pager_status: { fg: "black", bg: "lightgreen" },
55
+
56
+ search_highlight: { fg: "black", bg: "yellow" },
57
+ search_highlight_secondary: { fg: "black", bg: "lightgray" },
58
+ search: { fg: "black", bg: "cyan" },
59
+
60
+ popup: { fg: "#d8dee9", bg: "#3b4252" },
61
+ popup_selection: { fg: "#2e3440", bg: "#88c0d0" },
62
+ popup_input: { fg: "#eceff4", bg: "#434c5e" },
63
+ popup_frame: { fg: "#81a1c1", bg: "#3b4252" },
64
+ popup_counts: { fg: "#2e3440", bg: "white" },
65
+ },
66
+
67
+ solarized_dark: {
68
+ output: { fg: "#839496", bg: "#002b36" },
69
+ input: { fg: "#93a1a1", bg: "#073642" },
70
+ input_suggestion: { fg: "#657b83", bg: "#073642" },
71
+ cursor: { fg: "#002b36", bg: "#b58900" },
72
+
73
+ region: { fg: "#002b36", bg: "#b58900" },
74
+
75
+ status_good: { fg: "green", bg: "#002b36" },
76
+ status_info: { fg: "#839496", bg: "#002b36" },
77
+ status_warn: { fg: "#002b36", bg: "#cb4b16" },
78
+ status_error: { fg: "#fdf6e3", bg: "red" },
79
+
80
+ pager_status: { fg: "black", bg: "lightgreen" },
81
+
82
+ search_highlight: { fg: "black", bg: "yellow" },
83
+ search_highlight_secondary: { fg: "black", bg: "lightgray" },
84
+ search: { fg: "black", bg: "cyan" },
85
+
86
+ popup: { fg: "#839496", bg: "#073642" },
87
+ popup_selection: { fg: "#002b36", bg: "#b58900" },
88
+ popup_input: { fg: "#93a1a1", bg: "#002b36" },
89
+ popup_frame: { fg: "#268bd2", bg: "#073642" },
90
+ popup_counts: { fg: "#002b36", bg: "white" },
91
+ },
92
+
93
+ mono: {
94
+ output: { fg: "black", bg: "white" },
95
+ input: { fg: "white", bg: "black" },
96
+ input_suggestion: { fg: "gray", bg: "white" },
97
+ cursor: { fg: "black", bg: "white" },
98
+
99
+ region: { fg: "black", bg: "white" },
100
+
101
+ search_highlight: { fg: "black", bg: "yellow" },
102
+ search_highlight_secondary: { fg: "black", bg: "lightgray" },
103
+ search: { fg: "black", bg: "cyan" },
104
+
105
+ pager_status: { fg: "black", bg: "white" },
106
+
107
+ status_good: { fg: "green", bg: "white" },
108
+ status_info: { fg: "black", bg: "white" },
109
+ status_warn: { fg: "default", bg: "default" },
110
+ status_error: { fg: "default", bg: "default" },
111
+
112
+ popup: { fg: "black", bg: "white" },
113
+ popup_selection: { fg: "white", bg: "black" },
114
+ popup_input: { fg: "white", bg: "black" },
115
+ popup_frame: { fg: "black", bg: "white" },
116
+ popup_counts: { fg: "white", bg: "gray" },
117
+ },
118
+ }.freeze
119
+
120
+ def self.fetch(name)
121
+ return if name.nil?
122
+
123
+ THEMES[name.to_sym]
124
+ end
125
+
126
+ def self.names
127
+ THEMES.keys
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "themes/manager"
4
+ require_relative "themes/loader"
5
+ require_relative "themes/registry"
6
+ require_relative "themes/resolver"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fatty
4
+ VERSION = "0.99.0"
5
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fatty
4
+ class AlertView < Fatty::View
5
+ def initialize(id: "alert", z: 1_000, log: false)
6
+ super(id: id, z: z, log: log)
7
+ end
8
+
9
+ def draw(screen:, renderer:, terminal:, session:)
10
+ # session is AlertSession; it owns `current`
11
+ renderer.render_alert(session.current)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fatty
4
+ class CursorView < Fatty::View
5
+ def draw(screen:, renderer:, terminal:, session:)
6
+ # When paging is active, the output pane owns the screen and the shell
7
+ # input cursor should be turned off. This also prevents the underlying
8
+ # ShellSession from overriding the cursor while a modal (e.g. SearchSession)
9
+ # is displayed over the pager/status line.
10
+ if session.respond_to?(:pager_active?) && session.pager_active?
11
+ ::Curses.curs_set(0)
12
+ else
13
+ ::Curses.curs_set(1)
14
+ renderer.restore_cursor(session.field)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fatty
4
+ class InputView < Fatty::View
5
+ def draw(screen:, renderer:, terminal:, session:)
6
+ renderer.render_input_field(session.field)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fatty
4
+ class OutputView < View
5
+ def draw(screen:, renderer:, terminal:, session:)
6
+ renderer.render_output(session.output, viewport: session.viewport)
7
+ end
8
+ end
9
+ end