na 1.2.37 → 1.2.38
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +22 -0
- data/Gemfile.lock +1 -1
- data/README.md +79 -6
- data/bin/commands/add.rb +11 -13
- data/bin/commands/edit.rb +15 -19
- data/bin/commands/find.rb +11 -6
- data/bin/commands/init.rb +1 -1
- data/bin/commands/next.rb +56 -27
- data/bin/commands/projects.rb +1 -1
- data/bin/commands/saved.rb +3 -4
- data/bin/commands/tagged.rb +7 -7
- data/bin/commands/todos.rb +24 -14
- data/bin/commands/update.rb +24 -27
- data/bin/na +2 -1
- data/lib/na/action.rb +16 -25
- data/lib/na/actions.rb +8 -8
- data/lib/na/colors.rb +23 -1
- data/lib/na/editor.rb +13 -11
- data/lib/na/hash.rb +31 -0
- data/lib/na/next_action.rb +45 -38
- data/lib/na/pager.rb +1 -1
- data/lib/na/prompt.rb +6 -6
- data/lib/na/string.rb +23 -3
- data/lib/na/theme.rb +71 -0
- data/lib/na/todo.rb +2 -2
- data/lib/na/version.rb +1 -1
- data/lib/na.rb +1 -0
- data/src/_README.md +35 -15
- metadata +3 -2
data/bin/commands/todos.rb
CHANGED
@@ -8,24 +8,34 @@ class App
|
|
8
8
|
/, :, or a space, e.g. `na todos code/marked`'
|
9
9
|
arg_name 'QUERY', optional: true
|
10
10
|
command %i[todos] do |c|
|
11
|
-
c.
|
12
|
-
|
13
|
-
all_req = args.join(' ') !~ /[+!-]/
|
11
|
+
c.desc 'Open the todo database in an editor for manual modification'
|
12
|
+
c.switch %i[e edit]
|
14
13
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
14
|
+
c.action do |_global_options, options, args|
|
15
|
+
if options[:edit]
|
16
|
+
system("#{NA::Editor.default_editor(prefer_git_editor: false)} #{NA.database_path}")
|
17
|
+
editor = NA::Editor.default_editor(prefer_git_editor: false).highlight_filename
|
18
|
+
database = NA.database_path.highlight_filename
|
19
|
+
NA.notify("{b}#{NA.theme[:success]}Opened #{database}#{NA.theme[:success]} in #{editor}")
|
20
|
+
else
|
21
|
+
if args.count.positive?
|
22
|
+
all_req = args.join(' ') !~ /(?<=[, ])[+!-]/
|
23
|
+
|
24
|
+
tokens = [{ token: '*', required: all_req, negate: false }]
|
25
|
+
args.each do |arg|
|
26
|
+
arg.split(/ *, */).each do |a|
|
27
|
+
m = a.match(/^(?<req>[+!-])?(?<tok>.*?)$/)
|
28
|
+
tokens.push({
|
29
|
+
token: m['tok'],
|
30
|
+
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
31
|
+
negate: (!m['req'].nil? && m['req'] =~ /[!-]/) ? true : false
|
32
|
+
})
|
33
|
+
end
|
24
34
|
end
|
25
35
|
end
|
26
|
-
end
|
27
36
|
|
28
|
-
|
37
|
+
NA.list_todos(query: tokens)
|
38
|
+
end
|
29
39
|
end
|
30
40
|
end
|
31
41
|
end
|
data/bin/commands/update.rb
CHANGED
@@ -111,8 +111,8 @@ class App
|
|
111
111
|
]
|
112
112
|
`gum input #{opts.join(' ')}`.strip
|
113
113
|
elsif $stdin.isatty && options[:tagged].empty?
|
114
|
-
|
115
|
-
reader.read_line(NA::Color.template(
|
114
|
+
NA.notify("#{NA.theme[:prompt]}Enter search string:")
|
115
|
+
reader.read_line(NA::Color.template("#{NA.theme[:filename]}> #{NA.theme[:action]}")).strip
|
116
116
|
end
|
117
117
|
|
118
118
|
if action
|
@@ -123,35 +123,34 @@ class App
|
|
123
123
|
tokens = Regexp.new(action, Regexp::IGNORECASE)
|
124
124
|
else
|
125
125
|
tokens = []
|
126
|
-
all_req = action !~ /[
|
126
|
+
all_req = action !~ /[+!-]/ && !options[:or]
|
127
127
|
|
128
128
|
action.split(/ /).each do |arg|
|
129
129
|
m = arg.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
|
130
130
|
tokens.push({
|
131
131
|
token: m['tok'],
|
132
132
|
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
133
|
-
negate: !m['req'].nil? && m['req'] =~ /[
|
133
|
+
negate: !m['req'].nil? && m['req'] =~ /[!-]/ ? true : false
|
134
134
|
})
|
135
135
|
end
|
136
136
|
end
|
137
137
|
end
|
138
138
|
|
139
139
|
if (action.nil? || action.empty?) && options[:tagged].empty?
|
140
|
-
|
141
|
-
Process.exit 1
|
140
|
+
NA.notify("#{NA.theme[:error]}Empty input, cancelled", exit_code: 1)
|
142
141
|
end
|
143
142
|
|
144
|
-
all_req = options[:tagged].join(' ') !~ /[
|
143
|
+
all_req = options[:tagged].join(' ') !~ /[+!-]/ && !options[:or]
|
145
144
|
tags = []
|
146
145
|
options[:tagged].join(',').split(/ *, */).each do |arg|
|
147
|
-
m = arg.match(/^(?<req>[
|
146
|
+
m = arg.match(/^(?<req>[+!-])?(?<tag>[^ =<>$*~\^]+?) *(?:(?<op>[=<>~]{1,2}|[*$\^]=) *(?<val>.*?))?$/)
|
148
147
|
|
149
148
|
tags.push({
|
150
149
|
tag: m['tag'].wildcard_to_rx,
|
151
150
|
comp: m['op'],
|
152
151
|
value: m['val'],
|
153
152
|
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
154
|
-
negate: !m['req'].nil? && m['req'] =~ /[
|
153
|
+
negate: !m['req'].nil? && m['req'] =~ /[!-]/ ? true : false
|
155
154
|
})
|
156
155
|
end
|
157
156
|
|
@@ -170,7 +169,7 @@ class App
|
|
170
169
|
args << '--width $(tput cols)'
|
171
170
|
`gum write #{args.join(' ')}`.strip.split("\n")
|
172
171
|
else
|
173
|
-
|
172
|
+
NA.notify("#{NA.theme[:prompt]}Enter a note, {bw}CTRL-d#{NA.theme[:prompt]} to end editing:#{NA.theme[:action]}")
|
174
173
|
reader.read_multiline
|
175
174
|
end
|
176
175
|
end
|
@@ -182,13 +181,11 @@ class App
|
|
182
181
|
options[:project]
|
183
182
|
elsif NA.cwd_is == :project
|
184
183
|
NA.cwd
|
185
|
-
else
|
186
|
-
nil
|
187
184
|
end
|
188
185
|
|
189
186
|
if options[:file]
|
190
187
|
file = File.expand_path(options[:file])
|
191
|
-
NA.notify(
|
188
|
+
NA.notify("#{NA.theme[:error]}File not found", exit_code: 1) unless File.exist?(file)
|
192
189
|
|
193
190
|
targets = [file]
|
194
191
|
elsif options[:todo]
|
@@ -198,7 +195,7 @@ class App
|
|
198
195
|
todo.push({
|
199
196
|
token: m['tok'],
|
200
197
|
required: all_req || (!m['req'].nil? && m['req'] == '+'),
|
201
|
-
negate: !m['req'].nil? && m['req'] =~ /[
|
198
|
+
negate: !m['req'].nil? && m['req'] =~ /[!-]/ ? true : false
|
202
199
|
})
|
203
200
|
end
|
204
201
|
dirs = NA.match_working_dir(todo)
|
@@ -207,25 +204,25 @@ class App
|
|
207
204
|
targets = [dirs[0]]
|
208
205
|
elsif dirs.count.positive?
|
209
206
|
targets = NA.select_file(dirs, multiple: true)
|
210
|
-
NA.notify(
|
207
|
+
NA.notify("#{NA.theme[:error]}Cancelled", exit_code: 1) unless targets && targets.count.positive?
|
211
208
|
else
|
212
|
-
NA.notify(
|
209
|
+
NA.notify("#{NA.theme[:error]}Todo not found", exit_code: 1) unless targets && targets.count.positive?
|
213
210
|
|
214
211
|
end
|
215
212
|
else
|
216
213
|
files = NA.find_files_matching({
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
NA.notify(
|
214
|
+
depth: options[:depth],
|
215
|
+
done: options[:done],
|
216
|
+
project: target_proj,
|
217
|
+
regex: options[:regex],
|
218
|
+
require_na: false,
|
219
|
+
search: tokens,
|
220
|
+
tag: tags
|
221
|
+
})
|
222
|
+
NA.notify("#{NA.theme[:error]}No todo file found", exit_code: 1) if files.count.zero?
|
226
223
|
|
227
224
|
targets = files.count > 1 ? NA.select_file(files, multiple: true) : [files[0]]
|
228
|
-
NA.notify(
|
225
|
+
NA.notify("#{NA.theme[:error]}Cancelled", exit_code: 1) unless files.count.positive?
|
229
226
|
|
230
227
|
end
|
231
228
|
|
@@ -234,7 +231,7 @@ class App
|
|
234
231
|
options[:project] = 'Archive'
|
235
232
|
end
|
236
233
|
|
237
|
-
NA.notify(
|
234
|
+
NA.notify("#{NA.theme[:error]}No search terms provided", exit_code: 1) if tokens.nil? && options[:tagged].empty?
|
238
235
|
|
239
236
|
targets.each do |target|
|
240
237
|
NA.update_action(target, tokens,
|
data/bin/na
CHANGED
@@ -37,6 +37,7 @@ class App
|
|
37
37
|
default_command :next
|
38
38
|
|
39
39
|
NA::Color.coloring = $stdin.isatty
|
40
|
+
NA::Pager.paginate = $stdin.isatty
|
40
41
|
|
41
42
|
desc 'Add a next action (deprecated, for backwards compatibility)'
|
42
43
|
switch %i[a add], negatable: false
|
@@ -84,7 +85,7 @@ class App
|
|
84
85
|
|
85
86
|
pre do |global, _command, _options, _args|
|
86
87
|
NA.verbose = global[:debug]
|
87
|
-
NA::Pager.paginate = global[:pager]
|
88
|
+
NA::Pager.paginate = global[:pager] && $stdout.isatty
|
88
89
|
NA::Color.coloring = global[:color]
|
89
90
|
NA.extension = global[:ext]
|
90
91
|
NA.na_tag = global[:na_tag]
|
data/lib/na/action.rb
CHANGED
@@ -26,15 +26,15 @@ module NA
|
|
26
26
|
string += " @priority(#{priority})"
|
27
27
|
end
|
28
28
|
|
29
|
-
|
29
|
+
remove_tag.each do |tag|
|
30
30
|
string.gsub!(/(?<=\A| )@#{tag.gsub(/([()*?])/, '\\\\1')}(\(.*?\))?/, '')
|
31
31
|
string.strip!
|
32
|
-
string += " @#{tag}"
|
33
32
|
end
|
34
33
|
|
35
|
-
|
34
|
+
add_tag.each do |tag|
|
36
35
|
string.gsub!(/(?<=\A| )@#{tag.gsub(/([()*?])/, '\\\\1')}(\(.*?\))?/, '')
|
37
36
|
string.strip!
|
37
|
+
string += " @#{tag}"
|
38
38
|
end
|
39
39
|
|
40
40
|
string = "#{string.strip} @done(#{Time.now.strftime('%Y-%m-%d %H:%M')})" if finish && string !~ /(?<=\A| )@done/
|
@@ -74,25 +74,14 @@ module NA
|
|
74
74
|
## @param notes [Boolean] Include notes
|
75
75
|
##
|
76
76
|
def pretty(extension: 'taskpaper', template: {}, regexes: [], notes: false)
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
parent: '{c}',
|
81
|
-
parent_divider: '{xw}/',
|
82
|
-
action: '{bg}',
|
83
|
-
project: '{xbk}',
|
84
|
-
tags: '{m}',
|
85
|
-
value_parens: '{m}',
|
86
|
-
values: '{y}',
|
87
|
-
output: '%filename%parents| %action',
|
88
|
-
note: '{dw}'
|
89
|
-
}
|
90
|
-
template = default_template.merge(template)
|
77
|
+
theme = NA::Theme.load_theme
|
78
|
+
template = theme.merge(template)
|
79
|
+
|
91
80
|
# Create the hierarchical parent string
|
92
81
|
parents = @parent.map do |par|
|
93
|
-
NA::Color.template("#{template[:parent]}#{par}")
|
82
|
+
NA::Color.template("{x}#{template[:parent]}#{par}")
|
94
83
|
end.join(NA::Color.template(template[:parent_divider]))
|
95
|
-
parents = "{
|
84
|
+
parents = "#{NA.theme[:bracket]}[#{NA.theme[:error]}#{parents}#{NA.theme[:bracket]}]{x} "
|
96
85
|
|
97
86
|
# Create the project string
|
98
87
|
project = NA::Color.template("#{template[:project]}#{@project}{x} ")
|
@@ -101,7 +90,7 @@ module NA
|
|
101
90
|
file = @file.sub(%r{^\./}, '').sub(/#{ENV['HOME']}/, '~')
|
102
91
|
file = file.sub(/\.#{extension}$/, '')
|
103
92
|
# colorize the basename
|
104
|
-
file = file.
|
93
|
+
file = file.highlight_filename
|
105
94
|
file_tpl = "#{template[:file]}#{file} {x}"
|
106
95
|
filename = NA::Color.template(file_tpl)
|
107
96
|
|
@@ -200,13 +189,13 @@ module NA
|
|
200
189
|
tag_date = Time.parse(tag_val)
|
201
190
|
date = Chronic.parse(val)
|
202
191
|
|
192
|
+
raise ArgumentError if date.nil?
|
193
|
+
|
203
194
|
unless val =~ /(\d:\d|a[mp]|now)/i
|
204
195
|
tag_date = Time.parse(tag_date.strftime('%Y-%m-%d 12:00'))
|
205
196
|
date = Time.parse(date.strftime('%Y-%m-%d 12:00'))
|
206
197
|
end
|
207
198
|
|
208
|
-
# NA.notify("{dw}Comparing #{tag_date} #{tag[:comp]} #{date}{x}", debug: true)
|
209
|
-
|
210
199
|
case tag[:comp]
|
211
200
|
when /^>$/
|
212
201
|
tag_date > date
|
@@ -227,7 +216,7 @@ module NA
|
|
227
216
|
else
|
228
217
|
false
|
229
218
|
end
|
230
|
-
rescue
|
219
|
+
rescue ArgumentError
|
231
220
|
case tag[:comp]
|
232
221
|
when /^>$/
|
233
222
|
tag_val.to_f > val.to_f
|
@@ -239,12 +228,14 @@ module NA
|
|
239
228
|
tag_val.to_f >= val.to_f
|
240
229
|
when /^==?$/
|
241
230
|
tag_val =~ /^#{val.wildcard_to_rx}$/
|
231
|
+
when /^=~$/
|
232
|
+
tag_val =~ Regexp.new(val, Regexp::IGNORECASE)
|
242
233
|
when /^\$=$/
|
243
234
|
tag_val =~ /#{val.wildcard_to_rx}$/i
|
244
235
|
when /^\*=$/
|
245
|
-
tag_val =~
|
236
|
+
tag_val =~ /.*?#{val.wildcard_to_rx}.*?/i
|
246
237
|
when /^\^=$/
|
247
|
-
tag_val =~ /^#{val.wildcard_to_rx}/
|
238
|
+
tag_val =~ /^#{val.wildcard_to_rx}/i
|
248
239
|
else
|
249
240
|
false
|
250
241
|
end
|
data/lib/na/actions.rb
CHANGED
@@ -20,7 +20,7 @@ module NA
|
|
20
20
|
return if files.nil?
|
21
21
|
|
22
22
|
if nest
|
23
|
-
template =
|
23
|
+
template = NA.theme[:templates][:default]
|
24
24
|
|
25
25
|
parent_files = {}
|
26
26
|
out = []
|
@@ -40,7 +40,7 @@ module NA
|
|
40
40
|
out.concat(NA.output_children(projects, 0))
|
41
41
|
end
|
42
42
|
else
|
43
|
-
template =
|
43
|
+
template = NA.theme[:templates][:default]
|
44
44
|
|
45
45
|
each do |action|
|
46
46
|
if parent_files.key?(action.file)
|
@@ -62,22 +62,22 @@ module NA
|
|
62
62
|
else
|
63
63
|
template = if files.count.positive?
|
64
64
|
if files.count == 1
|
65
|
-
|
65
|
+
NA.theme[:templates][:single_file]
|
66
66
|
else
|
67
|
-
|
67
|
+
NA.theme[:templates][:multi_file]
|
68
68
|
end
|
69
69
|
elsif NA.find_files(depth: depth).count > 1
|
70
70
|
if depth > 1
|
71
|
-
|
71
|
+
NA.theme[:templates][:multi_file]
|
72
72
|
else
|
73
|
-
|
73
|
+
NA.theme[:templates][:single_file]
|
74
74
|
end
|
75
75
|
else
|
76
|
-
|
76
|
+
NA.theme[:templates][:default]
|
77
77
|
end
|
78
78
|
template += '%note' if notes
|
79
79
|
|
80
|
-
files.map { |f| NA.notify(
|
80
|
+
files.map { |f| NA.notify(f, debug: true) } if files
|
81
81
|
|
82
82
|
output = map { |action| action.pretty(template: { output: template }, regexes: regexes, notes: notes) }
|
83
83
|
NA::Pager.page(output.join("\n"))
|
data/lib/na/colors.rb
CHANGED
@@ -5,7 +5,7 @@ module NA
|
|
5
5
|
# Terminal output color functions.
|
6
6
|
module Color
|
7
7
|
# Regexp to match excape sequences
|
8
|
-
ESCAPE_REGEX = /(?<=\[)(?:(?:(?:[349]|10)[0-9]|[0-9])?;?)+(?=m)
|
8
|
+
ESCAPE_REGEX = /(?<=\[)(?:(?:(?:[349]|10)[0-9]|[0-9])?;?)+(?=m)/
|
9
9
|
|
10
10
|
# All available color names. Available as methods and string extensions.
|
11
11
|
#
|
@@ -214,9 +214,15 @@ module NA
|
|
214
214
|
## m: magenta, r: red, b: bold, u: underline, i: italic,
|
215
215
|
## x: reset (remove background, color, emphasis)
|
216
216
|
##
|
217
|
+
## Also accepts {#RGB} and {#RRGGBB} strings. Put a b before
|
218
|
+
## the hash to make it a background color
|
219
|
+
##
|
217
220
|
## @example Convert a templated string
|
218
221
|
## Color.template('{Rwb}Warning:{x} {w}you look a little {g}ill{x}')
|
219
222
|
##
|
223
|
+
## @example Convert using RGB colors
|
224
|
+
## Color.template('{#f0a}This is an RGB color')
|
225
|
+
##
|
220
226
|
## @param input [String, Array] The template
|
221
227
|
## string. If this is an array, the
|
222
228
|
## elements will be joined with a
|
@@ -228,6 +234,11 @@ module NA
|
|
228
234
|
input = input.join(' ') if input.is_a? Array
|
229
235
|
return input.gsub(/(?<!\\)\{(\w+)\}/i, '') unless NA::Color.coloring?
|
230
236
|
|
237
|
+
input = input.gsub(/(?<!\\)\{((?:[fb]g?)?#[a-f0-9]{3,6})\}/i) do
|
238
|
+
hex = Regexp.last_match(1)
|
239
|
+
rgb(hex)
|
240
|
+
end
|
241
|
+
|
231
242
|
fmt = input.gsub(/%/, '%%')
|
232
243
|
fmt = fmt.gsub(/(?<!\\)\{(\w+)\}/i) do
|
233
244
|
Regexp.last_match(1).split('').map { |c| "%<#{c}>s" }.join('')
|
@@ -301,6 +312,17 @@ module NA
|
|
301
312
|
is_bg = hex.match(/^bg?#/) ? true : false
|
302
313
|
hex_string = hex.sub(/^([fb]g?)?#/, '')
|
303
314
|
|
315
|
+
if hex_string.length == 3
|
316
|
+
parts = hex_string.match(/(?<r>.)(?<g>.)(?<b>.)/)
|
317
|
+
|
318
|
+
t = []
|
319
|
+
%w[r g b].each do |e|
|
320
|
+
t << parts[e]
|
321
|
+
t << parts[e]
|
322
|
+
end
|
323
|
+
hex_string = t.join('')
|
324
|
+
end
|
325
|
+
|
304
326
|
parts = hex_string.match(/(?<r>..)(?<g>..)(?<b>..)/)
|
305
327
|
t = []
|
306
328
|
%w[r g b].each do |e|
|
data/lib/na/editor.rb
CHANGED
@@ -1,24 +1,26 @@
|
|
1
1
|
module NA
|
2
2
|
module Editor
|
3
3
|
class << self
|
4
|
-
def default_editor
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
4
|
+
def default_editor(prefer_git_editor: true)
|
5
|
+
if prefer_git_editor
|
6
|
+
editor ||= ENV['NA_EDITOR'] || ENV['GIT_EDITOR'] || ENV['EDITOR']
|
7
|
+
else
|
8
|
+
editor ||= ENV['NA_EDITOR'] || ENV['EDITOR'] || ENV['GIT_EDITOR']
|
9
9
|
end
|
10
10
|
|
11
|
-
|
11
|
+
return editor if editor&.good? && TTY::Which.exist?(editor)
|
12
|
+
|
13
|
+
NA.notify("No EDITOR environment variable, testing available editors", debug: true)
|
12
14
|
editors = %w[vim vi code subl mate mvim nano emacs]
|
13
15
|
editors.each do |ed|
|
14
16
|
try = TTY::Which.which(ed)
|
15
17
|
if try
|
16
|
-
notify("Using editor #{try}", debug: true)
|
18
|
+
NA.notify("Using editor #{try}", debug: true)
|
17
19
|
return try
|
18
20
|
end
|
19
21
|
end
|
20
22
|
|
21
|
-
notify(
|
23
|
+
NA.notify("#{NA.theme[:error]}No editor found", exit_code: 5)
|
22
24
|
|
23
25
|
nil
|
24
26
|
end
|
@@ -49,7 +51,7 @@ module NA
|
|
49
51
|
def fork_editor(input = '', message: :default)
|
50
52
|
# raise NonInteractive, 'Non-interactive terminal' unless $stdout.isatty || ENV['DOING_EDITOR_TEST']
|
51
53
|
|
52
|
-
notify(
|
54
|
+
NA.notify("#{NA.theme[:error]}No EDITOR variable defined in environment", exit_code: 5) if default_editor.nil?
|
53
55
|
|
54
56
|
tmpfile = Tempfile.new(['na_temp', '.na'])
|
55
57
|
|
@@ -97,11 +99,11 @@ module NA
|
|
97
99
|
## @return [Array] [[String]title, [Note]note]
|
98
100
|
##
|
99
101
|
def format_input(input)
|
100
|
-
notify(
|
102
|
+
NA.notify("#{NA.theme[:error]}No content in entry", exit_code: 1) if input.nil? || input.strip.empty?
|
101
103
|
|
102
104
|
input_lines = input.split(/[\n\r]+/).delete_if(&:ignore?)
|
103
105
|
title = input_lines[0]&.strip
|
104
|
-
notify(
|
106
|
+
NA.notify("#{NA.theme[:error]}No content in first line", exit_code: 1) if title.nil? || title.strip.empty?
|
105
107
|
|
106
108
|
title.expand_date_tags
|
107
109
|
|
data/lib/na/hash.rb
CHANGED
@@ -4,4 +4,35 @@ class ::Hash
|
|
4
4
|
def symbolize_keys
|
5
5
|
each_with_object({}) { |(k, v), hsh| hsh[k.to_sym] = v.is_a?(Hash) ? v.symbolize_keys : v }
|
6
6
|
end
|
7
|
+
|
8
|
+
##
|
9
|
+
## Freeze all values in a hash
|
10
|
+
##
|
11
|
+
## @return Hash with all values frozen
|
12
|
+
##
|
13
|
+
def deep_freeze
|
14
|
+
chilled = {}
|
15
|
+
each do |k, v|
|
16
|
+
chilled[k] = v.is_a?(Hash) ? v.deep_freeze : v.freeze
|
17
|
+
end
|
18
|
+
|
19
|
+
chilled.freeze
|
20
|
+
end
|
21
|
+
|
22
|
+
def deep_freeze!
|
23
|
+
replace deep_thaw.deep_freeze
|
24
|
+
end
|
25
|
+
|
26
|
+
def deep_thaw
|
27
|
+
chilled = {}
|
28
|
+
each do |k, v|
|
29
|
+
chilled[k] = v.is_a?(Hash) ? v.deep_thaw : v.dup
|
30
|
+
end
|
31
|
+
|
32
|
+
chilled.dup
|
33
|
+
end
|
34
|
+
|
35
|
+
def deep_thaw!
|
36
|
+
replace deep_thaw
|
37
|
+
end
|
7
38
|
end
|