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 +4 -4
- data/README.md +19 -1
- data/lib/lazyrails/app.rb +29 -1
- data/lib/lazyrails/data_loader.rb +2 -2
- data/lib/lazyrails/generator_wizard.rb +513 -0
- data/lib/lazyrails/input_mode.rb +7 -1
- data/lib/lazyrails/message_handlers.rb +2 -0
- data/lib/lazyrails/panel_handlers.rb +7 -20
- data/lib/lazyrails/renderer.rb +13 -1
- data/lib/lazyrails/structs.rb +5 -1
- data/lib/lazyrails/version.rb +1 -1
- data/lib/lazyrails.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4ed79bb23e4ce7770dce3149046165a4c42938f8f5d1d3cbbc9b9232ca8d2e47
|
|
4
|
+
data.tar.gz: cf45f4fd01fe7ad2112752618419f8bb13e92bb8c52688c8afc7b36757529255
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
5
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
data/lib/lazyrails/input_mode.rb
CHANGED
|
@@ -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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
|
|
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 @
|
|
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 @
|
|
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
|
-
@
|
|
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
|
data/lib/lazyrails/renderer.rb
CHANGED
|
@@ -210,7 +210,19 @@ module LazyRails
|
|
|
210
210
|
end
|
|
211
211
|
|
|
212
212
|
def render_filter_bar
|
|
213
|
-
Flourish::Style.new.
|
|
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
|
data/lib/lazyrails/structs.rb
CHANGED
|
@@ -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)
|
data/lib/lazyrails/version.rb
CHANGED
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.
|
|
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-
|
|
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
|