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/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
|