standup_md 0.2.1 → 0.3.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.
@@ -1,575 +1,58 @@
1
1
  # frozen_string_literal: true
2
- require 'yaml'
3
- require 'date'
4
- require 'fileutils'
2
+
5
3
  require_relative 'standup_md/version'
4
+ require_relative 'standup_md/file'
5
+ require_relative 'standup_md/entry'
6
+ require_relative 'standup_md/entry_list'
7
+ require_relative 'standup_md/cli'
8
+ require_relative 'standup_md/config'
6
9
 
7
10
  ##
8
11
  # The class for handing reading/writing of entries.
9
- #
10
- # @example
11
- # su = StandupMD.new
12
- class StandupMD
12
+ module StandupMD
13
+ @config_file_loaded = false
13
14
 
14
15
  ##
15
- # Convenience method for calling +new+ + +load+. Accepts a +YAML+ config file
16
- # as an argument, and yields the standup instance if a block is given.
17
- #
18
- # @param [String] config_file File path to config file.
16
+ # Shorthand for +StanupMD::Cli+
19
17
  #
20
- # @example
21
- # su = StandupMD.load(bullet_character: '*')
22
- def self.load(config_file = nil)
23
- s = new(config_file)
24
- yield s if block_given?
25
- s.load
18
+ # @return [StanupMD::Cli]
19
+ def self.config
20
+ @config ||= StandupMD::Config.new
26
21
  end
27
22
 
28
- # :section: Attributes that aren't settable by user, but are gettable.
29
-
30
- ##
31
- # The string that will be used for the entry headers.
32
- #
33
- # @return [String]
34
- attr_reader :header
35
-
36
- ##
37
- # The file name should equal file_name_format parsed by Date.strftime.
38
- # The default is +Date.today.strftime('%Y_%m.md')+
39
- #
40
- # @return [String]
41
- #
42
- # @example
43
- # su = StandupMD.new { |s| s.file_name_format = '%y_%m.markdown' }
44
- # su.file
45
- # # => Users/johnsmith/.cache/standup_md/20_04.markdown
46
- attr_reader :file
47
-
48
- ##
49
- # The file that contains the previous entry. If previous entry was same month,
50
- # previous_file will be the same as file. If previous entry was last month,
51
- # and a file exists for last month, previous_file is last month's file.
52
- # If neither is true, returns an empty string.
53
- #
54
- # @return [String]
55
- #
56
- # @example
57
- # # Assuming the current month is April, 2020
58
- #
59
- # Dir.entries(su.directory)
60
- # # => []
61
- # su = StandupMD.new
62
- # su.previous_file
63
- # # => ''
64
- #
65
- # Dir.entries(su.directory)
66
- # # => ['2020_03.md']
67
- # su = StandupMD.new
68
- # su.previous_file
69
- # # => '2020_03.md'
70
- #
71
- # Dir.entries(su.directory)
72
- # # => ['2020_03.md', '2020_04.md']
73
- # su = StandupMD.new
74
- # su.previous_file
75
- # # => '2020_04.md'
76
- attr_reader :previous_file
77
-
78
- ##
79
- # The entry for today's date as a hash. If +file+ already has an entry for
80
- # today, it will be read and used as +current_entry+. If there is no entry
81
- # for today, one should be generated from scaffolding.
82
- #
83
- # @return [Hash]
84
- #
85
- # @example
86
- # StandupMD.new.current_entry
87
- # # => {
88
- # # '2020-04-02' => {
89
- # # 'Previous' => ['Task from yesterday'],
90
- # # 'Current' => ["<!-- ADD TODAY'S WORK HERE -->"],
91
- # # 'Impediments' => ['None'],
92
- # # 'Notes' => [],
93
- # # }
94
- # # }
95
- attr_reader :current_entry
96
-
97
- ##
98
- # All previous entry for the same month as today. If it's the first day of
99
- # the month, +all_previous_entries+ will be all of last month's entries. They
100
- # will be a hash in the same format as +current_entry+.
101
- #
102
- # @return [Hash]
103
- attr_reader :all_previous_entries
104
-
105
- ##
106
- # Current entry plus all previous entries. This will be a hash in the same
107
- # format at +current_entry+ and +all_previous_entries+.
108
- #
109
- # @return [Hash]
110
- attr_reader :all_entries
111
-
112
- # :section: Attributes that are settable by the user, but have custom setters.
113
-
114
- ##
115
- # The configuration file. Default is +nil+. If set to a string, and the file
116
- # exists, it is used to set options.
117
- #
118
- # @return [String] file path
119
- attr_reader :config_file
120
-
121
- ##
122
- # The options from +config_file+ as a hash.
123
- #
124
- # @return [Hash] Options from +config_file+
125
- attr_reader :config
126
-
127
- ##
128
- # The directory where the markdown files are kept.
129
- #
130
- # @return [String]
131
- #
132
- # @default
133
- # File.join(ENV['HOME'], '.cache', 'standup_md')
134
- attr_reader :directory
135
-
136
- ##
137
- # Array of tasks for today. This is the work expected to be performed today.
138
- # Default is an empty array, but when writing to file, the default is
139
- #
140
- # @return [Array]
141
- #
142
- # @default
143
- # ["<!-- ADD TODAY'S WORK HERE -->"]
144
- attr_reader :current_entry_tasks
145
-
146
- ##
147
- # Array of impediments for today's entry.
148
- #
149
- # @return [Array]
150
- attr_reader :impediments
151
-
152
- ##
153
- # Character used as bullets for list entries.
154
- #
155
- # @return [String] either - (dash) or * (asterisk)
156
- attr_reader :bullet_character
157
-
158
- ##
159
- # Number of octothorps that should preface entry headers.
160
- #
161
- # @return [Integer] between 1 and 5
162
- attr_reader :header_depth
163
-
164
- ##
165
- # Number of octothorps that should preface sub-headers.
166
- #
167
- # @return [Integer] between 2 and 6
168
- attr_reader :sub_header_depth
169
-
170
- ##
171
- # The tasks from the previous task's "Current" section.
172
- #
173
- # @return [Array]
174
- attr_reader :previous_entry_tasks
175
-
176
- ##
177
- # Array of notes to add to today's entry.
178
- #
179
- # @return [Array]
180
- attr_reader :notes
181
-
182
- # :section: Attributes with default getters and setters.
183
-
184
23
  ##
185
- # The format to use for file names. This should include a month (%m) and
186
- # year (%y) so the file can rotate every month. This will prevent files
187
- # from getting too large.
24
+ # Reset all configuration values to their defaults.
188
25
  #
189
- # @param [String] file_name_format Parsed by +strftime+
190
- #
191
- # @return [String]
192
- attr_accessor :file_name_format
193
-
194
- ##
195
- # The date format to use for entry headers.
196
- #
197
- # @param [String] header_date_format Parsed by +strftime+
198
- #
199
- # @return [String]
200
- attr_accessor :header_date_format
201
-
202
- ##
203
- # The header to use for the +Current+ section.
204
- #
205
- # @param [String] current_header
206
- #
207
- # @return [String]
208
- attr_accessor :current_header
209
-
210
- ##
211
- # The header to use for the +Previous+ section.
212
- #
213
- # @param [String] previous_header
214
- #
215
- # @return [String]
216
- attr_accessor :previous_header
217
-
218
- ##
219
- # The header to use for the +Impediments+ section.
220
- #
221
- # @param [String] impediments_header
222
- #
223
- # @return [String]
224
- attr_accessor :impediments_header
225
-
226
- ##
227
- # The header to use for the +Notes+ section.
228
- #
229
- # @param [String] notes_header
230
- #
231
- # @return [String]
232
- attr_accessor :notes_header
26
+ # @return [StandupMD::Config]
27
+ def self.reset_config
28
+ @config = StandupMD::Config.new
29
+ end
233
30
 
234
31
  ##
235
- # Constructor. Takes a path to a +YAML+ configuration file as an argument. If
236
- # passed, settings from the config file will be set. After +config_file+ is
237
- # loaded, yields +self+ so you can pass a block to access setters,
238
- # overwriting settings from +config_file+.
239
- #
240
- # @param [String] config_file The config file, if any, to load.
241
- #
242
- # @return [self]
32
+ # Allows for configuration via a block. Useful when making config files.
243
33
  #
244
34
  # @example
245
- # su = StandupMD.new('~/.standup_md.yml') do |s|
246
- # s.directory = @workdir
247
- # s.file_name_format = '%y_%m.markdown'
248
- # s.bullet_character = '*'
249
- # end
250
- def initialize(config_file = nil)
251
- @config = {}
252
- @notes = []
253
- @header_depth = 1
254
- @sub_header_depth = 2
255
- @bullet_character = '-'
256
- @current_entry_tasks = ["<!-- ADD TODAY'S WORK HERE -->"]
257
- @impediments = ['None']
258
- @file_name_format = '%Y_%m.md'
259
- @directory = File.join(ENV['HOME'], '.cache', 'standup_md')
260
- @header_date_format = '%Y-%m-%d'
261
- @current_header = 'Current'
262
- @previous_header = 'Previous'
263
- @impediments_header = 'Impediments'
264
- @notes_header = 'Notes'
265
- @sub_header_order = %w[previous current impediments notes]
266
- @config_file_loaded = false
267
- @config_file = config_file && File.expand_path(config_file)
268
-
269
- load_config_file if config_file
270
-
271
- yield self if block_given?
35
+ # StandupMD.configure { |s| s.cli.editor = 'mate' }
36
+ def self.configure
37
+ yield self.config
272
38
  end
273
39
 
274
40
  ##
275
41
  # Has a config file been loaded?
276
42
  #
277
43
  # @return [Boolean]
278
- def config_file_loaded?
44
+ def self.config_file_loaded?
279
45
  @config_file_loaded
280
46
  end
281
47
 
282
48
  ##
283
- # Has the file been written since instantiated?
284
- #
285
- # @return [boolean]
286
- #
287
- # @example
288
- # su = StandupMD.new
289
- # su.file_written?
290
- # # => false
291
- # su.write
292
- # su.file_written?
293
- # # => true
294
- def file_written?
295
- @file_written
296
- end
297
-
298
- ##
299
- # Was today's entry already in the file?
300
- #
301
- # @return [boolean] true if today's entry was already in the file
302
- def entry_previously_added?
303
- @entry_previously_added
304
- end
305
-
306
- ##
307
- # Setter for current entry tasks.
308
- #
309
- # @param [Array] tasks
310
- #
311
- # @return [Array]
312
- def previous_entry_tasks=(tasks)
313
- raise 'Must be an Array' unless tasks.is_a?(Array)
314
- @previous_entry_tasks = tasks
315
- end
316
-
317
- ##
318
- # Setter for notes.
319
- #
320
- # @param [Array] notes
321
- #
322
- # @return [Array]
323
- def notes=(tasks)
324
- raise 'Must be an Array' unless tasks.is_a?(Array)
325
- @notes = tasks
326
- end
327
-
328
- ##
329
- # Setter for current entry tasks.
330
- #
331
- # @param [Array] tasks
332
- #
333
- # @return [Array]
334
- def current_entry_tasks=(tasks)
335
- raise 'Must be an Array' unless tasks.is_a?(Array)
336
- @current_entry_tasks = tasks
337
- end
338
-
339
- ##
340
- # Setter for impediments.
341
- #
342
- # @param [Array] tasks
343
- #
344
- # @return [Array]
345
- def impediments=(tasks)
346
- raise 'Must be an Array' unless tasks.is_a?(Array)
347
- @impediments = tasks
348
- end
349
-
350
- ##
351
- # Setter for bullet_character. Must be * (asterisk) or - (dash).
352
- #
353
- # @param [String] character
354
- #
355
- # @return [String]
356
- def bullet_character=(character)
357
- raise 'Must be "-" or "*"' unless %w[- *].include?(character)
358
- @bullet_character = character
359
- end
360
-
361
- ##
362
- # Setter for +config_file+. Must be expanded in case the user uses `~` for
363
- # home.
49
+ # Loads a config file.
364
50
  #
365
51
  # @param [String] file
366
- #
367
- # @return [String]
368
- def config_file=(config_file)
369
- @config_file = File.expand_path(config_file)
370
- end
371
-
372
- ##
373
- # Setter for directory. Must be expanded in case the user uses `~` for home.
374
- # If the directory doesn't exist, it will be created. To reset instance
375
- # variables after changing the directory, you'll need to call load.
376
- #
377
- # @param [String] directory
378
- #
379
- # @return [String]
380
- def directory=(directory)
381
- directory = File.expand_path(directory)
382
- FileUtils.mkdir_p(directory) unless File.directory?(directory)
383
- @directory = directory
384
- end
385
-
386
- ##
387
- # Number of octothorps (#) to use before the main header.
388
- #
389
- # @param [Integer] depth
390
- #
391
- # @return [Integer]
392
- def header_depth=(depth)
393
- if !depth.between?(1, 5)
394
- raise 'Header depth out of bounds (1..5)'
395
- elsif depth >= sub_header_depth
396
- @sub_header_depth = depth + 1
397
- end
398
- @header_depth = depth
399
- end
400
-
401
- ##
402
- # Number of octothorps (#) to use before sub headers (Current, Previous, etc).
403
- #
404
- # @param [Integer] depth
405
- #
406
- # @return [Integer]
407
- def sub_header_depth=(depth)
408
- if !depth.between?(2, 6)
409
- raise 'Sub-header depth out of bounds (2..6)'
410
- elsif depth <= header_depth
411
- @header_depth = depth - 1
412
- end
413
- @sub_header_depth = depth
414
- end
415
-
416
- ##
417
- # Preferred order for sub-headers.
418
- #
419
- # @param [Array] Values must be %w[previous current impediment notes]
420
- #
421
- # @return [Array]
422
- def sub_header_order=(array)
423
- order = %w[previous current impediments notes]
424
- raise "Values must be #{order.join{', '}}" unless order.sort == array.sort
425
- @sub_header_order = array
426
- end
427
-
428
- ##
429
- # Return a copy of the sub-header order so the user can't modify the array.
430
- #
431
- # @return [Array]
432
- def sub_header_order
433
- @sub_header_order.dup
434
- end
435
-
436
- ##
437
- # Loads the config file
438
- #
439
- # @return [Hash] The config options
440
- def load_config_file
441
- raise 'No config file set' if config_file.nil?
442
- raise "File #{config_file} does not exist" unless File.file?(config_file)
443
- @config = YAML::load_file(config_file)
52
+ def self.load_config_file(file)
53
+ file = ::File.expand_path(file)
54
+ raise "File #{file} does not exist." unless ::File.file?(file)
444
55
  @config_file_loaded = true
445
- @config.each { |k, v| send("#{k}=", v) if respond_to?("#{k}=")}
446
- end
447
-
448
- ##
449
- # Writes a new entry to the file if the first entry in the file isn't today.
450
- #
451
- # @return [Boolean]
452
- def write
453
- File.open(file, 'w') do |f|
454
- all_entries.each do |head, s_heads|
455
- f.puts '#' * header_depth + ' ' + head
456
- sub_header_order.map { |value| "#{value}_header" }.each do |sub_head|
457
- sh = send(sub_head).capitalize
458
- next if !s_heads[sh] || s_heads[sh].empty?
459
- f.puts '#' * sub_header_depth + ' ' + sh
460
- s_heads[sh].each { |task| f.puts bullet_character + ' ' + task }
461
- end
462
- f.puts
463
- break if new_month?
464
- end
465
- end
466
- @file_written = true
467
- end
468
-
469
- ##
470
- # Sets internal instance variables. Called when first instantiated, or after
471
- # directory is set.
472
- #
473
- # @return [self]
474
- def load
475
- FileUtils.mkdir_p(directory) unless File.directory?(directory)
476
-
477
- @today = Date.today
478
- @header = today.strftime(header_date_format)
479
- @file_written = false
480
- @file = File.expand_path(File.join(directory, today.strftime(file_name_format)))
481
- @previous_file = get_previous_file
482
- @all_previous_entries = get_all_previous_entries
483
- @entry_previously_added = all_previous_entries.key?(header)
484
- @previous_entry_tasks = previous_entry[current_header]
485
- @current_entry = @all_previous_entries.delete(header) || new_entry
486
- @all_entries = {header => current_entry}.merge(all_previous_entries)
487
-
488
- FileUtils.touch(file) unless File.file?(file)
489
- self
490
- end
491
-
492
- ##
493
- # Alias of +load+
494
- #
495
- # @return [self]
496
- alias_method :reload, :load
497
-
498
- ##
499
- # Is today a different month than the previous entry?
500
- def new_month?
501
- file != previous_file
502
- end
503
-
504
- private
505
-
506
- ##
507
- # Scaffolding with which new entries will be created.
508
- def new_entry # :nodoc:
509
- {
510
- previous_header => previous_entry_tasks || [],
511
- current_header => current_entry_tasks,
512
- impediments_header => impediments,
513
- notes_header => notes,
514
- }
515
- end
516
-
517
- ##
518
- # Date object of today's date.
519
- def today # :nodoc:
520
- @today
521
- end
522
-
523
- def get_previous_file # :nodoc:
524
- return file if File.file?(file) && !File.zero?(file)
525
- prev_month_file = File.expand_path(File.join(
526
- directory,
527
- today.prev_month.strftime(file_name_format)
528
- ))
529
- File.file?(prev_month_file) ? prev_month_file : ''
530
- end
531
-
532
- def get_all_previous_entries # :nodoc:
533
- return {} unless File.file?(previous_file)
534
- prev_entries = {}
535
- entry_header = ''
536
- section_type = ''
537
- File.foreach(previous_file) do |line|
538
- line.chomp!
539
- next if line.strip.empty?
540
- if line.match(%r{^#{'#' * header_depth}\s+})
541
- entry_header = line.sub(%r{^\#{#{header_depth}}\s*}, '')
542
- section_type = notes_header
543
- prev_entries[entry_header] ||= {}
544
- elsif line.match(%r{^#{'#' * sub_header_depth}\s+})
545
- section_type = determine_section_type(
546
- line.sub(%r{^\#{#{sub_header_depth}}\s*}, '')
547
- )
548
- prev_entries[entry_header][section_type] = []
549
- else
550
- prev_entries[entry_header][section_type] << line.sub(
551
- %r{\s*#{bullet_character}\s*}, ''
552
- )
553
- end
554
- end
555
- prev_entries
556
- rescue => e
557
- raise "File malformation: #{e}"
558
- end
559
-
560
- def determine_section_type(line) # :nodoc:
561
- [
562
- current_header,
563
- previous_header,
564
- impediments_header,
565
- notes_header
566
- ].each { |header| return header if line.include?(header) }
567
- raise "Unknown header type [#{line}]"
568
- end
569
-
570
- def previous_entry # :nodoc:
571
- all_previous_entries.each do |key, value|
572
- return value unless key == header
573
- end
56
+ load file
574
57
  end
575
58
  end