gemview 1.0.0 → 1.2.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: 8629621d995d5e02e26408fe60b949ab64154916223d615ace7fe9a4b76afc3f
4
+ data.tar.gz: 9f1d30fcdae52a7a465fb993f16a298ac17cbdd09362c114b7c98f5c47babeb8
5
5
  SHA512:
6
- metadata.gz: c997980d1fca42aa4bb49a1f7d43443591907d04f3c20f93a6b72f045a77a8a49a9b85499cd3152b6ccbb349324417dd91be9323b78e57aa1b2825905504b5d8
7
- data.tar.gz: deec958df2e6515729a091d3f073df221297c01562ae2af1de65b787a566c7b46dde6d3260aa5bbf6063c3978982a225f259a2e3ff27135e4227c59c392a4535
6
+ metadata.gz: 58e9f2b048c6a20bd354d296709a28b3ecea843dbafb6b80f29ef4ffe9532d01fd5db4fadf01c0b2be0dd2457212c44d33395c1a6c667128f10e93d4ff0fea29
7
+ data.tar.gz: 1c28c79d4349727a7eb54be5a46ab865d3784b9b8277a31e5ce88063e69eaddd1f9459f46df522b9963a04b68e3805d870b5e65b165f303f0eced79bea2be344
data/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.2.0] - 2025-08-30
4
+
5
+ - Add prompt message to pager
6
+ - Add navigation section to the readme
7
+
8
+ - Remove table from info pages
9
+
10
+ ## [1.1.0] - 2025-08-16
11
+
12
+ - Add better support for showing the changelog
13
+ - Add ability to sort search results by total and version downloads
14
+ - Add ruby version to gem version output
15
+ - Add ability to disable readme and changelog viewing if they are unreachable
16
+ - Add support for Codeberg readmes and changelogs (before only Github and Gitlab were supported)
17
+
18
+ - Change pager to align TUI at the top of the screen
19
+ - Change default pager to use less for consistent paging
20
+
3
21
  ## [1.0.0] - 2024-12-08
4
22
 
5
23
  - 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
 
@@ -16,10 +18,22 @@ Commands:
16
18
  gemview version # Print version
17
19
  ```
18
20
 
21
+ ## Navigation
22
+
23
+ For menus the arrow keys and j/k can be used for movement. Press enter to select a choice and escape or q to return to the previous menu or quit.
24
+
25
+ For paged content `less` is used internally so all the normal navigation commands work as expected.
26
+
19
27
  ## Demo
20
28
 
21
29
  ![GIF demoing the info and search subcommands](./assets/gemview-v1.0.0.gif)
22
30
 
31
+ ## Implementation
32
+
33
+ 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.
34
+
35
+ 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.
36
+
23
37
  ## Development
24
38
 
25
39
  ### 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
64
+ class Version
65
+ attr_reader :version, :downloads, :release_date, :ruby_version
60
66
 
61
- attribute :downloads_count, Types::Strict::Integer
62
- alias_method :downloads, :downloads_count
63
-
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,16 +81,29 @@ module Gemview
75
81
 
76
82
  # @return [String]
77
83
  def selector_str
84
+ one_line_info = info.gsub(/\s+/, " ").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
 
92
+ # @return [String]
93
+ def title_str(subsection:)
94
+ Terminal.prettify_markdown(<<~LINE)
95
+ ## [#{version}] #{name} >> #{subsection}
96
+ LINE
97
+ end
98
+
84
99
  # @return [String]
85
100
  def header_str
86
- info_lines = Strings.wrap(info, 80).lines.map(&:strip)
87
- info_lines = info_lines.take(3).append("...") if info_lines.size > 3
101
+ info_lines = Strings.wrap(info.gsub(/\s+/, " ").strip, 79).lines.map(&:strip)
102
+
103
+ if info_lines.size > 3
104
+ info_lines = info_lines.take(3)
105
+ info_lines.last << "…"
106
+ end
88
107
 
89
108
  header = <<~HEADER
90
109
  ## [#{version}] #{name}
@@ -103,16 +122,21 @@ module Gemview
103
122
  Terminal.prettify_markdown(header)
104
123
  end
105
124
 
125
+ # @return [Boolean]
126
+ def dependencies?
127
+ !runtime_dependencies.nil? && !development_dependencies.nil?
128
+ end
129
+
106
130
  # @return [String]
107
131
  def dependencies_str
108
- runtime_deps_str = dependencies.runtime.join("\n").strip
132
+ runtime_deps_str = runtime_dependencies.join("\n").strip
109
133
  runtime_deps_str = if runtime_deps_str.empty?
110
134
  "(none)"
111
135
  else
112
136
  "```rb\n#{runtime_deps_str}\n```"
113
137
  end
114
138
 
115
- dev_deps_str = dependencies.development.join("\n").strip
139
+ dev_deps_str = development_dependencies.join("\n").strip
116
140
  dev_deps_str = if dev_deps_str.empty?
117
141
  "(none)"
118
142
  else
@@ -132,46 +156,53 @@ module Gemview
132
156
  Terminal.prettify_markdown(dependencies)
133
157
  end
134
158
 
159
+ # @return [String]
135
160
  def versions_str
136
161
  rows = self.class.versions(name: name).map do |version|
137
162
  pretty_downloads = Number.humanized_integer(version.downloads)
138
- "| #{version.release_date} | #{version.version} | #{pretty_downloads} |"
163
+ "| #{version.release_date} | #{version.version} | #{pretty_downloads} | #{version.ruby_version}"
139
164
  end
140
165
 
141
166
  table = <<~TABLE
142
167
  ## [Versions]
143
168
 
144
- | *Release Date* | *Version* | *Downloads* |
145
- |----------------|-----------|-------------|
169
+ | *Release Date* | *Gem Version* | *Downloads* | *Ruby Version* |
170
+ |----------------|---------------|-------------|----------------|
146
171
  #{rows.join("\n")}
147
172
  TABLE
148
173
 
149
174
  Terminal.prettify_markdown(table)
150
175
  end
151
176
 
152
- # @return [Array<String>]
153
- def urls
154
- [
155
- homepage_uri,
156
- source_code_uri,
157
- changelog_uri
158
- ].compact
159
- end
177
+ # @return [Gemview::GitRepo, nil]
178
+ def git_repo
179
+ return @git_repo if defined? @git_repo
160
180
 
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"
181
+ @git_repo = GitRepo.from_urls(
182
+ homepage_uri: homepage_uri,
183
+ source_code_uri: source_code_uri,
184
+ changelog_uri: changelog_uri,
185
+ version: version
186
+ )
165
187
  end
166
188
 
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
189
+ # @return [Boolean]
190
+ def git_repo? = !git_repo.nil?
191
+
192
+ # @return [Boolean]
193
+ def readme? = git_repo? && git_repo.readme?
194
+
195
+ # @return [String, nil]
196
+ def readme = git_repo&.readme
197
+
198
+ # @return [Boolean]
199
+ def changelog? = git_repo? && git_repo.changelog?
200
+
201
+ # @return [String, nil]
202
+ def changelog = git_repo&.changelog
172
203
 
173
204
  # @param name [String]
174
- # @param version [String|nil] will default to latest if not provided
205
+ # @param version [String, nil] will default to latest if not provided
175
206
  # @return [Gemview::Gem]
176
207
  def self.find(name:, version: nil)
177
208
  @find ||= {}
@@ -186,30 +217,30 @@ module Gemview
186
217
  # @param term [String] search term
187
218
  # @return [Array<Gemview::Gem>]
188
219
  def self.search(term:)
189
- Client.v1.search(term).map { |gem_hash| new gem_hash }
220
+ Client.v1.search(term).map { |gem_hash| new(gem_hash) }
190
221
  end
191
222
 
192
223
  # @param username [String] rubygems.org username
193
224
  # @return [Array<Gemview::Gem>]
194
225
  def self.author(username:)
195
- Client.v1.gems(username).map { |gem_hash| new gem_hash }
226
+ Client.v1.gems(username).map { |gem_hash| new(gem_hash) }
196
227
  end
197
228
 
198
229
  # @return [Array<Gemview::Gem>]
199
230
  def self.latest
200
- Client.v1.latest.map { |gem_hash| new gem_hash }
231
+ Client.v1.latest.map { |gem_hash| new(gem_hash) }
201
232
  end
202
233
 
203
234
  # @return [Array<Gemview::Gem>]
204
235
  def self.just_updated
205
- Client.v1.just_updated.map { |gem_hash| new gem_hash }
236
+ Client.v1.just_updated.map { |gem_hash| new(gem_hash) }
206
237
  end
207
238
 
208
239
  # @param name [String] gem name
209
240
  # @return [Array<Gemview::Gem::Version>]
210
241
  def self.versions(name:)
211
242
  @versions ||= {}
212
- @versions[name] ||= Client.v1.versions(name).map { |gem_hash| Version.new gem_hash }
243
+ @versions[name] ||= Client.v1.versions(name).map { |gem_hash| Version.new(gem_hash).freeze }.freeze
213
244
  end
214
245
  end
215
246
  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,25 @@ module Gemview
10
15
 
11
16
  # @param content [String]
12
17
  def self.page(content)
13
- TTY::Pager.page(content)
18
+ command = [
19
+ "less",
20
+ "--clear-screen", # make sure everything is top-justified
21
+ "--RAW-CONTROL-CHARS", # correctly interpret ANSI control sequences
22
+ "--tilde", # don't show tildes on lines after the output
23
+ "--prompt='(press h for help or q to quit)'"
24
+ ].join(" ")
25
+
26
+ TTY::Pager::SystemPager.new(command: command).page(content)
14
27
  end
15
28
 
16
29
  # @param prompt [String]
17
- # @param choices [Array<String>] or [Hash<String, String>] where all choices are unique
30
+ # @param choices [Array<String, Hash>, Proc] where all choices are unique
18
31
  # @yield [String] yields until the user exits the prompt gracefully
19
32
  def self.choose(message:, choices:, per_page: 6)
20
- while (choice = selector.select(message, choices, per_page))
21
- yield choice
33
+ loop do
34
+ choice_list = choices.is_a?(Proc) ? choices.call : choices
35
+ choice = selector.select(message, choice_list, per_page)
36
+ choice ? yield(choice) : break
22
37
  end
23
38
  end
24
39
 
@@ -82,10 +97,12 @@ module Gemview
82
97
  end
83
98
 
84
99
  # @param prompt [String]
85
- # @param choices [Array<String>] where all choices are unique
100
+ # @param choices [Array<String, Hash>] where all choices are unique
86
101
  # @param per_page [Integer] results per page
87
- # @return [String|nil]
102
+ # @return [String, nil]
88
103
  def select(message, choices, per_page)
104
+ # TODO: Add support for preserving the previous selection if this ever gets fixed upstream.
105
+ # Issue: https://github.com/piotrmurach/tty-prompt/issues/206
89
106
  choice = @prompt.select(
90
107
  message,
91
108
  choices,
@@ -93,10 +110,22 @@ module Gemview
93
110
  help: "(Press Enter to select and Escape to leave)",
94
111
  show_help: :always
95
112
  )
113
+
96
114
  choice unless @exit
97
115
  ensure
98
116
  @exit = false
99
117
  end
118
+
119
+ private
120
+
121
+ # @param choice [String, nil]
122
+ # @param choices [Array<String, Hash>]
123
+ # @return [Boolean]
124
+ def disabled_choice?(choice, choices)
125
+ choices.any? do |possible_choice|
126
+ possible_choice in {name: ^choice, disabled: String}
127
+ end
128
+ end
100
129
  end
101
130
  private_constant :Selector
102
131
  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.2.0"
5
5
  end
data/lib/gemview/view.rb CHANGED
@@ -3,22 +3,33 @@
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|
23
+ title = gem.title_str(subsection: choice)
13
24
  case choice
14
25
  when "Readme"
15
- Terminal.page([gem.header_str, gem.fetch_readme].join("\n"))
26
+ Terminal.page([title, gem.readme].join("\n")) if gem.readme
16
27
  when "Changelog"
17
- Terminal.page([gem.header_str, gem.fetch_changelog].join("\n"))
28
+ Terminal.page([title, gem.changelog].join("\n")) if gem.changelog
18
29
  when "Dependencies"
19
- Terminal.page([gem.header_str, gem.dependencies_str].join("\n"))
30
+ Terminal.page([title, gem.dependencies_str].join("\n"))
20
31
  when "Versions"
21
- Terminal.page([gem.header_str, gem.versions_str].join("\n"))
32
+ Terminal.page([title, gem.versions_str].join("\n"))
22
33
  else
23
34
  raise ArgumentError, "Unknown choice: #{choice}"
24
35
  end
@@ -30,8 +41,9 @@ module Gemview
30
41
  [gem.selector_str, gem]
31
42
  end
32
43
 
33
- Terminal.choose(message: "Choose a gem:", choices: gems_by_description) do |gem|
34
- info(gem: gem)
44
+ Terminal.clear_screen
45
+ Terminal.choose(message: "Choose a gem:", choices: gems_by_description.keys) do |description|
46
+ info(gem: gems_by_description.fetch(description))
35
47
  end
36
48
  end
37
49
  end
data/lib/gemview.rb CHANGED
@@ -12,16 +12,12 @@ module Gemview
12
12
  autoload :GitRepo, "gemview/git_repo"
13
13
  autoload :Number, "gemview/number"
14
14
  autoload :Terminal, "gemview/terminal"
15
- autoload :Version, "gemview/version"
16
15
  autoload :View, "gemview/view"
17
16
  end
18
17
 
19
18
  # External
20
19
  autoload :Gems, "gems"
21
20
  autoload :Strings, "strings"
22
- module Net
23
- autoload :HTTP, "net/http"
24
- end
25
21
 
26
22
  module TTY
27
23
  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.2.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-31 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: []