standup_md 0.2.0 → 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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