standup_md 0.2.1 → 0.3.4

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