lazyrails-tui 0.1.1 → 0.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d83f062c430544fd78a2fd3b73247ea956efa0b0f02d356a9e688cbb4e7868e5
4
- data.tar.gz: 7e45addc99130b2d9d9abe904a7c230019506f0e9763965a73d08a6d4ea74df3
3
+ metadata.gz: 4ed79bb23e4ce7770dce3149046165a4c42938f8f5d1d3cbbc9b9232ca8d2e47
4
+ data.tar.gz: cf45f4fd01fe7ad2112752618419f8bb13e92bb8c52688c8afc7b36757529255
5
5
  SHA512:
6
- metadata.gz: c6d74f2dc976ffdcf2e789939b99b0cbbb969ae6df70a5f9c317259a58e425ee8c4e12072949987fc1f05701ff55141ee4d3d8c8da4f83f4574b8dfb49737b24
7
- data.tar.gz: 3bec5f74501ae41077a46da4d37df99c766fe9d6cb60bd7c70e12de30e6f3de37be5905a451247229fad6ca719dc6f1f448a8511138112f9452b1be6bcb5c1f1
6
+ metadata.gz: c49ef390414997a41c1cb011dc429d8d30604be747716531a3625975416b5046bc01b45726b29c04c7543e629d20feb18168b12a02d208f5801e32dcfa5893c3
7
+ data.tar.gz: 8707e7f02c11bf636c67c173c246dca40d80753b5ad740ee2c0cec28e5d7d82d6545a2b1cff972e234aa6ffb08f7ed67054d1ccdbf694271a6288c6fda3b112f
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A [lazygit](https://github.com/jesseduffield/lazygit)-style terminal UI for Rails. Everything the Rails CLI can do, in one split-pane interface.
4
4
 
5
- <!-- TODO: add screenshot or demo GIF here -->
5
+ ![Lazyrails overview](docs/overview.gif)
6
6
 
7
7
  Built with [Chamomile](https://github.com/xjackk/chamomile) (chamomile + petals + flourish).
8
8
 
@@ -49,6 +49,24 @@ LazyRails has 13 panels you can tab between. Each one wraps a chunk of the Rails
49
49
 
50
50
  If you add a `.lazyrails.yml`, you also get a Custom Commands panel (see below).
51
51
 
52
+ ### Routes
53
+
54
+ Browse all routes with color-coded HTTP verbs. Filter with `/`, group by controller with `g`, and drill into any route for details.
55
+
56
+ ![Routes filtering](docs/routes-filter.gif)
57
+
58
+ ### Generator Wizard
59
+
60
+ Press `G` to open the generator menu. A multi-step wizard walks you through naming, adding fields with type selection, and reviewing the command before it runs.
61
+
62
+ ![Generator wizard](docs/generate-model.gif)
63
+
64
+ ### Inline Console
65
+
66
+ Evaluate Ruby expressions directly from the Console panel without dropping to `rails console`. Results stay in your history.
67
+
68
+ ![Console evaluation](docs/console-eval.gif)
69
+
52
70
  ## Keybindings
53
71
 
54
72
  `?` opens the full help overlay inside the app. Here are the essentials:
data/lib/lazyrails/app.rb CHANGED
@@ -2,7 +2,11 @@
2
2
 
3
3
  module LazyRails
4
4
  # Custom messages for async operations
5
- TestFinishedMsg = Data.define(:path, :status, :output)
5
+ TestFinishedMsg = Data.define(:path, :status, :output, :command_entry) do
6
+ def initialize(command_entry: nil, **kwargs)
7
+ super
8
+ end
9
+ end
6
10
 
7
11
  class App
8
12
  include Chamomile::Model
@@ -66,6 +70,7 @@ module LazyRails
66
70
  @welcome = WelcomeOverlay.new
67
71
  @help = HelpOverlay.new
68
72
  @user_settings = UserSettings.new
73
+ @generator_wizard = GeneratorWizard.new
69
74
 
70
75
  # Data stores
71
76
  @introspect_data = nil
@@ -125,6 +130,7 @@ module LazyRails
125
130
  def update(msg)
126
131
  return handle_welcome(msg) if @welcome.visible? && msg.is_a?(Chamomile::KeyMsg)
127
132
  return handle_help_key(msg) if @help.visible? && msg.is_a?(Chamomile::KeyMsg)
133
+ return handle_generator_wizard(msg) if @generator_wizard.visible? && msg.is_a?(Chamomile::KeyMsg)
128
134
  return handle_confirmation(msg) if @confirmation
129
135
  return handle_input_mode(msg) if @input_mode.active?
130
136
 
@@ -174,6 +180,8 @@ module LazyRails
174
180
  overlay_on(base, render_command_log_box)
175
181
  elsif @table_browser.visible?
176
182
  overlay_on(base, render_table_browser_box)
183
+ elsif @generator_wizard.visible?
184
+ overlay_on(base, @generator_wizard.render(width: @width, height: @height))
177
185
  elsif @menu.visible?
178
186
  overlay_on(base, @menu.render(width: @width, height: @height))
179
187
  elsif @confirmation
@@ -254,6 +262,26 @@ module LazyRails
254
262
  nil
255
263
  end
256
264
 
265
+ def handle_generator_wizard(msg)
266
+ result = @generator_wizard.handle_key(msg.key)
267
+ return nil unless result.is_a?(Hash)
268
+
269
+ case result[:action]
270
+ when :run
271
+ panel_type = generator_panel_type(@generator_wizard.gen_type)
272
+ return run_rails_cmd(result[:command], panel_type)
273
+ when :cancel
274
+ nil
275
+ end
276
+ end
277
+
278
+ def generator_panel_type(gen_type)
279
+ case gen_type
280
+ when "migration" then :database
281
+ else :models
282
+ end
283
+ end
284
+
257
285
  def shutdown
258
286
  @server.stop
259
287
  @log_watcher.stop
@@ -76,7 +76,7 @@ module LazyRails
76
76
  cmd(lambda {
77
77
  result = CommandRunner.run(test_cmd, dir: project_dir)
78
78
  status = result.success? ? :passed : :failed
79
- TestFinishedMsg.new(path: path, status: status, output: result.stdout + result.stderr)
79
+ TestFinishedMsg.new(path: path, status: status, output: result.stdout + result.stderr, command_entry: result)
80
80
  })
81
81
  end
82
82
 
@@ -118,7 +118,7 @@ module LazyRails
118
118
  error: result.success? ? nil : result.stderr.strip,
119
119
  duration_ms: result.duration_ms
120
120
  )
121
- EvalFinishedMsg.new(entry: entry)
121
+ EvalFinishedMsg.new(entry: entry, command_entry: result)
122
122
  rescue StandardError => e
123
123
  entry = EvalEntry.new(expression: expression, result: nil, error: e.message, duration_ms: 0)
124
124
  EvalFinishedMsg.new(entry: entry)
@@ -0,0 +1,513 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LazyRails
4
+ class GeneratorWizard
5
+ COLUMN_TYPES = %w[
6
+ string integer text boolean float decimal
7
+ datetime date time binary references
8
+ ].freeze
9
+
10
+ CONTROLLER_ACTIONS = %w[index show new create edit update destroy].freeze
11
+
12
+ attr_reader :gen_type, :step
13
+
14
+ def initialize
15
+ @visible = false
16
+ @gen_type = nil
17
+ @gen_label = nil
18
+ @step = :name
19
+ @name = ""
20
+ @fields = [] # [{name: "email", type: "string"}, ...]
21
+ @methods = [] # ["welcome", "reset_password"]
22
+ @method_input = +""
23
+ @field_name_input = +""
24
+ @type_cursor = 0
25
+ @field_cursor = 0
26
+ @action_toggles = {} # {"index" => true, "show" => false, ...}
27
+ @action_cursor = 0
28
+ @editing_field_name = true # true = typing field name, false = picking type
29
+ @error = nil
30
+ end
31
+
32
+ def visible? = @visible
33
+
34
+ def show(gen_type:, gen_label:)
35
+ @visible = true
36
+ @gen_type = gen_type
37
+ @gen_label = gen_label
38
+ @step = :name
39
+ @name = +""
40
+ @fields = []
41
+ @methods = []
42
+ @method_input = +""
43
+ @field_name_input = +""
44
+ @type_cursor = 0
45
+ @field_cursor = 0
46
+ @action_toggles = CONTROLLER_ACTIONS.each_with_object({}) { |a, h| h[a] = false }
47
+ @action_cursor = 0
48
+ @editing_field_name = true
49
+ @error = nil
50
+ end
51
+
52
+ def hide
53
+ @visible = false
54
+ end
55
+
56
+ # Returns nil normally, or a Hash with :action when done
57
+ # { action: :run, command: ["bin/rails", "generate", ...] }
58
+ # { action: :cancel }
59
+ def handle_key(key)
60
+ @error = nil
61
+
62
+ case @step
63
+ when :name then handle_name_key(key)
64
+ when :fields then handle_fields_key(key)
65
+ when :actions then handle_actions_key(key)
66
+ when :methods then handle_methods_key(key)
67
+ when :review then handle_review_key(key)
68
+ end
69
+ end
70
+
71
+ def render(width:, height:)
72
+ return "" unless @visible
73
+
74
+ content = case @step
75
+ when :name then render_name_step
76
+ when :fields then render_fields_step
77
+ when :actions then render_actions_step
78
+ when :methods then render_methods_step
79
+ when :review then render_review_step
80
+ end
81
+
82
+ menu_width = [width * 0.6, 50].max.to_i
83
+ menu_width = [menu_width, width - 4].min
84
+
85
+ step_label = step_indicator
86
+ footer = render_footer
87
+
88
+ box = Flourish::Style.new
89
+ .width(menu_width)
90
+ .border(Flourish::Border::ROUNDED)
91
+ .border_foreground("#b48ead")
92
+ .padding(0, 1)
93
+ .render("#{content}\n\n#{footer}")
94
+
95
+ box_lines = box.lines
96
+ if box_lines.any?
97
+ title_text = " Generate #{@gen_label} #{step_label} "
98
+ title_styled = Flourish::Style.new.foreground("#b48ead").bold.render(title_text)
99
+ box_lines[0] = ViewHelpers.inject_title(box_lines[0], title_styled, title_text.length)
100
+ end
101
+
102
+ box_lines.join
103
+ end
104
+
105
+ private
106
+
107
+ # ─── Step indicators ───────────────────────────────
108
+
109
+ def step_indicator
110
+ steps = step_names
111
+ idx = steps.index(@step) || 0
112
+ "(#{idx + 1}/#{steps.size})"
113
+ end
114
+
115
+ def step_names
116
+ case @gen_type
117
+ when "model", "scaffold" then %i[name fields review]
118
+ when "migration" then %i[name fields review]
119
+ when "controller" then %i[name actions review]
120
+ when "mailer" then %i[name methods review]
121
+ when "job", "channel", "stimulus" then %i[name review]
122
+ else %i[name review]
123
+ end
124
+ end
125
+
126
+ def next_step
127
+ steps = step_names
128
+ idx = steps.index(@step) || 0
129
+ steps[idx + 1] || :review
130
+ end
131
+
132
+ # ─── Name step ─────────────────────────────────────
133
+
134
+ def handle_name_key(key)
135
+ case key
136
+ when :escape
137
+ hide
138
+ { action: :cancel }
139
+ when :enter
140
+ if @name.strip.empty?
141
+ @error = "Name cannot be empty"
142
+ nil
143
+ else
144
+ @step = next_step
145
+ nil
146
+ end
147
+ when :backspace
148
+ @name.chop!
149
+ nil
150
+ else
151
+ @name << key.to_s if key.is_a?(String) && key.length == 1
152
+ nil
153
+ end
154
+ end
155
+
156
+ def render_name_step
157
+ lines = []
158
+ lines << "Enter the #{@gen_label.downcase} name:"
159
+ lines << ""
160
+
161
+ prompt = "> #{@name}\u2588"
162
+ lines << Flourish::Style.new.bold.render(prompt)
163
+
164
+ lines << ""
165
+ lines << Flourish::Style.new.foreground("#666666").render(name_hint)
166
+
167
+ if @error
168
+ lines << ""
169
+ lines << Flourish::Style.new.foreground("#ff6347").render(@error)
170
+ end
171
+
172
+ lines.join("\n")
173
+ end
174
+
175
+ def name_hint
176
+ case @gen_type
177
+ when "model" then "e.g. User, BlogPost, Admin::Setting"
178
+ when "scaffold" then "e.g. Article, Product, Admin::User"
179
+ when "migration" then "e.g. AddStatusToOrders, CreateProducts"
180
+ when "controller" then "e.g. Articles, Users, Admin::Dashboard"
181
+ when "job" then "e.g. ProcessPayment, SendNewsletter"
182
+ when "mailer" then "e.g. UserMailer, OrderNotification"
183
+ when "channel" then "e.g. Chat, Notifications"
184
+ when "stimulus" then "e.g. toggle, dropdown, search"
185
+ else "Enter a name"
186
+ end
187
+ end
188
+
189
+ # ─── Fields step (model/scaffold/migration) ────────
190
+
191
+ def handle_fields_key(key)
192
+ if @editing_field_name
193
+ handle_field_name_key(key)
194
+ else
195
+ handle_field_type_key(key)
196
+ end
197
+ end
198
+
199
+ def handle_field_name_key(key)
200
+ case key
201
+ when :escape
202
+ if @field_name_input.empty?
203
+ @step = :name
204
+ else
205
+ @field_name_input = +""
206
+ end
207
+ nil
208
+ when :enter, :tab
209
+ if @field_name_input.strip.empty?
210
+ # No field name entered — move to review (fields optional for migrations)
211
+ if @fields.any? || @gen_type == "migration"
212
+ @step = :review
213
+ else
214
+ @error = "Add at least one field, or press Esc to go back"
215
+ end
216
+ else
217
+ @editing_field_name = false
218
+ @type_cursor = 0
219
+ end
220
+ nil
221
+ when :backspace
222
+ if @field_name_input.empty? && @fields.any?
223
+ @fields.pop
224
+ else
225
+ @field_name_input.chop!
226
+ end
227
+ nil
228
+ else
229
+ @field_name_input << key.to_s if key.is_a?(String) && key.length == 1
230
+ nil
231
+ end
232
+ end
233
+
234
+ def handle_field_type_key(key)
235
+ case key
236
+ when :escape, :backspace
237
+ @editing_field_name = true
238
+ nil
239
+ when "j", :down
240
+ @type_cursor = (@type_cursor + 1) % COLUMN_TYPES.size
241
+ nil
242
+ when "k", :up
243
+ @type_cursor = (@type_cursor - 1) % COLUMN_TYPES.size
244
+ nil
245
+ when :enter
246
+ @fields << { name: @field_name_input.strip, type: COLUMN_TYPES[@type_cursor] }
247
+ @field_name_input = +""
248
+ @editing_field_name = true
249
+ nil
250
+ else
251
+ nil
252
+ end
253
+ end
254
+
255
+ def render_fields_step
256
+ lines = []
257
+ lines << "Add fields to #{@name}:"
258
+ lines << ""
259
+
260
+ # Show existing fields
261
+ if @fields.any?
262
+ @fields.each_with_index do |f, i|
263
+ marker = Flourish::Style.new.foreground("#a3be8c").render("\u2713")
264
+ lines << " #{marker} #{f[:name]}:#{f[:type]}"
265
+ end
266
+ lines << ""
267
+ end
268
+
269
+ if @editing_field_name
270
+ lines << "Field name: #{@field_name_input}\u2588"
271
+ lines << ""
272
+ hint = @fields.empty? ? "Type a field name, then press Enter to pick its type" : "Type a field name, Enter to pick type, Enter with empty to finish"
273
+ lines << Flourish::Style.new.foreground("#666666").render(hint)
274
+ if @fields.any?
275
+ lines << Flourish::Style.new.foreground("#666666").render("Backspace with empty input to remove last field")
276
+ end
277
+ else
278
+ lines << "Pick type for '#{@field_name_input}':"
279
+ lines << ""
280
+ COLUMN_TYPES.each_with_index do |t, i|
281
+ if i == @type_cursor
282
+ lines << ViewHelpers.selected_style.render(" #{t} ")
283
+ else
284
+ lines << " #{t}"
285
+ end
286
+ end
287
+ end
288
+
289
+ if @error
290
+ lines << ""
291
+ lines << Flourish::Style.new.foreground("#ff6347").render(@error)
292
+ end
293
+
294
+ lines.join("\n")
295
+ end
296
+
297
+ # ─── Actions step (controller) ─────────────────────
298
+
299
+ def handle_actions_key(key)
300
+ case key
301
+ when :escape
302
+ @step = :name
303
+ nil
304
+ when :enter
305
+ @step = :review
306
+ nil
307
+ when "j", :down
308
+ @action_cursor = (@action_cursor + 1) % CONTROLLER_ACTIONS.size
309
+ nil
310
+ when "k", :up
311
+ @action_cursor = (@action_cursor - 1) % CONTROLLER_ACTIONS.size
312
+ nil
313
+ when " "
314
+ action = CONTROLLER_ACTIONS[@action_cursor]
315
+ @action_toggles[action] = !@action_toggles[action]
316
+ nil
317
+ when "a"
318
+ # Toggle all
319
+ all_on = CONTROLLER_ACTIONS.all? { |a| @action_toggles[a] }
320
+ CONTROLLER_ACTIONS.each { |a| @action_toggles[a] = !all_on }
321
+ nil
322
+ else
323
+ nil
324
+ end
325
+ end
326
+
327
+ def render_actions_step
328
+ lines = []
329
+ lines << "Select actions for #{@name}Controller:"
330
+ lines << ""
331
+
332
+ CONTROLLER_ACTIONS.each_with_index do |action, i|
333
+ checked = @action_toggles[action]
334
+ marker = checked ? Flourish::Style.new.foreground("#a3be8c").render("[x]") : "[ ]"
335
+ text = "#{marker} #{action}"
336
+ if i == @action_cursor
337
+ lines << ViewHelpers.selected_style.render(" #{Flourish::ANSI.strip(text)} ")
338
+ else
339
+ lines << " #{text}"
340
+ end
341
+ end
342
+
343
+ lines << ""
344
+ lines << Flourish::Style.new.foreground("#666666").render("Space toggle | a toggle all | Enter continue")
345
+
346
+ lines.join("\n")
347
+ end
348
+
349
+ # ─── Methods step (mailer) ─────────────────────────
350
+
351
+ def handle_methods_key(key)
352
+ case key
353
+ when :escape
354
+ if @method_input.empty?
355
+ @step = :name
356
+ else
357
+ @method_input = +""
358
+ end
359
+ nil
360
+ when :enter
361
+ if @method_input.strip.empty?
362
+ if @methods.any?
363
+ @step = :review
364
+ else
365
+ @error = "Add at least one method, or press Esc to go back"
366
+ end
367
+ else
368
+ @methods << @method_input.strip
369
+ @method_input = +""
370
+ end
371
+ nil
372
+ when :backspace
373
+ if @method_input.empty? && @methods.any?
374
+ @methods.pop
375
+ else
376
+ @method_input.chop!
377
+ end
378
+ nil
379
+ else
380
+ @method_input << key.to_s if key.is_a?(String) && key.length == 1
381
+ nil
382
+ end
383
+ end
384
+
385
+ def render_methods_step
386
+ lines = []
387
+ lines << "Add methods to #{@name}:"
388
+ lines << ""
389
+
390
+ if @methods.any?
391
+ @methods.each do |m|
392
+ marker = Flourish::Style.new.foreground("#a3be8c").render("\u2713")
393
+ lines << " #{marker} #{m}"
394
+ end
395
+ lines << ""
396
+ end
397
+
398
+ lines << "Method name: #{@method_input}\u2588"
399
+ lines << ""
400
+ hint = @methods.empty? ? "Type a method name and press Enter" : "Enter another method, or Enter with empty to finish"
401
+ lines << Flourish::Style.new.foreground("#666666").render(hint)
402
+ if @methods.any?
403
+ lines << Flourish::Style.new.foreground("#666666").render("Backspace with empty input to remove last method")
404
+ end
405
+
406
+ if @error
407
+ lines << ""
408
+ lines << Flourish::Style.new.foreground("#ff6347").render(@error)
409
+ end
410
+
411
+ lines.join("\n")
412
+ end
413
+
414
+ # ─── Review step ───────────────────────────────────
415
+
416
+ def handle_review_key(key)
417
+ case key
418
+ when :escape
419
+ # Go back to previous step
420
+ steps = step_names
421
+ idx = steps.index(:review) || 0
422
+ @step = idx > 0 ? steps[idx - 1] : :name
423
+ @editing_field_name = true if @step == :fields
424
+ nil
425
+ when :enter
426
+ hide
427
+ { action: :run, command: build_command }
428
+ else
429
+ nil
430
+ end
431
+ end
432
+
433
+ def render_review_step
434
+ cmd = build_command
435
+ cmd_str = cmd.join(" ")
436
+
437
+ lines = []
438
+ lines << Flourish::Style.new.foreground("#a3be8c").bold.render("Ready to generate!")
439
+ lines << ""
440
+
441
+ lines << "Command:"
442
+ lines << Flourish::Style.new.bold.render(" $ #{cmd_str}")
443
+ lines << ""
444
+
445
+ # Summary
446
+ case @gen_type
447
+ when "model", "scaffold"
448
+ lines << "Name: #{@name}"
449
+ if @fields.any?
450
+ lines << "Fields:"
451
+ @fields.each { |f| lines << " - #{f[:name]} (#{f[:type]})" }
452
+ end
453
+ when "migration"
454
+ lines << "Migration: #{@name}"
455
+ if @fields.any?
456
+ lines << "Columns:"
457
+ @fields.each { |f| lines << " - #{f[:name]} (#{f[:type]})" }
458
+ end
459
+ when "controller"
460
+ lines << "Controller: #{@name}"
461
+ selected = CONTROLLER_ACTIONS.select { |a| @action_toggles[a] }
462
+ if selected.any?
463
+ lines << "Actions: #{selected.join(', ')}"
464
+ end
465
+ when "mailer"
466
+ lines << "Mailer: #{@name}"
467
+ if @methods.any?
468
+ lines << "Methods: #{@methods.join(', ')}"
469
+ end
470
+ else
471
+ lines << "Name: #{@name}"
472
+ end
473
+
474
+ lines.join("\n")
475
+ end
476
+
477
+ def build_command
478
+ cmd = %W[bin/rails generate #{@gen_type} #{@name.strip}]
479
+
480
+ case @gen_type
481
+ when "model", "scaffold", "migration"
482
+ @fields.each { |f| cmd << "#{f[:name]}:#{f[:type]}" }
483
+ when "controller"
484
+ CONTROLLER_ACTIONS.each { |a| cmd << a if @action_toggles[a] }
485
+ when "mailer"
486
+ @methods.each { |m| cmd << m }
487
+ end
488
+
489
+ cmd
490
+ end
491
+
492
+ # ─── Footer ────────────────────────────────────────
493
+
494
+ def render_footer
495
+ case @step
496
+ when :name
497
+ "Enter continue | Esc cancel"
498
+ when :fields
499
+ if @editing_field_name
500
+ "Enter pick type | Esc back"
501
+ else
502
+ "j/k navigate | Enter select | Esc back"
503
+ end
504
+ when :actions
505
+ "Space toggle | a all | Enter continue | Esc back"
506
+ when :methods
507
+ "Enter add/continue | Esc back"
508
+ when :review
509
+ "Enter run | Esc go back"
510
+ end
511
+ end
512
+ end
513
+ end
@@ -8,6 +8,7 @@ module LazyRails
8
8
  @active = false
9
9
  @purpose = nil
10
10
  @input = nil
11
+ @label = ""
11
12
  end
12
13
 
13
14
  def active? = @active
@@ -39,12 +40,17 @@ module LazyRails
39
40
  @input&.view || ""
40
41
  end
41
42
 
43
+ def styled_label
44
+ @label
45
+ end
46
+
42
47
  private
43
48
 
44
49
  def activate(purpose, prompt:, placeholder:)
45
50
  @active = true
46
51
  @purpose = purpose
47
- @input = Petals::TextInput.new(prompt: prompt, placeholder: placeholder)
52
+ @label = prompt
53
+ @input = Petals::TextInput.new(prompt: "", placeholder: placeholder)
48
54
  @input.focus
49
55
  end
50
56
  end
@@ -104,6 +104,7 @@ module LazyRails
104
104
  end
105
105
 
106
106
  def handle_test_finished(msg)
107
+ @command_log.add(msg.command_entry) if msg.command_entry
107
108
  tests_panel = find_panel(:tests)
108
109
  idx = tests_panel.items.index { |f| f.path == msg.path }
109
110
  if idx
@@ -129,6 +130,7 @@ module LazyRails
129
130
  end
130
131
 
131
132
  def handle_eval_finished(msg)
133
+ @command_log.add(msg.command_entry) if msg.command_entry
132
134
  panel = find_panel(:console)
133
135
  @eval_history.unshift(msg.entry)
134
136
  @eval_history = @eval_history.first(50)
@@ -57,7 +57,7 @@ module LazyRails
57
57
  when "M"
58
58
  start_confirmation("bin/rails db:rollback", tier: :yellow)
59
59
  when "c"
60
- @input_mode.start_input(:migration_name, prompt: "Migration name: ", placeholder: "CreateUsers")
60
+ @generator_wizard.show(gen_type: "migration", gen_label: "Migration")
61
61
  when "d"
62
62
  migration = current_panel.selected_item
63
63
  start_confirmation("bin/rails db:migrate:down VERSION=#{migration.version}", tier: :yellow) if migration
@@ -104,7 +104,9 @@ module LazyRails
104
104
  end
105
105
 
106
106
  def handle_models_key(msg)
107
- @input_mode.start_input(:generate_model, prompt: "Model name: ", placeholder: "User name:string email:string") if msg.key == "g"
107
+ if msg.key == "g"
108
+ @generator_wizard.show(gen_type: "model", gen_label: "Model")
109
+ end
108
110
  nil
109
111
  end
110
112
 
@@ -322,8 +324,7 @@ module LazyRails
322
324
  when :server_port then @input_mode.start_input(:change_port, prompt: "Port: ", placeholder: "3000")
323
325
  when :db_migrate then return run_rails_cmd("bin/rails db:migrate", :database)
324
326
  when :db_rollback then start_confirmation("bin/rails db:rollback", tier: :yellow)
325
- when :db_create_migration then @input_mode.start_input(:migration_name, prompt: "Migration name: ",
326
- placeholder: "CreateUsers")
327
+ when :db_create_migration then @generator_wizard.show(gen_type: "migration", gen_label: "Migration")
327
328
  when :db_browse_tables then browse_tables_action
328
329
  when :db_migrate_down then migrate_down_action
329
330
  when :db_migrate_up then return migrate_up_action
@@ -338,8 +339,7 @@ module LazyRails
338
339
  when :gem_update_all then start_confirmation("bundle update", tier: :yellow)
339
340
  when :gem_open then return gem_open_action
340
341
  when :routes_toggle_group then toggle_route_grouping
341
- when :model_generate then @input_mode.start_input(:generate_model, prompt: "Model name: ",
342
- placeholder: "User name:string email:string")
342
+ when :model_generate then @generator_wizard.show(gen_type: "model", gen_label: "Model")
343
343
  when :console_eval then @input_mode.start_input(:eval_expression, prompt: "ruby> ", placeholder: "User.count")
344
344
  when :console_open then return exec("bin/rails", "console")
345
345
  when :credentials_decrypt then return decrypt_selected_credential
@@ -370,10 +370,6 @@ module LazyRails
370
370
  current_panel.filter_text = value
371
371
  current_panel.reset_cursor
372
372
  nil
373
- when :migration_name
374
- run_rails_cmd(%w[bin/rails generate migration] + value.split, :database) unless value.empty?
375
- when :generate_model
376
- run_rails_cmd(%w[bin/rails generate model] + value.split, :models) unless value.empty?
377
373
  when :eval_expression
378
374
  run_eval_cmd(value) unless value.empty?
379
375
  when :table_where
@@ -391,8 +387,6 @@ module LazyRails
391
387
  set_flash("Invalid port: #{value}")
392
388
  end
393
389
  nil
394
- else
395
- handle_generator_submit(value, purpose)
396
390
  end
397
391
  end
398
392
 
@@ -583,15 +577,8 @@ module LazyRails
583
577
  gt = App::GENERATOR_TYPES.find { |g| g[:type] == gen_type }
584
578
  return unless gt
585
579
 
586
- @input_mode.start_input(:"generate_#{gen_type}", prompt: "#{gt[:label]} args: ", placeholder: gt[:placeholder])
580
+ @generator_wizard.show(gen_type: gen_type, gen_label: gt[:label])
587
581
  end
588
582
 
589
- def handle_generator_submit(value, purpose)
590
- gen_match = purpose.to_s.match(/\Agenerate_(.+)\z/)
591
- return unless gen_match && !value.empty?
592
-
593
- gen_type = gen_match[1]
594
- run_rails_cmd(%W[bin/rails generate #{gen_type}] + value.split, :models)
595
- end
596
583
  end
597
584
  end
@@ -210,7 +210,19 @@ module LazyRails
210
210
  end
211
211
 
212
212
  def render_filter_bar
213
- Flourish::Style.new.width(@width).render(@input_mode.view)
213
+ label = Flourish::Style.new.bold.foreground("#b48ead").render(" #{@input_mode.styled_label}")
214
+ input = @input_mode.view
215
+ hints = Flourish::Style.new.foreground("#666666").render("Enter submit \u2502 Esc cancel ")
216
+
217
+ hints_len = "Enter submit | Esc cancel ".length
218
+ label_len = @input_mode.styled_label.length + 1
219
+ input_area = @width - label_len - hints_len
220
+
221
+ # Build: [label][input padding...][hints]
222
+ input_visible = Flourish::ANSI.printable_width(input)
223
+ padding = input_area > input_visible ? " " * (input_area - input_visible) : ""
224
+
225
+ Flourish::Style.new.width(@width).render("#{label}#{input}#{padding}#{hints}")
214
226
  end
215
227
 
216
228
  def render_confirmation_box
@@ -156,7 +156,11 @@ module LazyRails
156
156
  TestsLoadedMsg = Data.define(:files, :error)
157
157
  CommandFinishedMsg = Data.define(:entry, :panel)
158
158
  TableRowsLoadedMsg = Data.define(:table, :columns, :rows, :total, :error)
159
- EvalFinishedMsg = Data.define(:entry)
159
+ EvalFinishedMsg = Data.define(:entry, :command_entry) do
160
+ def initialize(command_entry: nil, **kwargs)
161
+ super
162
+ end
163
+ end
160
164
  CredentialsLoadedMsg = Data.define(:environment, :content, :error)
161
165
  MailersLoadedMsg = Data.define(:previews, :error)
162
166
  MailerPreviewLoadedMsg = Data.define(:preview, :subject, :to, :from, :body, :error)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LazyRails
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.2"
5
5
  end
data/lib/lazyrails.rb CHANGED
@@ -36,6 +36,7 @@ require_relative "lazyrails/input_mode"
36
36
  require_relative "lazyrails/welcome_overlay"
37
37
  require_relative "lazyrails/help_overlay"
38
38
  require_relative "lazyrails/user_settings"
39
+ require_relative "lazyrails/generator_wizard"
39
40
 
40
41
  # Parsers
41
42
  require_relative "lazyrails/parsers/schema"
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.1.1
4
+ version: 0.1.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-09 00:00:00.000000000 Z
11
+ date: 2026-03-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: chamomile
@@ -76,6 +76,7 @@ files:
76
76
  - lib/lazyrails/eval_entry.rb
77
77
  - lib/lazyrails/file_cache.rb
78
78
  - lib/lazyrails/flash.rb
79
+ - lib/lazyrails/generator_wizard.rb
79
80
  - lib/lazyrails/help_overlay.rb
80
81
  - lib/lazyrails/input_mode.rb
81
82
  - lib/lazyrails/introspect.rb