solid_queue_tui 0.1.2 → 0.2.0

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.
@@ -3,84 +3,141 @@
3
3
  module SolidQueueTui
4
4
  module Views
5
5
  class QueuesView
6
+ include Confirmable
7
+ include Filterable
8
+ include Paginatable
9
+ include FormattingHelpers
10
+
6
11
  def initialize(tui)
7
12
  @tui = tui
8
- @table_state = RatatuiRuby::TableState.new(nil)
9
- @table_state.select(0)
10
- @selected_row = 0
13
+ @mode = :list
14
+ @selected_queue = nil
15
+
16
+ # List mode state
17
+ @list_table_state = RatatuiRuby::TableState.new(nil)
18
+ @list_table_state.select(0)
19
+ @list_selected_row = 0
11
20
  @queues = []
12
- @confirm_action = nil
21
+
22
+ # Detail mode state (via Paginatable + Filterable)
23
+ init_pagination
24
+ init_confirm
25
+ init_filter
26
+ end
27
+
28
+ def detail_mode? = @mode == :detail
29
+
30
+ def selected_queue_name
31
+ @selected_queue&.name
13
32
  end
14
33
 
15
34
  def update(queues:)
16
35
  @queues = queues
17
- @selected_row = @selected_row.clamp(0, [@queues.size - 1, 0].max)
18
- @table_state.select(@selected_row)
36
+ @list_selected_row = @list_selected_row.clamp(0, [@queues.size - 1, 0].max)
37
+ @list_table_state.select(@list_selected_row)
38
+ end
39
+
40
+ def update_detail(jobs:)
41
+ update_items(jobs)
42
+ end
43
+
44
+ def append(jobs:)
45
+ append_items(jobs)
19
46
  end
20
47
 
21
48
  def render(frame, area)
22
- if @confirm_action
23
- confirm_area, content_area = @tui.layout_split(
24
- area,
25
- direction: :vertical,
26
- constraints: [
27
- @tui.constraint_length(3),
28
- @tui.constraint_fill(1)
29
- ]
30
- )
31
- render_confirm(frame, confirm_area)
32
- render_table(frame, content_area)
49
+ if @mode == :list
50
+ render_list(frame, area)
33
51
  else
34
- render_table(frame, area)
52
+ render_detail(frame, area)
35
53
  end
36
54
  end
37
55
 
38
56
  def handle_input(event)
39
- if @confirm_action
57
+ if @mode == :list
58
+ handle_list_input(event)
59
+ elsif confirm_mode?
40
60
  handle_confirm_input(event)
61
+ elsif filter_mode?
62
+ handle_filter_input(event)
41
63
  else
42
- handle_normal_input(event)
64
+ handle_detail_input(event)
43
65
  end
44
66
  end
45
67
 
46
68
  def selected_item
47
- return nil if @queues.empty? || @selected_row >= @queues.size
48
- @queues[@selected_row]
69
+ if @mode == :list
70
+ return nil if @queues.empty? || @list_selected_row >= @queues.size
71
+ @queues[@list_selected_row]
72
+ else
73
+ return nil if items.empty? || @selected_row >= items.size
74
+ items[@selected_row]
75
+ end
49
76
  end
50
77
 
51
78
  def capturing_input?
52
- !!@confirm_action
79
+ detail_mode? || confirm_mode? || filter_mode?
53
80
  end
54
81
 
55
82
  def bindings
56
- if @confirm_action
57
- [
58
- { key: "y", action: "Confirm" },
59
- { key: "n/Esc", action: "Cancel" }
60
- ]
83
+ if @mode == :list
84
+ if confirm_mode?
85
+ confirm_bindings
86
+ else
87
+ [
88
+ { key: "j/k", action: "Navigate" },
89
+ { key: "Enter", action: "View Jobs" },
90
+ { key: "p", action: "Pause/Resume" },
91
+ { key: "G/g", action: "Bottom/Top" }
92
+ ]
93
+ end
61
94
  else
62
- [
63
- { key: "j/k", action: "Navigate" },
64
- { key: "p", action: "Pause/Resume" },
65
- { key: "G/g", action: "Bottom/Top" }
66
- ]
95
+ if confirm_mode?
96
+ confirm_bindings
97
+ elsif filter_mode?
98
+ filter_bindings
99
+ else
100
+ [
101
+ { key: "j/k", action: "Navigate" },
102
+ { key: "Enter", action: "Detail" },
103
+ { key: "/", action: "Filter" },
104
+ clear_filter_binding,
105
+ { key: "Esc", action: "Back" },
106
+ { key: "G/g", action: "Bottom/Top" }
107
+ ].compact
108
+ end
67
109
  end
68
110
  end
69
111
 
70
- def breadcrumb = "queues"
112
+ def breadcrumb
113
+ if @mode == :list
114
+ "queues"
115
+ else
116
+ base = "queues > #{@selected_queue&.name}"
117
+ @filters.empty? ? base : "#{base}:filtered"
118
+ end
119
+ end
71
120
 
72
121
  private
73
122
 
74
- def handle_normal_input(event)
123
+ # === List mode ===
124
+
125
+ def handle_list_input(event)
126
+ if confirm_mode?
127
+ return handle_confirm_input(event)
128
+ end
129
+
75
130
  case event
76
131
  in { type: :key, code: "j" } | { type: :key, code: "up" }
77
- move_selection(-1)
132
+ list_move_selection(-1)
78
133
  in { type: :key, code: "k" } | { type: :key, code: "down" }
79
- move_selection(1)
134
+ list_move_selection(1)
80
135
  in { type: :key, code: "g" }
81
- jump_to_top
136
+ list_jump_to_top
82
137
  in { type: :key, code: "G" }
83
- jump_to_bottom
138
+ list_jump_to_bottom
139
+ in { type: :key, code: "enter" }
140
+ enter_detail_mode
84
141
  in { type: :key, code: "p" }
85
142
  queue = selected_item
86
143
  if queue
@@ -92,40 +149,46 @@ module SolidQueueTui
92
149
  end
93
150
  end
94
151
 
95
- def handle_confirm_input(event)
96
- case event
97
- in { type: :key, code: "y" }
98
- @confirm_action = nil
99
- queue = selected_item
100
- return nil unless queue
101
- Actions::ToggleQueuePause.call(queue.name)
102
- :refresh
103
- in { type: :key, code: "n" } | { type: :key, code: "esc" }
104
- @confirm_action = nil
105
- nil
106
- else
107
- nil
108
- end
152
+ def enter_detail_mode
153
+ queue = selected_item
154
+ return nil unless queue
155
+ @selected_queue = queue
156
+ @mode = :detail
157
+ reset_pagination!
158
+ clear_filter
159
+ :enter_queue
109
160
  end
110
161
 
111
- def move_selection(delta)
162
+ def list_move_selection(delta)
112
163
  return if @queues.empty?
113
- @selected_row = (@selected_row + delta).clamp(0, @queues.size - 1)
114
- @table_state.select(@selected_row)
164
+ @list_selected_row = (@list_selected_row + delta).clamp(0, @queues.size - 1)
165
+ @list_table_state.select(@list_selected_row)
166
+ nil
115
167
  end
116
168
 
117
- def jump_to_top
118
- @selected_row = 0
119
- @table_state.select(0)
169
+ def list_jump_to_top
170
+ @list_selected_row = 0
171
+ @list_table_state.select(0)
172
+ nil
120
173
  end
121
174
 
122
- def jump_to_bottom
123
- return if @queues.empty?
124
- @selected_row = @queues.size - 1
125
- @table_state.select(@selected_row)
175
+ def list_jump_to_bottom
176
+ return nil if @queues.empty?
177
+ @list_selected_row = @queues.size - 1
178
+ @list_table_state.select(@list_selected_row)
179
+ nil
180
+ end
181
+
182
+ def render_list(frame, area)
183
+ if confirm_mode?
184
+ render_list_table(frame, area)
185
+ render_confirm_popup(frame, area)
186
+ else
187
+ render_list_table(frame, area)
188
+ end
126
189
  end
127
190
 
128
- def render_table(frame, area)
191
+ def render_list_table(frame, area)
129
192
  columns = [
130
193
  { key: :name, label: "QUEUE", width: :fill },
131
194
  { key: :size, label: "SIZE", width: 10 },
@@ -145,36 +208,112 @@ module SolidQueueTui
145
208
  title: "Queues",
146
209
  columns: columns,
147
210
  rows: rows,
148
- selected_row: @selected_row,
211
+ selected_row: @list_selected_row,
149
212
  empty_message: "No queues found"
150
213
  )
151
214
 
152
- table.render(frame, area, @table_state)
215
+ table.render(frame, area, @list_table_state)
153
216
  end
154
217
 
155
- def render_confirm(frame, area)
156
- queue = selected_item
157
- message = if @confirm_action == :pause
158
- "Pause queue '#{queue&.name}'? Workers will stop picking up jobs from this queue. [y/n]"
159
- else
160
- "Resume queue '#{queue&.name}'? [y/n]"
161
- end
162
-
163
- frame.render_widget(
164
- @tui.paragraph(
165
- text: " #{message}",
166
- style: @tui.style(fg: :yellow, modifiers: [:bold]),
167
- block: @tui.block(
168
- title: " Confirm ",
169
- title_style: @tui.style(fg: :red, modifiers: [:bold]),
170
- borders: [:all],
171
- border_type: :rounded,
172
- border_style: @tui.style(fg: :red)
173
- )
174
- ),
175
- area
218
+ # === Detail mode ===
219
+
220
+ def handle_detail_input(event)
221
+ case event
222
+ in { type: :key, code: "j" } | { type: :key, code: "up" }
223
+ move_selection(-1)
224
+ in { type: :key, code: "k" } | { type: :key, code: "down" }
225
+ result = move_selection(1)
226
+ return :load_more if result == :load_more
227
+ nil
228
+ in { type: :key, code: "g" }
229
+ jump_to_top
230
+ in { type: :key, code: "G" }
231
+ jump_to_bottom
232
+ in { type: :key, code: "enter" }
233
+ :open_detail
234
+ in { type: :key, code: "/" }
235
+ enter_filter_mode
236
+ nil
237
+ in { type: :key, code: "c" }
238
+ clear_filter
239
+ in { type: :key, code: "esc" }
240
+ exit_detail_mode
241
+ else
242
+ nil
243
+ end
244
+ end
245
+
246
+ def exit_detail_mode
247
+ @mode = :list
248
+ @selected_queue = nil
249
+ :exit_queue
250
+ end
251
+
252
+ def render_detail(frame, area)
253
+ if filter_mode?
254
+ filter_area, content_area = @tui.layout_split(
255
+ area,
256
+ direction: :vertical,
257
+ constraints: [
258
+ @tui.constraint_length(3),
259
+ @tui.constraint_fill(1)
260
+ ]
261
+ )
262
+ render_filter_input(frame, filter_area)
263
+ render_detail_table(frame, content_area)
264
+ else
265
+ render_detail_table(frame, area)
266
+ end
267
+ end
268
+
269
+ def render_detail_table(frame, area)
270
+ columns = [
271
+ { key: :id, label: "ID", width: 8 },
272
+ { key: :class_name, label: "CLASS", width: :fill },
273
+ { key: :priority, label: "PRI", width: 5 },
274
+ { key: :created_at, label: "ENQUEUED", width: 12 }
275
+ ]
276
+
277
+ rows = items.map do |job|
278
+ {
279
+ id: job.id,
280
+ class_name: job.class_name,
281
+ priority: job.priority,
282
+ created_at: time_ago(job.created_at)
283
+ }
284
+ end
285
+
286
+ table = Components::JobTable.new(
287
+ @tui,
288
+ title: filter_title("Queue '#{@selected_queue&.name}' — Pending"),
289
+ columns: columns,
290
+ rows: rows,
291
+ selected_row: @selected_row,
292
+ total_count: @total_count,
293
+ empty_message: "No pending jobs in this queue"
176
294
  )
295
+
296
+ table.render(frame, area, @table_state)
297
+ end
298
+
299
+ # === Confirmable hooks (list mode only) ===
300
+
301
+ def confirm_message
302
+ queue = @queues[@list_selected_row]
303
+ if @confirm_action == :pause
304
+ "Pause queue '#{queue&.name}'? Workers will stop picking up jobs from this queue. [y/n]"
305
+ else
306
+ "Resume queue '#{queue&.name}'? [y/n]"
307
+ end
177
308
  end
309
+
310
+ def execute_confirm_action(action)
311
+ queue = @queues[@list_selected_row]
312
+ return nil unless queue
313
+ Actions::ToggleQueuePause.call(queue.name)
314
+ :refresh
315
+ end
316
+
178
317
  end
179
318
  end
180
319
  end
@@ -3,13 +3,16 @@
3
3
  module SolidQueueTui
4
4
  module Views
5
5
  class RecurringTasksView
6
+ include Confirmable
7
+ include FormattingHelpers
8
+
6
9
  def initialize(tui)
7
10
  @tui = tui
8
11
  @table_state = RatatuiRuby::TableState.new(nil)
9
12
  @table_state.select(0)
10
13
  @selected_row = 0
11
14
  @tasks = []
12
- @confirm_action = nil
15
+ init_confirm
13
16
  end
14
17
 
15
18
  def update(tasks:)
@@ -19,24 +22,16 @@ module SolidQueueTui
19
22
  end
20
23
 
21
24
  def render(frame, area)
22
- if @confirm_action
23
- confirm_area, content_area = @tui.layout_split(
24
- area,
25
- direction: :vertical,
26
- constraints: [
27
- @tui.constraint_length(3),
28
- @tui.constraint_fill(1)
29
- ]
30
- )
31
- render_confirm(frame, confirm_area)
32
- render_table(frame, content_area)
25
+ if confirm_mode?
26
+ render_table(frame, area)
27
+ render_confirm_popup(frame, area)
33
28
  else
34
29
  render_table(frame, area)
35
30
  end
36
31
  end
37
32
 
38
33
  def handle_input(event)
39
- if @confirm_action
34
+ if confirm_mode?
40
35
  handle_confirm_input(event)
41
36
  else
42
37
  handle_normal_input(event)
@@ -49,15 +44,12 @@ module SolidQueueTui
49
44
  end
50
45
 
51
46
  def capturing_input?
52
- !!@confirm_action
47
+ confirm_mode?
53
48
  end
54
49
 
55
50
  def bindings
56
- if @confirm_action
57
- [
58
- { key: "y", action: "Confirm" },
59
- { key: "n/Esc", action: "Cancel" }
60
- ]
51
+ if confirm_mode?
52
+ confirm_bindings
61
53
  else
62
54
  [
63
55
  { key: "j/k", action: "Navigate" },
@@ -89,20 +81,16 @@ module SolidQueueTui
89
81
  end
90
82
  end
91
83
 
92
- def handle_confirm_input(event)
93
- case event
94
- in { type: :key, code: "y" }
95
- @confirm_action = nil
96
- task = selected_item
97
- return nil unless task
98
- Actions::EnqueueRecurringTask.call(task.key)
99
- :refresh
100
- in { type: :key, code: "n" } | { type: :key, code: "esc" }
101
- @confirm_action = nil
102
- nil
103
- else
104
- nil
105
- end
84
+ def confirm_message
85
+ task = selected_item
86
+ "Run '#{task&.key}' (#{task&.class_name || task&.command}) now? [y/n]"
87
+ end
88
+
89
+ def execute_confirm_action(action)
90
+ task = selected_item
91
+ return nil unless task
92
+ Actions::EnqueueRecurringTask.call(task.key)
93
+ :refresh
106
94
  end
107
95
 
108
96
  def move_selection(delta)
@@ -155,48 +143,13 @@ module SolidQueueTui
155
143
  table.render(frame, area, @table_state)
156
144
  end
157
145
 
158
- def render_confirm(frame, area)
159
- task = selected_item
160
- message = "Run '#{task&.key}' (#{task&.class_name || task&.command}) now? [y/n]"
161
-
162
- frame.render_widget(
163
- @tui.paragraph(
164
- text: " #{message}",
165
- style: @tui.style(fg: :yellow, modifiers: [:bold]),
166
- block: @tui.block(
167
- title: " Confirm ",
168
- title_style: @tui.style(fg: :red, modifiers: [:bold]),
169
- borders: [:all],
170
- border_type: :rounded,
171
- border_style: @tui.style(fg: :red)
172
- )
173
- ),
174
- area
175
- )
176
- end
177
-
146
+ # Override: returns "Never" for nil and "just now" for very recent
178
147
  def time_ago(time)
179
148
  return "Never" unless time
180
149
  seconds = (Time.now.utc - time).to_i
181
150
  return "just now" if seconds < 5
182
151
  "#{humanize_duration(seconds)} ago"
183
152
  end
184
-
185
- def time_until(time)
186
- return "n/a" unless time
187
- seconds = (time - Time.now.utc).to_i
188
- return "now" if seconds <= 0
189
- "in #{humanize_duration(seconds)}"
190
- end
191
-
192
- def humanize_duration(seconds)
193
- case seconds.abs
194
- when 0..59 then "#{seconds.abs}s"
195
- when 60..3599 then "#{seconds.abs / 60}m"
196
- when 3600..86399 then "#{seconds.abs / 3600}h"
197
- else "#{seconds.abs / 86400}d"
198
- end
199
- end
200
153
  end
201
154
  end
202
155
  end