fifi 0.1.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: fb6a94cb40940b9b08d22a254f5c880205c6bc7a32af9cfb3c6c1d657cd3c37d
4
+ data.tar.gz: ce8dd952c35424eac4a11e2bb1ddbaef749cf79b4b852206b94c265d52e4eeed
5
+ SHA512:
6
+ metadata.gz: f071be05fa94c1da9390d5176dcaf7f748e102e19b5bcb5d5cff1056ce2214a89de073d0066d6053b5d22c204ec50bceed825d11443ca900237249881537c984
7
+ data.tar.gz: b5e3bed6d2a0c0806c40832c4abdabdea105aa68e5f5a6cfb0fe32377d39809ae79d5fd3295e63aa6caa01c29c00b73dfa9b93b2bc03fea03d380796e5be466e
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # Fifi
2
+
3
+ Fifi is a tiny CLI to download or install Google Fonts. It pulls font files from
4
+ the Google Fonts GitHub repository via the GitHub API.
5
+
6
+ ## Usage
7
+
8
+ ```
9
+ gem install fifi
10
+
11
+ fifi install nunito
12
+ fifi install nunito, inter, open sans
13
+ fifi download nunito -o assets/fonts
14
+ fifi download nunito
15
+ ```
16
+
17
+ ## Install
18
+
19
+ ```
20
+ gem install fifi
21
+ ```
22
+
23
+ ## Options
24
+
25
+ - `-o, --output DIR`: Download destination for `download`.
26
+ - `-s, --static`: Prefer static fonts instead of variable.
27
+
28
+ ## Notes
29
+
30
+ - The GitHub API is rate-limited. If you hit limits, set `FIFI_GITHUB_TOKEN`
31
+ or `GITHUB_TOKEN` to a personal access token.
32
+
33
+ ## Install
34
+
35
+ Build and install the gem locally:
36
+
37
+ ```
38
+ gem build fifi.gemspec
39
+ gem install ./fifi-0.1.0.gem
40
+ ```
data/bin/fifi ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "fifi"
5
+
6
+ exit Fifi::CLI.start(ARGV)
data/lib/fifi/cli.rb ADDED
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+ require "fileutils"
5
+ require "fifi/google_fonts"
6
+
7
+ module Fifi
8
+ class CLI
9
+ def self.start(argv)
10
+ new(argv).run
11
+ end
12
+
13
+ def initialize(argv)
14
+ @argv = argv.dup
15
+ end
16
+
17
+ def run
18
+ return show_usage if @argv.empty?
19
+ return show_usage if help_flag?(@argv.first)
20
+
21
+ command = @argv.shift
22
+ case command
23
+ when "install"
24
+ run_install(@argv)
25
+ when "download"
26
+ run_download(@argv)
27
+ else
28
+ warn "Unknown command: #{command}"
29
+ show_usage(exit_code: 1)
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def run_install(args)
36
+ options = { variable: true }
37
+ parser = OptionParser.new do |opts|
38
+ opts.banner = "Usage: fifi install <fonts> [options]"
39
+ opts.on("-s", "--static", "Prefer static fonts (default: variable)") do
40
+ options[:variable] = false
41
+ end
42
+ opts.on("-h", "--help", "Show help") do
43
+ puts opts
44
+ return 0
45
+ end
46
+ end
47
+
48
+ parser.parse!(args)
49
+ fonts = parse_fonts(args)
50
+ if fonts.empty?
51
+ warn "No fonts provided."
52
+ puts parser
53
+ return 1
54
+ end
55
+
56
+ font_dir = default_font_dir
57
+ failures = 0
58
+
59
+ fonts.each do |font|
60
+ begin
61
+ files = GoogleFonts.new(font, variable: options[:variable]).fetch(font_dir)
62
+ puts "Installed #{font} (#{files.size} files) to #{font_dir}"
63
+ rescue StandardError => e
64
+ failures += 1
65
+ warn "Failed to install #{font}: #{e.message}"
66
+ end
67
+ end
68
+
69
+ if linux_platform? && failures < fonts.size
70
+ puts "If fonts are not visible, run: fc-cache -f"
71
+ end
72
+
73
+ failures.zero? ? 0 : 1
74
+ end
75
+
76
+ def run_download(args)
77
+ options = { variable: true, output: Dir.pwd }
78
+ parser = OptionParser.new do |opts|
79
+ opts.banner = "Usage: fifi download <fonts> [options]"
80
+ opts.on("-o", "--output DIR", "Output directory (default: current dir)") do |dir|
81
+ options[:output] = dir
82
+ end
83
+ opts.on("-s", "--static", "Prefer static fonts (default: variable)") do
84
+ options[:variable] = false
85
+ end
86
+ opts.on("-h", "--help", "Show help") do
87
+ puts opts
88
+ return 0
89
+ end
90
+ end
91
+
92
+ parser.parse!(args)
93
+ fonts = parse_fonts(args)
94
+ if fonts.empty?
95
+ warn "No fonts provided."
96
+ puts parser
97
+ return 1
98
+ end
99
+
100
+ failures = 0
101
+
102
+ fonts.each do |font|
103
+ begin
104
+ files = GoogleFonts.new(font, variable: options[:variable]).fetch(options[:output])
105
+ puts "Downloaded #{font} (#{files.size} files) to #{options[:output]}"
106
+ rescue StandardError => e
107
+ failures += 1
108
+ warn "Failed to download #{font}: #{e.message}"
109
+ end
110
+ end
111
+
112
+ failures.zero? ? 0 : 1
113
+ end
114
+
115
+ def parse_fonts(args)
116
+ raw = args.join(" ").strip
117
+ return [] if raw.empty?
118
+
119
+ raw.split(",").map { |font| font.strip }.reject(&:empty?)
120
+ end
121
+
122
+ def show_usage(exit_code: 0)
123
+ puts <<~USAGE
124
+ Usage:
125
+ fifi install <fonts> [options]
126
+ fifi download <fonts> [options]
127
+
128
+ Examples:
129
+ fifi install nunito
130
+ fifi install nunito, inter, open sans
131
+ fifi download nunito -o assets/fonts
132
+ fifi download nunito
133
+
134
+ Options:
135
+ -s, --static Prefer static fonts (default: variable)
136
+ -h, --help Show help
137
+ USAGE
138
+ exit_code
139
+ end
140
+
141
+ def help_flag?(arg)
142
+ %w[-h --help help].include?(arg)
143
+ end
144
+
145
+ def default_font_dir
146
+ if mac_platform?
147
+ File.join(Dir.home, "Library", "Fonts")
148
+ elsif windows_platform?
149
+ base = ENV["LOCALAPPDATA"] || File.join(Dir.home, "AppData", "Local")
150
+ File.join(base, "Microsoft", "Windows", "Fonts")
151
+ else
152
+ File.join(Dir.home, ".local", "share", "fonts")
153
+ end
154
+ end
155
+
156
+ def mac_platform?
157
+ /darwin/i.match?(RUBY_PLATFORM)
158
+ end
159
+
160
+ def windows_platform?
161
+ /mswin|mingw|cygwin/i.match?(RUBY_PLATFORM)
162
+ end
163
+
164
+ def linux_platform?
165
+ /linux/i.match?(RUBY_PLATFORM)
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "json"
5
+ require "open-uri"
6
+
7
+ module Fifi
8
+ class GoogleFonts
9
+ REPO_API_BASE = "https://api.github.com/repos/google/fonts/contents"
10
+ LICENSE_DIRS = %w[ofl apache ufl].freeze
11
+
12
+ def initialize(font_name, variable: true)
13
+ @font_name = font_name
14
+ @variable = variable
15
+ end
16
+
17
+ def fetch(destination_dir)
18
+ files = resolve_files
19
+ FileUtils.mkdir_p(destination_dir)
20
+
21
+ files.map do |file|
22
+ target = File.join(destination_dir, file[:name])
23
+ download_file(file[:url], target)
24
+ target
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def resolve_files
31
+ family_url = find_family_dir
32
+ raise "Font family not found in Google Fonts repo" unless family_url
33
+
34
+ entries = fetch_json(family_url)
35
+ static_dir = entries.find { |entry| entry["type"] == "dir" && entry["name"] == "static" }
36
+ root_fonts = entries.select { |entry| entry["type"] == "file" && font_file?(entry["name"]) }
37
+ variable_fonts = root_fonts.select { |entry| variable_file?(entry["name"]) }
38
+ static_root_fonts = root_fonts.reject { |entry| variable_file?(entry["name"]) }
39
+
40
+ chosen = if @variable
41
+ if variable_fonts.any?
42
+ variable_fonts
43
+ elsif static_dir
44
+ list_static_dir(static_dir["url"])
45
+ else
46
+ static_root_fonts.empty? ? root_fonts : static_root_fonts
47
+ end
48
+ else
49
+ if static_dir
50
+ list_static_dir(static_dir["url"])
51
+ elsif static_root_fonts.any?
52
+ static_root_fonts
53
+ else
54
+ variable_fonts.empty? ? root_fonts : variable_fonts
55
+ end
56
+ end
57
+
58
+ chosen.map { |entry| { name: entry["name"], url: entry["download_url"] } }
59
+ .reject { |entry| entry[:url].nil? }
60
+ end
61
+
62
+ def list_static_dir(url)
63
+ entries = fetch_json(url)
64
+ entries.select { |entry| entry["type"] == "file" && font_file?(entry["name"]) }
65
+ end
66
+
67
+ def find_family_dir
68
+ normalized = normalize_family(@font_name)
69
+ LICENSE_DIRS.each do |dir|
70
+ url = "#{REPO_API_BASE}/#{dir}/#{normalized}"
71
+ return url if url_exists?(url)
72
+ end
73
+ nil
74
+ end
75
+
76
+ def url_exists?(url)
77
+ fetch_json(url)
78
+ true
79
+ rescue OpenURI::HTTPError => e
80
+ return false if e.message.include?("404")
81
+ raise "GitHub API error: #{e.message}"
82
+ end
83
+
84
+ def fetch_json(url)
85
+ raw = URI.open(url, github_headers).read
86
+ JSON.parse(raw)
87
+ rescue OpenURI::HTTPError => e
88
+ raise "GitHub API error for #{url}: #{e.message}"
89
+ rescue JSON::ParserError => e
90
+ raise "Unexpected response from GitHub API for #{url}: #{e.message}"
91
+ end
92
+
93
+ def download_file(url, target)
94
+ URI.open(url, "rb", **github_headers) do |io|
95
+ File.open(target, "wb") { |file| IO.copy_stream(io, file) }
96
+ end
97
+ rescue OpenURI::HTTPError => e
98
+ raise "Failed to download #{@font_name} (#{e.message})"
99
+ end
100
+
101
+ def github_headers
102
+ headers = { "User-Agent" => "fifi" }
103
+ token = ENV["FIFI_GITHUB_TOKEN"] || ENV["GITHUB_TOKEN"]
104
+ headers["Authorization"] = "token #{token}" if token && !token.empty?
105
+ headers
106
+ end
107
+
108
+ def normalize_family(name)
109
+ name.downcase.gsub(/[^a-z0-9]/, "")
110
+ end
111
+
112
+ def font_file?(name)
113
+ name.match?(/\.(ttf|otf)$/i)
114
+ end
115
+
116
+ def variable_file?(name)
117
+ name.match?(/\[.+\]/) || name.match?(/VariableFont|\\bVF\\b/i)
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fifi
4
+ VERSION = "0.1.1"
5
+ end
data/lib/fifi.rb ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fifi/version"
4
+ require "fifi/cli"
metadata ADDED
@@ -0,0 +1,52 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fifi
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Fifi Contributors
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-02-12 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Fifi installs or downloads Google Fonts with a simple CLI.
14
+ email: []
15
+ executables:
16
+ - fifi
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - LICENSE.txt
21
+ - README.md
22
+ - bin/fifi
23
+ - lib/fifi.rb
24
+ - lib/fifi/cli.rb
25
+ - lib/fifi/google_fonts.rb
26
+ - lib/fifi/version.rb
27
+ homepage: https://github.com/Greyoxide/fifi
28
+ licenses:
29
+ - MIT
30
+ metadata:
31
+ source_code_uri: https://github.com/Greyoxide/fifi
32
+ bug_tracker_uri: https://github.com/Greyoxide/fifi/issues
33
+ post_install_message:
34
+ rdoc_options: []
35
+ require_paths:
36
+ - lib
37
+ required_ruby_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: 2.7.0
42
+ required_rubygems_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ requirements: []
48
+ rubygems_version: 3.0.3.1
49
+ signing_key:
50
+ specification_version: 4
51
+ summary: Install or download Google Fonts from the terminal.
52
+ test_files: []