gemview 1.0.0 → 1.1.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: ad8aeeac9963c5eb52cb650708827b9a25a223bfa4597af3466142700e51850b
4
- data.tar.gz: efdbcb9c871e4462e2e216adf675c9ba2b3373616f6494f60208c9ec39b6f8d3
3
+ metadata.gz: 316d82c9b1a5f8b3e567d04b2ef7541ecbd554992bfe8c7c97497d1aefbe5e8c
4
+ data.tar.gz: e8c2b2b065dd6404694e5218e5130473abdadbd1f71c4f4e13ca36a7c3d4bbf2
5
5
  SHA512:
6
- metadata.gz: c997980d1fca42aa4bb49a1f7d43443591907d04f3c20f93a6b72f045a77a8a49a9b85499cd3152b6ccbb349324417dd91be9323b78e57aa1b2825905504b5d8
7
- data.tar.gz: deec958df2e6515729a091d3f073df221297c01562ae2af1de65b787a566c7b46dde6d3260aa5bbf6063c3978982a225f259a2e3ff27135e4227c59c392a4535
6
+ metadata.gz: 7f414319c19f06dfe2e0e5fb158e06371d03f4f930def70ee7340b09cbde9093cf72a2a20522db142d3b517e507126401b0c58ee7ad14dc0eeb74ec4a2a8ca5b
7
+ data.tar.gz: ebde82434ab61ec53e68d66cc18048d8378663508b67664bf2c58488bc44ec429b2889a051dd011ef7c5c3152f726cc58fe55fd1fa647f8e39db0b856849571c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.1.0] - 2025-08-16
4
+
5
+ - Add better support for showing the changelog
6
+ - Add ability to sort search results by total and version downloads
7
+ - Add ruby version to gem version output
8
+ - Add ability to disable readme and changelog viewing if they are unreachable
9
+ - Add support for Codeberg readmes and changelogs (before only Github and Gitlab were supported)
10
+
11
+ - Change pager to align TUI at the top of the screen
12
+ - Change default pager to use less for consistent paging
13
+
3
14
  ## [1.0.0] - 2024-12-08
4
15
 
5
16
  - Initial release
data/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # Gemview
2
2
 
3
- An unofficial CLI interface for querying information from rubygems.org. It uses the [gems](https://rubygems.org/gems/gems) gem internally.
3
+ [![Gem Version](https://badge.fury.io/rb/gemview.svg)](https://badge.fury.io/rb/gemview)
4
+
5
+ An unofficial TUI interface for querying information from rubygems.org. It uses the [gems](https://rubygems.org/gems/gems) gem internally. It requires the presence of the `less` pager and is tested on macOS and Linux.
4
6
 
5
7
  Note: This gem is not directly affiliated with `rubygems.org`. It's just a hobby project.
6
8
 
@@ -20,6 +22,12 @@ Commands:
20
22
 
21
23
  ![GIF demoing the info and search subcommands](./assets/gemview-v1.0.0.gif)
22
24
 
25
+ ## Implementation
26
+
27
+ Changelog and readme fetching is only supported for `github.com` and `gitlab.com` currently. This works by parsing the URI associated with one of these two sites from gem metadata, building a new URI associated with the given file and trying to download it as a raw file. Let me know if there are any other platforms I should add support for.
28
+
29
+ Markdown highlighting is added on a best effort basis and if the parser fails for some reason it just falls back to the raw text file.
30
+
23
31
  ## Development
24
32
 
25
33
  ### Testing & Linting
@@ -40,15 +40,24 @@ module Gemview
40
40
 
41
41
  argument :term, type: :string, required: true, desc: "Search term"
42
42
 
43
- example %w[cli json]
43
+ option :downloads, values: %w[total version], desc: "Sort results by most downloads"
44
44
 
45
- def call(term:, **)
45
+ example ["cli", "json --downloads=total"]
46
+
47
+ def call(term:, downloads: nil, **)
46
48
  gems = Gem.search(term: term)
47
49
 
48
50
  if gems.empty?
49
51
  abort("Error: No gems found for the search term: #{term}")
50
52
  end
51
53
 
54
+ case downloads
55
+ when "total"
56
+ gems.sort_by! { |gem| -gem.downloads }
57
+ when "version"
58
+ gems.sort_by! { |gem| -gem.version_downloads }
59
+ end
60
+
52
61
  View.list(gems: gems)
53
62
  end
54
63
  end
data/lib/gemview/gem.rb CHANGED
@@ -1,70 +1,76 @@
1
- # frozen_string_literal: true
2
-
3
- require "dry-struct"
1
+ # frozen_string_literal: true"
4
2
 
5
3
  module Gemview
6
- class Gem < Dry::Struct
7
- module Types
8
- include Dry.Types()
9
- end
10
-
11
- transform_keys(&:to_sym)
12
-
13
- # resolve default types on nil
14
- transform_types do |type|
15
- if type.default?
16
- type.constructor do |value|
17
- value.nil? ? Dry::Types::Undefined : value
18
- end
19
- else
20
- type
4
+ class Gem
5
+ attr_reader(
6
+ :name,
7
+ :downloads,
8
+ :version,
9
+ :version_downloads,
10
+ :version_created_at,
11
+ :authors,
12
+ :info,
13
+ :licenses,
14
+ :project_uri,
15
+ :homepage_uri,
16
+ :source_code_uri,
17
+ :changelog_uri,
18
+ :development_dependencies,
19
+ :runtime_dependencies
20
+ )
21
+
22
+ # @param options [Hash]
23
+ def initialize(options)
24
+ @name = options.fetch("name")
25
+ @downloads = options.fetch("downloads")
26
+ @version = options.fetch("version")
27
+ @version_downloads = options.fetch("version_downloads")
28
+ # Note: This is not returned by `Gems.search`.
29
+ @version_created_at = options["version_created_at"]
30
+ &.then { |time| Time.parse(time) }
31
+ @authors = options.fetch("authors")
32
+ @info = options.fetch("info")
33
+ # Note: This is occasionally nil so a default value is required.
34
+ @licenses = (options.fetch("licenses") || []).freeze
35
+ @project_uri = options.fetch("project_uri")
36
+ @homepage_uri = options.fetch("homepage_uri")
37
+ @source_code_uri = options.fetch("source_code_uri")
38
+ @changelog_uri = options.fetch("changelog_uri")
39
+ # Note: Dependencies are not returned by `Gems.search`.
40
+ @development_dependencies = options
41
+ .dig("dependencies", "development")
42
+ &.map { |hash| Dependency.new(hash).freeze }
43
+ &.freeze
44
+ @runtime_dependencies = options
45
+ .dig("dependencies", "runtime")
46
+ &.map { |hash| Dependency.new(hash).freeze }
47
+ &.freeze
48
+ end
49
+
50
+ class Dependency
51
+ attr_reader :name, :requirements
52
+
53
+ # @param options [Hash]
54
+ def initialize(options)
55
+ @name = options.fetch("name")
56
+ @requirements = options.fetch("requirements")
21
57
  end
22
- end
23
-
24
- attribute :name, Types::Strict::String
25
- attribute :downloads, Types::Strict::Integer
26
- attribute :version, Types::Strict::String
27
- # Note: This is not returned by `Gems.search`.
28
- attribute? :version_created_at, Types::Params::Time
29
- attribute :authors, Types::Strict::String
30
- attribute :info, Types::Strict::String
31
- # Note: This is occasionally nil so a default value is required.
32
- attribute :licenses, Types::Array.of(Types::Strict::String).default([].freeze)
33
- attribute :project_uri, Types::Strict::String
34
- attribute :homepage_uri, Types::String.optional
35
- attribute :source_code_uri, Types::String.optional
36
- attribute :changelog_uri, Types::String.optional
37
-
38
- class Dependency < Dry::Struct
39
- transform_keys(&:to_sym)
40
-
41
- attribute :name, Types::Strict::String
42
- attribute :requirements, Types::Strict::String
43
58
 
44
59
  def to_str
45
60
  %(gem "#{name}", "#{requirements}")
46
61
  end
47
62
  end
48
63
 
49
- # Note: This is not returned by `Gems.search`.
50
- attribute? :dependencies do
51
- attribute :development, Types::Strict::Array.of(Dependency)
52
- attribute :runtime, Types::Strict::Array.of(Dependency)
53
- end
54
-
55
- class Version < Dry::Struct
56
- transform_keys(&:to_sym)
57
-
58
- attribute :number, Types::Strict::String
59
- alias_method :version, :number
60
-
61
- attribute :downloads_count, Types::Strict::Integer
62
- alias_method :downloads, :downloads_count
64
+ class Version
65
+ attr_reader :version, :downloads, :release_date, :ruby_version
63
66
 
64
- attribute :created_at, Types::Params::Time
65
-
66
- # @return [Date]
67
- def release_date = created_at.to_date
67
+ # @param options [Hash]
68
+ def initialize(options)
69
+ @version = options.fetch("number")
70
+ @downloads = options.fetch("downloads_count")
71
+ @release_date = Date.parse(options.fetch("created_at"))
72
+ @ruby_version = options.fetch("ruby_version") || "(unknown)"
73
+ end
68
74
  end
69
75
 
70
76
  # Ex. 1234567890 -> "1,234,567,890"
@@ -75,9 +81,11 @@ module Gemview
75
81
 
76
82
  # @return [String]
77
83
  def selector_str
84
+ one_line_info = info.lines.map(&:strip).reject(&:empty?).join(" ").strip
85
+
78
86
  <<~SELECT
79
87
  #{name} [#{version}]
80
- -- #{Strings.truncate(info.lines.map(&:strip).join(" "), 75)}
88
+ -- #{Strings.truncate(one_line_info, 75)}
81
89
  SELECT
82
90
  end
83
91
 
@@ -103,16 +111,21 @@ module Gemview
103
111
  Terminal.prettify_markdown(header)
104
112
  end
105
113
 
114
+ # @return [Boolean]
115
+ def dependencies?
116
+ !runtime_dependencies.nil? && !development_dependencies.nil?
117
+ end
118
+
106
119
  # @return [String]
107
120
  def dependencies_str
108
- runtime_deps_str = dependencies.runtime.join("\n").strip
121
+ runtime_deps_str = runtime_dependencies.join("\n").strip
109
122
  runtime_deps_str = if runtime_deps_str.empty?
110
123
  "(none)"
111
124
  else
112
125
  "```rb\n#{runtime_deps_str}\n```"
113
126
  end
114
127
 
115
- dev_deps_str = dependencies.development.join("\n").strip
128
+ dev_deps_str = development_dependencies.join("\n").strip
116
129
  dev_deps_str = if dev_deps_str.empty?
117
130
  "(none)"
118
131
  else
@@ -132,46 +145,53 @@ module Gemview
132
145
  Terminal.prettify_markdown(dependencies)
133
146
  end
134
147
 
148
+ # @return [String]
135
149
  def versions_str
136
150
  rows = self.class.versions(name: name).map do |version|
137
151
  pretty_downloads = Number.humanized_integer(version.downloads)
138
- "| #{version.release_date} | #{version.version} | #{pretty_downloads} |"
152
+ "| #{version.release_date} | #{version.version} | #{pretty_downloads} | #{version.ruby_version}"
139
153
  end
140
154
 
141
155
  table = <<~TABLE
142
156
  ## [Versions]
143
157
 
144
- | *Release Date* | *Version* | *Downloads* |
145
- |----------------|-----------|-------------|
158
+ | *Release Date* | *Gem Version* | *Downloads* | *Ruby Version* |
159
+ |----------------|---------------|-------------|----------------|
146
160
  #{rows.join("\n")}
147
161
  TABLE
148
162
 
149
163
  Terminal.prettify_markdown(table)
150
164
  end
151
165
 
152
- # @return [Array<String>]
153
- def urls
154
- [
155
- homepage_uri,
156
- source_code_uri,
157
- changelog_uri
158
- ].compact
159
- end
166
+ # @return [Gemview::GitRepo, nil]
167
+ def git_repo
168
+ return @git_repo if defined? @git_repo
160
169
 
161
- # @return [String|nil]
162
- def fetch_readme
163
- GitRepo.from_urls(urls: urls, version: version)&.readme ||
164
- "Info: Unable to find a valid readme based on available gem info"
170
+ @git_repo = GitRepo.from_urls(
171
+ homepage_uri: homepage_uri,
172
+ source_code_uri: source_code_uri,
173
+ changelog_uri: changelog_uri,
174
+ version: version
175
+ )
165
176
  end
166
177
 
167
- # @return [String|nil]
168
- def fetch_changelog
169
- GitRepo.from_urls(urls: urls, version: version)&.changelog ||
170
- "Info: Unable to find a valid changelog based on available gem info"
171
- end
178
+ # @return [Boolean]
179
+ def git_repo? = !git_repo.nil?
180
+
181
+ # @return [Boolean]
182
+ def readme? = git_repo? && git_repo.readme?
183
+
184
+ # @return [String, nil]
185
+ def readme = git_repo&.readme
186
+
187
+ # @return [Boolean]
188
+ def changelog? = git_repo? && git_repo.changelog?
189
+
190
+ # @return [String, nil]
191
+ def changelog = git_repo&.changelog
172
192
 
173
193
  # @param name [String]
174
- # @param version [String|nil] will default to latest if not provided
194
+ # @param version [String, nil] will default to latest if not provided
175
195
  # @return [Gemview::Gem]
176
196
  def self.find(name:, version: nil)
177
197
  @find ||= {}
@@ -186,30 +206,30 @@ module Gemview
186
206
  # @param term [String] search term
187
207
  # @return [Array<Gemview::Gem>]
188
208
  def self.search(term:)
189
- Client.v1.search(term).map { |gem_hash| new gem_hash }
209
+ Client.v1.search(term).map { |gem_hash| new(gem_hash) }
190
210
  end
191
211
 
192
212
  # @param username [String] rubygems.org username
193
213
  # @return [Array<Gemview::Gem>]
194
214
  def self.author(username:)
195
- Client.v1.gems(username).map { |gem_hash| new gem_hash }
215
+ Client.v1.gems(username).map { |gem_hash| new(gem_hash) }
196
216
  end
197
217
 
198
218
  # @return [Array<Gemview::Gem>]
199
219
  def self.latest
200
- Client.v1.latest.map { |gem_hash| new gem_hash }
220
+ Client.v1.latest.map { |gem_hash| new(gem_hash) }
201
221
  end
202
222
 
203
223
  # @return [Array<Gemview::Gem>]
204
224
  def self.just_updated
205
- Client.v1.just_updated.map { |gem_hash| new gem_hash }
225
+ Client.v1.just_updated.map { |gem_hash| new(gem_hash) }
206
226
  end
207
227
 
208
228
  # @param name [String] gem name
209
229
  # @return [Array<Gemview::Gem::Version>]
210
230
  def self.versions(name:)
211
231
  @versions ||= {}
212
- @versions[name] ||= Client.v1.versions(name).map { |gem_hash| Version.new gem_hash }
232
+ @versions[name] ||= Client.v1.versions(name).map { |gem_hash| Version.new(gem_hash).freeze }.freeze
213
233
  end
214
234
  end
215
235
  end
@@ -4,21 +4,24 @@ module Gemview
4
4
  class GitRepo
5
5
  HOSTS = [
6
6
  GITHUB = :github,
7
- GITLAB = :gitlab
7
+ GITLAB = :gitlab,
8
+ CODEBERG = :codeberg
8
9
  ].freeze
9
10
 
10
- # @param urls [Array<String>]
11
+ HTTPS_PORT = 443
12
+
13
+ # @param homepage_uri [String, nil]
14
+ # @param source_code_uri [String, nil]
15
+ # @param changelog_uri [String, nil]
11
16
  # @param version [String]
12
- # @return [Gemview::GitRepo|nil]
13
- def self.from_urls(urls:, version:)
14
- @from_urls ||= {}
15
-
16
- base_url, git_host = nil
17
- urls.each do |url|
18
- base_url, git_host = parse_base_url(url)
19
- if base_url && git_host
20
- return @from_urls[base_url] ||= new(
21
- base_url: base_url,
17
+ # @return [Gemview::GitRepo, nil]
18
+ def self.from_urls(homepage_uri:, source_code_uri:, changelog_uri:, version:)
19
+ [homepage_uri, source_code_uri, changelog_uri].compact.each do |uri|
20
+ base_uri, git_host = parse_base_uri(uri)
21
+ if base_uri && git_host
22
+ return new(
23
+ base_uri: base_uri,
24
+ changelog_uri: changelog_uri,
22
25
  git_host: git_host,
23
26
  version: version
24
27
  )
@@ -28,99 +31,176 @@ module Gemview
28
31
  end
29
32
 
30
33
  # @param [String]
31
- # @return [base_url as `String` and git_host as `Symbol`] or nil if unsuccessful
32
- def self.parse_base_url(url)
33
- github_base_url = url[%r{^https?://github\.com/[^/]+/[^/]+}, 0]
34
- return [github_base_url, GITHUB] if github_base_url
34
+ # @return [base_uri as `String` and git_host as `Symbol`] or nil if unsuccessful
35
+ def self.parse_base_uri(uri)
36
+ github_base_uri = uri[%r{^https?://github\.com/[^/]+/[^/]+}, 0]
37
+ return [github_base_uri, GITHUB] if github_base_uri
38
+
39
+ gitlab_base_uri = uri[%r{^https?://gitlab\.com/[^/]+/[^/]+}, 0]
40
+ return [gitlab_base_uri, GITLAB] if gitlab_base_uri
35
41
 
36
- gitlab_base_url = url[%r{^https?://gitlab\.com/[^/]+/[^/]+}, 0]
37
- [gitlab_base_url, GITLAB] if gitlab_base_url
42
+ codeberg_base_uri = uri[%r{^https?://codeberg\.org/[^/]+/[^/]+}, 0]
43
+ [codeberg_base_uri, CODEBERG] if codeberg_base_uri
38
44
  end
39
45
 
40
- private_class_method :new, :parse_base_url
46
+ private_class_method :new
41
47
 
42
- attr_reader :base_url, :git_host, :version
48
+ attr_reader :base_uri, :changelog_uri, :git_host, :version
43
49
 
44
- # @param base_url [String] base Git repo url for `HOSTS`
50
+ # @param base_uri [String] base Git repo uri for `HOSTS`
51
+ # @param changelog_uri [String, nil] from the gem metadata
45
52
  # @param git_host [Symbol] from `HOSTS`
46
53
  # @param version [String]
47
- def initialize(base_url:, git_host:, version:)
54
+ def initialize(base_uri:, changelog_uri:, git_host:, version:)
48
55
  raise ArgumentError, "Invalid host: #{git_host}" unless HOSTS.include?(git_host)
49
56
 
50
- @base_url = base_url.dup.freeze
57
+ @base_uri = base_uri.dup.freeze
58
+ @changelog_uri = changelog_uri.dup.freeze
51
59
  @git_host = git_host
52
60
  @version = version.dup.freeze
53
61
  end
54
62
 
55
- # @return [String|nil]
63
+ # @return [Boolean]
64
+ def readme? = !defined?(@readme) || !readme.nil?
65
+
66
+ # @return [String, nil]
56
67
  def readme
57
68
  return @readme if defined?(@readme)
58
69
 
59
70
  @readme = fetch_raw_file("README.md")
60
71
  end
61
72
 
62
- # @return [String|nil]
73
+ # @return [Boolean]
74
+ def changelog? = !defined?(@changelog) || !changelog.nil?
75
+
76
+ # @return [String, nil]
63
77
  def changelog
64
78
  return @changelog if defined?(@changelog)
65
79
 
66
- @changelog = fetch_raw_file("CHANGELOG.md")
80
+ filenames = [changelog_filename, "CHANGELOG.md"].compact.uniq
81
+ filenames.each do |filename|
82
+ break if (@changelog = fetch_raw_file(filename))
83
+ end
84
+
85
+ @changelog
67
86
  end
68
87
 
69
88
  private
70
89
 
90
+ def changelog_filename
91
+ return unless @changelog_uri&.end_with?(".md")
92
+
93
+ changelog_base_uri, changelog_git_host = self.class.parse_base_uri(@changelog_uri)
94
+ return if changelog_base_uri != base_uri
95
+ return if changelog_git_host != git_host
96
+
97
+ @changelog_uri.split("/").last
98
+ end
99
+
71
100
  # @param filename [String]
72
- # @return [String|nil]
101
+ # @return [String, nil]
73
102
  def fetch_raw_file(filename)
74
103
  case @git_host
75
104
  when GITHUB then github_raw_file(filename)
76
105
  when GITLAB then gitlab_raw_file(filename)
106
+ when CODEBERG then codeberg_raw_file(filename)
77
107
  end
78
108
  end
79
109
 
80
110
  # @param filename [String]
81
- # @return [String|nil]
111
+ # @return [String, nil]
82
112
  def github_raw_file(filename)
83
113
  # From: `https://github.com/charmbracelet/bubbles`
84
114
  # To: `https://raw.githubusercontent.com/charmbracelet/bubbles/refs/tags/v0.20.0/README.md`
85
- path = @base_url.sub(%r{^https?://github\.com}, "")
86
-
87
- [
88
- "https://raw.githubusercontent.com#{path}/refs/tags/v#{@version}/#{filename}",
89
- "https://raw.githubusercontent.com#{path}/refs/tags/#{@version}/#{filename}"
90
- ].each do |url|
91
- content = fetch(url)
92
- return content if content
93
- end
94
- nil
115
+ path = @base_uri.sub(%r{^https?://github\.com}, "")
116
+
117
+ fetch_markdown(
118
+ host: "raw.githubusercontent.com",
119
+ tag_paths: [
120
+ "#{path}/refs/tags/v#{@version}/#{filename}",
121
+ "#{path}/refs/tags/#{@version}/#{filename}"
122
+ ],
123
+ head_paths: [
124
+ "#{path}/refs/heads/main/#{filename}",
125
+ "#{path}/refs/heads/master/#{filename}"
126
+ ]
127
+ )
95
128
  end
96
129
 
97
130
  # @param filename [String]
98
- # @return [String|nil]
131
+ # @return [String, nil]
99
132
  def gitlab_raw_file(filename)
100
133
  # From: `https://gitlab.com/gitlab-org/gitlab`
101
- # To: `https://gitlab.com/gitlab-org/gitlab/-/raw/v17.5.1-ee/README.md?ref_type=tags&inline=false`
102
- path = @base_url.sub(%r{^https?://gitlab\.com}, "")
103
-
104
- [
105
- "https://gitlab.com#{path}/-/raw/v#{@version}/#{filename}?ref_type=tags&inline=false",
106
- "https://gitlab.com#{path}/-/raw/#{@version}/#{filename}?ref_type=tags&inline=false"
107
- ].each do |url|
108
- content = fetch(url)
109
- return content if content
110
- end
111
- nil
134
+ # To: `https://gitlab.com/gitlab-org/gitlab/-/raw/v17.5.1-ee/README.md`
135
+ path = @base_uri.sub(%r{^https?://gitlab\.com}, "")
136
+
137
+ fetch_markdown(
138
+ host: "gitlab.com",
139
+ tag_paths: [
140
+ "#{path}/-/raw/v#{@version}/#{filename}",
141
+ "#{path}/-/raw/#{@version}/#{filename}"
142
+ ],
143
+ head_paths: [
144
+ "#{path}/-/raw/main/#{filename}",
145
+ "#{path}/-/raw/master/#{filename}"
146
+ ]
147
+ )
148
+ end
149
+
150
+ # @param filename [String]
151
+ # @return [String, nil]
152
+ def codeberg_raw_file(filename)
153
+ # From: `https://codeberg.org/bendangelo/wiktionary_api`
154
+ # To: `https://codeberg.org/bendangelo/wiktionary_api/raw/tag/v0.1.1/README.md`
155
+ path = @base_uri.sub(%r{^https?://codeberg\.org}, "")
156
+
157
+ fetch_markdown(
158
+ host: "codeberg.org",
159
+ tag_paths: [
160
+ "#{path}/raw/tag/v#{@version}/#{filename}",
161
+ "#{path}/raw/tag/#{@version}/#{filename}"
162
+ ],
163
+ head_paths: [
164
+ "#{path}/raw/branch/main/#{filename}",
165
+ "#{path}/raw/branch/master/#{filename}"
166
+ ]
167
+ )
112
168
  end
113
169
 
114
- # @param url [String]
115
- # @return [String|nil]
116
- def fetch(url)
117
- response = Net::HTTP.get_response(URI(url))
118
- if response.is_a?(Net::HTTPSuccess)
119
- body = response.body.force_encoding("UTF-8")
120
- Terminal.prettify_markdown(body)
170
+ # @param host [String]
171
+ # @param tag_paths [Array<String>]
172
+ # @param head_paths [Array<String>]
173
+ # @return [String, nil]
174
+ def fetch_markdown(host:, tag_paths:, head_paths:)
175
+ body = nil
176
+
177
+ Net::HTTP.start(host, HTTPS_PORT, use_ssl: true, open_timeout: 2, read_timeout: 2) do |http|
178
+ tag_paths.each do |path|
179
+ response = http.get(path)
180
+ if response.is_a?(Net::HTTPSuccess)
181
+ body = response.body.force_encoding("UTF-8")
182
+ break
183
+ end
184
+ end
185
+
186
+ unless body
187
+ head_paths.each do |path|
188
+ response = http.get(path)
189
+ if response.is_a?(Net::HTTPSuccess)
190
+ body = <<~BODY
191
+ *FYI*: This was fetched from the HEAD branch of the Git repository.
192
+
193
+ #{response.body.force_encoding("UTF-8")}
194
+ BODY
195
+ break
196
+ end
197
+ end
198
+ end
121
199
  end
122
- rescue Net::HTTPError
123
- nil
200
+
201
+ Terminal.prettify_markdown(body) if body
202
+ rescue Net::OpenTimeout, Net::ReadTimeout
203
+ nil # this is best effort so we silence network errors here
124
204
  end
125
205
  end
126
206
  end
@@ -2,6 +2,11 @@
2
2
 
3
3
  module Gemview
4
4
  module Terminal
5
+ # Clears the screen using escape codes.
6
+ def self.clear_screen
7
+ print "\e[2J\e[f"
8
+ end
9
+
5
10
  # @param question [String]
6
11
  # @return [Boolean]
7
12
  def self.confirm(question:)
@@ -10,15 +15,18 @@ module Gemview
10
15
 
11
16
  # @param content [String]
12
17
  def self.page(content)
13
- TTY::Pager.page(content)
18
+ # Override the default pager command so that it is top justified to match the choice menus.
19
+ TTY::Pager::SystemPager.new(command: "less -c -r --tilde").page(content)
14
20
  end
15
21
 
16
22
  # @param prompt [String]
17
- # @param choices [Array<String>] or [Hash<String, String>] where all choices are unique
23
+ # @param choices [Array<String, Hash>, Proc] where all choices are unique
18
24
  # @yield [String] yields until the user exits the prompt gracefully
19
25
  def self.choose(message:, choices:, per_page: 6)
20
- while (choice = selector.select(message, choices, per_page))
21
- yield choice
26
+ loop do
27
+ choice_list = choices.is_a?(Proc) ? choices.call : choices
28
+ choice = selector.select(message, choice_list, per_page)
29
+ choice ? yield(choice) : break
22
30
  end
23
31
  end
24
32
 
@@ -82,10 +90,12 @@ module Gemview
82
90
  end
83
91
 
84
92
  # @param prompt [String]
85
- # @param choices [Array<String>] where all choices are unique
93
+ # @param choices [Array<String, Hash>] where all choices are unique
86
94
  # @param per_page [Integer] results per page
87
- # @return [String|nil]
95
+ # @return [String, nil]
88
96
  def select(message, choices, per_page)
97
+ # TODO: Add support for preserving the previous selection if this ever gets fixed upstream.
98
+ # Issue: https://github.com/piotrmurach/tty-prompt/issues/206
89
99
  choice = @prompt.select(
90
100
  message,
91
101
  choices,
@@ -93,10 +103,22 @@ module Gemview
93
103
  help: "(Press Enter to select and Escape to leave)",
94
104
  show_help: :always
95
105
  )
106
+
96
107
  choice unless @exit
97
108
  ensure
98
109
  @exit = false
99
110
  end
111
+
112
+ private
113
+
114
+ # @param choice [String, nil]
115
+ # @param choices [Array<String, Hash>]
116
+ # @return [Boolean]
117
+ def disabled_choice?(choice, choices)
118
+ choices.any? do |possible_choice|
119
+ possible_choice in {name: ^choice, disabled: String}
120
+ end
121
+ end
100
122
  end
101
123
  private_constant :Selector
102
124
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Gemview
4
- VERSION = "1.0.0"
4
+ VERSION = "1.1.0"
5
5
  end
data/lib/gemview/view.rb CHANGED
@@ -3,18 +3,28 @@
3
3
  module Gemview
4
4
  module View
5
5
  def self.info(gem:)
6
- gem = Gem.find(name: gem.name, version: gem.version) if gem.dependencies.nil?
6
+ gem = Gem.find(name: gem.name, version: gem.version) unless gem.dependencies?
7
7
  prompt = <<~PROMPT.chomp
8
8
  #{gem.header_str}
9
9
  More info:
10
10
  PROMPT
11
11
 
12
- Terminal.choose(message: prompt, choices: %w[Readme Changelog Dependencies Versions]) do |choice|
12
+ choices_proc = proc do
13
+ [
14
+ {name: "Readme", disabled: gem.readme? ? nil : "(missing)"},
15
+ {name: "Changelog", disabled: gem.changelog? ? nil : "(missing)"},
16
+ "Dependencies",
17
+ "Versions"
18
+ ]
19
+ end
20
+
21
+ Terminal.clear_screen
22
+ Terminal.choose(message: prompt, choices: choices_proc) do |choice|
13
23
  case choice
14
24
  when "Readme"
15
- Terminal.page([gem.header_str, gem.fetch_readme].join("\n"))
25
+ Terminal.page([gem.header_str, gem.readme].join("\n")) if gem.readme
16
26
  when "Changelog"
17
- Terminal.page([gem.header_str, gem.fetch_changelog].join("\n"))
27
+ Terminal.page([gem.header_str, gem.changelog].join("\n")) if gem.changelog
18
28
  when "Dependencies"
19
29
  Terminal.page([gem.header_str, gem.dependencies_str].join("\n"))
20
30
  when "Versions"
@@ -30,8 +40,9 @@ module Gemview
30
40
  [gem.selector_str, gem]
31
41
  end
32
42
 
33
- Terminal.choose(message: "Choose a gem:", choices: gems_by_description) do |gem|
34
- info(gem: gem)
43
+ Terminal.clear_screen
44
+ Terminal.choose(message: "Choose a gem:", choices: gems_by_description.keys) do |description|
45
+ info(gem: gems_by_description.fetch(description))
35
46
  end
36
47
  end
37
48
  end
data/lib/gemview.rb CHANGED
@@ -19,9 +19,6 @@ end
19
19
  # External
20
20
  autoload :Gems, "gems"
21
21
  autoload :Strings, "strings"
22
- module Net
23
- autoload :HTTP, "net/http"
24
- end
25
22
 
26
23
  module TTY
27
24
  autoload :Markdown, "tty-markdown"
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gemview
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
- - apainintheneck
8
- autorequire:
7
+ - Kevin Robell
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2024-12-08 00:00:00.000000000 Z
10
+ date: 2025-08-17 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: dry-cli
@@ -24,20 +23,6 @@ dependencies:
24
23
  - - "~>"
25
24
  - !ruby/object:Gem::Version
26
25
  version: 1.2.0
27
- - !ruby/object:Gem::Dependency
28
- name: dry-struct
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - "~>"
32
- - !ruby/object:Gem::Version
33
- version: 1.6.0
34
- type: :runtime
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - "~>"
39
- - !ruby/object:Gem::Version
40
- version: 1.6.0
41
26
  - !ruby/object:Gem::Dependency
42
27
  name: gems
43
28
  requirement: !ruby/object:Gem::Requirement
@@ -108,20 +93,6 @@ dependencies:
108
93
  - - "~>"
109
94
  - !ruby/object:Gem::Version
110
95
  version: 0.23.1
111
- - !ruby/object:Gem::Dependency
112
- name: zeitwerk
113
- requirement: !ruby/object:Gem::Requirement
114
- requirements:
115
- - - "<"
116
- - !ruby/object:Gem::Version
117
- version: '2.7'
118
- type: :runtime
119
- prerelease: false
120
- version_requirements: !ruby/object:Gem::Requirement
121
- requirements:
122
- - - "<"
123
- - !ruby/object:Gem::Version
124
- version: '2.7'
125
96
  description: 'An unofficial CLI interface to browse rubygems.org. Search for gems
126
97
  by name, see which ones have been recently updated and look at their dependencies.
127
98
 
@@ -155,7 +126,6 @@ metadata:
155
126
  homepage_uri: https://github.com/apainintheneck/gemview
156
127
  source_code_uri: https://github.com/apainintheneck/gemview
157
128
  changelog_uri: https://github.com/apainintheneck/gemview/blob/main/CHANGELOG.md
158
- post_install_message:
159
129
  rdoc_options: []
160
130
  require_paths:
161
131
  - lib
@@ -170,8 +140,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
170
140
  - !ruby/object:Gem::Version
171
141
  version: '0'
172
142
  requirements: []
173
- rubygems_version: 3.5.23
174
- signing_key:
143
+ rubygems_version: 3.6.2
175
144
  specification_version: 4
176
145
  summary: An unofficial CLI interface to browse rubygems.org
177
146
  test_files: []