Class: Fatty::ShellSession

Inherits:
OutputSession show all
Defined in:
lib/fatty/session/shell_session.rb

Instance Attribute Summary collapse

Attributes inherited from OutputSession

#output, #pager, #pager_field, #viewport

Attributes inherited from Session

#counter, #keymap, #terminal, #views

Instance Method Summary collapse

Methods inherited from OutputSession

#append_output, #follow_output!, #page_output_from_bottom!, #page_output_from_top!, #pager_active?, #pager_status_prompt, #pager_viewport, #reset_for_command!, #reset_output!, #reset_pager!, #resize_output!

Methods inherited from Session

#add_view, #close, #handle_resize, #inspect, #resolve_action, #update

Methods included from Actionable

included

Constructor Details

#initialize(prompt: "sh> ", on_accept: nil, completion_proc: nil, history_ctx: nil, history_path: :default) ⇒ ShellSession

Returns a new instance of ShellSession.



11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# File 'lib/fatty/session/shell_session.rb', line 11

def initialize(prompt: "sh> ", on_accept: nil, completion_proc: nil, history_ctx: nil, history_path: :default)
  super(
    keymap: Keymaps.emacs,
    views: [
      Fatty::OutputView.new(z: 0),
      Fatty::InputView.new(z: 10),
      Fatty::CursorView.new(z: 100),
    ]
  )
  @history = Fatty::History.for_path(history_path)
  @field = Fatty::InputField.new(
    prompt: prompt,
    history: @history,
    completion_proc: completion_proc,
    history_kind: :command,
    history_ctx: history_ctx,
  )

  @on_accept = on_accept
  @completion_proc = completion_proc
end

Instance Attribute Details

#fieldObject (readonly)

Returns the value of attribute field.



9
10
11
# File 'lib/fatty/session/shell_session.rb', line 9

def field
  @field
end

#historyObject (readonly)

Returns the value of attribute history.



9
10
11
# File 'lib/fatty/session/shell_session.rb', line 9

def history
  @history
end

Instance Method Details

#action_env(event:) ⇒ Object

Actions



115
116
117
118
119
120
121
122
123
124
# File 'lib/fatty/session/shell_session.rb', line 115

def action_env(event:)
  Fatty::ActionEnvironment.new(
    session: self,
    counter: counter,
    event: event,
    buffer: @field.buffer,
    field: @field,
    pager: pager,
  )
end

#apply_completion(candidate, range: nil) ⇒ Object



384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
# File 'lib/fatty/session/shell_session.rb', line 384

def apply_completion(candidate, range: nil)
  candidate = candidate.to_s
  return [] if candidate.empty?

  buffer = @field.buffer
  target = range || buffer.completion_replace_range
  old_text = buffer.text.dup
  old_end = target.end
  append_space =
    !candidate.match?(/\s\z/) &&
    (old_end >= old_text.length || old_text[old_end]&.match?(/\s/))
  inserted = append_space ? "#{candidate} " : candidate
  buffer.replace_range(target, inserted)
  []
end

#apply_completion_prefix(prefix) ⇒ Object



374
375
376
377
378
379
380
381
382
# File 'lib/fatty/session/shell_session.rb', line 374

def apply_completion_prefix(prefix)
  text = prefix.to_s
  commands = []
  unless text.empty?
    buffer = @field.buffer
    buffer.replace_range(completion_prefix_range, text)
  end
  commands
end

#completion_candidatesObject

Completion



331
332
333
334
335
336
337
338
339
340
341
342
343
344
# File 'lib/fatty/session/shell_session.rb', line 331

def completion_candidates
  path_candidates = @field.path_completion_candidates
  return path_candidates if path_candidates.any?

  return [] unless @completion_proc

  prefix = completion_prefix
  Array(@completion_proc.call(@field.buffer))
    .compact
    .map(&:to_s)
    .reject(&:empty?)
    .select { |s| s.start_with?(prefix) }
    .uniq
end

#completion_prefixObject



346
347
348
# File 'lib/fatty/session/shell_session.rb', line 346

def completion_prefix
  @field.buffer.completion_prefix
end

#completion_prefix_rangeObject



350
351
352
353
354
355
356
# File 'lib/fatty/session/shell_session.rb', line 350

def completion_prefix_range
  buffer = @field.buffer
  prefix = completion_prefix
  finish = buffer.cursor
  start = finish - prefix.length
  start...finish
end

#init(terminal:) ⇒ Object

Framework and Session Hooks



37
38
39
40
41
# File 'lib/fatty/session/shell_session.rb', line 37

def init(terminal:)
  super
  resize_output!
  []
end

#keymap_contextsObject



43
44
45
# File 'lib/fatty/session/shell_session.rb', line 43

def keymap_contexts
  pager_active? ? [:paging, :terminal] : [:input, :text, :terminal]
end

#longest_common_prefix(strings) ⇒ Object



358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
# File 'lib/fatty/session/shell_session.rb', line 358

def longest_common_prefix(strings)
  result = ""
  if strings.any?
    result = strings.first.to_s.dup
    strings.drop(1).each do |s|
      other = s.to_s
      i = 0
      max = [result.length, other.length].min
      i += 1 while i < max && result[i] == other[i]
      result = result[0...i]
      break if result.empty?
    end
  end
  result
end

#pager_status_viewport(screen) ⇒ Object



83
84
85
86
87
# File 'lib/fatty/session/shell_session.rb', line 83

def pager_status_viewport(screen)
  vp = pager_viewport.dup
  vp.height = [screen.output_rect.rows - 1, 1].max
  vp
end

#persist!Object

Save any state we want saved on quit, error, etc.



90
91
92
93
94
95
96
97
# File 'lib/fatty/session/shell_session.rb', line 90

def persist!
  return unless @history.respond_to?(:save!)

  Fatty.debug("ShellSession#persist!: saving history", tag: :history)
  @history.save!
rescue => e
  Fatty.error("ShellSession#persist!: failed to save history: #{e.class}: #{e.message}", tag: :history)
end

#submit_lineObject

Perform the on_accept action if defined, but catch a few special commands for quiting and clearing.



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/fatty/session/shell_session.rb', line 133

def submit_line
  line = @field.accept_line.to_s.strip
  return [] if line.empty?

  Fatty.info("ShellSession: accept_line: #{line}")

  case line
  when "exit", "quit"
    [[:terminal, :quit]]
  when "clear"
    reset_output!
    [[:send, :alert, :clear, {}]]
  else
    reset_for_command!
    anchor = output.lines.length
    pager.begin_command!(anchor: anchor)

    commands =
      if @on_accept
        @on_accept.call(line, accept_env)
      else
        run_default_command(line)
      end
    normalize_accept_commands(commands)
  end
rescue Errno::ENOENT
  [[:send, :alert, :show, { level: :error, message: "Command not found (#{line})" }]]
end

#tickObject

Called by Terminal#go on every loop iteration. Returns true if any visible state changed (dirty).



101
102
103
104
105
106
107
108
109
# File 'lib/fatty/session/shell_session.rb', line 101

def tick
  dirty = false
  # Animated autoscroll (e.g. after M-s in paging mode).
  if pager.autoscroll?
    step = [(viewport.height * 3) / 4, 1].max
    dirty ||= pager.autoscroll_step?(max_lines: step)
  end
  dirty
end

#update_key(ev) ⇒ Object



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/fatty/session/shell_session.rb', line 47

def update_key(ev)
  return [] unless ev.is_a?(Fatty::KeyEvent)

  key_str = "key=#{ev} raw=#{ev.raw}"
  Fatty.debug("ShellSession.update_key: #{key_str}", tag: :session)
  case ev.key
  when :resize
    [[:terminal, :handle_resize]]
  when :enter, :return
    # safety: if somehow not bound, still accept
    submit_line
  else
    [alert_cmd(:warn, "Unbound key: #{ev} (edit config in 'keybindings.yml' to bind)", ev: ev)]
  end
end

#view(screen:, renderer:) ⇒ Object



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/fatty/session/shell_session.rb', line 63

def view(screen:, renderer:)
  if pager_active?
    ::Curses.curs_set(0)
    viewport = pager_status_viewport(screen)
    highlights = pager.search_visible_highlights(viewport: viewport)

    renderer.render_output(output, viewport: viewport, highlights: highlights)
    renderer.render_pager_field(
      pager_field,
      row: screen.output_rect.rows - 1,
      role: :pager_status,
    )
  else
    ::Curses.curs_set(1)
    renderer.render_output(output, viewport: pager_viewport, highlights: nil)
    renderer.render_input_field(field)
    renderer.restore_cursor(field)
  end
end