journal-cli 1.0.17 → 1.0.19

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