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 +4 -4
- data/CHANGELOG.md +18 -0
- data/README.md +15 -1
- data/lib/gemview/commands.rb +11 -2
- data/lib/gemview/gem.rb +119 -88
- data/lib/gemview/git_repo.rb +139 -59
- data/lib/gemview/terminal.rb +35 -6
- data/lib/gemview/version.rb +1 -1
- data/lib/gemview/view.rb +20 -8
- data/lib/gemview.rb +0 -4
- metadata +4 -35
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8629621d995d5e02e26408fe60b949ab64154916223d615ace7fe9a4b76afc3f
|
4
|
+
data.tar.gz: 9f1d30fcdae52a7a465fb993f16a298ac17cbdd09362c114b7c98f5c47babeb8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
3
|
+
[](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
|

|
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
|
data/lib/gemview/commands.rb
CHANGED
@@ -40,15 +40,24 @@ module Gemview
|
|
40
40
|
|
41
41
|
argument :term, type: :string, required: true, desc: "Search term"
|
42
42
|
|
43
|
-
|
43
|
+
option :downloads, values: %w[total version], desc: "Sort results by most downloads"
|
44
44
|
|
45
|
-
|
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
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
50
|
-
|
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
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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(
|
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,
|
87
|
-
|
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 =
|
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 =
|
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 [
|
153
|
-
def
|
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
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
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 [
|
168
|
-
def
|
169
|
-
|
170
|
-
|
171
|
-
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
data/lib/gemview/git_repo.rb
CHANGED
@@ -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
|
-
|
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
|
13
|
-
def self.from_urls(
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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 [
|
32
|
-
def self.
|
33
|
-
|
34
|
-
return [
|
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
|
-
|
37
|
-
[
|
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
|
46
|
+
private_class_method :new
|
41
47
|
|
42
|
-
attr_reader :
|
48
|
+
attr_reader :base_uri, :changelog_uri, :git_host, :version
|
43
49
|
|
44
|
-
# @param
|
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(
|
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
|
-
@
|
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 [
|
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 [
|
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
|
-
|
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
|
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
|
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 = @
|
86
|
-
|
87
|
-
|
88
|
-
"
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
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
|
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
|
102
|
-
path = @
|
103
|
-
|
104
|
-
|
105
|
-
"
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
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
|
115
|
-
# @
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
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
|
-
|
123
|
-
|
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
|
data/lib/gemview/terminal.rb
CHANGED
@@ -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
|
-
|
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
|
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
|
-
|
21
|
-
|
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
|
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
|
data/lib/gemview/version.rb
CHANGED
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)
|
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
|
-
|
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([
|
26
|
+
Terminal.page([title, gem.readme].join("\n")) if gem.readme
|
16
27
|
when "Changelog"
|
17
|
-
Terminal.page([
|
28
|
+
Terminal.page([title, gem.changelog].join("\n")) if gem.changelog
|
18
29
|
when "Dependencies"
|
19
|
-
Terminal.page([
|
30
|
+
Terminal.page([title, gem.dependencies_str].join("\n"))
|
20
31
|
when "Versions"
|
21
|
-
Terminal.page([
|
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.
|
34
|
-
|
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.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
|
-
-
|
8
|
-
autorequire:
|
7
|
+
- Kevin Robell
|
9
8
|
bindir: exe
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
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.
|
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: []
|