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.
Files changed (120) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.org +11 -29
  3. data/README.md +2207 -18
  4. data/README.org +2066 -104
  5. data/exe/fatty +236 -93
  6. data/help/help.md +233 -0
  7. data/lib/fatty/action.rb +3 -7
  8. data/lib/fatty/actionable.rb +2 -0
  9. data/lib/fatty/alert.rb +65 -39
  10. data/lib/fatty/ansi/renderer.rb +26 -6
  11. data/lib/fatty/ansi.rb +1 -0
  12. data/lib/fatty/api/alert.rb +15 -0
  13. data/lib/fatty/api/choose.rb +145 -0
  14. data/lib/fatty/api/environment.rb +12 -0
  15. data/lib/fatty/api/keytest.rb +11 -0
  16. data/lib/fatty/api/menu.rb +104 -0
  17. data/lib/fatty/api/output.rb +55 -0
  18. data/lib/fatty/api/progress.rb +24 -0
  19. data/lib/fatty/api/prompt.rb +77 -0
  20. data/lib/fatty/api/status.rb +55 -0
  21. data/lib/fatty/api.rb +38 -0
  22. data/lib/fatty/callback_environment.rb +74 -0
  23. data/lib/fatty/colors/color.rb +366 -232
  24. data/lib/fatty/colors/pairs.rb +2 -0
  25. data/lib/fatty/command.rb +78 -0
  26. data/lib/fatty/config.rb +227 -39
  27. data/lib/fatty/config_files/config.yml +4 -12
  28. data/lib/fatty/config_files/keydefs.yml +13 -23
  29. data/lib/fatty/config_files/themes/capuchin_monk.yml +140 -0
  30. data/lib/fatty/config_files/themes/catppuccin_latte.yml +140 -0
  31. data/lib/fatty/config_files/themes/catppuccin_mocha.yml +140 -0
  32. data/lib/fatty/config_files/themes/cyberpunk.yml +141 -0
  33. data/lib/fatty/config_files/themes/dracula.yml +140 -0
  34. data/lib/fatty/config_files/themes/everforest_dark.yml +140 -0
  35. data/lib/fatty/config_files/themes/gruvbox_dark.yml +140 -0
  36. data/lib/fatty/config_files/themes/gruvbox_light.yml +140 -0
  37. data/lib/fatty/config_files/themes/mono.yml +119 -47
  38. data/lib/fatty/config_files/themes/monokai.yml +140 -0
  39. data/lib/fatty/config_files/themes/nordic.yml +1 -0
  40. data/lib/fatty/config_files/themes/onedark.yml +140 -0
  41. data/lib/fatty/config_files/themes/solarized_dark.yml +1 -0
  42. data/lib/fatty/config_files/themes/solarized_light.yml +140 -0
  43. data/lib/fatty/config_files/themes/terminal.yml +67 -0
  44. data/lib/fatty/config_files/themes/tokyo_night.yml +140 -0
  45. data/lib/fatty/config_files/themes/wordperfect.yml +80 -9
  46. data/lib/fatty/core_ext/hash.rb +19 -0
  47. data/lib/fatty/core_ext/string.rb +19 -12
  48. data/lib/fatty/core_ext.rb +1 -0
  49. data/lib/fatty/curses/context.rb +86 -84
  50. data/lib/fatty/curses/event_source.rb +128 -40
  51. data/lib/fatty/curses/key_decoder.rb +77 -11
  52. data/lib/fatty/env.rb +21 -20
  53. data/lib/fatty/help.rb +1 -29
  54. data/lib/fatty/history/entry.rb +2 -0
  55. data/lib/fatty/history.rb +17 -14
  56. data/lib/fatty/input_buffer.rb +71 -47
  57. data/lib/fatty/input_field.rb +216 -149
  58. data/lib/fatty/key_event.rb +67 -66
  59. data/lib/fatty/key_map.rb +54 -16
  60. data/lib/fatty/keymaps/emacs.rb +40 -6
  61. data/lib/fatty/log_formats/json.rb +2 -0
  62. data/lib/fatty/log_formats/text.rb +2 -0
  63. data/lib/fatty/logger.rb +7 -2
  64. data/lib/fatty/markdown/ansi_renderer.rb +75 -6
  65. data/lib/fatty/markdown/render.rb +2 -3
  66. data/lib/fatty/mouse_event.rb +142 -2
  67. data/lib/fatty/output_buffer.rb +2 -0
  68. data/lib/fatty/pager.rb +158 -47
  69. data/lib/fatty/progress.rb +417 -0
  70. data/lib/fatty/renderer/curses.rb +340 -184
  71. data/lib/fatty/renderer/truecolor.rb +266 -163
  72. data/lib/fatty/renderer.rb +115 -97
  73. data/lib/fatty/screen.rb +2 -0
  74. data/lib/fatty/session/alert_session.rb +39 -21
  75. data/lib/fatty/session/isearch_session.rb +92 -38
  76. data/lib/fatty/session/keytest_session.rb +105 -85
  77. data/lib/fatty/session/modal_session.rb +38 -1
  78. data/lib/fatty/session/output_session.rb +472 -21
  79. data/lib/fatty/session/popup_session.rb +251 -134
  80. data/lib/fatty/session/prompt_session.rb +68 -26
  81. data/lib/fatty/session/search_session.rb +79 -31
  82. data/lib/fatty/session/shell_session.rb +383 -368
  83. data/lib/fatty/session/status_session.rb +167 -0
  84. data/lib/fatty/session.rb +95 -95
  85. data/lib/fatty/terminal/popup_owner.rb +14 -11
  86. data/lib/fatty/terminal.rb +515 -813
  87. data/lib/fatty/themes/loader.rb +4 -2
  88. data/lib/fatty/themes/manager.rb +4 -0
  89. data/lib/fatty/themes/registry.rb +2 -0
  90. data/lib/fatty/themes/resolver.rb +175 -162
  91. data/lib/fatty/version.rb +1 -1
  92. data/lib/fatty/viewport.rb +8 -1
  93. data/lib/fatty.rb +35 -7
  94. metadata +42 -40
  95. data/.envrc +0 -2
  96. data/.simplecov +0 -23
  97. data/.yardopts +0 -4
  98. data/CHANGELOG.md +0 -34
  99. data/Rakefile +0 -15
  100. data/TODO.org +0 -163
  101. data/examples/markdown/native-markdown.md +0 -370
  102. data/examples/markdown/ox-gfm-markdown.md +0 -373
  103. data/examples/markdown/ox-gfm-markdown.org +0 -376
  104. data/fatty.gemspec +0 -42
  105. data/lib/fatty/accept_env.rb +0 -32
  106. data/lib/fatty/config_files/help.md +0 -120
  107. data/lib/fatty/config_files/help.org +0 -124
  108. data/lib/fatty/config_files/themes/wordperfect_light.yml +0 -77
  109. data/lib/fatty/menu_env.rb +0 -22
  110. data/lib/fatty/session/input_session.rb +0 -99
  111. data/lib/fatty/sessions.rb +0 -14
  112. data/lib/fatty/terminal/progress.rb +0 -374
  113. data/lib/fatty/view/alert_view.rb +0 -14
  114. data/lib/fatty/view/cursor_view.rb +0 -18
  115. data/lib/fatty/view/input_view.rb +0 -9
  116. data/lib/fatty/view/output_view.rb +0 -9
  117. data/lib/fatty/view/status_view.rb +0 -14
  118. data/lib/fatty/view.rb +0 -33
  119. data/lib/fatty/views.rb +0 -9
  120. data/sig/fatty.rbs +0 -4
@@ -4,11 +4,11 @@ module Fatty
4
4
  class PopUpSession < ModalSession
5
5
  action_on :session
6
6
 
7
- attr_reader :field, :filtered, :displayed, :selected, :title, :message
7
+ attr_reader :prompt, :items, :field, :filtered, :displayed, :title, :message
8
8
 
9
9
  MAX_WIDTH = 120
10
10
  DEFAULT_HEIGHT = 12
11
- MIN_LIST_H = 3
11
+ MIN_LIST_H = 1
12
12
  MAX_LIST_H = 20
13
13
  MARGIN = 2
14
14
 
@@ -19,21 +19,27 @@ module Fatty
19
19
  # - source: Proc that returns the candidate list. May accept (query) or be arity 0.
20
20
  # - matcher: Proc (item, query) -> truthy. Defaults to substring match.
21
21
  # - order: :as_given (default) or :reverse (presentation order).
22
- # - selection: :preserve (default), :top, :bottom (how selection behaves after refresh).
22
+ # - current: :preserve (default), :top, :bottom (how the current behaves after refresh).
23
23
  def initialize(
24
- source:,
25
- title: nil,
26
- message: nil,
27
- prompt: "> ",
28
- keymap: Keymaps.emacs,
29
- matcher: nil,
30
- order: :as_given,
31
- kind: nil,
32
- selection: :preserve,
33
- initial_query: nil,
34
- selection_mode: :single,
35
- validate_unique_labels: false
36
- )
24
+ source:,
25
+ title: nil,
26
+ message: nil,
27
+ prompt: "Narrow: ",
28
+ keymap: Keymaps.emacs,
29
+ matcher: nil,
30
+ order: :as_given,
31
+ kind: nil,
32
+ current: :preserve,
33
+ initial_query: nil,
34
+ show_counts: true,
35
+ show_filter: true,
36
+ selection_mode: :single,
37
+ history: nil,
38
+ history_kind: :popup_filter,
39
+ history_ctx: nil,
40
+ save_history: true,
41
+ validate_unique_labels: false
42
+ )
37
43
  super(keymap: keymap)
38
44
  @source = source
39
45
  @title = title&.to_s
@@ -42,64 +48,155 @@ module Fatty
42
48
  @matcher = matcher || method(:default_matcher)
43
49
  @order = order.to_sym
44
50
  @kind = kind&.to_sym
45
- @selection = selection.to_sym
51
+ @current_policy = current.to_sym
52
+ @current = nil
46
53
  @selection_mode = selection_mode.to_sym
54
+ @show_counts = !!show_counts
55
+ @history = history
56
+ @save_history = !!save_history
57
+ @field = InputField.new(
58
+ prompt: @prompt,
59
+ history: @history,
60
+ history_kind: history_kind,
61
+ history_ctx: history_ctx,
62
+ )
63
+ @show_filter = !!show_filter
47
64
  @validate_unique_labels = !!validate_unique_labels
48
-
49
- @field = InputField.new(prompt: @prompt)
50
65
  text = initial_query.to_s
51
66
  @field.buffer.replace(text) unless text.empty?
52
67
 
53
68
  @items = []
54
69
  @filtered = []
55
70
  @displayed = []
56
- @selected = 0
57
71
  @selected_labels = {}
58
72
 
59
73
  @last_query = nil
60
74
  @scroll_start = 0
75
+ refresh_items
61
76
  end
62
77
 
63
78
  #########################################################################################
64
- # Framework and Session Hooks
79
+ # Session Protocol
65
80
  #########################################################################################
66
81
 
67
82
  def init(terminal:)
83
+ commands = super
68
84
  refresh_items
69
85
  rebuild_windows!
70
- notify_owner(:popup_changed)
86
+ commands.concat(notify_owner(:popup_changed))
71
87
  end
72
88
 
73
- def keymap_contexts
74
- contexts = [:popup, :text]
75
- contexts.unshift(:popup_multi) if multi_select?
76
- contexts
89
+ def update(command)
90
+ commands =
91
+ case command.action
92
+ when :key
93
+ ev = command.payload.fetch(:event)
94
+ action, args = resolve_action(ev)
95
+
96
+ if action
97
+ normalize_action_result(apply_action(action, args, event: ev))
98
+ else
99
+ []
100
+ end
101
+ when :terminal_paste
102
+ text = command.payload.fetch(:text, "").to_s
103
+ env = action_env(event: nil)
104
+ @field.act_on(:paste, text, env: env)
105
+ refresh_items
106
+ ensure_scroll_visible
107
+ []
108
+ else
109
+ []
110
+ end
111
+ Array(commands)
77
112
  end
78
113
 
79
- def view(screen:, renderer:)
114
+ def view
80
115
  Fatty.debug("PopupSession#view: object_id=#{object_id} win_nil=#{@win.nil?}", tag: :session)
81
116
  return unless @win
82
117
 
83
118
  renderer.render_popup(session: self)
84
119
  end
85
120
 
86
- # Return the outer width and height of the window for this modal,
87
- # including any padding and borders.
88
- def geometry(cols:, rows:)
89
- max_w = max_width(cols: cols, margin: MARGIN, min_width: 10)
90
- max_h = max_height(rows: rows, margin: MARGIN, min_height: 5)
121
+ #########################################################################################
122
+ # Other public API methods
123
+ #########################################################################################
91
124
 
92
- desired_list_h = @filtered.length.clamp(MIN_LIST_H, MAX_LIST_H)
93
- height = clamp_height(
94
- desired_list_h + popup_extra_rows,
95
- max_height: max_h,
96
- min_height: 6,
97
- )
98
- width = clamp_width(
99
- MAX_WIDTH,
100
- max_width: max_w,
101
- )
102
- [width, height]
125
+ def state
126
+ [
127
+ title.to_s.dup.freeze,
128
+ message.to_s.dup.freeze,
129
+ displayed.map { |item| item.to_s.dup.freeze }.freeze,
130
+ current,
131
+ field.buffer.text.to_s.dup.freeze,
132
+ field.buffer.cursor,
133
+ field.buffer.virtual_suffix.to_s.dup.freeze,
134
+ selected_labels.map { |label| label.to_s.dup.freeze }.sort.freeze,
135
+ counts&.dup&.freeze,
136
+ renderer.screen.rows,
137
+ renderer.screen.cols,
138
+ ]
139
+ end
140
+
141
+ def current
142
+ current_index
143
+ end
144
+
145
+ def counts
146
+ {
147
+ total: total_count,
148
+ selected: selected_count,
149
+ matching: matching_count,
150
+ showing: showing_count
151
+ }
152
+ end
153
+
154
+ def counts_present?
155
+ @show_counts
156
+ end
157
+
158
+ def filter_present?
159
+ @show_filter
160
+ end
161
+
162
+ # Renderer calls this to determine which slice of items to display.
163
+ def scroll_start(list_h:)
164
+ max_start = @displayed.length - list_h
165
+ max_start = 0 if max_start < 0
166
+
167
+ @scroll_start = 0 if @scroll_start < 0
168
+ @scroll_start = max_start if @scroll_start > max_start
169
+ @scroll_start
170
+ end
171
+
172
+ def gutter_for(item:, selected:)
173
+ if multi_select?
174
+ selected_item_label?(item) ? SELECTED_GUTTER : UNSELECTED_GUTTER
175
+ else
176
+ ' '
177
+ end
178
+ end
179
+
180
+ # During path completion update the popup's candidates.
181
+ def replace_query(text)
182
+ @field.buffer.replace(text.to_s)
183
+ refresh_items
184
+ ensure_scroll_visible
185
+ notify_owner(:popup_changed)
186
+ end
187
+
188
+ def replace_source(source:, title: nil, message: nil, query: "", current: :top)
189
+ @source = source
190
+ @title = title.to_s if title
191
+ @message = message.to_s if message
192
+ @current_policy = current.to_sym
193
+ @field.buffer.replace(query.to_s)
194
+ @last_query = nil
195
+ @scroll_start = 0
196
+
197
+ refresh_items
198
+ ensure_scroll_visible
199
+ notify_owner(:popup_changed)
103
200
  end
104
201
 
105
202
  ############################################################################################
@@ -107,7 +204,7 @@ module Fatty
107
204
  ############################################################################################
108
205
 
109
206
  action :popup_cancel do
110
- notify_owner(:popup_cancelled) + [[:terminal, :pop_modal]]
207
+ notify_owner(:popup_cancelled) + [Command.terminal(:pop_modal)]
111
208
  end
112
209
 
113
210
  action :popup_accept do
@@ -115,32 +212,32 @@ module Fatty
115
212
  end
116
213
 
117
214
  action :popup_next do
118
- move_selected_by(1)
215
+ move_current_by(1)
119
216
  ensure_scroll_visible
120
217
  notify_owner(:popup_changed)
121
218
  end
122
219
 
123
220
  action :popup_prev do
124
- move_selected_by(-1)
221
+ move_current_by(-1)
125
222
  ensure_scroll_visible
126
223
  notify_owner(:popup_changed)
127
224
  end
128
225
 
129
226
  action :popup_page_down do
130
- move_selected_by(popup_list_height)
227
+ move_current_by(popup_list_height)
131
228
  ensure_scroll_visible
132
229
  notify_owner(:popup_changed)
133
230
  end
134
231
 
135
232
  action :popup_page_up do
136
- move_selected_by(-popup_list_height)
233
+ move_current_by(-popup_list_height)
137
234
  ensure_scroll_visible
138
235
  notify_owner(:popup_changed)
139
236
  end
140
237
 
141
238
  action :popup_top do
142
239
  unless @filtered.empty?
143
- @selected = 0
240
+ @current = 0
144
241
  recenter_scroll
145
242
  end
146
243
  notify_owner(:popup_changed)
@@ -148,7 +245,7 @@ module Fatty
148
245
 
149
246
  action :popup_bottom do
150
247
  unless @filtered.empty?
151
- @selected = [@filtered.length - 1, 0].max
248
+ @current = [@filtered.length - 1, 0].max
152
249
  recenter_scroll
153
250
  end
154
251
  notify_owner(:popup_changed)
@@ -169,7 +266,56 @@ module Fatty
169
266
  end
170
267
  end
171
268
 
172
- def handle_action(action, args, event:)
269
+ action :popup_tab do
270
+ if @kind == :completion
271
+ notify_owner(:popup_tab)
272
+ else
273
+ accept_selection
274
+ end
275
+ end
276
+
277
+ action :popup_backtab do
278
+ if @kind == :completion
279
+ notify_owner(:popup_backtab)
280
+ else
281
+ []
282
+ end
283
+ end
284
+
285
+ private
286
+
287
+ # simplecov:disable
288
+
289
+ def selected_labels
290
+ @selected_labels.keys
291
+ end
292
+
293
+ def keymap_contexts
294
+ contexts = [:popup, :text, :terminal]
295
+ contexts.unshift(:popup_multi) if multi_select?
296
+ contexts
297
+ end
298
+
299
+ # Return the outer width and height of the window for this modal,
300
+ # including any padding and borders.
301
+ def geometry(cols:, rows:)
302
+ max_w = max_width(cols: cols, margin: MARGIN, min_width: 10)
303
+ max_h = max_height(rows: rows, margin: MARGIN, min_height: 5)
304
+
305
+ desired_list_h = @filtered.length.clamp(MIN_LIST_H, MAX_LIST_H)
306
+ height = clamp_height(
307
+ desired_list_h + popup_extra_rows,
308
+ max_height: max_h,
309
+ min_height: 6,
310
+ )
311
+ width = clamp_width(
312
+ MAX_WIDTH,
313
+ max_width: max_w,
314
+ )
315
+ [width, height]
316
+ end
317
+
318
+ def apply_action(action, args, event:)
173
319
  env = action_env(event: event)
174
320
 
175
321
  if Fatty::Actions.lookup(action)&.fetch(:on) == :session
@@ -181,53 +327,52 @@ module Fatty
181
327
  notify_owner(:popup_changed)
182
328
  end
183
329
  rescue ActionError => e
184
- Fatty.error("PopUpSession#handle_action: ActionError #{e.message}", tag: :session)
330
+ Fatty.error("PopUpSession#apply_action: ActionError #{e.message}", tag: :session)
185
331
  []
186
332
  end
187
333
 
188
- def move_selected_by(delta)
334
+ def move_current_by(delta)
189
335
  return if @displayed.empty?
190
336
 
191
- msg = "PopUpSession#move_selected_by before: selected=#{@selected.inspect} delta=#{delta} len=#{@displayed.length}"
337
+ msg = "PopUpSession#move_current_by before: current=#{@current.inspect} delta=#{delta} len=#{@displayed.length}"
192
338
  Fatty.debug(msg)
193
- @selected = ((@selected || 0) + delta) % @displayed.length
194
- Fatty.debug("PopUpSession#move_selected_by after: selected=#{@selected.inspect}")
339
+ @current = ((@current || 0) + delta) % @displayed.length
340
+ Fatty.debug("PopUpSession#move_current_by after: selected=#{@current.inspect}")
195
341
  end
196
342
 
197
343
  def accept_selection
344
+ query = @field.buffer.text.to_s
345
+ @field.accept_line(save_history: @save_history) unless query.empty?
198
346
  if multi_select?
199
- payload = popup_payload(selected_result_hash)
347
+ items = selected_result_hash
348
+ if items.empty? && selected_item
349
+ items[item_label(selected_item)] = item_value(selected_item)
350
+ end
351
+ payload = popup_payload(items)
200
352
  else
201
353
  item = selected_item
202
- query = @field.buffer.text.to_s
203
-
204
354
  return [] if item.nil? && query.empty?
205
355
 
206
356
  item = query if item.nil?
207
357
  payload = popup_payload(item)
208
358
  end
209
359
  [
210
- [:terminal, :send_modal_owner, [:cmd, :popup_result, payload]],
211
- [:terminal, :pop_modal]
360
+ Command.terminal(
361
+ :send_modal_owner,
362
+ command: Command.session(:self, :popup_result, **payload),
363
+ ),
364
+ Command.terminal(:pop_modal),
212
365
  ]
213
366
  end
214
367
 
215
368
  def selected_item
216
- @displayed[@selected]
369
+ @displayed[@current]
217
370
  end
218
371
 
219
372
  def selected_item_label?(item)
220
373
  selected_label?(item_label(item))
221
374
  end
222
375
 
223
- def gutter_for(item:, selected:)
224
- if multi_select?
225
- selected_item_label?(item) ? SELECTED_GUTTER : UNSELECTED_GUTTER
226
- else
227
- ' '
228
- end
229
- end
230
-
231
376
  def refresh_items_if_query_changed
232
377
  q = @field.buffer.text.to_s
233
378
  return if q == @last_query
@@ -258,28 +403,16 @@ module Fatty
258
403
  validate_unique_labels!(@items) if @validate_unique_labels
259
404
 
260
405
  matcher = @matcher || method(:default_matcher)
261
-
262
406
  @filtered =
263
407
  if q.empty?
264
408
  @items
265
409
  else
266
410
  @items.select { |e| matcher.call(e, q) }
267
411
  end
268
-
269
412
  refresh_displayed_items
270
413
  apply_selection_policy!
271
414
  end
272
415
 
273
- # Renderer calls this to determine which slice of items to display.
274
- def scroll_start(list_h:)
275
- max_start = @displayed.length - list_h
276
- max_start = 0 if max_start < 0
277
-
278
- @scroll_start = 0 if @scroll_start < 0
279
- @scroll_start = max_start if @scroll_start > max_start
280
- @scroll_start
281
- end
282
-
283
416
  def toggle_selected_current!
284
417
  item = selected_item
285
418
  return unless item
@@ -295,7 +428,6 @@ module Fatty
295
428
  else
296
429
  @selected_labels[label] = true
297
430
  end
298
-
299
431
  refresh_displayed_items
300
432
  item
301
433
  end
@@ -322,21 +454,6 @@ module Fatty
322
454
  [@displayed.length - scroll_start(list_h: popup_list_height), popup_list_height].min
323
455
  end
324
456
 
325
- def counts
326
- {
327
- total: total_count,
328
- selected: selected_count,
329
- matching: matching_count,
330
- showing: showing_count
331
- }
332
- end
333
-
334
- def selected_labels
335
- @selected_labels.keys
336
- end
337
-
338
- private
339
-
340
457
  def validate_unique_labels!(items)
341
458
  counts = Hash.new(0)
342
459
  items.each do |item|
@@ -350,29 +467,20 @@ module Fatty
350
467
  raise ArgumentError, "duplicate chooser labels: #{shown}"
351
468
  end
352
469
 
353
- def popup_payload(item = selected_item)
354
- {
355
- kind: @kind,
356
- item: item,
357
- query: @field.buffer.text.to_s,
358
- index: @selected
359
- }
360
- end
361
-
362
470
  def popup_payload(item = selected_item)
363
471
  if multi_select?
364
472
  {
365
473
  kind: @kind,
366
474
  items: item,
367
475
  query: @field.buffer.text.to_s,
368
- index: @selected
476
+ index: @current
369
477
  }
370
478
  else
371
479
  {
372
480
  kind: @kind,
373
481
  item: item,
374
482
  query: @field.buffer.text.to_s,
375
- index: @selected
483
+ index: @current
376
484
  }
377
485
  end
378
486
  end
@@ -382,15 +490,20 @@ module Fatty
382
490
  end
383
491
 
384
492
  def popup_extra_rows
385
- rows = 4
493
+ rows = 2 # border rows
386
494
  rows += 1 if popup_has_message?
495
+ rows += 1 if counts_present?
496
+ rows += 1 if filter_present?
387
497
  rows
388
498
  end
389
499
 
390
500
  def notify_owner(name)
391
- return [] unless @kind
392
-
393
- [[:terminal, :send_modal_owner, [:cmd, name, popup_payload]]]
501
+ [
502
+ Command.terminal(
503
+ :send_modal_owner,
504
+ command: Command.session(:self, name, **popup_payload, session: self),
505
+ ),
506
+ ]
394
507
  end
395
508
 
396
509
  def popup_list_height
@@ -422,10 +535,10 @@ module Fatty
422
535
  top_zone = @scroll_start + band
423
536
  bot_zone = (@scroll_start + list_h - 1) - band
424
537
 
425
- if @selected < top_zone
426
- @scroll_start = @selected - band
427
- elsif @selected > bot_zone
428
- @scroll_start = @selected - (list_h - 1) + band
538
+ if @current < top_zone
539
+ @scroll_start = @current - band
540
+ elsif @current > bot_zone
541
+ @scroll_start = @current - (list_h - 1) + band
429
542
  end
430
543
 
431
544
  max_start = @displayed.length - list_h
@@ -437,7 +550,7 @@ module Fatty
437
550
 
438
551
  def recenter_scroll
439
552
  list_h = popup_list_height
440
- @scroll_start = @selected - (list_h / 2)
553
+ @scroll_start = @current - (list_h / 2)
441
554
 
442
555
  max_start = @displayed.length - list_h
443
556
  max_start = 0 if max_start < 0
@@ -469,32 +582,36 @@ module Fatty
469
582
  end
470
583
  end
471
584
 
585
+ def current_index
586
+ return 0 if @displayed.empty?
587
+
588
+ @current = 0 unless @current.is_a?(Integer)
589
+ @current.clamp(0, @displayed.length - 1)
590
+ end
591
+
472
592
  def apply_selection_policy!
473
- prior = @selected
593
+ prior = @current
474
594
  last_idx = [@displayed.length - 1, 0].max
475
-
476
- @selected = if @displayed.empty?
477
- 0
478
- else
479
- case @selection
480
- when :top
595
+ @current =
596
+ if @displayed.empty?
481
597
  0
482
- when :bottom
483
- last_idx
484
598
  else
485
- @selected.clamp(0, last_idx)
599
+ case @current_policy
600
+ when :top
601
+ 0
602
+ when :bottom
603
+ last_idx
604
+ else
605
+ @current.is_a?(Integer) ? @current.clamp(0, last_idx) : 0
606
+ end
486
607
  end
487
- end
488
-
489
- if @selection == :top || @selection == :bottom
490
- recenter_scroll
491
- elsif @selected != prior
608
+ if @current != prior
492
609
  ensure_scroll_visible
493
610
  end
494
611
  end
495
612
 
496
613
  def default_matcher(item, query)
497
- match_all_query_terms?(item, query)
614
+ Fatty::Search.match_all_terms?(item, query)
498
615
  end
499
616
 
500
617
  def action_env(event:)