etcetera 0.12.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 +7 -0
- data/.github/workflows/ci.yaml +72 -0
- data/CHANGELOG.md +3 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE-APACHE +176 -0
- data/LICENSE-MIT +23 -0
- data/LICENSE-WINDOWS-SYS +21 -0
- data/README.md +66 -0
- data/Rakefile +12 -0
- data/lib/etcetera/app_strategy/apple.rb +44 -0
- data/lib/etcetera/app_strategy/unix.rb +46 -0
- data/lib/etcetera/app_strategy/windows.rb +90 -0
- data/lib/etcetera/app_strategy/xdg.rb +109 -0
- data/lib/etcetera/app_strategy.rb +167 -0
- data/lib/etcetera/base_strategy/apple.rb +34 -0
- data/lib/etcetera/base_strategy/windows.rb +112 -0
- data/lib/etcetera/base_strategy/xdg.rb +102 -0
- data/lib/etcetera/base_strategy.rb +103 -0
- data/lib/etcetera/version.rb +6 -0
- data/lib/etcetera/windows_sys.rb +39 -0
- data/lib/etcetera.rb +133 -0
- data/sig/etcetera/app_strategy/apple.rbs +15 -0
- data/sig/etcetera/app_strategy/unix.rbs +14 -0
- data/sig/etcetera/app_strategy/windows.rbs +15 -0
- data/sig/etcetera/app_strategy/xdg.rbs +15 -0
- data/sig/etcetera/app_strategy.rbs +36 -0
- data/sig/etcetera/base_strategy/apple.rbs +10 -0
- data/sig/etcetera/base_strategy/windows.rbs +10 -0
- data/sig/etcetera/base_strategy/xdg.rbs +10 -0
- data/sig/etcetera/base_strategy.rbs +17 -0
- data/sig/etcetera.rbs +32 -0
- metadata +94 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Etcetera
|
|
4
|
+
module AppStrategy
|
|
5
|
+
# This strategy implements the {XDG Base Directories Specification}[https://specifications.freedesktop.org/basedir/latest/]. It is the most common on Linux, but is increasingly being adopted elsewhere.
|
|
6
|
+
#
|
|
7
|
+
# This initial example removes all the XDG environment variables to show the strategy’s use of the XDG default directories.
|
|
8
|
+
#
|
|
9
|
+
# require "etcetera"
|
|
10
|
+
#
|
|
11
|
+
# # Remove the environment variables that the strategy reads from.
|
|
12
|
+
# ENV.delete("XDG_CONFIG_HOME")
|
|
13
|
+
# ENV.delete("XDG_DATA_HOME")
|
|
14
|
+
# ENV.delete("XDG_CACHE_HOME")
|
|
15
|
+
# ENV.delete("XDG_STATE_HOME")
|
|
16
|
+
# ENV.delete("XDG_RUNTIME_DIR")
|
|
17
|
+
#
|
|
18
|
+
# app_strategy = Etcetera::AppStrategy::Xdg.new(Etcetera::AppStrategy::AppStrategyArgs.new(
|
|
19
|
+
# top_level_domain: "org",
|
|
20
|
+
# author: "Acme Corp",
|
|
21
|
+
# app_name: "Frobnicator Plus"
|
|
22
|
+
# ))
|
|
23
|
+
#
|
|
24
|
+
# home_dir = Etcetera.home_dir
|
|
25
|
+
#
|
|
26
|
+
# app_strategy.home_dir == home_dir # => true
|
|
27
|
+
# app_strategy.config_dir.relative_path_from(home_dir) # => #<Pathname:.config/frobnicator-plus>
|
|
28
|
+
# app_strategy.data_dir.relative_path_from(home_dir) # => #<Pathname:.local/share/frobnicator-plus>
|
|
29
|
+
# app_strategy.cache_dir.relative_path_from(home_dir) # => #<Pathname:.cache/frobnicator-plus>
|
|
30
|
+
# app_strategy.state_dir.relative_path_from(home_dir) # => #<Pathname:.local/state/frobnicator-plus>
|
|
31
|
+
# app_strategy.runtime_dir # => nil
|
|
32
|
+
#
|
|
33
|
+
# This next example gives the environment variables values:
|
|
34
|
+
#
|
|
35
|
+
# require "etcetera"
|
|
36
|
+
#
|
|
37
|
+
# # We need to conditionally set these to ensure that they are absolute paths both on Windows and other systems.
|
|
38
|
+
# windows = Gem.win_platform?
|
|
39
|
+
# config_path = windows ? "C:\\my_config_location\\" : "/my_config_location/"
|
|
40
|
+
# data_path = windows ? "C:\\my_data_location\\" : "/my_data_location/"
|
|
41
|
+
# cache_path = windows ? "C:\\my_cache_location\\" : "/my_cache_location/"
|
|
42
|
+
# state_path = windows ? "C:\\my_state_location\\" : "/my_state_location/"
|
|
43
|
+
# runtime_path = windows ? "C:\\my_runtime_location\\" : "/my_runtime_location/"
|
|
44
|
+
#
|
|
45
|
+
# ENV["XDG_CONFIG_HOME"] = config_path
|
|
46
|
+
# ENV["XDG_DATA_HOME"] = data_path
|
|
47
|
+
# ENV["XDG_CACHE_HOME"] = cache_path
|
|
48
|
+
# ENV["XDG_STATE_HOME"] = state_path
|
|
49
|
+
# ENV["XDG_RUNTIME_DIR"] = runtime_path
|
|
50
|
+
#
|
|
51
|
+
# app_strategy = Etcetera::AppStrategy::Xdg.new(Etcetera::AppStrategy::AppStrategyArgs.new(
|
|
52
|
+
# top_level_domain: "org",
|
|
53
|
+
# author: "Acme Corp",
|
|
54
|
+
# app_name: "Frobnicator Plus"
|
|
55
|
+
# ))
|
|
56
|
+
#
|
|
57
|
+
# app_strategy.config_dir == Pathname.new(config_path).join("frobnicator-plus") # => true
|
|
58
|
+
# app_strategy.data_dir == Pathname.new(data_path).join("frobnicator-plus") # => true
|
|
59
|
+
# app_strategy.cache_dir == Pathname.new(cache_path).join("frobnicator-plus") # => true
|
|
60
|
+
# app_strategy.state_dir == Pathname.new(state_path).join("frobnicator-plus") # => true
|
|
61
|
+
# app_strategy.runtime_dir == Pathname.new(runtime_path).join("frobnicator-plus") # => true
|
|
62
|
+
#
|
|
63
|
+
# The XDG spec requires that when the environment variables’ values are not absolute paths, their values should be ignored. This example exemplifies this behaviour:
|
|
64
|
+
#
|
|
65
|
+
# require "etcetera"
|
|
66
|
+
#
|
|
67
|
+
# # Remove the environment variables that the strategy reads from.
|
|
68
|
+
# ENV["XDG_CONFIG_HOME"] = "relative_path/"
|
|
69
|
+
# ENV["XDG_DATA_HOME"] = "./another_one/"
|
|
70
|
+
# ENV["XDG_CACHE_HOME"] = "yet_another/"
|
|
71
|
+
# ENV["XDG_STATE_HOME"] = "./and_another"
|
|
72
|
+
# ENV["XDG_RUNTIME_DIR"] = "relative_path/"
|
|
73
|
+
#
|
|
74
|
+
# app_strategy = Etcetera::AppStrategy::Xdg.new(Etcetera::AppStrategy::AppStrategyArgs.new(
|
|
75
|
+
# top_level_domain: "org",
|
|
76
|
+
# author: "Acme Corp",
|
|
77
|
+
# app_name: "Frobnicator Plus"
|
|
78
|
+
# ))
|
|
79
|
+
#
|
|
80
|
+
# home_dir = Etcetera.home_dir
|
|
81
|
+
#
|
|
82
|
+
# # We still get the default values.
|
|
83
|
+
# app_strategy.config_dir.relative_path_from(home_dir) # => #<Pathname:.config/frobnicator-plus>
|
|
84
|
+
# app_strategy.data_dir.relative_path_from(home_dir) # => #<Pathname:.local/share/frobnicator-plus>
|
|
85
|
+
# app_strategy.cache_dir.relative_path_from(home_dir) # => #<Pathname:.cache/frobnicator-plus>
|
|
86
|
+
# app_strategy.state_dir.relative_path_from(home_dir) # => #<Pathname:.local/state/frobnicator-plus>
|
|
87
|
+
# app_strategy.runtime_dir # => nil
|
|
88
|
+
class Xdg < Data.define(:base_strategy, :unixy_name)
|
|
89
|
+
include AppStrategy
|
|
90
|
+
|
|
91
|
+
# Create a new \Xdg \AppStrategy
|
|
92
|
+
def self.new(args) = super(base_strategy: BaseStrategy::Xdg.new, unixy_name: args.unixy_name)
|
|
93
|
+
|
|
94
|
+
private :base_strategy, :unixy_name
|
|
95
|
+
|
|
96
|
+
def home_dir = base_strategy.home_dir
|
|
97
|
+
|
|
98
|
+
def config_dir = base_strategy.config_dir.join(unixy_name)
|
|
99
|
+
|
|
100
|
+
def data_dir = base_strategy.data_dir.join(unixy_name)
|
|
101
|
+
|
|
102
|
+
def cache_dir = base_strategy.cache_dir.join(unixy_name)
|
|
103
|
+
|
|
104
|
+
def state_dir = base_strategy.state_dir.join(unixy_name)
|
|
105
|
+
|
|
106
|
+
def runtime_dir = base_strategy.runtime_dir&.join(unixy_name)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Etcetera
|
|
4
|
+
# These strategies require you to provide some information on your application, and they will in turn locate the configuration/data/cache directory specifically for your application.
|
|
5
|
+
#
|
|
6
|
+
# Allows applications to retrieve the paths of configuration, data, and cache directories specifically for them.
|
|
7
|
+
module AppStrategy
|
|
8
|
+
# The arguments to the creator method of an AppStrategy.
|
|
9
|
+
#
|
|
10
|
+
# +top_level_domain+:: The top level domain of the application, e.g. +com+, +org+, or <tt>io.github</tt>.
|
|
11
|
+
# +author+:: The name of the author of the application.
|
|
12
|
+
# +app_name+:: The application’s name. This should be capitalised if appropriate.
|
|
13
|
+
class AppStrategyArgs < Data.define(:top_level_domain, :author, :app_name)
|
|
14
|
+
def initialize(top_level_domain: "", author: "", app_name: "") = super
|
|
15
|
+
|
|
16
|
+
# Constructs a bunde identifier from an +AppStrategyArgs+.
|
|
17
|
+
#
|
|
18
|
+
# require "etcetera"
|
|
19
|
+
#
|
|
20
|
+
# strategy_args = Etcetera::AppStrategy::AppStrategyArgs.new(
|
|
21
|
+
# top_level_domain: "org",
|
|
22
|
+
# author: "Acme Corp",
|
|
23
|
+
# app_name: "Frobnicator Plus"
|
|
24
|
+
# )
|
|
25
|
+
#
|
|
26
|
+
# strategy_args.bundle_id # => "org.acme-corp.Frobnicator-Plus"
|
|
27
|
+
def bundle_id
|
|
28
|
+
author = self.author.downcase.tr(" ", "-")
|
|
29
|
+
app_name = self.app_name.tr(" ", "-")
|
|
30
|
+
[top_level_domain, author, app_name].reject(&:empty?).join(".")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Returns a ‘unixy’ version of the application’s name, akin to what would usually be used as a binary name.
|
|
34
|
+
#
|
|
35
|
+
# require "etcetera"
|
|
36
|
+
#
|
|
37
|
+
# strategy_args = Etcetera::AppStrategy::AppStrategyArgs.new(
|
|
38
|
+
# top_level_domain: "org",
|
|
39
|
+
# author: "Acme Corp",
|
|
40
|
+
# app_name: "Frobnicator Plus"
|
|
41
|
+
# )
|
|
42
|
+
#
|
|
43
|
+
# strategy_args.unixy_name # => "frobnicator-plus"
|
|
44
|
+
def unixy_name = app_name.downcase.tr(" ", "-")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
##
|
|
48
|
+
# :method: home_dir
|
|
49
|
+
# :call-seq:
|
|
50
|
+
# home_dir -> Pathname
|
|
51
|
+
#
|
|
52
|
+
# Gets the home directory of the current user.
|
|
53
|
+
|
|
54
|
+
# @!method home_dir
|
|
55
|
+
# Gets the home directory of the current user.
|
|
56
|
+
# @return [Pathname]
|
|
57
|
+
|
|
58
|
+
##
|
|
59
|
+
# :method: config_dir
|
|
60
|
+
# :call-seq:
|
|
61
|
+
# config_dir -> Pathname
|
|
62
|
+
#
|
|
63
|
+
# Gets the configuration directory for your application.
|
|
64
|
+
|
|
65
|
+
# @!method config_dir
|
|
66
|
+
# Gets the configuration directory for your application.
|
|
67
|
+
# @return [Pathname]
|
|
68
|
+
|
|
69
|
+
##
|
|
70
|
+
# :method: data_dir
|
|
71
|
+
# :call-seq:
|
|
72
|
+
# data_dir -> Pathname
|
|
73
|
+
#
|
|
74
|
+
# Gets the data directory for your application.
|
|
75
|
+
|
|
76
|
+
# @!method data_dir
|
|
77
|
+
# Gets the data directory for your application.
|
|
78
|
+
# @return [Pathname]
|
|
79
|
+
|
|
80
|
+
##
|
|
81
|
+
# :method: cache_dir
|
|
82
|
+
# :call-seq:
|
|
83
|
+
# cache_dir -> Pathname
|
|
84
|
+
#
|
|
85
|
+
# Gets the cache directory for your application.
|
|
86
|
+
|
|
87
|
+
# @!method cache_dir
|
|
88
|
+
# Gets the cache directory for your application.
|
|
89
|
+
# @return [Pathname]
|
|
90
|
+
|
|
91
|
+
##
|
|
92
|
+
# :method: state_dir
|
|
93
|
+
# :call-seq:
|
|
94
|
+
# state_dir -> Pathname or nil
|
|
95
|
+
#
|
|
96
|
+
# Gets the state directory for your application.
|
|
97
|
+
# Currently, only the Xdg & Unix strategies support
|
|
98
|
+
# this.
|
|
99
|
+
|
|
100
|
+
# @!method state_dir
|
|
101
|
+
# Gets the state directory for your application.
|
|
102
|
+
# Currently, only the {Etcetera::AppStrategy::Xdg Xdg} & {Etcetera::AppStrategy::Unix Unix} strategies support
|
|
103
|
+
# this.
|
|
104
|
+
# @return [Pathname, nil]
|
|
105
|
+
|
|
106
|
+
##
|
|
107
|
+
# :method: runtime_dir
|
|
108
|
+
# :call-seq:
|
|
109
|
+
# runtime_dir -> Pathname or nil
|
|
110
|
+
#
|
|
111
|
+
# Gets the runtime directory for your application.
|
|
112
|
+
# Currently, only the Xdg & Unix strategies support
|
|
113
|
+
# this.
|
|
114
|
+
#
|
|
115
|
+
# Note: The {XDG Base Directory Specification}[https://specifications.freedesktop.org/basedir/latest/] places additional requirements on this
|
|
116
|
+
# directory related to ownership, permissions, and persistence. This library does not check
|
|
117
|
+
# these requirements.
|
|
118
|
+
|
|
119
|
+
# @!method runtime_dir
|
|
120
|
+
# Gets the runtime directory for your application.
|
|
121
|
+
# Currently, only the {Etcetera::AppStrategy::Xdg Xdg} & {Etcetera::AppStrategy::Unix Unix} strategies support
|
|
122
|
+
# this.
|
|
123
|
+
#
|
|
124
|
+
# Note: The {XDG Base Directory Specification}[https://specifications.freedesktop.org/basedir/latest/] places additional requirements on this
|
|
125
|
+
# directory related to ownership, permissions, and persistence. This library does not check
|
|
126
|
+
# these requirements.
|
|
127
|
+
# @return [Pathname, nil]
|
|
128
|
+
|
|
129
|
+
# Constructs a path inside your application’s configuration directory to which a path of your choice has been appended.
|
|
130
|
+
def in_config_dir(path) = config_dir.join(path)
|
|
131
|
+
|
|
132
|
+
# Constructs a path inside your application’s data directory to which a path of your choice has been appended.
|
|
133
|
+
def in_data_dir(path) = data_dir.join(path)
|
|
134
|
+
|
|
135
|
+
# Constructs a path inside your application’s cache directory to which a path of your choice has been appended.
|
|
136
|
+
def in_cache_dir(path) = cache_dir.join(path)
|
|
137
|
+
|
|
138
|
+
# Constructs a path inside your application’s state directory to which a path of your choice has been appended.
|
|
139
|
+
#
|
|
140
|
+
# Currently, this is only implemented for the Xdg strategy.
|
|
141
|
+
def in_state_dir(path) = state_dir&.join(path)
|
|
142
|
+
|
|
143
|
+
# Constructs a path inside your application’s runtime directory to which a path of your choice has been appended.
|
|
144
|
+
# Currently, only the Xdg & Unix strategies support
|
|
145
|
+
# this.
|
|
146
|
+
#
|
|
147
|
+
# See the note in #runtime_dir for more information.
|
|
148
|
+
def in_runtime_dir(path) = runtime_dir&.join(path)
|
|
149
|
+
|
|
150
|
+
# Returns the current OS’s native AppStrategy.
|
|
151
|
+
# This uses the Windows strategy on \Windows, Apple on macOS & iOS, and Xdg everywhere else.
|
|
152
|
+
# This is the convention used by most GUI applications.
|
|
153
|
+
def self.choose_native_strategy(args)
|
|
154
|
+
if Gem.win_platform? then Windows.new(args)
|
|
155
|
+
elsif RUBY_PLATFORM.include?("darwin") then Apple.new(args)
|
|
156
|
+
else Xdg.new(args)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Returns the current OS’s default AppStrategy.
|
|
161
|
+
# This uses the Windows strategy on \Windows, and Xdg everywhere else.
|
|
162
|
+
# This is the convention used by most CLI applications.
|
|
163
|
+
def self.choose_app_strategy(args)
|
|
164
|
+
Gem.win_platform? ? Windows.new(args) : Xdg.new(args)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Etcetera
|
|
4
|
+
module BaseStrategy
|
|
5
|
+
# This is the strategy created by \Apple for use on macOS and iOS devices. It is always used by GUI apps on macOS, and is sometimes used by command-line applications there too. iOS only has GUIs, so all iOS applications follow this strategy. The specification is available {here}[https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html#//apple_ref/doc/uid/TP40010672-CH2-SW1].
|
|
6
|
+
#
|
|
7
|
+
# require "etcetera"
|
|
8
|
+
#
|
|
9
|
+
# base_strategy = Etcetera::BaseStrategy::Apple.new
|
|
10
|
+
#
|
|
11
|
+
# home_dir = Etcetera.home_dir
|
|
12
|
+
#
|
|
13
|
+
# base_strategy.home_dir == home_dir # => true
|
|
14
|
+
# base_strategy.config_dir.relative_path_from(home_dir) # => #<Pathname:Library/Preferences>
|
|
15
|
+
# base_strategy.data_dir.relative_path_from(home_dir) # => #<Pathname:Library/Application Support>
|
|
16
|
+
# base_strategy.cache_dir.relative_path_from(home_dir) # => #<Pathname:Library/Caches>
|
|
17
|
+
# base_strategy.state_dir # => nil
|
|
18
|
+
# base_strategy.runtime_dir # => nil
|
|
19
|
+
class Apple < Data.define(:home_dir)
|
|
20
|
+
# Create a new \Apple \BaseStrategy
|
|
21
|
+
def initialize(home_dir: Etcetera.home_dir) = super
|
|
22
|
+
|
|
23
|
+
def config_dir = home_dir.join("Library/Preferences/")
|
|
24
|
+
|
|
25
|
+
def data_dir = home_dir.join("Library/Application Support/")
|
|
26
|
+
|
|
27
|
+
def cache_dir = home_dir.join("Library/Caches/")
|
|
28
|
+
|
|
29
|
+
def state_dir = nil
|
|
30
|
+
|
|
31
|
+
def runtime_dir = nil
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Etcetera
|
|
4
|
+
module BaseStrategy # rubocop:disable Style/Documentation -- documented in base_strategy.rb
|
|
5
|
+
if Gem.win_platform?
|
|
6
|
+
require_relative "../windows_sys"
|
|
7
|
+
|
|
8
|
+
# The C runtime function the crate declares in an `extern "C"` block.
|
|
9
|
+
module CRT # :nodoc: all
|
|
10
|
+
extend Fiddle::Importer
|
|
11
|
+
|
|
12
|
+
dlload "ucrtbase.dll"
|
|
13
|
+
extern "size_t wcslen(void* buf)"
|
|
14
|
+
end
|
|
15
|
+
private_constant :CRT
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# This strategy follows Windows’ conventions. It seems that all \Windows GUI apps, and some command-line ones follow this pattern. The specification is available {here}[https://docs.microsoft.com/en-us/windows/win32/shell/knownfolderid].
|
|
19
|
+
#
|
|
20
|
+
# This initial example removes all the relevant environment variables to show the strategy’s use of the:
|
|
21
|
+
# - (on \Windows) SHGetKnownFolderPath API.
|
|
22
|
+
# - (on non-Windows) \Windows default directories.
|
|
23
|
+
#
|
|
24
|
+
# require "etcetera"
|
|
25
|
+
#
|
|
26
|
+
# # Remove the environment variables that the strategy reads from.
|
|
27
|
+
# ENV.delete("HOME")
|
|
28
|
+
# ENV.delete("APPDATA")
|
|
29
|
+
# ENV.delete("LOCALAPPDATA")
|
|
30
|
+
#
|
|
31
|
+
# base_strategy = Etcetera::BaseStrategy::Windows.new
|
|
32
|
+
#
|
|
33
|
+
# home_dir = Etcetera.home_dir
|
|
34
|
+
#
|
|
35
|
+
# base_strategy.home_dir == home_dir # => true
|
|
36
|
+
# base_strategy.config_dir.relative_path_from(home_dir) # => #<Pathname:AppData/Roaming>
|
|
37
|
+
# base_strategy.data_dir.relative_path_from(home_dir) # => #<Pathname:AppData/Roaming>
|
|
38
|
+
# base_strategy.cache_dir.relative_path_from(home_dir) # => #<Pathname:AppData/Local>
|
|
39
|
+
# base_strategy.state_dir # => nil
|
|
40
|
+
# base_strategy.runtime_dir # => nil
|
|
41
|
+
#
|
|
42
|
+
# This next example gives the environment variables values:
|
|
43
|
+
#
|
|
44
|
+
# require "etcetera"
|
|
45
|
+
#
|
|
46
|
+
# windows = Gem.win_platform?
|
|
47
|
+
# home_path = windows ? "C:/foo/" : Etcetera.home_dir.to_s
|
|
48
|
+
# data_path = windows ? "C:\\bar\\" : "/bar/"
|
|
49
|
+
# cache_path = windows ? "C:\\baz\\" : "/baz/"
|
|
50
|
+
#
|
|
51
|
+
# ENV["HOME"] = home_path
|
|
52
|
+
# ENV["APPDATA"] = data_path
|
|
53
|
+
# ENV["LOCALAPPDATA"] = cache_path
|
|
54
|
+
#
|
|
55
|
+
# base_strategy = Etcetera::BaseStrategy::Windows.new
|
|
56
|
+
#
|
|
57
|
+
# base_strategy.home_dir == Pathname.new(home_path) # => true
|
|
58
|
+
# base_strategy.config_dir == Pathname.new(data_path) # => true
|
|
59
|
+
# base_strategy.data_dir == Pathname.new(data_path) # => true
|
|
60
|
+
# base_strategy.cache_dir == Pathname.new(cache_path) # => true
|
|
61
|
+
# base_strategy.state_dir # => nil
|
|
62
|
+
# base_strategy.runtime_dir # => nil
|
|
63
|
+
class Windows < Data.define(:home_dir)
|
|
64
|
+
# Create a new \Windows \BaseStrategy
|
|
65
|
+
def initialize(home_dir: Etcetera.home_dir) = super
|
|
66
|
+
|
|
67
|
+
def config_dir = data_dir
|
|
68
|
+
|
|
69
|
+
def data_dir = dir_inner("APPDATA") || home_dir.join("AppData").join("Roaming")
|
|
70
|
+
|
|
71
|
+
def cache_dir = dir_inner("LOCALAPPDATA") || home_dir.join("AppData").join("Local")
|
|
72
|
+
|
|
73
|
+
def state_dir = nil
|
|
74
|
+
|
|
75
|
+
def runtime_dir = nil
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def dir_inner(env)
|
|
80
|
+
value = ENV.fetch(env, "")
|
|
81
|
+
value.empty? ? dir_crt(env) : Pathname.new(value)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
if Gem.win_platform?
|
|
85
|
+
# Ref: https://github.com/rust-lang/cargo/blob/home-0.5.11/crates/home/src/windows.rs
|
|
86
|
+
# We should keep this code in sync with the above.
|
|
87
|
+
def dir_crt(env)
|
|
88
|
+
folder_id = case env
|
|
89
|
+
when "APPDATA" then WindowsSys::FOLDERID_RoamingAppData
|
|
90
|
+
when "LOCALAPPDATA" then WindowsSys::FOLDERID_LocalAppData
|
|
91
|
+
else return nil
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
path = Fiddle::Pointer.new(0)
|
|
95
|
+
case WindowsSys.SHGetKnownFolderPath(folder_id.to_ptr, WindowsSys::KF_FLAG_DONT_VERIFY, nil, path.ref)
|
|
96
|
+
when WindowsSys::S_OK
|
|
97
|
+
path_slice = path[0, CRT.wcslen(path) * 2]
|
|
98
|
+
s = path_slice.force_encoding(Encoding::UTF_16LE).encode(Encoding::UTF_8, invalid: :replace)
|
|
99
|
+
WindowsSys.CoTaskMemFree(path)
|
|
100
|
+
Pathname.new(s)
|
|
101
|
+
else
|
|
102
|
+
# Free any allocated memory even on failure. A null ptr is a no-op for `CoTaskMemFree`.
|
|
103
|
+
WindowsSys.CoTaskMemFree(path)
|
|
104
|
+
nil
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
else
|
|
108
|
+
def dir_crt(_env) = nil
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Etcetera
|
|
4
|
+
module BaseStrategy
|
|
5
|
+
# This strategy implements the {XDG Base Directories Specification}[https://specifications.freedesktop.org/basedir/latest/]. It is the most common on Linux, but is increasingly being adopted elsewhere.
|
|
6
|
+
#
|
|
7
|
+
# This initial example removes all the XDG environment variables to show the strategy’s use of the XDG default directories.
|
|
8
|
+
#
|
|
9
|
+
# require "etcetera"
|
|
10
|
+
#
|
|
11
|
+
# # Remove the environment variables that the strategy reads from.
|
|
12
|
+
# ENV.delete("XDG_CONFIG_HOME")
|
|
13
|
+
# ENV.delete("XDG_DATA_HOME")
|
|
14
|
+
# ENV.delete("XDG_CACHE_HOME")
|
|
15
|
+
# ENV.delete("XDG_STATE_HOME")
|
|
16
|
+
# ENV.delete("XDG_RUNTIME_DIR")
|
|
17
|
+
#
|
|
18
|
+
# base_strategy = Etcetera::BaseStrategy::Xdg.new
|
|
19
|
+
#
|
|
20
|
+
# home_dir = Etcetera.home_dir
|
|
21
|
+
#
|
|
22
|
+
# base_strategy.home_dir == home_dir # => true
|
|
23
|
+
# base_strategy.config_dir.relative_path_from(home_dir) # => #<Pathname:.config>
|
|
24
|
+
# base_strategy.data_dir.relative_path_from(home_dir) # => #<Pathname:.local/share>
|
|
25
|
+
# base_strategy.cache_dir.relative_path_from(home_dir) # => #<Pathname:.cache>
|
|
26
|
+
# base_strategy.state_dir.relative_path_from(home_dir) # => #<Pathname:.local/state>
|
|
27
|
+
# base_strategy.runtime_dir # => nil
|
|
28
|
+
#
|
|
29
|
+
# This next example gives the environment variables values:
|
|
30
|
+
#
|
|
31
|
+
# require "etcetera"
|
|
32
|
+
#
|
|
33
|
+
# # We need to conditionally set these to ensure that they are absolute paths both on Windows and other systems.
|
|
34
|
+
# windows = Gem.win_platform?
|
|
35
|
+
# config_path = windows ? "C:\\foo\\" : "/foo/"
|
|
36
|
+
# data_path = windows ? "C:\\bar\\" : "/bar/"
|
|
37
|
+
# cache_path = windows ? "C:\\baz\\" : "/baz/"
|
|
38
|
+
# state_path = windows ? "C:\\foobar\\" : "/foobar/"
|
|
39
|
+
# runtime_path = windows ? "C:\\qux\\" : "/qux/"
|
|
40
|
+
#
|
|
41
|
+
# ENV["XDG_CONFIG_HOME"] = config_path
|
|
42
|
+
# ENV["XDG_DATA_HOME"] = data_path
|
|
43
|
+
# ENV["XDG_CACHE_HOME"] = cache_path
|
|
44
|
+
# ENV["XDG_STATE_HOME"] = state_path
|
|
45
|
+
# ENV["XDG_RUNTIME_DIR"] = runtime_path
|
|
46
|
+
#
|
|
47
|
+
# base_strategy = Etcetera::BaseStrategy::Xdg.new
|
|
48
|
+
#
|
|
49
|
+
# base_strategy.config_dir == Pathname.new(config_path) # => true
|
|
50
|
+
# base_strategy.data_dir == Pathname.new(data_path) # => true
|
|
51
|
+
# base_strategy.cache_dir == Pathname.new(cache_path) # => true
|
|
52
|
+
# base_strategy.state_dir == Pathname.new(state_path) # => true
|
|
53
|
+
# base_strategy.runtime_dir == Pathname.new(runtime_path) # => true
|
|
54
|
+
#
|
|
55
|
+
# The XDG spec requires that when the environment variables’ values are not absolute paths, their values should be ignored. This example exemplifies this behaviour:
|
|
56
|
+
#
|
|
57
|
+
# require "etcetera"
|
|
58
|
+
#
|
|
59
|
+
# # Remove the environment variables that the strategy reads from.
|
|
60
|
+
# ENV["XDG_CONFIG_HOME"] = "foo/"
|
|
61
|
+
# ENV["XDG_DATA_HOME"] = "bar/"
|
|
62
|
+
# ENV["XDG_CACHE_HOME"] = "baz/"
|
|
63
|
+
# ENV["XDG_STATE_HOME"] = "foobar/"
|
|
64
|
+
# ENV["XDG_RUNTIME_DIR"] = "qux/"
|
|
65
|
+
#
|
|
66
|
+
# base_strategy = Etcetera::BaseStrategy::Xdg.new
|
|
67
|
+
#
|
|
68
|
+
# home_dir = Etcetera.home_dir
|
|
69
|
+
#
|
|
70
|
+
# # We still get the default values.
|
|
71
|
+
# base_strategy.config_dir.relative_path_from(home_dir) # => #<Pathname:.config>
|
|
72
|
+
# base_strategy.data_dir.relative_path_from(home_dir) # => #<Pathname:.local/share>
|
|
73
|
+
# base_strategy.cache_dir.relative_path_from(home_dir) # => #<Pathname:.cache>
|
|
74
|
+
# base_strategy.state_dir.relative_path_from(home_dir) # => #<Pathname:.local/state>
|
|
75
|
+
# base_strategy.runtime_dir # => nil
|
|
76
|
+
class Xdg < Data.define(:home_dir)
|
|
77
|
+
# Create a new \Xdg \BaseStrategy
|
|
78
|
+
def initialize(home_dir: Etcetera.home_dir) = super
|
|
79
|
+
|
|
80
|
+
def config_dir = env_var_or_default("XDG_CONFIG_HOME", ".config/")
|
|
81
|
+
|
|
82
|
+
def data_dir = env_var_or_default("XDG_DATA_HOME", ".local/share/")
|
|
83
|
+
|
|
84
|
+
def cache_dir = env_var_or_default("XDG_CACHE_HOME", ".cache/")
|
|
85
|
+
|
|
86
|
+
def state_dir = env_var_or_default("XDG_STATE_HOME", ".local/state/")
|
|
87
|
+
|
|
88
|
+
def runtime_dir = env_var_or_none("XDG_RUNTIME_DIR")
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def env_var_or_none(env_var)
|
|
93
|
+
path = ENV[env_var]&.then { Pathname.new(it) }
|
|
94
|
+
|
|
95
|
+
# Return nil if the path obtained from the environment variable isn’t absolute.
|
|
96
|
+
path if path&.absolute?
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def env_var_or_default(env_var, default) = env_var_or_none(env_var) || home_dir.join(default)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Etcetera
|
|
4
|
+
# These strategies simply provide the user’s configuration, data, and cache directories, without knowing about the application specifically.
|
|
5
|
+
#
|
|
6
|
+
# Provides configuration, data, and cache directories of the current user.
|
|
7
|
+
module BaseStrategy
|
|
8
|
+
##
|
|
9
|
+
# :method: home_dir
|
|
10
|
+
# :call-seq:
|
|
11
|
+
# home_dir -> Pathname
|
|
12
|
+
#
|
|
13
|
+
# Gets the home directory of the current user.
|
|
14
|
+
|
|
15
|
+
# @!method home_dir
|
|
16
|
+
# Gets the home directory of the current user.
|
|
17
|
+
# @return [Pathname]
|
|
18
|
+
|
|
19
|
+
##
|
|
20
|
+
# :method: config_dir
|
|
21
|
+
# :call-seq:
|
|
22
|
+
# config_dir -> Pathname
|
|
23
|
+
#
|
|
24
|
+
# Gets the user’s configuration directory.
|
|
25
|
+
|
|
26
|
+
# @!method config_dir
|
|
27
|
+
# Gets the user’s configuration directory.
|
|
28
|
+
# @return [Pathname]
|
|
29
|
+
|
|
30
|
+
##
|
|
31
|
+
# :method: data_dir
|
|
32
|
+
# :call-seq:
|
|
33
|
+
# data_dir -> Pathname
|
|
34
|
+
#
|
|
35
|
+
# Gets the user’s data directory.
|
|
36
|
+
|
|
37
|
+
# @!method data_dir
|
|
38
|
+
# Gets the user’s data directory.
|
|
39
|
+
# @return [Pathname]
|
|
40
|
+
|
|
41
|
+
##
|
|
42
|
+
# :method: cache_dir
|
|
43
|
+
# :call-seq:
|
|
44
|
+
# cache_dir -> Pathname
|
|
45
|
+
#
|
|
46
|
+
# Gets the user’s cache directory.
|
|
47
|
+
|
|
48
|
+
# @!method cache_dir
|
|
49
|
+
# Gets the user’s cache directory.
|
|
50
|
+
# @return [Pathname]
|
|
51
|
+
|
|
52
|
+
##
|
|
53
|
+
# :method: state_dir
|
|
54
|
+
# :call-seq:
|
|
55
|
+
# state_dir -> Pathname or nil
|
|
56
|
+
#
|
|
57
|
+
# Gets the user’s state directory.
|
|
58
|
+
# Currently, only the Xdg strategy supports this.
|
|
59
|
+
|
|
60
|
+
# @!method state_dir
|
|
61
|
+
# Gets the user’s state directory.
|
|
62
|
+
# Currently, only the {Etcetera::BaseStrategy::Xdg Xdg} strategy supports this.
|
|
63
|
+
# @return [Pathname, nil]
|
|
64
|
+
|
|
65
|
+
##
|
|
66
|
+
# :method: runtime_dir
|
|
67
|
+
# :call-seq:
|
|
68
|
+
# runtime_dir -> Pathname or nil
|
|
69
|
+
#
|
|
70
|
+
# Gets the user’s runtime directory.
|
|
71
|
+
# Currently, only the Xdg strategy supports this.
|
|
72
|
+
#
|
|
73
|
+
# Note: The {XDG Base Directory Specification}[https://specifications.freedesktop.org/basedir/latest/] places additional requirements on this
|
|
74
|
+
# directory related to ownership, permissions, and persistence. This library does not check
|
|
75
|
+
# these requirements.
|
|
76
|
+
|
|
77
|
+
# @!method runtime_dir
|
|
78
|
+
# Gets the user’s runtime directory.
|
|
79
|
+
# Currently, only the {Etcetera::BaseStrategy::Xdg Xdg} strategy supports this.
|
|
80
|
+
#
|
|
81
|
+
# Note: The {XDG Base Directory Specification}[https://specifications.freedesktop.org/basedir/latest/] places additional requirements on this
|
|
82
|
+
# directory related to ownership, permissions, and persistence. This library does not check
|
|
83
|
+
# these requirements.
|
|
84
|
+
# @return [Pathname, nil]
|
|
85
|
+
|
|
86
|
+
# Returns the current OS’s native BaseStrategy.
|
|
87
|
+
# This uses the Windows strategy on \Windows, Apple on macOS & iOS, and Xdg everywhere else.
|
|
88
|
+
# This is the convention used by most GUI applications.
|
|
89
|
+
def self.choose_native_strategy
|
|
90
|
+
if Gem.win_platform? then Windows.new
|
|
91
|
+
elsif RUBY_PLATFORM.include?("darwin") then Apple.new
|
|
92
|
+
else Xdg.new
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Returns the current OS’s default BaseStrategy.
|
|
97
|
+
# This uses the Windows strategy on \Windows, and Xdg everywhere else.
|
|
98
|
+
# This is the convention used by most CLI applications.
|
|
99
|
+
def self.choose_base_strategy
|
|
100
|
+
Gem.win_platform? ? Windows.new : Xdg.new
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fiddle/import"
|
|
4
|
+
|
|
5
|
+
module Etcetera
|
|
6
|
+
# The parts of the windows-sys crate that the crate uses, bound with Fiddle.
|
|
7
|
+
# Only Windows loads this, as only Windows builds of the crate depend on it.
|
|
8
|
+
#
|
|
9
|
+
# Derived from windows-sys 0.61.2 (https://github.com/microsoft/windows-rs),
|
|
10
|
+
# Copyright (c) Microsoft Corporation, used under the MIT License; see
|
|
11
|
+
# LICENSE-WINDOWS-SYS.
|
|
12
|
+
module WindowsSys # :nodoc: all
|
|
13
|
+
# :stopdoc:
|
|
14
|
+
|
|
15
|
+
extend Fiddle::Importer
|
|
16
|
+
|
|
17
|
+
dlload "shell32.dll", "combase.dll"
|
|
18
|
+
|
|
19
|
+
GUID = struct(["uint32_t data1", "uint16_t data2", "uint16_t data3", "uint8_t data4[8]"])
|
|
20
|
+
|
|
21
|
+
def GUID.from_u128(uuid)
|
|
22
|
+
malloc(Fiddle::RUBY_FREE).tap do |guid|
|
|
23
|
+
guid.data1 = uuid >> 96
|
|
24
|
+
guid.data2 = uuid >> 80 & 0xffff
|
|
25
|
+
guid.data3 = uuid >> 64 & 0xffff
|
|
26
|
+
guid.data4 = [uuid & 0xffff_ffff_ffff_ffff].pack("Q>").bytes
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
S_OK = 0
|
|
31
|
+
KF_FLAG_DONT_VERIFY = 16_384
|
|
32
|
+
FOLDERID_LocalAppData = GUID.from_u128(0xf1b32785_6fba_4fcf_9d55_7b8e7f157091)
|
|
33
|
+
FOLDERID_RoamingAppData = GUID.from_u128(0x3eb685db_65f9_4cf6_a03a_e3ef65729f3d)
|
|
34
|
+
|
|
35
|
+
extern "int32_t SHGetKnownFolderPath(void* rfid, uint32_t dwflags, void* htoken, void* ppszpath)"
|
|
36
|
+
extern "void CoTaskMemFree(void* pv)"
|
|
37
|
+
end
|
|
38
|
+
private_constant :WindowsSys
|
|
39
|
+
end
|