ratamin 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 02ee6e23e9d84b754a71528330d35e2e19337d1dc4e9dc2573c0ead727e6f196
4
- data.tar.gz: cba72054dff87188c6c1764ff69ad78ecc0d11383138fa6068d5fb486016535a
3
+ metadata.gz: 443a14265f91ecb7a42d6ba90bfe3266fb6684978f4c2d9b323fe676969d2c11
4
+ data.tar.gz: 5227b26d004cad5fa090a7eff43947d41a6812e5e7f28a02b8633583a56a3a75
5
5
  SHA512:
6
- metadata.gz: 9516cf7d76cf5e85907cf6a4d5e0f2cee6d990b772259a7c86b970a47a1d5fd7d70e87bea8c544ec5b9d7ec8c382aa5bb0814f46665fee35b6ef027f006336ff
7
- data.tar.gz: 9d33dc214b7353fb81d867598ffaa5eb100a7e748dd4f247e833637f7df469803530fd715fd145f58f0acd8ef0ebbb0bdb70aca86ada03fcfd22e2aa3940e01d
6
+ metadata.gz: f53c415e467dfeb6cff17ca9bb584ee9b6248c8fb3243037c8ee87ab08b48134f881360e51a575e2186163da8ef3f9c74314c40d7812bc136132c731b85ed57a
7
+ data.tar.gz: 665b48695e5b0c74da01683528aa9c774676a83ca61bc5ad028eabd3641e6bd1fa8fb151397777f1e2e9daecdd9469809f73692a629d529927ff214a47a42b5f
@@ -8,27 +8,29 @@ Release the gem. If $ARGUMENTS is provided, that is the new version number. Othe
8
8
 
9
9
  Steps:
10
10
 
11
- 1. **Confirm version**: Read `lib/ratamin/version.rb`. If $ARGUMENTS differs from the current version, update the file and commit the change.
11
+ 1. **Confirm version**: Read `lib/ratamin/version.rb`. If $ARGUMENTS differs from the current version, update the file.
12
12
 
13
- 2. **Run tests**: `bundle exec rspec` abort if any tests fail.
13
+ 2. **Bundle**: Run `bundle` to update `Gemfile.lock` with the new version.
14
14
 
15
- 3. **Update CHANGELOG.md**: Add a new section for this version at the top (below the `## [Unreleased]` header if present), using Keep a Changelog format:
15
+ 3. **Run tests**: `bundle exec rspec` abort if any tests fail.
16
+
17
+ 4. **Update CHANGELOG.md**: Add a new section for this version at the top (below the `## [Unreleased]` header if present), using Keep a Changelog format:
16
18
  ```
17
19
  ## [X.Y.Z] - YYYY-MM-DD
18
20
  ### Added / Changed / Fixed
19
21
  - ...
20
22
  ```
21
- Ask the user what to put in the changelog if there is nothing obvious from recent commits. Commit the changelog update together with any version bump.
23
+ Ask the user what to put in the changelog if there is nothing obvious from recent commits. Commit `lib/ratamin/version.rb`, `Gemfile.lock`, and `CHANGELOG.md` together in a single commit.
22
24
 
23
- 4. **Build the gem**: `gem build ratamin.gemspec`
25
+ 5. **Build the gem**: `gem build ratamin.gemspec`
24
26
 
25
- 5. **Push to RubyGems**: `gem push ratamin-X.Y.Z.gem`
27
+ 6. **Push to RubyGems**: `gem push ratamin-X.Y.Z.gem`
26
28
 
27
- 6. **Create git tag**: `git tag vX.Y.Z`
29
+ 7. **Create git tag**: `git tag vX.Y.Z`
28
30
 
29
- 7. **Push commits and tag**: `git push origin main --tags`
31
+ 8. **Push commits and tag**: `git push origin main --tags`
30
32
 
31
- 8. **Create GitHub Release**: Extract the release notes for this version from CHANGELOG.md and run:
33
+ 9. **Create GitHub Release**: Extract the release notes for this version from CHANGELOG.md and run:
32
34
  ```
33
35
  gh release create vX.Y.Z --title "vX.Y.Z" --notes-file <(extracted notes)
34
36
  ```
data/CHANGELOG.md CHANGED
@@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.0] - 2026-03-15
11
+
12
+ ### Added
13
+ - Create new records with `n` key in table view
14
+ - `create_row(attributes)` method on the `DataSource` protocol
15
+ - Two-mode form UX: select mode (vim-style navigation) and inline edit mode
16
+ - Unsaved changes confirmation dialog when leaving a dirty form
17
+ - Ctrl+E opens `$EDITOR` for any field type (not just `:text`)
18
+ - Vim-style `j`/`k` field navigation in form select mode
19
+ - PageUp/PageDown pagination (replaces `n`/`p`)
20
+
21
+ ### Changed
22
+ - Form title is now dynamic ("New Record" vs "Edit Record")
23
+ - Removed duplicate help text from form body (shown only in status bar)
24
+ - Removed dead code from `TableView#compute_widths`
25
+
26
+ ### Fixed
27
+ - SQL logging no longer interferes with TUI in dev mode (suppressed for entire session)
28
+ - Esc in edit mode now discards field changes (Enter confirms)
29
+ - Save errors display inline instead of crashing
30
+ - Stale in-memory data no longer shown after failed save (record reloaded)
31
+ - Column labels use raw names instead of capitalized
32
+
10
33
  ## [0.2.0] - 2026-03-14
11
34
 
12
35
  ### Added
@@ -35,6 +58,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
35
58
  - RSpec test suite with in-memory SQLite for ActiveRecord integration tests
36
59
  - GitHub Actions CI on Ruby 3.3, 3.4, and 4.0
37
60
 
38
- [Unreleased]: https://github.com/onyxblade/ratamin/compare/v0.2.0...HEAD
61
+ [Unreleased]: https://github.com/onyxblade/ratamin/compare/v0.3.0...HEAD
62
+ [0.3.0]: https://github.com/onyxblade/ratamin/compare/v0.2.0...v0.3.0
39
63
  [0.2.0]: https://github.com/onyxblade/ratamin/compare/v0.1.0...v0.2.0
40
64
  [0.1.0]: https://github.com/onyxblade/ratamin/releases/tag/v0.1.0
data/README.md CHANGED
@@ -14,16 +14,9 @@ gem "ratamin"
14
14
 
15
15
  ## Usage
16
16
 
17
- ### Rails integration (recommended)
17
+ ### Rails integration (automatic)
18
18
 
19
- Add the Railtie to an initializer to unlock `Model.ratamin` and `scope.ratamin` across your entire app:
20
-
21
- ```ruby
22
- # config/initializers/ratamin.rb
23
- require "ratamin/railtie"
24
- ```
25
-
26
- Then define which columns to show in your model:
19
+ In a Rails app, `Model.ratamin` and `scope.ratamin` are available automatically — no configuration needed. Just add the gem and define which columns to show in your model:
27
20
 
28
21
  ```ruby
29
22
  class User < ApplicationRecord
@@ -44,7 +37,7 @@ User.where(role: "admin").ratamin # filtered scope
44
37
 
45
38
  Column type (`:text` vs `:string`) and editability (`id`, `created_at`, `updated_at` are read-only by default) are inferred from the schema. Any option explicitly passed to `column` overrides the inferred default.
46
39
 
47
- Without the Railtie, `scope.ratamin` is unavailable. You can still call `User.ratamin` after manually including `Ratamin::ModelMixin` in the model.
40
+ To opt out of the Rails integration, use `require: false` in your Gemfile and `require "ratamin"` manually — `scope.ratamin` will then be unavailable, but the core API still works.
48
41
 
49
42
  ### Manual usage (without Railtie)
50
43
 
@@ -92,19 +85,35 @@ Ratamin::App.new(ds).run
92
85
  | `k` / `Up` | Previous row |
93
86
  | `g` | First row |
94
87
  | `G` | Last row |
95
- | `Enter` | Edit selected record |
88
+ | `l` / `Enter` | Edit selected record |
96
89
  | `r` | Reload data |
97
- | `q` | Quit |
90
+ | `PageDown` | Next page |
91
+ | `PageUp` | Previous page |
92
+ | `Esc` / `q` | Quit |
98
93
 
99
- ### Form view
94
+ ### Form view — select mode
100
95
 
101
96
  | Key | Action |
102
97
  |-----|--------|
98
+ | `j` / `Down` | Next field |
99
+ | `k` / `Up` | Previous field |
103
100
  | `Tab` | Next field |
104
101
  | `Shift+Tab` | Previous field |
105
- | `Enter` | Save |
106
- | `Esc` | Cancel |
107
- | `e` | Open `$EDITOR` (on `:text` fields) |
102
+ | `l` / `Enter` | Edit field |
103
+ | `Ctrl+E` | Open `$EDITOR` |
104
+ | `s` | Save |
105
+ | `h` / `Esc` | Cancel (back to table) |
106
+
107
+ ### Form view — edit mode
108
+
109
+ | Key | Action |
110
+ |-----|--------|
111
+ | `Enter` | Confirm and move to next field |
112
+ | `Esc` | Discard changes and exit edit |
113
+ | `Ctrl+E` | Open `$EDITOR` |
114
+ | `Left` / `Right` | Move cursor |
115
+ | `Home` / `End` | Jump to start/end |
116
+ | `Backspace` / `Delete` | Delete character |
108
117
 
109
118
  ## Architecture
110
119
 
data/lib/ratamin/app.rb CHANGED
@@ -9,17 +9,20 @@ module Ratamin
9
9
  @table_view = TableView.new(data_source)
10
10
  @form_state = nil
11
11
  @form_view = nil
12
+ @form_action = nil
12
13
  @mode = :table
13
14
  end
14
15
 
15
16
  def run
16
- RatatuiRuby.run do |tui|
17
- loop do
18
- tui.draw { |frame| render(tui, frame) }
19
-
20
- event = tui.poll_event
21
- result = handle_event(event)
22
- break if result == :quit
17
+ @data_source.with_silenced_output do
18
+ RatatuiRuby.run do |tui|
19
+ loop do
20
+ tui.draw { |frame| render(tui, frame) }
21
+
22
+ event = tui.poll_event
23
+ result = handle_event(event)
24
+ break if result == :quit
25
+ end
23
26
  end
24
27
  end
25
28
  end
@@ -55,13 +58,20 @@ module Ratamin
55
58
  end
56
59
 
57
60
  def table_help
58
- help = " q:quit j/↓:down k/↑:up g:first G:last enter:edit r:reload"
59
- help += " n:next page p:prev page" if @data_source.paginated?
61
+ help = " Esc/q:quit j/↓:down k/↑:up g:first G:last l/Enter:edit n:new r:reload"
62
+ help += " PgDn:next PgUp:prev" if @data_source.paginated?
60
63
  help + " "
61
64
  end
62
65
 
63
66
  def form_help
64
- " Tab:next field Shift+Tab:prev Enter:save Esc:cancel "
67
+ case @form_state&.mode
68
+ when :edit
69
+ " Esc:discard Enter:confirm Ctrl+E:editor "
70
+ when :confirm_exit
71
+ " ←/→:switch Enter:confirm Esc:back "
72
+ else
73
+ " s:save h/Esc:back j/k:navigate l/Enter:edit Ctrl+E:editor "
74
+ end
65
75
  end
66
76
 
67
77
  def handle_event(event)
@@ -73,7 +83,7 @@ module Ratamin
73
83
 
74
84
  def handle_table_event(event)
75
85
  case event
76
- in {type: :key, code: "q"} | {type: :key, code: "c", modifiers: ["ctrl"]}
86
+ in {type: :key, code: "q"} | {type: :key, code: "esc"} | {type: :key, code: "c", modifiers: ["ctrl"]}
77
87
  :quit
78
88
  in {type: :key, code: "j"} | {type: :key, code: "down"}
79
89
  @table_view.select_next
@@ -87,14 +97,17 @@ module Ratamin
87
97
  in {type: :key, code: "G"} | {type: :key, code: "g", modifiers: ["shift"]}
88
98
  @table_view.select_last
89
99
  nil
90
- in {type: :key, code: "enter"}
100
+ in {type: :key, code: "enter"} | {type: :key, code: "l"}
91
101
  enter_form
92
102
  nil
93
- in {type: :key, code: "n"} if @data_source.paginated?
103
+ in {type: :key, code: "n"}
104
+ enter_new_form
105
+ nil
106
+ in {type: :key, code: "page_down"} if @data_source.paginated?
94
107
  @data_source.next_page
95
108
  @table_view.reset_selection
96
109
  nil
97
- in {type: :key, code: "p"} if @data_source.paginated?
110
+ in {type: :key, code: "page_up"} if @data_source.paginated?
98
111
  @data_source.prev_page
99
112
  @table_view.reset_selection
100
113
  nil
@@ -126,6 +139,15 @@ module Ratamin
126
139
 
127
140
  @form_state = FormState.new(@data_source.columns, row)
128
141
  @form_view = FormView.new(@form_state)
142
+ @form_action = :edit
143
+ @mode = :form
144
+ end
145
+
146
+ def enter_new_form
147
+ empty_row = @data_source.columns.each_with_object({}) { |col, h| h[col.key] = "" }
148
+ @form_state = FormState.new(@data_source.columns, empty_row)
149
+ @form_view = FormView.new(@form_state, title: " New Record ")
150
+ @form_action = :create
129
151
  @mode = :form
130
152
  end
131
153
 
@@ -133,17 +155,27 @@ module Ratamin
133
155
  @mode = :table
134
156
  @form_state = nil
135
157
  @form_view = nil
158
+ @form_action = nil
136
159
  end
137
160
 
138
161
  def save_form
139
- idx = @table_view.selected_index
140
- return unless idx
162
+ result = if @form_action == :create
163
+ @data_source.create_row(@form_state.changes)
164
+ else
165
+ idx = @table_view.selected_index
166
+ return unless idx
167
+ @data_source.update_row(idx, @form_state.changes)
168
+ end
141
169
 
142
- result = @data_source.update_row(idx, @form_state.changes)
143
170
  if result.ok?
171
+ if @form_action == :create
172
+ @data_source.reload!
173
+ @table_view.reset_selection
174
+ end
144
175
  exit_form
145
176
  else
146
177
  @form_state.set_errors(result.errors)
178
+ @form_state.reset_mode
147
179
  end
148
180
  end
149
181
 
@@ -22,6 +22,11 @@ module Ratamin
22
22
  UpdateResult.success
23
23
  end
24
24
 
25
+ def create_row(attributes)
26
+ @rows << attributes.dup
27
+ UpdateResult.success
28
+ end
29
+
25
30
  def reload!
26
31
  # no-op for array source
27
32
  end
@@ -3,7 +3,7 @@
3
3
  module Ratamin
4
4
  Column = Data.define(:key, :label, :width, :type, :editable) do
5
5
  def initialize(key:, label: nil, width: nil, type: :string, editable: true)
6
- super(key: key, label: label || key.to_s.capitalize, width: width, type: type, editable: editable)
6
+ super(key: key, label: label || key.to_s, width: width, type: type, editable: editable)
7
7
  end
8
8
  end
9
9
  end
@@ -7,6 +7,7 @@ module Ratamin
7
7
  # #rows -> [Hash] (current page of data)
8
8
  # #row_count -> Integer
9
9
  # #update_row(index, changes) -> UpdateResult
10
+ # #create_row(attributes) -> UpdateResult
10
11
  # #reload! -> void
11
12
  #
12
13
  # Pagination (optional, when #paginated? returns true):
@@ -20,5 +21,9 @@ module Ratamin
20
21
  def paginated?
21
22
  false
22
23
  end
24
+
25
+ def with_silenced_output
26
+ yield
27
+ end
23
28
  end
24
29
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Ratamin
4
4
  class FormState
5
- attr_reader :columns, :values, :field_index, :cursor_positions, :errors
5
+ attr_reader :columns, :values, :field_index, :cursor_positions, :errors, :mode, :confirm_selection
6
6
 
7
7
  def initialize(columns, row)
8
8
  @columns = columns
@@ -11,6 +11,7 @@ module Ratamin
11
11
  @field_index = first_editable_index || 0
12
12
  @cursor_positions = @values.map(&:length)
13
13
  @errors = []
14
+ @mode = :select
14
15
  end
15
16
 
16
17
  def errors?
@@ -25,47 +26,19 @@ module Ratamin
25
26
  @errors = []
26
27
  end
27
28
 
28
- def handle_event(event)
29
- # Enter/Esc always work, even when all fields are read-only.
30
- case event
31
- in {type: :key, code: "enter"}
32
- return :save
33
- in {type: :key, code: "esc"}
34
- return :cancel
35
- else
36
- nil
37
- end
38
-
39
- case event
40
- in {type: :key, code: "tab", modifiers: []}
41
- return next_field
42
- in {type: :key, code: "backtab"} | {type: :key, code: "tab", modifiers: ["shift"]}
43
- return prev_field
44
- else
45
- nil
46
- end
29
+ def reset_mode
30
+ @mode = :select
31
+ end
47
32
 
48
- return nil unless current_column&.editable
33
+ def dirty?
34
+ changes.any?
35
+ end
49
36
 
50
- case event
51
- in {type: :key, code: "e"} if current_column.type == :text
52
- return :editor
53
- in {type: :key, code: "backspace"}
54
- handle_backspace
55
- in {type: :key, code: "delete"}
56
- handle_delete
57
- in {type: :key, code: "left"}
58
- move_cursor_left
59
- in {type: :key, code: "right"}
60
- move_cursor_right
61
- in {type: :key, code: "home"}
62
- @cursor_positions[@field_index] = 0
63
- in {type: :key, code: "end"}
64
- @cursor_positions[@field_index] = @values[@field_index].length
65
- in {type: :key, code: c, modifiers: []} if c.length == 1
66
- insert_char(c)
67
- else
68
- nil
37
+ def handle_event(event)
38
+ case @mode
39
+ when :select then handle_select_event(event)
40
+ when :edit then handle_edit_event(event)
41
+ when :confirm_exit then handle_confirm_exit_event(event)
69
42
  end
70
43
  end
71
44
 
@@ -142,6 +115,92 @@ module Ratamin
142
115
 
143
116
  private
144
117
 
118
+ def handle_select_event(event)
119
+ case event
120
+ in {type: :key, code: "esc"} | {type: :key, code: "h", modifiers: []}
121
+ if dirty?
122
+ @confirm_selection = 0
123
+ @mode = :confirm_exit
124
+ else
125
+ return :cancel
126
+ end
127
+ in {type: :key, code: "s", modifiers: []}
128
+ return :save
129
+ in {type: :key, code: "e", modifiers: ["ctrl"]}
130
+ return :editor if current_column&.editable
131
+ in {type: :key, code: "enter"} | {type: :key, code: "l", modifiers: []}
132
+ enter_edit_mode
133
+ in {type: :key, code: "j", modifiers: []} | {type: :key, code: "down"}
134
+ next_field
135
+ in {type: :key, code: "k", modifiers: []} | {type: :key, code: "up"}
136
+ prev_field
137
+ in {type: :key, code: "tab", modifiers: []}
138
+ next_field
139
+ in {type: :key, code: "backtab"} | {type: :key, code: "tab", modifiers: ["shift"]}
140
+ prev_field
141
+ else
142
+ nil
143
+ end
144
+ end
145
+
146
+ def handle_edit_event(event)
147
+ case event
148
+ in {type: :key, code: "esc"}
149
+ discard_edit
150
+ in {type: :key, code: "enter"}
151
+ @mode = :select
152
+ in {type: :key, code: "e", modifiers: ["ctrl"]}
153
+ return :editor
154
+ in {type: :key, code: "backspace"}
155
+ handle_backspace
156
+ in {type: :key, code: "delete"}
157
+ handle_delete
158
+ in {type: :key, code: "left"}
159
+ move_cursor_left
160
+ in {type: :key, code: "right"}
161
+ move_cursor_right
162
+ in {type: :key, code: "home"}
163
+ @cursor_positions[@field_index] = 0
164
+ in {type: :key, code: "end"}
165
+ @cursor_positions[@field_index] = @values[@field_index].length
166
+ in {type: :key, code: c, modifiers: []} if c.length == 1
167
+ insert_char(c)
168
+ else
169
+ nil
170
+ end
171
+ end
172
+
173
+ def handle_confirm_exit_event(event)
174
+ case event
175
+ in {type: :key, code: "enter"}
176
+ @confirm_selection == 0 ? :save : :cancel
177
+ in {type: :key, code: "h", modifiers: []} | {type: :key, code: "left"} |
178
+ {type: :key, code: "l", modifiers: []} | {type: :key, code: "right"} |
179
+ {type: :key, code: "tab"}
180
+ @confirm_selection = 1 - @confirm_selection
181
+ nil
182
+ in {type: :key, code: "esc"}
183
+ @mode = :select
184
+ nil
185
+ else
186
+ nil
187
+ end
188
+ end
189
+
190
+ def enter_edit_mode
191
+ return unless current_column&.editable
192
+ @edit_snapshot = [@values[@field_index].dup, @cursor_positions[@field_index]]
193
+ @mode = :edit
194
+ end
195
+
196
+ def discard_edit
197
+ if @edit_snapshot
198
+ @values[@field_index], @cursor_positions[@field_index] = @edit_snapshot
199
+ @edit_snapshot = nil
200
+ end
201
+ @mode = :select
202
+ end
203
+
145
204
  def any_editable?
146
205
  columns.any?(&:editable)
147
206
  end
@@ -2,16 +2,17 @@
2
2
 
3
3
  module Ratamin
4
4
  class FormView
5
- attr_reader :state
5
+ attr_reader :state, :title
6
6
 
7
- def initialize(state)
7
+ def initialize(state, title: " Edit Record ")
8
8
  @state = state
9
+ @title = title
9
10
  end
10
11
 
11
12
  def render(tui, frame, area)
12
13
  border_style = state.errors? ? {fg: :red} : {fg: :yellow}
13
14
  inner_block = tui.block(
14
- title: " Edit Record ",
15
+ title: title,
15
16
  borders: [:all],
16
17
  border_type: :rounded,
17
18
  border_style: border_style
@@ -22,14 +23,8 @@ module Ratamin
22
23
  columns = state.columns
23
24
 
24
25
  constraints = []
25
- # Error lines
26
- if state.errors?
27
- constraints << RatatuiRuby::Layout::Constraint.length(state.errors.length + 1)
28
- end
29
- # Field rows
26
+ constraints << RatatuiRuby::Layout::Constraint.length(state.errors.length + 1) if state.errors?
30
27
  columns.each { constraints << RatatuiRuby::Layout::Constraint.length(1) }
31
- # Help line + spacer
32
- constraints << RatatuiRuby::Layout::Constraint.length(1)
33
28
  constraints << RatatuiRuby::Layout::Constraint.fill(1)
34
29
 
35
30
  regions = RatatuiRuby::Layout::Layout.split(
@@ -40,11 +35,9 @@ module Ratamin
40
35
 
41
36
  region_idx = 0
42
37
 
43
- # Render errors if present
44
38
  if state.errors?
45
39
  error_text = state.errors.map { |e| " ! #{e}" }.join("\n")
46
- error_widget = tui.paragraph(text: error_text, style: {fg: :red, modifiers: [:bold]})
47
- frame.render_widget(error_widget, regions[region_idx])
40
+ frame.render_widget(tui.paragraph(text: error_text, style: {fg: :red, modifiers: [:bold]}), regions[region_idx])
48
41
  region_idx += 1
49
42
  end
50
43
 
@@ -52,10 +45,11 @@ module Ratamin
52
45
 
53
46
  columns.each_with_index do |col, i|
54
47
  field_area = regions[region_idx + i]
55
- label_area, value_area = RatatuiRuby::Layout::Layout.split(
48
+ marker_area, label_area, value_area = RatatuiRuby::Layout::Layout.split(
56
49
  field_area,
57
50
  direction: :horizontal,
58
51
  constraints: [
52
+ RatatuiRuby::Layout::Constraint.length(2),
59
53
  RatatuiRuby::Layout::Constraint.length(label_width),
60
54
  RatatuiRuby::Layout::Constraint.fill(1)
61
55
  ]
@@ -64,34 +58,93 @@ module Ratamin
64
58
  active = i == state.field_index
65
59
 
66
60
  if col.editable
61
+ in_edit = active && state.mode == :edit
62
+ marker = active ? (in_edit ? "» " : "> ") : " "
63
+ marker_style = active ? {fg: :yellow} : {fg: :dark_gray}
67
64
  label_style = active ? {fg: :yellow, modifiers: [:bold]} : {fg: :dark_gray}
65
+
66
+ frame.render_widget(tui.paragraph(text: marker, style: marker_style), marker_area)
68
67
  frame.render_widget(tui.paragraph(text: "#{col.label}: ", style: label_style), label_area)
69
68
 
70
69
  display_value = if col.type == :text && state.values[i].length > 40
71
- "[press 'e' to edit in $EDITOR]"
70
+ "[Ctrl+E to edit in $EDITOR]"
72
71
  else
73
72
  state.values[i]
74
73
  end
75
74
 
76
- value_style = active ? {fg: :white, modifiers: [:underlined]} : {fg: :gray}
75
+ value_style = in_edit ? {fg: :white, modifiers: [:underlined]} : (active ? {fg: :yellow} : {fg: :gray})
77
76
  frame.render_widget(tui.paragraph(text: display_value, style: value_style), value_area)
78
77
 
79
- if active && col.type != :text
78
+ if in_edit && !(col.type == :text && state.values[i].length > 40)
80
79
  cursor_x = value_area.x + [state.cursor_positions[i], value_area.width - 1].min
81
80
  frame.set_cursor_position(cursor_x, value_area.y)
82
81
  end
83
82
  else
83
+ frame.render_widget(tui.paragraph(text: " ", style: {fg: :dark_gray}), marker_area)
84
84
  frame.render_widget(tui.paragraph(text: "#{col.label}: ", style: {fg: :dark_gray}), label_area)
85
85
  frame.render_widget(tui.paragraph(text: state.values[i], style: {fg: :dark_gray, modifiers: [:dim]}), value_area)
86
86
  end
87
87
  end
88
88
 
89
- help_area = regions[region_idx + columns.length]
90
- help = tui.paragraph(
91
- text: " Tab:next Shift+Tab:prev Enter:save Esc:cancel e:editor(text) ",
92
- style: {fg: :dark_gray}
89
+ render_confirm_dialog(tui, frame, area) if state.mode == :confirm_exit
90
+ end
91
+
92
+ private
93
+
94
+ def render_confirm_dialog(tui, frame, area)
95
+ # Clear overlay area
96
+ dialog_w = [36, area.width - 4].min
97
+ dialog_h = 5
98
+ dialog_x = area.x + (area.width - dialog_w) / 2
99
+ dialog_y = area.y + (area.height - dialog_h) / 2
100
+
101
+ dialog_area = RatatuiRuby::Layout::Rect.new(x: dialog_x, y: dialog_y, width: dialog_w, height: dialog_h)
102
+ frame.render_widget(tui.clear, dialog_area)
103
+
104
+ dialog_block = tui.block(
105
+ title: " Unsaved changes ",
106
+ borders: [:all],
107
+ border_type: :rounded,
108
+ border_style: {fg: :yellow}
109
+ )
110
+ inner = dialog_block.inner(dialog_area)
111
+ frame.render_widget(dialog_block, dialog_area)
112
+
113
+ # Prompt line
114
+ prompt_area, buttons_area = RatatuiRuby::Layout::Layout.split(
115
+ inner,
116
+ direction: :vertical,
117
+ constraints: [
118
+ RatatuiRuby::Layout::Constraint.length(1),
119
+ RatatuiRuby::Layout::Constraint.fill(1)
120
+ ]
93
121
  )
94
- frame.render_widget(help, help_area)
122
+
123
+ frame.render_widget(
124
+ tui.paragraph(text: "Save changes before leaving?", style: {fg: :white}),
125
+ prompt_area
126
+ )
127
+
128
+ # Buttons
129
+ sel = state.confirm_selection
130
+ save_style = sel == 0 ? {fg: :black, bg: :yellow, modifiers: [:bold]} : {fg: :dark_gray}
131
+ discard_style = sel == 1 ? {fg: :black, bg: :yellow, modifiers: [:bold]} : {fg: :dark_gray}
132
+
133
+ save_label = sel == 0 ? " [Save] " : " Save "
134
+ discard_label = sel == 1 ? " [Discard] " : " Discard "
135
+
136
+ btn_save, btn_discard, _ = RatatuiRuby::Layout::Layout.split(
137
+ buttons_area,
138
+ direction: :horizontal,
139
+ constraints: [
140
+ RatatuiRuby::Layout::Constraint.length(save_label.length),
141
+ RatatuiRuby::Layout::Constraint.length(discard_label.length),
142
+ RatatuiRuby::Layout::Constraint.fill(1)
143
+ ]
144
+ )
145
+
146
+ frame.render_widget(tui.paragraph(text: save_label, style: save_style), btn_save)
147
+ frame.render_widget(tui.paragraph(text: discard_label, style: discard_style), btn_discard)
95
148
  end
96
149
  end
97
150
  end
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "ratamin"
4
- require "rails"
5
-
6
3
  module Ratamin
7
4
  class Railtie < Rails::Railtie
8
5
  initializer "ratamin.extend_active_record" do
@@ -50,6 +50,20 @@ module Ratamin
50
50
  @records.length
51
51
  end
52
52
 
53
+ def create_row(attributes)
54
+ record = @scope.klass.new
55
+ cast_attrs = attributes.transform_keys(&:to_s)
56
+ record.assign_attributes(cast_attrs)
57
+
58
+ if record.save
59
+ UpdateResult.success
60
+ else
61
+ UpdateResult.failure(record.errors.full_messages)
62
+ end
63
+ rescue => e
64
+ UpdateResult.failure("#{e.class}: #{e.message}")
65
+ end
66
+
53
67
  def update_row(index, changes)
54
68
  record = @records[index]
55
69
  cast_changes = changes.transform_keys(&:to_s)
@@ -58,8 +72,21 @@ module Ratamin
58
72
  if record.save
59
73
  UpdateResult.success
60
74
  else
61
- UpdateResult.failure(record.errors.full_messages)
75
+ errors = record.errors.full_messages
76
+ record.reload
77
+ UpdateResult.failure(errors)
62
78
  end
79
+ rescue => e
80
+ record&.reload rescue nil
81
+ UpdateResult.failure("#{e.class}: #{e.message}")
82
+ end
83
+
84
+ def with_silenced_output
85
+ old_logger = ActiveRecord::Base.logger
86
+ ActiveRecord::Base.logger = nil
87
+ yield
88
+ ensure
89
+ ActiveRecord::Base.logger = old_logger
63
90
  end
64
91
 
65
92
  def reload!
@@ -97,10 +97,6 @@ module Ratamin
97
97
  end
98
98
 
99
99
  def compute_widths(columns, available_width)
100
- # Reserve space for borders (2) + highlight symbol (2) + column spacing
101
- usable = available_width - 4
102
- total_spacing = [columns.length - 1, 0].max
103
-
104
100
  columns.map do |col|
105
101
  if col.width
106
102
  RatatuiRuby::Layout::Constraint.length(col.width)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ratamin
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/ratamin.rb CHANGED
@@ -9,3 +9,5 @@ loader.setup
9
9
  module Ratamin
10
10
  class Error < StandardError; end
11
11
  end
12
+
13
+ require "ratamin/railtie" if defined?(Rails::Railtie)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ratamin
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - merely