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
data/lib/fatty/help.rb ADDED
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ # require "redcarpet"
4
+ # require_relative "ansi_renderer"
5
+
6
+ # module Fatty
7
+ # module Help
8
+ # def self.path
9
+ # File.expand_path("config_files/help.md", __dir__)
10
+ # end
11
+
12
+ # def self.render(width: 80)
13
+ # renderer = Fatty::AnsiRenderer.new(width: width)
14
+ # markdown = Redcarpet::Markdown.new(
15
+ # renderer,
16
+ # tables: true,
17
+ # fenced_code_blocks: true,
18
+ # autolink: true,
19
+ # )
20
+ # markdown.render(text)
21
+ # end
22
+
23
+ # def self.text
24
+ # File.read(path)
25
+ # end
26
+ # end
27
+ # end
28
+
29
+ # frozen_string_literal: true
30
+
31
+ module Fatty
32
+ module Help
33
+ def self.path
34
+ File.expand_path("config_files/help.md", __dir__)
35
+ end
36
+
37
+ def self.text
38
+ File.read(path)
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fatty
4
+ class History
5
+ class Entry
6
+ attr_reader :text, :kind, :ctx, :stamp
7
+
8
+ def initialize(text:, kind: :command, ctx: nil, stamp: nil)
9
+ @text = text.to_s
10
+ @kind = kind.to_sym
11
+ @ctx = normalize_ctx(ctx)
12
+ @stamp = stamp || Time.now
13
+ end
14
+
15
+ def to_h
16
+ {
17
+ "text" => text,
18
+ "kind" => kind.to_s,
19
+ "ctx" => ctx,
20
+ "stamp" => stamp.iso8601
21
+ }
22
+ end
23
+
24
+ def command?
25
+ kind == :command
26
+ end
27
+
28
+ def search?
29
+ kind == :search_string || kind == :search_regex
30
+ end
31
+
32
+ def ctx_fetch(key, default = nil)
33
+ ctx.fetch(key.to_s, default)
34
+ end
35
+
36
+ def self.from_h(hash)
37
+ hash = hash.transform_keys(&:to_s)
38
+
39
+ new(
40
+ text: hash.fetch("text", ""),
41
+ kind: hash.fetch("kind", "command").to_sym,
42
+ ctx: History.normalize_ctx(hash["ctx"]),
43
+ stamp: parse_stamp(hash["stamp"]),
44
+ )
45
+ end
46
+
47
+ def self.parse_stamp(value)
48
+ case value
49
+ when Time
50
+ value
51
+ when String
52
+ Time.iso8601(value)
53
+ end
54
+ rescue ArgumentError
55
+ nil
56
+ end
57
+
58
+ private_class_method :parse_stamp
59
+
60
+ private
61
+
62
+ def normalize_ctx(value)
63
+ return {} unless value.is_a?(Hash)
64
+
65
+ value.each_with_object({}) do |(key, val), out|
66
+ out[key.to_s] = val
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,289 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "history/entry"
4
+
5
+ module Fatty
6
+ class History
7
+ DEFAULT_HISTORY_FILE = File.expand_path("~/.fatty_history")
8
+ DEFAULT_HISTORY_MAX = 10_000
9
+
10
+ attr_reader :entries
11
+
12
+ def initialize(path: nil, max: nil)
13
+ @path =
14
+ case path
15
+ when :default
16
+ Config.config.dig(:history, :file) || DEFAULT_HISTORY_FILE
17
+ when nil, false
18
+ nil
19
+ else
20
+ path
21
+ end
22
+ @path = File.expand_path(@path) if @path
23
+ @max = max&.to_i || Config.config.dig(:history, :max)&.to_i || DEFAULT_HISTORY_MAX
24
+ @entries = []
25
+ @cursors = {}
26
+
27
+ if @path
28
+ Fatty.info("History loaded from #{@path}")
29
+ load
30
+ else
31
+ Fatty.info("In-memory History only: no path")
32
+ end
33
+ end
34
+
35
+ # A global default History object available to all sessions. Sets a class
36
+ # instance variable.
37
+ def self.default
38
+ for_path(:default)
39
+ end
40
+
41
+ def self.for_path(path = :default)
42
+ @instances ||= {}
43
+ @instances[path] ||= new(path: path)
44
+ end
45
+
46
+ def self.reset_instances!
47
+ @instances = {}
48
+ end
49
+
50
+ def self.default
51
+ for_path(:default)
52
+ end
53
+
54
+ ###################################################################################
55
+ # Accessing History items from a consuming application
56
+ ###################################################################################
57
+
58
+ include Enumerable
59
+
60
+ def each(&block)
61
+ return enum_for(:each) unless block
62
+
63
+ @entries.each(&block)
64
+ end
65
+
66
+ def for(kind: nil, ctx: nil)
67
+ return enum_for(:for, kind: kind, ctx: ctx) unless block_given?
68
+
69
+ each do |entry|
70
+ next if kind && entry.kind != kind.to_sym
71
+ next if ctx && !ctx_match?(entry, ctx)
72
+
73
+ yield entry
74
+ end
75
+ end
76
+
77
+ def recent(kind: nil, ctx: nil, limit: nil)
78
+ rows = self.for(kind: kind, ctx: ctx).to_a
79
+ rows = rows.last(limit) if limit
80
+ rows.reverse
81
+ end
82
+
83
+ ###################################################################################
84
+ # Manipulating History
85
+ ###################################################################################
86
+
87
+ def add(text, kind: :command, ctx: nil, stamp: nil, persist: true)
88
+ text = text.to_s
89
+ return if text.strip.empty?
90
+
91
+ kind = kind.to_sym
92
+ ctx = normalize_ctx(ctx)
93
+ entry = Entry.new(text: text, kind: kind, ctx: ctx, stamp: stamp)
94
+
95
+ @entries.reject! do |old_entry|
96
+ old_entry.text == text &&
97
+ old_entry.kind == kind &&
98
+ old_entry.ctx == ctx
99
+ end
100
+
101
+ @entries << entry
102
+ truncate!
103
+ append_to_file(entry) if persist
104
+ reset_cursor_for(kind, ctx: ctx)
105
+ text
106
+ end
107
+
108
+ def previous(current)
109
+ previous_for(:command, current: current)
110
+ end
111
+
112
+ def next
113
+ next_for(:command)
114
+ end
115
+
116
+ def reset_cursor
117
+ reset_cursor_for(:command)
118
+ end
119
+
120
+ def previous_for(*kinds, current:, ctx: nil)
121
+ ctx = normalize_ctx(ctx)
122
+ cursor = cursor_for(*kinds, ctx: ctx)
123
+ prefix = cursor[:prefix]
124
+
125
+ if cursor[:index].nil?
126
+ cursor[:scratch] = current.to_s
127
+ cursor[:prefix] = current.to_s
128
+ prefix = cursor[:prefix]
129
+ end
130
+
131
+ the_entries = entries_for(*kinds, ctx: ctx, prefix: prefix)
132
+ return current.to_s if the_entries.empty?
133
+
134
+ if cursor[:index].nil?
135
+ cursor[:index] = the_entries.length - 1
136
+ return the_entries[cursor[:index]].text
137
+ end
138
+
139
+ cursor[:index] -= 1 if cursor[:index].positive?
140
+ the_entries[cursor[:index]].text
141
+ end
142
+
143
+ def next_for(*kinds, ctx: nil)
144
+ ctx = normalize_ctx(ctx)
145
+ cursor = cursor_for(*kinds, ctx: ctx)
146
+ the_entries = entries_for(*kinds, ctx: ctx, prefix: cursor[:prefix])
147
+ return "" if the_entries.empty? || cursor[:index].nil?
148
+
149
+ if cursor[:index] < the_entries.length - 1
150
+ cursor[:index] += 1
151
+ the_entries[cursor[:index]].text
152
+ else
153
+ scratch = cursor[:scratch]
154
+ reset_cursor_for(*kinds, ctx: ctx)
155
+ scratch || ""
156
+ end
157
+ end
158
+
159
+ def reset_cursor_for(*kinds, ctx: nil)
160
+ ctx = normalize_ctx(ctx)
161
+ cursor = cursor_for(*kinds, ctx: ctx)
162
+ cursor[:index] = nil
163
+ cursor[:prefix] = nil
164
+ cursor[:scratch] = nil
165
+ end
166
+
167
+ def suggest_for(*kinds, prefix:, ctx: nil)
168
+ text = prefix.to_s
169
+ return if text.empty?
170
+
171
+ wanted_ctx = normalize_ctx(ctx)
172
+
173
+ unless wanted_ctx.empty?
174
+ local = entries_for(*kinds, ctx: wanted_ctx, prefix: text)
175
+ return local.last.text unless local.empty?
176
+ end
177
+
178
+ global = entries_for(*kinds, prefix: text)
179
+ global.last&.text
180
+ end
181
+
182
+ def self.normalize_ctx(ctx)
183
+ return {} unless ctx.is_a?(Hash)
184
+
185
+ ctx.each_with_object({}) { |(key, value), memo|
186
+ memo[key.to_s] = value
187
+ }.sort.to_h
188
+ end
189
+
190
+ private
191
+
192
+ def normalize_kinds(*kinds)
193
+ kinds.flatten.map(&:to_sym).uniq.sort
194
+ end
195
+
196
+ def normalize_ctx(ctx)
197
+ self.class.normalize_ctx(ctx)
198
+ end
199
+
200
+ def ctx_match?(entry, ctx)
201
+ wanted = normalize_ctx(ctx)
202
+ return true if wanted.empty?
203
+
204
+ wanted.all? do |key, value|
205
+ entry.ctx_fetch(key) == value
206
+ end
207
+ end
208
+
209
+ def cursor_for(*kinds, ctx: nil)
210
+ ctx = normalize_ctx(ctx)
211
+ key = [normalize_kinds(*kinds), normalize_ctx(ctx)]
212
+ @cursors[key] ||= { index: nil, scratch: nil, prefix: nil }
213
+ end
214
+
215
+ def prefix_match?(entry, prefix)
216
+ text = prefix.to_s
217
+ return true if text.empty?
218
+
219
+ entry.text.start_with?(text)
220
+ end
221
+
222
+ def entries_for(*kinds, ctx: nil, prefix: nil)
223
+ ctx = normalize_ctx(ctx)
224
+ wanted = normalize_kinds(*kinds)
225
+
226
+ matches = select do |entry|
227
+ wanted.include?(entry.kind) && prefix_match?(entry, prefix)
228
+ end
229
+
230
+ fallback, preferred = matches.partition { |entry| !ctx_match?(entry, ctx) }
231
+ fallback + preferred
232
+ end
233
+
234
+ def load
235
+ @entries.clear
236
+ @cursors.clear
237
+ return unless File.exist?(@path)
238
+
239
+ File.foreach(@path) do |line|
240
+ line = line.chomp
241
+ next if line.empty?
242
+
243
+ entry = parse_history_line(line)
244
+ next unless entry
245
+
246
+ add(
247
+ entry.text,
248
+ kind: entry.kind,
249
+ ctx: entry.ctx,
250
+ stamp: entry.stamp,
251
+ persist: false,
252
+ )
253
+ end
254
+ truncate!
255
+ end
256
+
257
+ def parse_history_line(line)
258
+ hash = JSON.parse(line)
259
+ Entry.from_h(hash)
260
+ rescue JSON::ParserError
261
+ Entry.new(text: line, kind: :command)
262
+ rescue StandardError
263
+ nil
264
+ end
265
+
266
+ def append_to_file(entry)
267
+ return unless @path
268
+
269
+ dir = File.dirname(@path)
270
+ FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
271
+
272
+ File.open(@path, "a") do |f|
273
+ f.puts(JSON.generate(entry.to_h))
274
+ f.flush
275
+ f.fsync
276
+ end
277
+ rescue => e
278
+ Fatty.error("History#append_to_file failed for #{@path}: #{e.class}: #{e.message}", tag: :history)
279
+ end
280
+
281
+ def truncate!
282
+ excess = @entries.length - @max
283
+ return if excess <= 0
284
+
285
+ @entries.shift(excess)
286
+ @cursors.clear
287
+ end
288
+ end
289
+ end