standup_md 0.2.0 → 0.3.3

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,576 +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
- # The class for handing reading/writing of entries.
9
- #
10
- # @example
11
- # su = StandupMD.new
12
- class StandupMD
11
+ # The main module for the gem. Provides access to configuration classes.
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
+ # Method for accessing the configuration.
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 directory. Must be expanded in case the user uses `~` for home.
363
- # If the directory doesn't exist, it will be created. To reset instance
364
- # variables after changing the directory, you'll need to call load.
49
+ # Loads a config file.
365
50
  #
366
51
  # @param [String] file
367
- #
368
- # @return [String]
369
- def config_file=(config_file)
370
- @config_file = File.expand_path(config_file)
371
- end
372
-
373
- ##
374
- # Setter for directory. Must be expanded in case the user uses `~` for home.
375
- # If the directory doesn't exist, it will be created. To reset instance
376
- # variables after changing the directory, you'll need to call load.
377
- #
378
- # @param [String] directory
379
- #
380
- # @return [String]
381
- def directory=(directory)
382
- directory = File.expand_path(directory)
383
- FileUtils.mkdir_p(directory) unless File.directory?(directory)
384
- @directory = directory
385
- end
386
-
387
- ##
388
- # Number of octothorps (#) to use before the main header.
389
- #
390
- # @param [Integer] depth
391
- #
392
- # @return [Integer]
393
- def header_depth=(depth)
394
- if !depth.between?(1, 5)
395
- raise 'Header depth out of bounds (1..5)'
396
- elsif depth >= sub_header_depth
397
- @sub_header_depth = depth + 1
398
- end
399
- @header_depth = depth
400
- end
401
-
402
- ##
403
- # Number of octothorps (#) to use before sub headers (Current, Previous, etc).
404
- #
405
- # @param [Integer] depth
406
- #
407
- # @return [Integer]
408
- def sub_header_depth=(depth)
409
- if !depth.between?(2, 6)
410
- raise 'Sub-header depth out of bounds (2..6)'
411
- elsif depth <= header_depth
412
- @header_depth = depth - 1
413
- end
414
- @sub_header_depth = depth
415
- end
416
-
417
- ##
418
- # Preferred order for sub-headers.
419
- #
420
- # @param [Array] Values must be %w[previous current impediment notes]
421
- #
422
- # @return [Array]
423
- def sub_header_order=(array)
424
- order = %w[previous current impediments notes]
425
- raise "Values must be #{order.join{', '}}" unless order.sort == array.sort
426
- @sub_header_order = array
427
- end
428
-
429
- ##
430
- # Return a copy of the sub-header order so the user can't modify the array.
431
- #
432
- # @return [Array]
433
- def sub_header_order
434
- @sub_header_order.dup
435
- end
436
-
437
- ##
438
- # Loads the config file
439
- #
440
- # @return [Hash] The config options
441
- def load_config_file
442
- raise 'No config file set' if config_file.nil?
443
- raise "File #{config_file} does not exist" unless File.file?(config_file)
444
- @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)
445
55
  @config_file_loaded = true
446
- @config.each { |k, v| send("#{k}=", v) }
447
- end
448
-
449
- ##
450
- # Writes a new entry to the file if the first entry in the file isn't today.
451
- #
452
- # @return [Boolean]
453
- def write
454
- File.open(file, 'w') do |f|
455
- all_entries.each do |head, s_heads|
456
- f.puts '#' * header_depth + ' ' + head
457
- sub_header_order.map { |value| "#{value}_header" }.each do |sub_head|
458
- sh = send(sub_head).capitalize
459
- next if !s_heads[sh] || s_heads[sh].empty?
460
- f.puts '#' * sub_header_depth + ' ' + sh
461
- s_heads[sh].each { |task| f.puts bullet_character + ' ' + task }
462
- end
463
- f.puts
464
- break if new_month?
465
- end
466
- end
467
- @file_written = true
468
- end
469
-
470
- ##
471
- # Sets internal instance variables. Called when first instantiated, or after
472
- # directory is set.
473
- #
474
- # @return [self]
475
- def load
476
- FileUtils.mkdir_p(directory) unless File.directory?(directory)
477
-
478
- @today = Date.today
479
- @header = today.strftime(header_date_format)
480
- @file_written = false
481
- @file = File.expand_path(File.join(directory, today.strftime(file_name_format)))
482
- @previous_file = get_previous_file
483
- @all_previous_entries = get_all_previous_entries
484
- @entry_previously_added = all_previous_entries.key?(header)
485
- @previous_entry_tasks = previous_entry[current_header]
486
- @current_entry = @all_previous_entries.delete(header) || new_entry
487
- @all_entries = {header => current_entry}.merge(all_previous_entries)
488
-
489
- FileUtils.touch(file) unless File.file?(file)
490
- self
491
- end
492
-
493
- ##
494
- # Alias of +load+
495
- #
496
- # @return [self]
497
- alias_method :reload, :load
498
-
499
- ##
500
- # Is today a different month than the previous entry?
501
- def new_month?
502
- file != previous_file
503
- end
504
-
505
- private
506
-
507
- ##
508
- # Scaffolding with which new entries will be created.
509
- def new_entry # :nodoc:
510
- {
511
- previous_header => previous_entry_tasks || [],
512
- current_header => current_entry_tasks,
513
- impediments_header => impediments,
514
- notes_header => notes,
515
- }
516
- end
517
-
518
- ##
519
- # Date object of today's date.
520
- def today # :nodoc:
521
- @today
522
- end
523
-
524
- def get_previous_file # :nodoc:
525
- return file if File.file?(file) && !File.zero?(file)
526
- prev_month_file = File.expand_path(File.join(
527
- directory,
528
- today.prev_month.strftime(file_name_format)
529
- ))
530
- File.file?(prev_month_file) ? prev_month_file : ''
531
- end
532
-
533
- def get_all_previous_entries # :nodoc:
534
- return {} unless File.file?(previous_file)
535
- prev_entries = {}
536
- entry_header = ''
537
- section_type = ''
538
- File.foreach(previous_file) do |line|
539
- line.chomp!
540
- next if line.strip.empty?
541
- if line.match(%r{^#{'#' * header_depth}\s+})
542
- entry_header = line.sub(%r{^\#{#{header_depth}}\s*}, '')
543
- section_type = notes_header
544
- prev_entries[entry_header] ||= {}
545
- elsif line.match(%r{^#{'#' * sub_header_depth}\s+})
546
- section_type = determine_section_type(
547
- line.sub(%r{^\#{#{sub_header_depth}}\s*}, '')
548
- )
549
- prev_entries[entry_header][section_type] = []
550
- else
551
- prev_entries[entry_header][section_type] << line.sub(
552
- %r{\s*#{bullet_character}\s*}, ''
553
- )
554
- end
555
- end
556
- prev_entries
557
- rescue => e
558
- raise "File malformation: #{e}"
559
- end
560
-
561
- def determine_section_type(line) # :nodoc:
562
- [
563
- current_header,
564
- previous_header,
565
- impediments_header,
566
- notes_header
567
- ].each { |header| return header if line.include?(header) }
568
- raise "Unknown header type [#{line}]"
569
- end
570
-
571
- def previous_entry # :nodoc:
572
- all_previous_entries.each do |key, value|
573
- return value unless key == header
574
- end
56
+ load file
575
57
  end
576
58
  end