sktop 0.1.5 → 0.1.6

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5a3ed9a8721d477335f08d009c76cd65793b96ab0c0a045466ebdfa05a5e7f12
4
- data.tar.gz: cba94dc737356f0b9295cc04c00155dd2cc7eb60372dec1774b60489ae4c476d
3
+ metadata.gz: 72e27a287d42246bf03393f3f5d8fd83fcf2dcb52d3c32a242f6631a556d5a90
4
+ data.tar.gz: a26fa56421cf3fa038f2d7fa7ec86e204189be4bd21419d1ddf896631f59a8cc
5
5
  SHA512:
6
- metadata.gz: e26bc9fe20fdb3d540c417802ca15db53b5263f52dcab6f696afb1b4cf6d52134e8dafe6d09abf8378e694fdb5c0307c61b7c84a0e523aee12f9db5b2cb00bd1
7
- data.tar.gz: 93eac6acd09dda870a90311fbd606a54ee7ad072919bba959951ecec91e31eb75d01e4a851aec1f111f9e0f193328c0f02e3adad914a15afdd17c67c02ec6ea5
6
+ metadata.gz: d7aa57eac65085688adadc80a4d587c6c3e0c100c09518675bf4cc65fdcfac93e37e404a7476eda3a4b4ec2d2b7a595f3e6231a2b1c414475c4907fd52ff4275
7
+ data.tar.gz: ef09ee20ae517a01e85c427bc291d8adff140e0239342336f22365df774efdf83c0df76b34839ed99e15d63a66c36de4eaeb4f7634a24d098c27fab6894055ec
data/lib/sktop/cli.rb CHANGED
@@ -195,7 +195,17 @@ module Sktop
195
195
  scheduled_jobs: collector.scheduled_jobs(limit: 500),
196
196
  dead_jobs: collector.dead_jobs(limit: 500)
197
197
  }
198
+
199
+ # If viewing queue jobs, refresh that data too
200
+ if @display.current_view == :queue_jobs && @display.selected_queue
201
+ snapshot[:queue_jobs] = collector.queue_jobs(@display.selected_queue, limit: 500)
202
+ end
203
+
198
204
  @data_mutex.synchronize do
205
+ # Preserve queue_jobs if not refreshed above but still in that view
206
+ if @cached_data && @cached_data[:queue_jobs] && !snapshot[:queue_jobs]
207
+ snapshot[:queue_jobs] = @cached_data[:queue_jobs]
208
+ end
199
209
  @cached_data = snapshot
200
210
  @data_version += 1
201
211
  end
@@ -282,6 +292,8 @@ module Sktop
282
292
  @display.current_view = :dead
283
293
  when 'm', 'M'
284
294
  @display.current_view = :main
295
+ when "\r", "\n" # Enter key
296
+ handle_enter_action
285
297
  when "\x12" # Ctrl+R - retry job
286
298
  handle_retry_action
287
299
  when "\x18" # Ctrl+X - delete job
@@ -299,8 +311,10 @@ module Sktop
299
311
  @display.select_up
300
312
  when "[B" # Down arrow
301
313
  @display.select_down
302
- when "[C" # Right arrow (unused for now)
303
- when "[D" # Left arrow (unused for now)
314
+ when "[C" # Right arrow - next view
315
+ @display.next_view
316
+ when "[D" # Left arrow - previous view
317
+ @display.previous_view
304
318
  when "[5~" # Page Up
305
319
  @display.page_up
306
320
  when "[6~" # Page Down
@@ -310,12 +324,12 @@ module Sktop
310
324
  when "x", "X" # Alt+X - Delete All
311
325
  handle_delete_all_action
312
326
  else
313
- # Just Escape key - go to main
314
- @display.current_view = :main
327
+ # Just Escape key - go back or to main
328
+ handle_escape_action
315
329
  end
316
330
  else
317
- # Just Escape key - go to main
318
- @display.current_view = :main
331
+ # Just Escape key - go back or to main
332
+ handle_escape_action
319
333
  end
320
334
  when "\u0003" # Ctrl+C
321
335
  raise Interrupt
@@ -362,6 +376,12 @@ module Sktop
362
376
  end
363
377
 
364
378
  def handle_delete_action
379
+ # Handle queue_jobs view separately
380
+ if @display.current_view == :queue_jobs
381
+ handle_delete_queue_job_action
382
+ return
383
+ end
384
+
365
385
  return unless [:retries, :dead].include?(@display.current_view)
366
386
 
367
387
  data = @data_mutex.synchronize { @cached_data }
@@ -500,5 +520,104 @@ module Sktop
500
520
  end
501
521
  end
502
522
 
523
+ def handle_enter_action
524
+ return unless @display.current_view == :queues
525
+
526
+ data = @data_mutex.synchronize { @cached_data }
527
+ unless data
528
+ @display.set_status("No data available")
529
+ return
530
+ end
531
+
532
+ queues = data[:queues]
533
+ selected_idx = @display.selected_index
534
+
535
+ if queues.empty?
536
+ @display.set_status("No queues")
537
+ return
538
+ end
539
+
540
+ if selected_idx >= queues.length
541
+ @display.set_status("Invalid selection")
542
+ return
543
+ end
544
+
545
+ queue = queues[selected_idx]
546
+ queue_name = queue[:name]
547
+
548
+ # Fetch jobs from the queue
549
+ begin
550
+ collector = StatsCollector.new
551
+ jobs = collector.queue_jobs(queue_name, limit: 500)
552
+
553
+ # Update cached data with queue jobs
554
+ @data_mutex.synchronize do
555
+ @cached_data[:queue_jobs] = jobs
556
+ end
557
+
558
+ @display.selected_queue = queue_name
559
+ @display.current_view = :queue_jobs
560
+ @display.set_status("Loaded #{jobs.length} jobs from #{queue_name}")
561
+ rescue => e
562
+ @display.set_status("Error loading queue: #{e.message}")
563
+ end
564
+ end
565
+
566
+ def handle_escape_action
567
+ if @display.current_view == :queue_jobs
568
+ # Go back to queues view
569
+ @display.current_view = :queues
570
+ @display.selected_queue = nil
571
+ else
572
+ # Go to main view
573
+ @display.current_view = :main
574
+ end
575
+ end
576
+
577
+ def handle_delete_queue_job_action
578
+ return unless @display.current_view == :queue_jobs
579
+
580
+ data = @data_mutex.synchronize { @cached_data }
581
+ unless data
582
+ @display.set_status("No data available")
583
+ return
584
+ end
585
+
586
+ jobs = data[:queue_jobs] || []
587
+ selected_idx = @display.selected_index
588
+ queue_name = @display.selected_queue
589
+
590
+ if jobs.empty?
591
+ @display.set_status("No jobs to delete")
592
+ return
593
+ end
594
+
595
+ if selected_idx >= jobs.length
596
+ @display.set_status("Invalid selection")
597
+ return
598
+ end
599
+
600
+ job = jobs[selected_idx]
601
+ unless job[:jid]
602
+ @display.set_status("Job has no JID")
603
+ return
604
+ end
605
+
606
+ begin
607
+ Sktop::JobActions.delete_queue_job(queue_name, job[:jid])
608
+ @display.set_status("Deleted #{job[:class]}")
609
+
610
+ # Refresh the queue jobs
611
+ collector = StatsCollector.new
612
+ new_jobs = collector.queue_jobs(queue_name, limit: 500)
613
+ @data_mutex.synchronize do
614
+ @cached_data[:queue_jobs] = new_jobs
615
+ end
616
+ @rendered_version = -1
617
+ rescue => e
618
+ @display.set_status("Error: #{e.message}")
619
+ end
620
+ end
621
+
503
622
  end
504
623
  end
data/lib/sktop/display.rb CHANGED
@@ -15,8 +15,11 @@ module Sktop
15
15
  @status_time = nil
16
16
  @connection_status = :connecting # :connecting, :connected, :updating, :error
17
17
  @last_update = nil
18
+ @selected_queue = nil # Track which queue is being viewed in queue_jobs view
18
19
  end
19
20
 
21
+ attr_accessor :selected_queue
22
+
20
23
  def scroll_up
21
24
  @scroll_offsets[@current_view] = [@scroll_offsets[@current_view] - 1, 0].max
22
25
  end
@@ -42,7 +45,7 @@ module Sktop
42
45
  end
43
46
 
44
47
  def selectable_view?
45
- [:processes, :retries, :dead].include?(@current_view)
48
+ [:queues, :queue_jobs, :processes, :retries, :dead].include?(@current_view)
46
49
  end
47
50
 
48
51
  def page_up(page_size = nil)
@@ -86,6 +89,18 @@ module Sktop
86
89
  # Don't reset scroll when switching views - preserve position
87
90
  end
88
91
 
92
+ VIEW_ORDER = [:main, :queues, :processes, :workers, :retries, :scheduled, :dead].freeze
93
+
94
+ def next_view
95
+ current_idx = VIEW_ORDER.index(@current_view) || 0
96
+ @current_view = VIEW_ORDER[(current_idx + 1) % VIEW_ORDER.length]
97
+ end
98
+
99
+ def previous_view
100
+ current_idx = VIEW_ORDER.index(@current_view) || 0
101
+ @current_view = VIEW_ORDER[(current_idx - 1) % VIEW_ORDER.length]
102
+ end
103
+
89
104
  def reset_cursor
90
105
  print @cursor.move_to(0, 0)
91
106
  print @cursor.hide
@@ -167,6 +182,10 @@ module Sktop
167
182
  def dead_jobs(limit: 50)
168
183
  @data[:dead_jobs]&.first(limit) || []
169
184
  end
185
+
186
+ def queue_jobs_cache
187
+ @data[:queue_jobs] || []
188
+ end
170
189
  end
171
190
 
172
191
  private
@@ -175,6 +194,8 @@ module Sktop
175
194
  case @current_view
176
195
  when :queues
177
196
  build_queues_detail(collector)
197
+ when :queue_jobs
198
+ build_queue_jobs_detail(collector)
178
199
  when :processes
179
200
  build_processes_detail(collector)
180
201
  when :workers
@@ -245,7 +266,18 @@ module Sktop
245
266
  lines << ""
246
267
  # Calculate available rows: height - header(1) - blank(1) - section(1) - table_header(1) - footer(1) = height - 5
247
268
  max_rows = terminal_height - 5
248
- queues_scrollable(collector.queues, max_rows).each_line(chomp: true) { |l| lines << l }
269
+ queues_selectable(collector.queues, max_rows).each_line(chomp: true) { |l| lines << l }
270
+ lines << :footer
271
+ lines
272
+ end
273
+
274
+ def build_queue_jobs_detail(collector)
275
+ lines = []
276
+ lines << header_bar
277
+ lines << ""
278
+ max_rows = terminal_height - 5
279
+ jobs = collector.respond_to?(:queue_jobs_cache) ? collector.queue_jobs_cache : []
280
+ queue_jobs_selectable(jobs, max_rows).each_line(chomp: true) { |l| lines << l }
249
281
  lines << :footer
250
282
  lines
251
283
  end
@@ -601,6 +633,133 @@ module Sktop
601
633
  lines.join("\n")
602
634
  end
603
635
 
636
+ # Selectable queues view - press Enter to view queue contents
637
+ def queues_selectable(queues, max_rows)
638
+ width = terminal_width
639
+ lines = []
640
+
641
+ scroll_offset = @scroll_offsets[@current_view]
642
+ data_rows = max_rows - 3 # Account for section bar, header, and status line
643
+ max_scroll = [queues.length - data_rows, 0].max
644
+ scroll_offset = [[scroll_offset, 0].max, max_scroll].min
645
+ @scroll_offsets[@current_view] = scroll_offset
646
+
647
+ # Clamp selected index
648
+ @selected_index[@current_view] = [[@selected_index[@current_view], 0].max, [queues.length - 1, 0].max].min
649
+
650
+ # Auto-scroll to keep selection visible
651
+ selected = @selected_index[@current_view]
652
+ if selected < scroll_offset
653
+ scroll_offset = selected
654
+ @scroll_offsets[@current_view] = scroll_offset
655
+ elsif selected >= scroll_offset + data_rows
656
+ scroll_offset = selected - data_rows + 1
657
+ @scroll_offsets[@current_view] = scroll_offset
658
+ end
659
+
660
+ scroll_indicator = queues.length > data_rows ? " [#{scroll_offset + 1}-#{[scroll_offset + data_rows, queues.length].min}/#{queues.length}]" : ""
661
+ lines << section_bar("Queues#{scroll_indicator} - ↑↓ select, Enter=view jobs, m=main")
662
+
663
+ if queues.empty?
664
+ lines << @pastel.dim(" No queues")
665
+ return lines.join("\n")
666
+ end
667
+
668
+ name_width = [40, width - 40].max
669
+ header = sprintf(" %-#{name_width}s %10s %10s %10s", "NAME", "SIZE", "LATENCY", "STATUS")
670
+ lines << format_table_header(header)
671
+
672
+ queues.drop(scroll_offset).first(data_rows).each_with_index do |queue, idx|
673
+ actual_idx = scroll_offset + idx
674
+ row = format_queue_row(queue, name_width)
675
+
676
+ if actual_idx == selected
677
+ lines << @pastel.black.on_white(row + " " * [width - visible_string_length(row), 0].max)
678
+ else
679
+ lines << row
680
+ end
681
+ end
682
+
683
+ remaining = queues.length - scroll_offset - data_rows
684
+ if remaining > 0
685
+ lines << @pastel.dim(" ↓ #{remaining} more")
686
+ end
687
+
688
+ # Status message
689
+ if @status_message && @status_time && (Time.now - @status_time) < 3
690
+ lines << @pastel.green(" #{@status_message}")
691
+ end
692
+
693
+ lines.join("\n")
694
+ end
695
+
696
+ # Selectable queue jobs view - press Ctrl+X to delete job
697
+ def queue_jobs_selectable(jobs, max_rows)
698
+ width = terminal_width
699
+ lines = []
700
+
701
+ scroll_offset = @scroll_offsets[@current_view]
702
+ data_rows = max_rows - 3 # Account for section bar, header, and status line
703
+ max_scroll = [jobs.length - data_rows, 0].max
704
+ scroll_offset = [[scroll_offset, 0].max, max_scroll].min
705
+ @scroll_offsets[@current_view] = scroll_offset
706
+
707
+ # Clamp selected index
708
+ @selected_index[@current_view] = [[@selected_index[@current_view], 0].max, [jobs.length - 1, 0].max].min
709
+
710
+ # Auto-scroll to keep selection visible
711
+ selected = @selected_index[@current_view]
712
+ if selected < scroll_offset
713
+ scroll_offset = selected
714
+ @scroll_offsets[@current_view] = scroll_offset
715
+ elsif selected >= scroll_offset + data_rows
716
+ scroll_offset = selected - data_rows + 1
717
+ @scroll_offsets[@current_view] = scroll_offset
718
+ end
719
+
720
+ queue_name = @selected_queue || "unknown"
721
+ scroll_indicator = jobs.length > data_rows ? " [#{scroll_offset + 1}-#{[scroll_offset + data_rows, jobs.length].min}/#{jobs.length}]" : ""
722
+ lines << section_bar("Queue: #{queue_name}#{scroll_indicator} - ↑↓ select, ^X=delete, Esc=back")
723
+
724
+ if jobs.empty?
725
+ lines << @pastel.dim(" No jobs in queue")
726
+ return lines.join("\n")
727
+ end
728
+
729
+ job_width = [35, (width - 50) / 2].max
730
+ args_width = [35, (width - 50) / 2].max
731
+
732
+ header = sprintf(" %-#{job_width}s %-20s %-#{args_width}s", "JOB", "ENQUEUED", "ARGS")
733
+ lines << format_table_header(header)
734
+
735
+ jobs.drop(scroll_offset).first(data_rows).each_with_index do |job, idx|
736
+ actual_idx = scroll_offset + idx
737
+ klass = truncate(job[:class].to_s, job_width)
738
+ enqueued = job[:enqueued_at]&.strftime("%Y-%m-%d %H:%M:%S") || "N/A"
739
+ args = truncate(job[:args].inspect, args_width)
740
+
741
+ row = sprintf(" %-#{job_width}s %-20s %-#{args_width}s", klass, enqueued, args)
742
+
743
+ if actual_idx == selected
744
+ lines << @pastel.black.on_white(row + " " * [width - visible_string_length(row), 0].max)
745
+ else
746
+ lines << row
747
+ end
748
+ end
749
+
750
+ remaining = jobs.length - scroll_offset - data_rows
751
+ if remaining > 0
752
+ lines << @pastel.dim(" ↓ #{remaining} more")
753
+ end
754
+
755
+ # Status message
756
+ if @status_message && @status_time && (Time.now - @status_time) < 3
757
+ lines << @pastel.green(" #{@status_message}")
758
+ end
759
+
760
+ lines.join("\n")
761
+ end
762
+
604
763
  def format_queue_row(queue, name_width)
605
764
  name = truncate(queue[:name], name_width)
606
765
  size = format_number(queue[:size])
@@ -1083,31 +1242,36 @@ module Sktop
1083
1242
  end
1084
1243
 
1085
1244
  def function_bar
1086
- items = if @current_view == :main
1087
- [
1088
- ["q", "Queues"],
1089
- ["p", "Procs"],
1090
- ["w", "Workers"],
1091
- ["r", "Retries"],
1092
- ["s", "Sched"],
1093
- ["d", "Dead"],
1094
- ["^C", "Quit"]
1095
- ]
1096
- else
1097
- [
1098
- ["m", "Main"],
1099
- ["q", "Queues"],
1100
- ["p", "Procs"],
1101
- ["w", "Workers"],
1102
- ["r", "Retries"],
1103
- ["s", "Sched"],
1104
- ["d", "Dead"],
1105
- ["^C", "Quit"]
1106
- ]
1107
- end
1245
+ # Map keys to views for highlighting
1246
+ view_keys = {
1247
+ "m" => :main,
1248
+ "q" => :queues,
1249
+ "p" => :processes,
1250
+ "w" => :workers,
1251
+ "r" => :retries,
1252
+ "s" => :scheduled,
1253
+ "d" => :dead
1254
+ }
1255
+
1256
+ items = [
1257
+ ["m", "Main"],
1258
+ ["q", "Queues"],
1259
+ ["p", "Procs"],
1260
+ ["w", "Workers"],
1261
+ ["r", "Retries"],
1262
+ ["s", "Sched"],
1263
+ ["d", "Dead"],
1264
+ ["^C", "Quit"]
1265
+ ]
1108
1266
 
1109
1267
  bar = items.map do |key, label|
1110
- @pastel.black.on_cyan.bold(key) + @pastel.white.on_blue(label)
1268
+ view = view_keys[key]
1269
+ if view && view == @current_view
1270
+ # Highlight current view with inverted colors
1271
+ @pastel.black.on_white.bold(key) + @pastel.black.on_green.bold(label)
1272
+ else
1273
+ @pastel.black.on_cyan.bold(key) + @pastel.white.on_blue(label)
1274
+ end
1111
1275
  end.join(" ")
1112
1276
 
1113
1277
  width = terminal_width
@@ -41,6 +41,15 @@ module Sktop
41
41
  count
42
42
  end
43
43
 
44
+ def delete_queue_job(queue_name, jid)
45
+ queue = Sidekiq::Queue.new(queue_name)
46
+ job = queue.find { |j| j.jid == jid }
47
+ raise "Job not found in queue #{queue_name} (JID: #{jid})" unless job
48
+
49
+ job.delete
50
+ true
51
+ end
52
+
44
53
  def quiet_process(identity)
45
54
  process = find_process(identity)
46
55
  raise "Process not found (identity: #{identity})" unless process
@@ -108,6 +108,19 @@ module Sktop
108
108
  end
109
109
  end
110
110
 
111
+ def queue_jobs(queue_name, limit: 100)
112
+ queue = Sidekiq::Queue.new(queue_name)
113
+ queue.first(limit).map do |job|
114
+ {
115
+ jid: job.jid,
116
+ class: job.klass,
117
+ args: job.args,
118
+ enqueued_at: job.enqueued_at,
119
+ created_at: job.created_at
120
+ }
121
+ end
122
+ end
123
+
111
124
  def scheduled_jobs(limit: 10)
112
125
  Sidekiq::ScheduledSet.new.first(limit).map do |job|
113
126
  {
data/lib/sktop/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sktop
4
- VERSION = "0.1.5"
4
+ VERSION = "0.1.6"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sktop
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - James
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-01-17 00:00:00.000000000 Z
11
+ date: 2026-01-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sidekiq