diary-ruby 0.1.0

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.
data/README.md ADDED
@@ -0,0 +1,56 @@
1
+ **DON'T USE THIS YET**
2
+
3
+ # diary-ruby
4
+
5
+ A toy CLI app for me. Playing with encryption and the `$EDITOR` env var.
6
+
7
+ ## Usage
8
+
9
+ $ gem install diary-ruby
10
+ $ diaryrb
11
+
12
+ TYPE TYPE TYPE
13
+
14
+ Now you have a diary too.
15
+
16
+ You can create a config file to make it easier to manage multiple diaries.
17
+ `diaryrb` looks for a config file in ~/.diaryrb/config.yaml. Valid config
18
+ options are `passphrase` and `path`. For example:
19
+
20
+ ```yaml
21
+ default:
22
+ path: "/Users/yername/Dropbox/Documents/notes.diary"
23
+
24
+ secure.store:
25
+ passphrase: "this is the passphrase, I put it in a config file! 82acf427f94c513f8d7f81995a549361089d903f"
26
+ path: "~/secure.secret.diary"
27
+ ```
28
+
29
+ If a config file is used, diaryrb uses the -d option to pick a diary by name:
30
+
31
+ $ diaryrb -d default
32
+
33
+ would load the diary at `/Users/.../notes.diary`, while
34
+
35
+ $ diaryrb -d mynotes
36
+
37
+ would create a new diary file named "mynotes" in the current directory.
38
+
39
+ ## Development
40
+
41
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run
42
+ `rake test` to run the tests. You can also run `bin/console` for an interactive
43
+ prompt that will allow you to experiment.
44
+
45
+ To install this gem onto your local machine, run `bundle exec rake install`. To
46
+ release a new version, update the version number in `version.rb`, and then run
47
+ `bundle exec rake release`, which will create a git tag for the version, push
48
+ git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
49
+
50
+ ## Contributing
51
+
52
+ Bug reports and pull requests are welcome on GitHub at
53
+ https://github.com/abachman/diary-ruby. This project is intended to be a
54
+ safe, welcoming space for collaboration, and contributors are expected to
55
+ adhere to the [Contributor Covenant](contributor-covenant.org) code of conduct.
56
+
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ end
9
+
10
+ task :default => :test
data/TODO.md ADDED
@@ -0,0 +1,4 @@
1
+ - [x] add "last post" date to top of template
2
+ - [ ] add search to exe/diaryrb
3
+ - [ ] add date range filters to exe/diaryrb
4
+ - [ ] add edit functionality
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "diary-ruby"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,39 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'diary-ruby/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "diary-ruby"
8
+ spec.version = Diary::VERSION
9
+ spec.authors = ["Adam Bachman"]
10
+ spec.email = ["adam.bachman@gmail.com"]
11
+ spec.licenses = ['GPL-3.0']
12
+
13
+ spec.summary = %q{A CLI diary: diaryrb}
14
+ spec.description = %q{A command line diary: diaryrb}
15
+ spec.homepage = "https://github.com/abachman/diary-ruby"
16
+
17
+ # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
18
+ # delete this section to allow pushing this gem to any host.
19
+ if spec.respond_to?(:metadata)
20
+ spec.metadata['allowed_push_host'] = "https://rubygems.org"
21
+ else
22
+ raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
23
+ end
24
+
25
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
26
+ spec.bindir = "exe"
27
+ spec.executables = ['diaryrb']
28
+ spec.require_paths = ["lib"]
29
+
30
+ spec.add_development_dependency "bundler", "~> 1.10"
31
+ spec.add_development_dependency "rake", "~> 10.0"
32
+ spec.add_development_dependency "minitest", '~> 5.8'
33
+ spec.add_development_dependency "minitest-display", '~> 0.3'
34
+
35
+ spec.add_dependency 'sinatra', '~> 1.4'
36
+ spec.add_dependency 'rdiscount', '~> 2.1'
37
+ spec.add_dependency 'slop', '~> 4.2'
38
+ spec.add_dependency 'launchy', '~> 2.4'
39
+ end
data/exe/diaryrb ADDED
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'diary-ruby'
4
+ require 'slop'
5
+ require 'launchy'
6
+ require 'thread'
7
+
8
+ DEFAULT_DIARY = "diaryrb.store"
9
+
10
+ prompt_for_password = false
11
+ opts = Slop.parse do |o|
12
+ # o.string '-c', '--configuration', 'config file location'
13
+ o.string '-d', '--diary', "choose diary storage file (leave blank for default, #{DEFAULT_DIARY})"
14
+ o.string '-p', '--passphrase', 'Use given encryption passphrase or prompt if option is used but no passphrase is given.', default: false do |v|
15
+ if v.start_with?('-') || v.nil? || v.strip.size == 0
16
+ prompt_for_password = true
17
+ end
18
+ end
19
+
20
+ # usage modes
21
+ o.separator ''
22
+ o.separator "Actions (can't be used in combination):"
23
+ o.string '-e', '--edit', 'edit a specific post'
24
+ o.bool '-l', '--list', 'list all posts by date'
25
+ o.bool '-s', '--serve', 'start Diary webserver'
26
+
27
+ o.separator ''
28
+ o.separator 'Other options:'
29
+ o.bool '-v', '--verbose', 'enable verbose mode'
30
+ o.on '-V', '--version', 'print the version and quit' do
31
+ puts "diaryrb #{ Diary::VERSION }"
32
+ exit
33
+ end
34
+ o.on '-h', '--help', 'show help and quit' do
35
+ puts o
36
+ exit
37
+ end
38
+ end
39
+
40
+ # Global config options
41
+ Diary::Configuration.verbose = opts.verbose?
42
+
43
+ # Get diary name from configuration or command line
44
+ _diary = DEFAULT_DIARY
45
+ if opts[:diary]
46
+ # ALWAYS prefer the given argument
47
+ _diary = opts[:diary]
48
+ elsif Diary::Configuration.exists?
49
+ if Diary::Configuration.has_diary_config?('default')
50
+ Diary.debug "LOADING default DIARY"
51
+ _diary = 'default'
52
+ Diary::Configuration.load_config('default')
53
+ else
54
+ Diary.debug "CONFIG EXISTS, NO DEFAULT. LOADING DIARY #{ _diary }"
55
+ end
56
+ end
57
+
58
+ Diary::Configuration.current_diary = _diary
59
+
60
+ _passphrase = nil
61
+ if prompt_for_password
62
+ require 'io/console'
63
+ print "Enter passphrase (leave blank for none): "
64
+ _passphrase = STDIN.noecho {|io| io.gets}.chomp
65
+ elsif opts[:passphrase] && opts[:passphrase].strip.size > 0
66
+ _passphrase = opts[:passphrase]
67
+ elsif ENV['PASSPHRASE']
68
+ _passphrase = ENV['PASSPHRASE']
69
+ elsif Diary::Configuration.passphrase
70
+ _passphrase = Diary::Configuration.passphrase
71
+ end
72
+
73
+ diary_path = Diary::Configuration.path || Diary::Configuration.current_diary
74
+ Diary.debug "LOADING DIARY #{ Diary::Configuration.current_diary } AT PATH #{ diary_path }"
75
+ if _passphrase.nil? || _passphrase.size == 0
76
+ Diary.debug "LOADING WITH NO PASSPHRASE!"
77
+ $store = Diary::Store.new(diary_path)
78
+ else
79
+ Diary.debug "LOADING WITH PASSPHRASE #{ _passphrase.gsub(/./, '*') }"
80
+ $store = Diary::SecureStore.new(diary_path, _passphrase)
81
+ end
82
+
83
+ # this is like rake db:migrate
84
+ $store.write do |db|
85
+ db[:entries] ||= []
86
+ db[:entries] = db[:entries].compact.uniq.sort
87
+ db[:tags] ||= []
88
+ end
89
+
90
+ if opts.list?
91
+ entries = $store.read(:entries)
92
+ puts ''
93
+
94
+ if entries.nil? || entries.size == 0
95
+ puts "No entries"
96
+ exit
97
+ else
98
+ entries.uniq.sort.reverse.each do |entry_key|
99
+ puts "#{ entry_key } #{ $store.read(entry_key)[:text][0..40].gsub("\n", ' ') }..."
100
+ end
101
+ end
102
+ elsif opts.serve?
103
+ Diary::Server.store = $store
104
+ t = Thread.new do
105
+ Diary::Server.run!
106
+ end
107
+ Launchy.open('http://localhost:4567')
108
+ t.join
109
+ else
110
+ def finish(file)
111
+ file.close
112
+ file.unlink # deletes the temp file
113
+ end
114
+
115
+ def parse_and_store(file)
116
+ diary_entry = Diary::Parser.parse_file(file)
117
+ $store.write_entry(diary_entry)
118
+ end
119
+
120
+ # create a tempfile to store entry in progress in EDITOR
121
+ file = Tempfile.new(['diary', '.md'])
122
+ file.sync = true
123
+
124
+ # default new entry
125
+ entry_source = {
126
+ day: Time.now.strftime("%F"),
127
+ time: Time.now.strftime("%T"),
128
+ tags: "",
129
+ title: "",
130
+ text: "text goes here"
131
+ }
132
+
133
+ # if --edit option is used with a valid entry, load it
134
+ if opts[:edit] && (entry_hash = $store.read(opts[:edit]))
135
+ entry = Diary::Entry.from_store(entry_hash)
136
+ entry_source = entry.to_hash
137
+ entry_source[:tags] = entry_source[:tags].join(', ')
138
+ end
139
+
140
+ # prepare entry and launch editor
141
+ tmpl = Diary::Entry.generate(entry_source, $store)
142
+ file.write(tmpl)
143
+
144
+ ed = "vim -f"
145
+ if ENV['DIARY_EDITOR']
146
+ ed = ENV['DIARY_EDITOR']
147
+ elsif ENV['EDITOR']
148
+ ed = ENV['EDITOR']
149
+ end
150
+
151
+ pid = fork do
152
+ exec("#{ ed } #{ file.path }")
153
+ end
154
+
155
+ # wait for child to finish, exit when the editor exits
156
+ exit_signal = Queue.new
157
+
158
+ trap("CLD") do
159
+ Diary.log "CHILD PID #{pid} TERMINATED"
160
+ exit_signal.push(true)
161
+ end
162
+
163
+ Diary.log "WAITING FOR EDITOR IN PROCESS #{ pid }"
164
+
165
+ # Polling based observation of tempfile, save to Store whenever Entry is updated
166
+ omtime = File.mtime(file.path)
167
+ while true do
168
+ quitter = exit_signal.pop(true) rescue nil
169
+
170
+ mtime = File.mtime(file.path)
171
+ if mtime != omtime
172
+ Diary.log "FILE MODIFIED, UPDATING ENTRY"
173
+ parse_and_store(file)
174
+ omtime = mtime
175
+ end
176
+
177
+ if quitter
178
+ Diary.debug "QUIT SIGNAL RECEIVED"
179
+ # parse_and_store(file)
180
+ Diary.debug "CLEANING UP..."
181
+ finish(file)
182
+ Diary.debug "EXIT"
183
+ exit 0
184
+ end
185
+
186
+ sleep 1
187
+ end
188
+ end
data/lib/diary-ruby.rb ADDED
@@ -0,0 +1,20 @@
1
+ require "diary-ruby/version"
2
+ require "diary-ruby/store"
3
+ require "diary-ruby/entry"
4
+ require "diary-ruby/parser"
5
+ require "diary-ruby/configuration"
6
+ require "diary-ruby/server/server"
7
+
8
+ module Diary
9
+ def self.log(message)
10
+ if Diary::Configuration.verbose
11
+ puts message
12
+ end
13
+ end
14
+
15
+ def self.debug(message)
16
+ if Diary::Configuration.verbose
17
+ puts message
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,88 @@
1
+ require 'pathname'
2
+ require 'yaml'
3
+
4
+ DIARY_DIR = ".diaryrb"
5
+
6
+ module Diary
7
+ # Read only app configuration
8
+ #
9
+ class Configuration
10
+ class Error < StandardError
11
+ end
12
+
13
+ class << self
14
+ attr_accessor :current_diary
15
+ attr_accessor :verbose
16
+
17
+ # Find config directory starting at current dir and then moving up the tree
18
+ def config_dir(dir = Pathname.new("."))
19
+ app_config_dir = dir + DIARY_DIR
20
+
21
+ if dir.children.include?(app_config_dir)
22
+ app_config_dir.expand_path
23
+ else
24
+ return nil if dir.expand_path.root?
25
+
26
+ # go up the stack
27
+ config_dir(dir.parent)
28
+ end
29
+ end
30
+
31
+ def method_missing(method)
32
+ config[method.to_s]
33
+ end
34
+
35
+ def exists?
36
+ !config_dir.nil?
37
+ end
38
+
39
+ def has_diary_config?(diary_identifier)
40
+ load_global_settings
41
+ global_settings.has_key?(diary_identifier)
42
+ end
43
+
44
+ def global_settings
45
+ @global_settings || {}
46
+ end
47
+
48
+ # load a specific diary
49
+ def load_config(diary_identifier)
50
+ @config = load_config_for_diary(diary_identifier)
51
+ end
52
+
53
+ # default to current_diary
54
+ def config
55
+ @config ||= load_config_for_diary(current_diary)
56
+ end
57
+
58
+ private
59
+
60
+ def load_config_for_diary(diary_identifier)
61
+ if !exists?
62
+ # no config file exists, build empty configuration options starting now
63
+ {}
64
+ else
65
+ load_global_settings
66
+ if global_settings.has_key?(diary_identifier)
67
+ global_settings[diary_identifier]
68
+ else
69
+ # configuration for this diary doesn't exist, build empty
70
+ # configuration options starting now
71
+ {}
72
+ end
73
+ end
74
+ end
75
+
76
+ def load_global_settings
77
+ @global_settings ||= begin
78
+ if exists?
79
+ cf = File.join(config_dir, 'config.yaml')
80
+ YAML.load(File.open(cf))
81
+ else
82
+ {}
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,85 @@
1
+ TEMPLATE = "# last entry posted at %{last_update_at}
2
+
3
+ DAY %{day}
4
+ TIME %{time}
5
+ TAGS %{tags}
6
+ TITLE %{title}
7
+
8
+ ---
9
+
10
+ %{text}
11
+ "
12
+
13
+ require 'rdiscount'
14
+
15
+ module Diary
16
+ class Entry
17
+ attr_accessor :version, :day, :time, :tags, :text, :title, :key
18
+
19
+ CURRENT_VERSION = 1
20
+
21
+ def self.from_store(record)
22
+ if record[:version] == 1
23
+ self.new(
24
+ record[:version],
25
+ day: record[:day],
26
+ time: record[:time],
27
+ tags: record[:tags],
28
+ text: record[:text],
29
+ title: record[:title],
30
+ key: record[:key],
31
+ )
32
+ end
33
+ end
34
+
35
+ def self.keygen(day, time)
36
+ "%s-%s" % [day, time]
37
+ end
38
+
39
+ def self.generate(options={}, store)
40
+ options[:last_update_at] = store.read(:last_update_at)
41
+
42
+ # convert Arrays to dumb CSV
43
+ options.each do |(k, v)|
44
+ if v.is_a?(Array)
45
+ options[k] = v.join(', ')
46
+ end
47
+ end
48
+
49
+ TEMPLATE % options
50
+ end
51
+
52
+ def initialize(version, options={})
53
+ @version = version || CURRENT_VERSION
54
+
55
+ @day = options[:day]
56
+ @time = options[:time]
57
+ @tags = options[:tags] || []
58
+ @text = options[:text]
59
+ @title = options[:title]
60
+
61
+ if options[:key].nil?
62
+ @key = Entry.keygen(day, time)
63
+ else
64
+ @key = options[:key]
65
+ end
66
+ end
67
+
68
+ def formatted_text
69
+ RDiscount.new(text).to_html
70
+ end
71
+
72
+ def to_hash
73
+ {
74
+ version: version,
75
+ day: day,
76
+ time: time,
77
+ tags: tags,
78
+ text: text,
79
+ title: '',
80
+ key: key,
81
+ }
82
+ end
83
+ end
84
+ end
85
+