lazyrails-tui 0.3.1 → 0.3.2
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 +4 -4
- data/bin/lazyrails +1 -1
- data/lib/lazyrails/app.rb +147 -0
- data/lib/lazyrails/command_log_overlay.rb +36 -4
- data/lib/lazyrails/data_loader.rb +5 -0
- data/lib/lazyrails/help_overlay.rb +9 -0
- data/lib/lazyrails/message_handlers.rb +6 -0
- data/lib/lazyrails/panel_handlers.rb +2 -2
- data/lib/lazyrails/renderer.rb +51 -10
- data/lib/lazyrails/server_manager.rb +17 -4
- data/lib/lazyrails/version.rb +1 -1
- data/lib/lazyrails/views/command_log_view.rb +56 -30
- data/lib/lazyrails/views/rake_view.rb +44 -2
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 425bb616025ba482fb9c62f4831fa1a1406d0258d1871929970271fdf5aec6c1
|
|
4
|
+
data.tar.gz: 6eec64b46c70ce39ddc12e97b602f525bd5377e8a321db3dd8f789e30bf354a7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d0063d9de772c58862725cb7f5c8adb748dfb0581a1bdc60564d8681075b908ff8541e7be0947b9d6ba9fdd96f395296fa4e245ec7ecd462e32f9e50702f1dd7
|
|
7
|
+
data.tar.gz: 2a0d7d625bf68f306bc3b1dea48390408e391de5088844e1938eedc46c93ba2cb4ec84cd4390c16aedd0aae2bc8d1913c212244ab6b3d874b8772fa96bf3e304
|
data/bin/lazyrails
CHANGED
data/lib/lazyrails/app.rb
CHANGED
|
@@ -91,6 +91,9 @@ module LazyRails
|
|
|
91
91
|
@jobs_available = nil
|
|
92
92
|
@jobs_tick_counter = 0
|
|
93
93
|
@pending_job_action = nil
|
|
94
|
+
@panel_busy = Set.new
|
|
95
|
+
@last_command_result = {}
|
|
96
|
+
@force_quit = false
|
|
94
97
|
|
|
95
98
|
# Caches
|
|
96
99
|
@detail_content = ""
|
|
@@ -137,6 +140,7 @@ module LazyRails
|
|
|
137
140
|
case msg
|
|
138
141
|
when Chamomile::ResizeEvent then handle_resize(msg)
|
|
139
142
|
when Chamomile::KeyEvent then return handle_key(msg)
|
|
143
|
+
when Chamomile::MouseEvent then return handle_mouse(msg)
|
|
140
144
|
when Chamomile::TickEvent then return handle_tick
|
|
141
145
|
when Chamomile::InterruptEvent then return shutdown
|
|
142
146
|
when IntrospectLoadedEvent then return handle_introspect_loaded(msg)
|
|
@@ -283,6 +287,13 @@ module LazyRails
|
|
|
283
287
|
end
|
|
284
288
|
|
|
285
289
|
def shutdown
|
|
290
|
+
unless @panel_busy.empty? || @force_quit
|
|
291
|
+
busy_names = @panel_busy.map { |t| t.to_s.capitalize }.join(", ")
|
|
292
|
+
@force_quit = true
|
|
293
|
+
set_flash("#{busy_names} still running. Press q again to quit (tasks will continue in background).")
|
|
294
|
+
return nil
|
|
295
|
+
end
|
|
296
|
+
|
|
286
297
|
@server.stop
|
|
287
298
|
@log_watcher.stop
|
|
288
299
|
quit
|
|
@@ -381,6 +392,7 @@ module LazyRails
|
|
|
381
392
|
if msg.key == :tab && msg.shift?
|
|
382
393
|
clear_panel_state
|
|
383
394
|
@focused_panel = (@focused_panel - 1) % @panels.size
|
|
395
|
+
@detail_viewport.goto_top
|
|
384
396
|
update_detail_content
|
|
385
397
|
return nil
|
|
386
398
|
end
|
|
@@ -392,23 +404,32 @@ module LazyRails
|
|
|
392
404
|
when :tab, :right, "l"
|
|
393
405
|
clear_panel_state
|
|
394
406
|
@focused_panel = (@focused_panel + 1) % @panels.size
|
|
407
|
+
@detail_viewport.goto_top
|
|
395
408
|
update_detail_content
|
|
396
409
|
when :left, "h"
|
|
397
410
|
clear_panel_state
|
|
398
411
|
@focused_panel = (@focused_panel - 1) % @panels.size
|
|
412
|
+
@detail_viewport.goto_top
|
|
399
413
|
update_detail_content
|
|
400
414
|
when "1".."9"
|
|
401
415
|
idx = msg.key.to_i - 1
|
|
402
416
|
if idx < @panels.size
|
|
403
417
|
@focused_panel = idx
|
|
418
|
+
@detail_viewport.goto_top
|
|
404
419
|
update_detail_content
|
|
405
420
|
end
|
|
406
421
|
when "j", :down
|
|
407
422
|
current_panel.move_cursor(1, panel_visible_height)
|
|
423
|
+
@detail_viewport.goto_top
|
|
408
424
|
update_detail_content
|
|
409
425
|
when "k", :up
|
|
410
426
|
current_panel.move_cursor(-1, panel_visible_height)
|
|
427
|
+
@detail_viewport.goto_top
|
|
411
428
|
update_detail_content
|
|
429
|
+
when "J"
|
|
430
|
+
@detail_viewport.scroll_down(1)
|
|
431
|
+
when "K"
|
|
432
|
+
@detail_viewport.scroll_up(1)
|
|
412
433
|
when :enter then return handle_enter
|
|
413
434
|
when "/" then start_filter
|
|
414
435
|
when "G" then show_generator_menu
|
|
@@ -421,6 +442,127 @@ module LazyRails
|
|
|
421
442
|
nil
|
|
422
443
|
end
|
|
423
444
|
|
|
445
|
+
# ─── Mouse handling ──────────────────────────────────
|
|
446
|
+
|
|
447
|
+
MOUSE_SCROLL_LINES = 3
|
|
448
|
+
|
|
449
|
+
def handle_mouse(msg)
|
|
450
|
+
if @help.visible?
|
|
451
|
+
handle_help_mouse(msg)
|
|
452
|
+
return nil
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
if @command_log_overlay.visible?
|
|
456
|
+
handle_command_log_mouse(msg)
|
|
457
|
+
return nil
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
# Ignore mouse during modal overlays
|
|
461
|
+
return nil if @welcome.visible? || @menu.visible? || @confirmation ||
|
|
462
|
+
@table_browser.visible? || @generator_wizard.visible? || @input_mode.active?
|
|
463
|
+
|
|
464
|
+
# Convert 1-based terminal coordinates to 0-based
|
|
465
|
+
mx = msg.x - 1
|
|
466
|
+
my = msg.y - 1
|
|
467
|
+
left_width = (@width * LEFT_WIDTH_RATIO).to_i
|
|
468
|
+
|
|
469
|
+
if mx < left_width
|
|
470
|
+
handle_left_pane_mouse(msg, my)
|
|
471
|
+
else
|
|
472
|
+
handle_right_pane_mouse(msg)
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
nil
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
def handle_left_pane_mouse(msg, my)
|
|
479
|
+
if msg.wheel?
|
|
480
|
+
delta = msg.button == Chamomile::MOUSE_WHEEL_DOWN ? 1 : -1
|
|
481
|
+
current_panel.move_cursor(delta, panel_visible_height)
|
|
482
|
+
@detail_viewport.goto_top
|
|
483
|
+
update_detail_content
|
|
484
|
+
elsif msg.button == Chamomile::MOUSE_LEFT && msg.press?
|
|
485
|
+
panel_idx = panel_index_at_y(my)
|
|
486
|
+
return unless panel_idx
|
|
487
|
+
|
|
488
|
+
changed = false
|
|
489
|
+
|
|
490
|
+
if panel_idx != @focused_panel
|
|
491
|
+
clear_panel_state
|
|
492
|
+
@focused_panel = panel_idx
|
|
493
|
+
changed = true
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
item_idx = item_index_at_y(panel_idx, my)
|
|
497
|
+
if item_idx && item_idx != current_panel.cursor
|
|
498
|
+
delta = item_idx - current_panel.cursor
|
|
499
|
+
current_panel.move_cursor(delta, panel_visible_height)
|
|
500
|
+
changed = true
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
if changed
|
|
504
|
+
@detail_viewport.goto_top
|
|
505
|
+
update_detail_content
|
|
506
|
+
end
|
|
507
|
+
end
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
def handle_right_pane_mouse(msg)
|
|
511
|
+
return unless msg.wheel?
|
|
512
|
+
|
|
513
|
+
if msg.button == Chamomile::MOUSE_WHEEL_DOWN
|
|
514
|
+
@detail_viewport.scroll_down(MOUSE_SCROLL_LINES)
|
|
515
|
+
else
|
|
516
|
+
@detail_viewport.scroll_up(MOUSE_SCROLL_LINES)
|
|
517
|
+
end
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
def handle_help_mouse(msg)
|
|
521
|
+
return unless msg.wheel?
|
|
522
|
+
|
|
523
|
+
if msg.button == Chamomile::MOUSE_WHEEL_DOWN
|
|
524
|
+
@help.handle_key(:down)
|
|
525
|
+
else
|
|
526
|
+
@help.handle_key(:up)
|
|
527
|
+
end
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
def handle_command_log_mouse(msg)
|
|
531
|
+
return unless msg.wheel?
|
|
532
|
+
|
|
533
|
+
if msg.button == Chamomile::MOUSE_WHEEL_DOWN
|
|
534
|
+
@command_log_overlay.handle_key(:down)
|
|
535
|
+
else
|
|
536
|
+
@command_log_overlay.handle_key(:up)
|
|
537
|
+
end
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
def panel_index_at_y(y)
|
|
541
|
+
heights = distribute_panel_heights(@height - 2)
|
|
542
|
+
cumulative = 0
|
|
543
|
+
heights.each_with_index do |h, i|
|
|
544
|
+
return i if y >= cumulative && y < cumulative + h
|
|
545
|
+
cumulative += h
|
|
546
|
+
end
|
|
547
|
+
nil
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def item_index_at_y(panel_index, y)
|
|
551
|
+
heights = distribute_panel_heights(@height - 2)
|
|
552
|
+
panel_start = heights[0...panel_index].sum
|
|
553
|
+
panel_h = heights[panel_index]
|
|
554
|
+
|
|
555
|
+
# Content area is inside the border (1 line top, 1 line bottom)
|
|
556
|
+
content_y = y - panel_start - 1
|
|
557
|
+
return nil if content_y < 0 || content_y >= panel_h - 2
|
|
558
|
+
|
|
559
|
+
panel = @panels[panel_index]
|
|
560
|
+
idx = panel.scroll_offset + content_y
|
|
561
|
+
return nil if idx >= panel.filtered_items.size
|
|
562
|
+
|
|
563
|
+
idx
|
|
564
|
+
end
|
|
565
|
+
|
|
424
566
|
def handle_table_browser_key(msg)
|
|
425
567
|
signal = @table_browser.handle_key(msg.key)
|
|
426
568
|
case signal
|
|
@@ -474,6 +616,11 @@ module LazyRails
|
|
|
474
616
|
# ─── Confirmation ─────────────────────────────────────
|
|
475
617
|
|
|
476
618
|
def start_confirmation(command, tier: nil, required_text: nil)
|
|
619
|
+
if @panel_busy.include?(current_panel.type)
|
|
620
|
+
set_flash("Command already running...")
|
|
621
|
+
return nil
|
|
622
|
+
end
|
|
623
|
+
|
|
477
624
|
tier ||= Confirmation.detect_tier(command)
|
|
478
625
|
return run_rails_cmd(command, current_panel.type) if tier == :green
|
|
479
626
|
|
|
@@ -9,6 +9,7 @@ module LazyRails
|
|
|
9
9
|
@visible = false
|
|
10
10
|
@cursor = 0
|
|
11
11
|
@detail = nil
|
|
12
|
+
@scroll_offset = 0
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
def visible? = @visible
|
|
@@ -17,6 +18,7 @@ module LazyRails
|
|
|
17
18
|
@visible = true
|
|
18
19
|
@cursor = 0
|
|
19
20
|
@detail = nil
|
|
21
|
+
@scroll_offset = 0
|
|
20
22
|
end
|
|
21
23
|
|
|
22
24
|
def hide
|
|
@@ -40,17 +42,38 @@ module LazyRails
|
|
|
40
42
|
@detail = nil
|
|
41
43
|
nil
|
|
42
44
|
when :enter
|
|
43
|
-
|
|
45
|
+
entry = @command_log.entries[@cursor]
|
|
46
|
+
@detail = @detail ? nil : entry
|
|
44
47
|
nil
|
|
45
48
|
when "q" then :quit
|
|
46
49
|
end
|
|
47
50
|
end
|
|
48
51
|
|
|
49
|
-
def render(width:)
|
|
52
|
+
def render(width:, height: nil)
|
|
50
53
|
return "No commands executed yet.\n\nPress L or Esc to close." if @command_log.empty?
|
|
51
54
|
|
|
52
55
|
header = Chamomile::Style.new.bold.render("Command Log")
|
|
53
|
-
|
|
56
|
+
footer = "j/k navigate | Enter detail | L or Esc close"
|
|
57
|
+
|
|
58
|
+
if height
|
|
59
|
+
# Reserve: header(1) + blank(1) + footer blank(1) + footer(1) = 4
|
|
60
|
+
usable = height - 4
|
|
61
|
+
list_height = if @detail
|
|
62
|
+
# Split: top portion for list, rest for detail
|
|
63
|
+
[(usable * 0.4).to_i, 3].max
|
|
64
|
+
else
|
|
65
|
+
[usable, 3].max
|
|
66
|
+
end
|
|
67
|
+
else
|
|
68
|
+
list_height = @command_log.size
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
clamp_scroll(list_height)
|
|
72
|
+
|
|
73
|
+
list = Views::CommandLogView.render_window(
|
|
74
|
+
@command_log, width: width - 4, selected: @cursor,
|
|
75
|
+
offset: @scroll_offset, limit: list_height
|
|
76
|
+
)
|
|
54
77
|
|
|
55
78
|
parts = [header, "", list]
|
|
56
79
|
|
|
@@ -61,8 +84,17 @@ module LazyRails
|
|
|
61
84
|
end
|
|
62
85
|
|
|
63
86
|
parts << ""
|
|
64
|
-
parts <<
|
|
87
|
+
parts << footer
|
|
65
88
|
parts.join("\n")
|
|
66
89
|
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def clamp_scroll(visible_height)
|
|
94
|
+
@scroll_offset = @cursor if @cursor < @scroll_offset
|
|
95
|
+
@scroll_offset = @cursor - visible_height + 1 if @cursor >= @scroll_offset + visible_height
|
|
96
|
+
max_offset = [@command_log.size - visible_height, 0].max
|
|
97
|
+
@scroll_offset = @scroll_offset.clamp(0, max_offset)
|
|
98
|
+
end
|
|
67
99
|
end
|
|
68
100
|
end
|
|
@@ -61,7 +61,10 @@ module LazyRails
|
|
|
61
61
|
|
|
62
62
|
def run_rails_cmd(command, panel_type)
|
|
63
63
|
display = command.is_a?(Array) ? command.join(" ") : command
|
|
64
|
+
@panel_busy << panel_type
|
|
65
|
+
@last_command_result.delete(panel_type)
|
|
64
66
|
set_flash("Running: #{display}...")
|
|
67
|
+
update_detail_content
|
|
65
68
|
project_dir = @project.dir
|
|
66
69
|
cmd(lambda {
|
|
67
70
|
result = CommandRunner.run(command, dir: project_dir)
|
|
@@ -70,7 +73,9 @@ module LazyRails
|
|
|
70
73
|
end
|
|
71
74
|
|
|
72
75
|
def run_test_file_cmd(test_file)
|
|
76
|
+
@panel_busy << :tests
|
|
73
77
|
set_flash("Running: #{test_file.path}...")
|
|
78
|
+
update_detail_content
|
|
74
79
|
project_dir = @project.dir
|
|
75
80
|
path = test_file.path
|
|
76
81
|
test_cmd = path.start_with?("spec/") ? ["bundle", "exec", "rspec", path] : ["bin/rails", "test", path]
|
|
@@ -7,6 +7,7 @@ module LazyRails
|
|
|
7
7
|
["Tab/Shift+Tab", "Cycle panels"],
|
|
8
8
|
["1-9", "Jump to panel"],
|
|
9
9
|
["j/k", "Scroll up/down"],
|
|
10
|
+
["J/K", "Scroll detail pane"],
|
|
10
11
|
["Enter", "Select / expand"],
|
|
11
12
|
["q", "Quit"],
|
|
12
13
|
["?", "Toggle help"],
|
|
@@ -55,11 +56,19 @@ module LazyRails
|
|
|
55
56
|
["f", "Cycle filter"],
|
|
56
57
|
["R", "Refresh"]
|
|
57
58
|
] },
|
|
59
|
+
{ section: "Rake", bindings: [
|
|
60
|
+
["Enter", "Run selected task"]
|
|
61
|
+
] },
|
|
58
62
|
{ section: "Gems", bindings: [
|
|
59
63
|
["Enter", "Show gem info"],
|
|
60
64
|
["u", "Update gem"],
|
|
61
65
|
["U", "Update all"],
|
|
62
66
|
["o", "Open homepage"]
|
|
67
|
+
] },
|
|
68
|
+
{ section: "Mouse", bindings: [
|
|
69
|
+
["Scroll wheel", "Scroll panels / detail"],
|
|
70
|
+
["Left click", "Focus panel / select item"],
|
|
71
|
+
["Shift+drag", "Select text (copy)"]
|
|
63
72
|
] }
|
|
64
73
|
].freeze
|
|
65
74
|
|
|
@@ -91,8 +91,12 @@ module LazyRails
|
|
|
91
91
|
end
|
|
92
92
|
|
|
93
93
|
def handle_command_finished(msg)
|
|
94
|
+
@panel_busy.delete(msg.panel)
|
|
95
|
+
@force_quit = false
|
|
96
|
+
@last_command_result[msg.panel] = msg.entry
|
|
94
97
|
@command_log.add(msg.entry)
|
|
95
98
|
set_flash("#{msg.entry.success? ? "\u2713" : "\u2717"} #{msg.entry.command} (#{msg.entry.duration_ms}ms)")
|
|
99
|
+
update_detail_content
|
|
96
100
|
|
|
97
101
|
case msg.panel
|
|
98
102
|
when :database, :models then return load_introspect_cmd
|
|
@@ -104,6 +108,8 @@ module LazyRails
|
|
|
104
108
|
end
|
|
105
109
|
|
|
106
110
|
def handle_test_finished(msg)
|
|
111
|
+
@panel_busy.delete(:tests)
|
|
112
|
+
@force_quit = false
|
|
107
113
|
@command_log.add(msg.command_entry) if msg.command_entry
|
|
108
114
|
tests_panel = find_panel(:tests)
|
|
109
115
|
idx = tests_panel.items.index { |f| f.path == msg.path }
|
|
@@ -193,7 +193,7 @@ module LazyRails
|
|
|
193
193
|
when "e"
|
|
194
194
|
item = current_panel.selected_item
|
|
195
195
|
if item && item.status == "scheduled"
|
|
196
|
-
start_confirmation("Dispatch scheduled job ##{item.id} now?", tier: :
|
|
196
|
+
start_confirmation("Dispatch scheduled job ##{item.id} now?", tier: :yellow)
|
|
197
197
|
@pending_job_action = PendingJobAction.new(action: :dispatch, job_id: item.id)
|
|
198
198
|
end
|
|
199
199
|
when "f"
|
|
@@ -540,7 +540,7 @@ module LazyRails
|
|
|
540
540
|
item = current_panel.selected_item
|
|
541
541
|
return unless item && item.status == "scheduled"
|
|
542
542
|
|
|
543
|
-
start_confirmation("Dispatch scheduled job ##{item.id} now?", tier: :
|
|
543
|
+
start_confirmation("Dispatch scheduled job ##{item.id} now?", tier: :yellow)
|
|
544
544
|
@pending_job_action = PendingJobAction.new(action: :dispatch, job_id: item.id)
|
|
545
545
|
end
|
|
546
546
|
|
data/lib/lazyrails/renderer.rb
CHANGED
|
@@ -34,8 +34,14 @@ module LazyRails
|
|
|
34
34
|
|
|
35
35
|
box_lines = box.lines
|
|
36
36
|
if box_lines.any?
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
running = @panel_busy.include?(panel.type)
|
|
38
|
+
title_label = running ? "#{panel.title} \u25CF" : panel.title
|
|
39
|
+
title = " #{title_label} "
|
|
40
|
+
title_styled = if running
|
|
41
|
+
Chamomile::Style.new.foreground("#e5c07b").bold.render(title)
|
|
42
|
+
else
|
|
43
|
+
Chamomile::Style.new.foreground(border_color).bold.render(title)
|
|
44
|
+
end
|
|
39
45
|
box_lines[0] = inject_title(box_lines[0], title_styled, title.length)
|
|
40
46
|
end
|
|
41
47
|
|
|
@@ -50,7 +56,8 @@ module LazyRails
|
|
|
50
56
|
def panel_cache_key(panel, focused, width, height)
|
|
51
57
|
base = [panel.items.object_id, panel.items.size, panel.cursor,
|
|
52
58
|
panel.scroll_offset, panel.filter_text, panel.loading,
|
|
53
|
-
panel.error, panel.title, focused, width, height
|
|
59
|
+
panel.error, panel.title, focused, width, height,
|
|
60
|
+
@panel_busy.include?(panel.type)]
|
|
54
61
|
case panel.type
|
|
55
62
|
when :status
|
|
56
63
|
base << @about_data.object_id
|
|
@@ -126,9 +133,21 @@ module LazyRails
|
|
|
126
133
|
|
|
127
134
|
box_lines = box.lines
|
|
128
135
|
if box_lines.any?
|
|
129
|
-
|
|
136
|
+
scrollable = @detail_viewport.total_line_count > @detail_viewport.height
|
|
137
|
+
title = if scrollable
|
|
138
|
+
pct = (@detail_viewport.scroll_percent * 100).round
|
|
139
|
+
" Detail (#{pct}%) "
|
|
140
|
+
else
|
|
141
|
+
" Detail "
|
|
142
|
+
end
|
|
130
143
|
title_styled = Chamomile::Style.new.foreground(border_color).bold.render(title)
|
|
131
144
|
box_lines[0] = inject_title(box_lines[0], title_styled, title.length)
|
|
145
|
+
|
|
146
|
+
if scrollable && box_lines.size > 1
|
|
147
|
+
footer = " \u2191\u2193 scroll "
|
|
148
|
+
footer_styled = Chamomile::Style.new.foreground(border_color).render(footer)
|
|
149
|
+
box_lines[-1] = inject_title(box_lines[-1], footer_styled, footer.length)
|
|
150
|
+
end
|
|
132
151
|
end
|
|
133
152
|
|
|
134
153
|
box_lines.join
|
|
@@ -151,7 +170,10 @@ module LazyRails
|
|
|
151
170
|
"Select a route."
|
|
152
171
|
end
|
|
153
172
|
when :database
|
|
154
|
-
if
|
|
173
|
+
if @panel_busy.include?(:database)
|
|
174
|
+
running_label = Chamomile::Style.new.foreground("#e5c07b").bold.render("\u25CF Running command...")
|
|
175
|
+
"#{running_label}\n\nOutput will appear when complete."
|
|
176
|
+
elsif item
|
|
155
177
|
Views::DatabaseView.render_detail(item, @project.dir, width: detail_width,
|
|
156
178
|
file_cache: @file_cache)
|
|
157
179
|
else
|
|
@@ -160,11 +182,18 @@ module LazyRails
|
|
|
160
182
|
when :models
|
|
161
183
|
item ? Views::ModelsView.render_detail(item, width: detail_width) : "Select a model."
|
|
162
184
|
when :tests
|
|
163
|
-
|
|
185
|
+
if @panel_busy.include?(:tests) && item
|
|
186
|
+
running_label = Chamomile::Style.new.foreground("#e5c07b").bold.render("\u25CF Running...")
|
|
187
|
+
"#{item.path}\n\n#{running_label}\n\nOutput will appear here when complete."
|
|
188
|
+
elsif item
|
|
189
|
+
Views::TestsView.render_detail(item, width: detail_width)
|
|
190
|
+
else
|
|
191
|
+
"Select a test file."
|
|
192
|
+
end
|
|
164
193
|
when :gems
|
|
165
194
|
item ? Views::GemsView.render_detail(item, width: detail_width) : "Select a gem."
|
|
166
195
|
when :rake
|
|
167
|
-
|
|
196
|
+
render_rake_detail(item, detail_width)
|
|
168
197
|
when :console
|
|
169
198
|
if item
|
|
170
199
|
Views::ConsoleView.render_detail(item,
|
|
@@ -196,8 +225,8 @@ module LazyRails
|
|
|
196
225
|
return Chamomile::Style.new.foreground("#e5c07b").width(@width).render(" #{@flash.message}".slice(0, @width)) if @flash.active?
|
|
197
226
|
|
|
198
227
|
hints = [
|
|
199
|
-
["Tab", "navigate"], ["j/k", "scroll"], ["
|
|
200
|
-
["
|
|
228
|
+
["Tab", "navigate"], ["j/k", "scroll"], ["J/K", "scroll detail"],
|
|
229
|
+
["Enter", "select"], ["Shift+drag", "copy text"], ["x", "actions"], ["?", "help"], ["q", "quit"]
|
|
201
230
|
]
|
|
202
231
|
bar = " #{hints.map do |key, desc|
|
|
203
232
|
styled_key = Chamomile::Style.new.bold.foreground('#b48ead').render(key)
|
|
@@ -260,7 +289,7 @@ module LazyRails
|
|
|
260
289
|
end
|
|
261
290
|
|
|
262
291
|
def render_command_log_box
|
|
263
|
-
content = @command_log_overlay.render(width: @width - 8)
|
|
292
|
+
content = @command_log_overlay.render(width: @width - 8, height: @height - 8)
|
|
264
293
|
|
|
265
294
|
box_width = [@width - 4, 60].max
|
|
266
295
|
box_width = [box_width, @width - 2].min
|
|
@@ -327,6 +356,18 @@ module LazyRails
|
|
|
327
356
|
end
|
|
328
357
|
end
|
|
329
358
|
|
|
359
|
+
def render_rake_detail(item, width)
|
|
360
|
+
return "Select a rake task." unless item
|
|
361
|
+
|
|
362
|
+
if @panel_busy.include?(:rake)
|
|
363
|
+
Views::RakeView.render_detail_running(item, width: width)
|
|
364
|
+
elsif @last_command_result[:rake]
|
|
365
|
+
Views::RakeView.render_detail_with_output(item, @last_command_result[:rake], width: width)
|
|
366
|
+
else
|
|
367
|
+
Views::RakeView.render_detail(item, width: width)
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
|
|
330
371
|
def inject_title(top_line, styled_title, title_visible_len)
|
|
331
372
|
ViewHelpers.inject_title(top_line, styled_title, title_visible_len)
|
|
332
373
|
end
|
|
@@ -145,10 +145,23 @@ module LazyRails
|
|
|
145
145
|
|
|
146
146
|
# Detect server startup across common Ruby servers
|
|
147
147
|
def server_ready?(line)
|
|
148
|
-
line.include?("Listening on") ||
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
148
|
+
ready = line.include?("Listening on") || # Puma
|
|
149
|
+
line.include?("listening on") || # Falcon, Vite
|
|
150
|
+
line.include?("port=") || # WEBrick ("port=3000")
|
|
151
|
+
line.include?("Ctrl-C to stop") # WEBrick, Thin
|
|
152
|
+
detect_actual_port(line) if ready
|
|
153
|
+
ready
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Extract the real port from server output so we display the correct one
|
|
157
|
+
def detect_actual_port(line)
|
|
158
|
+
# Puma: "Listening on http://0.0.0.0:3100"
|
|
159
|
+
# Falcon: "listening on http://localhost:3000"
|
|
160
|
+
# WEBrick: "port=3100"
|
|
161
|
+
if (match = line.match(/:(\d{2,5})\b/))
|
|
162
|
+
detected = match[1].to_i
|
|
163
|
+
@port = detected if detected.positive? && detected < 65_536
|
|
164
|
+
end
|
|
152
165
|
end
|
|
153
166
|
end
|
|
154
167
|
end
|
data/lib/lazyrails/version.rb
CHANGED
|
@@ -6,37 +6,51 @@ module LazyRails
|
|
|
6
6
|
DIM = "#666666"
|
|
7
7
|
|
|
8
8
|
def self.render(command_log, width:, selected: 0)
|
|
9
|
+
render_window(command_log, width: width, selected: selected,
|
|
10
|
+
offset: 0, limit: command_log.size)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.render_window(command_log, width:, selected: 0, offset: 0, limit: nil)
|
|
9
14
|
return "No commands executed yet." if command_log.empty?
|
|
10
15
|
|
|
11
|
-
command_log.entries
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
lines = []
|
|
19
|
-
if i == selected
|
|
20
|
-
lines << ViewHelpers.selected_style.render(text)
|
|
21
|
-
else
|
|
22
|
-
styled_icon = Chamomile::Style.new.foreground(color).render(icon)
|
|
23
|
-
lines << "#{styled_icon} #{cmd_text.ljust(width - 12)} #{duration}"
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
if entry.annotation
|
|
27
|
-
annotation_line = " \u21b3 #{entry.annotation}"
|
|
28
|
-
lines << Chamomile::Style.new.foreground(DIM).render(annotation_line)
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
if entry.undo_command
|
|
32
|
-
undo_line = " \u21b3 Undo: #{entry.undo_command.join(' ')}"
|
|
33
|
-
lines << Chamomile::Style.new.foreground(DIM).render(undo_line)
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
lines.join("\n")
|
|
16
|
+
entries = command_log.entries
|
|
17
|
+
limit ||= entries.size
|
|
18
|
+
visible = entries[offset, limit] || []
|
|
19
|
+
|
|
20
|
+
visible.each_with_index.map do |entry, i|
|
|
21
|
+
actual_index = i + offset
|
|
22
|
+
render_entry(entry, width: width, selected: actual_index == selected)
|
|
37
23
|
end.join("\n")
|
|
38
24
|
end
|
|
39
25
|
|
|
26
|
+
def self.render_entry(entry, width:, selected:)
|
|
27
|
+
icon = entry.success? ? "\u2713" : "\u2717"
|
|
28
|
+
color = entry.success? ? "#04b575" : "#ff6347"
|
|
29
|
+
duration = format("%.1fs", entry.duration_ms / 1000.0)
|
|
30
|
+
cmd_text = ViewHelpers.truncate(entry.command, width - 12)
|
|
31
|
+
text = "#{icon} #{cmd_text.ljust(width - 12)} #{duration}"
|
|
32
|
+
|
|
33
|
+
lines = []
|
|
34
|
+
if selected
|
|
35
|
+
lines << ViewHelpers.selected_style.render(text)
|
|
36
|
+
else
|
|
37
|
+
styled_icon = Chamomile::Style.new.foreground(color).render(icon)
|
|
38
|
+
lines << "#{styled_icon} #{cmd_text.ljust(width - 12)} #{duration}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
if entry.annotation
|
|
42
|
+
annotation_line = " \u21b3 #{entry.annotation}"
|
|
43
|
+
lines << Chamomile::Style.new.foreground(DIM).render(annotation_line)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
if entry.undo_command
|
|
47
|
+
undo_line = " \u21b3 Undo: #{entry.undo_command.join(' ')}"
|
|
48
|
+
lines << Chamomile::Style.new.foreground(DIM).render(undo_line)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
lines.join("\n")
|
|
52
|
+
end
|
|
53
|
+
|
|
40
54
|
def self.render_detail(entry, width:)
|
|
41
55
|
lines = []
|
|
42
56
|
lines << "Command: #{entry.command}"
|
|
@@ -59,19 +73,31 @@ module LazyRails
|
|
|
59
73
|
if entry.stdout && !entry.stdout.empty?
|
|
60
74
|
lines << ""
|
|
61
75
|
lines << "Output:"
|
|
62
|
-
lines << ("
|
|
63
|
-
lines <<
|
|
76
|
+
lines << ("\u2500" * [width - 4, 40].min)
|
|
77
|
+
truncated_output(entry.stdout, max_lines: 20).each { |l| lines << l }
|
|
64
78
|
end
|
|
65
79
|
|
|
66
80
|
if entry.stderr && !entry.stderr.empty?
|
|
67
81
|
lines << ""
|
|
68
82
|
lines << "Errors:"
|
|
69
|
-
lines << ("
|
|
70
|
-
lines <<
|
|
83
|
+
lines << ("\u2500" * [width - 4, 40].min)
|
|
84
|
+
truncated_output(entry.stderr, max_lines: 15).each { |l| lines << l }
|
|
71
85
|
end
|
|
72
86
|
|
|
73
87
|
lines.join("\n")
|
|
74
88
|
end
|
|
89
|
+
|
|
90
|
+
def self.truncated_output(text, max_lines:)
|
|
91
|
+
all_lines = text.lines.map(&:rstrip)
|
|
92
|
+
if all_lines.size > max_lines
|
|
93
|
+
shown = all_lines.first(max_lines)
|
|
94
|
+
remaining = all_lines.size - max_lines
|
|
95
|
+
shown << Chamomile::Style.new.foreground(DIM).render("... #{remaining} more lines (see full output in detail pane)")
|
|
96
|
+
shown
|
|
97
|
+
else
|
|
98
|
+
all_lines
|
|
99
|
+
end
|
|
100
|
+
end
|
|
75
101
|
end
|
|
76
102
|
end
|
|
77
103
|
end
|
|
@@ -36,12 +36,54 @@ module LazyRails
|
|
|
36
36
|
def self.render_detail(task, width:)
|
|
37
37
|
lines = []
|
|
38
38
|
lines << task.name
|
|
39
|
-
lines << ("
|
|
39
|
+
lines << ("\u2500" * [width - 4, 40].min)
|
|
40
40
|
lines << ""
|
|
41
41
|
lines << "Description: #{task.description.to_s.empty? ? '(none)' : task.description}"
|
|
42
42
|
lines << "Source: #{task.source.to_s.empty? ? '(unknown)' : task.source}"
|
|
43
43
|
lines << ""
|
|
44
|
-
lines << "
|
|
44
|
+
lines << Chamomile::Style.new.foreground("#666666").render("Press Enter to run")
|
|
45
|
+
lines.join("\n")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.render_detail_running(task, width:)
|
|
49
|
+
lines = []
|
|
50
|
+
lines << task.name
|
|
51
|
+
lines << ("\u2500" * [width - 4, 40].min)
|
|
52
|
+
lines << ""
|
|
53
|
+
lines << Chamomile::Style.new.foreground("#e5c07b").bold.render("\u25CF Running...")
|
|
54
|
+
lines << ""
|
|
55
|
+
lines << Chamomile::Style.new.foreground("#666666").render("Output will appear here when complete.")
|
|
56
|
+
lines.join("\n")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.render_detail_with_output(task, result, width:)
|
|
60
|
+
lines = []
|
|
61
|
+
lines << task.name
|
|
62
|
+
lines << ("\u2500" * [width - 4, 40].min)
|
|
63
|
+
lines << ""
|
|
64
|
+
lines << "Description: #{task.description.to_s.empty? ? '(none)' : task.description}"
|
|
65
|
+
lines << "Source: #{task.source.to_s.empty? ? '(unknown)' : task.source}"
|
|
66
|
+
lines << ""
|
|
67
|
+
lines << ("\u2500" * [width - 4, 40].min)
|
|
68
|
+
|
|
69
|
+
lines << if result.success?
|
|
70
|
+
Chamomile::Style.new.foreground("#98c379").bold.render("\u2713 #{result.command} (#{result.duration_ms}ms)")
|
|
71
|
+
else
|
|
72
|
+
Chamomile::Style.new.foreground("#ff6347").bold.render("\u2717 #{result.command} (exit #{result.exit_code})")
|
|
73
|
+
end
|
|
74
|
+
lines << ""
|
|
75
|
+
|
|
76
|
+
output = result.stdout.to_s.strip
|
|
77
|
+
error_output = result.stderr.to_s.strip
|
|
78
|
+
output.each_line { |l| lines << l.rstrip } unless output.empty?
|
|
79
|
+
unless error_output.empty?
|
|
80
|
+
lines << "" unless output.empty?
|
|
81
|
+
error_output.each_line do |l|
|
|
82
|
+
lines << Chamomile::Style.new.foreground("#ff6347").render(l.rstrip)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
lines << "(no output)" if output.empty? && error_output.empty?
|
|
86
|
+
|
|
45
87
|
lines.join("\n")
|
|
46
88
|
end
|
|
47
89
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lazyrails-tui
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jack Killilea
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-03-
|
|
11
|
+
date: 2026-03-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: chamomile
|