timeless 0.1.0 → 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a45d84d4290e39bba5b6fe3a99fd101cefd7f97c
4
- data.tar.gz: 761ab4cf1594beb69534198f556a11f936806801
3
+ metadata.gz: 06ba9bc95d407ecb088dc4c016fc7a66c78417cf
4
+ data.tar.gz: 560ac4abb81ef3f80476a0b7baa154ff3e3257e2
5
5
  SHA512:
6
- metadata.gz: 5b97c16e7cc2fc1b1b919c93518a11c7ae6a115e11ff1138251bf26bd699da7d18b11f59b0a222c4ca242105d596528b52e1bb6149abd4f1df8f4c37adeffd3f
7
- data.tar.gz: 72160f36426d2659db29cb1cfc57deb81948e1a24d483639197f881c0068c65b7b06455fc684d018793e091d7d85b0bd5e22d3c96d9b485d116d175affe9fddd
6
+ metadata.gz: f7b8f73506f3c399c5f17b0ce60620cfb31d63b09d87fb1bf89f3bf0babeb16cda0fae91d383405e9d848929837517e5567f186c5197c9b6cea2e49363c30a46
7
+ data.tar.gz: d44c950c14ba467675f0d0cb9c8f4a58b6ffd8ea93bba6329709944cc39aa4b9f3b45455f478a5aa39ec20c00d2cb17826e34dc5b70a2a23e235c002302f2dee
data/Gemfile CHANGED
@@ -1,4 +1,6 @@
1
- source 'https://rubygems.org'
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
2
4
 
3
5
  # Specify your gem's dependencies in timeless.gemspec
4
6
  gemspec
@@ -1,22 +1,21 @@
1
- Copyright (c) 2015 Adolfo Villafiorita
1
+ The MIT License (MIT)
2
2
 
3
- MIT License
3
+ Copyright (c) 2015-2017 Adolfo Villafiorita
4
4
 
5
- Permission is hereby granted, free of charge, to any person obtaining
6
- a copy of this software and associated documentation files (the
7
- "Software"), to deal in the Software without restriction, including
8
- without limitation the rights to use, copy, modify, merge, publish,
9
- distribute, sublicense, and/or sell copies of the Software, and to
10
- permit persons to whom the Software is furnished to do so, subject to
11
- the following conditions:
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
12
11
 
13
- The above copyright notice and this permission notice shall be
14
- included in all copies or substantial portions of the Software.
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
15
14
 
16
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,152 @@
1
+ Timeless
2
+ ========
3
+
4
+ Timeless is a simple command line time tracker.
5
+
6
+ Installation
7
+ ------------
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ruby
12
+ gem 'timeless'
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install timeless
21
+
22
+ Usage
23
+ -----
24
+
25
+ timeless is a simple command line time tracker. Entries store the
26
+ following information:
27
+
28
+ 1. start time
29
+ 2. end time
30
+ 3. notes
31
+
32
+ timeless allows one to
33
+
34
+ - run a pomodoro timer
35
+ - clock time using a "stopwatch" (with start and stop commands)
36
+ - manually entering data
37
+ - print a text report, possibly filtered by date ranges of notes'
38
+ content
39
+ - export to csv
40
+
41
+ Timeless has also some basic support for key-pair values in the notes
42
+ form. They can be used to assign a special meaning to some strings and
43
+ improve filtering and exporting. Two special keys are `p` for projects
44
+ and `c` for clients. These two keys are exported in dedicated columns
45
+ when exporting data to csv.
46
+
47
+ Some non nominal conditions are also handled (e.g., starting a clock
48
+ twice, trying to stop when no clock was started).
49
+
50
+ If you do not want to type `timeless` every time, you can invoke a console:
51
+
52
+ timeless console
53
+ timeless:000> start
54
+ timeless:001> stop
55
+ timeless:002>
56
+
57
+ More information with:
58
+
59
+ timeless help # get list of available commands
60
+ timeless help command # help about command
61
+ timeless man # output this README file
62
+
63
+
64
+ Examples
65
+ --------
66
+
67
+ Get information about command syntax:
68
+
69
+ timeless -h
70
+
71
+ Run a pomodoro timer:
72
+
73
+ timeless pom p:project meeting with c:john
74
+ timeless pom --duration 60 p:prj2
75
+ timeless pom --long # default to 50 minutes
76
+
77
+ Start clocking using a stopwatch:
78
+
79
+ timeless start requirements doc
80
+ timeless start --at 'thirty minutes ago' fixing bug 182 for c:tim
81
+ timeless start --force --at 'five minutes ago' requirements document for p:prj1
82
+
83
+ Stop clocking:
84
+
85
+ timeless stop
86
+ timeless stop forgot notes on start
87
+ timeless stop --last # reuse the notes of last entry
88
+ timeless stop --at 'five minutes ago'
89
+ timeless stop --start '1 hour ago' --at 'now' clocked a full entry
90
+
91
+ Enter a full entry:
92
+
93
+ timeless clock activity on p:project # from end of last entry to now
94
+ timeless clock --start 'three hours ago' --stop 'five minutes ago'
95
+
96
+ What was i doing?
97
+
98
+ timeless current # print current entry
99
+ timeless forget # forget pending clock
100
+
101
+ What did I do?
102
+
103
+ timeless last # print last entry
104
+
105
+ Reporting and exporting
106
+
107
+ timeless statement
108
+ timeless balance --from yesterday --filter p:project
109
+ timeless export
110
+
111
+ Notice that even though `timeless` uses CSV as its native format, the
112
+ export command exports to a CSV file which is more easily parsed by a
113
+ Spreadsheet such as LibreOffice.
114
+
115
+ If something goes wrong
116
+ -----------------------
117
+
118
+ Data is stored in the CSV file `~/.timeless.csv`. You can edit the file
119
+ to manually fix entries if you make some mistake.
120
+
121
+ Version History
122
+ ---------------
123
+
124
+ **0.4.0**
125
+
126
+ - second public release
127
+ - new `balance` command
128
+ - the `report` command has been renamed `statement`
129
+ - interactive `console`
130
+ - more detailed help for commands
131
+ - fixed a bug in the last command: now it returns the last activity in
132
+ chronological order (rather than the last activity which has been clocked)
133
+
134
+ **0.2.0**
135
+ - initial public release
136
+
137
+
138
+
139
+ License
140
+ -------
141
+
142
+ Licensed under the terms of the MIT License.
143
+
144
+ Contributing
145
+ ------------
146
+
147
+ 1. Fork it ( https://github.com/\[my-github-username\]/timeless/fork )
148
+ 2. Create your feature branch (\`git checkout -b my-new-feature\`)
149
+ 3. Commit your changes (\`git commit -am 'Add some feature'\`)
150
+ 4. Push to the branch (\`git push origin my-new-feature\`)
151
+ 5. Create a new Pull Request
152
+
data/Rakefile CHANGED
@@ -1,2 +1,2 @@
1
1
  require "bundler/gem_tasks"
2
-
2
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "timeless"
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(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,22 @@
1
+ ;; forms-mode for timeless -*- emacs-lisp -*-
2
+
3
+ (setq forms-file (expand-file-name "~/.timeless.csv"))
4
+
5
+ ;; Use `forms-enumerate' to set field names and number thereof.
6
+ (setq forms-number-of-fields
7
+ (forms-enumerate
8
+ '(start-datetime
9
+ end-datetime
10
+ description)))
11
+
12
+ (setq forms-field-sep ",")
13
+
14
+ ;; The format list.
15
+ (setq forms-format-list
16
+ (list
17
+ "====== Timeless Record ======\n\n"
18
+ "Start: " start-datetime "\n"
19
+ "End: " end-datetime "\n"
20
+ "Description: " description "\n"
21
+ ))
22
+
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'timeless'
4
+
5
+ Timeless::CommandSemantics.reps Timeless::CommandSyntax.commands, ARGV
@@ -1,7 +1,8 @@
1
1
  require "timeless/version"
2
2
 
3
- module Timeless
4
- require "timeless/pomodoro"
5
- require "timeless/stopwatch"
6
- require "timeless/storage"
7
- end
3
+ require "timeless/storage"
4
+ require "timeless/stopwatch"
5
+ require "timeless/pomodoro"
6
+
7
+ require "timeless/cli/command_syntax"
8
+ require "timeless/cli/command_semantics"
@@ -0,0 +1,352 @@
1
+ require 'timeless'
2
+
3
+ require 'chronic'
4
+ require 'readline'
5
+ require 'fileutils'
6
+
7
+ module Timeless
8
+ module CommandSemantics
9
+ APPNAME = 'timeless'
10
+ VERSION = Timeless::VERSION
11
+
12
+ #
13
+ # Main App Starts Here!
14
+ #
15
+ def self.version opts = nil, argv = []
16
+ puts "#{APPNAME} version #{VERSION}"
17
+ end
18
+
19
+ def self.man opts = nil, argv = []
20
+ path = File.join(File.dirname(__FILE__), "/../../../README.md")
21
+ file = File.open(path, "r")
22
+ contents = file.read
23
+ puts contents
24
+ end
25
+
26
+ def self.help opts = nil, argv = []
27
+ all_commands = CommandSyntax.commands
28
+
29
+ if argv != []
30
+ argv.map { |x| puts all_commands[x.to_sym][2] }
31
+ else
32
+ puts "#{APPNAME} command [options] [args]"
33
+ puts ""
34
+ puts "Available commands:"
35
+ puts ""
36
+ all_commands.keys.each do |key|
37
+ puts " " + all_commands[key][0].banner
38
+ end
39
+ end
40
+ end
41
+
42
+ def self.console opts, argv = []
43
+ all_commands = CommandSyntax.commands
44
+ all_commands.delete(:console)
45
+
46
+ i = 0
47
+ while true
48
+ string = Readline.readline("#{APPNAME}:%03d> " % i, true)
49
+ string.gsub!(/^#{APPNAME} /, "") # as a courtesy, remove any leading appname string
50
+ if string == "exit" or string == "quit" or string == "." then
51
+ exit 0
52
+ end
53
+ reps all_commands, string.split(' ')
54
+ i = i + 1
55
+ end
56
+ end
57
+
58
+ # read-eval-print step
59
+ def self.reps all_commands, argv
60
+ if argv == [] or argv[0] == "--help" or argv[0] == "-h"
61
+ CommandSemantics.help
62
+ exit 0
63
+ else
64
+ command = argv[0]
65
+ syntax_and_semantics = all_commands[command.to_sym]
66
+ if syntax_and_semantics
67
+ opts = syntax_and_semantics[0]
68
+ function = syntax_and_semantics[1]
69
+
70
+ begin
71
+ parser = Slop::Parser.new(opts)
72
+
73
+ result = parser.parse(argv[1..-1])
74
+ options = result.to_hash
75
+ arguments = result.arguments
76
+
77
+ eval "CommandSemantics::#{function}(options, arguments)"
78
+ rescue Slop::Error => e
79
+ puts "#{APPNAME}: #{e}"
80
+ rescue Exception => e
81
+ puts e
82
+ end
83
+ else
84
+ puts "#{APPNAME}: '#{command}' is not a valid command. See '#{APPNAME} help'"
85
+ end
86
+ end
87
+ end
88
+
89
+ #
90
+ # APP SPECIFIC COMMANDS
91
+ #
92
+ def self.pom opts, argv
93
+ duration = opts.to_hash[:duration] ||
94
+ (opts.to_hash[:long] ? Timeless::Pomodoro::WORKING_LONG : Timeless::Pomodoro::WORKING)
95
+
96
+ start, stop, notes = Timeless::Pomodoro.run_pomodoro_timer(duration, argv.join(" "))
97
+ Timeless::Storage.store(start, stop, notes)
98
+ puts last_entry
99
+ end
100
+
101
+
102
+ def self.start opts, argv
103
+ start = Chronic.parse(opts.to_hash[:at]) # nil if no option specified
104
+ notes = argv.join(" ") # empty string is no args specified
105
+ force = opts.to_hash[:force]
106
+
107
+ if Timeless::Stopwatch.clocking? and not force
108
+ start, notes = Timeless::Stopwatch.get_start # for information purposes only
109
+ puts "There is a clock started at #{start} (notes: \"#{notes}\"). Use --force to override."
110
+ else
111
+ Timeless::Stopwatch.start(start, notes)
112
+ puts "Clock started at #{start ? start : Time.now}."
113
+ puts "You may want to specify notes for the entry when you stop clocking." if notes == ""
114
+ end
115
+ end
116
+
117
+ def self.forget opts, argv
118
+ if File.exists?(Timeless::Stopwatch::START_FILENAME)
119
+ entry = Timeless::Storage.last
120
+ Timeless::Stopwatch.forget
121
+ puts "Forgotten timer started at #{entry[0]} (#{entry[2]})."
122
+ else
123
+ puts "You don't have an active timer"
124
+ end
125
+ end
126
+
127
+ def self.stop opts, argv
128
+ start = Chronic.parse(opts.to_hash[:start]) # nil if no option specified
129
+ stop = Chronic.parse(opts.to_hash[:at]) # nil if no option specified
130
+ notes = argv.join(" ") # empty string if no args specified
131
+
132
+ manage_stop start, stop, notes, opts[:last]
133
+ end
134
+
135
+ def self.clock opts, argv
136
+ start = Chronic.parse(opts.to_hash[:start]) # nil if no option specified
137
+ stop_opt = Chronic.parse(opts.to_hash[:stop]) # nil if no option specified
138
+ end_opt = Chronic.parse(opts.to_hash[:end]) # nil if no option specified
139
+ stop = stop_opt ? stop_opt : end_opt # stop_opt if --stop, end_opt if --end, nil otherwise
140
+ notes = argv.join(" ") # empty string if no args specified
141
+
142
+ if stop_opt and end_opt then
143
+ puts "Timeless error: specify end time with either --end or --stop (not both)"
144
+ exit
145
+ end
146
+
147
+ manage_stop start, stop, notes, opts[:last]
148
+ end
149
+
150
+ def self.current opts, argv
151
+ if Timeless::Stopwatch.clocking? then
152
+ start, notes = Timeless::Stopwatch.get_start
153
+ puts "Timeless: you have been clocking #{"%.0d" % ((Time.now- Time.parse(start)) / 60)} minutes"
154
+ puts "on: #{notes}" if notes
155
+ else
156
+ puts "There is no clock started."
157
+ end
158
+ end
159
+
160
+ def self.last opts, argv
161
+ puts last_entry
162
+ end
163
+
164
+ def self.list opts, argv
165
+ values = Timeless::Storage.get_key argv[0]
166
+ puts "Timeless: list of keys #{argv[0]} found in timesheets:"
167
+ values.each do |value|
168
+ puts value
169
+ end
170
+ end
171
+
172
+ def self.export opts, argv
173
+ Timeless::Storage.export
174
+ end
175
+
176
+ def self.gaps opts, argv
177
+ from = opts.to_hash[:from] ? Chronic.parse(opts.to_hash[:from]) : nil
178
+ to = opts.to_hash[:to] ? Chronic.parse(opts.to_hash[:to]) : nil
179
+ interval = opts.to_hash[:interval] || 1
180
+
181
+ entries = Timeless::Storage.get(from, to)
182
+ previous_stop = from || Time.parse(entries[0][0])
183
+ day = ""
184
+
185
+ printf "%-18s %-5s - %-5s %-6s %-s\n", "Day", "Start", "End", "Gap", "Command to fix"
186
+ entries.sort { |x,y| Time.parse(x[0]) <=> Time.parse(y[0]) }.each do |entry|
187
+ current_start = Time.parse(entry[0])
188
+
189
+ current_day = current_start.strftime("%a %b %d, %Y")
190
+
191
+ if (current_start.day == previous_stop.day and current_start - previous_stop > interval * 60) then
192
+
193
+ if current_day != day then
194
+ day = current_day
195
+ else
196
+ current_day = "" # avoid printing the day if same as before
197
+ end
198
+
199
+ duration = (current_start - previous_stop) / 60
200
+ printf "%-18s %5s - %5s %02i:%02i %s\n",
201
+ current_day,
202
+ previous_stop.strftime("%H:%M"),
203
+ current_start.strftime("%H:%M"),
204
+ duration / 60, duration % 60,
205
+ "timeless clock --start '#{previous_stop}' --end '#{current_start}'"
206
+
207
+ end
208
+ previous_stop = Time.parse(entry[1])
209
+ end
210
+ end
211
+
212
+
213
+ def self.statement opts, argv
214
+ from = opts.to_hash[:from] ? Chronic.parse(opts.to_hash[:from]) : nil
215
+ to = opts.to_hash[:to] ? Chronic.parse(opts.to_hash[:to]) : nil
216
+
217
+ entries = Timeless::Storage.get(from, to, opts.to_hash[:filter])
218
+ # it could become a function of a reporting module
219
+ printf "%-18s %-5s - %-5s %-8s %-s\n", "Day", "Start", "End", "Duration", "Notes"
220
+ total = 0
221
+ day = ""
222
+ entries.sort { |x,y| Time.parse(x[0]) <=> Time.parse(y[0]) }.each do |entry|
223
+ current_day = Time.parse(entry[0]).strftime("%a %b %d, %Y")
224
+ if current_day != day then
225
+ day = current_day
226
+ else
227
+ current_day = "" # avoid printing the day if same as before
228
+ end
229
+
230
+ duration = (Time.parse(entry[1]) - Time.parse(entry[0])) / 60
231
+ total = total + duration
232
+
233
+ printf "%-18s %5s - %5s %5s %s\n",
234
+ current_day,
235
+ Time.parse(entry[0]).strftime("%H:%M"),
236
+ Time.parse(entry[1]).strftime("%H:%M"),
237
+ in_hours(duration),
238
+ entry[2]
239
+ end
240
+ puts "----------------------------------------------------------------------"
241
+ printf "Total %02i:%02i\n", total / 60, total % 60
242
+ end
243
+
244
+ def self.balance opts, argv
245
+ from = opts.to_hash[:from] ? Chronic.parse(opts.to_hash[:from]) : nil
246
+ to = opts.to_hash[:to] ? Chronic.parse(opts.to_hash[:to]) : nil
247
+
248
+ # {key1 => {value1 => total .. } , ... }
249
+ hash = Timeless::Storage::balance from, to, opts.to_hash[:filter]
250
+
251
+ total = 0
252
+ puts "Projects"
253
+ puts "========"
254
+ hash["p"].each do |key, value|
255
+ printf "%-20s%5s\n", key, in_hours(value)
256
+ total += value
257
+ end
258
+ puts "-------------------------"
259
+ printf "%-20s%5s\n", "Total", in_hours(total)
260
+
261
+ total = 0
262
+ puts ""
263
+ puts "Clients"
264
+ puts "======="
265
+ hash["c"].each do |key, value|
266
+ printf "%-20s%5s\n", key, in_hours(value)
267
+ total += value
268
+ end
269
+ puts "-------------------------"
270
+ printf "%-20s%5s\n", "Total", in_hours(total)
271
+
272
+ total = 0
273
+ puts ""
274
+ puts "Activities"
275
+ puts "=========="
276
+ hash["a"].each do |key, value|
277
+ printf "%-20s%5s\n", key, in_hours(value)
278
+ total += value
279
+ end
280
+ puts "-------------------------"
281
+ printf "%-20s%5s\n", "Total", in_hours(total)
282
+ end
283
+
284
+
285
+ private
286
+
287
+ def self.in_hours value
288
+ sprintf "%02i:%02i", value / 60, value % 60
289
+ end
290
+
291
+ def self.last_entry
292
+ start, stop, notes = Timeless::Storage.last
293
+
294
+ interval = Time.parse(stop) - Time.parse(start)
295
+ seconds = interval % 60
296
+ minutes = (interval / 60) % 60
297
+ hours = interval / 3600
298
+
299
+ sprintf "Timeless: you clocked %02d:%02d:%02d on %s\nYou stopped clocking at: %s", hours, minutes, seconds, notes, stop
300
+ end
301
+
302
+ #
303
+ # code shared by stop and clock (which accept slightly different options,
304
+ # but behave in the same way
305
+ #
306
+ def self.manage_stop start, stop, notes, reuse_last
307
+ # syntax check:
308
+ # - --start is illegal if there is a running clock
309
+ # - --last is illegal if there are notes
310
+ #
311
+ # however:
312
+ # - --last and notes prevail over notes stored when starting the clock
313
+ if Timeless::Stopwatch.clocking? and start
314
+ puts "Timeless error: you specified --start with a running clock. Use 'timeless forget' or drop --start"
315
+ end
316
+ if reuse_last and notes != "" then
317
+ puts "Timeless error: you specified both --last and notes. Choose one or the other"
318
+ end
319
+
320
+ if reuse_last then
321
+ _, _, notes = Timeless::Storage.last
322
+ end
323
+
324
+ if Timeless::Stopwatch.clocking?
325
+ start, stop, notes = Timeless::Stopwatch.stop(start, stop, notes) # merge passed with data in running clock
326
+ Timeless::Storage.store(start, stop, notes)
327
+
328
+ puts "Clock stopped at #{stop}."
329
+ puts last_entry
330
+ else
331
+ start = start ? start : Timeless::Storage.last[1]
332
+ stop = stop ? stop : Time.now
333
+ Timeless::Storage.store(start, stop, notes)
334
+
335
+ puts "Clock stopped at #{stop}."
336
+ puts last_entry
337
+ end
338
+ end
339
+
340
+ # GENERAL UTILITIES
341
+
342
+ def self.backup filename
343
+ FileUtils::cp filename, filename + "~"
344
+ puts "Backup copy #{filename} created in #{filename}~."
345
+ end
346
+
347
+ def self.backup_and_write filename, content
348
+ backup(filename) if File.exist?(filename)
349
+ File.open(filename, "w") { |f| f.puts content }
350
+ end
351
+ end
352
+ end