From 43bc3c2eec52a7a0327ebeae722315b115f07e29 Mon Sep 17 00:00:00 2001 From: Mike McQuaid Date: Sat, 17 Oct 2026 13:50:21 +0100 Subject: [PATCH] Truncate summary versions to fit terminal width - Reserve space for names, arrows and download sizes in install and upgrade summaries. - Retain at least 11 version characters before an ellipsis so narrow terminals still show useful versions. - Keep full version strings when output is redirected. - Document descriptive names for numeric values in `AGENTS.md`. --- AGENTS.md | 3 +- Library/Homebrew/test/formatter_spec.rb | 63 +++++++++++++++++++++++++ Library/Homebrew/utils/formatter.rb | 26 ++++++++-- 3 files changed, 87 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 7bfc95b6cd..6dfa1f422e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ Most importantly, run `./bin/brew lgtm` (style checks, type checks and tests) to verify any file edits before prompting for input. -Inline any new or existing methods or variables used only once unless they are needed for unit tests. +Inline any new or existing methods or variables used only once unless they are needed for unit tests or explain magic numbers. Keep the diff as small, DRY and YAGNI as possible. Re-read relevant files after each prompt and preserve user edits and comments. Before finishing, check your work and point out anything the user may not have considered. @@ -45,6 +45,7 @@ This is a Ruby repository with Bash scripts for faster execution that provides t - Use `&>/dev/null` instead of `>/dev/null 2>&1`. - Put a comment immediately above each `shellcheck disable` explaining why it is needed. - Prefer self-documenting code through strings and variable names to new explanatory comments. +- Avoid magic numbers; use self-explanatory intermediate variables or constants to make their meaning clear. - Use UK spelling and punctuation; avoid em dashes and Oxford commas. - Wrap human-written user-facing terminal output at around 80 characters; generated output and code are exempt. - Update `docs/` when the behaviour it describes changes. diff --git a/Library/Homebrew/test/formatter_spec.rb b/Library/Homebrew/test/formatter_spec.rb index b823f14be5..aa15a7a643 100644 --- a/Library/Homebrew/test/formatter_spec.rb +++ b/Library/Homebrew/test/formatter_spec.rb @@ -126,6 +126,69 @@ RSpec.describe Formatter do end end + describe ".version_changes" do + before do + allow($stdout).to receive(:tty?).and_return(true) + allow(Tty).to receive(:width).and_return(40) + end + + test_each([ + ["both versions", "1" * 40, "2" * 40, "edge #{"1" * 12}... -> #{"2" * 13}..."], + ["the old version", "1" * 40, "2.0", "edge #{"1" * 25}... -> 2.0"], + ["the new version", "1.0", "2" * 40, "edge 1.0 -> #{"2" * 25}..."], + ["a version without an old version", "2" * 40, nil, "edge #{"2" * 32}..."], + ]) do |description, old_version, new_version, expected| + it "truncates #{description} to fit the terminal" do + expect(described_class.version_changes([version_change("edge", old_version, new_version)])) + .to eq([expected]) + end + end + + it "reserves space for download sizes and keeps columns aligned" do + allow(Tty).to receive(:width).and_return(50) + + expect(described_class.version_changes([ + version_change("edge", "1" * 40, "2" * 40), + version_change("testball", "1.0", "2.0", size: "500B"), + ])).to eq([ + "edge 11111111111... -> 22222222222...", + "testball 1.0 -> 2.0 (500B)", + ]) + end + + test_each([ + ["123.456.789,abcdef", "234.567.890,ghijkl", "edge 123.456.789... -> 234.567.890..."], + ["123.456.789", "234.567.890", "edge 123.456.789 -> 234.567.890"], + ["123.456.789,abcdef", nil, "edge 123.456.789..."], + ]) do |old_version, new_version, expected| + it "keeps at least 11 version characters even in a very narrow terminal" do + allow(Tty).to receive(:width).and_return(1) + + expect(described_class.version_changes([version_change("edge", old_version, new_version)])) + .to eq([expected]) + end + end + + it "preserves versions that fit the terminal exactly" do + expect(described_class.version_changes([version_change("edge", "1" * 15, "2" * 16)])) + .to eq(["edge #{"1" * 15} -> #{"2" * 16}"]) + end + + it "preserves full versions when output is redirected" do + allow($stdout).to receive(:tty?).and_return(false) + + expect(described_class.version_changes([version_change("edge", "1" * 40, "2" * 40)])) + .to eq(["edge #{"1" * 40} -> #{"2" * 40}"]) + end + + it "preserves full versions when the terminal width is unknown" do + allow(Tty).to receive(:width).and_return(0) + + expect(described_class.version_changes([version_change("edge", "1" * 40, "2" * 40)])) + .to eq(["edge #{"1" * 40} -> #{"2" * 40}"]) + end + end + describe ".disk_usage_readable_size_unit" do it "returns size and unit for bytes" do expect(described_class.disk_usage_readable_size_unit(500)).to eq([500, "B"]) diff --git a/Library/Homebrew/utils/formatter.rb b/Library/Homebrew/utils/formatter.rb index c0267d6fce..39b4986c15 100644 --- a/Library/Homebrew/utils/formatter.rb +++ b/Library/Homebrew/utils/formatter.rb @@ -132,16 +132,34 @@ module Formatter # like `(2.4MB)` in its own column. sig { params(changes: T::Array[VersionChange]).returns(T::Array[String]) } def self.version_changes(changes) + name_width = changes.map { |change| change.name.length }.max || 0 + separator = (changes.one? ? " " : " ") old_width = changes.map { |change| (change.old_version || change.new_version).length }.max || 0 + new_width = changes.filter_map { |change| change.new_version.length if change.old_version }.max || 0 + # Keep full versions in redirected output or when the terminal width is unknown. + if $stdout.tty? && Tty.width.positive? + # Reserve space for names, separators, size parentheses and the upgrade arrow. + available_width = Tty.width - name_width - separator.length + size_width = changes.filter_map { |change| change.size&.length }.max + size_parentheses_width = 2 + available_width -= size_width + size_parentheses_width + separator.length if size_width + upgrade_arrow_width = 4 + available_width -= upgrade_arrow_width if new_width.positive? + + # Keep room for XXX.YYY.ZZZ plus "...", even if a very narrow terminal must wrap. + minimum_version_width = 14 + # Share space evenly, letting a long version use space left by a short one. + old_width = [old_width, [available_width - new_width, available_width / 2, minimum_version_width].max].min + new_width = [available_width - old_width, minimum_version_width].max + end + versions = changes.map do |change| old_version = change.old_version - next change.new_version unless old_version + next truncate(change.new_version, max: old_width) unless old_version - "#{old_version.ljust(old_width)} -> #{change.new_version}" + "#{truncate(old_version, max: old_width).ljust(old_width)} -> #{truncate(change.new_version, max: new_width)}" end - name_width = changes.map { |change| change.name.length }.max || 0 versions_width = versions.map(&:length).max || 0 - separator = (changes.one? ? " " : " ") changes.each_with_index.map do |change, index| size = "(#{change.size})" if change.size