journal-cli 1.0.16 → 1.0.18

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 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