journal-cli 1.0.17 → 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: a4136001bd545e3de4b52f3419a3519be7d553aa01f7b68a646f968bee9c08c9
4
- data.tar.gz: c87a3326570e1776685ef4d9ab383890e391d1cdf2dfb44c34890f2120d813aa
3
+ metadata.gz: 2ebc5eaefc846fb4343859ff0148a7798e93e2a4bcc04c5c73ce18151e59ea94
4
+ data.tar.gz: 87ca031548e115350383a28da8ec853d6d665dd2a9c88ac3ea32105350120785
5
5
  SHA512:
6
- metadata.gz: 9005246872181e4dd93006b2b3c65d10575b868d1448cb641ac6da486612a63ba73b33ee9f5853ca2cd34e40ae28022701232736160145f059e31c725b35ccaf
7
- data.tar.gz: 295e23386630c3df7f7cc68ce4bfed45861255d7441ccb7b9daace7667ee85f65580ba4026797c4b5173bb6c0df9abcc1983703109dd26301bdd725b67577736
6
+ metadata.gz: 7d15d09cce5d189a2d6168e02e9c3122334a88ea9e011b82fa97b0a732901913b6668407f3474211ef6b40b3b068363d9778ff4471a8ded4366089338f3fd7af
7
+ data.tar.gz: 3bce2d14e6d5f078b481b9d38184e9cd005c336aeecb36bf4a5daa8b7ef66510c60d46d3cd9db97e254266c8c91a07bbf209785b3aedd2d27f6a255e256f0341
data/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
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
+
1
14
  ### 1.0.17
2
15
 
3
16
  2023-09-08 07:21
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- journal-cli (1.0.17)
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,16 +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
- puts "Entered into Day One"
66
+ Journal.notify('{bg}Entered one entry into Day One')
67
67
  end
68
68
 
69
69
  def save_single_markdown
70
70
  dir = if @journal.key?('entries_folder')
71
71
  File.join(File.expand_path(@journal['entries_folder']), 'entries')
72
72
  elsif Journal.config.key?('entries_folder')
73
- File.join(File.expand_path(Journal.config['entries_folder']), @key, 'entries')
73
+ File.join(File.expand_path(Journal.config['entries_folder']), @key)
74
74
  else
75
- File.expand_path('~/.local/share/journal', @key, 'entries')
75
+ File.expand_path("~/.local/share/journal/#{@key}/entries")
76
76
  end
77
77
 
78
78
  FileUtils.mkdir_p(dir) unless File.directory?(dir)
@@ -85,16 +85,16 @@ module Journal
85
85
  f.puts
86
86
  f.puts to_markdown(yaml: false, title: false)
87
87
  end
88
- puts "Saved #{target}"
88
+ Journal.notify "{bg}Added new entry to {bw}#{target}"
89
89
  end
90
90
 
91
91
  def save_daily_markdown
92
92
  dir = if @journal.key?('entries_folder')
93
93
  File.join(File.expand_path(@journal['entries_folder']), 'entries')
94
94
  elsif Journal.config.key?('entries_folder')
95
- File.join(File.expand_path(Journal.config['entries_folder']), @key, 'entries')
95
+ File.join(File.expand_path(Journal.config['entries_folder']), @key)
96
96
  else
97
- File.join(File.expand_path('~/.local/share/journal/entries'), @key, 'entries')
97
+ File.join(File.expand_path("~/.local/share/journal/#{@key}/entries"))
98
98
  end
99
99
 
100
100
  FileUtils.mkdir_p(dir) unless File.directory?(dir)
@@ -106,7 +106,7 @@ module Journal
106
106
  else
107
107
  File.open(target, 'w') { |f| f.puts to_markdown(yaml: true, title: true, date: false, time: true) }
108
108
  end
109
- puts "Saved #{target}"
109
+ Journal.notify "{bg}Saved daily Markdown to {bw}#{target}"
110
110
  end
111
111
 
112
112
  def save_individual_markdown
@@ -124,7 +124,7 @@ module Journal
124
124
  filename = @date.strftime('%Y-%m-%d_%H:%M.md')
125
125
  target = File.join(dir, filename)
126
126
  File.open(target, 'w') { |f| f.puts to_markdown(yaml: true, title: true) }
127
- puts "Saved #{target}"
127
+ puts "Saved new entry to #{target}"
128
128
  end
129
129
 
130
130
  def print_answer(prompt, type, key, data)
@@ -144,18 +144,33 @@ module Journal
144
144
  end
145
145
  end
146
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
+
147
162
  def to_markdown(yaml: false, title: false, date: false, time: false)
148
163
  @output = []
149
164
 
150
165
  if yaml
151
166
  @date.localtime
152
- @output << <<~EOYAML
153
- ---
154
- title: #{@title}
155
- date: #{@date.strftime('%x %X')}
156
- ---
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
157
171
 
158
- EOYAML
172
+ @output << YAML.dump(yaml_data).strip
173
+ @output << '---'
159
174
  end
160
175
 
161
176
  if title
@@ -199,7 +214,7 @@ module Journal
199
214
  elsif Journal.config.key?('entries_folder')
200
215
  File.expand_path(Journal.config['entries_folder'])
201
216
  else
202
- File.expand_path("~/.local/share/journal")
217
+ File.expand_path('~/.local/share/journal')
203
218
  end
204
219
  FileUtils.mkdir_p(dir) unless File.directory?(dir)
205
220
  db = File.join(dir, "#{@key}.json")
@@ -218,13 +233,18 @@ module Journal
218
233
  v.each do |key, value|
219
234
  result = case value.class.to_s
220
235
  when /Weather/
221
- { '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
+ }
222
241
  else
223
242
  value
224
243
  end
225
244
  if jk == k
226
245
  output[jk][key] = result
227
246
  else
247
+ output[jk][k] ||= {}
228
248
  output[jk][k][key] = result
229
249
  end
230
250
  end
@@ -246,7 +266,7 @@ module Journal
246
266
  data.sort_by! { |e| e['date'] }
247
267
 
248
268
  File.open(db, 'w') { |f| f.puts JSON.pretty_generate(data) }
249
- puts "Saved #{db}"
269
+ Journal.notify "{bg}Saved {bw}#{db}"
250
270
  end
251
271
  end
252
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.17'
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.17
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-08 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