railbow 0.6.2 → 0.7.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: de5537d849ecc612b80236db0a17543d6fad2ea18aeb0c3872dd48ced2dfdf5f
4
- data.tar.gz: a6833af7828722eee5153a48bcf96f7008760948324337c46af90ecee3a86520
3
+ metadata.gz: cdfe578f284175c30e3b9f4c77a83f62b8e63b8019e575590f8ad471e5639efb
4
+ data.tar.gz: 5874946340d18626a566b0190f734f6a94a55a3d40dfb0777760ab377b9d295d
5
5
  SHA512:
6
- metadata.gz: e9d7ae02de3f895c3300dbcd547ad52ddb0309a53a999b2ad52694bc3d46430d90a0518451721df72868fed730f9b60e1c66ffa0e616d01207d2908b560950d0
7
- data.tar.gz: 9fd9c71099808af2b42b186f13ff31112e9195e39af6b94f9650f649e6c7e1b7346d06133992229f9d354a24b955dd2c98ed9b4c77c85a1d727c31c7800bbdb6
6
+ metadata.gz: 3fc62787e1bfd0913be00ca297c5c9210a312b067c68960c13699298a3a064e396a20bc952e65544d19059a03c02454c99413b8f59fd84e6890d9145e8ba03c1
7
+ data.tar.gz: e2253cf2117f5ed4a6702f9500774789d05007876f09ad043b014f9e7b0d53f9616ea21fc32247463081e131aa533b2bf149cb65be99f8f08b37478a281e22ee
data/CHANGELOG.md CHANGED
@@ -5,6 +5,64 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.7.0] - 2026-10-09
9
+
10
+ ### Added
11
+
12
+ - `RBW_TZ` (or `tz:` in `.railbow.yml`) picks the timezone railbow shows
13
+ dates in: `local` (the default), `utc`, or any zone Rails knows, such as
14
+ `America/New_York`. It covers the `db:migrate:status` date column, calendar
15
+ separators and landed badges, `rails notes` blame dates, the `rails about`
16
+ schema version, and where `RBW_SINCE` windows start.
17
+ - A railbow bug no longer breaks the command it decorates. If rendering
18
+ raises, railbow prints a one-line `railbow:` warning to stderr and falls back
19
+ to the stock Rails output for `rails about`, `rails stats`, `rails notes`,
20
+ `rails routes`, migrations and `db:migrate:status` (including multi-database
21
+ runs), then stays plain for the rest of the process. A migration body is
22
+ never re-run by the fallback.
23
+
24
+ ### Changed
25
+
26
+ - Dates in `db:migrate:status` (and calendar separators) are now shown in
27
+ local time. They used to repeat the UTC digits of the Migration ID, so on a
28
+ machine outside UTC the column shifts by the UTC offset. Set `tz: utc` (or
29
+ `RBW_TZ=utc`) for the previous display.
30
+ - `rails about` now says something the stock output does not. The environment
31
+ is colored (production in bold red, development green, anything else
32
+ yellow), the database schema version shows its time and age, version
33
+ numbers share one accent color, the application root steps back, and list
34
+ values such as Middleware get a line per item. The Rails title line is kept,
35
+ and the old "Property / Value" header row is gone.
36
+ - `rails stats` is redrawn in the `db:migrate:status` table style: code and
37
+ test directories under their own headings, thousands grouped, zeros dimmed,
38
+ M/C and LOC/M dimmed for tests, a bar for each directory's share of all LOC,
39
+ and a bold Total row. Directories registered by other gems (such as
40
+ rspec-rails) are included.
41
+ - The default `since` window is now 90 days (was 70), close to three months.
42
+ Set `since: 70d` (or `RBW_SINCE=70d`) to keep the old window.
43
+
44
+ ### Fixed
45
+
46
+ - `rails routes` no longer leaves a blank line before every piece of a wrapped
47
+ route name when the other columns nearly fill the terminal. A group too wide
48
+ for the terminal drops its Prefix column, and a table never wraps its last
49
+ column into lines that would run past the edge: the value stays on its row.
50
+ - `RBW_DATE=rel` ages are no longer off by the machine's UTC offset.
51
+ Migration IDs are UTC timestamps but were read as local time.
52
+ - `rails about` no longer crashes with `uninitialized class variable
53
+ @@properties in Railbow::AboutFormatter`.
54
+ - `rails stats` is formatted again. Since Rails 8.0 it is a Rails command
55
+ that never loads rake tasks, so railbow's hook never ran, and on every Rails
56
+ version the formatter read the wrong data and printed nothing of its own.
57
+ On Rails 7.2 and 8.0, `rake stats` no longer risks running railbow's
58
+ Rails 8.1 replacement task next to the stock one.
59
+ - `rails notes` no longer prints `fatal: no such path ... in HEAD` for notes
60
+ in untracked or gitignored files (such as `app/assets/builds`), and no
61
+ longer hides those notes as older than the `since` window. They are dated
62
+ as uncommitted, the way `git blame` dates uncommitted lines, and with
63
+ `RBW_GIT=author` they show dim dashes in the author column instead of git's
64
+ "Not Committed Yet" squeezed into a name like "No Y".
65
+
8
66
  ## [0.6.2] - 2026-09-21
9
67
 
10
68
  ### Changed
data/README.md CHANGED
@@ -19,7 +19,7 @@
19
19
  - **Routes** - color-coded HTTP verbs and highlighted parameters in `rails routes`
20
20
  - **Stats** - beautiful `rails stats` tables
21
21
  - **Notes** - `rails notes` with git blame, author colors, date filtering, and sorting
22
- - **About** - polished `rails about` output
22
+ - **About** - `rails about` with the environment color-coded (production in red) and the schema version shown as a date and age
23
23
  - **Git Integration** - authors, diffs, branch origin, landing dates, uncommitted file indicators
24
24
  - **Calendar View** - month separators plus week ticks or full week rows, with optional per-section counts
25
25
  - **Multiple Databases** - read one table by default, the rest summarized in a line each (and expanded when they have migrations pending); sharded databases merged with a status per shard
@@ -104,7 +104,7 @@ Out of the box you get:
104
104
  - **Landing dates** - `⤻ Mar 04` badge when a migration was merged to main after its creation
105
105
  - **Branch badges** - `⎇ PS-142` showing the source branch/ticket
106
106
  - **Affected tables** - color-coded table names extracted from migration files
107
- - **Time filtering** - only the last 70 days shown by default (`since: 70d`), as a soft limit: never fewer than 10 migrations, however old (`since_min: 10`)
107
+ - **Time filtering** - only the last 90 days shown by default (`since: 90d`), as a soft limit: never fewer than 10 migrations, however old (`since_min: 10`)
108
108
  - **Your migrations highlighted** - rows authored by you are visually distinct
109
109
  - **Pending migrations greyed out** - a `down` row keeps its status glyph but loses its colors
110
110
 
@@ -197,7 +197,7 @@ rather than an empty table, which is what keeps the `cache`, `queue` and
197
197
 
198
198
  ```
199
199
  ⋯ cache · 5 migrations, all applied · latest Feb 12 2024
200
- ⋯ queue · 12 migrations, 3 pending outside the 70d window - RBW_SINCE=all
200
+ ⋯ queue · 12 migrations, 3 pending outside the 90d window - RBW_SINCE=all
201
201
  ```
202
202
 
203
203
  Databases that share a `migrations_paths` (horizontal sharding) run the same
@@ -236,7 +236,21 @@ HTTP verbs are color-coded (GET=green, POST=yellow, PATCH/PUT=cyan, DELETE=red),
236
236
 
237
237
  ### `rails stats`
238
238
 
239
- Code statistics rendered as a colorful table with highlighted totals and code-to-test ratio.
239
+ Code statistics in the same table style as `db:migrate:status`, code and tests grouped, with each directory's share of all LOC:
240
+
241
+ ```
242
+ Name Lines LOC Classes Methods M/C LOC/M Share of LOC
243
+ Code │ │ │ │ │ │ │
244
+ Controllers │ 57,143 │ 42,693 │ 212 │ 3,472 │ 16 │ 10 │ █████████▊ 22.0%
245
+ Models │ 76,529 │ 48,353 │ 302 │ 4,819 │ 15 │ 8 │ ███████████ 24.9%
246
+ Views │ 98,412 │ 70,120 │ 0 │ 0 │ 0 │ 0 │ ████████████████ 36.1%
247
+ Tests │ │ │ │ │ │ │
248
+ Model specs │ 40,210 │ 33,000 │ 0 │ 120 │ 0 │ 273 │ ███████▌ 17.0%
249
+ │ │ │ │ │ │ │
250
+ Total │ 272,294 │ 194,166 │ 514 │ 8,411 │ 16 │ 21 │
251
+
252
+ Code 161,166 LOC · Tests 33,000 LOC · Code to Test Ratio 1:0.2
253
+ ```
240
254
 
241
255
  ### `rails notes`
242
256
 
@@ -266,7 +280,7 @@ rake railbow:init
266
280
  ### Example `.railbow.yml`
267
281
 
268
282
  ```yaml
269
- since: 70d
283
+ since: 90d
270
284
  since_min: 10
271
285
  date: rel
272
286
  git: "author:me,diff,mask:auto"
@@ -293,9 +307,10 @@ Every option can also be set via `RBW_*` environment variables, which override c
293
307
  |---|---|---|
294
308
  | `RBW_PLAIN` | `1` | Disable all formatting |
295
309
  | `RBW_FORCE` | `1` | Force formatting even when piped, in CI, or run by an LLM agent (`RBW_PLAIN=1` still wins) |
296
- | `RBW_SINCE` | `2mo`, `70d`, `1y`, `all` | Filter migrations by time period |
310
+ | `RBW_SINCE` | `2mo`, `90d`, `1y`, `all` | Filter migrations by time period |
297
311
  | `RBW_SINCE_MIN` | `10`, `0` | Never show fewer than this many migrations, however old (default 10; `0` disables) |
298
312
  | `RBW_DATE` | `full`, `rel`, `short`, `custom(%b %d)` | Date display format |
313
+ | `RBW_TZ` | `local` (default), `utc`, `America/New_York` | Timezone dates are shown in. Migration IDs are UTC; this picks the clock they are displayed on |
299
314
  | `RBW_GIT` | `author:me,diff,mask:auto` | Git integration options |
300
315
  | `RBW_VIEW` | `calendar,tables` | Enable calendar view and table detection |
301
316
  | `RBW_CALENDAR` | `wticks`, `wdividers,counts`, `` (empty) | Week markers: tick marks, full week separator rows, per-section counts. Empty means month separators only |
@@ -344,6 +359,11 @@ Formatting auto-disables when:
344
359
  formatted output to a file, or letting an agent inspect the real rendering).
345
360
  An explicit `RBW_PLAIN=1` always wins over `RBW_FORCE`.
346
361
 
362
+ If railbow itself hits an error while rendering, the command still works: it
363
+ prints a one-line `railbow:` warning to stderr naming the error, then shows the
364
+ stock Rails output and stays plain for the rest of the run. Migrations are never
365
+ re-run by this; only their output falls back.
366
+
347
367
  ## Requirements
348
368
 
349
369
  - Ruby >= 3.1.0
@@ -357,6 +377,10 @@ bundle exec rake # Run tests + linting (RSpec + Standard)
357
377
  bin/console # Interactive prompt
358
378
  ```
359
379
 
380
+ To see a change in a real app (one that loads this checkout), set `RBW_APP` to
381
+ its path, then `just shot "rails routes"` saves a PNG and
382
+ `just cast "rails routes" 100` a scrolling GIF at 100 columns, in `tmp/shots/`.
383
+
360
384
  ## Contributing
361
385
 
362
386
  Bug reports and pull requests are welcome at [github.com/amberpixels/railbow](https://github.com/amberpixels/railbow).
@@ -1,37 +1,73 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "formatters/base"
4
- require_relative "config"
5
- require_relative "table"
6
4
 
7
5
  module Railbow
8
6
  module AboutFormatter
7
+ TITLE = "About your application's environment"
8
+
9
9
  def to_s
10
10
  return super if Railbow.plain?
11
11
 
12
- formatter = Formatters::Base.new
12
+ Railbow.guard(-> { super }) { railbow_to_s }
13
+ end
14
+
15
+ private
16
+
17
+ # Same shape as Rails' own listing (title, then one aligned row per
18
+ # property), with the values styled by what they mean. Properties other
19
+ # gems register render as plain text.
20
+ def railbow_to_s
21
+ f = Formatters::Base.new
22
+ width = properties.map { |(name, _)| name.length }.max.to_i
23
+ lines = [f.bold(TITLE)]
13
24
 
14
- columns = [
15
- Table::Column.new(label: "Property", sticky: true),
16
- Table::Column.new(label: "Value")
17
- ]
25
+ properties.each do |(name, value)|
26
+ # Lists such as Middleware get a line per item instead of one long run.
27
+ values = Array(value).map { |v| railbow_style_value(f, name, v.to_s) }
28
+ lines << "#{f.bold(name.ljust(width))} #{values.first}"
29
+ values.drop(1).each { |v| lines << "#{" " * width} #{v}" }
30
+ end
31
+
32
+ lines.join("\n")
33
+ end
18
34
 
19
- rows = @@properties.map do |(name, value)|
20
- val = value.respond_to?(:call) ? value.call : value
21
- [formatter.bold(name), val.to_s]
35
+ def railbow_style_value(f, name, value)
36
+ case name
37
+ when "Environment" then railbow_style_environment(f, value)
38
+ when "Application root" then f.dim(value)
39
+ when "Database schema version" then railbow_style_schema_version(f, value)
40
+ when /version\z/i then railbow_style_version(f, value)
41
+ else value
22
42
  end
43
+ end
44
+
45
+ # Production is the one environment worth a jolt.
46
+ def railbow_style_environment(f, value)
47
+ case value
48
+ when "production" then f.bold(f.red(value))
49
+ when "development" then f.green(value)
50
+ else f.yellow(value)
51
+ end
52
+ end
53
+
54
+ # The version number takes the accent; anything trailing it, like Ruby's
55
+ # build date and platform, steps back.
56
+ def railbow_style_version(f, value)
57
+ match = value.match(/\d+(?:\.\w+)+/)
58
+ return value unless match
59
+
60
+ tail = match.post_match.empty? ? "" : f.dim(match.post_match)
61
+ "#{match.pre_match}#{f.cyan(match[0])}#{tail}"
62
+ end
63
+
64
+ # Shown on railbow's clock (RBW_TZ), with the zone named because the
65
+ # digits of the version itself are UTC.
66
+ def railbow_style_schema_version(f, value)
67
+ time = Railbow::Clock.from_version(value)
68
+ return value unless time
23
69
 
24
- renderer = Table::Renderer.new(
25
- columns: columns,
26
- theme: Table::Themes::PLAIN,
27
- compact: Railbow::Params.compact_options,
28
- aliases: Railbow::Config.table_aliases
29
- )
30
-
31
- output = +"\n"
32
- output << renderer.render(rows)
33
- output << "\n"
34
- output
70
+ "#{f.cyan(value)} #{f.dim("(#{time.strftime("%Y-%m-%d %H:%M %Z")}, #{f.format_relative_time_from(time)})")}"
35
71
  end
36
72
  end
37
73
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "date"
4
+ require_relative "clock"
4
5
 
5
6
  module Railbow
6
7
  # Turns an ordered list of migration versions into the calendar furniture that
@@ -58,10 +59,7 @@ module Railbow
58
59
  end
59
60
 
60
61
  def parse_date(version)
61
- v = version.to_s
62
- Date.new(v[0..3].to_i, v[4..5].to_i, v[6..7].to_i)
63
- rescue Date::Error
64
- nil
62
+ Railbow::Clock.from_version(version)&.to_date
65
63
  end
66
64
 
67
65
  def month_key(date)
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+
5
+ module Railbow
6
+ # The clock railbow shows times on. Migration IDs and git dates are fixed
7
+ # moments; RBW_TZ (or `tz:` in .railbow.yml) only picks the zone they are
8
+ # shown in: "local" (the default), "utc", or any zone name Rails accepts,
9
+ # e.g. "America/New_York" or "Eastern Time (US & Canada)".
10
+ module Clock
11
+ @zones = {}
12
+
13
+ module_function
14
+
15
+ # A migration ID is the UTC time the migration was generated, which is
16
+ # how Rails names them. Returns nil for anything that is not one.
17
+ def from_version(version)
18
+ v = version.to_s
19
+ return nil unless v.match?(/\A\d{14}\z/)
20
+
21
+ utc = Time.utc(
22
+ v[0..3].to_i, v[4..5].to_i, v[6..7].to_i,
23
+ v[8..9].to_i, v[10..11].to_i, v[12..13].to_i
24
+ )
25
+ in_zone(utc)
26
+ rescue ArgumentError
27
+ nil
28
+ end
29
+
30
+ def in_zone(time)
31
+ case (z = zone)
32
+ when :local then time.getlocal
33
+ when :utc then time.getutc
34
+ else time.in_time_zone(z)
35
+ end
36
+ end
37
+
38
+ def today
39
+ in_zone(Time.now).to_date
40
+ end
41
+
42
+ # The migration ID of the midnight that starts date on this clock, so a
43
+ # day-based window can be compared against migration IDs directly.
44
+ def version_at(date)
45
+ start = case (z = zone)
46
+ when :local then Time.local(date.year, date.month, date.day)
47
+ when :utc then Time.utc(date.year, date.month, date.day)
48
+ else z.local(date.year, date.month, date.day)
49
+ end
50
+ start.utc.strftime("%Y%m%d%H%M%S")
51
+ end
52
+
53
+ def zone
54
+ name = Params.tz
55
+ @zones[name] ||= resolve(name)
56
+ end
57
+
58
+ def resolve(name)
59
+ case name.downcase
60
+ when "local" then :local
61
+ when "utc" then :utc
62
+ else
63
+ require "active_support"
64
+ require "active_support/time"
65
+ ActiveSupport::TimeZone[name] || begin
66
+ warn " Warning: unrecognized RBW_TZ=#{name}, showing local time"
67
+ :local
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -1,22 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Railbow
4
- # Assigns maximally-spaced colors to a set of unique labels.
4
+ # Spreads a set of labels evenly around the color wheel by rank. Hashing
5
+ # into a palette could land two labels on neighboring colors; by rank, two
6
+ # labels sit opposite each other, three about 120 deg apart, and so on.
5
7
  #
6
- # Instead of hashing into a fixed palette (where two labels can collide
7
- # onto adjacent colors), this assigns colors by rank — evenly spacing
8
- # hues around the color wheel. Two labels always get maximally different
9
- # colors, three labels get ~120 deg apart, etc.
10
- #
11
- # Usage:
12
8
  # assigner = Railbow::ColorAssigner.new(["Alice", "Bob", "Carol"])
13
9
  # assigner.color_for("Alice") # => "\e[38;5;174m"
14
10
  # assigner.code_for("Bob") # => 116 (256-color code)
15
11
  #
16
12
  class ColorAssigner
17
- # Soft/muted 256-color codes arranged around the hue wheel.
18
- # 12 stops, evenly spaced in hue, all at moderate saturation/lightness
19
- # so they look pleasant on dark terminals.
13
+ # Muted 256-color codes in hue order, at moderate saturation and
14
+ # lightness so they sit well on dark terminals.
20
15
  WHEEL = [
21
16
  174, # 0° coral / muted rose
22
17
  216, # 30° peach
@@ -39,13 +34,12 @@ module Railbow
39
34
  end
40
35
  end
41
36
 
42
- # Returns the 256-color ANSI escape for the given label.
43
37
  def color_for(label)
44
38
  code = code_for(label)
45
39
  "\e[38;5;#{code}m"
46
40
  end
47
41
 
48
- # Returns just the 256-color code integer for the given label.
42
+ # An unknown label gets the first color.
49
43
  def code_for(label)
50
44
  @mapping[label] || WHEEL[0]
51
45
  end
@@ -13,7 +13,7 @@ module Railbow
13
13
  "Status" => {"up" => "↑↑", "down" => "↓↓"}
14
14
  }
15
15
  },
16
- "since" => "70d",
16
+ "since" => "90d",
17
17
  "since_min" => 10,
18
18
  "git" => "author:all,diff,mask:auto",
19
19
  "author_format" => "short",
@@ -31,11 +31,10 @@ module Railbow
31
31
  git_email = Fixtures::DEMO_USER_EMAIL
32
32
  git_name = Fixtures::DEMO_USER_NAME
33
33
 
34
- # Latest migration date - for "fresh" landed badge detection
34
+ # Decides which landed badges count as fresh.
35
35
  latest_version = migrations.last[:version]
36
36
  latest_mig_date = parse_version_date(latest_version)
37
37
 
38
- # Check if any landed tags exist
39
38
  has_landed_tags = migrations.any? { |m|
40
39
  next false unless m[:landed_date]
41
40
  mig_date = parse_version_date(m[:version])
@@ -55,13 +54,12 @@ module Railbow
55
54
  truncate_fn: ->(cell_raw, max_w) { formatter.name_with_tags_fitted(cell_raw, max_w) },
56
55
  shrinkable: true, shrink_floor: 24)
57
56
  ]
58
- # No Author column in default config (author:me only highlights)
57
+ # No Who column: author:me only highlights.
59
58
 
60
59
  tables_truncate_fn = ->(cell_raw, max_w) { formatter.table_tags_fitted(cell_raw, max_w) }
61
60
  table_columns << Railbow::Table::Column.new(label: "Tables", droppable: 1,
62
61
  truncate: true, truncate_fn: tables_truncate_fn)
63
62
 
64
- # Build rows
65
63
  highlight_rows = Set.new
66
64
  down_rows = Set.new
67
65
  rows = migrations.each_with_index.map do |m, idx|
@@ -74,10 +72,8 @@ module Railbow
74
72
 
75
73
  display_name = m[:name]
76
74
 
77
- # Diff tag (branch badge)
78
75
  diff_tag = m[:branch] ? formatter.diff_tag_branch(m[:branch]) : nil
79
76
 
80
- # Landed badge
81
77
  landed_tag = nil
82
78
  if m[:landed_date]
83
79
  mig_date = parse_version_date(m[:version])
@@ -87,7 +83,6 @@ module Railbow
87
83
  end
88
84
  end
89
85
 
90
- # Append tags to display_name
91
86
  tags = [landed_tag, diff_tag].compact.join(" ")
92
87
  if !tags.empty? && name_col_width
93
88
  tags_width = formatter.display_width(formatter.strip_ansi(tags))
@@ -102,20 +97,18 @@ module Railbow
102
97
 
103
98
  created_at = formatter.format_date(m[:version], date_format)
104
99
 
105
- # Highlight current user's rows (author:me mode)
100
+ # author:me highlighting.
106
101
  if m[:author_email] == git_email ||
107
102
  (git_name && m[:author_name] && m[:author_name].downcase == git_name.downcase)
108
103
  highlight_rows << idx
109
104
  end
110
105
 
111
- # Table tags
112
106
  table_tags = formatter.table_tags(m[:tables])
113
107
 
114
108
  [colored_status, m[:version], created_at, display_name, table_tags]
115
109
  end
116
110
 
117
- # Calendar furniture. Driven by the real params, so RBW_CALENDAR can be
118
- # tried out here without a Rails project.
111
+ # Real params, so RBW_CALENDAR can be tried without a Rails project.
119
112
  calendar = Railbow::Calendar.build(
120
113
  migrations.map { |m| m[:version] },
121
114
  weeks: Railbow::Params.calendar_wdividers?,
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "zlib"
4
+ require_relative "../clock"
4
5
  require_relative "../text_utils"
5
6
 
6
7
  module Railbow
@@ -69,23 +70,20 @@ module Railbow
69
70
  table_names.map { |t| table_tag(t) }.join(" ")
70
71
  end
71
72
 
72
- # Re-fits a pre-formatted table_tags string within max_width,
73
- # showing full table names and "+N" for overflow.
74
- # Accepts the full formatted string (with ANSI) and splits it into segments.
73
+ # Re-fits a formatted table_tags string within max_width: as many whole
74
+ # tags as fit plus "+N" for the rest, then one truncated tag, then "+N".
75
75
  def table_tags_fitted(formatted_str, max_width)
76
76
  return formatted_str if formatted_str.nil? || formatted_str.empty?
77
77
 
78
- # Split into individual tag segments: each is "\e[38;5;NNNm● name\e[0m"
78
+ # Each tag is "\e[38;5;NNNm● name\e[0m"
79
79
  segments = formatted_str.scan(/\e\[38;5;\d+m● [^\e]+\e\[0m/)
80
80
  return formatted_str if segments.empty?
81
81
 
82
82
  total = segments.size
83
83
  plain_segments = segments.map { |s| strip_ansi(s) }
84
84
 
85
- # Try fitting all tags
86
85
  return formatted_str if display_width(plain_segments.join(" ")) <= max_width
87
86
 
88
- # Try fitting progressively fewer tags with +N suffix
89
87
  (total - 1).downto(1) do |count|
90
88
  remaining = total - count
91
89
  suffix = " +#{remaining}"
@@ -95,17 +93,13 @@ module Railbow
95
93
  end
96
94
  end
97
95
 
98
- # Try fitting a truncated first table name + overflow.
99
- # Need at least 7 chars: "● t… +N" (dot + color prefix use no visible width beyond the dot)
96
+ # A truncated first tag needs 7 columns at least: "● t… +N"
100
97
  if max_width >= 7
101
98
  suffix = (total > 1) ? " +#{total - 1}" : ""
102
- # Available width for "● name…" part
103
99
  avail = max_width - display_width(suffix)
104
- # "● " prefix = 2 chars visible, plus at least 1 char of name + ellipsis (1 char)
105
100
  if avail >= 4 # "● " (2) + at least 1 char + "…" (1)
106
101
  first_plain = plain_segments[0] # e.g. "● some_table"
107
102
  name_part = first_plain.sub(/^● /, "")
108
- # Truncate name to fit: avail - 2 ("● ") - 1 ("…") = chars for name
109
103
  name_max = avail - 2 - 1
110
104
  truncated_name = name_part[0, name_max]
111
105
  color_match = segments[0].match(/\e\[38;5;\d+m/)
@@ -114,16 +108,13 @@ module Railbow
114
108
  end
115
109
  end
116
110
 
117
- # Absolute fallback: just +N
118
111
  "+#{total}"
119
112
  end
120
113
 
121
- # Re-fits a Migration Name cell whose tags (branch, landed) were padded
122
- # flush against the right edge of a wider column. Truncating such a cell
123
- # from the right eats the tags whole and leaves a bare ellipsis floating
124
- # after the padding, so the name gives up the space instead and the tags
125
- # stay put. Below NAME_TAGS_FLOOR there is no room for both and the tags
126
- # are dropped outright - the name is what the row is about.
114
+ # Re-fits a Migration Name cell whose tags (branch, landed) sit flush
115
+ # right. Cutting from the right would eat the tags and leave a bare
116
+ # ellipsis, so the name gives up the space instead. Below NAME_TAGS_FLOOR
117
+ # the tags go: the name is what the row is about.
127
118
  def name_with_tags_fitted(cell, max_width)
128
119
  str = cell.to_s
129
120
  return truncate_ansi(str, max_width) if display_width(strip_ansi(str)) <= max_width
@@ -170,16 +161,8 @@ module Railbow
170
161
 
171
162
  def format_date(timestamp, mode = "full")
172
163
  ts = timestamp.to_s
173
- return ts if ts.length != 14
174
-
175
- time = begin
176
- Time.new(
177
- ts[0..3].to_i, ts[4..5].to_i, ts[6..7].to_i,
178
- ts[8..9].to_i, ts[10..11].to_i, ts[12..13].to_i
179
- )
180
- rescue ArgumentError
181
- return ts
182
- end
164
+ time = Railbow::Clock.from_version(ts)
165
+ return ts unless time
183
166
 
184
167
  case mode
185
168
  when "full"
@@ -195,8 +178,6 @@ module Railbow
195
178
  end
196
179
  end
197
180
 
198
- private
199
-
200
181
  def format_relative_time_from(time)
201
182
  diff = Time.now - time
202
183
  return "just now" if diff < 0
@@ -217,8 +198,6 @@ module Railbow
217
198
  else "~#{years}y ago"
218
199
  end
219
200
  end
220
-
221
- public
222
201
  end
223
202
  end
224
203
  end
data/lib/railbow/init.rb CHANGED
@@ -20,7 +20,7 @@ module Railbow
20
20
  # Every key can also be overridden via RBW_* environment variables.
21
21
 
22
22
  # Time window for migrations to display (e.g. 30d, 2mo, 1y, all)
23
- since: "70d"
23
+ since: "90d"
24
24
 
25
25
  # Never show fewer than this many migrations, however old they are.
26
26
  # The window above is a soft limit: a database with five migrations shows
@@ -60,6 +60,10 @@ module Railbow
60
60
  # Date format: full, rel, short, or custom(%b %d, %Y)
61
61
  # date: "full"
62
62
 
63
+ # Timezone dates are shown in: local (this machine), utc, or any zone
64
+ # Rails knows, e.g. America/New_York or "Eastern Time (US & Canada)"
65
+ # tz: "local"
66
+
63
67
  # Multi-database runs (comma-separated). Leave empty ("") to expand every
64
68
  # database into its own section, in database.yml order; a database with
65
69
  # nothing in the time window collapses to a one-line summary either way.