journal-cli 1.0.16 → 1.0.18

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f3855c36879c6970f3e312add21e3b1d4b8667c69d32d32d59389211a47ce87a
4
- data.tar.gz: 8b7fe4a087f81ed075ad41bbfe84df52e250779fae1591719138d90dfbf6e093
3
+ metadata.gz: 2ebc5eaefc846fb4343859ff0148a7798e93e2a4bcc04c5c73ce18151e59ea94
4
+ data.tar.gz: 87ca031548e115350383a28da8ec853d6d665dd2a9c88ac3ea32105350120785
5
5
  SHA512:
6
- metadata.gz: 81f6c91df724bd480d8c67284c23b7d1405f5bffbfb2a5626a03ede1444decd3fa6c77f1c55c8414ad65f7c3c1359f58e7a559d592ae43ac76cca4e1e0794be6
7
- data.tar.gz: 756849fb99ee4e287a5e3943b35c3bb1d6b7d1a9cf72d9441b7c849060a80300d310809a8b6ac3e74d9f7b6afbdff4bf968891f020857746ca77773b15e2b09f
6
+ metadata.gz: 7d15d09cce5d189a2d6168e02e9c3122334a88ea9e011b82fa97b0a732901913b6668407f3474211ef6b40b3b068363d9778ff4471a8ded4366089338f3fd7af
7
+ data.tar.gz: 3bce2d14e6d5f078b481b9d38184e9cd005c336aeecb36bf4a5daa8b7ef66510c60d46d3cd9db97e254266c8c91a07bbf209785b3aedd2d27f6a255e256f0341
data/CHANGELOG.md CHANGED
@@ -1,3 +1,24 @@
1
+ ### 1.0.18
2
+
3
+ 2023-09-09 12:29
4
+
5
+ #### IMPROVED
6
+
7
+ - Include the answers to all questions as YAML front matter when writing individual Markdown files. This allows for tools like [obsidian-dataview](https://github.com/blacksmithgu/obsidian-dataview) to be used as parsers
8
+
9
+ #### FIXED
10
+
11
+ - Daily markdown was being saved to /journal/entries/KEY/entries
12
+ - Missing color library
13
+
14
+ ### 1.0.17
15
+
16
+ 2023-09-08 07:21
17
+
18
+ #### IMPROVED
19
+
20
+ - More confirmation messages when saving
21
+
1
22
  ### 1.0.16
2
23
 
3
24
  2023-09-07 11:12
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- journal-cli (1.0.16)
4
+ journal-cli (1.0.18)
5
5
  chronic (~> 0.10, >= 0.10.2)
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -48,7 +48,7 @@ This file contains a YAML definition of your journal. Each journal gets a top-le
48
48
 
49
49
  You can include weather data automatically by setting a question type to 'weather'. In order for this to work, you'll need to define `zip` and `weather_api` keys. `zip` is just your zip code, and `weather_api` is a key from WeatherAPI.com. Sign up [here](https://www.weatherapi.com/) for a free plan, and then visit the [profile page](https://www.weatherapi.com/my/) to see your API key at the top.
50
50
 
51
- ### Journal configuration
51
+ ### Journal Configuration
52
52
 
53
53
  Edit the file at `~/.config/journal/journals.yaml` following this structure:
54
54
 
@@ -128,6 +128,28 @@ journals: # required key
128
128
 
129
129
  A journal must contain a `sections` key, and each section must contain a `questions` key with an array of questions. Each question must (at minimum) have a `prompt`, `key`, and `type`.
130
130
 
131
+ If a question has a key `secondary_question`, the prompt will be repeated with the secondary question until it's returned empty, answers will be joined together.
132
+
133
+ ### Naming Keys
134
+
135
+ If you want data stored in a nested object, you can set a question type to `dictionary` and set the prompt to `null` (or just leave the key out), but give it a key that will serve as the parent in the object. Then in the nested questions, give them a key in the dot format `[PARENT_KEY].[CHILD_KEY]`. Section keys automatically nest their children, but if you want to go deeper, you could have a question with the key `health` and type `dictionary`, then have questions with keys like `health.rating` and `health.notes`. If the section key was `status`, the resulting dictionary would look like this in the JSON:
136
+
137
+ ```json
138
+ {
139
+ "date": "2023-09-08 12:19:40 UTC",
140
+ "data": {
141
+ "status": {
142
+ "health": {
143
+ "rating": 4,
144
+ "notes": "Feeling much better today. Still a bit groggy."
145
+ }
146
+ }
147
+ }
148
+ }
149
+ ```
150
+
151
+ If a question has the same key as its parent section, it will be moved up the chain so that you don't get `{ 'journal': { 'journal': 'Journal notes' } }`. You'll just get `{ 'journal': 'Journal notes' }`. This offers a way to organize data with fewer levels of nesting in the output.
152
+
131
153
  ## Usage
132
154
 
133
155
  Once your configuration file is set up, you can just run `journal JOURNAL_KEY` to begin prompting for the answers to the configured questions.
data/bin/journal CHANGED
@@ -58,6 +58,11 @@ optparse = OptionParser.new do |opts|
58
58
  Process.exit 0
59
59
  end
60
60
 
61
+ Color.coloring = $stdout.isatty
62
+ opts.on('--[no-]color', 'Colorize output') do |c|
63
+ Color.coloring = c
64
+ end
65
+
61
66
  opts.on('-h', '--help', 'Display help') do
62
67
  puts opts
63
68
  puts
@@ -63,15 +63,16 @@ module Journal
63
63
  cmd << %(-t #{@journal['tags'].join(' ')}) if @journal.key?('tags')
64
64
  cmd << %(-date "#{@date.strftime('%Y-%m-%d %I:%M %p')}")
65
65
  `echo #{Shellwords.escape(to_markdown(yaml: false, title: true))} | #{cmd.join(' ')} -- new`
66
+ Journal.notify('{bg}Entered one entry into Day One')
66
67
  end
67
68
 
68
69
  def save_single_markdown
69
70
  dir = if @journal.key?('entries_folder')
70
71
  File.join(File.expand_path(@journal['entries_folder']), 'entries')
71
72
  elsif Journal.config.key?('entries_folder')
72
- File.join(File.expand_path(Journal.config['entries_folder']), @key, 'entries')
73
+ File.join(File.expand_path(Journal.config['entries_folder']), @key)
73
74
  else
74
- File.expand_path('~/.local/share/journal', @key, 'entries')
75
+ File.expand_path("~/.local/share/journal/#{@key}/entries")
75
76
  end
76
77
 
77
78
  FileUtils.mkdir_p(dir) unless File.directory?(dir)
@@ -84,16 +85,16 @@ module Journal
84
85
  f.puts
85
86
  f.puts to_markdown(yaml: false, title: false)
86
87
  end
87
- puts "Saved #{target}"
88
+ Journal.notify "{bg}Added new entry to {bw}#{target}"
88
89
  end
89
90
 
90
91
  def save_daily_markdown
91
92
  dir = if @journal.key?('entries_folder')
92
93
  File.join(File.expand_path(@journal['entries_folder']), 'entries')
93
94
  elsif Journal.config.key?('entries_folder')
94
- File.join(File.expand_path(Journal.config['entries_folder']), @key, 'entries')
95
+ File.join(File.expand_path(Journal.config['entries_folder']), @key)
95
96
  else
96
- File.join(File.expand_path('~/.local/share/journal/entries'), @key, 'entries')
97
+ File.join(File.expand_path("~/.local/share/journal/#{@key}/entries"))
97
98
  end
98
99
 
99
100
  FileUtils.mkdir_p(dir) unless File.directory?(dir)
@@ -105,7 +106,7 @@ module Journal
105
106
  else
106
107
  File.open(target, 'w') { |f| f.puts to_markdown(yaml: true, title: true, date: false, time: true) }
107
108
  end
108
- puts "Saved #{target}"
109
+ Journal.notify "{bg}Saved daily Markdown to {bw}#{target}"
109
110
  end
110
111
 
111
112
  def save_individual_markdown
@@ -123,7 +124,7 @@ module Journal
123
124
  filename = @date.strftime('%Y-%m-%d_%H:%M.md')
124
125
  target = File.join(dir, filename)
125
126
  File.open(target, 'w') { |f| f.puts to_markdown(yaml: true, title: true) }
126
- puts "Saved #{target}"
127
+ puts "Saved new entry to #{target}"
127
128
  end
128
129
 
129
130
  def print_answer(prompt, type, key, data)
@@ -143,18 +144,33 @@ module Journal
143
144
  end
144
145
  end
145
146
 
147
+ def weather_to_yaml(answers)
148
+ data = {}
149
+ answers.each do |k, v|
150
+ case v.class.to_s
151
+ when /Hash/
152
+ data[k] = weather_to_yaml(v)
153
+ when /Weather/
154
+ data[k] = v.to_s
155
+ else
156
+ data[k] = v
157
+ end
158
+ end
159
+ data
160
+ end
161
+
146
162
  def to_markdown(yaml: false, title: false, date: false, time: false)
147
163
  @output = []
148
164
 
149
165
  if yaml
150
166
  @date.localtime
151
- @output << <<~EOYAML
152
- ---
153
- title: #{@title}
154
- date: #{@date.strftime('%x %X')}
155
- ---
167
+ yaml_data = { 'title' => @title, 'date' => @date.strftime('%x %X')}
168
+ @data.each do |key, data|
169
+ yaml_data = yaml_data.merge(weather_to_yaml(data.answers))
170
+ end
156
171
 
157
- EOYAML
172
+ @output << YAML.dump(yaml_data).strip
173
+ @output << '---'
158
174
  end
159
175
 
160
176
  if title
@@ -198,7 +214,7 @@ module Journal
198
214
  elsif Journal.config.key?('entries_folder')
199
215
  File.expand_path(Journal.config['entries_folder'])
200
216
  else
201
- File.expand_path("~/.local/share/journal")
217
+ File.expand_path('~/.local/share/journal')
202
218
  end
203
219
  FileUtils.mkdir_p(dir) unless File.directory?(dir)
204
220
  db = File.join(dir, "#{@key}.json")
@@ -217,13 +233,18 @@ module Journal
217
233
  v.each do |key, value|
218
234
  result = case value.class.to_s
219
235
  when /Weather/
220
- { 'high' => value.data[:high], 'low' => value.data[:low], 'condition' => value.data[:condition] }
236
+ {
237
+ 'high' => value.data[:high],
238
+ 'low' => value.data[:low],
239
+ 'condition' => value.data[:condition]
240
+ }
221
241
  else
222
242
  value
223
243
  end
224
244
  if jk == k
225
245
  output[jk][key] = result
226
246
  else
247
+ output[jk][k] ||= {}
227
248
  output[jk][k][key] = result
228
249
  end
229
250
  end
@@ -245,6 +266,7 @@ module Journal
245
266
  data.sort_by! { |e| e['date'] }
246
267
 
247
268
  File.open(db, 'w') { |f| f.puts JSON.pretty_generate(data) }
269
+ Journal.notify "{bg}Saved {bw}#{db}"
248
270
  end
249
271
  end
250
272
  end
@@ -0,0 +1,363 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Cribbed from <https://github.com/flori/term-ansicolor>
4
+ # Terminal output color functions.
5
+ module Color
6
+ # Regexp to match excape sequences
7
+ ESCAPE_REGEX = /(?<=\[)(?:(?:(?:[349]|10)[0-9]|[0-9])?;?)+(?=m)/
8
+
9
+ # All available color names. Available as methods and string extensions.
10
+ #
11
+ # @example Use a color as a method. Color reset will be added to end of string.
12
+ # Color.yellow('This text is yellow') => "\e[33mThis text is yellow\e[0m"
13
+ #
14
+ # @example Use a color as a string extension. Color reset added automatically.
15
+ # 'This text is green'.green => "\e[1;32mThis text is green\e[0m"
16
+ #
17
+ # @example Send a text string as a color
18
+ # Color.send('red') => "\e[31m"
19
+ ATTRIBUTES = [
20
+ [:clear, 0], # String#clear is already used to empty string in Ruby 1.9
21
+ [:reset, 0], # synonym for :clear
22
+ [:bold, 1],
23
+ [:dark, 2],
24
+ [:italic, 3], # not widely implemented
25
+ [:underline, 4],
26
+ [:underscore, 4], # synonym for :underline
27
+ [:blink, 5],
28
+ [:rapid_blink, 6], # not widely implemented
29
+ [:negative, 7], # no reverse because of String#reverse
30
+ [:concealed, 8],
31
+ [:strikethrough, 9], # not widely implemented
32
+ [:strike, 9], # not widely implemented
33
+ [:black, 30],
34
+ [:red, 31],
35
+ [:green, 32],
36
+ [:yellow, 33],
37
+ [:blue, 34],
38
+ [:magenta, 35],
39
+ [:purple, 35],
40
+ [:cyan, 36],
41
+ [:white, 37],
42
+ [:bgblack, 40],
43
+ [:bgred, 41],
44
+ [:bggreen, 42],
45
+ [:bgyellow, 43],
46
+ [:bgblue, 44],
47
+ [:bgmagenta, 45],
48
+ [:bgpurple, 45],
49
+ [:bgcyan, 46],
50
+ [:bgwhite, 47],
51
+ [:boldblack, 90],
52
+ [:boldred, 91],
53
+ [:boldgreen, 92],
54
+ [:boldyellow, 93],
55
+ [:boldblue, 94],
56
+ [:boldmagenta, 95],
57
+ [:boldpurple, 95],
58
+ [:boldcyan, 96],
59
+ [:boldwhite, 97],
60
+ [:boldbgblack, 100],
61
+ [:boldbgred, 101],
62
+ [:boldbggreen, 102],
63
+ [:boldbgyellow, 103],
64
+ [:boldbgblue, 104],
65
+ [:boldbgmagenta, 105],
66
+ [:boldbgpurple, 105],
67
+ [:boldbgcyan, 106],
68
+ [:boldbgwhite, 107],
69
+ [:softpurple, '0;35;40'],
70
+ [:hotpants, '7;34;40'],
71
+ [:knightrider, '7;30;40'],
72
+ [:flamingo, '7;31;47'],
73
+ [:yeller, '1;37;43'],
74
+ [:whiteboard, '1;30;47'],
75
+ [:chalkboard, '1;37;40'],
76
+ [:led, '0;32;40'],
77
+ [:redacted, '0;30;40'],
78
+ [:alert, '1;31;43'],
79
+ [:error, '1;37;41'],
80
+ [:default, '0;39']
81
+ ].map(&:freeze).freeze
82
+
83
+ # Array of attribute keys only
84
+ ATTRIBUTE_NAMES = ATTRIBUTES.transpose.first
85
+
86
+ # Returns true if Color supports the +feature+.
87
+ #
88
+ # The feature :clear, that is mixing the clear color attribute into String,
89
+ # is only supported on ruby implementations, that do *not* already
90
+ # implement the String#clear method. It's better to use the reset color
91
+ # attribute instead.
92
+ def support?(feature)
93
+ case feature
94
+ when :clear
95
+ !String.instance_methods(false).map(&:to_sym).include?(:clear)
96
+ end
97
+ end
98
+
99
+ # Template coloring
100
+ class ::String
101
+ ##
102
+ ## Extract the longest valid %color name from a string.
103
+ ##
104
+ ## Allows %colors to bleed into other text and still
105
+ ## be recognized, e.g. %greensomething still finds
106
+ ## %green.
107
+ ##
108
+ ## @return [String] a valid color name
109
+ ##
110
+ def validate_color
111
+ valid_color = nil
112
+ compiled = ''
113
+ normalize_color.split('').each do |char|
114
+ compiled += char
115
+ valid_color = compiled if Color.attributes.include?(compiled.to_sym) || compiled =~ /^([fb]g?)?#([a-f0-9]{6})$/i
116
+ end
117
+
118
+ valid_color
119
+ end
120
+
121
+ ##
122
+ ## Normalize a color name, removing underscores,
123
+ ## replacing "bright" with "bold", and converting
124
+ ## bgbold to boldbg
125
+ ##
126
+ ## @return [String] Normalized color name
127
+ ##
128
+ def normalize_color
129
+ gsub(/_/, '').sub(/bright/i, 'bold').sub(/bgbold/, 'boldbg')
130
+ end
131
+
132
+ # Get the calculated ANSI color at the end of the
133
+ # string
134
+ #
135
+ # @return ANSI escape sequence to match color
136
+ #
137
+ def last_color_code
138
+ m = scan(ESCAPE_REGEX)
139
+
140
+ em = ['0']
141
+ fg = nil
142
+ bg = nil
143
+ rgbf = nil
144
+ rgbb = nil
145
+
146
+ m.each do |c|
147
+ case c
148
+ when '0'
149
+ em = ['0']
150
+ fg, bg, rgbf, rgbb = nil
151
+ when /^[34]8/
152
+ case c
153
+ when /^3/
154
+ fg = nil
155
+ rgbf = c
156
+ when /^4/
157
+ bg = nil
158
+ rgbb = c
159
+ end
160
+ else
161
+ c.split(/;/).each do |i|
162
+ x = i.to_i
163
+ if x <= 9
164
+ em << x
165
+ elsif x >= 30 && x <= 39
166
+ rgbf = nil
167
+ fg = x
168
+ elsif x >= 40 && x <= 49
169
+ rgbb = nil
170
+ bg = x
171
+ elsif x >= 90 && x <= 97
172
+ rgbf = nil
173
+ fg = x
174
+ elsif x >= 100 && x <= 107
175
+ rgbb = nil
176
+ bg = x
177
+ end
178
+ end
179
+ end
180
+ end
181
+
182
+ escape = "\e[#{em.join(';')}m"
183
+ escape += "\e[#{rgbb}m" if rgbb
184
+ escape += "\e[#{rgbf}m" if rgbf
185
+ escape + "\e[#{[fg, bg].delete_if(&:nil?).join(';')}m"
186
+ end
187
+ end
188
+
189
+ class << self
190
+ # Returns true if the coloring function of this module
191
+ # is switched on, false otherwise.
192
+ def coloring?
193
+ @coloring
194
+ end
195
+
196
+ attr_writer :coloring
197
+
198
+ ##
199
+ ## Enables colored output
200
+ ##
201
+ ## @example Turn color on or off based on TTY
202
+ ## Color.coloring = STDOUT.isatty
203
+ def coloring
204
+ @coloring ||= true
205
+ end
206
+
207
+ ##
208
+ ## Convert a template string to a colored string.
209
+ ## Colors are specified with single letters inside
210
+ ## curly braces. Uppercase changes background color.
211
+ ##
212
+ ## w: white, k: black, g: green, l: blue, y: yellow, c: cyan,
213
+ ## m: magenta, r: red, b: bold, u: underline, i: italic,
214
+ ## x: reset (remove background, color, emphasis)
215
+ ##
216
+ ## Also accepts {#RGB} and {#RRGGBB} strings. Put a b before
217
+ ## the hash to make it a background color
218
+ ##
219
+ ## @example Convert a templated string
220
+ ## Color.template('{Rwb}Warning:{x} {w}you look a little {g}ill{x}')
221
+ ##
222
+ ## @example Convert using RGB colors
223
+ ## Color.template('{#f0a}This is an RGB color')
224
+ ##
225
+ ## @param input [String, Array] The template
226
+ ## string. If this is an array, the
227
+ ## elements will be joined with a
228
+ ## space.
229
+ ##
230
+ ## @return [String] Colorized string
231
+ ##
232
+ def template(input)
233
+ input = input.join(' ') if input.is_a? Array
234
+ return input.gsub(/(?<!\\)\{(\w+)\}/i, '') unless Color.coloring?
235
+
236
+ input = input.gsub(/(?<!\\)\{((?:[fb]g?)?#[a-f0-9]{3,6})\}/i) do
237
+ hex = Regexp.last_match(1)
238
+ rgb(hex)
239
+ end
240
+
241
+ fmt = input.gsub(/%/, '%%')
242
+ fmt = fmt.gsub(/(?<!\\)\{(\w+)\}/i) do
243
+ Regexp.last_match(1).split('').map { |c| "%<#{c}>s" }.join('')
244
+ end
245
+
246
+ colors = { w: white, k: black, g: green, l: blue,
247
+ y: yellow, c: cyan, m: magenta, r: red,
248
+ W: bgwhite, K: bgblack, G: bggreen, L: bgblue,
249
+ Y: bgyellow, C: bgcyan, M: bgmagenta, R: bgred,
250
+ d: dark, b: bold, u: underline, i: italic, x: reset }
251
+
252
+ format(fmt, colors)
253
+ end
254
+ end
255
+
256
+ ATTRIBUTES.each do |c, v|
257
+ new_method = <<-EOSCRIPT
258
+ # Color string as #{c}
259
+ def #{c}(string = nil)
260
+ result = ''
261
+ result << "\e[#{v}m" if Color.coloring?
262
+ if block_given?
263
+ result << yield
264
+ elsif string.respond_to?(:to_str)
265
+ result << string.to_str
266
+ elsif respond_to?(:to_str)
267
+ result << to_str
268
+ else
269
+ return result #only switch on
270
+ end
271
+ result << "\e[0m" if Color.coloring?
272
+ result
273
+ end
274
+ EOSCRIPT
275
+
276
+ module_eval(new_method)
277
+
278
+ next unless c =~ /bold/
279
+
280
+ # Accept brightwhite in addition to boldwhite
281
+ new_method = <<-EOSCRIPT
282
+ # color string as #{c}
283
+ def #{c.to_s.sub(/bold/, 'bright')}(string = nil)
284
+ result = ''
285
+ result << "\e[#{v}m" if Color.coloring?
286
+ if block_given?
287
+ result << yield
288
+ elsif string.respond_to?(:to_str)
289
+ result << string.to_str
290
+ elsif respond_to?(:to_str)
291
+ result << to_str
292
+ else
293
+ return result #only switch on
294
+ end
295
+ result << "\e[0m" if Color.coloring?
296
+ result
297
+ end
298
+ EOSCRIPT
299
+
300
+ module_eval(new_method)
301
+ end
302
+
303
+ ##
304
+ ## Generate escape codes for hex colors
305
+ ##
306
+ ## @param hex [String] The hexadecimal color code
307
+ ##
308
+ ## @return [String] ANSI escape string
309
+ ##
310
+ def rgb(hex)
311
+ is_bg = hex.match(/^bg?#/) ? true : false
312
+ hex_string = hex.sub(/^([fb]g?)?#/, '')
313
+
314
+ if hex_string.length == 3
315
+ parts = hex_string.match(/(?<r>.)(?<g>.)(?<b>.)/)
316
+
317
+ t = []
318
+ %w[r g b].each do |e|
319
+ t << parts[e]
320
+ t << parts[e]
321
+ end
322
+ hex_string = t.join('')
323
+ end
324
+
325
+ parts = hex_string.match(/(?<r>..)(?<g>..)(?<b>..)/)
326
+ t = []
327
+ %w[r g b].each do |e|
328
+ t << parts[e].hex
329
+ end
330
+
331
+ "\e[#{is_bg ? '48' : '38'};2;#{t.join(';')}m"
332
+ end
333
+
334
+ # Regular expression that is used to scan for ANSI-sequences while
335
+ # uncoloring strings.
336
+ COLORED_REGEXP = /\e\[(?:(?:(?:[349]|10)[0-9]|[0-9])?;?)+m/
337
+
338
+ # Returns an uncolored version of the string, that is all
339
+ # ANSI-sequences are stripped from the string.
340
+ def uncolor(string = nil) # :yields:
341
+ if block_given?
342
+ yield.to_str.gsub(COLORED_REGEXP, '')
343
+ elsif string.respond_to?(:to_str)
344
+ string.to_str.gsub(COLORED_REGEXP, '')
345
+ elsif respond_to?(:to_str)
346
+ to_str.gsub(COLORED_REGEXP, '')
347
+ else
348
+ ''
349
+ end
350
+ end
351
+
352
+ # Returns an array of all Color attributes as symbols.
353
+ def attributes
354
+ ATTRIBUTE_NAMES
355
+ end
356
+ extend self
357
+ end
358
+
359
+ class ::String
360
+ def x
361
+ Color.template(self)
362
+ end
363
+ end
@@ -17,7 +17,7 @@ module Journal
17
17
  @type = question['type']
18
18
  @min = question['min']&.to_i || 1
19
19
  @max = question['max']&.to_i || 5
20
- @prompt = question['prompt']
20
+ @prompt = question['prompt'] || nil
21
21
  @secondary_prompt = question['secondary_prompt'] || nil
22
22
  end
23
23
 
@@ -27,6 +27,8 @@ module Journal
27
27
  ## @return [Number, String] the response based on @type
28
28
  ##
29
29
  def ask
30
+ return nil if @prompt.nil?
31
+
30
32
  case @type
31
33
  when /^(int|num)/i
32
34
  read_number
@@ -36,16 +38,20 @@ module Journal
36
38
  Weather.new(Journal.config['weather_api'], Journal.config['zip'])
37
39
  when /^multi/
38
40
  read_lines
41
+ when /^date/
42
+ read_date
39
43
  end
40
44
  end
41
45
 
46
+ private
47
+
42
48
  ##
43
49
  ## Read a numeric entry
44
50
  ##
45
51
  ## @return [Number] integer response
46
52
  ##
47
53
  def read_number
48
- puts "#{@prompt} (#{@min}-#{@max})"
54
+ Journal.notify("{by}#{@prompt} {c}({bw}#{@min}{c}-{bw}#{@max})")
49
55
  res = `gum input --placeholder "#{@prompt} (#{@min}-#{@max})"`.strip
50
56
  return nil if res.strip.empty?
51
57
 
@@ -55,6 +61,12 @@ module Journal
55
61
  res
56
62
  end
57
63
 
64
+ def read_date(prompt: nil)
65
+ Journal.notify("{by}#{prompt.nil? ? @prompt : prompt} (natural language)")
66
+ line = `gum input --placeholder "#{@prompt} (blank to end editing)"`
67
+ Chronic.parse(line)
68
+ end
69
+
58
70
  ##
59
71
  ## Reads a line.
60
72
  ##
@@ -66,7 +78,7 @@ module Journal
66
78
  ##
67
79
  def read_line(prompt: nil)
68
80
  output = []
69
- puts prompt.nil? ? @prompt : @secondary_prompt
81
+ Journal.notify("{by}#{prompt.nil? ? @prompt : @secondary_prompt}")
70
82
 
71
83
  line = `gum input --placeholder "#{@prompt} (blank to end editing)"`
72
84
  return output.join("\n") if line =~ /^ *$/
@@ -87,7 +99,7 @@ module Journal
87
99
  ##
88
100
  def read_lines(prompt: nil)
89
101
  output = []
90
- puts (prompt.nil? ? @prompt : @secondary_prompt) + ' (CTRL-d to save)'
102
+ Journal.notify("{by}#{prompt.nil? ? @prompt : @secondary_prompt} {c}({bw}CTRL-d{c} to save)'")
91
103
  line = `gum write --placeholder "#{prompt}" --width 80 --char-limit 0`
92
104
  return output.join("\n") if line.strip.empty?
93
105
 
@@ -16,6 +16,7 @@ module Journal
16
16
  @key = section['key']
17
17
  @title = section['title']
18
18
  @questions = section['questions'].map { |question| Question.new(question) }
19
+ @questions.delete_if { |q| q.prompt.nil? }
19
20
  @answers = {}
20
21
  ask_questions
21
22
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Journal
4
- VERSION = '1.0.16'
4
+ VERSION = '1.0.18'
5
5
  end
@@ -54,6 +54,10 @@ module Journal
54
54
  }
55
55
  end
56
56
 
57
+ def to_s
58
+ "#{@data[:temp].round} and #{@data[:current_condition]} (#{@data[:high].round}/#{@data[:low].round})"
59
+ end
60
+
57
61
  def to_markdown
58
62
  output = []
59
63
 
data/lib/journal-cli.rb CHANGED
@@ -8,6 +8,7 @@ require 'chronic'
8
8
  require 'fileutils'
9
9
 
10
10
  require_relative 'journal-cli/version'
11
+ require_relative 'journal-cli/color'
11
12
  require_relative 'journal-cli/data'
12
13
  require_relative 'journal-cli/weather'
13
14
  require_relative 'journal-cli/checkin'
@@ -18,6 +19,16 @@ require_relative 'journal-cli/question'
18
19
  # Main Journal module
19
20
  module Journal
20
21
  class << self
22
+ def notify(string, debug: false, exit_code: nil)
23
+ if debug
24
+ $stderr.puts "{dw}#{string}{x}".x
25
+ else
26
+ $stderr.puts "#{string}{x}".x
27
+ end
28
+
29
+ Process.exit exit_code unless exit_code.nil?
30
+ end
31
+
21
32
  def config
22
33
  unless @config
23
34
  config = File.expand_path('~/.config/journal/journals.yaml')
@@ -49,8 +60,7 @@ module Journal
49
60
  @config = YAML.load(IO.read(config))
50
61
 
51
62
  if @config['journals'].key?('demo')
52
- puts "Demo journal detected, please edit the configuration file at #{config}"
53
- Process.exit 1
63
+ Journal.notify("{br}Demo journal detected, please edit the configuration file at {bw}#{config}", exit_code: 1)
54
64
  end
55
65
  end
56
66
 
data/src/_README.md CHANGED
@@ -51,7 +51,7 @@ This file contains a YAML definition of your journal. Each journal gets a top-le
51
51
 
52
52
  You can include weather data automatically by setting a question type to 'weather'. In order for this to work, you'll need to define `zip` and `weather_api` keys. `zip` is just your zip code, and `weather_api` is a key from WeatherAPI.com. Sign up [here](https://www.weatherapi.com/) for a free plan, and then visit the [profile page](https://www.weatherapi.com/my/) to see your API key at the top.
53
53
 
54
- ### Journal configuration
54
+ ### Journal Configuration
55
55
 
56
56
  Edit the file at `~/.config/journal/journals.yaml` following this structure:
57
57
 
@@ -131,6 +131,28 @@ journals: # required key
131
131
 
132
132
  A journal must contain a `sections` key, and each section must contain a `questions` key with an array of questions. Each question must (at minimum) have a `prompt`, `key`, and `type`.
133
133
 
134
+ If a question has a key `secondary_question`, the prompt will be repeated with the secondary question until it's returned empty, answers will be joined together.
135
+
136
+ ### Naming Keys
137
+
138
+ If you want data stored in a nested object, you can set a question type to `dictionary` and set the prompt to `null` (or just leave the key out), but give it a key that will serve as the parent in the object. Then in the nested questions, give them a key in the dot format `[PARENT_KEY].[CHILD_KEY]`. Section keys automatically nest their children, but if you want to go deeper, you could have a question with the key `health` and type `dictionary`, then have questions with keys like `health.rating` and `health.notes`. If the section key was `status`, the resulting dictionary would look like this in the JSON:
139
+
140
+ ```json
141
+ {
142
+ "date": "2023-09-08 12:19:40 UTC",
143
+ "data": {
144
+ "status": {
145
+ "health": {
146
+ "rating": 4,
147
+ "notes": "Feeling much better today. Still a bit groggy."
148
+ }
149
+ }
150
+ }
151
+ }
152
+ ```
153
+
154
+ If a question has the same key as its parent section, it will be moved up the chain so that you don't get `{ 'journal': { 'journal': 'Journal notes' } }`. You'll just get `{ 'journal': 'Journal notes' }`. This offers a way to organize data with fewer levels of nesting in the output.
155
+
134
156
  ## Usage
135
157
 
136
158
  Once your configuration file is set up, you can just run `journal JOURNAL_KEY` to begin prompting for the answers to the configured questions.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: journal-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.16
4
+ version: 1.0.18
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brett Terpstra
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-09-07 00:00:00.000000000 Z
11
+ date: 2023-09-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -173,6 +173,7 @@ files:
173
173
  - journal-cli.gemspec
174
174
  - lib/journal-cli.rb
175
175
  - lib/journal-cli/checkin.rb
176
+ - lib/journal-cli/color.rb
176
177
  - lib/journal-cli/data.rb
177
178
  - lib/journal-cli/question.rb
178
179
  - lib/journal-cli/section.rb