fatty 0.99.0 → 0.99.3
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/CHANGELOG.org +11 -29
- data/README.md +2207 -18
- data/README.org +2066 -104
- data/exe/fatty +236 -93
- data/help/help.md +233 -0
- data/lib/fatty/action.rb +3 -7
- data/lib/fatty/actionable.rb +2 -0
- data/lib/fatty/alert.rb +65 -39
- data/lib/fatty/ansi/renderer.rb +26 -6
- data/lib/fatty/ansi.rb +1 -0
- data/lib/fatty/api/alert.rb +15 -0
- data/lib/fatty/api/choose.rb +145 -0
- data/lib/fatty/api/environment.rb +12 -0
- data/lib/fatty/api/keytest.rb +11 -0
- data/lib/fatty/api/menu.rb +104 -0
- data/lib/fatty/api/output.rb +55 -0
- data/lib/fatty/api/progress.rb +24 -0
- data/lib/fatty/api/prompt.rb +77 -0
- data/lib/fatty/api/status.rb +55 -0
- data/lib/fatty/api.rb +38 -0
- data/lib/fatty/callback_environment.rb +74 -0
- data/lib/fatty/colors/color.rb +366 -232
- data/lib/fatty/colors/pairs.rb +2 -0
- data/lib/fatty/command.rb +78 -0
- data/lib/fatty/config.rb +227 -39
- data/lib/fatty/config_files/config.yml +4 -12
- data/lib/fatty/config_files/keydefs.yml +13 -23
- data/lib/fatty/config_files/themes/capuchin_monk.yml +140 -0
- data/lib/fatty/config_files/themes/catppuccin_latte.yml +140 -0
- data/lib/fatty/config_files/themes/catppuccin_mocha.yml +140 -0
- data/lib/fatty/config_files/themes/cyberpunk.yml +141 -0
- data/lib/fatty/config_files/themes/dracula.yml +140 -0
- data/lib/fatty/config_files/themes/everforest_dark.yml +140 -0
- data/lib/fatty/config_files/themes/gruvbox_dark.yml +140 -0
- data/lib/fatty/config_files/themes/gruvbox_light.yml +140 -0
- data/lib/fatty/config_files/themes/mono.yml +119 -47
- data/lib/fatty/config_files/themes/monokai.yml +140 -0
- data/lib/fatty/config_files/themes/nordic.yml +1 -0
- data/lib/fatty/config_files/themes/onedark.yml +140 -0
- data/lib/fatty/config_files/themes/solarized_dark.yml +1 -0
- data/lib/fatty/config_files/themes/solarized_light.yml +140 -0
- data/lib/fatty/config_files/themes/terminal.yml +67 -0
- data/lib/fatty/config_files/themes/tokyo_night.yml +140 -0
- data/lib/fatty/config_files/themes/wordperfect.yml +80 -9
- data/lib/fatty/core_ext/hash.rb +19 -0
- data/lib/fatty/core_ext/string.rb +19 -12
- data/lib/fatty/core_ext.rb +1 -0
- data/lib/fatty/curses/context.rb +86 -84
- data/lib/fatty/curses/event_source.rb +128 -40
- data/lib/fatty/curses/key_decoder.rb +77 -11
- data/lib/fatty/env.rb +21 -20
- data/lib/fatty/help.rb +1 -29
- data/lib/fatty/history/entry.rb +2 -0
- data/lib/fatty/history.rb +17 -14
- data/lib/fatty/input_buffer.rb +71 -47
- data/lib/fatty/input_field.rb +216 -149
- data/lib/fatty/key_event.rb +67 -66
- data/lib/fatty/key_map.rb +54 -16
- data/lib/fatty/keymaps/emacs.rb +40 -6
- data/lib/fatty/log_formats/json.rb +2 -0
- data/lib/fatty/log_formats/text.rb +2 -0
- data/lib/fatty/logger.rb +7 -2
- data/lib/fatty/markdown/ansi_renderer.rb +75 -6
- data/lib/fatty/markdown/render.rb +2 -3
- data/lib/fatty/mouse_event.rb +142 -2
- data/lib/fatty/output_buffer.rb +2 -0
- data/lib/fatty/pager.rb +158 -47
- data/lib/fatty/progress.rb +417 -0
- data/lib/fatty/renderer/curses.rb +340 -184
- data/lib/fatty/renderer/truecolor.rb +266 -163
- data/lib/fatty/renderer.rb +115 -97
- data/lib/fatty/screen.rb +2 -0
- data/lib/fatty/session/alert_session.rb +39 -21
- data/lib/fatty/session/isearch_session.rb +92 -38
- data/lib/fatty/session/keytest_session.rb +105 -85
- data/lib/fatty/session/modal_session.rb +38 -1
- data/lib/fatty/session/output_session.rb +472 -21
- data/lib/fatty/session/popup_session.rb +251 -134
- data/lib/fatty/session/prompt_session.rb +68 -26
- data/lib/fatty/session/search_session.rb +79 -31
- data/lib/fatty/session/shell_session.rb +383 -368
- data/lib/fatty/session/status_session.rb +167 -0
- data/lib/fatty/session.rb +95 -95
- data/lib/fatty/terminal/popup_owner.rb +14 -11
- data/lib/fatty/terminal.rb +515 -813
- data/lib/fatty/themes/loader.rb +4 -2
- data/lib/fatty/themes/manager.rb +4 -0
- data/lib/fatty/themes/registry.rb +2 -0
- data/lib/fatty/themes/resolver.rb +175 -162
- data/lib/fatty/version.rb +1 -1
- data/lib/fatty/viewport.rb +8 -1
- data/lib/fatty.rb +35 -7
- metadata +42 -40
- data/.envrc +0 -2
- data/.simplecov +0 -23
- data/.yardopts +0 -4
- data/CHANGELOG.md +0 -34
- data/Rakefile +0 -15
- data/TODO.org +0 -163
- data/examples/markdown/native-markdown.md +0 -370
- data/examples/markdown/ox-gfm-markdown.md +0 -373
- data/examples/markdown/ox-gfm-markdown.org +0 -376
- data/fatty.gemspec +0 -42
- data/lib/fatty/accept_env.rb +0 -32
- data/lib/fatty/config_files/help.md +0 -120
- data/lib/fatty/config_files/help.org +0 -124
- data/lib/fatty/config_files/themes/wordperfect_light.yml +0 -77
- data/lib/fatty/menu_env.rb +0 -22
- data/lib/fatty/session/input_session.rb +0 -99
- data/lib/fatty/sessions.rb +0 -14
- data/lib/fatty/terminal/progress.rb +0 -374
- data/lib/fatty/view/alert_view.rb +0 -14
- data/lib/fatty/view/cursor_view.rb +0 -18
- data/lib/fatty/view/input_view.rb +0 -9
- data/lib/fatty/view/output_view.rb +0 -9
- data/lib/fatty/view/status_view.rb +0 -14
- data/lib/fatty/view.rb +0 -33
- data/lib/fatty/views.rb +0 -9
- data/sig/fatty.rbs +0 -4
data/lib/fatty/terminal.rb
CHANGED
|
@@ -1,738 +1,430 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative 'terminal/progress'
|
|
4
3
|
require_relative 'terminal/popup_owner'
|
|
5
4
|
|
|
6
5
|
module Fatty
|
|
6
|
+
# Fatty aims at providing a full-featured, curses-based platform for
|
|
7
|
+
# REPL-style interaction with
|
|
8
|
+
# - command-line editing (by default using Emacs-like keybindings),
|
|
9
|
+
# - an output pane for normal output, a status pane for out-of-band
|
|
10
|
+
# messages to the user, and an alert pane for library-related error or
|
|
11
|
+
# information messages,
|
|
12
|
+
# - history traversal and context-aware history,
|
|
13
|
+
# - command completions based on input-so-far,
|
|
14
|
+
# - popup facilities for things such as prompting the user for a string,
|
|
15
|
+
# selecting one or more values from a list, selecting user-defined
|
|
16
|
+
# actions from a menu, and more,
|
|
17
|
+
# - a variety of progress indicators to keep the user informed about what's
|
|
18
|
+
# happening in a long-running process.
|
|
19
|
+
# - scrolling and searching long output to the output pane,
|
|
20
|
+
# - rendering markdown text in the output pane,
|
|
21
|
+
# - a theming system with several pre-defined color themes and facilities
|
|
22
|
+
# for the user to add their own,
|
|
23
|
+
# - a way to define keys that curses does not recognize and assign actions
|
|
24
|
+
# to them, including actions the user defines
|
|
25
|
+
# - rendering ANSI colored text to the output pane and the status pane,
|
|
26
|
+
# - a way to suspend Fatty and exit to your real terminal and come back to
|
|
27
|
+
# where you left off when you exit that terminal session.
|
|
28
|
+
#
|
|
29
|
+
# In other words, Fatty does much of the heavy lifting needed for a
|
|
30
|
+
# comfortable, full-featured interactive application.
|
|
31
|
+
#
|
|
32
|
+
# Fatty does not purport to be a "terminal" program but sits on top of them
|
|
33
|
+
# using curses and truecolor rendering into your normal terminal.
|
|
34
|
+
#
|
|
35
|
+
# Terminal is the main entry point. A library user normally creates a
|
|
36
|
+
# Terminal, configures the prompt, completion, history, and accept callback,
|
|
37
|
+
# then calls #go.
|
|
38
|
+
#
|
|
39
|
+
# Initialization options:
|
|
40
|
+
#
|
|
41
|
+
# - prompt :: either a fixed string or a proc that, when called, returns a
|
|
42
|
+
# string to be used as the command-line prompt;
|
|
43
|
+
#
|
|
44
|
+
# - on_accept :: a proc that takes the edited line and performs some action
|
|
45
|
+
# on it; the proc will have available to it a variable, `env`, that
|
|
46
|
+
# contains an CallbackEnvironment object containing a reference to the
|
|
47
|
+
# ShellSession and Terminal so that it can call actions on them, most
|
|
48
|
+
# commonly `env.session.append_output(text)` to write output to the output
|
|
49
|
+
# pane;
|
|
50
|
+
#
|
|
51
|
+
# - completion_proc :: a proc that returns an Array of strings based on the
|
|
52
|
+
# input-so-far to suggest completions that can be inserted at that point;
|
|
53
|
+
#
|
|
54
|
+
# - history_path :: a path or sentinel describing input history is loaded
|
|
55
|
+
# from and persisted;
|
|
56
|
+
#
|
|
57
|
+
# - history_ctx :: a proc that provides a "context" for history so that it
|
|
58
|
+
# can be "favored" by history requests in the same context later;
|
|
59
|
+
#
|
|
60
|
+
# - env :: an optional Fatty::Env describing the runtime environment; when
|
|
61
|
+
# omitted, Fatty will detect it with `Fatty::Env.detect`.
|
|
62
|
+
#
|
|
7
63
|
class Terminal
|
|
8
64
|
SCROLL_RENDER_THROTTLE = 0.05
|
|
9
|
-
DEFAULT_STATUS_MAX_ROWS = 4
|
|
10
65
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
# Suggested shapes:
|
|
14
|
-
#
|
|
15
|
-
# Terminal/runtime commands:
|
|
16
|
-
# [:terminal, :quit]
|
|
17
|
-
# [:terminal, :push, session]
|
|
18
|
-
# [:terminal, :pop]
|
|
19
|
-
#
|
|
20
|
-
# Session-targeted commands (no special casing):
|
|
21
|
-
# [:send, :alert, :show, { level: :warn, message: "No matches" }]
|
|
22
|
-
# [:send, :alert, :clear, {}]
|
|
23
|
-
#
|
|
24
|
-
# You can add more later; Terminal only needs a small dispatcher.
|
|
25
|
-
|
|
26
|
-
attr_reader :screen, :renderer, :event_source, :status_text, :status_role, :env
|
|
66
|
+
attr_reader :screen, :renderer, :event_source, :env, :ctx
|
|
67
|
+
attr_reader :shell_session, :status_session, :alert_session, :focused_session
|
|
27
68
|
|
|
28
69
|
def initialize(prompt: "> ",
|
|
29
70
|
on_accept: nil,
|
|
71
|
+
app_name: nil,
|
|
72
|
+
app_config_dir: nil,
|
|
30
73
|
completion_proc: nil,
|
|
31
74
|
history_path: :default,
|
|
32
75
|
history_ctx: nil,
|
|
33
76
|
env: nil)
|
|
34
77
|
@prompt = Prompt.ensure(prompt)
|
|
35
78
|
@on_accept = on_accept
|
|
79
|
+
@app_name = app_name
|
|
80
|
+
@app_config_dir = app_config_dir
|
|
36
81
|
@completion_proc = completion_proc
|
|
37
82
|
@history_path = history_path
|
|
38
83
|
@history_ctx = history_ctx
|
|
39
84
|
@env = env
|
|
85
|
+
@renderer = nil
|
|
40
86
|
|
|
41
87
|
@running = false
|
|
42
|
-
@stack = []
|
|
43
|
-
@pinned = []
|
|
44
88
|
@sessions = []
|
|
45
89
|
@sessions_by_id = {}
|
|
46
90
|
@modal_stack = []
|
|
47
|
-
@
|
|
48
|
-
@status_role = :info
|
|
49
|
-
@status_transient = false
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
# --- Status line management ------------------------------------------------
|
|
53
|
-
|
|
54
|
-
def set_status(text, role: :info, transient: false)
|
|
55
|
-
old_rows = status_rows
|
|
56
|
-
str =
|
|
57
|
-
if text.is_a?(Array)
|
|
58
|
-
text.map { |part| part.is_a?(Hash) && part.key?(:text) ? part[:text] : part }.join
|
|
59
|
-
else
|
|
60
|
-
text.to_s
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
if str.empty?
|
|
64
|
-
@status_text = nil
|
|
65
|
-
@status_role = :info
|
|
66
|
-
@status_transient = false
|
|
67
|
-
else
|
|
68
|
-
@status_text = text
|
|
69
|
-
@status_role = role
|
|
70
|
-
@status_transient = transient
|
|
71
|
-
end
|
|
72
|
-
refresh_layout! if @screen && old_rows != status_rows
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def clear_status
|
|
76
|
-
old_rows = status_rows
|
|
77
|
-
@status_text = nil
|
|
78
|
-
@status_role = :info
|
|
79
|
-
@status_transient = false
|
|
80
|
-
refresh_layout! if @screen && old_rows != status_rows
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def transient_status?
|
|
84
|
-
!!@status_transient
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
# Display a message to the user in the status line, colored according to
|
|
88
|
-
# the Config for "info".
|
|
89
|
-
def info(text)
|
|
90
|
-
return $stderr.puts(text) unless @ctx
|
|
91
|
-
|
|
92
|
-
set_status(text.to_s, role: :info, transient: true)
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
# Display a message to the user in the status line, colored according to
|
|
96
|
-
# the Config for "good," i.e., success.
|
|
97
|
-
def good(text)
|
|
98
|
-
return $stderr.puts(text) unless @ctx
|
|
99
|
-
|
|
100
|
-
set_status(text.to_s, role: :good, transient: true)
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
# Display a message to the user in the status line, colored according to
|
|
104
|
-
# the Config for "warn," i.e., short of an error but not complete
|
|
105
|
-
# success either.
|
|
106
|
-
def warn(text)
|
|
107
|
-
return $stderr.puts(text) unless @ctx
|
|
108
|
-
|
|
109
|
-
set_status(text.to_s, role: :warn, transient: true)
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
# Display a message to the user in the status line, colored according to
|
|
113
|
-
# the Config for "oops," i.e., a soft failure.
|
|
114
|
-
def oops(text)
|
|
115
|
-
return $stderr.puts(text) unless @ctx
|
|
116
|
-
|
|
117
|
-
set_status(text.to_s, role: :error, transient: true)
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
# --- Session management ------------------------------------------------
|
|
121
|
-
|
|
122
|
-
def push(session)
|
|
123
|
-
Fatty.debug("Terminal#push(#{session})", tag: :session)
|
|
124
|
-
@stack << session
|
|
125
|
-
register(session)
|
|
126
|
-
commands = session.init(terminal: self)
|
|
127
|
-
apply_commands(commands)
|
|
128
|
-
session
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
def pop
|
|
132
|
-
Fatty.debug("Terminal#pop -> #{@stack.last}", tag: :session)
|
|
133
|
-
@stack.pop
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
def pin(session)
|
|
137
|
-
Fatty.debug("Terminal#pin(#{session})", tag: :session)
|
|
138
|
-
@pinned << session
|
|
139
|
-
register(session)
|
|
140
|
-
commands = session.init(terminal: self)
|
|
141
|
-
apply_commands(commands)
|
|
142
|
-
session
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
def active_session
|
|
146
|
-
top = @modal_stack.last
|
|
147
|
-
return top[:session] if top
|
|
148
|
-
|
|
149
|
-
focused_session
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
def focused_session
|
|
153
|
-
top = @stack.last
|
|
154
|
-
return top[:session] if top.is_a?(Hash)
|
|
155
|
-
|
|
156
|
-
top
|
|
157
|
-
end
|
|
91
|
+
@status_rows = 0
|
|
158
92
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
@
|
|
93
|
+
@ctx = nil
|
|
94
|
+
@immediate_render = false
|
|
95
|
+
@session_dirty = false
|
|
96
|
+
@render_requested = false
|
|
97
|
+
@last_render_time = nil
|
|
98
|
+
@restore_cursor_after_render = true
|
|
99
|
+
@render_count = 0
|
|
100
|
+
@deferred_count = 0
|
|
164
101
|
end
|
|
165
102
|
|
|
166
|
-
def
|
|
167
|
-
@
|
|
103
|
+
def inspect
|
|
104
|
+
"Terminal: prompt: #{@prompt}; history: #{@history_path}; sessions size: #{@sessions.size} active: #{active_session.id}"
|
|
168
105
|
end
|
|
169
106
|
|
|
170
|
-
def
|
|
171
|
-
|
|
172
|
-
msg = "Terminal#push_modal: size=#{@modal_stack.length} session=#{session.class} object_id=#{session.object_id}"
|
|
173
|
-
Fatty.debug(msg, tag: :session)
|
|
174
|
-
register(session)
|
|
175
|
-
@renderer.invalidate! if defined?(@renderer) && @renderer
|
|
176
|
-
commands = session.init(terminal: self)
|
|
177
|
-
apply_commands(commands)
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
def pop_modal
|
|
181
|
-
top = @modal_stack.pop
|
|
182
|
-
msg = "Terminal#pop_modal: size=#{@modal_stack.length} popped=#{top && top[:session].class}"
|
|
183
|
-
Fatty.debug(msg, tag: :session)
|
|
184
|
-
session = top && top[:session]
|
|
185
|
-
|
|
186
|
-
session.close if session&.respond_to?(:close)
|
|
187
|
-
@renderer&.invalidate!
|
|
188
|
-
nil
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
# Return the owner of the top modal session without modifying the stack.
|
|
192
|
-
def modal_owner
|
|
193
|
-
top = @modal_stack.last
|
|
194
|
-
top && top[:owner]
|
|
107
|
+
def to_s
|
|
108
|
+
inspect
|
|
195
109
|
end
|
|
196
110
|
|
|
197
111
|
# --- Runtime -----------------------------------------------------------
|
|
198
|
-
|
|
112
|
+
# Run the terminal.
|
|
113
|
+
#
|
|
114
|
+
# This method owns Fatty's main lifecycle:
|
|
115
|
+
#
|
|
116
|
+
# - load configuration and logging;
|
|
117
|
+
# - start curses and build the screen, renderer, and event source;
|
|
118
|
+
# - install the default permanent sessions;
|
|
119
|
+
# - enter the event loop;
|
|
120
|
+
# - restore the real terminal and persist session state on exit.
|
|
121
|
+
#
|
|
122
|
+
# The event loop has two sources of work. First, it polls the EventSource
|
|
123
|
+
# for user input, resize events, and other terminal events. Any event is
|
|
124
|
+
# routed to the active session, whose #update method may mutate session
|
|
125
|
+
# state and return commands for Terminal to process. Second, the active
|
|
126
|
+
# session is ticked once per loop so it can animate, poll, or otherwise
|
|
127
|
+
# mark itself dirty without receiving a key event.
|
|
128
|
+
#
|
|
129
|
+
# Whenever an event or tick changes state, Terminal schedules a render. Most
|
|
130
|
+
# renders happen immediately. The exception is fast scrolling in the normal
|
|
131
|
+
# curses renderer, where repeated output updates may be throttled to avoid
|
|
132
|
+
# excessive redraws. Truecolor rendering and direct user input are rendered
|
|
133
|
+
# immediately.
|
|
134
|
+
#
|
|
135
|
+
# Fatal errors are logged and re-raised, but cleanup still runs: curses is
|
|
136
|
+
# stopped so the real terminal is restored, and persistent sessions are given
|
|
137
|
+
# a chance to save their state.
|
|
199
138
|
def go
|
|
200
139
|
preflight!
|
|
201
140
|
start_curses!
|
|
202
141
|
install_default_sessions!
|
|
142
|
+
Fatty.debug("@sessions: #{@sessions.map(&:id).join('==')}")
|
|
203
143
|
|
|
204
144
|
@running = true
|
|
205
|
-
|
|
206
|
-
pending_scroll_render = false
|
|
145
|
+
@last_render_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
146
|
+
@pending_scroll_render = false
|
|
207
147
|
render_frame
|
|
208
148
|
|
|
209
|
-
# For performance logging
|
|
210
|
-
perf_started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
211
149
|
loop_count = 0
|
|
212
|
-
event_count = 0
|
|
213
|
-
tick_dirty_count = 0
|
|
214
|
-
render_count = 0
|
|
215
|
-
deferred_count = 0
|
|
216
|
-
frame_ms = 0.0
|
|
217
|
-
|
|
218
150
|
while @running
|
|
219
151
|
loop_count += 1
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
tick_dirty = !!s&.tick
|
|
234
|
-
tick_dirty_count += 1 if tick_dirty
|
|
235
|
-
dirty ||= tick_dirty
|
|
236
|
-
rescue StandardError => e
|
|
237
|
-
Fatty.error("Terminal#go tick failed: #{e.class}: #{e.message}", tag: :terminal)
|
|
238
|
-
dirty = true
|
|
239
|
-
immediate = true
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
if dirty
|
|
243
|
-
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
244
|
-
|
|
245
|
-
if immediate || renderer.context.truecolor || !scrolling_output?
|
|
246
|
-
render_frame
|
|
247
|
-
last_render = now
|
|
248
|
-
pending_scroll_render = false
|
|
249
|
-
render_count += 1
|
|
250
|
-
elsif now - last_render >= SCROLL_RENDER_THROTTLE
|
|
251
|
-
render_frame
|
|
252
|
-
last_render = now
|
|
253
|
-
pending_scroll_render = false
|
|
254
|
-
render_count += 1
|
|
255
|
-
else
|
|
256
|
-
pending_scroll_render = true
|
|
257
|
-
deferred_count += 1
|
|
258
|
-
end
|
|
259
|
-
elsif pending_scroll_render
|
|
260
|
-
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
261
|
-
if now - last_render >= SCROLL_RENDER_THROTTLE
|
|
262
|
-
render_frame
|
|
263
|
-
last_render = now
|
|
264
|
-
pending_scroll_render = false
|
|
265
|
-
render_count += 1
|
|
152
|
+
@session_dirty = false
|
|
153
|
+
@immediate_render = false
|
|
154
|
+
if (cmd = event_source.next_event)
|
|
155
|
+
begin
|
|
156
|
+
apply_command(cmd)
|
|
157
|
+
@session_dirty = true
|
|
158
|
+
@immediate_render = true
|
|
159
|
+
rescue StandardError => e
|
|
160
|
+
Fatty.error("Terminal#go apply_command failed: #{e.class}: #{e.message}", tag: :terminal)
|
|
161
|
+
Fatty.error(e.backtrace.join("\n"), tag: :terminal) if e.backtrace
|
|
162
|
+
@session_dirty = true
|
|
163
|
+
@immediate_render = true
|
|
164
|
+
raise
|
|
266
165
|
end
|
|
267
166
|
end
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
if now - perf_started_at >= 1.0
|
|
271
|
-
avg_frame_ms =
|
|
272
|
-
if render_count.zero?
|
|
273
|
-
0.0
|
|
274
|
-
else
|
|
275
|
-
(frame_ms / render_count).round(2)
|
|
276
|
-
end
|
|
277
|
-
|
|
278
|
-
# Performance logging
|
|
279
|
-
Fatty.debug(
|
|
280
|
-
"perf loops=#{loop_count} events=#{event_count} " \
|
|
281
|
-
"tick_dirty=#{tick_dirty_count} renders=#{render_count} " \
|
|
282
|
-
"deferred=#{deferred_count} avg_frame_ms=#{avg_frame_ms} " \
|
|
283
|
-
"scrolling=#{scrolling_output?}",
|
|
284
|
-
tag: :perf,
|
|
285
|
-
)
|
|
286
|
-
perf_started_at = now
|
|
287
|
-
loop_count = 0
|
|
288
|
-
event_count = 0
|
|
289
|
-
tick_dirty_count = 0
|
|
290
|
-
render_count = 0
|
|
291
|
-
deferred_count = 0
|
|
292
|
-
frame_ms = 0.0
|
|
293
|
-
end
|
|
167
|
+
@session_dirty ||= active_session&.tick
|
|
168
|
+
render_frame if render_due?
|
|
294
169
|
end
|
|
295
170
|
rescue => e
|
|
296
171
|
Fatty.error("Terminal#go fatal error: #{e.class}: #{e.message}", tag: :terminal)
|
|
297
172
|
Fatty.error(e.backtrace.join("\n"), tag: :terminal) if e.backtrace
|
|
298
173
|
raise
|
|
299
174
|
ensure
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
rescue => e
|
|
303
|
-
Fatty.error("Terminal#go stop_curses! failed: #{e.class}: #{e.message}", tag: :terminal)
|
|
304
|
-
Fatty.error(e.backtrace.join("\n"), tag: :terminal) if e.backtrace
|
|
305
|
-
end
|
|
175
|
+
cleanup_after_go
|
|
176
|
+
end
|
|
306
177
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
178
|
+
# --- Suspension ------------------------------------------------
|
|
179
|
+
|
|
180
|
+
# Suspend fatty and run the block in the normal terminal, then restore
|
|
181
|
+
# fatty's terminal on exit.
|
|
182
|
+
def suspend
|
|
183
|
+
stop_curses!
|
|
184
|
+
yield
|
|
185
|
+
ensure
|
|
186
|
+
start_curses!
|
|
187
|
+
refresh_layout!
|
|
188
|
+
render_frame
|
|
313
189
|
end
|
|
314
190
|
|
|
315
|
-
#
|
|
316
|
-
#
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
labels = items.map(&:first)
|
|
322
|
-
popup = Fatty::PopUpSession.new(
|
|
323
|
-
source: labels,
|
|
324
|
-
kind: :terminal_choose,
|
|
325
|
-
title: "Choose",
|
|
326
|
-
message: prompt,
|
|
327
|
-
prompt: "> ",
|
|
328
|
-
selection: :top,
|
|
329
|
-
validate_unique_labels: true,
|
|
330
|
-
)
|
|
191
|
+
# A command is either bound for this Terminal or it's meant to be
|
|
192
|
+
# forwarded to a Session. This method routes the command to its proper
|
|
193
|
+
# destination.
|
|
194
|
+
def apply_command(command)
|
|
195
|
+
command = Fatty::Command.coerce(command)
|
|
196
|
+
return unless command
|
|
331
197
|
|
|
332
|
-
|
|
198
|
+
@apply_command_depth ||= 0
|
|
199
|
+
outermost = @apply_command_depth.zero?
|
|
200
|
+
@apply_command_depth += 1
|
|
333
201
|
|
|
334
|
-
|
|
335
|
-
|
|
202
|
+
Fatty.debug(
|
|
203
|
+
"Terminal#apply_command: active_session: #{active_session.id}",
|
|
204
|
+
tag: :session,
|
|
205
|
+
)
|
|
206
|
+
Fatty.debug("Terminal#apply_command(#{command.inspect})", tag: :session)
|
|
336
207
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
result = idx ? items[idx][1] : quit_value
|
|
341
|
-
done = true
|
|
208
|
+
if user_interaction_command?(command)
|
|
209
|
+
apply_command(Command.session(:status, :clear))
|
|
210
|
+
apply_command(Command.session(:alert, :clear))
|
|
342
211
|
end
|
|
343
212
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
213
|
+
if command.terminal?
|
|
214
|
+
apply_terminal_command(command)
|
|
215
|
+
else
|
|
216
|
+
apply_session_command(command)
|
|
347
217
|
end
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
render_frame
|
|
353
|
-
|
|
354
|
-
while !done && @running
|
|
355
|
-
dirty = false
|
|
356
|
-
msg = event_source.next_event
|
|
357
|
-
if msg
|
|
358
|
-
dispatch_message(msg)
|
|
359
|
-
dirty = true
|
|
360
|
-
end
|
|
361
|
-
|
|
362
|
-
s = active_session
|
|
363
|
-
begin
|
|
364
|
-
tick_dirty = !!s&.tick
|
|
365
|
-
dirty ||= tick_dirty
|
|
366
|
-
rescue StandardError => e
|
|
367
|
-
Fatty.error("Terminal#choose tick failed: #{e.class}: #{e.message}", tag: :terminal)
|
|
368
|
-
dirty = true
|
|
369
|
-
end
|
|
370
|
-
|
|
371
|
-
render_frame if dirty
|
|
372
|
-
end
|
|
373
|
-
ensure
|
|
374
|
-
render_frame
|
|
218
|
+
ensure
|
|
219
|
+
if command
|
|
220
|
+
@apply_command_depth -= 1
|
|
221
|
+
render_frame if outermost && render_due?
|
|
375
222
|
end
|
|
376
|
-
|
|
377
|
-
result
|
|
378
223
|
end
|
|
379
224
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
idx = default ? 0 : 1
|
|
383
|
-
choose(
|
|
384
|
-
prompt,
|
|
385
|
-
choices: [["Yes", true], ["No", false]],
|
|
386
|
-
initial_choice_idx: idx,
|
|
387
|
-
quit_value: false,
|
|
388
|
-
)
|
|
225
|
+
def interrupt_pending?
|
|
226
|
+
event_source.interrupt_pending?
|
|
389
227
|
end
|
|
390
228
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
def choose_multi(prompt, choices:, quit_value: nil)
|
|
394
|
-
items = normalize_choices(choices)
|
|
395
|
-
raise ArgumentError, "choices must not be empty" if items.empty?
|
|
396
|
-
|
|
397
|
-
labels = items.map(&:first)
|
|
398
|
-
popup = Fatty::PopUpSession.new(
|
|
399
|
-
source: labels,
|
|
400
|
-
kind: :terminal_choose_multi,
|
|
401
|
-
title: "Choose Many",
|
|
402
|
-
message: prompt,
|
|
403
|
-
prompt: "> ",
|
|
404
|
-
selection: :top,
|
|
405
|
-
selection_mode: :multiple,
|
|
406
|
-
validate_unique_labels: true,
|
|
407
|
-
)
|
|
408
|
-
|
|
409
|
-
done = false
|
|
410
|
-
result = nil
|
|
411
|
-
|
|
412
|
-
label_to_value = items.to_h
|
|
413
|
-
acc_proc = ->(payload) do
|
|
414
|
-
selected = payload[:items] || {}
|
|
415
|
-
|
|
416
|
-
result =
|
|
417
|
-
selected.each_with_object({}) do |(label, _), h|
|
|
418
|
-
h[label] = label_to_value.fetch(label, quit_value)
|
|
419
|
-
end
|
|
420
|
-
|
|
421
|
-
done = true
|
|
422
|
-
end
|
|
423
|
-
|
|
424
|
-
cancel_proc = -> do
|
|
425
|
-
result = quit_value
|
|
426
|
-
done = true
|
|
427
|
-
end
|
|
428
|
-
|
|
429
|
-
owner = PopupOwner.new(on_result: acc_proc, on_cancel: cancel_proc)
|
|
430
|
-
|
|
431
|
-
begin
|
|
432
|
-
push_modal(popup, owner: owner)
|
|
433
|
-
render_frame
|
|
434
|
-
|
|
435
|
-
while !done && @running
|
|
436
|
-
dirty = false
|
|
437
|
-
msg = event_source.next_event
|
|
438
|
-
if msg
|
|
439
|
-
dispatch_message(msg)
|
|
440
|
-
dirty = true
|
|
441
|
-
end
|
|
442
|
-
|
|
443
|
-
s = active_session
|
|
444
|
-
begin
|
|
445
|
-
tick_dirty = !!s&.tick
|
|
446
|
-
dirty ||= tick_dirty
|
|
447
|
-
rescue StandardError => e
|
|
448
|
-
Fatty.error("Terminal#choose_multi tick failed: #{e.class}: #{e.message}", tag: :terminal)
|
|
449
|
-
dirty = true
|
|
450
|
-
end
|
|
451
|
-
|
|
452
|
-
render_frame if dirty
|
|
453
|
-
end
|
|
454
|
-
ensure
|
|
455
|
-
render_frame
|
|
456
|
-
end
|
|
457
|
-
|
|
458
|
-
result
|
|
229
|
+
def modal_active?
|
|
230
|
+
@modal_stack.any?
|
|
459
231
|
end
|
|
460
232
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
def prompt(prompt, initial: "", quit_value: nil, history_key: nil)
|
|
465
|
-
history_ctx = { prompt: (history_key || prompt).to_s }
|
|
466
|
-
|
|
467
|
-
popup = Fatty::PromptSession.new(
|
|
468
|
-
title: "Prompt",
|
|
469
|
-
message: prompt,
|
|
470
|
-
prompt: "> ",
|
|
471
|
-
initial: initial,
|
|
472
|
-
kind: :terminal_prompt,
|
|
473
|
-
history_ctx: history_ctx,
|
|
474
|
-
)
|
|
475
|
-
done = false
|
|
476
|
-
result = nil
|
|
233
|
+
def running?
|
|
234
|
+
@running
|
|
235
|
+
end
|
|
477
236
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
end
|
|
237
|
+
def tick_active_session
|
|
238
|
+
active_session&.tick
|
|
239
|
+
end
|
|
482
240
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
241
|
+
def render_frame
|
|
242
|
+
renderer.begin_frame
|
|
243
|
+
focused_session&.view
|
|
244
|
+
status_session.view
|
|
245
|
+
alert_session.view
|
|
246
|
+
if (top = @modal_stack.last)
|
|
247
|
+
top[:session].view
|
|
486
248
|
end
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
push_modal(popup, owner: owner)
|
|
492
|
-
render_frame
|
|
493
|
-
|
|
494
|
-
while !done && @running
|
|
495
|
-
dirty = false
|
|
496
|
-
msg = event_source.next_event
|
|
497
|
-
if msg
|
|
498
|
-
dispatch_message(msg)
|
|
499
|
-
dirty = true
|
|
500
|
-
end
|
|
501
|
-
|
|
502
|
-
s = active_session
|
|
503
|
-
begin
|
|
504
|
-
tick_dirty = !!s&.tick
|
|
505
|
-
dirty ||= tick_dirty
|
|
506
|
-
rescue StandardError => e
|
|
507
|
-
Fatty.error("Terminal#prompt tick failed: #{e.class}: #{e.message}", tag: :terminal)
|
|
508
|
-
dirty = true
|
|
509
|
-
end
|
|
510
|
-
|
|
511
|
-
render_frame if dirty
|
|
512
|
-
end
|
|
513
|
-
ensure
|
|
514
|
-
render_frame
|
|
249
|
+
if @restore_cursor_after_render
|
|
250
|
+
restore_active_cursor
|
|
251
|
+
else
|
|
252
|
+
renderer.hide_cursor
|
|
515
253
|
end
|
|
516
|
-
|
|
517
|
-
|
|
254
|
+
renderer.finish_frame
|
|
255
|
+
@pending_scroll_render = false
|
|
256
|
+
@render_requested = false
|
|
257
|
+
@session_dirty = false
|
|
258
|
+
@last_render_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
518
259
|
end
|
|
519
260
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
total: total,
|
|
527
|
-
style: style,
|
|
528
|
-
role: role,
|
|
529
|
-
width: width,
|
|
530
|
-
)
|
|
261
|
+
def without_cursor_restore
|
|
262
|
+
old_restore_cursor = @restore_cursor_after_render
|
|
263
|
+
@restore_cursor_after_render = false
|
|
264
|
+
yield
|
|
265
|
+
ensure
|
|
266
|
+
@restore_cursor_after_render = old_restore_cursor
|
|
531
267
|
end
|
|
532
268
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
# [["Label", proc { ... }], ...]
|
|
537
|
-
# or:
|
|
538
|
-
# { "Label" => proc { ... } }
|
|
539
|
-
#
|
|
540
|
-
# The proc may accept:
|
|
541
|
-
# 0 args
|
|
542
|
-
# terminal:
|
|
543
|
-
# terminal:, label:
|
|
544
|
-
# terminal:, label:, payload:
|
|
545
|
-
def menu(prompt, choices:, initial_choice_idx: 0, quit_value: nil)
|
|
546
|
-
items = normalize_choices(choices)
|
|
547
|
-
raise ArgumentError, "choices must not be empty" if items.empty?
|
|
548
|
-
|
|
549
|
-
labels = items.map(&:first)
|
|
550
|
-
popup = Fatty::PopUpSession.new(
|
|
551
|
-
source: labels,
|
|
552
|
-
kind: :terminal_menu,
|
|
553
|
-
title: "Menu",
|
|
554
|
-
message: prompt,
|
|
555
|
-
prompt: "> ",
|
|
556
|
-
selection: :top,
|
|
557
|
-
validate_unique_labels: true,
|
|
558
|
-
)
|
|
559
|
-
|
|
560
|
-
popup.instance_variable_set(:@selected, initial_choice_idx.to_i.clamp(0, labels.length - 1))
|
|
561
|
-
|
|
562
|
-
done = false
|
|
563
|
-
result = nil
|
|
564
|
-
menu_session = active_session
|
|
565
|
-
acc_proc = ->(payload) do
|
|
566
|
-
label = payload[:item]
|
|
567
|
-
idx = labels.index(label)
|
|
568
|
-
action = idx ? items[idx][1] : nil
|
|
569
|
-
result = call_menu_action(
|
|
570
|
-
action,
|
|
571
|
-
session: menu_session,
|
|
572
|
-
label: label,
|
|
573
|
-
payload: payload,
|
|
574
|
-
)
|
|
575
|
-
done = true
|
|
576
|
-
end
|
|
577
|
-
cancel_proc = -> do
|
|
578
|
-
result = quit_value
|
|
579
|
-
done = true
|
|
580
|
-
end
|
|
581
|
-
|
|
582
|
-
owner = PopupOwner.new(on_result: acc_proc, on_cancel: cancel_proc)
|
|
583
|
-
begin
|
|
584
|
-
push_modal(popup, owner: owner)
|
|
585
|
-
render_frame
|
|
269
|
+
def history
|
|
270
|
+
shell_session&.history || Fatty::History.for_path(@history_path)
|
|
271
|
+
end
|
|
586
272
|
|
|
587
|
-
|
|
588
|
-
dirty = false
|
|
589
|
-
msg = event_source.next_event
|
|
273
|
+
private
|
|
590
274
|
|
|
591
|
-
|
|
592
|
-
dispatch_message(msg)
|
|
593
|
-
dirty = true
|
|
594
|
-
end
|
|
275
|
+
# simplecov:disable
|
|
595
276
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
277
|
+
# * Session management
|
|
278
|
+
#
|
|
279
|
+
# Terminal coordinates a small set of Session objects. A session is a UI
|
|
280
|
+
# component with three standard phases:
|
|
281
|
+
#
|
|
282
|
+
# - init :: receive the terminal and initialize session state;
|
|
283
|
+
# - update :: receive events or commands, mutate session state accordingly,
|
|
284
|
+
# and, optionally, return commands requesting that Terminal perform
|
|
285
|
+
# additional actions or route commands to other sessions;
|
|
286
|
+
# - view :: render the session's current state.
|
|
287
|
+
#
|
|
288
|
+
# Terminal is not itself a Session subclass, but it acts as the coordinator
|
|
289
|
+
# for all sessions. It routes input events to the active session, routes
|
|
290
|
+
# explicit commands either to itself or to a named session, and renders the
|
|
291
|
+
# sessions in a fixed order.
|
|
292
|
+
#
|
|
293
|
+
# Terminal keeps sessions in two groups:
|
|
294
|
+
#
|
|
295
|
+
# - permanent sessions :: a collection of always-present sessions. One
|
|
296
|
+
# permanent session is designated the focused session and receives
|
|
297
|
+
# keyboard input whenever no modal session is active. In the standard
|
|
298
|
+
# configuration this is a ShellSession. Other permanent sessions, such
|
|
299
|
+
# as StatusSession and AlertSession, are output-only sessions that do
|
|
300
|
+
# not receive input focus.
|
|
301
|
+
# - modal sessions :: temporary sessions displayed above the permanent
|
|
302
|
+
# sessions, usually as popups. While a modal session is present, it
|
|
303
|
+
# becomes the active session and receives input first. The modal stack
|
|
304
|
+
# also records an owner so that modal results can be sent back to the
|
|
305
|
+
# session or helper that opened the modal.
|
|
306
|
+
#
|
|
307
|
+
# Rendering follows the same layering: permanent sessions are rendered
|
|
308
|
+
# first, followed by the top modal session.
|
|
309
|
+
#
|
|
310
|
+
def install_session(session, focus: false)
|
|
311
|
+
@sessions << session
|
|
312
|
+
register(session)
|
|
313
|
+
@focused_session = session if focus
|
|
604
314
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
render_frame
|
|
609
|
-
end
|
|
610
|
-
result
|
|
315
|
+
commands = session.init(terminal: self)
|
|
316
|
+
apply_commands(commands)
|
|
317
|
+
session
|
|
611
318
|
end
|
|
612
319
|
|
|
613
|
-
def
|
|
614
|
-
|
|
615
|
-
env = MenuEnv.new(
|
|
616
|
-
terminal: self,
|
|
617
|
-
session: session,
|
|
618
|
-
label: label,
|
|
619
|
-
payload: payload,
|
|
620
|
-
)
|
|
621
|
-
if action.arity.zero?
|
|
622
|
-
action.call
|
|
623
|
-
else
|
|
624
|
-
action.call(env)
|
|
625
|
-
end
|
|
626
|
-
else
|
|
627
|
-
action
|
|
628
|
-
end
|
|
320
|
+
def register(session)
|
|
321
|
+
@sessions_by_id[session.id] = session
|
|
629
322
|
end
|
|
630
323
|
|
|
631
|
-
def
|
|
632
|
-
|
|
324
|
+
def deregister(session)
|
|
325
|
+
@sessions_by_id.delete(session.id)
|
|
633
326
|
end
|
|
634
327
|
|
|
635
|
-
def
|
|
636
|
-
@
|
|
328
|
+
def install_default_sessions!
|
|
329
|
+
@alert_session = Fatty::AlertSession.new(id: :alert)
|
|
330
|
+
install_session(@alert_session)
|
|
331
|
+
@status_session = Fatty::StatusSession.new(id: :status)
|
|
332
|
+
install_session(@status_session)
|
|
333
|
+
@shell_session = ShellSession.new(
|
|
334
|
+
id: :shell,
|
|
335
|
+
prompt: @prompt,
|
|
336
|
+
on_accept: @on_accept,
|
|
337
|
+
completion_proc: @completion_proc,
|
|
338
|
+
history_path: @history_path,
|
|
339
|
+
history_ctx: @history_ctx,
|
|
340
|
+
)
|
|
341
|
+
install_session(@shell_session, focus: true)
|
|
637
342
|
end
|
|
638
343
|
|
|
639
|
-
def
|
|
640
|
-
|
|
344
|
+
def push_modal(session, owner: focused_session)
|
|
345
|
+
commands = session.init(terminal: self)
|
|
346
|
+
register(session)
|
|
347
|
+
@modal_stack << { session: session, owner: owner }
|
|
641
348
|
|
|
642
|
-
|
|
643
|
-
|
|
349
|
+
msg = "Terminal#push_modal: size=#{@modal_stack.length} session=#{session.class} object_id=#{session.object_id}"
|
|
350
|
+
Fatty.debug(msg, tag: :session)
|
|
644
351
|
|
|
645
|
-
|
|
646
|
-
|
|
352
|
+
@renderer&.invalidate!
|
|
353
|
+
apply_commands(commands)
|
|
647
354
|
end
|
|
648
355
|
|
|
649
|
-
def
|
|
650
|
-
|
|
356
|
+
def pop_modal
|
|
357
|
+
top = @modal_stack.pop
|
|
358
|
+
msg = "Terminal#pop_modal: size=#{@modal_stack.length} popped=#{top && top[:session].class}"
|
|
359
|
+
Fatty.debug(msg, tag: :session)
|
|
651
360
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
.
|
|
655
|
-
|
|
361
|
+
session = top && top[:session]
|
|
362
|
+
if session
|
|
363
|
+
session.close
|
|
364
|
+
deregister(session)
|
|
365
|
+
end
|
|
366
|
+
@renderer&.invalidate!
|
|
367
|
+
nil
|
|
656
368
|
end
|
|
657
369
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
def
|
|
661
|
-
|
|
662
|
-
|
|
370
|
+
# If we have modal sessions, the most recent one is active; otherwise, the
|
|
371
|
+
# focused_session, i.e., the ShellSession.
|
|
372
|
+
def active_session
|
|
373
|
+
top = @modal_stack.last
|
|
374
|
+
top ? top[:session] : focused_session
|
|
375
|
+
end
|
|
663
376
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
377
|
+
# Return the owner of the top modal session without modifying the stack.
|
|
378
|
+
def modal_owner
|
|
379
|
+
top = @modal_stack.last
|
|
380
|
+
top && top[:owner]
|
|
667
381
|
end
|
|
668
382
|
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
rescue FatConfig::ParseError => ex
|
|
683
|
-
msg = "Terminal#preflight!: configuration error: #{ex.class}: #{ex.message}"
|
|
684
|
-
warn msg
|
|
685
|
-
begin
|
|
686
|
-
Fatty.error(msg, tag: :config)
|
|
687
|
-
rescue StandardError
|
|
688
|
-
nil
|
|
383
|
+
# Return the Session object associated with `id`, which will be a symbol
|
|
384
|
+
# like :status or :alert. Special id's :active and :focused resolve to
|
|
385
|
+
# the focused session (the one currently taking in key events) and the
|
|
386
|
+
# active session, the top modal session or, if there is none, the focused
|
|
387
|
+
# session.
|
|
388
|
+
def find_session(id)
|
|
389
|
+
case id
|
|
390
|
+
when :active
|
|
391
|
+
active_session
|
|
392
|
+
when :focused
|
|
393
|
+
focused_session
|
|
394
|
+
else
|
|
395
|
+
@sessions_by_id[id]
|
|
689
396
|
end
|
|
690
|
-
exit(1)
|
|
691
397
|
end
|
|
692
398
|
|
|
693
|
-
|
|
694
|
-
@ctx = Fatty::Curses::Context.new
|
|
695
|
-
@ctx.start
|
|
399
|
+
# --- Rendering ---------------------------------------------------------
|
|
696
400
|
|
|
697
|
-
|
|
698
|
-
@
|
|
401
|
+
def render_due?
|
|
402
|
+
return true if @render_requested
|
|
699
403
|
|
|
700
|
-
|
|
701
|
-
if @
|
|
702
|
-
|
|
404
|
+
result =
|
|
405
|
+
if @session_dirty
|
|
406
|
+
immediate_render_due?
|
|
407
|
+
elsif @pending_scroll_render
|
|
408
|
+
scroll_render_due?
|
|
703
409
|
else
|
|
704
|
-
|
|
410
|
+
false
|
|
705
411
|
end
|
|
706
|
-
@
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
@event_source =
|
|
711
|
-
Fatty::Curses::EventSource.new(context: @ctx, key_decoder: key_decoder, poll_ms: 50)
|
|
712
|
-
self
|
|
412
|
+
if @session_dirty && !result
|
|
413
|
+
@pending_scroll_render = true
|
|
414
|
+
end
|
|
415
|
+
result
|
|
713
416
|
end
|
|
714
417
|
|
|
715
|
-
def
|
|
716
|
-
@
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
$stdout.write("\e[0 q") # DECSCUSR: restore terminal default cursor
|
|
721
|
-
$stdout.flush
|
|
722
|
-
rescue StandardError
|
|
723
|
-
# best-effort cleanup
|
|
724
|
-
end
|
|
418
|
+
def immediate_render_due?
|
|
419
|
+
@immediate_render ||
|
|
420
|
+
renderer.context.truecolor ||
|
|
421
|
+
!scrolling_output? ||
|
|
422
|
+
scroll_render_due?
|
|
725
423
|
end
|
|
726
424
|
|
|
727
|
-
def
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
prompt: @prompt,
|
|
731
|
-
on_accept: @on_accept,
|
|
732
|
-
completion_proc: @completion_proc,
|
|
733
|
-
history_path: @history_path,
|
|
734
|
-
history_ctx: @history_ctx,
|
|
735
|
-
))
|
|
425
|
+
def scroll_render_due?
|
|
426
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
427
|
+
now - @last_render_time >= SCROLL_RENDER_THROTTLE
|
|
736
428
|
end
|
|
737
429
|
|
|
738
430
|
def scrolling_output?
|
|
@@ -749,24 +441,23 @@ module Fatty
|
|
|
749
441
|
screen.resize(
|
|
750
442
|
rows: @ctx.rows,
|
|
751
443
|
cols: @ctx.cols,
|
|
752
|
-
status_rows: status_rows,
|
|
444
|
+
status_rows: @status_rows,
|
|
753
445
|
)
|
|
754
446
|
@ctx.apply_layout(screen)
|
|
755
447
|
renderer.screen = screen
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
end
|
|
448
|
+
apply_command(Command.session(:status, :resize))
|
|
449
|
+
apply_command(Command.session(:focused, :resize))
|
|
759
450
|
if (top = @modal_stack.last)
|
|
760
|
-
session
|
|
761
|
-
apply_commands(session.handle_resize) if session.respond_to?(:handle_resize)
|
|
451
|
+
apply_command(Command.session(top[:session].id, :resize))
|
|
762
452
|
end
|
|
763
453
|
renderer.sync_backgrounds! if renderer.context.truecolor
|
|
764
454
|
renderer.invalidate!
|
|
765
455
|
end
|
|
766
456
|
|
|
767
|
-
def
|
|
768
|
-
kind, event = message
|
|
769
|
-
kind == :key && event.respond_to?(:key) && event.key == :resize
|
|
457
|
+
def resize_command?(command)
|
|
458
|
+
# kind, event = message
|
|
459
|
+
# kind == :key && event.respond_to?(:key) && event.key == :resize
|
|
460
|
+
command.action == :resize
|
|
770
461
|
end
|
|
771
462
|
|
|
772
463
|
def handle_resize
|
|
@@ -777,25 +468,28 @@ module Fatty
|
|
|
777
468
|
return [] if size == @last_handled_resize_size
|
|
778
469
|
|
|
779
470
|
@last_handled_resize_size = size
|
|
780
|
-
|
|
781
471
|
did_resize_term = false
|
|
782
|
-
|
|
783
472
|
# ncurses must be told to finalize internal resize state before we
|
|
784
473
|
# draw ANSI output. Without this, ANSI writes after resize may be
|
|
785
474
|
# ignored or clipped. Guarded to avoid recursive resize storms.
|
|
786
475
|
unless @inside_resize_term
|
|
787
476
|
@inside_resize_term = true
|
|
788
477
|
did_resize_term = true
|
|
789
|
-
|
|
790
478
|
# Use ncurses' high-level resize finalizer. This updates stdscr/curscr
|
|
791
479
|
# and ncurses bookkeeping before we rebuild Fatty's own layout and draw
|
|
792
480
|
# the ANSI overlay. resize_term is lower-level and is not equivalent here.
|
|
793
481
|
::Curses.resizeterm(rows, cols)
|
|
794
482
|
end
|
|
795
483
|
|
|
796
|
-
renderer.clear_physical_screen! if renderer.context.truecolor
|
|
484
|
+
# renderer.clear_physical_screen! if renderer.context.truecolor
|
|
485
|
+
|
|
486
|
+
screen.resize(rows: rows, cols: cols, status_rows: @status_rows)
|
|
487
|
+
Fatty.info(
|
|
488
|
+
"handle_resize after=#{screen.rows}x#{screen.cols} " \
|
|
489
|
+
"output_after=#{screen.output_rect.inspect}",
|
|
490
|
+
tag: :resize,
|
|
491
|
+
)
|
|
797
492
|
|
|
798
|
-
screen.resize(rows: rows, cols: cols, status_rows: status_rows)
|
|
799
493
|
renderer.context.apply_layout(screen)
|
|
800
494
|
renderer.screen = screen
|
|
801
495
|
renderer.sync_backgrounds! if renderer.context.truecolor
|
|
@@ -810,11 +504,102 @@ module Fatty
|
|
|
810
504
|
cmds = session.handle_resize
|
|
811
505
|
apply_commands(cmds)
|
|
812
506
|
end
|
|
507
|
+
@render_requested = true
|
|
813
508
|
[]
|
|
814
509
|
ensure
|
|
815
510
|
@inside_resize_term = false if did_resize_term
|
|
816
511
|
end
|
|
817
512
|
|
|
513
|
+
# --- Initialization ---------------------------------------------------------
|
|
514
|
+
|
|
515
|
+
#
|
|
516
|
+
# These methods get Fatty started by reading the config, preparing
|
|
517
|
+
# Curses, the Logger, installing key definitions and mappings, and
|
|
518
|
+
# installing themes.
|
|
519
|
+
#
|
|
520
|
+
def preflight!
|
|
521
|
+
Fatty::Config.configure_app(
|
|
522
|
+
app_name: @app_name,
|
|
523
|
+
app_config_dir: @app_config_dir,
|
|
524
|
+
)
|
|
525
|
+
Fatty::Config.install_defaults!
|
|
526
|
+
Fatty::Config.config
|
|
527
|
+
Fatty::Logger.configure
|
|
528
|
+
if Fatty::Logger.logger
|
|
529
|
+
Fatty.info("Fatty #{Fatty::VERSION} loaded from #{__dir__}", tag: :config)
|
|
530
|
+
Fatty.info("Logger configured to log to #{Logger.path}", tag: :config)
|
|
531
|
+
Fatty.info("Read config from #{Config.user_config_path}", tag: :config)
|
|
532
|
+
Fatty.info("Config", config: Config.config, tag: :config)
|
|
533
|
+
end
|
|
534
|
+
Fatty::Config.keydefs
|
|
535
|
+
Fatty::Config.keybindings
|
|
536
|
+
Fatty::Themes::Manager.load!
|
|
537
|
+
Thread.report_on_exception = true
|
|
538
|
+
rescue FatConfig::ParseError => ex
|
|
539
|
+
msg = "Terminal#preflight!: configuration error: #{ex.class}: #{ex.message}"
|
|
540
|
+
warn msg
|
|
541
|
+
begin
|
|
542
|
+
Fatty.error(msg, tag: :config)
|
|
543
|
+
rescue StandardError
|
|
544
|
+
nil
|
|
545
|
+
end
|
|
546
|
+
exit(1)
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
def start_curses!
|
|
550
|
+
@ctx = Fatty::Curses::Context.new
|
|
551
|
+
@ctx.start
|
|
552
|
+
|
|
553
|
+
@screen = Fatty::Screen.new(rows: ::Curses.lines, cols: ::Curses.cols, status_rows: 0)
|
|
554
|
+
@ctx.apply_layout(@screen)
|
|
555
|
+
|
|
556
|
+
@renderer =
|
|
557
|
+
if @ctx.truecolor_enabled?
|
|
558
|
+
Fatty::Renderer::Truecolor.new(context: @ctx, screen: @screen, palette: @ctx.palette)
|
|
559
|
+
else
|
|
560
|
+
Fatty::Renderer::Curses.new(context: @ctx, screen: @screen, palette: @ctx.palette)
|
|
561
|
+
end
|
|
562
|
+
@renderer.invalidate!
|
|
563
|
+
@renderer.clear_physical_screen! if @ctx.truecolor_enabled?
|
|
564
|
+
@renderer.sync_backgrounds! if @ctx.truecolor_enabled?
|
|
565
|
+
|
|
566
|
+
@env ||= Fatty::Env.detect
|
|
567
|
+
key_decoder = Fatty::Curses::KeyDecoder.new(env: @env)
|
|
568
|
+
@event_source =
|
|
569
|
+
Fatty::Curses::EventSource.new(context: @ctx, key_decoder: key_decoder, poll_ms: 50)
|
|
570
|
+
self
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
# --- Shutdown ---------------------------------------------------------
|
|
574
|
+
|
|
575
|
+
def cleanup_after_go
|
|
576
|
+
begin
|
|
577
|
+
stop_curses!
|
|
578
|
+
rescue => e
|
|
579
|
+
Fatty.error("Terminal#go stop_curses! failed: #{e.class}: #{e.message}", tag: :terminal)
|
|
580
|
+
Fatty.error(e.backtrace.join("\n"), tag: :terminal) if e.backtrace
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
begin
|
|
584
|
+
persist_sessions!
|
|
585
|
+
rescue => e
|
|
586
|
+
Fatty.error("Terminal#go persist_sessions! failed: #{e.class}: #{e.message}", tag: :terminal)
|
|
587
|
+
Fatty.error(e.backtrace.join("\n"), tag: :terminal) if e.backtrace
|
|
588
|
+
end
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
def stop_curses!
|
|
592
|
+
@ctx&.close
|
|
593
|
+
ensure
|
|
594
|
+
begin
|
|
595
|
+
$stdout.write("\e[0m") # SGR reset
|
|
596
|
+
$stdout.write("\e[0 q") # DECSCUSR: restore terminal default cursor
|
|
597
|
+
$stdout.flush
|
|
598
|
+
rescue StandardError
|
|
599
|
+
# best-effort cleanup
|
|
600
|
+
end
|
|
601
|
+
end
|
|
602
|
+
|
|
818
603
|
def persist_sessions!
|
|
819
604
|
sessions = []
|
|
820
605
|
sessions << @focused_session if @focused_session
|
|
@@ -833,235 +618,152 @@ module Fatty
|
|
|
833
618
|
|
|
834
619
|
# --- Dispatch ----------------------------------------------------------
|
|
835
620
|
|
|
836
|
-
def
|
|
837
|
-
|
|
838
|
-
return
|
|
839
|
-
|
|
840
|
-
# Clear transient alerts on the next user keypress.
|
|
841
|
-
if key_event_message?(message) && !resize_message?(message) && find_session(:alert)
|
|
842
|
-
apply_command([:send, :alert, :clear, {}])
|
|
843
|
-
end
|
|
621
|
+
def user_interaction_command?(command)
|
|
622
|
+
return false if command.action == :resize
|
|
623
|
+
return false unless command.target == :active
|
|
624
|
+
return false unless command.action == :key
|
|
844
625
|
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
clear_status
|
|
848
|
-
end
|
|
849
|
-
|
|
850
|
-
Fatty.debug("Terminal#dispatch_message: #{message.inspect}", tag: :session)
|
|
851
|
-
commands = s.update(message)
|
|
852
|
-
Fatty.debug("Terminal#dispatch_message: session=#{s.class} -> cmds=#{commands.inspect}", tag: :session)
|
|
853
|
-
|
|
854
|
-
apply_commands(commands)
|
|
626
|
+
event = command.payload[:event]
|
|
627
|
+
event && event.key != :resize
|
|
855
628
|
end
|
|
856
629
|
|
|
857
|
-
# Return whether
|
|
858
|
-
def
|
|
859
|
-
|
|
630
|
+
# Return whether command is a key event
|
|
631
|
+
def key_event_command?(command)
|
|
632
|
+
command.action == :key
|
|
860
633
|
end
|
|
861
634
|
|
|
862
635
|
def apply_commands(commands)
|
|
636
|
+
commands = [commands] if commands.is_a?(Fatty::Command)
|
|
863
637
|
Array(commands).each do |cmd|
|
|
864
638
|
apply_command(cmd)
|
|
865
639
|
end
|
|
866
640
|
end
|
|
867
641
|
|
|
868
|
-
#
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
def apply_command(cmd)
|
|
872
|
-
Fatty.debug("Terminal#apply_command(#{cmd})", tag: :session)
|
|
873
|
-
return if cmd.nil?
|
|
874
|
-
|
|
875
|
-
unless cmd.is_a?(Array) && cmd.first.is_a?(Symbol)
|
|
876
|
-
raise ArgumentError, "command must be an Array starting with a Symbol, got: #{cmd.inspect}"
|
|
877
|
-
end
|
|
878
|
-
|
|
879
|
-
case cmd[0]
|
|
880
|
-
when :terminal
|
|
881
|
-
apply_terminal_command(cmd)
|
|
882
|
-
when :send
|
|
883
|
-
apply_send_command(cmd)
|
|
884
|
-
else
|
|
885
|
-
raise ArgumentError, "unknown command domain #{cmd[0].inspect} (cmd=#{cmd.inspect})"
|
|
886
|
-
end
|
|
887
|
-
end
|
|
888
|
-
|
|
889
|
-
# Apply a command meant to be applied by this Terminal
|
|
890
|
-
def apply_terminal_command(cmd)
|
|
891
|
-
_, name, *rest = cmd
|
|
892
|
-
case name
|
|
642
|
+
# Apply a command meant to be applied by this Terminal.
|
|
643
|
+
def apply_terminal_command(command)
|
|
644
|
+
case command.action
|
|
893
645
|
when :quit
|
|
894
646
|
persist_sessions!
|
|
895
647
|
quit
|
|
896
|
-
when :
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
648
|
+
when :register_session
|
|
649
|
+
register(command.payload.fetch(:session))
|
|
650
|
+
when :set_status_rows
|
|
651
|
+
@status_rows = command.payload.fetch(:rows)
|
|
652
|
+
when :refresh_layout
|
|
653
|
+
refresh_layout!
|
|
901
654
|
when :push_modal
|
|
902
|
-
session =
|
|
903
|
-
|
|
904
|
-
push_modal(session, owner: focused_session)
|
|
905
|
-
Fatty.debug("Terminal#apply_terminal_command(:push_modal) after size=#{@modal_stack.length}", tag: :session)
|
|
655
|
+
session = command.payload.fetch(:session)
|
|
656
|
+
push_modal(session, owner: command.payload[:owner] || focused_session)
|
|
906
657
|
when :pop_modal
|
|
907
|
-
Fatty.debug("Terminal#apply_terminal_command(:pop_modal) before size=#{@modal_stack.length}", tag: :session)
|
|
908
658
|
pop_modal
|
|
909
|
-
Fatty.debug("Terminal#apply_terminal_command(:pop_modal) aftersize=#{@modal_stack.length}", tag: :session)
|
|
910
659
|
when :send_modal_owner
|
|
911
|
-
|
|
660
|
+
command = command.payload.fetch(:command)
|
|
912
661
|
owner = modal_owner
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
662
|
+
commands = owner ? owner.update(command) : []
|
|
663
|
+
apply_commands(commands)
|
|
664
|
+
when :choose_theme
|
|
665
|
+
Fatty.debug("choose_theme: themes=#{Fatty::Themes::Manager.theme_names.inspect}", tag: :theme)
|
|
666
|
+
choose_theme
|
|
916
667
|
when :cycle_theme
|
|
917
668
|
new_theme = Fatty::Themes::Manager.cycle
|
|
669
|
+
Fatty.debug("cycle_theme: new_theme=#{new_theme.inspect}", tag: :theme)
|
|
918
670
|
renderer.apply_theme!(new_theme)
|
|
919
|
-
apply_command(
|
|
671
|
+
apply_command(
|
|
672
|
+
Command.session(
|
|
673
|
+
:alert,
|
|
674
|
+
:show,
|
|
675
|
+
role: :info,
|
|
676
|
+
text: "Theme: #{new_theme}",
|
|
677
|
+
))
|
|
920
678
|
when :set_theme
|
|
921
|
-
theme =
|
|
679
|
+
theme = command.payload.fetch(:theme)
|
|
922
680
|
Fatty::Themes::Manager.set(theme)
|
|
923
681
|
renderer.apply_theme!(theme)
|
|
924
|
-
apply_command(
|
|
925
|
-
|
|
682
|
+
apply_command(
|
|
683
|
+
Command.session(
|
|
684
|
+
:alert,
|
|
685
|
+
:show,
|
|
686
|
+
role: :info,
|
|
687
|
+
text: "Theme: #{theme}",
|
|
688
|
+
))
|
|
689
|
+
when :resize
|
|
926
690
|
handle_resize
|
|
927
691
|
else
|
|
928
|
-
raise ArgumentError, "unknown terminal command #{
|
|
692
|
+
raise ArgumentError, "unknown terminal command #{command.action.inspect} (cmd=#{command.inspect})"
|
|
929
693
|
end
|
|
930
694
|
end
|
|
931
695
|
|
|
932
|
-
# Forward a command
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
session = find_session(recipient)
|
|
938
|
-
raise ArgumentError, "no session registered with id=#{recipient.inspect}" unless session
|
|
696
|
+
# Forward a command to the target session.
|
|
697
|
+
def apply_session_command(command)
|
|
698
|
+
session = find_session(command.id)
|
|
699
|
+
raise ArgumentError, "no session registered with id=#{command.id.inspect}" unless session
|
|
939
700
|
|
|
940
|
-
payload
|
|
941
|
-
|
|
942
|
-
raise ArgumentError, "send payload must be a Hash, got: #{payload.inspect}"
|
|
701
|
+
unless command.payload.is_a?(Hash)
|
|
702
|
+
raise ArgumentError, "session command payload must be a Hash, got: #{command.payload.inspect}"
|
|
943
703
|
end
|
|
944
704
|
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
commands = session.update(message)
|
|
705
|
+
commands = session.update(command)
|
|
706
|
+
@session_dirty = true
|
|
707
|
+
@immediate_render = true if %i[status alert].include?(session.id)
|
|
950
708
|
apply_commands(commands)
|
|
951
709
|
end
|
|
952
710
|
|
|
953
|
-
def
|
|
954
|
-
|
|
955
|
-
end
|
|
956
|
-
|
|
957
|
-
# --- Choose helpers ---------------------------------------------------------
|
|
958
|
-
|
|
959
|
-
def normalize_choices(choices)
|
|
960
|
-
Array(choices).map do |choice|
|
|
961
|
-
if choice.is_a?(Array) && choice.length == 2
|
|
962
|
-
[choice[0].to_s, choice[1]]
|
|
963
|
-
else
|
|
964
|
-
[choice.to_s, choice]
|
|
965
|
-
end
|
|
966
|
-
end
|
|
711
|
+
def hide_cursor
|
|
712
|
+
renderer.hide_cursor
|
|
967
713
|
end
|
|
968
714
|
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
def render_frame
|
|
972
|
-
renderer.begin_frame
|
|
973
|
-
sessions = @pinned + @stack
|
|
974
|
-
sessions.each do |s|
|
|
975
|
-
s.view(screen: screen, renderer: renderer)
|
|
976
|
-
end
|
|
977
|
-
Fatty::StatusView.new.render(
|
|
978
|
-
screen: screen,
|
|
979
|
-
renderer: renderer,
|
|
980
|
-
terminal: self,
|
|
981
|
-
)
|
|
982
|
-
if (top = @modal_stack.last)
|
|
983
|
-
top[:session].view(screen: screen, renderer: renderer)
|
|
984
|
-
end
|
|
985
|
-
|
|
986
|
-
restore_active_cursor
|
|
987
|
-
renderer.finish_frame
|
|
715
|
+
def show_cursor
|
|
716
|
+
renderer.show_cursor
|
|
988
717
|
end
|
|
989
718
|
|
|
990
719
|
def restore_active_cursor
|
|
991
720
|
if @modal_stack && !@modal_stack.empty?
|
|
992
|
-
session = @modal_stack.last[:session]
|
|
993
|
-
|
|
994
|
-
if session.respond_to?(:pager_active?) && session.pager_active?
|
|
995
|
-
::Curses.curs_set(0)
|
|
996
|
-
return
|
|
997
|
-
end
|
|
998
|
-
|
|
999
|
-
if session.respond_to?(:win) && session.respond_to?(:field) && session.field
|
|
1000
|
-
win = session.win
|
|
1001
|
-
unless win
|
|
1002
|
-
::Curses.curs_set(0)
|
|
1003
|
-
return
|
|
1004
|
-
end
|
|
1005
|
-
|
|
1006
|
-
begin
|
|
1007
|
-
maxy = win.maxy
|
|
1008
|
-
maxx = win.maxx
|
|
1009
|
-
rescue RuntimeError
|
|
1010
|
-
::Curses.curs_set(0)
|
|
1011
|
-
return
|
|
1012
|
-
end
|
|
1013
|
-
|
|
1014
|
-
# If the popup is too small to place a cursor safely, just hide it.
|
|
1015
|
-
if maxy < 3 || maxx < 3
|
|
1016
|
-
::Curses.curs_set(0)
|
|
1017
|
-
return
|
|
1018
|
-
end
|
|
1019
|
-
|
|
1020
|
-
::Curses.curs_set(1)
|
|
1021
|
-
|
|
1022
|
-
cursor_x = session.field.cursor_x
|
|
1023
|
-
cursor_x = 0 if cursor_x.nil?
|
|
1024
|
-
|
|
1025
|
-
if session.is_a?(Fatty::PopUpSession)
|
|
1026
|
-
input_row = maxy - 2
|
|
1027
|
-
cursor_x = cursor_x.clamp(0, [maxx - 3, 0].max)
|
|
1028
|
-
begin
|
|
1029
|
-
win.setpos(input_row, 1 + cursor_x)
|
|
1030
|
-
rescue RuntimeError
|
|
1031
|
-
::Curses.curs_set(0)
|
|
1032
|
-
end
|
|
1033
|
-
elsif session.is_a?(Fatty::PromptSession)
|
|
1034
|
-
message_rows = session.message && !session.message.empty? ? 1 : 0
|
|
1035
|
-
input_row = 1 + message_rows
|
|
1036
|
-
cursor_x = cursor_x.clamp(0, [maxx - 3, 0].max)
|
|
1037
|
-
begin
|
|
1038
|
-
win.setpos(input_row, 1 + cursor_x)
|
|
1039
|
-
rescue RuntimeError
|
|
1040
|
-
::Curses.curs_set(0)
|
|
1041
|
-
end
|
|
1042
|
-
else
|
|
1043
|
-
::Curses.curs_set(0)
|
|
1044
|
-
end
|
|
1045
|
-
|
|
1046
|
-
return
|
|
1047
|
-
end
|
|
1048
|
-
|
|
1049
|
-
::Curses.curs_set(0)
|
|
1050
721
|
return
|
|
1051
722
|
end
|
|
1052
723
|
|
|
1053
724
|
session = active_session
|
|
1054
725
|
return unless session
|
|
1055
726
|
|
|
727
|
+
if session.respond_to?(:cursor_visible?) && !session.cursor_visible?
|
|
728
|
+
renderer.hide_cursor
|
|
729
|
+
return
|
|
730
|
+
end
|
|
731
|
+
|
|
1056
732
|
if session.respond_to?(:pager_active?) && session.pager_active?
|
|
1057
|
-
|
|
733
|
+
renderer.hide_cursor
|
|
1058
734
|
return
|
|
1059
735
|
end
|
|
1060
736
|
|
|
1061
737
|
return unless session.respond_to?(:field) && session.field
|
|
1062
738
|
|
|
1063
|
-
|
|
739
|
+
renderer.show_cursor
|
|
1064
740
|
renderer.restore_cursor(session.field)
|
|
1065
741
|
end
|
|
742
|
+
|
|
743
|
+
def choose_theme
|
|
744
|
+
current = Fatty::Themes::Manager.current
|
|
745
|
+
names = Fatty::Themes::Manager.theme_names
|
|
746
|
+
ordered = [current] + (names - [current])
|
|
747
|
+
|
|
748
|
+
chooser = PopUpSession.new(
|
|
749
|
+
source: ordered,
|
|
750
|
+
kind: :theme_chooser,
|
|
751
|
+
title: "Themes",
|
|
752
|
+
prompt: "Theme: ",
|
|
753
|
+
current: :top,
|
|
754
|
+
)
|
|
755
|
+
owner = PopupOwner.new(
|
|
756
|
+
on_result: ->(payload) do
|
|
757
|
+
Fatty::Command.terminal(:set_theme, theme: payload[:item])
|
|
758
|
+
end,
|
|
759
|
+
on_cancel: -> do
|
|
760
|
+
Fatty::Command.terminal(:set_theme, theme: current)
|
|
761
|
+
end,
|
|
762
|
+
on_change: ->(payload) do
|
|
763
|
+
Fatty::Command.terminal(:set_theme, theme: payload[:item])
|
|
764
|
+
end,
|
|
765
|
+
)
|
|
766
|
+
push_modal(chooser, owner: owner)
|
|
767
|
+
end
|
|
1066
768
|
end
|
|
1067
769
|
end
|