journal-cli 1.0.17 → 1.0.19

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: ac1922ce0228559564036c16e95a4d3042ff0dd36a5eb1137286ec7377b0919b
4
+ data.tar.gz: 57a0801095a4f5809607819d8eaded1840b4ba5db9ebc41a9090abde76995644
5
5
  SHA512:
6
- metadata.gz: 9005246872181e4dd93006b2b3c65d10575b868d1448cb641ac6da486612a63ba73b33ee9f5853ca2cd34e40ae28022701232736160145f059e31c725b35ccaf
7
- data.tar.gz: 295e23386630c3df7f7cc68ce4bfed45861255d7441ccb7b9daace7667ee85f65580ba4026797c4b5173bb6c0df9abcc1983703109dd26301bdd725b67577736
6
+ metadata.gz: c2811f0647206c869d8c7708f90fcdcc0833c3de261d6024bc5ec5a164d2117b25de7d570e4676103e88387ff407fa3a4d8744b6cef6f63c220ad942c1d1c7ad
7
+ data.tar.gz: d281c93a7d23e567ad0c36d41cd417cd030011f5687f1ffcfcef07f54d01501863cdbf19ad64b2215a25ab9265b4e073cb0c26f9683d4c97868def1577c62e1c
data/CHANGELOG.md CHANGED
@@ -1,3 +1,24 @@
1
+ ### 1.0.19
2
+
3
+ 2023-09-11 10:52
4
+
5
+ #### IMPROVED
6
+
7
+ - Better output of date types in Markdown formats
8
+
9
+ ### 1.0.18
10
+
11
+ 2023-09-09 12:29
12
+
13
+ #### IMPROVED
14
+
15
+ - 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
16
+
17
+ #### FIXED
18
+
19
+ - Daily markdown was being saved to /journal/entries/KEY/entries
20
+ - Missing color library
21
+
1
22
  ### 1.0.17
2
23
 
3
24
  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.19)
5
5
  chronic (~> 0.10, >= 0.10.2)
6
6
 
7
7
  GEM
@@ -75,6 +75,7 @@ GEM
75
75
  unicode-display_width (>= 1.1.1, < 3)
76
76
  thor (1.2.1)
77
77
  unicode-display_width (2.3.0)
78
+ yard (0.9.34)
78
79
 
79
80
  PLATFORMS
80
81
  arm64-darwin-21
@@ -94,6 +95,7 @@ DEPENDENCIES
94
95
  simplecov (~> 0.21)
95
96
  simplecov-console (~> 0.9)
96
97
  standard (~> 1.3)
98
+ yard (~> 0.9, >= 0.9.26)
97
99
 
98
100
  BUNDLED WITH
99
101
  2.4.1
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,38 @@ 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
+ ### Question Types
134
+
135
+ A question `type` can be one of:
136
+
137
+ - `text` or `string` will request a single-line string, submitted on return
138
+ - `multiline` for multiline strings (opens a readline editor, use ctrl-d to save)
139
+ - `weather` will just insert current weather data with no prompt
140
+ - `integer` or `number` will request numeric input
141
+ - `date` will request a natural language date which will be parsed into a date object
142
+
143
+ ### Naming Keys
144
+
145
+ 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:
146
+
147
+ ```json
148
+ {
149
+ "date": "2023-09-08 12:19:40 UTC",
150
+ "data": {
151
+ "status": {
152
+ "health": {
153
+ "rating": 4,
154
+ "notes": "Feeling much better today. Still a bit groggy."
155
+ }
156
+ }
157
+ }
158
+ }
159
+ ```
160
+
161
+ 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.
162
+
131
163
  ## Usage
132
164
 
133
165
  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/Rakefile CHANGED
@@ -11,7 +11,7 @@ Rake::RDocTask.new do |rd|
11
11
  end
12
12
 
13
13
  YARD::Rake::YardocTask.new do |t|
14
- t.files = ['lib/na/*.rb']
14
+ t.files = ['lib/journal-cli/*.rb']
15
15
  t.options = ['--markup-provider=redcarpet', '--markup=markdown', '--no-private', '-p', 'yard_templates']
16
16
  # t.stats_options = ['--list-undoc']
17
17
  end
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
data/journal-cli.gemspec CHANGED
@@ -31,6 +31,7 @@ Gem::Specification.new do |spec|
31
31
  spec.add_development_dependency "gem-release", "~> 2.2"
32
32
  spec.add_development_dependency "parse_gemspec-cli", "~> 1.0"
33
33
  spec.add_development_dependency "rake", "~> 13.0"
34
+ spec.add_development_dependency('yard', '~> 0.9', '>= 0.9.26')
34
35
  spec.add_development_dependency "rspec", "~> 3.0"
35
36
  spec.add_development_dependency "simplecov", "~> 0.21"
36
37
  spec.add_development_dependency "simplecov-console", "~> 0.9"
@@ -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)
@@ -135,6 +135,8 @@ module Journal
135
135
  hr
136
136
  when /^(int|num)/
137
137
  @output << "#{prompt}: #{data[key]} " unless data[key].nil?
138
+ when /^date/
139
+ @output << "#{prompt}: #{data[key].strftime('%Y-%m-%d %H:%M')}" unless data[key].nil?
138
140
  else
139
141
  unless data[key].strip.empty?
140
142
  header prompt
@@ -144,18 +146,37 @@ module Journal
144
146
  end
145
147
  end
146
148
 
149
+ def weather_to_yaml(answers)
150
+ data = {}
151
+ answers.each do |k, v|
152
+ case v.class.to_s
153
+ when /String/
154
+ next
155
+ when /Hash/
156
+ data[k] = weather_to_yaml(v)
157
+ when /Date/
158
+ data[k] = v.strftime('%Y-%m-%d %H:%M')
159
+ when /Weather/
160
+ data[k] = v.to_s
161
+ else
162
+ data[k] = v
163
+ end
164
+ end
165
+ data
166
+ end
167
+
147
168
  def to_markdown(yaml: false, title: false, date: false, time: false)
148
169
  @output = []
149
170
 
150
171
  if yaml
151
172
  @date.localtime
152
- @output << <<~EOYAML
153
- ---
154
- title: #{@title}
155
- date: #{@date.strftime('%x %X')}
156
- ---
173
+ yaml_data = { 'title' => @title, 'date' => @date.strftime('%x %X')}
174
+ @data.each do |key, data|
175
+ yaml_data = yaml_data.merge(weather_to_yaml(data.answers))
176
+ end
157
177
 
158
- EOYAML
178
+ @output << YAML.dump(yaml_data).strip
179
+ @output << '---'
159
180
  end
160
181
 
161
182
  if title
@@ -199,7 +220,7 @@ module Journal
199
220
  elsif Journal.config.key?('entries_folder')
200
221
  File.expand_path(Journal.config['entries_folder'])
201
222
  else
202
- File.expand_path("~/.local/share/journal")
223
+ File.expand_path('~/.local/share/journal')
203
224
  end
204
225
  FileUtils.mkdir_p(dir) unless File.directory?(dir)
205
226
  db = File.join(dir, "#{@key}.json")
@@ -218,13 +239,18 @@ module Journal
218
239
  v.each do |key, value|
219
240
  result = case value.class.to_s
220
241
  when /Weather/
221
- { 'high' => value.data[:high], 'low' => value.data[:low], 'condition' => value.data[:condition] }
242
+ {
243
+ 'high' => value.data[:high],
244
+ 'low' => value.data[:low],
245
+ 'condition' => value.data[:condition]
246
+ }
222
247
  else
223
248
  value
224
249
  end
225
250
  if jk == k
226
251
  output[jk][key] = result
227
252
  else
253
+ output[jk][k] ||= {}
228
254
  output[jk][k][key] = result
229
255
  end
230
256
  end
@@ -246,7 +272,7 @@ module Journal
246
272
  data.sort_by! { |e| e['date'] }
247
273
 
248
274
  File.open(db, 'w') { |f| f.puts JSON.pretty_generate(data) }
249
- puts "Saved #{db}"
275
+ Journal.notify "{bg}Saved {bw}#{db}"
250
276
  end
251
277
  end
252
278
  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.19'
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,38 @@ 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
+ ### Question Types
137
+
138
+ A question `type` can be one of:
139
+
140
+ - `text` or `string` will request a single-line string, submitted on return
141
+ - `multiline` for multiline strings (opens a readline editor, use ctrl-d to save)
142
+ - `weather` will just insert current weather data with no prompt
143
+ - `integer` or `number` will request numeric input
144
+ - `date` will request a natural language date which will be parsed into a date object
145
+
146
+ ### Naming Keys
147
+
148
+ 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:
149
+
150
+ ```json
151
+ {
152
+ "date": "2023-09-08 12:19:40 UTC",
153
+ "data": {
154
+ "status": {
155
+ "health": {
156
+ "rating": 4,
157
+ "notes": "Feeling much better today. Still a bit groggy."
158
+ }
159
+ }
160
+ }
161
+ }
162
+ ```
163
+
164
+ 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.
165
+
134
166
  ## Usage
135
167
 
136
168
  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.19
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-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -66,6 +66,26 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '13.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: yard
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.9'
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: 0.9.26
79
+ type: :development
80
+ prerelease: false
81
+ version_requirements: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - "~>"
84
+ - !ruby/object:Gem::Version
85
+ version: '0.9'
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: 0.9.26
69
89
  - !ruby/object:Gem::Dependency
70
90
  name: rspec
71
91
  requirement: !ruby/object:Gem::Requirement
@@ -173,6 +193,7 @@ files:
173
193
  - journal-cli.gemspec
174
194
  - lib/journal-cli.rb
175
195
  - lib/journal-cli/checkin.rb
196
+ - lib/journal-cli/color.rb
176
197
  - lib/journal-cli/data.rb
177
198
  - lib/journal-cli/question.rb
178
199
  - lib/journal-cli/section.rb