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,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fatty
|
|
4
|
+
class ISearchSession < Session
|
|
5
|
+
action_on :session
|
|
6
|
+
|
|
7
|
+
attr_reader :field, :direction
|
|
8
|
+
|
|
9
|
+
def id = :isearch
|
|
10
|
+
|
|
11
|
+
DEFAULT_ISEARCH_HISTORY_FILE = File.expand_path("~/.fatty_search_history")
|
|
12
|
+
DEFAULT_ISEARCH_HISTORY_MAX = 200
|
|
13
|
+
|
|
14
|
+
def initialize(direction: :forward, last_pattern: nil, history: nil)
|
|
15
|
+
super(keymap: Keymaps.emacs, views: [])
|
|
16
|
+
|
|
17
|
+
@direction = direction.to_sym
|
|
18
|
+
@failed = false
|
|
19
|
+
@last_text = nil
|
|
20
|
+
@last_pattern = last_pattern.to_s
|
|
21
|
+
|
|
22
|
+
@field = Fatty::InputField.new(
|
|
23
|
+
prompt: Prompt.new { isearch_prompt },
|
|
24
|
+
history: history,
|
|
25
|
+
history_kind: :search_string,
|
|
26
|
+
# ? history_ctx: -> { { session: "pager_isearch" } },
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
#########################################################################################
|
|
31
|
+
# Framework and Session Hooks
|
|
32
|
+
#########################################################################################
|
|
33
|
+
|
|
34
|
+
def keymap_contexts
|
|
35
|
+
[:isearch, :text, :terminal]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def view(screen:, renderer:)
|
|
39
|
+
row = screen.output_rect.rows - 1
|
|
40
|
+
|
|
41
|
+
::Curses.curs_set(1)
|
|
42
|
+
renderer.render_pager_field(@field, row: row, role: :search_input)
|
|
43
|
+
renderer.restore_output_cursor(@field, row: row)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
############################################################################################
|
|
47
|
+
# Actions
|
|
48
|
+
############################################################################################
|
|
49
|
+
|
|
50
|
+
desc "Return the line so far as the search string"
|
|
51
|
+
action :isearch_accept do
|
|
52
|
+
accept!
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
desc "Quit the I-Search session"
|
|
56
|
+
action :isearch_cancel do
|
|
57
|
+
cancel!
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
desc "Move to the next matching text"
|
|
61
|
+
action :isearch_next do
|
|
62
|
+
step_next!
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
desc "Move to the prior matching text"
|
|
66
|
+
action :isearch_prev do
|
|
67
|
+
step_prev!
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def update_cmd(name, payload)
|
|
73
|
+
cmds = []
|
|
74
|
+
case name
|
|
75
|
+
when :isearch_set_failed
|
|
76
|
+
@failed = !!payload[:failed]
|
|
77
|
+
@field.prompt = isearch_prompt
|
|
78
|
+
end
|
|
79
|
+
cmds
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def action_env(event:)
|
|
83
|
+
ActionEnvironment.new(
|
|
84
|
+
session: self,
|
|
85
|
+
counter: counter,
|
|
86
|
+
event: event,
|
|
87
|
+
buffer: @field.buffer,
|
|
88
|
+
field: @field,
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def handle_action(action, args, event:)
|
|
93
|
+
env = action_env(event: event)
|
|
94
|
+
|
|
95
|
+
if Fatty::Actions.lookup(action)&.fetch(:on) == :session
|
|
96
|
+
Fatty::Actions.call(action, env, *args)
|
|
97
|
+
else
|
|
98
|
+
@field.act_on(action, *args, env: env)
|
|
99
|
+
maybe_preview!
|
|
100
|
+
end
|
|
101
|
+
rescue Fatty::ActionError
|
|
102
|
+
[]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def isearch_prompt
|
|
106
|
+
base = @failed ? "Failing I-search: " : "I-search: "
|
|
107
|
+
arrow = @direction == :backward ? "↑ " : "↓ "
|
|
108
|
+
arrow + base
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def accept!
|
|
112
|
+
pattern = @field.accept_line.to_s
|
|
113
|
+
cmds = []
|
|
114
|
+
cmds << [:terminal, :send_modal_owner, [:cmd, :pager_isearch_commit, { pattern: pattern, direction: @direction }]]
|
|
115
|
+
cmds << [:terminal, :pop_modal]
|
|
116
|
+
cmds
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def cancel!
|
|
120
|
+
[[:terminal, :send_modal_owner, [:cmd, :pager_isearch_cancel, {}]], [:terminal, :pop_modal]]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def step_next!
|
|
124
|
+
@direction = :forward
|
|
125
|
+
cmds = []
|
|
126
|
+
cmds.concat(prefill_last_pattern_if_empty!)
|
|
127
|
+
cmds << [:terminal, :send_modal_owner, [:cmd, :pager_isearch_step, { direction: @direction }]]
|
|
128
|
+
cmds
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def step_prev!
|
|
132
|
+
@direction = :backward
|
|
133
|
+
cmds = []
|
|
134
|
+
cmds.concat(prefill_last_pattern_if_empty!)
|
|
135
|
+
cmds << [:terminal, :send_modal_owner, [:cmd, :pager_isearch_step, { direction: @direction }]]
|
|
136
|
+
cmds
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def prefill_last_pattern_if_empty!
|
|
140
|
+
current = @field.buffer.text.to_s
|
|
141
|
+
pat = @last_pattern.to_s
|
|
142
|
+
return [] unless current.empty?
|
|
143
|
+
return [] if pat.strip.empty?
|
|
144
|
+
|
|
145
|
+
env = ActionEnvironment.new(
|
|
146
|
+
session: self,
|
|
147
|
+
counter: counter,
|
|
148
|
+
event: nil,
|
|
149
|
+
buffer: @field.buffer,
|
|
150
|
+
field: @field,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Replace buffer contents with the last pattern, then preview immediately.
|
|
154
|
+
@field.act_on(:replace, pat, env: env)
|
|
155
|
+
maybe_preview!
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def maybe_preview!
|
|
159
|
+
text = @field.buffer.text.to_s
|
|
160
|
+
return [] if @last_text == text
|
|
161
|
+
|
|
162
|
+
@last_text = text.dup
|
|
163
|
+
[
|
|
164
|
+
[
|
|
165
|
+
:terminal,
|
|
166
|
+
:send_modal_owner,
|
|
167
|
+
[:cmd, :pager_isearch_update, { pattern: text, direction: @direction }],
|
|
168
|
+
]
|
|
169
|
+
]
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
|
|
5
|
+
module Fatty
|
|
6
|
+
class KeyTestSession < Session
|
|
7
|
+
def id = :keytest
|
|
8
|
+
|
|
9
|
+
def initialize(owner: nil)
|
|
10
|
+
super(keymap: nil, views: [])
|
|
11
|
+
@owner = owner
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def init(terminal:)
|
|
15
|
+
super
|
|
16
|
+
@suggestions = []
|
|
17
|
+
@owner ||= terminal.focused_session
|
|
18
|
+
show_quit_status
|
|
19
|
+
force_scrolling_output!
|
|
20
|
+
append <<~TEXT
|
|
21
|
+
Key test mode
|
|
22
|
+
|
|
23
|
+
Press keys to inspect Fatty's decoding and binding resolution.
|
|
24
|
+
Press q, ESC, or C-g to leave key test mode.
|
|
25
|
+
|
|
26
|
+
TEXT
|
|
27
|
+
[]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def update_key(ev)
|
|
31
|
+
if quit_key?(ev)
|
|
32
|
+
append "Leaving key test mode.\n\n"
|
|
33
|
+
return [[:terminal, :pop_modal]]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# append " TERM: #{terminal_name}\n"
|
|
37
|
+
append ev.report
|
|
38
|
+
snippet = ev.suggested_snippet(terminal_name)
|
|
39
|
+
append snippet + "\n"
|
|
40
|
+
@suggestions << snippet unless snippet.empty?
|
|
41
|
+
show_quit_status
|
|
42
|
+
[]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def view(screen:, renderer:)
|
|
46
|
+
# Intentionally blank. The underlying shell session renders the output
|
|
47
|
+
# pane; this modal only captures keys and appends diagnostic text.
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def close
|
|
52
|
+
write_suggestions!
|
|
53
|
+
restore_owner_pager!
|
|
54
|
+
terminal.clear_status if terminal.respond_to?(:clear_status)
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def append(text)
|
|
61
|
+
return unless @owner&.respond_to?(:append_output)
|
|
62
|
+
|
|
63
|
+
force_scrolling_output!
|
|
64
|
+
|
|
65
|
+
before = @owner.output.lines.length
|
|
66
|
+
@owner.append_output(text.to_s, follow: false)
|
|
67
|
+
|
|
68
|
+
height = @owner.viewport.height.to_i
|
|
69
|
+
added = @owner.output.lines.length - before
|
|
70
|
+
|
|
71
|
+
if added >= height
|
|
72
|
+
@owner.viewport.top = before
|
|
73
|
+
@owner.viewport.clamp!(@owner.output.lines)
|
|
74
|
+
else
|
|
75
|
+
@owner.viewport.page_bottom(@owner.output.lines)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
terminal.renderer.reset_frame_cache if terminal.respond_to?(:renderer)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def force_scrolling_output!
|
|
82
|
+
if @owner&.respond_to?(:pager)
|
|
83
|
+
@owner.pager.paging_to_scrolling
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def quit_key?(ev)
|
|
88
|
+
ev.key == :q ||
|
|
89
|
+
ev.key == :escape ||
|
|
90
|
+
(ev.key == :g && ev.ctrl?)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def terminal_name
|
|
94
|
+
env =
|
|
95
|
+
if terminal.respond_to?(:env)
|
|
96
|
+
terminal.env
|
|
97
|
+
else
|
|
98
|
+
terminal.instance_variable_get(:@env)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
env && env[:terminal] || ENV.fetch("TERM", "unknown")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def show_quit_status
|
|
105
|
+
terminal.warn("KeyTest — press q, ESC, or C-g to quit (TERM: #{terminal_name})")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def write_suggestions!
|
|
109
|
+
suggestions = @suggestions.uniq
|
|
110
|
+
return if suggestions.empty?
|
|
111
|
+
|
|
112
|
+
path = File.join(Dir.tmpdir, "fatty-keytest-#{Time.now.strftime('%Y%m%d-%H%M%S')}.yml")
|
|
113
|
+
|
|
114
|
+
File.write(path, suggestion_file_text(suggestions))
|
|
115
|
+
|
|
116
|
+
append <<~TEXT
|
|
117
|
+
=======================================================
|
|
118
|
+
KeyTest suggestions written to:
|
|
119
|
+
#{path}
|
|
120
|
+
|
|
121
|
+
TEXT
|
|
122
|
+
|
|
123
|
+
terminal.good("KeyTest suggestions written to #{path}")
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def suggestion_file_text(suggestions)
|
|
127
|
+
out = <<~TEXT
|
|
128
|
+
# Fatty keytest suggestions
|
|
129
|
+
# Generated at #{Time.now}
|
|
130
|
+
|
|
131
|
+
KEYNAMES
|
|
132
|
+
|
|
133
|
+
Known/common key names for keydefs.yml/keybindings.yml:
|
|
134
|
+
TEXT
|
|
135
|
+
|
|
136
|
+
Fatty::Curses.valid_keynames.each do |name|
|
|
137
|
+
out << " #{name}\n"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
out << <<~TEXT
|
|
141
|
+
|
|
142
|
+
Printable keys are also valid key names, for example:
|
|
143
|
+
a
|
|
144
|
+
y
|
|
145
|
+
]
|
|
146
|
+
/
|
|
147
|
+
Use ctrl/meta/shift flags in keybindings.yml for modifiers.
|
|
148
|
+
|
|
149
|
+
NOTE: You can also define additional key names in keydefs.yml.
|
|
150
|
+
For example: if your terminal reports code 652 for the Pause key:
|
|
151
|
+
|
|
152
|
+
terminal:
|
|
153
|
+
tmux:
|
|
154
|
+
map:
|
|
155
|
+
652:
|
|
156
|
+
key: pause
|
|
157
|
+
|
|
158
|
+
Then you can bind it to an action in keybindings.yml:
|
|
159
|
+
|
|
160
|
+
- key: pause
|
|
161
|
+
action: your_action
|
|
162
|
+
|
|
163
|
+
ACTIONS
|
|
164
|
+
|
|
165
|
+
Valid action names for keybindings.yml:
|
|
166
|
+
|
|
167
|
+
NOTE: Fatty also supports user-defined actions. The extension mechanism is still
|
|
168
|
+
evolving; see README.md for the current plugin/action registration details.
|
|
169
|
+
TEXT
|
|
170
|
+
|
|
171
|
+
Fatty::Actions.catalog_by_target.each do |target, names|
|
|
172
|
+
out << "\n"
|
|
173
|
+
out << "#{target} actions:\n"
|
|
174
|
+
|
|
175
|
+
names.each do |name|
|
|
176
|
+
out << " #{name}\n"
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
out << <<~TEXT
|
|
181
|
+
|
|
182
|
+
CONTEXTS
|
|
183
|
+
|
|
184
|
+
Valid contexts for keybindings.yml:\n
|
|
185
|
+
TEXT
|
|
186
|
+
|
|
187
|
+
Fatty::KeyMap.valid_contexts.each do |name|
|
|
188
|
+
out << " #{name}\n"
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
out << <<~TEXT
|
|
192
|
+
If context is omitted, the default is #{Fatty::KeyMap::DEFAULT_CONTEXT}.
|
|
193
|
+
|
|
194
|
+
MOUSE EVENTS
|
|
195
|
+
|
|
196
|
+
Mouse events may also be bound in keybindings.yml, for example:
|
|
197
|
+
|
|
198
|
+
- mouse: <BUTTONNAME>
|
|
199
|
+
context: paging
|
|
200
|
+
action: page_up
|
|
201
|
+
|
|
202
|
+
Where <BUTTONNAME> is one of:
|
|
203
|
+
scroll_up
|
|
204
|
+
scroll_down
|
|
205
|
+
left_pressed,
|
|
206
|
+
left_released,
|
|
207
|
+
left_clicked,
|
|
208
|
+
left_double_clicked,
|
|
209
|
+
left_triple_clicked,
|
|
210
|
+
middle_pressed,
|
|
211
|
+
middle_released,
|
|
212
|
+
middle_clicked,
|
|
213
|
+
middle_double_clicked,
|
|
214
|
+
middle_triple_clicked,
|
|
215
|
+
right_pressed,
|
|
216
|
+
right_released,
|
|
217
|
+
right_clicked,
|
|
218
|
+
right_double_clicked,
|
|
219
|
+
right_triple_clicked
|
|
220
|
+
|
|
221
|
+
All of which may also be combined with modifiers: ctrl, meta, and shift.
|
|
222
|
+
|
|
223
|
+
TEXT
|
|
224
|
+
out << "\n"
|
|
225
|
+
out << suggestions.join("\n\n")
|
|
226
|
+
out << "\n"
|
|
227
|
+
out
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def restore_owner_pager!
|
|
231
|
+
if @owner&.respond_to?(:pager)
|
|
232
|
+
@owner.pager.quit_paging
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fatty
|
|
4
|
+
class ModalSession < Session
|
|
5
|
+
attr_reader :win
|
|
6
|
+
private attr_writer :win
|
|
7
|
+
|
|
8
|
+
def close
|
|
9
|
+
Fatty.debug("#{self.class}#close: object_id=#{object_id}", tag: :session)
|
|
10
|
+
old_win = win
|
|
11
|
+
self.win = nil
|
|
12
|
+
safely_close_window(old_win)
|
|
13
|
+
nil
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def handle_resize
|
|
17
|
+
rebuild_windows!
|
|
18
|
+
[]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def rebuild_windows!
|
|
22
|
+
old_win = win
|
|
23
|
+
self.win = nil
|
|
24
|
+
safely_close_window(old_win)
|
|
25
|
+
|
|
26
|
+
cols = ::Curses.cols
|
|
27
|
+
rows = ::Curses.lines
|
|
28
|
+
width, height = geometry(cols: cols, rows: rows)
|
|
29
|
+
x = (cols - width) / 2
|
|
30
|
+
y = (rows - height) / 2
|
|
31
|
+
self.win = ::Curses::Window.new(height, width, y, x)
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Return the outer width and height of the window for this modal,
|
|
36
|
+
# including any padding and borders.
|
|
37
|
+
def geometry(cols:, rows:)
|
|
38
|
+
raise NotImplementedError, "#{self.class} must implement #geometry"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Common helpers for computing geometry
|
|
42
|
+
|
|
43
|
+
def max_width(cols:, margin:, min_width:)
|
|
44
|
+
[cols - (margin * 2), min_width].max
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def max_height(rows:, margin:, min_height:)
|
|
48
|
+
[rows - (margin * 2), min_height].max
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def clamp_width(width, max_width:, hard_max: nil)
|
|
52
|
+
width = [width, max_width].min
|
|
53
|
+
width = [width, hard_max].min if hard_max
|
|
54
|
+
width
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def clamp_height(height, max_height:, min_height:)
|
|
58
|
+
height.clamp(min_height, max_height)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fatty
|
|
4
|
+
class OutputSession < Session
|
|
5
|
+
attr_reader :output, :viewport, :pager, :pager_field
|
|
6
|
+
|
|
7
|
+
def initialize(keymap: nil, views: [])
|
|
8
|
+
super(keymap: keymap, views: views)
|
|
9
|
+
@output = Fatty::OutputBuffer.new(max_lines: 500_000)
|
|
10
|
+
@viewport = Fatty::Viewport.new(height: 10)
|
|
11
|
+
mode = Fatty::Config.config.dig(:output, :mode)&.to_sym || :paging
|
|
12
|
+
@default_output_mode = mode
|
|
13
|
+
@pager = Fatty::Pager.new(output: @output, viewport: @viewport, mode: mode)
|
|
14
|
+
@pager_field = Fatty::InputField.new(prompt: -> { pager_status_prompt })
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def append_output(text, follow: true)
|
|
18
|
+
ntrim = @output.append(text.to_s)
|
|
19
|
+
@pager.on_append(ntrim: ntrim)
|
|
20
|
+
|
|
21
|
+
# When callers request follow behavior (scrolling mode),
|
|
22
|
+
# keep the viewport at the bottom unless paging is actively paused.
|
|
23
|
+
if follow && !@pager.paused?
|
|
24
|
+
case @pager.mode
|
|
25
|
+
when :scrolling
|
|
26
|
+
@viewport.page_bottom(@output.lines)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
ntrim
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def reset_output!
|
|
33
|
+
@output.lines.clear
|
|
34
|
+
@viewport.reset
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def resize_output!
|
|
38
|
+
was_at_bottom = pager.at_bottom?
|
|
39
|
+
@viewport.height = terminal.screen.output_rect.rows
|
|
40
|
+
pager.preserve_after_resize!(was_at_bottom: was_at_bottom)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def reset_for_command!
|
|
44
|
+
reset_output!
|
|
45
|
+
mode = @default_output_mode # Fatty::Config.config.dig(:output, :mode)&.to_sym || :paging
|
|
46
|
+
@pager.reset!(mode: mode)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# True when the pager is currently holding the screen (i.e., paging mode is
|
|
50
|
+
# active and the output is paused).
|
|
51
|
+
def pager_active?
|
|
52
|
+
@pager.reserve_prompt_row?
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# When the pager is active, the last output row is reserved for the pager
|
|
56
|
+
# InputField. This returns the viewport to use for rendering the output
|
|
57
|
+
# content itself.
|
|
58
|
+
def pager_viewport
|
|
59
|
+
if pager_active? && @viewport.height > 1
|
|
60
|
+
Fatty::Viewport.new(top: @viewport.top, height: @viewport.height - 1)
|
|
61
|
+
else
|
|
62
|
+
@viewport
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def pager_status_prompt
|
|
67
|
+
total = @output.lines.length
|
|
68
|
+
visible_h = pager_active? ? pager.page_height : @viewport.height
|
|
69
|
+
bottom = [@viewport.top + visible_h, total].min
|
|
70
|
+
pct =
|
|
71
|
+
if total.positive?
|
|
72
|
+
((bottom * 100.0) / total).round
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
search = pager.search_label
|
|
76
|
+
search = " [#{search}]" if search && !search.empty?
|
|
77
|
+
if pct
|
|
78
|
+
" #{pager.nav_arrow} --More-- #{bottom}/#{total} (#{pct}%)#{search} "
|
|
79
|
+
else
|
|
80
|
+
" #{pager.nav_arrow} --More-- #{bottom}#{search} "
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def reset_pager!
|
|
85
|
+
# Start the next command from the bottom of existing scrollback.
|
|
86
|
+
@viewport.page_bottom(@output.lines)
|
|
87
|
+
@pager.reset!(total_lines: @output.lines.length, mode: @default_output_mode)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def follow_output!
|
|
91
|
+
pager.end_of_output
|
|
92
|
+
[]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def page_output_from_top!
|
|
96
|
+
pager.page_top
|
|
97
|
+
[]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def page_output_from_bottom!
|
|
101
|
+
pager.page_bottom
|
|
102
|
+
[]
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|