fatty 0.99.8.3 → 0.99.8.5
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 +4 -4
- data/README.md +5 -0
- data/README.org +5 -0
- data/help/help.md +5 -0
- data/lib/fatty/colors/pairs.rb +8 -0
- data/lib/fatty/config_files/themes/cyberpunk.yml +1 -1
- data/lib/fatty/config_files/themes/nordic.yml +4 -0
- data/lib/fatty/config_files/themes/solarized_dark.yml +4 -0
- data/lib/fatty/curses/context.rb +9 -0
- data/lib/fatty/help.rb +116 -0
- data/lib/fatty/input_field.rb +6 -2
- data/lib/fatty/keymaps/emacs.rb +1 -0
- data/lib/fatty/markdown/ansi_renderer.rb +14 -4
- data/lib/fatty/markdown/render.rb +3 -2
- data/lib/fatty/renderer/curses.rb +19 -24
- data/lib/fatty/renderer/truecolor.rb +1 -1
- data/lib/fatty/session/keybindings_session.rb +61 -0
- data/lib/fatty/session/output_session.rb +17 -3
- data/lib/fatty/session.rb +6 -0
- data/lib/fatty/terminal.rb +51 -9
- data/lib/fatty/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6d2ce5d4499e640b71d333ef143a02f3590aa44514f6ad64c21017ff4eb829f7
|
|
4
|
+
data.tar.gz: 5c9ac04a0c7975c71756c3a9364ab0222e43ec5696150cfed3526b589b3cdd48
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a9fab1feaae8bacc7dcda281b9081fed3bab00440445763ab25577399e233321574828b1b014fc3ab9121d35aee69be66dbf2380cc03a16697c4aab4605930cb
|
|
7
|
+
data.tar.gz: 7c6c437ca0b0b3e5ab28a4488a30a770ef3ab127b2467c83b255e9551db7ce40294e469c2e4817f964171bde56847ef6bdfe99e2a936bbcc17c729bd0c7b6226
|
data/README.md
CHANGED
|
@@ -990,6 +990,11 @@ Alerts are short-lived, non-scrolling messages shown below the input field. They
|
|
|
990
990
|
|
|
991
991
|
## Command-line Editing
|
|
992
992
|
|
|
993
|
+
Press `M-?` (Alt-?, or Escape followed by ?) to browse the current
|
|
994
|
+
keybindings, including user overrides and action descriptions. The help
|
|
995
|
+
uses the usual pager controls, including search. Press `q` to return to
|
|
996
|
+
the previous output, pager position, and unfinished command line.
|
|
997
|
+
|
|
993
998
|
`fatty` aims to have a full-featured command-line editor for editing a single line of text that is sent to the `on_accept` proc for the `Terminal`. The default keybindings are based on Emacs keybindings, but they are fully customizable. Implementing `vim`-like keybindings is not currently possible because `fatty` has no notion of editing "modes" as would be required for any kind of a modal editor—at least not yet. Maybe some day.
|
|
994
999
|
|
|
995
1000
|
|
data/README.org
CHANGED
|
@@ -855,6 +855,11 @@ uses this area to warn the user of unrecognized key codes and of unbound key
|
|
|
855
855
|
presses.
|
|
856
856
|
|
|
857
857
|
** Command-line Editing
|
|
858
|
+
Press =M-?= (Alt-?, or Escape followed by ?) to browse the current
|
|
859
|
+
keybindings, including user overrides and action descriptions. The help
|
|
860
|
+
uses the usual pager controls, including search. Press =q= to return to
|
|
861
|
+
the previous output, pager position, and unfinished command line.
|
|
862
|
+
|
|
858
863
|
=fatty= aims to have a full-featured command-line editor for editing a single
|
|
859
864
|
line of text that is sent to the =on_accept= proc for the =Terminal=. The
|
|
860
865
|
default keybindings are based on Emacs keybindings, but they are fully
|
data/help/help.md
CHANGED
|
@@ -60,6 +60,11 @@ Here are the commands builtin to `fatty`
|
|
|
60
60
|
|
|
61
61
|
## Keybindings
|
|
62
62
|
|
|
63
|
+
Press `M-?` (Alt-?, or Escape followed by ?) to browse the current
|
|
64
|
+
keybindings, including user overrides and action descriptions. Use the
|
|
65
|
+
usual pager controls to scroll and search; `q` restores the previous output
|
|
66
|
+
and unfinished command line.
|
|
67
|
+
|
|
63
68
|
The following tables explain the keybindings available in \`fatty\` in different contexts. Named keys are indicated by \`:name\` and key categories, such as \`<digits>\` are indicated with brackets.
|
|
64
69
|
|
|
65
70
|
|
data/lib/fatty/colors/pairs.rb
CHANGED
|
@@ -37,6 +37,10 @@ module Fatty
|
|
|
37
37
|
POPUP_FRAME = 33
|
|
38
38
|
POPUP_COUNTS = 34
|
|
39
39
|
|
|
40
|
+
MARKDOWN_H1 = 35
|
|
41
|
+
MARKDOWN_H2 = 36
|
|
42
|
+
MARKDOWN_TABLE_HEADER = 37
|
|
43
|
+
|
|
40
44
|
# Role -> pair id mapping for iteration.
|
|
41
45
|
ROLE_TO_PAIR = {
|
|
42
46
|
output: OUTPUT,
|
|
@@ -69,6 +73,10 @@ module Fatty
|
|
|
69
73
|
popup_input: POPUP_INPUT,
|
|
70
74
|
popup_frame: POPUP_FRAME,
|
|
71
75
|
popup_counts: POPUP_COUNTS,
|
|
76
|
+
|
|
77
|
+
markdown_h1: MARKDOWN_H1,
|
|
78
|
+
markdown_h2: MARKDOWN_H2,
|
|
79
|
+
markdown_table_header: MARKDOWN_TABLE_HEADER,
|
|
72
80
|
}.freeze
|
|
73
81
|
end
|
|
74
82
|
end
|
data/lib/fatty/curses/context.rb
CHANGED
|
@@ -155,6 +155,7 @@ module Fatty
|
|
|
155
155
|
|
|
156
156
|
::Curses.reset_prog_mode
|
|
157
157
|
::Curses.refresh
|
|
158
|
+
touch_windows!
|
|
158
159
|
enable_bracketed_paste!
|
|
159
160
|
nil
|
|
160
161
|
end
|
|
@@ -172,6 +173,14 @@ module Fatty
|
|
|
172
173
|
@started = false
|
|
173
174
|
end
|
|
174
175
|
|
|
176
|
+
def touch_windows!
|
|
177
|
+
[@output_win, @status_win, @input_win, @alert_win].each do |win|
|
|
178
|
+
win.touch if win&.respond_to?(:touch)
|
|
179
|
+
end
|
|
180
|
+
::Curses.stdscr.touch if ::Curses.respond_to?(:stdscr) && ::Curses.stdscr.respond_to?(:touch)
|
|
181
|
+
nil
|
|
182
|
+
end
|
|
183
|
+
|
|
175
184
|
# Map a Fatty::Ansi::Style to a curses attribute.
|
|
176
185
|
#
|
|
177
186
|
# - If style has no explicit fg/bg, we keep the themed role pair.
|
data/lib/fatty/help.rb
CHANGED
|
@@ -2,6 +2,49 @@
|
|
|
2
2
|
|
|
3
3
|
module Fatty
|
|
4
4
|
module Help
|
|
5
|
+
CONTEXT_DESCRIPTIONS = {
|
|
6
|
+
text: "The text context supplies the shared editing keys for moving the cursor, " \
|
|
7
|
+
"deleting text, undoing edits, and copying or pasting. It is active while editing " \
|
|
8
|
+
"the command line, prompts, search queries, and popup filters. The input context " \
|
|
9
|
+
"below adds command-line-specific keys; other text fields add their own context. " \
|
|
10
|
+
"Those more specific bindings take precedence over text bindings.",
|
|
11
|
+
input: "The input context is active while you compose a command at the main prompt. " \
|
|
12
|
+
"It adds command history, completion, and submission to the shared text editing " \
|
|
13
|
+
"keys above. Fatty checks input bindings first, then text, then terminal. " \
|
|
14
|
+
"These input bindings do not apply to other prompts or search fields, or while paging output.",
|
|
15
|
+
terminal: "The terminal context supplies shared controls available from command input, " \
|
|
16
|
+
"the pager, prompts, searches, and popups. It is checked after the other active " \
|
|
17
|
+
"contexts, so these keys work throughout the normal interface unless a more " \
|
|
18
|
+
"specific context binds the same key. Special modes such as key testing handle " \
|
|
19
|
+
"keys separately.",
|
|
20
|
+
paging: "The paging context is active while you browse output, including this help. " \
|
|
21
|
+
"Its keys scroll, jump, search, narrow the displayed lines, or leave the pager. " \
|
|
22
|
+
"Printable keys act as pager commands rather than editing the command line. " \
|
|
23
|
+
"Terminal bindings remain available when paging does not bind the same key.",
|
|
24
|
+
popup: "The popup context is active in menus and selection lists. Its keys move " \
|
|
25
|
+
"among items, accept a choice, or cancel. Text bindings edit the popup's filter, " \
|
|
26
|
+
"while popup bindings take precedence for navigation and selection. Terminal " \
|
|
27
|
+
"bindings are checked last.",
|
|
28
|
+
popup_multi: "The popup_multi context adds selection controls to popups that let " \
|
|
29
|
+
"you choose multiple items. Its bindings are checked before popup, text, and " \
|
|
30
|
+
"terminal bindings. By default, Space toggles the current item instead of " \
|
|
31
|
+
"inserting a space into the filter.",
|
|
32
|
+
prompt: "The prompt context is active when Fatty asks you to enter an answer in " \
|
|
33
|
+
"a prompt. It supplies acceptance, cancellation, and prompt-history controls. " \
|
|
34
|
+
"The shared text bindings edit your answer; terminal bindings are checked last. " \
|
|
35
|
+
"The main command line uses input instead of this context.",
|
|
36
|
+
search: "The search context is active while you enter a pager search query. It " \
|
|
37
|
+
"provides search history, acceptance and cancellation, match navigation, and " \
|
|
38
|
+
"switching between literal text and regular expressions. Text bindings edit " \
|
|
39
|
+
"the query, and terminal bindings are checked last. After leaving the search " \
|
|
40
|
+
"field, paging bindings control the output again.",
|
|
41
|
+
isearch: "The isearch context is active during incremental pager search, where " \
|
|
42
|
+
"the match updates as you type. Its keys step through matches, recall search " \
|
|
43
|
+
"history, accept the search, or cancel and restore the earlier view. Text " \
|
|
44
|
+
"bindings edit the query, and terminal bindings are checked last.",
|
|
45
|
+
}.freeze
|
|
46
|
+
private_constant :CONTEXT_DESCRIPTIONS
|
|
47
|
+
|
|
5
48
|
def self.path
|
|
6
49
|
File.expand_path("../../help/help.md", __dir__)
|
|
7
50
|
end
|
|
@@ -9,5 +52,78 @@ module Fatty
|
|
|
9
52
|
def self.text
|
|
10
53
|
File.read(path)
|
|
11
54
|
end
|
|
55
|
+
|
|
56
|
+
def self.keybindings(keymap, width: 83)
|
|
57
|
+
lines = ["# Current keybindings", "",
|
|
58
|
+
"Fatty is the terminal user-interface library used by this application. " \
|
|
59
|
+
"It provides command-line editing, output paging and search, and interactive prompts and menus.", "",
|
|
60
|
+
"Use the pager to browse or search; quit paging to return.", "",
|
|
61
|
+
"The tables below show the loaded keybindings for each context, including user overrides. " \
|
|
62
|
+
"Keys use Emacs-like notation: C- means hold Control, M- means hold Alt (also called Meta), " \
|
|
63
|
+
"and C-M- means hold both. Shift may be indicated by the final key name, as in M-R, " \
|
|
64
|
+
"or by an explicit S- prefix, as in M-S-r. Both examples mean Alt-Shift-R. " \
|
|
65
|
+
"Printable characters insert text in text-entry contexts.", "",
|
|
66
|
+
"The Action column gives the name of the Fatty action bound to each key. " \
|
|
67
|
+
"These names are useful when customizing keybindings or working on Fatty itself."]
|
|
68
|
+
key_width, action_width, description_width = keybinding_column_widths(width)
|
|
69
|
+
keymap.bindings.sort_by { |context, _| [[:text, :input].index(context) || 2, context.to_s] }.each do |context, bindings|
|
|
70
|
+
description = CONTEXT_DESCRIPTIONS.fetch(context) do
|
|
71
|
+
"This context is supplied by the application or a custom keymap. Its bindings " \
|
|
72
|
+
"apply when the application activates it; the application determines how " \
|
|
73
|
+
"it combines with other contexts."
|
|
74
|
+
end
|
|
75
|
+
lines << "" << "## In the #{context} context:" << "" << description << ""
|
|
76
|
+
lines << "| Key | Action | Description |" << "| --- | --- | --- |"
|
|
77
|
+
entries = bindings.map do |gesture, binding|
|
|
78
|
+
key = gesture.is_a?(MouseGesture) ? gesture.button : gesture.key
|
|
79
|
+
label = KeyEvent.key_to_str(key: key, ctrl: gesture.ctrl, meta: gesture.meta, shift: gesture.shift)
|
|
80
|
+
action, *args = Array(binding)
|
|
81
|
+
description = Actions.lookup(action)&.fetch(:doc)
|
|
82
|
+
action_text = ([action] + args.map(&:inspect)).join(" ")
|
|
83
|
+
[label, action_text, description]
|
|
84
|
+
end
|
|
85
|
+
entries.sort_by(&:first).each do |label, action, description|
|
|
86
|
+
keys = wrap_cell(label, key_width)
|
|
87
|
+
actions = wrap_cell(action, action_width)
|
|
88
|
+
descriptions = wrap_cell(description, description_width)
|
|
89
|
+
[keys.length, actions.length, descriptions.length].max.times do |index|
|
|
90
|
+
lines << "| #{escape_cell(keys[index])} | #{escape_cell(actions[index])} | #{escape_cell(descriptions[index])} |"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
lines.join("\n")
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def self.keybinding_column_widths(width)
|
|
98
|
+
width = [width, 83].min
|
|
99
|
+
key_width = [[width / 6, 12].min, 3].max
|
|
100
|
+
action_width = [[width - 12 - key_width - 11, 27].min, 6].max
|
|
101
|
+
[key_width, action_width, [width - 12 - key_width - action_width, 11].max]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def self.wrap_cell(text, width)
|
|
105
|
+
lines = []
|
|
106
|
+
line = +""
|
|
107
|
+
text.to_s.split.each do |word|
|
|
108
|
+
if !line.empty? && line.length + word.length + 1 > width
|
|
109
|
+
lines << line
|
|
110
|
+
line = +""
|
|
111
|
+
end
|
|
112
|
+
while word.length > width
|
|
113
|
+
lines << word.slice!(0, width)
|
|
114
|
+
end
|
|
115
|
+
line << " " unless line.empty?
|
|
116
|
+
line << word
|
|
117
|
+
end
|
|
118
|
+
lines << line unless line.empty?
|
|
119
|
+
lines
|
|
120
|
+
end
|
|
121
|
+
private_class_method :wrap_cell
|
|
122
|
+
|
|
123
|
+
def self.escape_cell(text)
|
|
124
|
+
# Entities keep punctuation keys (especially pipes) inside their cells.
|
|
125
|
+
text.to_s.each_char.map { |char| char.match?(/[[:alnum:] ]/) ? char : "&##{char.ord};" }.join
|
|
126
|
+
end
|
|
127
|
+
private_class_method :escape_cell
|
|
12
128
|
end
|
|
13
129
|
end
|
data/lib/fatty/input_field.rb
CHANGED
|
@@ -519,9 +519,13 @@ module Fatty
|
|
|
519
519
|
end
|
|
520
520
|
|
|
521
521
|
def autosuggestion_suffix
|
|
522
|
-
|
|
522
|
+
suggestion = autosuggestion.to_s
|
|
523
|
+
text = buffer.text.to_s
|
|
524
|
+
return "" if suggestion.empty?
|
|
525
|
+
return "" unless suggestion.start_with?(text)
|
|
526
|
+
return "" if suggestion == text
|
|
523
527
|
|
|
524
|
-
|
|
528
|
+
suggestion.delete_prefix(text)
|
|
525
529
|
end
|
|
526
530
|
|
|
527
531
|
def accept_autosuggestion!
|
data/lib/fatty/keymaps/emacs.rb
CHANGED
|
@@ -7,12 +7,14 @@ module Fatty
|
|
|
7
7
|
class AnsiRenderer < Redcarpet::Render::Base
|
|
8
8
|
HARD_BREAK = "\uE000"
|
|
9
9
|
|
|
10
|
-
def initialize(width: 80, palette: nil, theme: nil, truecolor: false)
|
|
10
|
+
def initialize(width: 80, palette: nil, theme: nil, truecolor: false, table_widths: nil, table_borders: false)
|
|
11
11
|
super()
|
|
12
12
|
@width = width.to_i
|
|
13
13
|
@palette = palette || {}
|
|
14
14
|
@theme = theme || {}
|
|
15
15
|
@truecolor = truecolor
|
|
16
|
+
@table_widths = table_widths
|
|
17
|
+
@table_borders = table_borders
|
|
16
18
|
end
|
|
17
19
|
|
|
18
20
|
def block_code(code, language)
|
|
@@ -37,6 +39,10 @@ module Fatty
|
|
|
37
39
|
text
|
|
38
40
|
end
|
|
39
41
|
|
|
42
|
+
def entity(text)
|
|
43
|
+
CGI.unescapeHTML(text.to_s)
|
|
44
|
+
end
|
|
45
|
+
|
|
40
46
|
def raw_html(html)
|
|
41
47
|
text = CGI.unescapeHTML(html.to_s)
|
|
42
48
|
render_inline_html(text)
|
|
@@ -186,6 +192,10 @@ module Fatty
|
|
|
186
192
|
end
|
|
187
193
|
end
|
|
188
194
|
|
|
195
|
+
if @table_borders
|
|
196
|
+
rendered.unshift(render_table_separator(widths, joints: ["┌", "┬", "┐"]))
|
|
197
|
+
rendered << render_table_separator(widths, joints: ["└", "┴", "┘"])
|
|
198
|
+
end
|
|
189
199
|
rendered.join("\n") + "\n\n"
|
|
190
200
|
end
|
|
191
201
|
|
|
@@ -207,7 +217,7 @@ module Fatty
|
|
|
207
217
|
end
|
|
208
218
|
end
|
|
209
219
|
|
|
210
|
-
widths
|
|
220
|
+
widths.each_with_index.map { |width, index| [width, Array(@table_widths)[index].to_i].max }
|
|
211
221
|
end
|
|
212
222
|
|
|
213
223
|
def render_table_row_cells(row, widths, header: false)
|
|
@@ -220,12 +230,12 @@ module Fatty
|
|
|
220
230
|
" │ #{cells.join(' │ ')} │"
|
|
221
231
|
end
|
|
222
232
|
|
|
223
|
-
def render_table_separator(widths)
|
|
233
|
+
def render_table_separator(widths, joints: @table_borders ? ["├", "┼", "┤"] : [TABLE_BAR, TABLE_BAR, TABLE_BAR])
|
|
224
234
|
parts = widths.map do |width|
|
|
225
235
|
TABLE_DASH * (width + 2)
|
|
226
236
|
end
|
|
227
237
|
|
|
228
|
-
" #{
|
|
238
|
+
" #{joints[0]}#{parts.join(joints[1])}#{joints[2]}"
|
|
229
239
|
end
|
|
230
240
|
|
|
231
241
|
def pad_visible(text, width)
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
module Fatty
|
|
4
4
|
module Markdown
|
|
5
|
-
def self.render(text, width: 80, palette: nil, theme: nil, truecolor: false)
|
|
5
|
+
def self.render(text, width: 80, palette: nil, theme: nil, truecolor: false, table_widths: nil, table_borders: false)
|
|
6
6
|
markdown = Redcarpet::Markdown.new(
|
|
7
|
-
Fatty::AnsiRenderer.new(width: width, palette: palette, theme: theme, truecolor: truecolor
|
|
7
|
+
Fatty::AnsiRenderer.new(width: width, palette: palette, theme: theme, truecolor: truecolor,
|
|
8
|
+
table_widths: table_widths, table_borders: table_borders),
|
|
8
9
|
tables: true,
|
|
9
10
|
fenced_code_blocks: true,
|
|
10
11
|
autolink: true,
|
|
@@ -523,27 +523,16 @@ module Fatty
|
|
|
523
523
|
base_attr = pair_attr(:output, fallback: ::Curses::A_NORMAL)
|
|
524
524
|
win.attrset(base_attr)
|
|
525
525
|
win.scrl(delta)
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
visible_line = lines[y]
|
|
532
|
-
draw_output_row(
|
|
533
|
-
win,
|
|
534
|
-
line: visible_line.text,
|
|
535
|
-
fragments: visible_line.fragments,
|
|
536
|
-
y: y,
|
|
537
|
-
abs_line: visible_line.number - 1,
|
|
538
|
-
highlights: highlights,
|
|
539
|
-
)
|
|
526
|
+
exposed_rows =
|
|
527
|
+
if delta.positive?
|
|
528
|
+
[height - delta, 0].max...height
|
|
529
|
+
else
|
|
530
|
+
0...[-delta, height].min
|
|
540
531
|
end
|
|
541
|
-
else
|
|
542
|
-
count = -delta
|
|
543
|
-
count = height if count > height
|
|
544
532
|
|
|
545
|
-
|
|
546
|
-
|
|
533
|
+
exposed_rows.each do |y|
|
|
534
|
+
visible_line = lines[y]
|
|
535
|
+
if visible_line
|
|
547
536
|
draw_output_row(
|
|
548
537
|
win,
|
|
549
538
|
line: visible_line.text,
|
|
@@ -552,6 +541,11 @@ module Fatty
|
|
|
552
541
|
abs_line: visible_line.number - 1,
|
|
553
542
|
highlights: highlights,
|
|
554
543
|
)
|
|
544
|
+
else
|
|
545
|
+
win.setpos(y, 0)
|
|
546
|
+
win.attrset(base_attr)
|
|
547
|
+
win.addstr("~")
|
|
548
|
+
win.clrtoeol
|
|
555
549
|
end
|
|
556
550
|
end
|
|
557
551
|
stage_window(win)
|
|
@@ -624,15 +618,16 @@ module Fatty
|
|
|
624
618
|
win.bkgdset(base_attr) if win.respond_to?(:bkgdset)
|
|
625
619
|
win.erase
|
|
626
620
|
|
|
627
|
-
|
|
621
|
+
viewport.height.times do |y|
|
|
622
|
+
visible_line = lines[y]
|
|
628
623
|
draw_output_row(
|
|
629
624
|
win,
|
|
630
|
-
line: visible_line.text,
|
|
631
|
-
fragments: visible_line
|
|
625
|
+
line: visible_line ? visible_line.text : "~",
|
|
626
|
+
fragments: visible_line&.fragments,
|
|
632
627
|
y: y,
|
|
633
|
-
abs_line: visible_line.number - 1,
|
|
628
|
+
abs_line: visible_line ? visible_line.number - 1 : nil,
|
|
634
629
|
highlights: highlights,
|
|
635
|
-
line_number: visible_line
|
|
630
|
+
line_number: visible_line&.number,
|
|
636
631
|
line_number_width: line_number_width,
|
|
637
632
|
)
|
|
638
633
|
end
|
|
@@ -510,7 +510,7 @@ module Fatty
|
|
|
510
510
|
|
|
511
511
|
rect.rows.times do |y|
|
|
512
512
|
visible_line = lines[y]
|
|
513
|
-
text = visible_line
|
|
513
|
+
text = visible_line ? visible_line.text : (y < viewport.height ? "~" : "")
|
|
514
514
|
abs_line = visible_line ? visible_line.number - 1 : nil
|
|
515
515
|
ranges = highlight_ranges_for_line(highlights, abs_line)
|
|
516
516
|
segments = []
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fatty
|
|
4
|
+
class KeybindingsSession < OutputSession
|
|
5
|
+
def init(terminal:)
|
|
6
|
+
super
|
|
7
|
+
width = [screen.output_rect.cols, 83].min
|
|
8
|
+
text = Markdown.render(
|
|
9
|
+
Help.keybindings(keymap, width: width),
|
|
10
|
+
width: [width, 80].min,
|
|
11
|
+
table_widths: Help.keybinding_column_widths(width),
|
|
12
|
+
table_borders: true,
|
|
13
|
+
)
|
|
14
|
+
# Keep Markdown roles semantic so theme changes recolor existing help.
|
|
15
|
+
sections = text.lines.chunk { |line| help_role(line.chomp) }
|
|
16
|
+
[
|
|
17
|
+
Command.session(id, :resize),
|
|
18
|
+
*sections.map { |role, lines| Command.session(id, :append, text: lines.join, role: role, follow: false) },
|
|
19
|
+
Command.session(id, :set_mode, mode: :paging),
|
|
20
|
+
]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def update(command)
|
|
24
|
+
return [Command.terminal(:pop_modal)] if command.action == :quit_paging
|
|
25
|
+
|
|
26
|
+
super
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def handle_resize
|
|
30
|
+
[Command.session(id, :resize)]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def show_keybindings
|
|
34
|
+
[]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def help_role(line)
|
|
40
|
+
if line == "Current keybindings"
|
|
41
|
+
:markdown_h1
|
|
42
|
+
elsif keymap.contexts.any? { |context| line == "In the #{context} context:" }
|
|
43
|
+
:markdown_h2
|
|
44
|
+
elsif line.start_with?(" │ Key ")
|
|
45
|
+
:markdown_table_header
|
|
46
|
+
else
|
|
47
|
+
:output
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def keymap_contexts
|
|
52
|
+
[:paging, :terminal]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def apply_action(action, args, event:)
|
|
56
|
+
return [Command.terminal(:pop_modal)] if action == :quit_paging
|
|
57
|
+
|
|
58
|
+
super
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -24,6 +24,7 @@ module Fatty
|
|
|
24
24
|
@line_numbers = false
|
|
25
25
|
@narrow_query = nil
|
|
26
26
|
@visible_lines = nil
|
|
27
|
+
@pending_command_reset = false
|
|
27
28
|
mode = Fatty::Config.config.dig(:output, :mode)&.to_sym || :paging
|
|
28
29
|
@default_output_mode = mode
|
|
29
30
|
@pager = Fatty::Pager.new(output: @output, viewport: @viewport, mode: mode, lines: -> { visible_lines })
|
|
@@ -52,6 +53,7 @@ module Fatty
|
|
|
52
53
|
[]
|
|
53
54
|
end
|
|
54
55
|
when :append
|
|
56
|
+
prepare_for_command_output!
|
|
55
57
|
case payload[:mode]
|
|
56
58
|
when :scrolling
|
|
57
59
|
pager.toggle_paging_mode if pager.mode == :paging
|
|
@@ -86,11 +88,15 @@ module Fatty
|
|
|
86
88
|
resize_output!
|
|
87
89
|
[]
|
|
88
90
|
when :begin_command
|
|
89
|
-
|
|
90
|
-
|
|
91
|
+
@narrow_query = nil
|
|
92
|
+
invalidate_visible_lines!
|
|
93
|
+
@pending_command_reset = true
|
|
91
94
|
[]
|
|
92
95
|
when :finish_command
|
|
93
|
-
|
|
96
|
+
unless @pending_command_reset
|
|
97
|
+
pager.finish_command!
|
|
98
|
+
end
|
|
99
|
+
@pending_command_reset = false
|
|
94
100
|
[]
|
|
95
101
|
when :quit_paging
|
|
96
102
|
pager.quit
|
|
@@ -509,6 +515,14 @@ module Fatty
|
|
|
509
515
|
@pager.reset!(mode: mode)
|
|
510
516
|
end
|
|
511
517
|
|
|
518
|
+
def prepare_for_command_output!
|
|
519
|
+
return unless @pending_command_reset
|
|
520
|
+
|
|
521
|
+
reset_for_command!
|
|
522
|
+
pager.begin_command!(anchor: output.lines.length)
|
|
523
|
+
@pending_command_reset = false
|
|
524
|
+
end
|
|
525
|
+
|
|
512
526
|
# When the pager is active, the last output row is reserved for the pager
|
|
513
527
|
# InputField. This returns the viewport to use for rendering the output
|
|
514
528
|
# content itself.
|
data/lib/fatty/session.rb
CHANGED
|
@@ -69,6 +69,11 @@ module Fatty
|
|
|
69
69
|
Command.terminal(:cycle_theme)
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
+
desc "Display current keybindings in a temporary pager"
|
|
73
|
+
action :show_keybindings do
|
|
74
|
+
Command.terminal(:push_modal, session: KeybindingsSession.new(keymap: keymap), owner: self)
|
|
75
|
+
end
|
|
76
|
+
|
|
72
77
|
desc "Choose a theme from a popup"
|
|
73
78
|
action :choose_theme do
|
|
74
79
|
Command.terminal(:choose_theme)
|
|
@@ -168,6 +173,7 @@ require_relative "session/popup_session"
|
|
|
168
173
|
require_relative "session/search_session"
|
|
169
174
|
require_relative "session/isearch_session"
|
|
170
175
|
require_relative "session/output_session"
|
|
176
|
+
require_relative "session/keybindings_session"
|
|
171
177
|
require_relative "session/shell_session"
|
|
172
178
|
require_relative 'session/prompt_session'
|
|
173
179
|
require_relative 'session/keytest_session'
|
data/lib/fatty/terminal.rb
CHANGED
|
@@ -97,6 +97,7 @@ module Fatty
|
|
|
97
97
|
@last_render_time = nil
|
|
98
98
|
@restore_cursor_after_render = true
|
|
99
99
|
@render_count = 0
|
|
100
|
+
@deferred_render = false
|
|
100
101
|
@deferred_count = 0
|
|
101
102
|
end
|
|
102
103
|
|
|
@@ -149,6 +150,8 @@ module Fatty
|
|
|
149
150
|
text: warning,
|
|
150
151
|
),
|
|
151
152
|
)
|
|
153
|
+
elsif (hint = keybinding_help_hint)
|
|
154
|
+
apply_command(Command.session(:alert, :show, role: :info, text: hint))
|
|
152
155
|
end
|
|
153
156
|
|
|
154
157
|
Fatty.debug("@sessions: #{@sessions.map(&:id).join('==')}")
|
|
@@ -161,6 +164,9 @@ module Fatty
|
|
|
161
164
|
loop_count = 0
|
|
162
165
|
while @running
|
|
163
166
|
loop_count += 1
|
|
167
|
+
deferred_render = @deferred_render
|
|
168
|
+
@deferred_render = false
|
|
169
|
+
render_frame if deferred_render
|
|
164
170
|
@session_dirty = false
|
|
165
171
|
@immediate_render = false
|
|
166
172
|
if (cmd = event_source.next_event)
|
|
@@ -194,8 +200,11 @@ module Fatty
|
|
|
194
200
|
yield
|
|
195
201
|
ensure
|
|
196
202
|
ctx.resume
|
|
203
|
+
reconcile_terminal_size!
|
|
197
204
|
renderer.invalidate!
|
|
205
|
+
renderer.clear_physical_screen! if renderer.context.truecolor
|
|
198
206
|
render_frame
|
|
207
|
+
@deferred_render = true
|
|
199
208
|
end
|
|
200
209
|
|
|
201
210
|
# A command is either bound for this Terminal or it's meant to be
|
|
@@ -253,8 +262,8 @@ module Fatty
|
|
|
253
262
|
focused_session&.view
|
|
254
263
|
status_session.view
|
|
255
264
|
alert_session.view
|
|
256
|
-
|
|
257
|
-
|
|
265
|
+
@modal_stack.each do |entry|
|
|
266
|
+
entry[:session].view
|
|
258
267
|
end
|
|
259
268
|
if @restore_cursor_after_render
|
|
260
269
|
restore_active_cursor
|
|
@@ -457,8 +466,8 @@ module Fatty
|
|
|
457
466
|
renderer.screen = screen
|
|
458
467
|
apply_command(Command.session(:status, :resize))
|
|
459
468
|
apply_command(Command.session(:focused, :resize))
|
|
460
|
-
|
|
461
|
-
apply_command(Command.session(
|
|
469
|
+
@modal_stack.each do |entry|
|
|
470
|
+
apply_command(Command.session(entry[:session].id, :resize))
|
|
462
471
|
end
|
|
463
472
|
renderer.sync_backgrounds! if renderer.context.truecolor
|
|
464
473
|
renderer.invalidate!
|
|
@@ -470,9 +479,9 @@ module Fatty
|
|
|
470
479
|
command.action == :resize
|
|
471
480
|
end
|
|
472
481
|
|
|
473
|
-
def handle_resize
|
|
474
|
-
rows
|
|
475
|
-
cols
|
|
482
|
+
def handle_resize(rows: nil, cols: nil)
|
|
483
|
+
rows ||= ::Curses.lines
|
|
484
|
+
cols ||= ::Curses.cols
|
|
476
485
|
size = [rows, cols]
|
|
477
486
|
|
|
478
487
|
return [] if size == @last_handled_resize_size
|
|
@@ -509,8 +518,8 @@ module Fatty
|
|
|
509
518
|
out.resize_output! if out.respond_to?(:resize_output!)
|
|
510
519
|
end
|
|
511
520
|
|
|
512
|
-
|
|
513
|
-
session =
|
|
521
|
+
@modal_stack.each do |entry|
|
|
522
|
+
session = entry[:session]
|
|
514
523
|
cmds = session.handle_resize
|
|
515
524
|
apply_commands(cmds)
|
|
516
525
|
end
|
|
@@ -520,6 +529,22 @@ module Fatty
|
|
|
520
529
|
@inside_resize_term = false if did_resize_term
|
|
521
530
|
end
|
|
522
531
|
|
|
532
|
+
def reconcile_terminal_size!
|
|
533
|
+
return unless @screen && @ctx
|
|
534
|
+
|
|
535
|
+
rows, cols = STDOUT.winsize
|
|
536
|
+
return if rows.to_i <= 0 || cols.to_i <= 0
|
|
537
|
+
|
|
538
|
+
if [rows, cols] == [@screen.rows, @screen.cols]
|
|
539
|
+
@ctx.apply_layout(@screen)
|
|
540
|
+
renderer.screen = @screen
|
|
541
|
+
else
|
|
542
|
+
handle_resize(rows:, cols:)
|
|
543
|
+
end
|
|
544
|
+
rescue SystemCallError, IOError
|
|
545
|
+
nil
|
|
546
|
+
end
|
|
547
|
+
|
|
523
548
|
# --- Initialization ---------------------------------------------------------
|
|
524
549
|
|
|
525
550
|
#
|
|
@@ -527,6 +552,23 @@ module Fatty
|
|
|
527
552
|
# Curses, the Logger, installing key definitions and mappings, and
|
|
528
553
|
# installing themes.
|
|
529
554
|
#
|
|
555
|
+
def keybinding_help_hint
|
|
556
|
+
map = shell_session&.keymap
|
|
557
|
+
return unless map
|
|
558
|
+
|
|
559
|
+
gesture = map.bindings.values.flat_map(&:keys).uniq.find do |key|
|
|
560
|
+
next unless key.is_a?(KeyGesture)
|
|
561
|
+
|
|
562
|
+
event = KeyEvent.new(key: key.key, ctrl: key.ctrl, meta: key.meta, shift: key.shift)
|
|
563
|
+
binding = map.resolve(event, contexts: [:input, :text, :terminal])
|
|
564
|
+
Array(binding).first == :show_keybindings
|
|
565
|
+
end
|
|
566
|
+
return unless gesture
|
|
567
|
+
|
|
568
|
+
label = KeyEvent.key_to_str(key: gesture.key, ctrl: gesture.ctrl, meta: gesture.meta, shift: gesture.shift)
|
|
569
|
+
"Press #{label} for keybinding help."
|
|
570
|
+
end
|
|
571
|
+
|
|
530
572
|
def preflight!
|
|
531
573
|
Fatty::Config.configure_app(
|
|
532
574
|
app_name: @app_name,
|
data/lib/fatty/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fatty
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.99.8.
|
|
4
|
+
version: 0.99.8.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Daniel E. Doherty
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-10-03 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: curses
|
|
@@ -224,6 +224,7 @@ files:
|
|
|
224
224
|
- lib/fatty/session.rb
|
|
225
225
|
- lib/fatty/session/alert_session.rb
|
|
226
226
|
- lib/fatty/session/isearch_session.rb
|
|
227
|
+
- lib/fatty/session/keybindings_session.rb
|
|
227
228
|
- lib/fatty/session/keytest_session.rb
|
|
228
229
|
- lib/fatty/session/modal_session.rb
|
|
229
230
|
- lib/fatty/session/output_session.rb
|