journal-cli 1.0.17 → 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 +13 -0
- data/Gemfile.lock +1 -1
- data/README.md +23 -1
- data/bin/journal +5 -0
- data/lib/journal-cli/checkin.rb +37 -17
- 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,16 @@
|
|
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
|
+
|
1
14
|
### 1.0.17
|
2
15
|
|
3
16
|
2023-09-08 07:21
|
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,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
|
-
|
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
|
73
|
+
File.join(File.expand_path(Journal.config['entries_folder']), @key)
|
74
74
|
else
|
75
|
-
File.expand_path(
|
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
|
-
|
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
|
95
|
+
File.join(File.expand_path(Journal.config['entries_folder']), @key)
|
96
96
|
else
|
97
|
-
File.join(File.expand_path(
|
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
|
-
|
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)
|
@@ -144,18 +144,33 @@ module Journal
|
|
144
144
|
end
|
145
145
|
end
|
146
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
|
+
|
147
162
|
def to_markdown(yaml: false, title: false, date: false, time: false)
|
148
163
|
@output = []
|
149
164
|
|
150
165
|
if yaml
|
151
166
|
@date.localtime
|
152
|
-
@
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
---
|
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
|
157
171
|
|
158
|
-
|
172
|
+
@output << YAML.dump(yaml_data).strip
|
173
|
+
@output << '---'
|
159
174
|
end
|
160
175
|
|
161
176
|
if title
|
@@ -199,7 +214,7 @@ module Journal
|
|
199
214
|
elsif Journal.config.key?('entries_folder')
|
200
215
|
File.expand_path(Journal.config['entries_folder'])
|
201
216
|
else
|
202
|
-
File.expand_path(
|
217
|
+
File.expand_path('~/.local/share/journal')
|
203
218
|
end
|
204
219
|
FileUtils.mkdir_p(dir) unless File.directory?(dir)
|
205
220
|
db = File.join(dir, "#{@key}.json")
|
@@ -218,13 +233,18 @@ module Journal
|
|
218
233
|
v.each do |key, value|
|
219
234
|
result = case value.class.to_s
|
220
235
|
when /Weather/
|
221
|
-
{
|
236
|
+
{
|
237
|
+
'high' => value.data[:high],
|
238
|
+
'low' => value.data[:low],
|
239
|
+
'condition' => value.data[:condition]
|
240
|
+
}
|
222
241
|
else
|
223
242
|
value
|
224
243
|
end
|
225
244
|
if jk == k
|
226
245
|
output[jk][key] = result
|
227
246
|
else
|
247
|
+
output[jk][k] ||= {}
|
228
248
|
output[jk][k][key] = result
|
229
249
|
end
|
230
250
|
end
|
@@ -246,7 +266,7 @@ module Journal
|
|
246
266
|
data.sort_by! { |e| e['date'] }
|
247
267
|
|
248
268
|
File.open(db, 'w') { |f| f.puts JSON.pretty_generate(data) }
|
249
|
-
|
269
|
+
Journal.notify "{bg}Saved {bw}#{db}"
|
250
270
|
end
|
251
271
|
end
|
252
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
|