journal-cli 1.0.22 → 1.0.24

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1255e03e2644a0bb96f72622c8815595f41bec61e1add52529b697097f4b94f6
4
- data.tar.gz: 613eca7fbaccd6536b3edd23da7160c337ad1bf7dc7dc509b41ddf85e788c478
3
+ metadata.gz: 110b0fe8610e39052c3f85c417739821f66103cd0457a23a03188a158062962f
4
+ data.tar.gz: b56dfb4649456dd53e22aa86fece669f8d162fd6048537e06ba8c6aeb0db61c1
5
5
  SHA512:
6
- metadata.gz: 34c25cc5c7c076f47dfa2fedd6bd7434dc8b7268b2662cef74bdcabbd6fdbe99b1a3882ad4c129e6c5434b169fd0d5a377bbeef78207e993b026125339386a96
7
- data.tar.gz: 3a1fccc6120ed77cc602a1448a9949b49aa42ea658d3c93d11de7d77aad9a6bc0e707ac125f0faaa93bad5bb3de2b921fe35f57afd1a112a01982ce5bc9b01b6
6
+ metadata.gz: 237a1faa0d7d4acfb0bb1d0c3775b46055529e432dbba25e38b723cd5cab39e68db7796d824c870739f8fa9b28f2c1d040df1d4fb84bd3e57581cc18ba931ea7
7
+ data.tar.gz: cc8e68cf73df3bf13a71b2eb9126a136dd9f989a869c519dece7f733e1a89dfd5552626fc249bf7c6bc2ea95ee2a1a23836a6c8ed410202b1d4578b6be8d655e
data/CHANGELOG.md CHANGED
@@ -1,3 +1,20 @@
1
+ ### 1.0.24
2
+
3
+ 2023-09-20 09:12
4
+
5
+ ### 1.0.23
6
+
7
+ 2023-09-20 08:30
8
+
9
+ #### NEW
10
+
11
+ - Question types weather.forecast and weather.current allow more specific weather entry types
12
+ - Time-based conditions for questions and sections (`condition: before noon` or `condition: < 12pm`)
13
+
14
+ #### FIXED
15
+
16
+ - Test for existence of dayone2 binary before attempting to write a Day One entry, provide error message
17
+
1
18
  ### 1.0.22
2
19
 
3
20
  2023-09-18 10:23
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- journal-cli (1.0.21)
4
+ journal-cli (1.0.24)
5
5
  chronic (~> 0.10, >= 0.10.2)
6
6
  tty-reader (~> 0.9, >= 0.9.0)
7
7
  tty-which (~> 0.5, >= 0.5.0)
data/README.md CHANGED
@@ -52,6 +52,10 @@ This file contains a YAML definition of your journal. Each journal gets a top-le
52
52
 
53
53
  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.
54
54
 
55
+ If a question type is set to `weather.forecast`, only the predicted condition, high, and low will be included in the JSON data for the question. A full printout of hourly temps will be included in the Markdown/Day One output.
56
+
57
+ If the question type is `weather.current`, only the current condition and temperature will be recorded to the JSON, and a string containing "[TEMP] and [CONDITION]" (e.g. "64 and Sunny") will be recorded to Markdown/Day One for the question.
58
+
55
59
  ### Journal Configuration
56
60
 
57
61
  Edit the file at `~/.config/journal/journals.yaml` following this structure:
@@ -96,9 +100,12 @@ journals: # required key
96
100
  - title: Weather # Title of the section (will create template sections in Day One)
97
101
  key: weather # the key to use in the structured data, will contain all of the answers
98
102
  questions: # required key
99
- - prompt: Current weather # The prompt shown on the command line, will also become a header in the journal entries (Markdown, Day One)
103
+ - prompt: Current Weather
104
+ key: weather.current
105
+ type: weather.current
106
+ - prompt: Weather Forecast # The prompt shown on the command line, will also become a header in the journal entries (Markdown, Day One)
100
107
  key: weather.forecast # if a key contains a dot, it will create nested data, e.g. `{ 'weather': { 'forecast': data } }`
101
- type: weather # Set this to weather for weather data
108
+ type: weather.forecast # Set this to weather for weather data
102
109
  - title: Health # New section
103
110
  key: health
104
111
  questions:
@@ -141,10 +148,18 @@ A question `type` can be one of:
141
148
  - `text` or `string` will request a single-line string, submitted on return
142
149
  - `multiline` for multiline strings (opens a readline editor, use ctrl-d to save)
143
150
  - `weather` will just insert current weather data with no prompt
151
+ * `weather.forecast` will insert just the forecast
152
+ * `weather.current` will insert just the current temperature and condition
144
153
  - `number` or `float` will request numeric input, stored as a float (decimal)
145
154
  - `integer` will convert numeric input to the nearest integer
146
155
  - `date` will request a natural language date which will be parsed into a date object
147
156
 
157
+ ### Conditional Questions
158
+
159
+ You can have a question only show up based on conditions. Currently the only condition is time based. Just add a key called `condition` to the question definition, then include a natural language string like `before noon` or `after 3pm`. If the condition is matched, then the question will be displayed, otherwise it will be skipped and its data entry in the JSON will be null.
160
+
161
+ Conditions can be applied to individual questions, or to entire sections, depending on where the `condition` key is placed.
162
+
148
163
  ### Naming Keys
149
164
 
150
165
  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 questions, 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:
data/bin/journal CHANGED
@@ -4,6 +4,8 @@ $LOAD_PATH.unshift File.join(__dir__, '..', 'lib')
4
4
  require 'journal-cli'
5
5
  require 'optparse'
6
6
 
7
+ trap('SIGINT') { exit! }
8
+
7
9
  module Journal
8
10
  class << self
9
11
  def usage
@@ -33,8 +35,10 @@ module Journal
33
35
  Time.now
34
36
  end
35
37
 
38
+ Journal.date = date
39
+
36
40
  if Journal.config['journals'].key?(journal)
37
- checkin = Journal::Checkin.new(journal, date)
41
+ checkin = Journal::Checkin.new(journal)
38
42
  checkin.go
39
43
  else
40
44
  puts "Journal #{journal} not found"
@@ -3,10 +3,10 @@ module Journal
3
3
  class Checkin
4
4
  attr_reader :key, :date, :data, :config, :journal, :sections, :title, :output
5
5
 
6
- def initialize(journal, date)
6
+ def initialize(journal)
7
7
  @key = journal
8
8
  @output = []
9
- @date = date
9
+ @date = Journal.date
10
10
  @date.localtime
11
11
 
12
12
  raise StandardError, "No journal with key #{@key} found" unless Journal.config['journals'].key? @key
@@ -36,7 +36,7 @@ module Journal
36
36
  end
37
37
 
38
38
  def hr
39
- @output << "\n---\n"
39
+ @output << "\n* * * * * *\n"
40
40
  end
41
41
 
42
42
  def go
@@ -58,6 +58,11 @@ module Journal
58
58
  end
59
59
 
60
60
  def save_day_one_entry
61
+ unless TTY::Which.exist?('dayone2')
62
+ Journal.notify('{br}Day One CLI not installed, no Day One entry created')
63
+ return
64
+ end
65
+ @date.localtime
61
66
  cmd = ['dayone2']
62
67
  cmd << %(-j "#{@journal['journal']}") if @journal.key?('journal')
63
68
  cmd << %(-t #{@journal['tags'].join(' ')}) if @journal.key?('tags')
@@ -128,11 +133,17 @@ module Journal
128
133
  end
129
134
 
130
135
  def print_answer(prompt, type, key, data)
136
+ return if data.nil? || !data.key?(key)
137
+
131
138
  case type
132
139
  when /^(weather|forecast)/
133
140
  header prompt
134
- @output << data[key].to_markdown
135
- hr
141
+ @output << case type
142
+ when /current$/
143
+ data[key].current
144
+ else
145
+ data[key].to_markdown
146
+ end
136
147
  when /^(int|num)/
137
148
  @output << "#{prompt}: #{data[key]} " unless data[key].nil?
138
149
  when /^date/
@@ -158,7 +169,14 @@ module Journal
158
169
  v.localtime
159
170
  data[k] = v.strftime('%Y-%m-%d %H:%M')
160
171
  when /Weather/
161
- data[k] = v.to_s
172
+ data[k] = case k
173
+ when /current$/
174
+ v.current
175
+ when /forecast$/
176
+ data[k] = v.forecast
177
+ else
178
+ data[k] = v.to_s
179
+ end
162
180
  else
163
181
  data[k] = v
164
182
  end
@@ -240,11 +258,18 @@ module Journal
240
258
  v.each do |key, value|
241
259
  result = case value.class.to_s
242
260
  when /Weather/
243
- {
244
- 'high' => value.data[:high],
245
- 'low' => value.data[:low],
246
- 'condition' => value.data[:condition]
247
- }
261
+ if key =~ /current$/
262
+ {
263
+ 'temp' => value.data[:temp],
264
+ 'condition' => value.data[:current_condition]
265
+ }
266
+ else
267
+ {
268
+ 'high' => value.data[:high],
269
+ 'low' => value.data[:low],
270
+ 'condition' => value.data[:condition]
271
+ }
272
+ end
248
273
  else
249
274
  value
250
275
  end
@@ -3,7 +3,7 @@
3
3
  module Journal
4
4
  # Individual question
5
5
  class Question
6
- attr_reader :key, :type, :min, :max, :prompt, :secondary_prompt, :gum
6
+ attr_reader :key, :type, :min, :max, :prompt, :secondary_prompt, :gum, :condition
7
7
 
8
8
  ##
9
9
  ## Initializes the given question.
@@ -20,6 +20,7 @@ module Journal
20
20
  @prompt = question['prompt'] || nil
21
21
  @secondary_prompt = question['secondary_prompt'] || nil
22
22
  @gum = TTY::Which.exist?('gum')
23
+ @condition = question.key?('condition') ? question['condition'].parse_condition : true
23
24
  end
24
25
 
25
26
  ##
@@ -27,9 +28,11 @@ module Journal
27
28
  ##
28
29
  ## @return [Number, String] the response based on @type
29
30
  ##
30
- def ask
31
+ def ask(condition)
31
32
  return nil if @prompt.nil?
32
33
 
34
+ return nil unless @condition && condition
35
+
33
36
  case @type
34
37
  when /^int/i
35
38
  read_number(integer: true)
@@ -126,6 +129,7 @@ module Journal
126
129
  ##
127
130
  ##
128
131
  def read_number_gum
132
+ trap('SIGINT') { exit! }
129
133
  res = `gum input --placeholder "#{@min}-#{@max}"`.strip
130
134
  return nil if res.strip.empty?
131
135
 
@@ -140,6 +144,7 @@ module Journal
140
144
  ## @return [Number] integer response
141
145
  ##
142
146
  def read_line_tty
147
+ trap('SIGINT') { exit! }
143
148
  reader = TTY::Reader.new
144
149
  res = reader.read_line('>> ')
145
150
  return nil if res.strip.empty?
@@ -155,6 +160,7 @@ module Journal
155
160
  ## @return [Number] integer response
156
161
  ##
157
162
  def read_line_gum(prompt)
163
+ trap('SIGINT') { exit! }
158
164
  `gum input --placeholder "#{prompt} (blank to end answer)"`
159
165
  end
160
166
 
@@ -164,6 +170,7 @@ module Journal
164
170
  ## @return [string] multiline input
165
171
  ##
166
172
  def read_mutliline_tty
173
+ trap('SIGINT') { exit! }
167
174
  reader = TTY::Reader.new
168
175
  res = reader.read_multiline
169
176
  res.join("\n")
@@ -175,6 +182,7 @@ module Journal
175
182
  ## @return [string] multiline input
176
183
  ##
177
184
  def read_multiline_gum(prompt)
185
+ trap('SIGINT') { exit! }
178
186
  `gum write --placeholder "#{prompt}" --width 80 --char-limit 0`
179
187
  end
180
188
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Journal
4
4
  class Section
5
- attr_accessor :key, :title, :questions, :answers
5
+ attr_accessor :key, :title, :questions, :answers, :condition
6
6
 
7
7
  ##
8
8
  ## Initializes the given section.
@@ -15,6 +15,7 @@ module Journal
15
15
  def initialize(section)
16
16
  @key = section['key']
17
17
  @title = section['title']
18
+ @condition = section.key?('condition') ? section['condition'].parse_condition : true
18
19
  @questions = section['questions'].map { |question| Question.new(question) }
19
20
  @questions.delete_if { |q| q.prompt.nil? }
20
21
  @answers = {}
@@ -38,9 +39,9 @@ module Journal
38
39
  res = res[key]
39
40
  end
40
41
 
41
- res[keys.last] = question.ask
42
+ res[keys.last] = question.ask(@condition)
42
43
  else
43
- @answers[question.key] = question.ask
44
+ @answers[question.key] = question.ask(@condition)
44
45
  end
45
46
  end
46
47
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ # String helpers
4
+ class ::String
5
+ def parse_condition
6
+ condition = dup
7
+ time_rx = /(?<comp>[<>=]{1,2}|before|after) +(?<time>(?:noon|midnight|[0-9]+) *(?:am|pm)?)$/i
8
+ return true unless condition =~ time_rx
9
+
10
+ now = Journal.date
11
+ m = condition.match(time_rx)
12
+ time = Chronic.parse(m['time'])
13
+ Journal.notify("{br}Invalid time string in question (#{m['time']})", exit_code: 4) unless time
14
+
15
+ case m['comp']
16
+ when /^<=$/
17
+ now <= time
18
+ when /^(<|bef)/i
19
+ now < time
20
+ when /^>=/
21
+ now >= time
22
+ when /^(>|aft)/i
23
+ now > time
24
+ end
25
+ # TODO: Other condition types
26
+ end
27
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Journal
4
- VERSION = '1.0.22'
4
+ VERSION = '1.0.24'
5
5
  end
@@ -54,6 +54,14 @@ module Journal
54
54
  }
55
55
  end
56
56
 
57
+ def current
58
+ "#{@data[:temp]} and #{@data[:current_condition]}"
59
+ end
60
+
61
+ def forecast
62
+ "#{@data[:condition]} #{@data[:high]}/#{@data[:low]}"
63
+ end
64
+
57
65
  def to_s
58
66
  "#{@data[:temp].round} and #{@data[:current_condition]} (#{@data[:high].round}/#{@data[:low].round})"
59
67
  end
@@ -61,8 +69,8 @@ module Journal
61
69
  def to_markdown
62
70
  output = []
63
71
 
64
- output << "Forecast for #{@data[:day]}: #{@data[:condition]} #{@data[:high]}/#{@data[:low]} "
65
- output << "Currently: #{@data[:temp]} and #{@data[:current_condition]}"
72
+ output << "Forecast for #{@data[:day]}: #{forecast} "
73
+ output << "Currently: #{current}"
66
74
  output << ''
67
75
 
68
76
  # Hours
data/lib/journal-cli.rb CHANGED
@@ -11,6 +11,7 @@ require 'tty-which'
11
11
  require 'tty-reader'
12
12
  require_relative 'journal-cli/version'
13
13
  require_relative 'journal-cli/color'
14
+ require_relative 'journal-cli/string'
14
15
  require_relative 'journal-cli/data'
15
16
  require_relative 'journal-cli/weather'
16
17
  require_relative 'journal-cli/checkin'
@@ -21,6 +22,8 @@ require_relative 'journal-cli/question'
21
22
  # Main Journal module
22
23
  module Journal
23
24
  class << self
25
+ attr_accessor :date
26
+
24
27
  def notify(string, debug: false, exit_code: nil)
25
28
  if debug
26
29
  $stderr.puts "{dw}#{string}{x}".x
data/src/_README.md CHANGED
@@ -55,6 +55,10 @@ This file contains a YAML definition of your journal. Each journal gets a top-le
55
55
 
56
56
  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.
57
57
 
58
+ If a question type is set to `weather.forecast`, only the predicted condition, high, and low will be included in the JSON data for the question. A full printout of hourly temps will be included in the Markdown/Day One output.
59
+
60
+ If the question type is `weather.current`, only the current condition and temperature will be recorded to the JSON, and a string containing "[TEMP] and [CONDITION]" (e.g. "64 and Sunny") will be recorded to Markdown/Day One for the question.
61
+
58
62
  ### Journal Configuration
59
63
 
60
64
  Edit the file at `~/.config/journal/journals.yaml` following this structure:
@@ -99,9 +103,12 @@ journals: # required key
99
103
  - title: Weather # Title of the section (will create template sections in Day One)
100
104
  key: weather # the key to use in the structured data, will contain all of the answers
101
105
  questions: # required key
102
- - prompt: Current weather # The prompt shown on the command line, will also become a header in the journal entries (Markdown, Day One)
106
+ - prompt: Current Weather
107
+ key: weather.current
108
+ type: weather.current
109
+ - prompt: Weather Forecast # The prompt shown on the command line, will also become a header in the journal entries (Markdown, Day One)
103
110
  key: weather.forecast # if a key contains a dot, it will create nested data, e.g. `{ 'weather': { 'forecast': data } }`
104
- type: weather # Set this to weather for weather data
111
+ type: weather.forecast # Set this to weather for weather data
105
112
  - title: Health # New section
106
113
  key: health
107
114
  questions:
@@ -144,10 +151,18 @@ A question `type` can be one of:
144
151
  - `text` or `string` will request a single-line string, submitted on return
145
152
  - `multiline` for multiline strings (opens a readline editor, use ctrl-d to save)
146
153
  - `weather` will just insert current weather data with no prompt
154
+ * `weather.forecast` will insert just the forecast
155
+ * `weather.current` will insert just the current temperature and condition
147
156
  - `number` or `float` will request numeric input, stored as a float (decimal)
148
157
  - `integer` will convert numeric input to the nearest integer
149
158
  - `date` will request a natural language date which will be parsed into a date object
150
159
 
160
+ ### Conditional Questions
161
+
162
+ You can have a question only show up based on conditions. Currently the only condition is time based. Just add a key called `condition` to the question definition, then include a natural language string like `before noon` or `after 3pm`. If the condition is matched, then the question will be displayed, otherwise it will be skipped and its data entry in the JSON will be null.
163
+
164
+ Conditions can be applied to individual questions, or to entire sections, depending on where the `condition` key is placed.
165
+
151
166
  ### Naming Keys
152
167
 
153
168
  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 questions, 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:
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.22
4
+ version: 1.0.24
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-18 00:00:00.000000000 Z
11
+ date: 2023-09-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: tty-which
@@ -238,6 +238,7 @@ files:
238
238
  - lib/journal-cli/question.rb
239
239
  - lib/journal-cli/section.rb
240
240
  - lib/journal-cli/sections.rb
241
+ - lib/journal-cli/string.rb
241
242
  - lib/journal-cli/version.rb
242
243
  - lib/journal-cli/weather.rb
243
244
  - src/_README.md