fontist 1.13.1 → 1.14.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/fontist/cli.rb CHANGED
@@ -1,10 +1,13 @@
1
1
  require "thor"
2
+ require "fontist/cli/class_options"
2
3
  require "fontist/repo_cli"
3
4
  require "fontist/import_cli"
4
- require "fontist/google_cli"
5
+ require "fontist/fontconfig_cli"
5
6
 
6
7
  module Fontist
7
8
  class CLI < Thor
9
+ include ClassOptions
10
+
8
11
  STATUS_SUCCESS = 0
9
12
  STATUS_UNKNOWN_ERROR = 1
10
13
  STATUS_NON_SUPPORTED_FONT_ERROR = 2
@@ -18,6 +21,9 @@ module Fontist
18
21
  STATUS_REPO_COULD_NOT_BE_UPDATED = 10
19
22
  STATUS_MANUAL_FONT_ERROR = 11
20
23
  STATUS_SIZE_LIMIT_ERROR = 12
24
+ STATUS_FORMULA_NOT_FOUND = 13
25
+ STATUS_FONTCONFIG_NOT_FOUND = 14
26
+ STATUS_FONTCONFIG_FILE_NOT_FOUND = 15
21
27
 
22
28
  ERROR_TO_STATUS = {
23
29
  Fontist::Errors::UnsupportedFontError => [STATUS_NON_SUPPORTED_FONT_ERROR],
@@ -39,6 +45,10 @@ module Fontist
39
45
  Fontist::Errors::FontIndexCorrupted => [STATUS_FONT_INDEX_CORRUPTED],
40
46
  Fontist::Errors::RepoNotFoundError => [STATUS_REPO_NOT_FOUND],
41
47
  Fontist::Errors::MainRepoNotFoundError => [STATUS_MAIN_REPO_NOT_FOUND],
48
+ Fontist::Errors::FormulaNotFoundError => [STATUS_FORMULA_NOT_FOUND],
49
+ Fontist::Errors::FontconfigNotFoundError => [STATUS_FONTCONFIG_NOT_FOUND],
50
+ Fontist::Errors::FontconfigFileNotFoundError =>
51
+ [STATUS_FONTCONFIG_FILE_NOT_FOUND],
42
52
  }.freeze
43
53
 
44
54
  def self.exit_on_failure?
@@ -49,9 +59,18 @@ module Fontist
49
59
  type: :boolean,
50
60
  desc: "Use Preferred Family when available"
51
61
 
62
+ class_option :quiet,
63
+ aliases: :q,
64
+ type: :boolean,
65
+ desc: "Hide all messages"
66
+
67
+ class_option :formulas_path, type: :string, desc: "Path to formulas"
68
+
52
69
  desc "install FONT", "Install font"
53
70
  option :force, type: :boolean, aliases: :f,
54
- desc: "Install even if it's already installed in system"
71
+ desc: "Install even if already installed in system"
72
+ option :formula, type: :boolean, aliases: :F,
73
+ desc: "Install whole formula instead of a font"
55
74
  option :accept_all_licenses, type: :boolean,
56
75
  aliases: ["--confirm-license", :a],
57
76
  desc: "Accept all license agreements"
@@ -62,13 +81,15 @@ module Fontist
62
81
  option :version, type: :string, aliases: :V,
63
82
  desc: "Specify particular version of a font"
64
83
  option :smallest, type: :boolean, aliases: :s,
65
- desc: "Install the smallest formula if several"
84
+ desc: "Install the smallest font by file size if several"
66
85
  option :newest, type: :boolean, aliases: :n,
67
86
  desc: "Install the newest version of a font if several"
68
87
  option :size_limit,
69
88
  type: :numeric, aliases: :S,
70
- desc: "Specify size limit for formula " \
89
+ desc: "Specify upper limit for file size of a formula to be installed" \
71
90
  "(default is #{Fontist.formula_size_limit_in_megabytes} MB)"
91
+ option :update_fontconfig, type: :boolean, aliases: :u,
92
+ desc: "Update fontconfig"
72
93
  def install(font)
73
94
  handle_class_options(options)
74
95
  confirmation = options[:accept_all_licenses] ? "yes" : "no"
@@ -134,8 +155,12 @@ module Fontist
134
155
  end
135
156
 
136
157
  desc "manifest-install MANIFEST", "Install fonts from MANIFEST (yaml)"
137
- option :accept_all_licenses, type: :boolean, aliases: "--confirm-license", desc: "Accept all license agreements"
138
- option :hide_licenses, type: :boolean, desc: "Hide license texts"
158
+ option :accept_all_licenses, type: :boolean,
159
+ aliases: ["--confirm-license", :a],
160
+ desc: "Accept all license agreements"
161
+ option :hide_licenses, type: :boolean,
162
+ aliases: :h,
163
+ desc: "Hide license texts"
139
164
  def manifest_install(manifest)
140
165
  handle_class_options(options)
141
166
  paths = Fontist::Manifest::Install.from_file(
@@ -178,28 +203,17 @@ module Fontist
178
203
  STATUS_SUCCESS
179
204
  end
180
205
 
181
- desc "import-sil", "Import formulas from SIL"
182
- def import_sil
183
- handle_class_options(options)
184
- require "fontist/import/sil_import"
185
- Fontist::Import::SilImport.new.call
186
- end
187
-
188
206
  desc "repo SUBCOMMAND ...ARGS", "Manage custom repositories"
189
207
  subcommand "repo", Fontist::RepoCLI
190
208
 
191
209
  desc "import SUBCOMMAND ...ARGS", "Manage imports"
192
210
  subcommand "import", Fontist::ImportCLI
193
211
 
194
- desc "google SUBCOMMAND ...ARGS", "Manage Google formulas"
195
- subcommand "google", Fontist::GoogleCLI
212
+ desc "fontconfig SUBCOMMAND ...ARGS", "Manage fontconfig"
213
+ subcommand "fontconfig", Fontist::FontconfigCLI
196
214
 
197
215
  private
198
216
 
199
- def handle_class_options(options)
200
- Fontist.preferred_family = options[:preferred_family]
201
- end
202
-
203
217
  def success
204
218
  STATUS_SUCCESS
205
219
  end
@@ -4,6 +4,18 @@ module Fontist
4
4
 
5
5
  class BinaryCallError < GeneralError; end
6
6
 
7
+ class FontconfigNotFoundError < GeneralError
8
+ def initialize
9
+ super("Could not find fontconfig.")
10
+ end
11
+ end
12
+
13
+ class FontconfigFileNotFoundError < GeneralError
14
+ def initialize
15
+ super("Fontist file could not be found in fontconfig configuration.")
16
+ end
17
+ end
18
+
7
19
  class FontIndexCorrupted < GeneralError; end
8
20
 
9
21
  class FontNotFoundError < GeneralError; end
@@ -12,6 +24,16 @@ module Fontist
12
24
  # it depends on this exception to automatically download formulas
13
25
  class FormulaIndexNotFoundError < GeneralError; end
14
26
 
27
+ class FormulaNotFoundError < GeneralError
28
+ def initialize(formula)
29
+ super(<<~MSG.chomp)
30
+ Formula '#{formula}' not found locally nor available in the Fontist formula repository.
31
+ Perhaps it is available at the latest Fontist formula repository.
32
+ You can update the formula repository using the command `fontist update` and try again.
33
+ MSG
34
+ end
35
+ end
36
+
15
37
  class MainRepoNotFoundError < FormulaIndexNotFoundError; end
16
38
 
17
39
  class InvalidResourceError < GeneralError; end
data/lib/fontist/font.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require "fontist/font_installer"
2
2
  require "fontist/font_path"
3
3
  require "fontist/formula_picker"
4
+ require "fontist/fontconfig"
4
5
 
5
6
  module Fontist
6
7
  class Font
@@ -14,6 +15,8 @@ module Fontist
14
15
  @smallest = options[:smallest]
15
16
  @newest = options[:newest]
16
17
  @size_limit = options[:size_limit]
18
+ @by_formula = options[:formula]
19
+ @update_fontconfig = options[:update_fontconfig]
17
20
 
18
21
  check_or_create_fontist_path!
19
22
  end
@@ -48,6 +51,8 @@ module Fontist
48
51
  end
49
52
 
50
53
  def install
54
+ return install_formula if @by_formula
55
+
51
56
  (find_system_font unless @force) || download_font || manual_font ||
52
57
  raise_non_supported_font
53
58
  end
@@ -103,6 +108,22 @@ module Fontist
103
108
  end
104
109
  end
105
110
 
111
+ def install_formula
112
+ download_formula || raise_formula_not_found
113
+ end
114
+
115
+ def download_formula
116
+ formula = Formula.find_by_key(@name)
117
+ return unless formula
118
+ return unless formula.downloadable?
119
+
120
+ request_formula_installation(formula)
121
+ end
122
+
123
+ def raise_formula_not_found
124
+ raise Errors::FormulaNotFoundError.new(@name)
125
+ end
126
+
106
127
  def font_installer(formula)
107
128
  FontInstaller.new(formula, no_progress: @no_progress)
108
129
  end
@@ -147,14 +168,22 @@ module Fontist
147
168
  def download_font
148
169
  return if sufficient_formulas.empty?
149
170
 
150
- sufficient_formulas.flat_map do |formula|
151
- confirmation = check_and_confirm_required_license(formula)
152
- paths = font_installer(formula).install(confirmation: confirmation)
171
+ paths = sufficient_formulas.flat_map do |formula|
172
+ request_formula_installation(formula)
173
+ end
153
174
 
154
- Fontist.ui.say("Fonts installed at:")
155
- paths.each do |path|
156
- Fontist.ui.say("- #{path}")
157
- end
175
+ update_fontconfig
176
+
177
+ paths
178
+ end
179
+
180
+ def request_formula_installation(formula)
181
+ confirmation = check_and_confirm_required_license(formula)
182
+ paths = font_installer(formula).install(confirmation: confirmation)
183
+
184
+ Fontist.ui.say("Fonts installed at:")
185
+ paths.each do |path|
186
+ Fontist.ui.say("- #{path}")
158
187
  end
159
188
  end
160
189
 
@@ -197,6 +226,12 @@ module Fontist
197
226
  MSG
198
227
  end
199
228
 
229
+ def update_fontconfig
230
+ return unless @update_fontconfig
231
+
232
+ Fontconfig.update
233
+ end
234
+
200
235
  def manual_font
201
236
  return if manual_formulas.empty?
202
237
 
@@ -0,0 +1,87 @@
1
+ module Fontist
2
+ class Fontconfig
3
+ def self.update
4
+ new.update
5
+ end
6
+
7
+ def self.remove(options = {})
8
+ new(options).remove
9
+ end
10
+
11
+ def initialize(options = {})
12
+ @options = options
13
+ end
14
+
15
+ def update
16
+ ensure_fontconfig_installed
17
+ create_config
18
+ regenerate_fontconfig_cache
19
+ end
20
+
21
+ def remove
22
+ return handle_file_not_found unless config_exists?
23
+
24
+ regenerate_fontconfig_cache if fontconfig_installed?
25
+ remove_config
26
+ end
27
+
28
+ private
29
+
30
+ def ensure_fontconfig_installed
31
+ raise Errors::FontconfigNotFoundError unless fontconfig_installed?
32
+ end
33
+
34
+ def fontconfig_installed?
35
+ Utils::System.fontconfig_installed?
36
+ end
37
+
38
+ def create_config
39
+ return if File.exist?(config_path)
40
+
41
+ FileUtils.mkdir_p(File.dirname(config_path))
42
+ File.write(config_path, config_content)
43
+ end
44
+
45
+ def config_path
46
+ File.join(xdg_config_home, "fontconfig", "conf.d", "10-fontist.conf")
47
+ end
48
+
49
+ def xdg_config_home
50
+ ENV["XDG_CONFIG_HOME"] || File.join(Dir.home, ".config")
51
+ end
52
+
53
+ def config_content
54
+ <<~CONTENT
55
+ <?xml version='1.0'?>
56
+ <!DOCTYPE fontconfig SYSTEM 'fonts.dtd'>
57
+ <fontconfig>
58
+ <dir>#{Fontist.fonts_path}</dir>
59
+ </fontconfig>
60
+ CONTENT
61
+ end
62
+
63
+ def regenerate_fontconfig_cache
64
+ Helpers.run("fc-cache -f")
65
+ end
66
+
67
+ def ensure_file_exists
68
+ return if @options[:force]
69
+
70
+ raise Errors::FontconfigFileNotFoundError unless File.exist?(config_path)
71
+ end
72
+
73
+ def config_exists?
74
+ File.exist?(config_path)
75
+ end
76
+
77
+ def handle_file_not_found
78
+ return if @options[:force]
79
+
80
+ raise Errors::FontconfigFileNotFoundError
81
+ end
82
+
83
+ def remove_config
84
+ FileUtils.rm(config_path)
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,29 @@
1
+ module Fontist
2
+ class FontconfigCLI < Thor
3
+ include CLI::ClassOptions
4
+
5
+ desc "update", "Update fontconfig configuration to use fontist fonts"
6
+ def update
7
+ handle_class_options(options)
8
+ Fontconfig.update
9
+ Fontist.ui.success("Fontconfig file has been successfully updated.")
10
+ CLI::STATUS_SUCCESS
11
+ rescue Errors::FontconfigNotFoundError => e
12
+ Fontist.ui.error(e.message)
13
+ CLI::STATUS_FONTCONFIG_NOT_FOUND
14
+ end
15
+
16
+ desc "remove", "Remove fontist file in fontconfig configuration"
17
+ option :force, type: :boolean, aliases: :f,
18
+ desc: "Proceed even if does not exist"
19
+ def remove
20
+ handle_class_options(options)
21
+ Fontconfig.remove(options)
22
+ Fontist.ui.success("Fontconfig file has been successfully removed.")
23
+ CLI::STATUS_SUCCESS
24
+ rescue Errors::FontconfigFileNotFoundError => e
25
+ Fontist.ui.error(e.message)
26
+ CLI::STATUS_FONTCONFIG_FILE_NOT_FOUND
27
+ end
28
+ end
29
+ end
@@ -45,6 +45,13 @@ module Fontist
45
45
  end.flatten
46
46
  end
47
47
 
48
+ def self.find_by_key(key)
49
+ path = Fontist.formulas_path.join("#{key}.yml")
50
+ return unless File.exist?(path)
51
+
52
+ new_from_file(path)
53
+ end
54
+
48
55
  def self.new_from_file(path)
49
56
  data = YAML.load_file(path)
50
57
  new(data, path)
@@ -72,7 +79,7 @@ module Fontist
72
79
  end
73
80
 
74
81
  def key
75
- @data["key"] || default_key
82
+ key_from_path
76
83
  end
77
84
 
78
85
  def description
@@ -137,7 +144,7 @@ module Fontist
137
144
 
138
145
  private
139
146
 
140
- def default_key
147
+ def key_from_path
141
148
  escaped = Regexp.escape(Fontist.formulas_path.to_s + "/")
142
149
  @path.sub(Regexp.new("^" + escaped), "").sub(/\.yml$/, "")
143
150
  end
@@ -3,5 +3,26 @@ module Fontist
3
3
  def self.parse_to_object(data)
4
4
  JSON.parse(data.to_json, object_class: OpenStruct)
5
5
  end
6
+
7
+ def self.run(command)
8
+ Fontist.ui.debug("Run `#{command}`")
9
+
10
+ result = `#{command}`
11
+ unless $CHILD_STATUS.to_i.zero?
12
+ raise Errors::BinaryCallError,
13
+ "Failed to run #{command}, status: #{$CHILD_STATUS}"
14
+ end
15
+
16
+ result
17
+ end
18
+
19
+ def self.silence_stream(stream)
20
+ old_stream = stream.dup
21
+ stream.reopen(RbConfig::CONFIG["host_os"] =~ /mswin|mingw/ ? "NUL:" : "/dev/null") # rubocop:disable Performance/RegexpMatch, Metrics/LineLength
22
+ stream.sync = true
23
+ yield
24
+ ensure
25
+ stream.reopen(old_stream)
26
+ end
6
27
  end
7
28
  end
@@ -55,12 +55,32 @@ module Fontist
55
55
  end
56
56
 
57
57
  def save(builder)
58
- filename = Import.name_to_filename(builder.name)
59
- path = @options[:formula_dir] ? File.join(@options[:formula_dir], filename) : filename
58
+ path = vacant_path
60
59
  yaml = YAML.dump(Helpers::HashHelper.stringify_keys(builder.formula))
61
60
  File.write(path, yaml)
62
61
  path
63
62
  end
63
+
64
+ def vacant_path
65
+ path = path_from_name
66
+ return path unless @options[:keep_existing] && File.exist?(path)
67
+
68
+ 2.upto(9) do |i|
69
+ candidate = path.sub(/\.yml$/, "#{i}.yml")
70
+ return candidate unless File.exist?(candidate)
71
+ end
72
+
73
+ raise Errors::GeneralError, "Formula #{path} already exists."
74
+ end
75
+
76
+ def path_from_name
77
+ filename = Import.name_to_filename(builder.name)
78
+ if @options[:formula_dir]
79
+ File.join(@options[:formula_dir], filename)
80
+ else
81
+ filename
82
+ end
83
+ end
64
84
  end
65
85
  end
66
86
  end
@@ -26,9 +26,9 @@ module Fontist
26
26
 
27
27
  def read
28
28
  switch_to_temp_dir do |tmp_dir|
29
- extract_ttfs(tmp_dir).map do |path|
30
- Otf::FontFile.new(path)
31
- end
29
+ extract_ttfs(tmp_dir)
30
+ .map { |path| Otf::FontFile.new(path) }
31
+ .reject { |font_file| hidden_or_pua_encoded?(font_file) }
32
32
  end
33
33
  end
34
34
 
@@ -47,6 +47,10 @@ module Fontist
47
47
  end
48
48
  end
49
49
 
50
+ def hidden_or_pua_encoded?(font_file)
51
+ font_file.family_name.start_with?(".")
52
+ end
53
+
50
54
  def detect_extension
51
55
  base_extension = "ttc"
52
56
 
@@ -4,9 +4,10 @@ require_relative "text_helper"
4
4
  module Fontist
5
5
  module Import
6
6
  class FormulaBuilder
7
- FORMULA_ATTRIBUTES = %i[description homepage resources
7
+ FORMULA_ATTRIBUTES = %i[platforms description homepage resources
8
8
  font_collections fonts extract copyright
9
- license_url open_license digest command].freeze
9
+ license_url requires_license_agreement
10
+ open_license digest command].freeze
10
11
 
11
12
  attr_writer :archive,
12
13
  :url,
@@ -28,9 +29,15 @@ module Fontist
28
29
  def name
29
30
  return @options[:name] if @options[:name]
30
31
 
31
- unique_names = both_fonts.map(&:family_name).uniq
32
- TextHelper.longest_common_prefix(unique_names) ||
33
- both_fonts.first.family_name
32
+ common = %i[family_name type]
33
+ .map { |attr| both_fonts.map(&attr).uniq }
34
+ .map { |names| TextHelper.longest_common_prefix(names) }
35
+ .map { |prefix| prefix unless prefix == "Regular" }
36
+ .compact
37
+ .join(" ")
38
+ return common unless common.empty?
39
+
40
+ both_fonts.map(&:family_name).first
34
41
  end
35
42
 
36
43
  private
@@ -50,6 +57,10 @@ module Fontist
50
57
  files
51
58
  end
52
59
 
60
+ def platforms
61
+ @options[:platforms]
62
+ end
63
+
53
64
  def description
54
65
  name
55
66
  end
@@ -175,12 +186,17 @@ module Fontist
175
186
  both_fonts.map(&:license_url).compact.first
176
187
  end
177
188
 
189
+ def requires_license_agreement
190
+ @options[:requires_license_agreement]
191
+ end
192
+
178
193
  def open_license
179
- unless @license_text
194
+ unless @license_text || requires_license_agreement
180
195
  Fontist.ui.error("WARN: please add license manually")
181
- return
182
196
  end
183
197
 
198
+ return unless @license_text
199
+
184
200
  Fontist.ui.error("WARN: ensure it's an open license, otherwise " \
185
201
  "change the 'open_license' attribute to " \
186
202
  "'requires_license_agreement'")
@@ -4,15 +4,7 @@ module Fontist
4
4
  module SystemHelper
5
5
  class << self
6
6
  def run(command)
7
- Fontist.ui.say("Run `#{command}`") if Fontist.debug?
8
-
9
- result = `#{command}`
10
- unless $CHILD_STATUS.to_i.zero?
11
- raise Errors::BinaryCallError,
12
- "Failed to run #{command}, status: #{$CHILD_STATUS}"
13
- end
14
-
15
- result
7
+ Fontist::Helpers.run(command)
16
8
  end
17
9
  end
18
10
  end