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