journal-cli 1.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.devcontainer/Dockerfile +11 -0
- data/.devcontainer/devcontainer.json +17 -0
- data/.editorconfig +9 -0
- data/.github/actions/setup/action.yml +34 -0
- data/.github/workflows/_build.yml +36 -0
- data/.github/workflows/_publish.yml +47 -0
- data/.github/workflows/check.yml +112 -0
- data/.github/workflows/format.yml +41 -0
- data/.github/workflows/publish.yml +56 -0
- data/.github/workflows/version.yml +52 -0
- data/.gitignore +67 -0
- data/.rspec +4 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +39 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +99 -0
- data/LICENSE.txt +20 -0
- data/README.md +127 -0
- data/README.rdoc +6 -0
- data/Rakefile +85 -0
- data/bin/journal +32 -0
- data/lib/journal-cli/checkin.rb +262 -0
- data/lib/journal-cli/data.rb +21 -0
- data/lib/journal-cli/version.rb +5 -0
- data/lib/journal-cli/weather.rb +103 -0
- data/lib/journal-cli.rb +13 -0
- data/src/_README.md +161 -0
- metadata +208 -0
data/README.md
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
|
2
|
+
A CLI for journaling to structured data, Markdown, and Day One
|
3
|
+
|
4
|
+
## Description
|
5
|
+
|
6
|
+
The `journal` command reads a journal definition and provides command line prompts to fill it out. The results are stored in a JSON database for each journal, and can optionally output to Markdown (individual files per entry, daily digest, or one large file for the journal).
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
First, you need [Gum](https://github.com/charmbracelet/gum) installed. The easiest way is with [Homebrew](https://brew.sh/):
|
11
|
+
|
12
|
+
```
|
13
|
+
$ brew install gum
|
14
|
+
```
|
15
|
+
|
16
|
+
Use RubyGems to install journal:
|
17
|
+
|
18
|
+
```
|
19
|
+
$ gem install journal-cli
|
20
|
+
```
|
21
|
+
|
22
|
+
If you run into errors, try running with the `--user-install` flag:
|
23
|
+
|
24
|
+
```
|
25
|
+
$ gem install --user-install journal-cli
|
26
|
+
```
|
27
|
+
|
28
|
+
> I've noticed lately with `asdf` that I have to run `asdf reshim` after installing gems containing binaries.
|
29
|
+
|
30
|
+
If you want to use Day One with journal, you'll need to [install the Day One CLI](https://dayoneapp.com/guides/tips-and-tutorials/command-line-interface-cli/).
|
31
|
+
|
32
|
+
## Configuration
|
33
|
+
|
34
|
+
A config must be created at `~/.config/journal/journals.yaml`:
|
35
|
+
|
36
|
+
```
|
37
|
+
$ mkdir -p ~/.config/journal
|
38
|
+
$ touch ~/.config/journal/journals.yaml
|
39
|
+
```
|
40
|
+
|
41
|
+
This file contains a YAML definition of your journal. Each journal gets a top-level key, which is what you'll specify it with on the command line. It gets a few settings, and then you define sections containing questions.
|
42
|
+
|
43
|
+
### Weather
|
44
|
+
|
45
|
+
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.
|
46
|
+
|
47
|
+
### Journal configuration
|
48
|
+
|
49
|
+
Edit the file at `~/.config/journal/journals.yaml` following this structure:
|
50
|
+
|
51
|
+
```yaml
|
52
|
+
daily: # journal key, will be used on the command line as `journal daily`
|
53
|
+
dayone: true # Enable or disable Day One integration
|
54
|
+
journal: Journal # Day One journal to add to (if using Day One integration)
|
55
|
+
markdown: daily # Type of Markdown file to create, false to skip (can be daily, individual, or digest)
|
56
|
+
title: Daily Journal # Title for every entry, date will be appended where needed
|
57
|
+
sections: # Required key
|
58
|
+
- title: null # The title for the section. If null, no section header will be created
|
59
|
+
key: journal # The key for the data collected, must be one word, alphanumeric characters and _ only
|
60
|
+
questions: # Required key
|
61
|
+
- prompt: How are you feeling? # The question to ask
|
62
|
+
key: journal # alphanumeric characters and _ only, will be nested in section key
|
63
|
+
type: multiline # The type of entry expected (numeric, string, or multiline)
|
64
|
+
```
|
65
|
+
|
66
|
+
Keys must be alphanumeric characters and `_` (underscore) only. Titles and questions can be anything, but if they contain a colon (:), you'll need to quote the string.
|
67
|
+
|
68
|
+
A more complex configuration file can contain multiple journals with multiple questions defined:
|
69
|
+
|
70
|
+
```yaml
|
71
|
+
zip: 55987 # Your zip code for weather integration
|
72
|
+
weather_api: XXXXXXXXXXXX # Your weatherapi.com API key
|
73
|
+
journals: # required key
|
74
|
+
mood: # name of the journal
|
75
|
+
journal: Mood Journal # Optional, Day One journal to add to
|
76
|
+
tags: [checkin] # Optional, array of tags to add to Day One entries
|
77
|
+
markdown: individual # Can be daily or individual, any other value will create a single file
|
78
|
+
dayone: true # true to log entries to Day One, false to skip
|
79
|
+
title: "Mood checkin %M" # The title of the entry. Use %M to insert AM or PM
|
80
|
+
sections: # required key
|
81
|
+
- title: Weather # Title of the section (will create template sections in Day One)
|
82
|
+
key: weather # the key to use in the structured data, will contain all of the answers
|
83
|
+
questions: # required key
|
84
|
+
- prompt: Current weather # The prompt shown on the command line, will also become a header in the journal entries (Markdown, Day One)
|
85
|
+
key: weather.forecast # if a key contains a dot, it will create nested data, e.g. `{ 'weather': { 'forecast': data } }`
|
86
|
+
type: weather # Set this to weather for weather data
|
87
|
+
- title: Health # New section
|
88
|
+
key: health
|
89
|
+
questions:
|
90
|
+
- prompt: Health rating
|
91
|
+
key: health.rating
|
92
|
+
type: numeric # type can be numeric, string, or multiline
|
93
|
+
min: 1 # Only need min/max definitions on numeric types (defaults 1-5)
|
94
|
+
max: 5
|
95
|
+
- prompt: Health notes
|
96
|
+
key: health.notes
|
97
|
+
type: multiline
|
98
|
+
- title: Journal # New section
|
99
|
+
key: journal
|
100
|
+
questions:
|
101
|
+
- prompt: Daily notes
|
102
|
+
key: notes
|
103
|
+
type: multiline
|
104
|
+
daily: # New journal
|
105
|
+
journal: Journal
|
106
|
+
markdown: daily
|
107
|
+
dayone: true
|
108
|
+
title: Daily Journal
|
109
|
+
sections:
|
110
|
+
- title: null
|
111
|
+
key: journal
|
112
|
+
questions:
|
113
|
+
- prompt: How are you feeling?
|
114
|
+
key: journal
|
115
|
+
type: multiline
|
116
|
+
```
|
117
|
+
|
118
|
+
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`.
|
119
|
+
|
120
|
+
## Usage
|
121
|
+
|
122
|
+
Once your configuration file is set up, you can just run `journal JOURNAL_KEY` to begin prompting for the answers to the configured questions.
|
123
|
+
|
124
|
+
Answers will always be written to `~/.local/share/journal/[KEY].json` (where [KEY] is the journal key, one data file for each journal). If you've specified `daily` or `individual` Markdown formats, entries will be written to Markdown files in `~/.local/share/journal/entries/[KEY]`, either in a `%Y-%m-%d.md` file (daily), or in timestamped individual files. If `digest` is specified for the `markdown` key, a single file will be created at `~/.local/share/journal/[KEY].md`.
|
125
|
+
|
126
|
+
At present there's no tool for querying the dataset created. You just need to parse the JSON and use your language of choice to extract the data. Numeric entries are stored as numbers, and every entry is timestamped, so you should be able to do some advanced analysis once you have enough data.
|
127
|
+
|
data/README.rdoc
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rspec/core/rake_task'
|
3
|
+
require 'rdoc/task'
|
4
|
+
require 'standard/rake'
|
5
|
+
require 'yard'
|
6
|
+
|
7
|
+
Rake::RDocTask.new do |rd|
|
8
|
+
rd.main = "README.rdoc"
|
9
|
+
rd.rdoc_files.include("README.rdoc", "lib/**/*.rb", "bin/**/*")
|
10
|
+
rd.title = 'Journal'
|
11
|
+
end
|
12
|
+
|
13
|
+
YARD::Rake::YardocTask.new do |t|
|
14
|
+
t.files = ['lib/na/*.rb']
|
15
|
+
t.options = ['--markup-provider=redcarpet', '--markup=markdown', '--no-private', '-p', 'yard_templates']
|
16
|
+
# t.stats_options = ['--list-undoc']
|
17
|
+
end
|
18
|
+
|
19
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
20
|
+
t.rspec_opts = "--pattern {spec,lib}/**/*_spec.rb"
|
21
|
+
end
|
22
|
+
|
23
|
+
task default: %i[test]
|
24
|
+
|
25
|
+
desc 'Alias for build'
|
26
|
+
task :package => :build
|
27
|
+
|
28
|
+
task test: "spec"
|
29
|
+
task lint: "standard"
|
30
|
+
task format: "standard:fix"
|
31
|
+
|
32
|
+
desc "Open an interactive ruby console"
|
33
|
+
task :console do
|
34
|
+
require "irb"
|
35
|
+
require "bundler/setup"
|
36
|
+
require "journal-cli"
|
37
|
+
ARGV.clear
|
38
|
+
IRB.start
|
39
|
+
end
|
40
|
+
|
41
|
+
desc 'Development version check'
|
42
|
+
task :ver do
|
43
|
+
gver = `git ver`
|
44
|
+
cver = IO.read(File.join(File.dirname(__FILE__), 'CHANGELOG.md')).match(/^#+ (\d+\.\d+\.\d+(\w+)?)/)[1]
|
45
|
+
res = `grep VERSION lib/journal-cli/version.rb`
|
46
|
+
version = res.match(/VERSION *= *['"](\d+\.\d+\.\d+(\w+)?)/)[1]
|
47
|
+
puts "git tag: #{gver}"
|
48
|
+
puts "version.rb: #{version}"
|
49
|
+
puts "changelog: #{cver}"
|
50
|
+
end
|
51
|
+
|
52
|
+
desc 'Changelog version check'
|
53
|
+
task :cver do
|
54
|
+
puts IO.read(File.join(File.dirname(__FILE__), 'CHANGELOG.md')).match(/^#+ (\d+\.\d+\.\d+(\w+)?)/)[1]
|
55
|
+
end
|
56
|
+
|
57
|
+
desc 'Bump incremental version number'
|
58
|
+
task :bump, :type do |_, args|
|
59
|
+
args.with_defaults(type: 'inc')
|
60
|
+
version_file = 'lib/journal-cli/version.rb'
|
61
|
+
content = IO.read(version_file)
|
62
|
+
content.sub!(/VERSION = '(?<major>\d+)\.(?<minor>\d+)\.(?<inc>\d+)(?<pre>\S+)?'/) do
|
63
|
+
m = Regexp.last_match
|
64
|
+
major = m['major'].to_i
|
65
|
+
minor = m['minor'].to_i
|
66
|
+
inc = m['inc'].to_i
|
67
|
+
pre = m['pre']
|
68
|
+
|
69
|
+
case args[:type]
|
70
|
+
when /^maj/
|
71
|
+
major += 1
|
72
|
+
minor = 0
|
73
|
+
inc = 0
|
74
|
+
when /^min/
|
75
|
+
minor += 1
|
76
|
+
inc = 0
|
77
|
+
else
|
78
|
+
inc += 1
|
79
|
+
end
|
80
|
+
|
81
|
+
$stdout.puts "At version #{major}.#{minor}.#{inc}#{pre}"
|
82
|
+
"VERSION = '#{major}.#{minor}.#{inc}#{pre}'"
|
83
|
+
end
|
84
|
+
File.open(version_file, 'w+') { |f| f.puts content }
|
85
|
+
end
|
data/bin/journal
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift File.join(__dir__, '..', 'lib')
|
4
|
+
require 'journal-cli'
|
5
|
+
|
6
|
+
raise ArgumentError, 'no journal specified' if ARGV.count.zero?
|
7
|
+
|
8
|
+
case ARGV[0]
|
9
|
+
when /(-v|--version)/
|
10
|
+
puts "journal v#{Journal::VERSION}"
|
11
|
+
Process.exit 0
|
12
|
+
when /(help|-h|--help)/
|
13
|
+
puts "journal v#{Journal::VERSION}"
|
14
|
+
puts
|
15
|
+
puts 'Usage: journal [type] [date]'
|
16
|
+
puts
|
17
|
+
puts 'Available journal types:'
|
18
|
+
config = Journal::Checkin.new
|
19
|
+
puts(config.config['journals'].keys.map { |k| "- #{k}" })
|
20
|
+
Process.exit 0
|
21
|
+
end
|
22
|
+
|
23
|
+
journal = ARGV.shift
|
24
|
+
|
25
|
+
date = if ARGV.length.positive?
|
26
|
+
Chronic.parse(ARGV.join(' '), future: false)
|
27
|
+
else
|
28
|
+
Time.now
|
29
|
+
end
|
30
|
+
checkin = Journal::Checkin.new
|
31
|
+
checkin.start(journal, date)
|
32
|
+
checkin.go
|
@@ -0,0 +1,262 @@
|
|
1
|
+
module Journal
|
2
|
+
# Main class
|
3
|
+
class Checkin
|
4
|
+
attr_reader :key, :date, :data, :config, :journal, :title, :output
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
config = File.expand_path('~/.config/journal/journals.yaml')
|
8
|
+
raise StandardError, 'No journals configured' unless File.exist?(config)
|
9
|
+
|
10
|
+
@config = YAML.load(IO.read(config))
|
11
|
+
end
|
12
|
+
|
13
|
+
def start(journal, date)
|
14
|
+
@key = journal
|
15
|
+
@output = []
|
16
|
+
@date = date
|
17
|
+
|
18
|
+
raise StandardError, "No journal with key #{@key} found" unless @config['journals'].key? @key
|
19
|
+
|
20
|
+
@journal = @config['journals'][@key]
|
21
|
+
|
22
|
+
@data = {}
|
23
|
+
meridian = @date.hour < 13 ? 'AM' : 'PM'
|
24
|
+
@title = @journal['title'].sub(/%M/, meridian)
|
25
|
+
end
|
26
|
+
|
27
|
+
def title(string)
|
28
|
+
@output << "\n## #{string}\n" unless string.nil?
|
29
|
+
end
|
30
|
+
|
31
|
+
def header(string)
|
32
|
+
@output << "\n##### #{string}\n" unless string.nil?
|
33
|
+
end
|
34
|
+
|
35
|
+
def section(string)
|
36
|
+
@output << "\n###### #{string}\n" unless string.nil?
|
37
|
+
end
|
38
|
+
|
39
|
+
def newline
|
40
|
+
@output << "\n"
|
41
|
+
end
|
42
|
+
|
43
|
+
def hr
|
44
|
+
@output << "\n---\n"
|
45
|
+
end
|
46
|
+
|
47
|
+
def ask_question(q)
|
48
|
+
res = case q['type']
|
49
|
+
when /^(int|num)/i
|
50
|
+
min = q['min'] || 1
|
51
|
+
max = q['max'] || 5
|
52
|
+
get_number(q['prompt'], min: min, max: max)
|
53
|
+
when /^(text|string|line)/i
|
54
|
+
puts q['prompt']
|
55
|
+
add_prompt = q['secondary_prompt'] || nil
|
56
|
+
get_line(q['prompt'], add_prompt: add_prompt)
|
57
|
+
when /^(weather|forecast)/i
|
58
|
+
Weather.new(@config['weather_api'], @config['zip'])
|
59
|
+
when /^multi/
|
60
|
+
puts q['prompt']
|
61
|
+
add_prompt = q['secondary_prompt'] || nil
|
62
|
+
get_lines(q['prompt'], add_prompt: add_prompt)
|
63
|
+
end
|
64
|
+
|
65
|
+
res
|
66
|
+
end
|
67
|
+
|
68
|
+
def go
|
69
|
+
results = Data.new(@journal['questions'])
|
70
|
+
@journal['sections'].each do |s|
|
71
|
+
results[s['key']] = {
|
72
|
+
title: s['title'],
|
73
|
+
answers: {}
|
74
|
+
}
|
75
|
+
|
76
|
+
s['questions'].each do |q|
|
77
|
+
if q['key'] =~ /\./
|
78
|
+
res = results[s['key']][:answers]
|
79
|
+
keys = q['key'].split(/\./)
|
80
|
+
keys.each_with_index do |key, i|
|
81
|
+
next if i == keys.count - 1
|
82
|
+
|
83
|
+
res[key] = {} unless res.key?(key)
|
84
|
+
res = res[key]
|
85
|
+
end
|
86
|
+
|
87
|
+
res[keys.last] = ask_question(q)
|
88
|
+
else
|
89
|
+
results[s['key']][:answers][q['key']] = ask_question(q)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
@data = results
|
95
|
+
|
96
|
+
if @journal['dayone']
|
97
|
+
cmd = ['dayone2']
|
98
|
+
cmd << %(-j "#{@journal['journal']}") if @journal.key?('journal')
|
99
|
+
cmd << %(-t #{@journal['tags'].join(' ')}) if @journal.key?('tags')
|
100
|
+
cmd << %(-date "#{@date.strftime('%Y-%m-%d %I:%M %p')}")
|
101
|
+
`echo #{Shellwords.escape(to_markdown)} | #{cmd.join(' ')} -- new`
|
102
|
+
end
|
103
|
+
|
104
|
+
if @journal['markdown']
|
105
|
+
if @journal['markdown'] =~ /^da(y|ily)/
|
106
|
+
dir = File.expand_path("~/.local/share/journal/entries/#{@key}")
|
107
|
+
FileUtils.mkdir_p(dir) unless File.directory?(dir)
|
108
|
+
filename = "#{@date.strftime('%Y-%m-%d')}.md"
|
109
|
+
target = File.join(dir, filename)
|
110
|
+
if File.exist? target
|
111
|
+
File.open(target, 'a') { |f| f.puts to_markdown(yaml: false, title: true, date: false, time: true) }
|
112
|
+
else
|
113
|
+
File.open(target, 'w') { |f| f.puts to_markdown(yaml: true, title: true, date: false, time: true) }
|
114
|
+
end
|
115
|
+
elsif @journal['markdown'] =~ /^(ind|separate)/
|
116
|
+
dir = File.expand_path("~/.local/share/journal/entries/#{@key}")
|
117
|
+
FileUtils.mkdir_p(dir) unless File.directory?(dir)
|
118
|
+
filename = @date.strftime('%Y-%m-%d_%H:%M.md')
|
119
|
+
File.open(File.join(dir, filename), 'w') { |f| f.puts to_markdown(yaml: true, title: true) }
|
120
|
+
else
|
121
|
+
dir = File.expand_path('~/.local/share/journal/entries/')
|
122
|
+
FileUtils.mkdir_p(dir) unless File.directory?(dir)
|
123
|
+
filename = "#{@key}.md"
|
124
|
+
File.open(File.join(dir, filename), 'a') do |f|
|
125
|
+
f.puts
|
126
|
+
f.puts "## #{@title} #{@date.strftime('%x %X')}"
|
127
|
+
f.puts
|
128
|
+
f.puts to_markdown(yaml: false, title: false)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
save_data
|
134
|
+
end
|
135
|
+
|
136
|
+
def print_answer(prompt, type, key, data)
|
137
|
+
case type
|
138
|
+
when /^(weather|forecast)/
|
139
|
+
header prompt
|
140
|
+
@output << data[key].to_markdown
|
141
|
+
hr
|
142
|
+
when /^(int|num)/
|
143
|
+
@output << "#{prompt}: #{data[key]} " unless data[key].nil?
|
144
|
+
else
|
145
|
+
unless data[key].strip.empty?
|
146
|
+
header prompt
|
147
|
+
@output << data[key]
|
148
|
+
end
|
149
|
+
hr
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def to_markdown(yaml: false, title: false, date: false, time: false)
|
154
|
+
@output = []
|
155
|
+
|
156
|
+
if yaml
|
157
|
+
@output << <<~EOYAML
|
158
|
+
---
|
159
|
+
title: #{@title}
|
160
|
+
date: #{@date.strftime('%x %X')}
|
161
|
+
---
|
162
|
+
|
163
|
+
EOYAML
|
164
|
+
end
|
165
|
+
|
166
|
+
if title
|
167
|
+
if date || time
|
168
|
+
fmt = ''
|
169
|
+
fmt += '%x' if date
|
170
|
+
fmt += '%X' if time
|
171
|
+
title "#{@title} #{@date.strftime(fmt)}"
|
172
|
+
else
|
173
|
+
title @title
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
@journal['sections'].each do |s|
|
178
|
+
section s['title']
|
179
|
+
|
180
|
+
s['questions'].each do |q|
|
181
|
+
if q['key'] =~ /\./
|
182
|
+
res = @data[s['key']][:answers].dup
|
183
|
+
keys = q['key'].split(/\./)
|
184
|
+
keys.each_with_index do |key, i|
|
185
|
+
next if i == keys.count - 1
|
186
|
+
|
187
|
+
res = res[key]
|
188
|
+
end
|
189
|
+
print_answer(q['prompt'], q['type'], keys.last, res)
|
190
|
+
else
|
191
|
+
print_answer(q['prompt'], q['type'], q['key'], @data[s['key']][:answers])
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
@output.join("\n")
|
197
|
+
end
|
198
|
+
|
199
|
+
def save_data
|
200
|
+
db = File.expand_path("~/.local/share/journal/#{@key}.json")
|
201
|
+
data = if File.exist?(db)
|
202
|
+
JSON.parse(IO.read(db))
|
203
|
+
else
|
204
|
+
[]
|
205
|
+
end
|
206
|
+
date = @date.utc
|
207
|
+
output = {}
|
208
|
+
|
209
|
+
@data.each do |k, v|
|
210
|
+
v[:answers].each do |q, a|
|
211
|
+
if a.is_a? Hash
|
212
|
+
output[q] = {}
|
213
|
+
a.each do |key, value|
|
214
|
+
output[q][key] = case value.class.to_s
|
215
|
+
when /Weather/
|
216
|
+
{ 'high' => value.data[:high], 'low' => value.data[:low], 'condition' => value.data[:condition] }
|
217
|
+
else
|
218
|
+
value
|
219
|
+
end
|
220
|
+
end
|
221
|
+
else
|
222
|
+
output[q] = a
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
data << { 'date' => date, 'data' => output }
|
227
|
+
File.open(db, 'w') { |f| f.puts JSON.pretty_generate(data) }
|
228
|
+
end
|
229
|
+
|
230
|
+
def get_number(prompt, min: 1, max: 5)
|
231
|
+
puts "#{prompt} (#{min}-#{max})"
|
232
|
+
res = `gum input --placeholder "#{prompt} (#{min}-#{max})"`.strip
|
233
|
+
return nil if res.strip.empty?
|
234
|
+
|
235
|
+
res = res.to_i
|
236
|
+
|
237
|
+
res = get_number(prompt, min: min, max: max) if res < min || res > max
|
238
|
+
res
|
239
|
+
end
|
240
|
+
|
241
|
+
def get_line(prompt, add_prompt: nil)
|
242
|
+
output = []
|
243
|
+
puts prompt
|
244
|
+
line = `gum input --placeholder "#{prompt} (blank to end editing)"`
|
245
|
+
return output.join("\n") if line =~ /^ *$/
|
246
|
+
|
247
|
+
output << line
|
248
|
+
output << get_line(add_prompt, add_prompt: add_prompt) if add_prompt
|
249
|
+
output.join("\n")
|
250
|
+
end
|
251
|
+
|
252
|
+
def get_lines(prompt, add_prompt: nil)
|
253
|
+
output = []
|
254
|
+
line = `gum write --placeholder "#{prompt}" --width 80 --char-limit 0`
|
255
|
+
return output.join("\n") if line.strip.empty?
|
256
|
+
|
257
|
+
output << line
|
258
|
+
output << get_lines(add_prompt, add_prompt: add_prompt) if add_prompt
|
259
|
+
output.join("\n")
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Journal
|
4
|
+
# Data handler
|
5
|
+
class Data < Hash
|
6
|
+
attr_reader :questions
|
7
|
+
|
8
|
+
def initialize(questions)
|
9
|
+
@questions = questions
|
10
|
+
super
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_data
|
14
|
+
output = {}
|
15
|
+
@questions.each do |q|
|
16
|
+
output[q['key']] = self[q['key']]
|
17
|
+
end
|
18
|
+
output
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Journal
|
4
|
+
class Weather
|
5
|
+
attr_reader :data
|
6
|
+
|
7
|
+
def initialize(api, zip)
|
8
|
+
res = `curl -SsL 'http://api.weatherapi.com/v1/forecast.json?key=#{api}&q=#{zip}&aqi=no'`
|
9
|
+
data = JSON.parse(res)
|
10
|
+
|
11
|
+
raise StandardError, 'invalid JSON response' if data.nil?
|
12
|
+
|
13
|
+
raise StandardError, 'missing conditions' unless data['current']
|
14
|
+
|
15
|
+
curr_temp = data['current']['temp_f']
|
16
|
+
curr_condition = data['current']['condition']['text']
|
17
|
+
|
18
|
+
raise StandardError, 'mising forecast' unless data['forecast']
|
19
|
+
|
20
|
+
forecast = data['forecast']['forecastday'][0]
|
21
|
+
|
22
|
+
day = forecast['date']
|
23
|
+
high = forecast['day']['maxtemp_f']
|
24
|
+
low = forecast['day']['mintemp_f']
|
25
|
+
condition = forecast['day']['condition']['text']
|
26
|
+
|
27
|
+
hours = forecast['hour']
|
28
|
+
temps = [
|
29
|
+
{ temp: hours[8]['temp_f'], condition: hours[8]['condition']['text'] },
|
30
|
+
{ temp: hours[10]['temp_f'], condition: hours[10]['condition']['text'] },
|
31
|
+
{ temp: hours[12]['temp_f'], condition: hours[12]['condition']['text'] },
|
32
|
+
{ temp: hours[14]['temp_f'], condition: hours[14]['condition']['text'] },
|
33
|
+
{ temp: hours[16]['temp_f'], condition: hours[16]['condition']['text'] },
|
34
|
+
{ temp: hours[18]['temp_f'], condition: hours[18]['condition']['text'] },
|
35
|
+
{ temp: hours[19]['temp_f'], condition: hours[20]['condition']['text'] }
|
36
|
+
]
|
37
|
+
|
38
|
+
@data = {
|
39
|
+
day: day,
|
40
|
+
high: high,
|
41
|
+
low: low,
|
42
|
+
temp: curr_temp,
|
43
|
+
condition: condition,
|
44
|
+
current_condition: curr_condition,
|
45
|
+
temps: temps
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_data
|
50
|
+
{
|
51
|
+
high: high,
|
52
|
+
low: low,
|
53
|
+
condition: curr_condition
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
def to_markdown
|
58
|
+
output = []
|
59
|
+
|
60
|
+
output << "Forecast for #{@data[:day]}: #{@data[:condition]} #{@data[:high]}/#{@data[:low]} "
|
61
|
+
output << "Currently: #{@data[:temp]} and #{@data[:current_condition]}"
|
62
|
+
output << ''
|
63
|
+
|
64
|
+
# Hours
|
65
|
+
hours_text = %w[8am 10am 12pm 2pm 4pm 6pm 8pm]
|
66
|
+
step_out = ['|']
|
67
|
+
@data[:temps].each_with_index do |_h, i|
|
68
|
+
width = @data[:temps][i][:condition].length + 1
|
69
|
+
step_out << format("%#{width}s |", hours_text[i])
|
70
|
+
end
|
71
|
+
|
72
|
+
output << step_out.join('')
|
73
|
+
|
74
|
+
# table separator
|
75
|
+
step_out = ['|']
|
76
|
+
@data[:temps].each do |temp|
|
77
|
+
width = temp[:condition].length + 1
|
78
|
+
step_out << "#{'-' * width}-|"
|
79
|
+
end
|
80
|
+
|
81
|
+
output << step_out.join('')
|
82
|
+
|
83
|
+
# Conditions
|
84
|
+
step_out = ['|']
|
85
|
+
@data[:temps].each do |temp|
|
86
|
+
step_out << format(' %s |', temp[:condition])
|
87
|
+
end
|
88
|
+
|
89
|
+
output << step_out.join('')
|
90
|
+
|
91
|
+
# Temps
|
92
|
+
step_out = ['|']
|
93
|
+
@data[:temps].each do |temp|
|
94
|
+
width = temp[:condition].length + 1
|
95
|
+
step_out << format("%#{width}s |", temp[:temp])
|
96
|
+
end
|
97
|
+
|
98
|
+
output << step_out.join('')
|
99
|
+
|
100
|
+
output.join("\n")
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
data/lib/journal-cli.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'time'
|
4
|
+
require 'shellwords'
|
5
|
+
require 'json'
|
6
|
+
require 'yaml'
|
7
|
+
require 'chronic'
|
8
|
+
require 'fileutils'
|
9
|
+
|
10
|
+
require_relative "journal-cli/version"
|
11
|
+
require_relative "journal-cli/data"
|
12
|
+
require_relative "journal-cli/weather"
|
13
|
+
require_relative "journal-cli/checkin"
|