Class: Fatty::PopUpSession

Inherits:
ModalSession show all
Defined in:
lib/fatty/session/popup_session.rb

Constant Summary collapse

MAX_WIDTH =
120
DEFAULT_HEIGHT =
12
MIN_LIST_H =
3
MAX_LIST_H =
20
MARGIN =
2
SELECTED_GUTTER =
'[X] '
UNSELECTED_GUTTER =
'[ ] '

Instance Attribute Summary collapse

Attributes inherited from ModalSession

#win

Attributes inherited from Session

#counter, #keymap, #terminal, #views

Instance Method Summary collapse

Methods inherited from ModalSession

#clamp_height, #clamp_width, #close, #handle_resize, #max_height, #max_width, #rebuild_windows!

Methods inherited from Session

#add_view, #close, #handle_resize, #inspect, #persist!, #resolve_action, #tick, #update

Methods included from Actionable

included

Constructor Details

#initialize(source:, title: nil, message: nil, prompt: "> ", keymap: Keymaps.emacs, matcher: nil, order: :as_given, kind: nil, selection: :preserve, initial_query: nil, selection_mode: :single, validate_unique_labels: false) ⇒ PopUpSession

API:

  • source: Proc that returns the candidate list. May accept (query) or be arity 0.
  • matcher: Proc (item, query) -> truthy. Defaults to substring match.
  • order: :as_given (default) or :reverse (presentation order).
  • selection: :preserve (default), :top, :bottom (how selection behaves after refresh).


23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/fatty/session/popup_session.rb', line 23

def initialize(
  source:,
  title: nil,
  message: nil,
  prompt: "> ",
  keymap: Keymaps.emacs,
  matcher: nil,
  order: :as_given,
  kind: nil,
  selection: :preserve,
  initial_query: nil,
  selection_mode: :single,
  validate_unique_labels: false
)
  super(keymap: keymap)
  @source = source
  @title = title&.to_s
  @message = message&.to_s
  @prompt = Prompt.ensure(prompt)
  @matcher = matcher || method(:default_matcher)
  @order = order.to_sym
  @kind = kind&.to_sym
  @selection = selection.to_sym
  @selection_mode = selection_mode.to_sym
  @validate_unique_labels = !!validate_unique_labels

  @field = InputField.new(prompt: @prompt)
  text = initial_query.to_s
  @field.buffer.replace(text) unless text.empty?

  @items = []
  @filtered = []
  @displayed = []
  @selected = 0
  @selected_labels = {}

  @last_query = nil
  @scroll_start = 0
end

Instance Attribute Details

#displayedObject (readonly)

Returns the value of attribute displayed.



7
8
9
# File 'lib/fatty/session/popup_session.rb', line 7

def displayed
  @displayed
end

#fieldObject (readonly)

Returns the value of attribute field.



7
8
9
# File 'lib/fatty/session/popup_session.rb', line 7

def field
  @field
end

#filteredObject (readonly)

Returns the value of attribute filtered.



7
8
9
# File 'lib/fatty/session/popup_session.rb', line 7

def filtered
  @filtered
end

#messageObject (readonly)

Returns the value of attribute message.



7
8
9
# File 'lib/fatty/session/popup_session.rb', line 7

def message
  @message
end

#selectedObject (readonly)

Returns the value of attribute selected.



7
8
9
# File 'lib/fatty/session/popup_session.rb', line 7

def selected
  @selected
end

#titleObject (readonly)

Returns the value of attribute title.



7
8
9
# File 'lib/fatty/session/popup_session.rb', line 7

def title
  @title
end

Instance Method Details

#accept_selectionObject



197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/fatty/session/popup_session.rb', line 197

def accept_selection
  if multi_select?
    payload = popup_payload(selected_result_hash)
  else
    item = selected_item
    query = @field.buffer.text.to_s

    return [] if item.nil? && query.empty?

    item = query if item.nil?
    payload = popup_payload(item)
  end
  [
    [:terminal, :send_modal_owner, [:cmd, :popup_result, payload]],
    [:terminal, :pop_modal]
  ]
end

#countsObject



325
326
327
328
329
330
331
332
# File 'lib/fatty/session/popup_session.rb', line 325

def counts
  {
    total: total_count,
    selected: selected_count,
    matching: matching_count,
    showing: showing_count
  }
end

#geometry(cols:, rows:) ⇒ Object

Return the outer width and height of the window for this modal, including any padding and borders.



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/fatty/session/popup_session.rb', line 88

def geometry(cols:, rows:)
  max_w = max_width(cols: cols, margin: MARGIN, min_width: 10)
  max_h = max_height(rows: rows, margin: MARGIN, min_height: 5)

  desired_list_h = @filtered.length.clamp(MIN_LIST_H, MAX_LIST_H)
  height = clamp_height(
    desired_list_h + popup_extra_rows,
    max_height: max_h,
    min_height: 6,
  )
  width = clamp_width(
    MAX_WIDTH,
    max_width: max_w,
  )
  [width, height]
end

#gutter_for(item:, selected:) ⇒ Object



223
224
225
226
227
228
229
# File 'lib/fatty/session/popup_session.rb', line 223

def gutter_for(item:, selected:)
  if multi_select?
    selected_item_label?(item) ? SELECTED_GUTTER : UNSELECTED_GUTTER
  else
    ' '
  end
end

#handle_action(action, args, event:) ⇒ Object



172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/fatty/session/popup_session.rb', line 172

def handle_action(action, args, event:)
  env = action_env(event: event)

  if Fatty::Actions.lookup(action)&.fetch(:on) == :session
    Fatty::Actions.call(action, env, *args)
  else
    @field.act_on(action, *args, env: env)
    refresh_items_if_query_changed
    ensure_scroll_visible
    notify_owner(:popup_changed)
  end
rescue ActionError => e
  Fatty.error("PopUpSession#handle_action: ActionError #{e.message}", tag: :session)
  []
end

#init(terminal:) ⇒ Object

Framework and Session Hooks



67
68
69
70
71
# File 'lib/fatty/session/popup_session.rb', line 67

def init(terminal:)
  refresh_items
  rebuild_windows!
  notify_owner(:popup_changed)
end

#keymap_contextsObject



73
74
75
76
77
# File 'lib/fatty/session/popup_session.rb', line 73

def keymap_contexts
  contexts = [:popup, :text]
  contexts.unshift(:popup_multi) if multi_select?
  contexts
end

#matching_countObject



309
310
311
# File 'lib/fatty/session/popup_session.rb', line 309

def matching_count
  @filtered.length
end

#move_selected_by(delta) ⇒ Object



188
189
190
191
192
193
194
195
# File 'lib/fatty/session/popup_session.rb', line 188

def move_selected_by(delta)
  return if @displayed.empty?

  msg = "PopUpSession#move_selected_by before: selected=#{@selected.inspect} delta=#{delta} len=#{@displayed.length}"
  Fatty.debug(msg)
  @selected = ((@selected || 0) + delta) % @displayed.length
  Fatty.debug("PopUpSession#move_selected_by after: selected=#{@selected.inspect}")
end

#refresh_displayed_itemsObject



240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'lib/fatty/session/popup_session.rb', line 240

def refresh_displayed_items
  if multi_select?
    selected_missing =
      @items.select do |item|
        label = item_label(item)
        selected_label?(label) && !@filtered.include?(item)
      end

    @displayed = selected_missing + @filtered
  else
    @displayed = @filtered.dup
  end
end

#refresh_itemsObject



254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
# File 'lib/fatty/session/popup_session.rb', line 254

def refresh_items
  q = @field.buffer.text.to_s
  @items = Array(call_source(q))
  apply_order!
  validate_unique_labels!(@items) if @validate_unique_labels

  matcher = @matcher || method(:default_matcher)

  @filtered =
    if q.empty?
      @items
    else
      @items.select { |e| matcher.call(e, q) }
    end

  refresh_displayed_items
  apply_selection_policy!
end

#refresh_items_if_query_changedObject



231
232
233
234
235
236
237
238
# File 'lib/fatty/session/popup_session.rb', line 231

def refresh_items_if_query_changed
  q = @field.buffer.text.to_s
  return if q == @last_query

  @last_query = q.dup.freeze
  Fatty.debug("popup query changed", tag: :popup, q: q, last: @last_query)
  refresh_items
end

#scroll_start(list_h:) ⇒ Object

Renderer calls this to determine which slice of items to display.



274
275
276
277
278
279
280
281
# File 'lib/fatty/session/popup_session.rb', line 274

def scroll_start(list_h:)
  max_start = @displayed.length - list_h
  max_start = 0 if max_start < 0

  @scroll_start = 0 if @scroll_start < 0
  @scroll_start = max_start if @scroll_start > max_start
  @scroll_start
end

#selected_countObject



313
314
315
316
317
318
319
# File 'lib/fatty/session/popup_session.rb', line 313

def selected_count
  if multi_select?
    @selected_labels.length
  else
    selected_item ? 1 : 0
  end
end

#selected_itemObject



215
216
217
# File 'lib/fatty/session/popup_session.rb', line 215

def selected_item
  @displayed[@selected]
end

#selected_item_label?(item) ⇒ Boolean

Returns:

  • (Boolean)


219
220
221
# File 'lib/fatty/session/popup_session.rb', line 219

def selected_item_label?(item)
  selected_label?(item_label(item))
end

#selected_labelsObject



334
335
336
# File 'lib/fatty/session/popup_session.rb', line 334

def selected_labels
  @selected_labels.keys
end

#showing_countObject



321
322
323
# File 'lib/fatty/session/popup_session.rb', line 321

def showing_count
  [@displayed.length - scroll_start(list_h: popup_list_height), popup_list_height].min
end

#toggle_selected_current!Object



283
284
285
286
287
288
# File 'lib/fatty/session/popup_session.rb', line 283

def toggle_selected_current!
  item = selected_item
  return unless item

  toggle_selected_item!(item)
end

#toggle_selected_item!(item) ⇒ Object



290
291
292
293
294
295
296
297
298
299
300
301
# File 'lib/fatty/session/popup_session.rb', line 290

def toggle_selected_item!(item)
  label = item_label(item)

  if selected_label?(label)
    @selected_labels.delete(label)
  else
    @selected_labels[label] = true
  end

  refresh_displayed_items
  item
end

#total_countObject

Count methods for display to user.



305
306
307
# File 'lib/fatty/session/popup_session.rb', line 305

def total_count
  @items.length
end

#view(screen:, renderer:) ⇒ Object



79
80
81
82
83
84
# File 'lib/fatty/session/popup_session.rb', line 79

def view(screen:, renderer:)
  Fatty.debug("PopupSession#view: object_id=#{object_id} win_nil=#{@win.nil?}", tag: :session)
  return unless @win

  renderer.render_popup(session: self)
end