Class: Fatty::Terminal
- Inherits:
-
Object
- Object
- Fatty::Terminal
- Defined in:
- lib/fatty/terminal.rb,
lib/fatty/terminal/popup_owner.rb
Overview
Fatty aims at providing a full-featured, curses-based platform for REPL-style interaction with
- command-line editing (by default using Emacs-like keybindings),
- an output pane for normal output, a status pane for out-of-band messages to the user, and an alert pane for library-related error or information messages,
- history traversal and context-aware history,
- command completions based on input-so-far,
- popup facilities for things such as prompting the user for a string, selecting one or more values from a list, selecting user-defined actions from a menu, and more,
- a variety of progress indicators to keep the user informed about what's happening in a long-running process.
- scrolling and searching long output to the output pane,
- rendering markdown text in the output pane,
- a theming system with several pre-defined color themes and facilities for the user to add their own,
- a way to define keys that curses does not recognize and assign actions to them, including actions the user defines
- rendering ANSI colored text to the output pane and the status pane,
- a way to suspend Fatty and exit to your real terminal and come back to where you left off when you exit that terminal session.
In other words, Fatty does much of the heavy lifting needed for a comfortable, full-featured interactive application.
Fatty does not purport to be a "terminal" program but sits on top of them using curses and truecolor rendering into your normal terminal.
Terminal is the main entry point. A library user normally creates a Terminal, configures the prompt, completion, history, and accept callback, then calls #go.
Initialization options:
- - prompt
either a fixed string or a proc that, when called, returns a string to be used as the command-line prompt;
- - on_accept
a proc that takes the edited line and performs some action on it; the proc will have available to it a variable,
env, that contains an CallbackEnvironment object containing a reference to the ShellSession and Terminal so that it can call actions on them, most commonlyenv.session.append_output(text)to write output to the output pane;- - completion_proc
a proc that returns an Array of strings based on the input-so-far to suggest completions that can be inserted at that point;
- - history_path
a path or sentinel describing input history is loaded from and persisted;
- - history_ctx
a proc that provides a "context" for history so that it can be "favored" by history requests in the same context later;
- - env
an optional Fatty::Env describing the runtime environment; when omitted, Fatty will detect it with
Fatty::Env.detect.
Defined Under Namespace
Classes: PopupOwner
Constant Summary collapse
- SCROLL_RENDER_THROTTLE =
0.05
Instance Attribute Summary collapse
-
#alert_session ⇒ Object
readonly
Returns the value of attribute alert_session.
-
#ctx ⇒ Object
readonly
Returns the value of attribute ctx.
-
#env ⇒ Object
readonly
Returns the value of attribute env.
-
#event_source ⇒ Object
readonly
Returns the value of attribute event_source.
-
#focused_session ⇒ Object
readonly
Returns the value of attribute focused_session.
-
#renderer ⇒ Object
readonly
Returns the value of attribute renderer.
-
#screen ⇒ Object
readonly
Returns the value of attribute screen.
-
#shell_session ⇒ Object
readonly
Returns the value of attribute shell_session.
-
#status_session ⇒ Object
readonly
Returns the value of attribute status_session.
Instance Method Summary collapse
-
#apply_command(command) ⇒ Object
A command is either bound for this Terminal or it's meant to be forwarded to a Session.
-
#go ⇒ Object
--- Runtime ----------------------------------------------------------- Run the terminal.
- #history ⇒ Object
-
#initialize(prompt: "> ", on_accept: nil, app_name: nil, app_config_dir: nil, completion_proc: nil, history_path: :default, history_ctx: nil, env: nil) ⇒ Terminal
constructor
A new instance of Terminal.
- #inspect ⇒ Object
- #interrupt_pending? ⇒ Boolean
- #modal_active? ⇒ Boolean
- #render_frame ⇒ Object
- #running? ⇒ Boolean
-
#suspend ⇒ Object
Suspend fatty and run the block in the normal terminal, then restore fatty's terminal on exit.
- #tick_active_session ⇒ Object
- #to_s ⇒ Object
- #without_cursor_restore ⇒ Object
Constructor Details
#initialize(prompt: "> ", on_accept: nil, app_name: nil, app_config_dir: nil, completion_proc: nil, history_path: :default, history_ctx: nil, env: nil) ⇒ Terminal
Returns a new instance of Terminal.
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
# File 'lib/fatty/terminal.rb', line 69 def initialize(prompt: "> ", on_accept: nil, app_name: nil, app_config_dir: nil, completion_proc: nil, history_path: :default, history_ctx: nil, env: nil) @prompt = Prompt.ensure(prompt) @on_accept = on_accept @app_name = app_name @app_config_dir = app_config_dir @completion_proc = completion_proc @history_path = history_path @history_ctx = history_ctx @env = env @renderer = nil @running = false @sessions = [] @sessions_by_id = {} @modal_stack = [] @status_rows = 0 @ctx = nil @immediate_render = false @session_dirty = false @render_requested = false @last_render_time = nil @restore_cursor_after_render = true @render_count = 0 @deferred_count = 0 end |
Instance Attribute Details
#alert_session ⇒ Object (readonly)
Returns the value of attribute alert_session.
67 68 69 |
# File 'lib/fatty/terminal.rb', line 67 def alert_session @alert_session end |
#ctx ⇒ Object (readonly)
Returns the value of attribute ctx.
66 67 68 |
# File 'lib/fatty/terminal.rb', line 66 def ctx @ctx end |
#env ⇒ Object (readonly)
Returns the value of attribute env.
66 67 68 |
# File 'lib/fatty/terminal.rb', line 66 def env @env end |
#event_source ⇒ Object (readonly)
Returns the value of attribute event_source.
66 67 68 |
# File 'lib/fatty/terminal.rb', line 66 def event_source @event_source end |
#focused_session ⇒ Object (readonly)
Returns the value of attribute focused_session.
67 68 69 |
# File 'lib/fatty/terminal.rb', line 67 def focused_session @focused_session end |
#renderer ⇒ Object (readonly)
Returns the value of attribute renderer.
66 67 68 |
# File 'lib/fatty/terminal.rb', line 66 def renderer @renderer end |
#screen ⇒ Object (readonly)
Returns the value of attribute screen.
66 67 68 |
# File 'lib/fatty/terminal.rb', line 66 def screen @screen end |
#shell_session ⇒ Object (readonly)
Returns the value of attribute shell_session.
67 68 69 |
# File 'lib/fatty/terminal.rb', line 67 def shell_session @shell_session end |
#status_session ⇒ Object (readonly)
Returns the value of attribute status_session.
67 68 69 |
# File 'lib/fatty/terminal.rb', line 67 def status_session @status_session end |
Instance Method Details
#apply_command(command) ⇒ Object
A command is either bound for this Terminal or it's meant to be forwarded to a Session. This method routes the command to its proper destination.
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 |
# File 'lib/fatty/terminal.rb', line 194 def apply_command(command) command = Fatty::Command.coerce(command) return unless command @apply_command_depth ||= 0 outermost = @apply_command_depth.zero? @apply_command_depth += 1 Fatty.debug( "Terminal#apply_command: active_session: #{active_session.id}", tag: :session, ) Fatty.debug("Terminal#apply_command(#{command.inspect})", tag: :session) if user_interaction_command?(command) apply_command(Command.session(:status, :clear)) apply_command(Command.session(:alert, :clear)) end if command.terminal? apply_terminal_command(command) else apply_session_command(command) end ensure if command @apply_command_depth -= 1 render_frame if outermost && render_due? end end |
#go ⇒ Object
--- Runtime ----------------------------------------------------------- Run the terminal.
This method owns Fatty's main lifecycle:
- load configuration and logging;
- start curses and build the screen, renderer, and event source;
- install the default permanent sessions;
- enter the event loop;
- restore the real terminal and persist session state on exit.
The event loop has two sources of work. First, it polls the EventSource for user input, resize events, and other terminal events. Any event is routed to the active session, whose #update method may mutate session state and return commands for Terminal to process. Second, the active session is ticked once per loop so it can animate, poll, or otherwise mark itself dirty without receiving a key event.
Whenever an event or tick changes state, Terminal schedules a render. Most renders happen immediately. The exception is fast scrolling in the normal curses renderer, where repeated output updates may be throttled to avoid excessive redraws. Truecolor rendering and direct user input are rendered immediately.
Fatal errors are logged and re-raised, but cleanup still runs: curses is stopped so the real terminal is restored, and persistent sessions are given a chance to save their state.
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 |
# File 'lib/fatty/terminal.rb', line 138 def go preflight! start_curses! install_default_sessions! Fatty.debug("@sessions: #{@sessions.map(&:id).join('==')}") @running = true @last_render_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) @pending_scroll_render = false render_frame loop_count = 0 while @running loop_count += 1 @session_dirty = false @immediate_render = false if (cmd = event_source.next_event) begin apply_command(cmd) @session_dirty = true @immediate_render = true rescue StandardError => e Fatty.error("Terminal#go apply_command failed: #{e.class}: #{e.}", tag: :terminal) Fatty.error(e.backtrace.join("\n"), tag: :terminal) if e.backtrace @session_dirty = true @immediate_render = true raise end end @session_dirty ||= active_session&.tick render_frame if render_due? end rescue => e Fatty.error("Terminal#go fatal error: #{e.class}: #{e.}", tag: :terminal) Fatty.error(e.backtrace.join("\n"), tag: :terminal) if e.backtrace raise ensure cleanup_after_go end |
#history ⇒ Object
269 270 271 |
# File 'lib/fatty/terminal.rb', line 269 def history shell_session&.history || Fatty::History.for_path(@history_path) end |
#inspect ⇒ Object
103 104 105 |
# File 'lib/fatty/terminal.rb', line 103 def inspect "Terminal: prompt: #{@prompt}; history: #{@history_path}; sessions size: #{@sessions.size} active: #{active_session.id}" end |
#interrupt_pending? ⇒ Boolean
225 226 227 |
# File 'lib/fatty/terminal.rb', line 225 def interrupt_pending? event_source.interrupt_pending? end |
#modal_active? ⇒ Boolean
229 230 231 |
# File 'lib/fatty/terminal.rb', line 229 def modal_active? @modal_stack.any? end |
#render_frame ⇒ Object
241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 |
# File 'lib/fatty/terminal.rb', line 241 def render_frame renderer.begin_frame focused_session&.view status_session.view alert_session.view if (top = @modal_stack.last) top[:session].view end if @restore_cursor_after_render restore_active_cursor else renderer.hide_cursor end renderer.finish_frame @pending_scroll_render = false @render_requested = false @session_dirty = false @last_render_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) end |
#running? ⇒ Boolean
233 234 235 |
# File 'lib/fatty/terminal.rb', line 233 def running? @running end |
#suspend ⇒ Object
Suspend fatty and run the block in the normal terminal, then restore fatty's terminal on exit.
182 183 184 185 186 187 188 189 |
# File 'lib/fatty/terminal.rb', line 182 def suspend stop_curses! yield ensure start_curses! refresh_layout! render_frame end |
#tick_active_session ⇒ Object
237 238 239 |
# File 'lib/fatty/terminal.rb', line 237 def tick_active_session active_session&.tick end |
#to_s ⇒ Object
107 108 109 |
# File 'lib/fatty/terminal.rb', line 107 def to_s inspect end |
#without_cursor_restore ⇒ Object
261 262 263 264 265 266 267 |
# File 'lib/fatty/terminal.rb', line 261 def without_cursor_restore old_restore_cursor = @restore_cursor_after_render @restore_cursor_after_render = false yield ensure @restore_cursor_after_render = old_restore_cursor end |