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 +4 -4
- data/CHANGELOG.md +11 -26
- data/Gemfile.lock +1 -1
- data/README.md +23 -12
- data/bin/journal +47 -25
- data/lib/journal-cli/checkin.rb +82 -139
- data/lib/journal-cli/question.rb +99 -0
- data/lib/journal-cli/section.rb +47 -0
- data/lib/journal-cli/sections.rb +13 -0
- data/lib/journal-cli/version.rb +1 -1
- data/lib/journal-cli.rb +23 -4
- data/src/_README.md +23 -12
- metadata +4 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: daaf3cad805bf34bae2691d8d5738316aef4318bf9f6a9562fcbc38c27f0e3be
|
4
|
+
data.tar.gz: 861bca0aae3312d48dd070dc5c88cffadb709242e9374f25856ac6b5b5189cfd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0c982533e8d04247ccfffe560549f01ea57725bfcc0391294ece7bb32adf56fdee95589700fcdb6ca855dddaa446dfe06c6d15a394befd231683680194678670
|
7
|
+
data.tar.gz: 787c597423d1ef820068b6498fd5004edf17e19c2e6f723e69b6feca919c8cc8bbe9127304749ce3c88b320d69e330823f51e0de14f7c1995e0c2d1a98fdc690
|
data/CHANGELOG.md
CHANGED
@@ -1,43 +1,28 @@
|
|
1
|
-
### 1.0.
|
1
|
+
### 1.0.10
|
2
2
|
|
3
|
-
2023-09-06
|
3
|
+
2023-09-06 16:03
|
4
4
|
|
5
|
-
|
5
|
+
#### IMPROVED
|
6
6
|
|
7
|
-
|
7
|
+
- Refactoring code
|
8
8
|
|
9
|
-
|
9
|
+
### 1.0.9
|
10
10
|
|
11
|
-
-
|
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
|
-
-
|
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
|
-
|
26
|
-
|
27
|
-
#### NEW
|
17
|
+
### 1.0.5
|
28
18
|
|
29
|
-
-
|
30
|
-
- Multiple journals, multiple sections
|
19
|
+
2023-09-06 09:24
|
31
20
|
|
32
|
-
### 1.0.
|
21
|
+
### 1.0.0
|
33
22
|
|
34
|
-
2023-09-06 09:
|
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
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
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
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
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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 =
|
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
|
-
|
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)
|
data/lib/journal-cli/checkin.rb
CHANGED
@@ -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
|
12
|
+
raise StandardError, "No journal with key #{@key} found" unless Journal.config['journals'].key? @key
|
19
13
|
|
20
|
-
@journal =
|
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
|
-
|
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
|
-
|
77
|
-
|
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
|
-
|
84
|
-
res = res[key]
|
85
|
-
end
|
48
|
+
return unless @journal['markdown']
|
86
49
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
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
|
-
|
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
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
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
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
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
|
-
|
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
|
-
@
|
178
|
-
section
|
149
|
+
@sections.each do |key, section|
|
150
|
+
answers = section.answers
|
151
|
+
section section.title
|
179
152
|
|
180
|
-
|
181
|
-
if
|
182
|
-
res =
|
183
|
-
keys =
|
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(
|
162
|
+
print_answer(question.prompt, question.type, keys.last, res)
|
190
163
|
else
|
191
|
-
print_answer(
|
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 |
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
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[
|
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
|
data/lib/journal-cli/version.rb
CHANGED
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
|
11
|
-
require_relative
|
12
|
-
require_relative
|
13
|
-
require_relative
|
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
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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.
|
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
|