journal-cli 1.0.5 → 1.0.10

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: 5c6fdd6421db40eaa592ff8fee665dc350314988633ac566abf2e758439d3906
4
- data.tar.gz: e3ab8c9d573c34ef3544261d69bbe07e225005e7451cf5c99332f77a04901d05
3
+ metadata.gz: daaf3cad805bf34bae2691d8d5738316aef4318bf9f6a9562fcbc38c27f0e3be
4
+ data.tar.gz: 861bca0aae3312d48dd070dc5c88cffadb709242e9374f25856ac6b5b5189cfd
5
5
  SHA512:
6
- metadata.gz: 797789942a88387b3d3c6151f8778c6762e32a3e1feac7045a7be86aff3388241b2ce71fd260843b5ddc8323969c2b770d306286313935f12493a71b55e7feb7
7
- data.tar.gz: a24535361f2225a6f21ea4abbb23b8e1b0c38aa733e6447de34883d529381f5b69c223373ef4057e8a0f64b5181fb0dceb0c973f7bab5efd25dd2ce1c6ecd0e1
6
+ metadata.gz: 0c982533e8d04247ccfffe560549f01ea57725bfcc0391294ece7bb32adf56fdee95589700fcdb6ca855dddaa446dfe06c6d15a394befd231683680194678670
7
+ data.tar.gz: 787c597423d1ef820068b6498fd5004edf17e19c2e6f723e69b6feca919c8cc8bbe9127304749ce3c88b320d69e330823f51e0de14f7c1995e0c2d1a98fdc690
data/CHANGELOG.md CHANGED
@@ -1,43 +1,28 @@
1
- ### 1.0.5
1
+ ### 1.0.10
2
2
 
3
- 2023-09-06 09:24
3
+ 2023-09-06 16:03
4
4
 
5
- ### 1.0.4
5
+ #### IMPROVED
6
6
 
7
- 2023-09-06 09:23
7
+ - Refactoring code
8
8
 
9
- #### NEW
9
+ ### 1.0.9
10
10
 
11
- - Initial journal command
12
- - Multiple journals, multiple sections
13
-
14
- ### 1.0.3
15
-
16
- 2023-09-06 09:10
11
+ 2023-09-06 11:58
17
12
 
18
13
  #### NEW
19
14
 
20
- - Initial journal command
21
- - Multiple journals, multiple sections
22
-
23
- ### 1.0.2
15
+ - If the second argument is a natural language date, use the parsed result instead of the current time for the entry
24
16
 
25
- 2023-09-06 09:09
26
-
27
- #### NEW
17
+ ### 1.0.5
28
18
 
29
- - Initial journal command
30
- - Multiple journals, multiple sections
19
+ 2023-09-06 09:24
31
20
 
32
- ### 1.0.1
21
+ ### 1.0.0
33
22
 
34
- 2023-09-06 09:07
23
+ 2023-09-06 09:23
35
24
 
36
25
  #### NEW
37
26
 
38
27
  - Initial journal command
39
28
  - Multiple journals, multiple sections
40
-
41
- ### 1.0.0
42
-
43
- 2023-09-05 16:46
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- journal-cli (1.0.1)
4
+ journal-cli (1.0.10)
5
5
  chronic (~> 0.10, >= 0.10.2)
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -49,18 +49,19 @@ You can include weather data automatically by setting a question type to 'weathe
49
49
  Edit the file at `~/.config/journal/journals.yaml` following this structure:
50
50
 
51
51
  ```yaml
52
- daily: # journal key, will be used on the command line as `journal daily`
53
- dayone: true # Enable or disable Day One integration
54
- journal: Journal # Day One journal to add to (if using Day One integration)
55
- markdown: daily # Type of Markdown file to create, false to skip (can be daily, individual, or digest)
56
- title: Daily Journal # Title for every entry, date will be appended where needed
57
- sections: # Required key
58
- - title: null # The title for the section. If null, no section header will be created
59
- key: journal # The key for the data collected, must be one word, alphanumeric characters and _ only
60
- questions: # Required key
61
- - prompt: How are you feeling? # The question to ask
62
- key: journal # alphanumeric characters and _ only, will be nested in section key
63
- type: multiline # The type of entry expected (numeric, string, or multiline)
52
+ journals:
53
+ daily: # journal key, will be used on the command line as `journal daily`
54
+ dayone: true # Enable or disable Day One integration
55
+ journal: Journal # Day One journal to add to (if using Day One integration)
56
+ markdown: daily # Type of Markdown file to create, false to skip (can be daily, individual, or digest)
57
+ title: Daily Journal # Title for every entry, date will be appended where needed
58
+ sections: # Required key
59
+ - title: null # The title for the section. If null, no section header will be created
60
+ key: journal # The key for the data collected, must be one word, alphanumeric characters and _ only
61
+ questions: # Required key
62
+ - prompt: How are you feeling? # The question to ask
63
+ key: journal # alphanumeric characters and _ only, will be nested in section key
64
+ type: multiline # The type of entry expected (numeric, string, or multiline)
64
65
  ```
65
66
 
66
67
  Keys must be alphanumeric characters and `_` (underscore) only. Titles and questions can be anything, but if they contain a colon (:), you'll need to quote the string.
@@ -121,7 +122,17 @@ A journal must contain a `sections` key, and each section must contain a `questi
121
122
 
122
123
  Once your configuration file is set up, you can just run `journal JOURNAL_KEY` to begin prompting for the answers to the configured questions.
123
124
 
125
+ If a second argument contains a natural language date, the journal entry will be set to that date instead of the current time. For example, `journal mood "yesterday 5pm"` will create a new entry (in the journal configured for `mood`) for yesterday at 5pm.
126
+
124
127
  Answers will always be written to `~/.local/share/journal/[KEY].json` (where [KEY] is the journal key, one data file for each journal). If you've specified `daily` or `individual` Markdown formats, entries will be written to Markdown files in `~/.local/share/journal/entries/[KEY]`, either in a `%Y-%m-%d.md` file (daily), or in timestamped individual files. If `digest` is specified for the `markdown` key, a single file will be created at `~/.local/share/journal/[KEY].md`.
125
128
 
126
129
  At present there's no tool for querying the dataset created. You just need to parse the JSON and use your language of choice to extract the data. Numeric entries are stored as numbers, and every entry is timestamped, so you should be able to do some advanced analysis once you have enough data.
127
130
 
131
+ ### Answering prompts
132
+
133
+ Questions with numeric answers will have a valid range assigned. Enter just a number within the range and hit return.
134
+
135
+ Questions with type 'string' or 'text' will save when you hit return. Pressing return without typing anything will leave that answer blank, and it will be ignored when exporting to Markdown or Day One (an empty value will exist in the JSON database).
136
+
137
+ When using the mutiline type, you'll get an edit field that responds to most control-key navigation and allows insertion and movement. To save a multiline field, press Escape or type CTRL-d.
138
+
data/bin/journal CHANGED
@@ -3,30 +3,52 @@
3
3
  $LOAD_PATH.unshift File.join(__dir__, '..', 'lib')
4
4
  require 'journal-cli'
5
5
 
6
- raise ArgumentError, 'no journal specified' if ARGV.count.zero?
7
-
8
- case ARGV[0]
9
- when /(-v|--version)/
10
- puts "journal v#{Journal::VERSION}"
11
- Process.exit 0
12
- when /(help|-h|--help)/
13
- puts "journal v#{Journal::VERSION}"
14
- puts
15
- puts 'Usage: journal [type] [date]'
16
- puts
17
- puts 'Available journal types:'
18
- config = Journal::Checkin.new
19
- puts(config.config['journals'].keys.map { |k| "- #{k}" })
20
- Process.exit 0
21
- end
6
+ module Journal
7
+ class << self
8
+ def usage
9
+ puts "journal v#{Journal::VERSION}"
10
+ puts
11
+ puts 'Usage: journal [type] [date]'
12
+ puts
13
+ puts 'Available journal types:'
14
+ config = Journal.config
15
+ puts(config['journals'].keys.map { |k| "- #{k}" })
16
+ end
17
+
18
+ def run(args)
19
+ if args.count.zero?
20
+ puts "No journal specified"
21
+ usage
22
+ Process.exit 1
23
+ end
24
+
25
+ case args[0]
26
+ when /(-v|--version)/
27
+ puts "journal v#{Journal::VERSION}"
28
+ Process.exit 0
29
+ when /(help|-h|--help)/
30
+ usage
31
+ Process.exit 0
32
+ end
22
33
 
23
- journal = ARGV.shift
34
+ journal = args.shift
35
+
36
+ date = if args.length.positive?
37
+ Chronic.parse(args.join(' '), future: false)
38
+ else
39
+ Time.now
40
+ end
41
+
42
+ if Journal.config['journals'].key?(journal)
43
+ checkin = Journal::Checkin.new(journal, date)
44
+ checkin.go
45
+ else
46
+ puts "Journal #{journal} not found"
47
+ usage
48
+ Process.exit 1
49
+ end
50
+ end
51
+ end
52
+ end
24
53
 
25
- date = if ARGV.length.positive?
26
- Chronic.parse(ARGV.join(' '), future: false)
27
- else
28
- Time.now
29
- end
30
- checkin = Journal::Checkin.new
31
- checkin.start(journal, date)
32
- checkin.go
54
+ Journal.run(ARGV)
@@ -1,23 +1,18 @@
1
1
  module Journal
2
2
  # Main class
3
3
  class Checkin
4
- attr_reader :key, :date, :data, :config, :journal, :title, :output
4
+ attr_reader :key, :date, :data, :config, :journal, :sections, :title, :output
5
5
 
6
- def initialize
7
- config = File.expand_path('~/.config/journal/journals.yaml')
8
- raise StandardError, 'No journals configured' unless File.exist?(config)
9
-
10
- @config = YAML.load(IO.read(config))
11
- end
12
-
13
- def start(journal, date)
6
+ def initialize(journal, date)
14
7
  @key = journal
15
8
  @output = []
16
9
  @date = date
10
+ @date.localtime
17
11
 
18
- raise StandardError, "No journal with key #{@key} found" unless @config['journals'].key? @key
12
+ raise StandardError, "No journal with key #{@key} found" unless Journal.config['journals'].key? @key
19
13
 
20
- @journal = @config['journals'][@key]
14
+ @journal = Journal.config['journals'][@key]
15
+ @sections = Sections.new(@journal['sections'])
21
16
 
22
17
  @data = {}
23
18
  meridian = @date.hour < 13 ? 'AM' : 'PM'
@@ -44,93 +39,69 @@ module Journal
44
39
  @output << "\n---\n"
45
40
  end
46
41
 
47
- def ask_question(q)
48
- res = case q['type']
49
- when /^(int|num)/i
50
- min = q['min'] || 1
51
- max = q['max'] || 5
52
- get_number(q['prompt'], min: min, max: max)
53
- when /^(text|string|line)/i
54
- puts q['prompt']
55
- add_prompt = q['secondary_prompt'] || nil
56
- get_line(q['prompt'], add_prompt: add_prompt)
57
- when /^(weather|forecast)/i
58
- Weather.new(@config['weather_api'], @config['zip'])
59
- when /^multi/
60
- puts q['prompt']
61
- add_prompt = q['secondary_prompt'] || nil
62
- get_lines(q['prompt'], add_prompt: add_prompt)
63
- end
64
-
65
- res
66
- end
67
-
68
42
  def go
69
- results = Data.new(@journal['questions'])
70
- @journal['sections'].each do |s|
71
- results[s['key']] = {
72
- title: s['title'],
73
- answers: {}
74
- }
43
+ @sections.each { |key, section| @data[key] = section }
75
44
 
76
- s['questions'].each do |q|
77
- if q['key'] =~ /\./
78
- res = results[s['key']][:answers]
79
- keys = q['key'].split(/\./)
80
- keys.each_with_index do |key, i|
81
- next if i == keys.count - 1
45
+ save_data
46
+ save_day_one_entry if @journal['dayone']
82
47
 
83
- res[key] = {} unless res.key?(key)
84
- res = res[key]
85
- end
48
+ return unless @journal['markdown']
86
49
 
87
- res[keys.last] = ask_question(q)
88
- else
89
- results[s['key']][:answers][q['key']] = ask_question(q)
90
- end
91
- end
50
+ case @journal['markdown']
51
+ when /^da(y|ily)/
52
+ save_daily_markdown
53
+ when /^(ind|sep)/
54
+ save_individual_markdown
55
+ else
56
+ save_single_markdown
92
57
  end
58
+ end
93
59
 
94
- @data = results
60
+ def save_day_one_entry
61
+ cmd = ['dayone2']
62
+ cmd << %(-j "#{@journal['journal']}") if @journal.key?('journal')
63
+ cmd << %(-t #{@journal['tags'].join(' ')}) if @journal.key?('tags')
64
+ cmd << %(-date "#{@date.strftime('%Y-%m-%d %I:%M %p')}")
65
+ `echo #{Shellwords.escape(to_markdown(yaml: false, title: true))} | #{cmd.join(' ')} -- new`
66
+ end
95
67
 
96
- if @journal['dayone']
97
- cmd = ['dayone2']
98
- cmd << %(-j "#{@journal['journal']}") if @journal.key?('journal')
99
- cmd << %(-t #{@journal['tags'].join(' ')}) if @journal.key?('tags')
100
- cmd << %(-date "#{@date.strftime('%Y-%m-%d %I:%M %p')}")
101
- `echo #{Shellwords.escape(to_markdown)} | #{cmd.join(' ')} -- new`
68
+ def save_single_markdown
69
+ dir = File.expand_path('~/.local/share/journal/entries/')
70
+ FileUtils.mkdir_p(dir) unless File.directory?(dir)
71
+ filename = "#{@key}.md"
72
+ @date.localtime
73
+ target = File.join(dir, filename)
74
+ File.open(target, 'a') do |f|
75
+ f.puts
76
+ f.puts "## #{@title} #{@date.strftime('%x %X')}"
77
+ f.puts
78
+ f.puts to_markdown(yaml: false, title: false)
102
79
  end
80
+ puts "Saved #{target}"
81
+ end
103
82
 
104
- if @journal['markdown']
105
- if @journal['markdown'] =~ /^da(y|ily)/
106
- dir = File.expand_path("~/.local/share/journal/entries/#{@key}")
107
- FileUtils.mkdir_p(dir) unless File.directory?(dir)
108
- filename = "#{@date.strftime('%Y-%m-%d')}.md"
109
- target = File.join(dir, filename)
110
- if File.exist? target
111
- File.open(target, 'a') { |f| f.puts to_markdown(yaml: false, title: true, date: false, time: true) }
112
- else
113
- File.open(target, 'w') { |f| f.puts to_markdown(yaml: true, title: true, date: false, time: true) }
114
- end
115
- elsif @journal['markdown'] =~ /^(ind|separate)/
116
- dir = File.expand_path("~/.local/share/journal/entries/#{@key}")
117
- FileUtils.mkdir_p(dir) unless File.directory?(dir)
118
- filename = @date.strftime('%Y-%m-%d_%H:%M.md')
119
- File.open(File.join(dir, filename), 'w') { |f| f.puts to_markdown(yaml: true, title: true) }
120
- else
121
- dir = File.expand_path('~/.local/share/journal/entries/')
122
- FileUtils.mkdir_p(dir) unless File.directory?(dir)
123
- filename = "#{@key}.md"
124
- File.open(File.join(dir, filename), 'a') do |f|
125
- f.puts
126
- f.puts "## #{@title} #{@date.strftime('%x %X')}"
127
- f.puts
128
- f.puts to_markdown(yaml: false, title: false)
129
- end
130
- end
83
+ def save_daily_markdown
84
+ dir = File.expand_path("~/.local/share/journal/entries/#{@key}")
85
+ FileUtils.mkdir_p(dir) unless File.directory?(dir)
86
+ @date.localtime
87
+ filename = "#{@date.strftime('%Y-%m-%d')}.md"
88
+ target = File.join(dir, filename)
89
+ if File.exist? target
90
+ File.open(target, 'a') { |f| f.puts to_markdown(yaml: false, title: true, date: false, time: true) }
91
+ else
92
+ File.open(target, 'w') { |f| f.puts to_markdown(yaml: true, title: true, date: false, time: true) }
131
93
  end
94
+ puts "Saved #{target}"
95
+ end
132
96
 
133
- save_data
97
+ def save_individual_markdown
98
+ dir = File.expand_path("~/.local/share/journal/entries/#{@key}")
99
+ FileUtils.mkdir_p(dir) unless File.directory?(dir)
100
+ @date.localtime
101
+ filename = @date.strftime('%Y-%m-%d_%H:%M.md')
102
+ target = File.join(dir, filename)
103
+ File.open(target, 'w') { |f| f.puts to_markdown(yaml: true, title: true) }
104
+ puts "Saved #{target}"
134
105
  end
135
106
 
136
107
  def print_answer(prompt, type, key, data)
@@ -154,6 +125,7 @@ module Journal
154
125
  @output = []
155
126
 
156
127
  if yaml
128
+ @date.localtime
157
129
  @output << <<~EOYAML
158
130
  ---
159
131
  title: #{@title}
@@ -174,21 +146,22 @@ module Journal
174
146
  end
175
147
  end
176
148
 
177
- @journal['sections'].each do |s|
178
- section s['title']
149
+ @sections.each do |key, section|
150
+ answers = section.answers
151
+ section section.title
179
152
 
180
- s['questions'].each do |q|
181
- if q['key'] =~ /\./
182
- res = @data[s['key']][:answers].dup
183
- keys = q['key'].split(/\./)
153
+ section.questions.each do |question|
154
+ if question.key =~ /\./
155
+ res = section.answers.dup
156
+ keys = question.key.split(/\./)
184
157
  keys.each_with_index do |key, i|
185
158
  next if i == keys.count - 1
186
159
 
187
160
  res = res[key]
188
161
  end
189
- print_answer(q['prompt'], q['type'], keys.last, res)
162
+ print_answer(question.prompt, question.type, keys.last, res)
190
163
  else
191
- print_answer(q['prompt'], q['type'], q['key'], @data[s['key']][:answers])
164
+ print_answer(question.prompt, question.type, question.key, section.answers)
192
165
  end
193
166
  end
194
167
  end
@@ -197,6 +170,7 @@ module Journal
197
170
  end
198
171
 
199
172
  def save_data
173
+ @date.localtime
200
174
  db = File.expand_path("~/.local/share/journal/#{@key}.json")
201
175
  data = if File.exist?(db)
202
176
  JSON.parse(IO.read(db))
@@ -206,57 +180,26 @@ module Journal
206
180
  date = @date.utc
207
181
  output = {}
208
182
 
209
- @data.each do |k, v|
210
- v[:answers].each do |q, a|
211
- if a.is_a? Hash
212
- output[q] = {}
213
- a.each do |key, value|
214
- output[q][key] = case value.class.to_s
215
- when /Weather/
216
- { 'high' => value.data[:high], 'low' => value.data[:low], 'condition' => value.data[:condition] }
217
- else
218
- value
219
- end
183
+ @data.each do |jk, journal|
184
+ output[jk] = {}
185
+ journal.answers.each do |k, v|
186
+ if v.is_a? Hash
187
+ output[jk][k] = {}
188
+ v.each do |key, value|
189
+ output[jk][k][key] = case value.class.to_s
190
+ when /Weather/
191
+ { 'high' => value.data[:high], 'low' => value.data[:low], 'condition' => value.data[:condition] }
192
+ else
193
+ value
194
+ end
220
195
  end
221
196
  else
222
- output[q] = a
197
+ output[jk][k] = v
223
198
  end
224
199
  end
225
200
  end
226
201
  data << { 'date' => date, 'data' => output }
227
202
  File.open(db, 'w') { |f| f.puts JSON.pretty_generate(data) }
228
203
  end
229
-
230
- def get_number(prompt, min: 1, max: 5)
231
- puts "#{prompt} (#{min}-#{max})"
232
- res = `gum input --placeholder "#{prompt} (#{min}-#{max})"`.strip
233
- return nil if res.strip.empty?
234
-
235
- res = res.to_i
236
-
237
- res = get_number(prompt, min: min, max: max) if res < min || res > max
238
- res
239
- end
240
-
241
- def get_line(prompt, add_prompt: nil)
242
- output = []
243
- puts prompt
244
- line = `gum input --placeholder "#{prompt} (blank to end editing)"`
245
- return output.join("\n") if line =~ /^ *$/
246
-
247
- output << line
248
- output << get_line(add_prompt, add_prompt: add_prompt) if add_prompt
249
- output.join("\n")
250
- end
251
-
252
- def get_lines(prompt, add_prompt: nil)
253
- output = []
254
- line = `gum write --placeholder "#{prompt}" --width 80 --char-limit 0`
255
- return output.join("\n") if line.strip.empty?
256
-
257
- output << line
258
- output << get_lines(add_prompt, add_prompt: add_prompt) if add_prompt
259
- output.join("\n")
260
- end
261
204
  end
262
205
  end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Journal
4
+ # Individual question
5
+ class Question
6
+ attr_reader :key, :type, :min, :max, :prompt, :secondary_prompt
7
+
8
+ ##
9
+ ## Initializes the given question.
10
+ ##
11
+ ## @param question [Hash] The question with key, prompt, and type, optionally min and max
12
+ ##
13
+ ## @return [Question] the question object
14
+ ##
15
+ def initialize(question)
16
+ @key = question['key']
17
+ @type = question['type']
18
+ @min = question['min']&.to_i || 1
19
+ @max = question['max']&.to_i || 5
20
+ @prompt = question['prompt']
21
+ @secondary_prompt = question['secondary_prompt'] || nil
22
+ end
23
+
24
+ ##
25
+ ## Ask the question, prompting for input based on type
26
+ ##
27
+ ## @return [Number, String] the response based on @type
28
+ ##
29
+ def ask
30
+ case @type
31
+ when /^(int|num)/i
32
+ read_number
33
+ when /^(text|string|line)/i
34
+ read_line
35
+ when /^(weather|forecast)/i
36
+ Weather.new(Journal.config['weather_api'], Journal.config['zip'])
37
+ when /^multi/
38
+ read_lines
39
+ end
40
+ end
41
+
42
+ ##
43
+ ## Read a numeric entry
44
+ ##
45
+ ## @return [Number] integer response
46
+ ##
47
+ def read_number
48
+ puts "#{@prompt} (#{@min}-#{@max})"
49
+ res = `gum input --placeholder "#{@prompt} (#{@min}-#{@max})"`.strip
50
+ return nil if res.strip.empty?
51
+
52
+ res = res.to_i
53
+
54
+ res = read_number if res < @min || res > @max
55
+ res
56
+ end
57
+
58
+ ##
59
+ ## Reads a line.
60
+ ##
61
+ ## @param prompt [String] If not nil, will trigger
62
+ ## asking for a secondary response
63
+ ## until a blank entry is given
64
+ ##
65
+ ## @return [String] the single-line response
66
+ ##
67
+ def read_line(prompt: nil)
68
+ output = []
69
+ puts prompt.nil? ? @prompt : @secondary_prompt
70
+
71
+ line = `gum input --placeholder "#{@prompt} (blank to end editing)"`
72
+ return output.join("\n") if line =~ /^ *$/
73
+
74
+ output << line
75
+ output << read_line(prompt: @secondary_prompt) unless @secondary_prompt.nil?
76
+ output.join("\n").strip
77
+ end
78
+
79
+ ##
80
+ ## Reads multiple lines.
81
+ ##
82
+ ## @param prompt [String] if not nil, will trigger
83
+ ## asking for a secondary response
84
+ ## until a blank entry is given
85
+ ##
86
+ ## @return [String] the multi-line response
87
+ ##
88
+ def read_lines(prompt: nil)
89
+ output = []
90
+ puts prompt.nil? ? @prompt : @secondary_prompt
91
+ line = `gum write --placeholder "#{prompt}" --width 80 --char-limit 0`
92
+ return output.join("\n") if line.strip.empty?
93
+
94
+ output << line
95
+ output << read_lines(prompt: @secondary_prompt) unless @secondary_prompt.nil?
96
+ output.join("\n").strip
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Journal
4
+ class Section
5
+ attr_accessor :key, :title, :questions, :answers
6
+
7
+ ##
8
+ ## Initializes the given section.
9
+ ##
10
+ ## @param section [Hash] The section as defined in
11
+ ## configuration
12
+ ##
13
+ ## @return [Section] the configured section
14
+ ##
15
+ def initialize(section)
16
+ @key = section['key']
17
+ @title = section['title']
18
+ @questions = section['questions'].map { |question| Question.new(question) }
19
+ @answers = {}
20
+ ask_questions
21
+ end
22
+
23
+ ##
24
+ ## Ask the questions detailed in the 'questions' section of the configuration
25
+ ##
26
+ ## @return [Hash] the question responses
27
+ ##
28
+ def ask_questions
29
+ @questions.each do |question|
30
+ if question.key =~ /\./
31
+ res = @answers
32
+ keys = question.key.split(/\./)
33
+ keys.each_with_index do |key, i|
34
+ next if i == keys.count - 1
35
+
36
+ res[key] = {} unless res.key?(key)
37
+ res = res[key]
38
+ end
39
+
40
+ res[keys.last] = question.ask
41
+ else
42
+ @answers[question.key] = question.ask
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Journal
4
+ class Sections < Hash
5
+ def initialize(sections)
6
+ sections.each do |sect|
7
+ section = Section.new(sect)
8
+ self[section.key] = section
9
+ end
10
+ super
11
+ end
12
+ end
13
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Journal
4
- VERSION = '1.0.5'
4
+ VERSION = '1.0.10'
5
5
  end
data/lib/journal-cli.rb CHANGED
@@ -7,7 +7,26 @@ require 'yaml'
7
7
  require 'chronic'
8
8
  require 'fileutils'
9
9
 
10
- require_relative "journal-cli/version"
11
- require_relative "journal-cli/data"
12
- require_relative "journal-cli/weather"
13
- require_relative "journal-cli/checkin"
10
+ require_relative 'journal-cli/version'
11
+ require_relative 'journal-cli/data'
12
+ require_relative 'journal-cli/weather'
13
+ require_relative 'journal-cli/checkin'
14
+ require_relative 'journal-cli/sections'
15
+ require_relative 'journal-cli/section'
16
+ require_relative 'journal-cli/question'
17
+
18
+ # Main Journal module
19
+ module Journal
20
+ class << self
21
+ def config
22
+ unless @config
23
+ config = File.expand_path('~/.config/journal/journals.yaml')
24
+ raise StandardError, 'No journals configured' unless File.exist?(config)
25
+
26
+ @config = YAML.load(IO.read(config))
27
+ end
28
+
29
+ @config
30
+ end
31
+ end
32
+ end
data/src/_README.md CHANGED
@@ -53,18 +53,19 @@ You can include weather data automatically by setting a question type to 'weathe
53
53
  Edit the file at `~/.config/journal/journals.yaml` following this structure:
54
54
 
55
55
  ```yaml
56
- daily: # journal key, will be used on the command line as `journal daily`
57
- dayone: true # Enable or disable Day One integration
58
- journal: Journal # Day One journal to add to (if using Day One integration)
59
- markdown: daily # Type of Markdown file to create, false to skip (can be daily, individual, or digest)
60
- title: Daily Journal # Title for every entry, date will be appended where needed
61
- sections: # Required key
62
- - title: null # The title for the section. If null, no section header will be created
63
- key: journal # The key for the data collected, must be one word, alphanumeric characters and _ only
64
- questions: # Required key
65
- - prompt: How are you feeling? # The question to ask
66
- key: journal # alphanumeric characters and _ only, will be nested in section key
67
- type: multiline # The type of entry expected (numeric, string, or multiline)
56
+ journals:
57
+ daily: # journal key, will be used on the command line as `journal daily`
58
+ dayone: true # Enable or disable Day One integration
59
+ journal: Journal # Day One journal to add to (if using Day One integration)
60
+ markdown: daily # Type of Markdown file to create, false to skip (can be daily, individual, or digest)
61
+ title: Daily Journal # Title for every entry, date will be appended where needed
62
+ sections: # Required key
63
+ - title: null # The title for the section. If null, no section header will be created
64
+ key: journal # The key for the data collected, must be one word, alphanumeric characters and _ only
65
+ questions: # Required key
66
+ - prompt: How are you feeling? # The question to ask
67
+ key: journal # alphanumeric characters and _ only, will be nested in section key
68
+ type: multiline # The type of entry expected (numeric, string, or multiline)
68
69
  ```
69
70
 
70
71
  Keys must be alphanumeric characters and `_` (underscore) only. Titles and questions can be anything, but if they contain a colon (:), you'll need to quote the string.
@@ -125,10 +126,20 @@ A journal must contain a `sections` key, and each section must contain a `questi
125
126
 
126
127
  Once your configuration file is set up, you can just run `journal JOURNAL_KEY` to begin prompting for the answers to the configured questions.
127
128
 
129
+ If a second argument contains a natural language date, the journal entry will be set to that date instead of the current time. For example, `journal mood "yesterday 5pm"` will create a new entry (in the journal configured for `mood`) for yesterday at 5pm.
130
+
128
131
  Answers will always be written to `~/.local/share/journal/[KEY].json` (where [KEY] is the journal key, one data file for each journal). If you've specified `daily` or `individual` Markdown formats, entries will be written to Markdown files in `~/.local/share/journal/entries/[KEY]`, either in a `%Y-%m-%d.md` file (daily), or in timestamped individual files. If `digest` is specified for the `markdown` key, a single file will be created at `~/.local/share/journal/[KEY].md`.
129
132
 
130
133
  At present there's no tool for querying the dataset created. You just need to parse the JSON and use your language of choice to extract the data. Numeric entries are stored as numbers, and every entry is timestamped, so you should be able to do some advanced analysis once you have enough data.
131
134
 
135
+ ### Answering prompts
136
+
137
+ Questions with numeric answers will have a valid range assigned. Enter just a number within the range and hit return.
138
+
139
+ Questions with type 'string' or 'text' will save when you hit return. Pressing return without typing anything will leave that answer blank, and it will be ignored when exporting to Markdown or Day One (an empty value will exist in the JSON database).
140
+
141
+ When using the mutiline type, you'll get an edit field that responds to most control-key navigation and allows insertion and movement. To save a multiline field, press Escape or type CTRL-d.
142
+
132
143
  <!--END README-->
133
144
  ## Contributing
134
145
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: journal-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.5
4
+ version: 1.0.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brett Terpstra
@@ -174,6 +174,9 @@ files:
174
174
  - lib/journal-cli.rb
175
175
  - lib/journal-cli/checkin.rb
176
176
  - lib/journal-cli/data.rb
177
+ - lib/journal-cli/question.rb
178
+ - lib/journal-cli/section.rb
179
+ - lib/journal-cli/sections.rb
177
180
  - lib/journal-cli/version.rb
178
181
  - lib/journal-cli/weather.rb
179
182
  - src/_README.md