Class: Fatty::Terminal

Inherits:
Object
  • Object
show all
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 commonly env.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

Instance Method Summary collapse

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_sessionObject (readonly)

Returns the value of attribute alert_session.



67
68
69
# File 'lib/fatty/terminal.rb', line 67

def alert_session
  @alert_session
end

#ctxObject (readonly)

Returns the value of attribute ctx.



66
67
68
# File 'lib/fatty/terminal.rb', line 66

def ctx
  @ctx
end

#envObject (readonly)

Returns the value of attribute env.



66
67
68
# File 'lib/fatty/terminal.rb', line 66

def env
  @env
end

#event_sourceObject (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_sessionObject (readonly)

Returns the value of attribute focused_session.



67
68
69
# File 'lib/fatty/terminal.rb', line 67

def focused_session
  @focused_session
end

#rendererObject (readonly)

Returns the value of attribute renderer.



66
67
68
# File 'lib/fatty/terminal.rb', line 66

def renderer
  @renderer
end

#screenObject (readonly)

Returns the value of attribute screen.



66
67
68
# File 'lib/fatty/terminal.rb', line 66

def screen
  @screen
end

#shell_sessionObject (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_sessionObject (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

#goObject

--- 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.message}", 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.message}", tag: :terminal)
  Fatty.error(e.backtrace.join("\n"), tag: :terminal) if e.backtrace
  raise
ensure
  cleanup_after_go
end

#historyObject



269
270
271
# File 'lib/fatty/terminal.rb', line 269

def history
  shell_session&.history || Fatty::History.for_path(@history_path)
end

#inspectObject



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

Returns:

  • (Boolean)


225
226
227
# File 'lib/fatty/terminal.rb', line 225

def interrupt_pending?
  event_source.interrupt_pending?
end

Returns:

  • (Boolean)


229
230
231
# File 'lib/fatty/terminal.rb', line 229

def modal_active?
  @modal_stack.any?
end

#render_frameObject



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

Returns:

  • (Boolean)


233
234
235
# File 'lib/fatty/terminal.rb', line 233

def running?
  @running
end

#suspendObject

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_sessionObject



237
238
239
# File 'lib/fatty/terminal.rb', line 237

def tick_active_session
  active_session&.tick
end

#to_sObject



107
108
109
# File 'lib/fatty/terminal.rb', line 107

def to_s
  inspect
end

#without_cursor_restoreObject



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