journal-cli 1.0.16 → 1.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -0
- data/Gemfile.lock +1 -1
- data/README.md +23 -1
- data/bin/journal +5 -0
- data/lib/journal-cli/checkin.rb +37 -15
- data/lib/journal-cli/color.rb +363 -0
- data/lib/journal-cli/question.rb +16 -4
- data/lib/journal-cli/section.rb +1 -0
- data/lib/journal-cli/version.rb +1 -1
- data/lib/journal-cli/weather.rb +4 -0
- data/lib/journal-cli.rb +12 -2
- data/src/_README.md +23 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2ebc5eaefc846fb4343859ff0148a7798e93e2a4bcc04c5c73ce18151e59ea94
|
|
4
|
+
data.tar.gz: 87ca031548e115350383a28da8ec853d6d665dd2a9c88ac3ea32105350120785
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7d15d09cce5d189a2d6168e02e9c3122334a88ea9e011b82fa97b0a732901913b6668407f3474211ef6b40b3b068363d9778ff4471a8ded4366089338f3fd7af
|
|
7
|
+
data.tar.gz: 3bce2d14e6d5f078b481b9d38184e9cd005c336aeecb36bf4a5daa8b7ef66510c60d46d3cd9db97e254266c8c91a07bbf209785b3aedd2d27f6a255e256f0341
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
### 1.0.18
|
|
2
|
+
|
|
3
|
+
2023-09-09 12:29
|
|
4
|
+
|
|
5
|
+
#### IMPROVED
|
|
6
|
+
|
|
7
|
+
- Include the answers to all questions as YAML front matter when writing individual Markdown files. This allows for tools like [obsidian-dataview](https://github.com/blacksmithgu/obsidian-dataview) to be used as parsers
|
|
8
|
+
|
|
9
|
+
#### FIXED
|
|
10
|
+
|
|
11
|
+
- Daily markdown was being saved to /journal/entries/KEY/entries
|
|
12
|
+
- Missing color library
|
|
13
|
+
|
|
14
|
+
### 1.0.17
|
|
15
|
+
|
|
16
|
+
2023-09-08 07:21
|
|
17
|
+
|
|
18
|
+
#### IMPROVED
|
|
19
|
+
|
|
20
|
+
- More confirmation messages when saving
|
|
21
|
+
|
|
1
22
|
### 1.0.16
|
|
2
23
|
|
|
3
24
|
2023-09-07 11:12
|
data/Gemfile.lock
CHANGED
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
|
|
51
|
+
### Journal Configuration
|
|
52
52
|
|
|
53
53
|
Edit the file at `~/.config/journal/journals.yaml` following this structure:
|
|
54
54
|
|
|
@@ -128,6 +128,28 @@ journals: # required key
|
|
|
128
128
|
|
|
129
129
|
A journal must contain a `sections` key, and each section must contain a `questions` key with an array of questions. Each question must (at minimum) have a `prompt`, `key`, and `type`.
|
|
130
130
|
|
|
131
|
+
If a question has a key `secondary_question`, the prompt will be repeated with the secondary question until it's returned empty, answers will be joined together.
|
|
132
|
+
|
|
133
|
+
### Naming Keys
|
|
134
|
+
|
|
135
|
+
If you want data stored in a nested object, you can set a question type to `dictionary` and set the prompt to `null` (or just leave the key out), but give it a key that will serve as the parent in the object. Then in the nested questions, give them a key in the dot format `[PARENT_KEY].[CHILD_KEY]`. Section keys automatically nest their children, but if you want to go deeper, you could have a question with the key `health` and type `dictionary`, then have questions with keys like `health.rating` and `health.notes`. If the section key was `status`, the resulting dictionary would look like this in the JSON:
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"date": "2023-09-08 12:19:40 UTC",
|
|
140
|
+
"data": {
|
|
141
|
+
"status": {
|
|
142
|
+
"health": {
|
|
143
|
+
"rating": 4,
|
|
144
|
+
"notes": "Feeling much better today. Still a bit groggy."
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
If a question has the same key as its parent section, it will be moved up the chain so that you don't get `{ 'journal': { 'journal': 'Journal notes' } }`. You'll just get `{ 'journal': 'Journal notes' }`. This offers a way to organize data with fewer levels of nesting in the output.
|
|
152
|
+
|
|
131
153
|
## Usage
|
|
132
154
|
|
|
133
155
|
Once your configuration file is set up, you can just run `journal JOURNAL_KEY` to begin prompting for the answers to the configured questions.
|
data/bin/journal
CHANGED
data/lib/journal-cli/checkin.rb
CHANGED
|
@@ -63,15 +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
|
+
Journal.notify('{bg}Entered one entry into Day One')
|
|
66
67
|
end
|
|
67
68
|
|
|
68
69
|
def save_single_markdown
|
|
69
70
|
dir = if @journal.key?('entries_folder')
|
|
70
71
|
File.join(File.expand_path(@journal['entries_folder']), 'entries')
|
|
71
72
|
elsif Journal.config.key?('entries_folder')
|
|
72
|
-
File.join(File.expand_path(Journal.config['entries_folder']), @key
|
|
73
|
+
File.join(File.expand_path(Journal.config['entries_folder']), @key)
|
|
73
74
|
else
|
|
74
|
-
File.expand_path(
|
|
75
|
+
File.expand_path("~/.local/share/journal/#{@key}/entries")
|
|
75
76
|
end
|
|
76
77
|
|
|
77
78
|
FileUtils.mkdir_p(dir) unless File.directory?(dir)
|
|
@@ -84,16 +85,16 @@ module Journal
|
|
|
84
85
|
f.puts
|
|
85
86
|
f.puts to_markdown(yaml: false, title: false)
|
|
86
87
|
end
|
|
87
|
-
|
|
88
|
+
Journal.notify "{bg}Added new entry to {bw}#{target}"
|
|
88
89
|
end
|
|
89
90
|
|
|
90
91
|
def save_daily_markdown
|
|
91
92
|
dir = if @journal.key?('entries_folder')
|
|
92
93
|
File.join(File.expand_path(@journal['entries_folder']), 'entries')
|
|
93
94
|
elsif Journal.config.key?('entries_folder')
|
|
94
|
-
File.join(File.expand_path(Journal.config['entries_folder']), @key
|
|
95
|
+
File.join(File.expand_path(Journal.config['entries_folder']), @key)
|
|
95
96
|
else
|
|
96
|
-
File.join(File.expand_path(
|
|
97
|
+
File.join(File.expand_path("~/.local/share/journal/#{@key}/entries"))
|
|
97
98
|
end
|
|
98
99
|
|
|
99
100
|
FileUtils.mkdir_p(dir) unless File.directory?(dir)
|
|
@@ -105,7 +106,7 @@ module Journal
|
|
|
105
106
|
else
|
|
106
107
|
File.open(target, 'w') { |f| f.puts to_markdown(yaml: true, title: true, date: false, time: true) }
|
|
107
108
|
end
|
|
108
|
-
|
|
109
|
+
Journal.notify "{bg}Saved daily Markdown to {bw}#{target}"
|
|
109
110
|
end
|
|
110
111
|
|
|
111
112
|
def save_individual_markdown
|
|
@@ -123,7 +124,7 @@ module Journal
|
|
|
123
124
|
filename = @date.strftime('%Y-%m-%d_%H:%M.md')
|
|
124
125
|
target = File.join(dir, filename)
|
|
125
126
|
File.open(target, 'w') { |f| f.puts to_markdown(yaml: true, title: true) }
|
|
126
|
-
puts "Saved #{target}"
|
|
127
|
+
puts "Saved new entry to #{target}"
|
|
127
128
|
end
|
|
128
129
|
|
|
129
130
|
def print_answer(prompt, type, key, data)
|
|
@@ -143,18 +144,33 @@ module Journal
|
|
|
143
144
|
end
|
|
144
145
|
end
|
|
145
146
|
|
|
147
|
+
def weather_to_yaml(answers)
|
|
148
|
+
data = {}
|
|
149
|
+
answers.each do |k, v|
|
|
150
|
+
case v.class.to_s
|
|
151
|
+
when /Hash/
|
|
152
|
+
data[k] = weather_to_yaml(v)
|
|
153
|
+
when /Weather/
|
|
154
|
+
data[k] = v.to_s
|
|
155
|
+
else
|
|
156
|
+
data[k] = v
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
data
|
|
160
|
+
end
|
|
161
|
+
|
|
146
162
|
def to_markdown(yaml: false, title: false, date: false, time: false)
|
|
147
163
|
@output = []
|
|
148
164
|
|
|
149
165
|
if yaml
|
|
150
166
|
@date.localtime
|
|
151
|
-
@
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
---
|
|
167
|
+
yaml_data = { 'title' => @title, 'date' => @date.strftime('%x %X')}
|
|
168
|
+
@data.each do |key, data|
|
|
169
|
+
yaml_data = yaml_data.merge(weather_to_yaml(data.answers))
|
|
170
|
+
end
|
|
156
171
|
|
|
157
|
-
|
|
172
|
+
@output << YAML.dump(yaml_data).strip
|
|
173
|
+
@output << '---'
|
|
158
174
|
end
|
|
159
175
|
|
|
160
176
|
if title
|
|
@@ -198,7 +214,7 @@ module Journal
|
|
|
198
214
|
elsif Journal.config.key?('entries_folder')
|
|
199
215
|
File.expand_path(Journal.config['entries_folder'])
|
|
200
216
|
else
|
|
201
|
-
File.expand_path(
|
|
217
|
+
File.expand_path('~/.local/share/journal')
|
|
202
218
|
end
|
|
203
219
|
FileUtils.mkdir_p(dir) unless File.directory?(dir)
|
|
204
220
|
db = File.join(dir, "#{@key}.json")
|
|
@@ -217,13 +233,18 @@ module Journal
|
|
|
217
233
|
v.each do |key, value|
|
|
218
234
|
result = case value.class.to_s
|
|
219
235
|
when /Weather/
|
|
220
|
-
{
|
|
236
|
+
{
|
|
237
|
+
'high' => value.data[:high],
|
|
238
|
+
'low' => value.data[:low],
|
|
239
|
+
'condition' => value.data[:condition]
|
|
240
|
+
}
|
|
221
241
|
else
|
|
222
242
|
value
|
|
223
243
|
end
|
|
224
244
|
if jk == k
|
|
225
245
|
output[jk][key] = result
|
|
226
246
|
else
|
|
247
|
+
output[jk][k] ||= {}
|
|
227
248
|
output[jk][k][key] = result
|
|
228
249
|
end
|
|
229
250
|
end
|
|
@@ -245,6 +266,7 @@ module Journal
|
|
|
245
266
|
data.sort_by! { |e| e['date'] }
|
|
246
267
|
|
|
247
268
|
File.open(db, 'w') { |f| f.puts JSON.pretty_generate(data) }
|
|
269
|
+
Journal.notify "{bg}Saved {bw}#{db}"
|
|
248
270
|
end
|
|
249
271
|
end
|
|
250
272
|
end
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Cribbed from <https://github.com/flori/term-ansicolor>
|
|
4
|
+
# Terminal output color functions.
|
|
5
|
+
module Color
|
|
6
|
+
# Regexp to match excape sequences
|
|
7
|
+
ESCAPE_REGEX = /(?<=\[)(?:(?:(?:[349]|10)[0-9]|[0-9])?;?)+(?=m)/
|
|
8
|
+
|
|
9
|
+
# All available color names. Available as methods and string extensions.
|
|
10
|
+
#
|
|
11
|
+
# @example Use a color as a method. Color reset will be added to end of string.
|
|
12
|
+
# Color.yellow('This text is yellow') => "\e[33mThis text is yellow\e[0m"
|
|
13
|
+
#
|
|
14
|
+
# @example Use a color as a string extension. Color reset added automatically.
|
|
15
|
+
# 'This text is green'.green => "\e[1;32mThis text is green\e[0m"
|
|
16
|
+
#
|
|
17
|
+
# @example Send a text string as a color
|
|
18
|
+
# Color.send('red') => "\e[31m"
|
|
19
|
+
ATTRIBUTES = [
|
|
20
|
+
[:clear, 0], # String#clear is already used to empty string in Ruby 1.9
|
|
21
|
+
[:reset, 0], # synonym for :clear
|
|
22
|
+
[:bold, 1],
|
|
23
|
+
[:dark, 2],
|
|
24
|
+
[:italic, 3], # not widely implemented
|
|
25
|
+
[:underline, 4],
|
|
26
|
+
[:underscore, 4], # synonym for :underline
|
|
27
|
+
[:blink, 5],
|
|
28
|
+
[:rapid_blink, 6], # not widely implemented
|
|
29
|
+
[:negative, 7], # no reverse because of String#reverse
|
|
30
|
+
[:concealed, 8],
|
|
31
|
+
[:strikethrough, 9], # not widely implemented
|
|
32
|
+
[:strike, 9], # not widely implemented
|
|
33
|
+
[:black, 30],
|
|
34
|
+
[:red, 31],
|
|
35
|
+
[:green, 32],
|
|
36
|
+
[:yellow, 33],
|
|
37
|
+
[:blue, 34],
|
|
38
|
+
[:magenta, 35],
|
|
39
|
+
[:purple, 35],
|
|
40
|
+
[:cyan, 36],
|
|
41
|
+
[:white, 37],
|
|
42
|
+
[:bgblack, 40],
|
|
43
|
+
[:bgred, 41],
|
|
44
|
+
[:bggreen, 42],
|
|
45
|
+
[:bgyellow, 43],
|
|
46
|
+
[:bgblue, 44],
|
|
47
|
+
[:bgmagenta, 45],
|
|
48
|
+
[:bgpurple, 45],
|
|
49
|
+
[:bgcyan, 46],
|
|
50
|
+
[:bgwhite, 47],
|
|
51
|
+
[:boldblack, 90],
|
|
52
|
+
[:boldred, 91],
|
|
53
|
+
[:boldgreen, 92],
|
|
54
|
+
[:boldyellow, 93],
|
|
55
|
+
[:boldblue, 94],
|
|
56
|
+
[:boldmagenta, 95],
|
|
57
|
+
[:boldpurple, 95],
|
|
58
|
+
[:boldcyan, 96],
|
|
59
|
+
[:boldwhite, 97],
|
|
60
|
+
[:boldbgblack, 100],
|
|
61
|
+
[:boldbgred, 101],
|
|
62
|
+
[:boldbggreen, 102],
|
|
63
|
+
[:boldbgyellow, 103],
|
|
64
|
+
[:boldbgblue, 104],
|
|
65
|
+
[:boldbgmagenta, 105],
|
|
66
|
+
[:boldbgpurple, 105],
|
|
67
|
+
[:boldbgcyan, 106],
|
|
68
|
+
[:boldbgwhite, 107],
|
|
69
|
+
[:softpurple, '0;35;40'],
|
|
70
|
+
[:hotpants, '7;34;40'],
|
|
71
|
+
[:knightrider, '7;30;40'],
|
|
72
|
+
[:flamingo, '7;31;47'],
|
|
73
|
+
[:yeller, '1;37;43'],
|
|
74
|
+
[:whiteboard, '1;30;47'],
|
|
75
|
+
[:chalkboard, '1;37;40'],
|
|
76
|
+
[:led, '0;32;40'],
|
|
77
|
+
[:redacted, '0;30;40'],
|
|
78
|
+
[:alert, '1;31;43'],
|
|
79
|
+
[:error, '1;37;41'],
|
|
80
|
+
[:default, '0;39']
|
|
81
|
+
].map(&:freeze).freeze
|
|
82
|
+
|
|
83
|
+
# Array of attribute keys only
|
|
84
|
+
ATTRIBUTE_NAMES = ATTRIBUTES.transpose.first
|
|
85
|
+
|
|
86
|
+
# Returns true if Color supports the +feature+.
|
|
87
|
+
#
|
|
88
|
+
# The feature :clear, that is mixing the clear color attribute into String,
|
|
89
|
+
# is only supported on ruby implementations, that do *not* already
|
|
90
|
+
# implement the String#clear method. It's better to use the reset color
|
|
91
|
+
# attribute instead.
|
|
92
|
+
def support?(feature)
|
|
93
|
+
case feature
|
|
94
|
+
when :clear
|
|
95
|
+
!String.instance_methods(false).map(&:to_sym).include?(:clear)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Template coloring
|
|
100
|
+
class ::String
|
|
101
|
+
##
|
|
102
|
+
## Extract the longest valid %color name from a string.
|
|
103
|
+
##
|
|
104
|
+
## Allows %colors to bleed into other text and still
|
|
105
|
+
## be recognized, e.g. %greensomething still finds
|
|
106
|
+
## %green.
|
|
107
|
+
##
|
|
108
|
+
## @return [String] a valid color name
|
|
109
|
+
##
|
|
110
|
+
def validate_color
|
|
111
|
+
valid_color = nil
|
|
112
|
+
compiled = ''
|
|
113
|
+
normalize_color.split('').each do |char|
|
|
114
|
+
compiled += char
|
|
115
|
+
valid_color = compiled if Color.attributes.include?(compiled.to_sym) || compiled =~ /^([fb]g?)?#([a-f0-9]{6})$/i
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
valid_color
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
##
|
|
122
|
+
## Normalize a color name, removing underscores,
|
|
123
|
+
## replacing "bright" with "bold", and converting
|
|
124
|
+
## bgbold to boldbg
|
|
125
|
+
##
|
|
126
|
+
## @return [String] Normalized color name
|
|
127
|
+
##
|
|
128
|
+
def normalize_color
|
|
129
|
+
gsub(/_/, '').sub(/bright/i, 'bold').sub(/bgbold/, 'boldbg')
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Get the calculated ANSI color at the end of the
|
|
133
|
+
# string
|
|
134
|
+
#
|
|
135
|
+
# @return ANSI escape sequence to match color
|
|
136
|
+
#
|
|
137
|
+
def last_color_code
|
|
138
|
+
m = scan(ESCAPE_REGEX)
|
|
139
|
+
|
|
140
|
+
em = ['0']
|
|
141
|
+
fg = nil
|
|
142
|
+
bg = nil
|
|
143
|
+
rgbf = nil
|
|
144
|
+
rgbb = nil
|
|
145
|
+
|
|
146
|
+
m.each do |c|
|
|
147
|
+
case c
|
|
148
|
+
when '0'
|
|
149
|
+
em = ['0']
|
|
150
|
+
fg, bg, rgbf, rgbb = nil
|
|
151
|
+
when /^[34]8/
|
|
152
|
+
case c
|
|
153
|
+
when /^3/
|
|
154
|
+
fg = nil
|
|
155
|
+
rgbf = c
|
|
156
|
+
when /^4/
|
|
157
|
+
bg = nil
|
|
158
|
+
rgbb = c
|
|
159
|
+
end
|
|
160
|
+
else
|
|
161
|
+
c.split(/;/).each do |i|
|
|
162
|
+
x = i.to_i
|
|
163
|
+
if x <= 9
|
|
164
|
+
em << x
|
|
165
|
+
elsif x >= 30 && x <= 39
|
|
166
|
+
rgbf = nil
|
|
167
|
+
fg = x
|
|
168
|
+
elsif x >= 40 && x <= 49
|
|
169
|
+
rgbb = nil
|
|
170
|
+
bg = x
|
|
171
|
+
elsif x >= 90 && x <= 97
|
|
172
|
+
rgbf = nil
|
|
173
|
+
fg = x
|
|
174
|
+
elsif x >= 100 && x <= 107
|
|
175
|
+
rgbb = nil
|
|
176
|
+
bg = x
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
escape = "\e[#{em.join(';')}m"
|
|
183
|
+
escape += "\e[#{rgbb}m" if rgbb
|
|
184
|
+
escape += "\e[#{rgbf}m" if rgbf
|
|
185
|
+
escape + "\e[#{[fg, bg].delete_if(&:nil?).join(';')}m"
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
class << self
|
|
190
|
+
# Returns true if the coloring function of this module
|
|
191
|
+
# is switched on, false otherwise.
|
|
192
|
+
def coloring?
|
|
193
|
+
@coloring
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
attr_writer :coloring
|
|
197
|
+
|
|
198
|
+
##
|
|
199
|
+
## Enables colored output
|
|
200
|
+
##
|
|
201
|
+
## @example Turn color on or off based on TTY
|
|
202
|
+
## Color.coloring = STDOUT.isatty
|
|
203
|
+
def coloring
|
|
204
|
+
@coloring ||= true
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
##
|
|
208
|
+
## Convert a template string to a colored string.
|
|
209
|
+
## Colors are specified with single letters inside
|
|
210
|
+
## curly braces. Uppercase changes background color.
|
|
211
|
+
##
|
|
212
|
+
## w: white, k: black, g: green, l: blue, y: yellow, c: cyan,
|
|
213
|
+
## m: magenta, r: red, b: bold, u: underline, i: italic,
|
|
214
|
+
## x: reset (remove background, color, emphasis)
|
|
215
|
+
##
|
|
216
|
+
## Also accepts {#RGB} and {#RRGGBB} strings. Put a b before
|
|
217
|
+
## the hash to make it a background color
|
|
218
|
+
##
|
|
219
|
+
## @example Convert a templated string
|
|
220
|
+
## Color.template('{Rwb}Warning:{x} {w}you look a little {g}ill{x}')
|
|
221
|
+
##
|
|
222
|
+
## @example Convert using RGB colors
|
|
223
|
+
## Color.template('{#f0a}This is an RGB color')
|
|
224
|
+
##
|
|
225
|
+
## @param input [String, Array] The template
|
|
226
|
+
## string. If this is an array, the
|
|
227
|
+
## elements will be joined with a
|
|
228
|
+
## space.
|
|
229
|
+
##
|
|
230
|
+
## @return [String] Colorized string
|
|
231
|
+
##
|
|
232
|
+
def template(input)
|
|
233
|
+
input = input.join(' ') if input.is_a? Array
|
|
234
|
+
return input.gsub(/(?<!\\)\{(\w+)\}/i, '') unless Color.coloring?
|
|
235
|
+
|
|
236
|
+
input = input.gsub(/(?<!\\)\{((?:[fb]g?)?#[a-f0-9]{3,6})\}/i) do
|
|
237
|
+
hex = Regexp.last_match(1)
|
|
238
|
+
rgb(hex)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
fmt = input.gsub(/%/, '%%')
|
|
242
|
+
fmt = fmt.gsub(/(?<!\\)\{(\w+)\}/i) do
|
|
243
|
+
Regexp.last_match(1).split('').map { |c| "%<#{c}>s" }.join('')
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
colors = { w: white, k: black, g: green, l: blue,
|
|
247
|
+
y: yellow, c: cyan, m: magenta, r: red,
|
|
248
|
+
W: bgwhite, K: bgblack, G: bggreen, L: bgblue,
|
|
249
|
+
Y: bgyellow, C: bgcyan, M: bgmagenta, R: bgred,
|
|
250
|
+
d: dark, b: bold, u: underline, i: italic, x: reset }
|
|
251
|
+
|
|
252
|
+
format(fmt, colors)
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
ATTRIBUTES.each do |c, v|
|
|
257
|
+
new_method = <<-EOSCRIPT
|
|
258
|
+
# Color string as #{c}
|
|
259
|
+
def #{c}(string = nil)
|
|
260
|
+
result = ''
|
|
261
|
+
result << "\e[#{v}m" if Color.coloring?
|
|
262
|
+
if block_given?
|
|
263
|
+
result << yield
|
|
264
|
+
elsif string.respond_to?(:to_str)
|
|
265
|
+
result << string.to_str
|
|
266
|
+
elsif respond_to?(:to_str)
|
|
267
|
+
result << to_str
|
|
268
|
+
else
|
|
269
|
+
return result #only switch on
|
|
270
|
+
end
|
|
271
|
+
result << "\e[0m" if Color.coloring?
|
|
272
|
+
result
|
|
273
|
+
end
|
|
274
|
+
EOSCRIPT
|
|
275
|
+
|
|
276
|
+
module_eval(new_method)
|
|
277
|
+
|
|
278
|
+
next unless c =~ /bold/
|
|
279
|
+
|
|
280
|
+
# Accept brightwhite in addition to boldwhite
|
|
281
|
+
new_method = <<-EOSCRIPT
|
|
282
|
+
# color string as #{c}
|
|
283
|
+
def #{c.to_s.sub(/bold/, 'bright')}(string = nil)
|
|
284
|
+
result = ''
|
|
285
|
+
result << "\e[#{v}m" if Color.coloring?
|
|
286
|
+
if block_given?
|
|
287
|
+
result << yield
|
|
288
|
+
elsif string.respond_to?(:to_str)
|
|
289
|
+
result << string.to_str
|
|
290
|
+
elsif respond_to?(:to_str)
|
|
291
|
+
result << to_str
|
|
292
|
+
else
|
|
293
|
+
return result #only switch on
|
|
294
|
+
end
|
|
295
|
+
result << "\e[0m" if Color.coloring?
|
|
296
|
+
result
|
|
297
|
+
end
|
|
298
|
+
EOSCRIPT
|
|
299
|
+
|
|
300
|
+
module_eval(new_method)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
##
|
|
304
|
+
## Generate escape codes for hex colors
|
|
305
|
+
##
|
|
306
|
+
## @param hex [String] The hexadecimal color code
|
|
307
|
+
##
|
|
308
|
+
## @return [String] ANSI escape string
|
|
309
|
+
##
|
|
310
|
+
def rgb(hex)
|
|
311
|
+
is_bg = hex.match(/^bg?#/) ? true : false
|
|
312
|
+
hex_string = hex.sub(/^([fb]g?)?#/, '')
|
|
313
|
+
|
|
314
|
+
if hex_string.length == 3
|
|
315
|
+
parts = hex_string.match(/(?<r>.)(?<g>.)(?<b>.)/)
|
|
316
|
+
|
|
317
|
+
t = []
|
|
318
|
+
%w[r g b].each do |e|
|
|
319
|
+
t << parts[e]
|
|
320
|
+
t << parts[e]
|
|
321
|
+
end
|
|
322
|
+
hex_string = t.join('')
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
parts = hex_string.match(/(?<r>..)(?<g>..)(?<b>..)/)
|
|
326
|
+
t = []
|
|
327
|
+
%w[r g b].each do |e|
|
|
328
|
+
t << parts[e].hex
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
"\e[#{is_bg ? '48' : '38'};2;#{t.join(';')}m"
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# Regular expression that is used to scan for ANSI-sequences while
|
|
335
|
+
# uncoloring strings.
|
|
336
|
+
COLORED_REGEXP = /\e\[(?:(?:(?:[349]|10)[0-9]|[0-9])?;?)+m/
|
|
337
|
+
|
|
338
|
+
# Returns an uncolored version of the string, that is all
|
|
339
|
+
# ANSI-sequences are stripped from the string.
|
|
340
|
+
def uncolor(string = nil) # :yields:
|
|
341
|
+
if block_given?
|
|
342
|
+
yield.to_str.gsub(COLORED_REGEXP, '')
|
|
343
|
+
elsif string.respond_to?(:to_str)
|
|
344
|
+
string.to_str.gsub(COLORED_REGEXP, '')
|
|
345
|
+
elsif respond_to?(:to_str)
|
|
346
|
+
to_str.gsub(COLORED_REGEXP, '')
|
|
347
|
+
else
|
|
348
|
+
''
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# Returns an array of all Color attributes as symbols.
|
|
353
|
+
def attributes
|
|
354
|
+
ATTRIBUTE_NAMES
|
|
355
|
+
end
|
|
356
|
+
extend self
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
class ::String
|
|
360
|
+
def x
|
|
361
|
+
Color.template(self)
|
|
362
|
+
end
|
|
363
|
+
end
|
data/lib/journal-cli/question.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
data/lib/journal-cli/section.rb
CHANGED
data/lib/journal-cli/version.rb
CHANGED
data/lib/journal-cli/weather.rb
CHANGED
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
|
-
|
|
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
|
|
54
|
+
### Journal Configuration
|
|
55
55
|
|
|
56
56
|
Edit the file at `~/.config/journal/journals.yaml` following this structure:
|
|
57
57
|
|
|
@@ -131,6 +131,28 @@ journals: # required key
|
|
|
131
131
|
|
|
132
132
|
A journal must contain a `sections` key, and each section must contain a `questions` key with an array of questions. Each question must (at minimum) have a `prompt`, `key`, and `type`.
|
|
133
133
|
|
|
134
|
+
If a question has a key `secondary_question`, the prompt will be repeated with the secondary question until it's returned empty, answers will be joined together.
|
|
135
|
+
|
|
136
|
+
### Naming Keys
|
|
137
|
+
|
|
138
|
+
If you want data stored in a nested object, you can set a question type to `dictionary` and set the prompt to `null` (or just leave the key out), but give it a key that will serve as the parent in the object. Then in the nested questions, give them a key in the dot format `[PARENT_KEY].[CHILD_KEY]`. Section keys automatically nest their children, but if you want to go deeper, you could have a question with the key `health` and type `dictionary`, then have questions with keys like `health.rating` and `health.notes`. If the section key was `status`, the resulting dictionary would look like this in the JSON:
|
|
139
|
+
|
|
140
|
+
```json
|
|
141
|
+
{
|
|
142
|
+
"date": "2023-09-08 12:19:40 UTC",
|
|
143
|
+
"data": {
|
|
144
|
+
"status": {
|
|
145
|
+
"health": {
|
|
146
|
+
"rating": 4,
|
|
147
|
+
"notes": "Feeling much better today. Still a bit groggy."
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
If a question has the same key as its parent section, it will be moved up the chain so that you don't get `{ 'journal': { 'journal': 'Journal notes' } }`. You'll just get `{ 'journal': 'Journal notes' }`. This offers a way to organize data with fewer levels of nesting in the output.
|
|
155
|
+
|
|
134
156
|
## Usage
|
|
135
157
|
|
|
136
158
|
Once your configuration file is set up, you can just run `journal JOURNAL_KEY` to begin prompting for the answers to the configured questions.
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: journal-cli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.18
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brett Terpstra
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2023-09-
|
|
11
|
+
date: 2023-09-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|
|
@@ -173,6 +173,7 @@ files:
|
|
|
173
173
|
- journal-cli.gemspec
|
|
174
174
|
- lib/journal-cli.rb
|
|
175
175
|
- lib/journal-cli/checkin.rb
|
|
176
|
+
- lib/journal-cli/color.rb
|
|
176
177
|
- lib/journal-cli/data.rb
|
|
177
178
|
- lib/journal-cli/question.rb
|
|
178
179
|
- lib/journal-cli/section.rb
|