fontist 1.18.2 → 1.20.0

Sign up to get free protection for your applications and to get access to all the features.
data/docs/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "scripts": {
3
+ "dev": "vitepress dev",
4
+ "build": "vitepress build",
5
+ "preview": "vitepress preview",
6
+ "format": "prettier -w ."
7
+ },
8
+ "type": "module",
9
+ "devDependencies": {
10
+ "@types/node": "latest",
11
+ "prettier": "^3.2.4",
12
+ "typescript": "latest",
13
+ "vitepress": "^1.0.0-rc.44",
14
+ "vue": "^3.4.15",
15
+ "vue-tsc": "^1.8.27"
16
+ }
17
+ }
Binary file
Binary file
@@ -0,0 +1,143 @@
1
+ # Fontist CLI reference
2
+
3
+ <!-- This can be converted to a folder & multiple pages at any time. -->
4
+
5
+ ## `fontist cache`
6
+
7
+ The only subcommand available on `fontist cache` is `fontist cache clear`. It clears the `~/.fontist` cache.
8
+
9
+ ```sh
10
+ fontist cache clear
11
+ ```
12
+
13
+ ```
14
+ Cache has been successfully removed.
15
+ ```
16
+
17
+ ## `fontist config`
18
+
19
+ `fontist config` lets you edit the Fontist config file from the command line instead of opening an editor. There are four subcommands available:
20
+
21
+ - `fontist config delete <key>`
22
+ - `fontist config keys`
23
+ - `fontist config set <key> <value>`
24
+ - `fontist config show`
25
+
26
+ Here's an example of these commands being used to edit the config file:
27
+
28
+ ```sh
29
+ fontist config keys
30
+ fontist config set font_path /var/myfonts
31
+ fontist config delete font_path
32
+ fontist config show
33
+ ```
34
+
35
+ ```
36
+ $ fontist config keys
37
+ Available keys:
38
+ fonts_path (default: /home/octocat/.fontist/fonts)
39
+ open_timeout (default: 10)
40
+ read_timeout (default: 10)
41
+
42
+ $ fontist config set fonts_path /var/myfonts
43
+ 'fonts_path' set to '/var/myfonts'.
44
+
45
+ $ fontist config delete fonts_path
46
+ 'fonts_path' reset to default ('/home/octocat/.fontist/fonts').
47
+
48
+ $ fontist config show
49
+ Config is empty.
50
+ ```
51
+
52
+ ### Config reference
53
+
54
+ <!-- Move this into its own '/reference/config' page if this grows a lot. -->
55
+
56
+ - **`fonts_path`:** Where to put the `.ttf` files. Defaults to `~/.fontist/fonts`
57
+
58
+ - **`open_timeout`:** Defaults to 10.
59
+
60
+ - **`read_timeout`:** Defaults to 10.
61
+
62
+ ## `fontist status [font-name]`
63
+
64
+ Prints the paths to a particular installed font or all fonts if the `font-name` is omitted. This searches **all fonts available on your system** even those not managed by Fontist.
65
+
66
+ ```sh
67
+ fontist status
68
+ ```
69
+
70
+ ```
71
+ Fonts found at:
72
+ - /usr/share/fonts/truetype/ubuntu/UbuntuMono-B.ttf
73
+ - /usr/share/fonts/truetype/ubuntu/UbuntuMono-BI.ttf
74
+ - /usr/share/fonts/truetype/ubuntu/UbuntuMono-R.ttf
75
+ - /usr/share/fonts/truetype/ubuntu/UbuntuMono-RI.ttf
76
+ - /home/octocat/.fontist/fonts/Arial.ttf (from ms_truetype formula)
77
+ - /home/octocat/.fontist/fonts/ArialBI.ttf (from ms_truetype formula)
78
+ - /home/octocat/.fontist/fonts/ArialBd.ttf (from ms_truetype formula)
79
+ - /home/octocat/.fontist/fonts/ArialI.ttf (from ms_truetype formula)
80
+ ```
81
+
82
+ Here's an example narrowed to a specific font:
83
+
84
+ ```sh
85
+ fontist status "Open Sans"
86
+ ```
87
+
88
+ ```
89
+ Fonts found at:
90
+ - /home/octocat/.fontist/fonts/OpenSans-Bold.ttf (from open_sans formula)
91
+ - /home/octocat/.fontist/fonts/OpenSans-BoldItalic.ttf (from open_sans formula)
92
+ - /home/octocat/.fontist/fonts/OpenSans-Italic.ttf (from open_sans formula)
93
+ - /home/octocat/.fontist/fonts/OpenSans-Regular.ttf (from open_sans formula)
94
+ ```
95
+
96
+ ## `fontist list [font-name]`
97
+
98
+ Lists the installation status of `font-name` or all fonts if no font name provided.
99
+
100
+ ```sh
101
+ fontist status
102
+ ```
103
+
104
+ ```
105
+ Fonts found at:
106
+ - /usr/share/fonts/truetype/ubuntu/UbuntuMono-B.ttf
107
+ - /usr/share/fonts/truetype/ubuntu/UbuntuMono-BI.ttf
108
+ - /usr/share/fonts/truetype/ubuntu/UbuntuMono-R.ttf
109
+ - /usr/share/fonts/truetype/ubuntu/UbuntuMono-RI.ttf
110
+ - /home/octocat/.fontist/fonts/Arial.ttf (from ms_truetype formula)
111
+ - /home/octocat/.fontist/fonts/ArialBI.ttf (from ms_truetype formula)
112
+ - /home/octocat/.fontist/fonts/ArialBd.ttf (from ms_truetype formula)
113
+ - /home/octocat/.fontist/fonts/ArialI.ttf (from ms_truetype formula)
114
+ ```
115
+
116
+ Here's an example getting the status of a specific font:
117
+
118
+ ```sh
119
+ fontist status "Fira Mono"
120
+ ```
121
+
122
+ ```
123
+ Font "Fira Mono" not found locally.
124
+ 'Fira Mono' font is missing, please run `fontist install 'Fira Mono'` to download the font.
125
+ ```
126
+
127
+ ## Environment variables
128
+
129
+ ### `FONTIST_PATH`
130
+
131
+ By default Fontist uses the `~/.fontist` directory to store fonts and its files. It can be changed with the `FONTIST_PATH` environment variable.
132
+
133
+ ```sh
134
+ FONTIST_PATH=/var/fontist2 fontist update
135
+ ```
136
+
137
+ ## Excluded fonts
138
+
139
+ `fontist` excludes some fonts from usage when they break other software:
140
+
141
+ - `NISC18030.ttf` (GB18030 Bitmap) - macOS [fontist/fontist#344](https://github.com/fontist/fontist/issues/344)
142
+
143
+ [📑 View the up-to-date list of known problematic fonts on GitHub](https://github.com/fontist/fontist/blob/main/lib/fontist/exclude.yml)
data/fontist.gemspec CHANGED
@@ -31,6 +31,7 @@ Gem::Specification.new do |spec|
31
31
 
32
32
  spec.add_runtime_dependency "down", "~> 5.0"
33
33
  spec.add_runtime_dependency "extract_ttc", "~> 0.1"
34
+ spec.add_runtime_dependency "fuzzy_match", "~> 2.1"
34
35
  spec.add_runtime_dependency "json", "~> 2.0"
35
36
  spec.add_runtime_dependency "nokogiri", "~> 1.0"
36
37
  spec.add_runtime_dependency "mime-types", "~> 3.0"
@@ -50,4 +51,6 @@ Gem::Specification.new do |spec|
50
51
  spec.add_development_dependency "rubocop", "~> 1.22.1"
51
52
  spec.add_development_dependency "rubocop-rails", "~> 2.9"
52
53
  spec.add_development_dependency "rubocop-performance", "~> 1.10"
54
+ spec.add_development_dependency "vcr"
55
+ spec.add_development_dependency "webmock"
53
56
  end
@@ -22,6 +22,12 @@ module Fontist
22
22
  type: :boolean,
23
23
  desc: "Avoid using cache during download"
24
24
 
25
+ base.class_option :interactive,
26
+ aliases: :i,
27
+ type: :boolean,
28
+ default: true,
29
+ desc: "Interactive mode"
30
+
25
31
  base.class_option :formulas_path,
26
32
  type: :string,
27
33
  desc: "Path to formulas"
@@ -32,6 +38,7 @@ module Fontist
32
38
  Fontist.preferred_family = options[:preferred_family]
33
39
  Fontist.log_level = log_level(options)
34
40
  Fontist.use_cache = !options[:no_cache]
41
+ Fontist.interactive = options[:interactive]
35
42
 
36
43
  if options[:formulas_path]
37
44
  Fontist.formulas_path = Pathname.new(options[:formulas_path])
data/lib/fontist/cli.rb CHANGED
@@ -27,6 +27,7 @@ module Fontist
27
27
  STATUS_FONTCONFIG_NOT_FOUND = 14
28
28
  STATUS_FONTCONFIG_FILE_NOT_FOUND = 15
29
29
  STATUS_FONTIST_VERSION_ERROR = 15
30
+ STATUS_INVALID_CONFIG_ATTRIBUTE = 16
30
31
 
31
32
  ERROR_TO_STATUS = {
32
33
  Fontist::Errors::UnsupportedFontError => [STATUS_NON_SUPPORTED_FONT_ERROR],
@@ -15,42 +15,58 @@ module Fontist
15
15
  end
16
16
 
17
17
  def set(key, value)
18
+ attr = key.to_sym
19
+ unless default_values.key?(attr)
20
+ raise Errors::InvalidConfigAttributeError,
21
+ "No such attribute '#{attr}' exists."
22
+ end
23
+
18
24
  v = normalize_value(value)
19
- @custom_values[key.to_s] = v
25
+ if respond_to?("#{attr}=")
26
+ public_send("#{attr}=", v)
27
+ else
28
+ @custom_values[attr] = v
29
+ end
20
30
 
21
31
  persist
22
32
  end
23
33
 
24
34
  def delete(key)
25
- @custom_values.delete(key.to_s)
35
+ @custom_values.delete(key.to_sym)
26
36
 
27
37
  persist
28
38
  end
29
39
 
30
40
  def default_value(key)
31
- default_values[key.to_s]
41
+ default_values[key.to_sym]
32
42
  end
33
43
 
34
44
  def default_values
35
- { open_timeout: 10,
36
- read_timeout: 10 }.transform_keys(&:to_s)
45
+ { fonts_path: Fontist.fontist_path.join("fonts"),
46
+ open_timeout: 10,
47
+ read_timeout: 10 }
37
48
  end
38
49
 
39
50
  def persist
51
+ values = @custom_values.transform_keys(&:to_s)
40
52
  FileUtils.mkdir_p(File.dirname(Fontist.config_path))
41
- File.write(Fontist.config_path, YAML.dump(@custom_values))
53
+ File.write(Fontist.config_path, YAML.dump(values))
42
54
  end
43
55
 
44
56
  def load
45
57
  @custom_values = load_config_file
46
58
  end
47
59
 
60
+ def fonts_path=(value)
61
+ @custom_values[:fonts_path] = File.expand_path(value)
62
+ end
63
+
48
64
  private
49
65
 
50
66
  def load_config_file
51
67
  return {} unless File.exist?(Fontist.config_path)
52
68
 
53
- YAML.load_file(Fontist.config_path)
69
+ YAML.load_file(Fontist.config_path).transform_keys(&:to_sym)
54
70
  end
55
71
 
56
72
  def normalize_value(value)
@@ -2,15 +2,19 @@ module Fontist
2
2
  class ConfigCLI < Thor
3
3
  include CLI::ClassOptions
4
4
 
5
- STATUS_SUCCESS = 0
6
-
7
5
  desc "show", "Show values of the current config"
8
6
  def show
9
7
  handle_class_options(options)
10
8
  values = Config.instance.custom_values
11
- Fontist.ui.success("Current config:")
12
- Fontist.ui.success(format_hash(values))
13
- STATUS_SUCCESS
9
+
10
+ if values.empty?
11
+ Fontist.ui.success("Config is empty.")
12
+ else
13
+ Fontist.ui.success("Current config:")
14
+ Fontist.ui.success(format_hash(values))
15
+ end
16
+
17
+ CLI::STATUS_SUCCESS
14
18
  end
15
19
 
16
20
  desc "set KEY VALUE", "Set the KEY attribute to VALUE in the current config"
@@ -18,7 +22,10 @@ module Fontist
18
22
  handle_class_options(options)
19
23
  Config.instance.set(key, value)
20
24
  Fontist.ui.success("'#{key}' set to '#{value}'.")
21
- STATUS_SUCCESS
25
+ CLI::STATUS_SUCCESS
26
+ rescue Errors::InvalidConfigAttributeError => e
27
+ Fontist.ui.error(e.message)
28
+ CLI::STATUS_INVALID_CONFIG_ATTRIBUTE
22
29
  end
23
30
 
24
31
  desc "delete KEY", "Delete the KEY attribute from the current config"
@@ -28,13 +35,24 @@ module Fontist
28
35
  Fontist.ui.success(
29
36
  "'#{key}' reset to default ('#{Config.instance.default_value(key)}').",
30
37
  )
31
- STATUS_SUCCESS
38
+ CLI::STATUS_SUCCESS
39
+ end
40
+
41
+ desc "keys", "Print all available config attributes"
42
+ def keys
43
+ handle_class_options(options)
44
+ Fontist.ui.say("Available keys:")
45
+ Config.instance.default_values.each do |key, value|
46
+ Fontist.ui.say("#{key} (default: #{value})")
47
+ end
48
+ CLI::STATUS_SUCCESS
32
49
  end
33
50
 
34
51
  private
35
52
 
36
53
  def format_hash(hash)
37
- YAML.dump(hash).gsub(/^---.*$/, "").strip
54
+ h = hash.transform_keys(&:to_s)
55
+ YAML.dump(h).gsub(/^---.*$/, "").strip
38
56
  end
39
57
  end
40
58
  end
@@ -40,6 +40,8 @@ module Fontist
40
40
 
41
41
  class MainRepoNotFoundError < FormulaIndexNotFoundError; end
42
42
 
43
+ class InvalidConfigAttributeError < GeneralError; end
44
+
43
45
  class InvalidResourceError < GeneralError; end
44
46
 
45
47
  class LicensingError < GeneralError; end
data/lib/fontist/font.rb CHANGED
@@ -2,6 +2,7 @@ require "fontist/font_installer"
2
2
  require "fontist/font_path"
3
3
  require "fontist/formula_picker"
4
4
  require "fontist/fontconfig"
5
+ require "fontist/formula_suggestion"
5
6
 
6
7
  module Fontist
7
8
  class Font
@@ -109,17 +110,48 @@ module Fontist
109
110
  end
110
111
 
111
112
  def install_formula
112
- download_formula || raise_formula_not_found
113
+ download_formula || make_suggestions || raise_formula_not_found
113
114
  end
114
115
 
115
116
  def download_formula
116
- formula = Formula.find_by_key(@name)
117
+ formula = Formula.find_by_key_or_name(@name)
117
118
  return unless formula
118
119
  return unless formula.downloadable?
119
120
 
120
121
  request_formula_installation(formula)
121
122
  end
122
123
 
124
+ def make_suggestions
125
+ return unless Fontist.interactive?
126
+
127
+ suggestions = fuzzy_search_formulas
128
+ return if suggestions.empty?
129
+
130
+ choice = offer_to_choose(suggestions)
131
+ return unless choice
132
+
133
+ request_formula_installation(choice)
134
+ end
135
+
136
+ def fuzzy_search_formulas
137
+ @formula_suggestion ||= FormulaSuggestion.new
138
+ @formula_suggestion.find(@name)
139
+ end
140
+
141
+ def offer_to_choose(formulas)
142
+ Fontist.ui.say("Formula '#{@name}' not found. Did you mean?")
143
+
144
+ formulas.each_with_index do |formula, index|
145
+ Fontist.ui.say("[#{index}] #{formula.name}")
146
+ end
147
+
148
+ choice = Fontist.ui.ask("Please type number or " \
149
+ "press ENTER to skip installation:").chomp
150
+ return unless choice.to_i.to_s == choice
151
+
152
+ formulas[choice.to_i]
153
+ end
154
+
123
155
  def raise_formula_not_found
124
156
  raise Errors::FormulaNotFoundError.new(@name)
125
157
  end
@@ -193,7 +225,7 @@ module Fontist
193
225
  def check_and_confirm_required_license(formula)
194
226
  return @confirmation unless formula.license_required
195
227
 
196
- show_license(formula.license) unless @hide_licenses
228
+ show_license(formula) unless @hide_licenses
197
229
  return @confirmation if @confirmation.casecmp?("yes")
198
230
 
199
231
  confirmation = ask_for_agreement
@@ -204,8 +236,8 @@ module Fontist
204
236
  )
205
237
  end
206
238
 
207
- def show_license(license)
208
- Fontist.ui.say(license_agrement_message(license))
239
+ def show_license(formula)
240
+ Fontist.ui.say(license_agrement_message(formula))
209
241
  end
210
242
 
211
243
  def ask_for_agreement
@@ -215,20 +247,28 @@ module Fontist
215
247
  )
216
248
  end
217
249
 
218
- def license_agrement_message(license)
250
+ def license_agrement_message(formula)
251
+ human_name = human_name(formula)
252
+
219
253
  <<~MSG
220
- FONT LICENSE ACCEPTANCE REQUIRED FOR "#{name}":
254
+ FONT LICENSE ACCEPTANCE REQUIRED FOR "#{human_name}":
221
255
 
222
256
  Fontist can install this font if you accept its licensing conditions.
223
257
 
224
- FONT LICENSE BEGIN ("#{name}")
258
+ FONT LICENSE BEGIN ("#{human_name}")
225
259
  -----------------------------------------------------------------------
226
- #{license}
260
+ #{formula.license}
227
261
  -----------------------------------------------------------------------
228
- FONT LICENSE END ("#{name}")
262
+ FONT LICENSE END ("#{human_name}")
229
263
  MSG
230
264
  end
231
265
 
266
+ def human_name(formula)
267
+ return formula.name if @by_formula
268
+
269
+ formula.font_by_name(@name).name
270
+ end
271
+
232
272
  def update_fontconfig
233
273
  return unless @update_fontconfig
234
274
 
@@ -5,6 +5,11 @@ require "git"
5
5
 
6
6
  module Fontist
7
7
  class Formula
8
+ NAMESPACES = {
9
+ "sil" => "SIL",
10
+ "macos" => "macOS",
11
+ }.freeze
12
+
8
13
  def self.update_formulas_repo
9
14
  Update.call
10
15
  end
@@ -15,6 +20,12 @@ module Fontist
15
20
  end
16
21
  end
17
22
 
23
+ def self.all_keys
24
+ Dir[Fontist.formulas_path.join("**/*.yml").to_s].map do |path|
25
+ path.sub("#{Fontist.formulas_path}/", "").sub(".yml", "")
26
+ end
27
+ end
28
+
18
29
  def self.find(font_name)
19
30
  Indexes::FontIndex.from_yaml.load_formulas(font_name).first
20
31
  end
@@ -45,6 +56,10 @@ module Fontist
45
56
  end.flatten
46
57
  end
47
58
 
59
+ def self.find_by_key_or_name(name)
60
+ find_by_key(name) || find_by_name(name)
61
+ end
62
+
48
63
  def self.find_by_key(key)
49
64
  path = Fontist.formulas_path.join("#{key}.yml")
50
65
  return unless File.exist?(path)
@@ -52,6 +67,16 @@ module Fontist
52
67
  new_from_file(path)
53
68
  end
54
69
 
70
+ def self.find_by_name(name)
71
+ key = name_to_key(name)
72
+
73
+ find_by_key(key)
74
+ end
75
+
76
+ def self.name_to_key(name)
77
+ name.downcase.gsub(" ", "_")
78
+ end
79
+
55
80
  def self.find_by_font_file(font_file)
56
81
  key = Indexes::FilenameIndex
57
82
  .from_yaml
@@ -69,7 +94,7 @@ module Fontist
69
94
 
70
95
  def initialize(data, path)
71
96
  @data = data
72
- @path = path
97
+ @path = real_path(path)
73
98
  end
74
99
 
75
100
  def to_index_formula
@@ -89,7 +114,13 @@ module Fontist
89
114
  end
90
115
 
91
116
  def key
92
- key_from_path
117
+ @key ||= {}
118
+ @key[@path] ||= key_from_path
119
+ end
120
+
121
+ def name
122
+ @name ||= {}
123
+ @name[key] ||= namespace.empty? ? base_name : "#{namespace}/#{base_name}"
93
124
  end
94
125
 
95
126
  def description
@@ -142,6 +173,12 @@ module Fontist
142
173
  @data["instructions"]
143
174
  end
144
175
 
176
+ def font_by_name(name)
177
+ fonts.find do |font|
178
+ font.name.casecmp?(name)
179
+ end
180
+ end
181
+
145
182
  def fonts_by_name(name)
146
183
  fonts.select do |font|
147
184
  font.name.casecmp?(name)
@@ -166,9 +203,40 @@ module Fontist
166
203
 
167
204
  private
168
205
 
206
+ def real_path(path)
207
+ Dir.glob(path).first
208
+ end
209
+
169
210
  def key_from_path
170
211
  escaped = Regexp.escape("#{Fontist.formulas_path}/")
171
- @path.sub(Regexp.new("^#{escaped}"), "").sub(/\.yml$/, "")
212
+ @path.sub(Regexp.new("^#{escaped}"), "").sub(/\.yml$/, "").to_s
213
+ end
214
+
215
+ def namespace
216
+ namespace_from_mappings || namespace_from_key
217
+ end
218
+
219
+ def namespace_from_mappings
220
+ parts = key.split("/")
221
+ namespace_from_key = parts.take(parts.size - 1).join("/")
222
+ NAMESPACES[namespace_from_key]
223
+ end
224
+
225
+ def namespace_from_key
226
+ parts = key.downcase.gsub("_", " ").split("/")
227
+ parts.take(parts.size - 1).map do |namespace|
228
+ namespace.split.map(&:capitalize).join(" ")
229
+ end.join("/")
230
+ end
231
+
232
+ def base_name
233
+ @data["name"] || base_name_from_key
234
+ end
235
+
236
+ def base_name_from_key
237
+ key.split("/").last
238
+ .downcase.gsub("_", " ")
239
+ .split.map(&:capitalize).join(" ")
172
240
  end
173
241
 
174
242
  def fonts_by_family
@@ -0,0 +1,55 @@
1
+ require "fuzzy_match"
2
+
3
+ module Fontist
4
+ class FormulaSuggestion
5
+ MINIMUM_REQUIRED_SCORE = 0.6
6
+
7
+ def initialize
8
+ @fuzzy_match = prepare_search_engine
9
+ end
10
+
11
+ def find(name)
12
+ @fuzzy_match.find_all_with_score(normalize(name))
13
+ .tap { |res| Fontist.ui.debug(prettify_result(res)) }
14
+ .select { |_key, score, _| score >= MINIMUM_REQUIRED_SCORE }
15
+ .take(10)
16
+ .map(&:first)
17
+ .map { |x| Formula.find_by_key_or_name(x) }
18
+ .select(&:downloadable?)
19
+ end
20
+
21
+ private
22
+
23
+ def normalize(name)
24
+ name.gsub(" ", "_")
25
+ end
26
+
27
+ def prepare_search_engine
28
+ dict = Formula.all_keys
29
+ stop_words = namespaces(dict).map { |ns| /^#{Regexp.escape(ns)}/i }
30
+
31
+ FuzzyMatch.new(dict, stop_words: stop_words)
32
+ end
33
+
34
+ def namespaces(keys)
35
+ keys.map do |key|
36
+ parts = key.split("/")
37
+ parts.size
38
+ parts.take(parts.size - 1).join("/")
39
+ end.uniq
40
+ end
41
+
42
+ def prettify_result(result)
43
+ list = result.map do |key, dice, leve|
44
+ sprintf(
45
+ "%<dice>.3f %<leve>.3f %<key>s",
46
+ dice: dice,
47
+ leve: leve,
48
+ key: key,
49
+ )
50
+ end
51
+
52
+ "FuzzyMatch:\n#{list.join("\n")}"
53
+ end
54
+ end
55
+ end
@@ -1,3 +1,3 @@
1
1
  module Fontist
2
- VERSION = "1.18.2".freeze
2
+ VERSION = "1.20.0".freeze
3
3
  end
data/lib/fontist.rb CHANGED
@@ -37,7 +37,7 @@ module Fontist
37
37
  end
38
38
 
39
39
  def self.fonts_path
40
- Fontist.fontist_path.join("fonts")
40
+ Pathname.new(config[:fonts_path])
41
41
  end
42
42
 
43
43
  def self.formulas_repo_path
@@ -151,4 +151,12 @@ module Fontist
151
151
  def self.log_level=(level)
152
152
  Fontist.ui.level = level
153
153
  end
154
+
155
+ def self.interactive?
156
+ @interactive || false
157
+ end
158
+
159
+ def self.interactive=(bool)
160
+ @interactive = bool
161
+ end
154
162
  end