syc-task 0.0.7 → 0.1.15

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.
data/lib/syctask.rb CHANGED
@@ -5,9 +5,12 @@ require 'syctask/task_service.rb'
5
5
  require 'syctask/task_scheduler.rb'
6
6
  require 'syctask/task_planner.rb'
7
7
  require 'syctask/schedule.rb'
8
- require 'sycutil/console.rb'
9
8
  require 'syctask/task_tracker.rb'
9
+ require 'syctask/environment.rb'
10
+ require 'sycutil/console.rb'
10
11
  require 'syctime/time_util.rb'
12
+ require 'syctask/settings.rb'
13
+ require 'syctask/statistics.rb'
11
14
 
12
15
  # Add requires for other files you add to your project here, so
13
16
  # you just need to require this one file in your bin file
@@ -1,6 +1,431 @@
1
1
  module Syctask
2
2
 
3
- # Default working directory of the application
4
- WORK_DIR = File.expand_path('~/.tasks')
3
+ # System directory of syctask
4
+ SYC_DIR = File.expand_path('~/.syc/syctask')
5
+ # ID file where the last issued ID is saved
6
+ ID = SYC_DIR + "/id"
7
+ # File that contains all issued IDs
8
+ IDS = SYC_DIR + "/ids"
9
+ # File with tags
10
+ TAGS = SYC_DIR + "/tags"
11
+ # File with the general purpose tasks
12
+ DEFAULT_TASKS = SYC_DIR + "/default_tasks"
13
+ # File that holds the default task directory
14
+ DEFAULT_TASKS_DIR = SYC_DIR + "/default_tasks_dir"
15
+ # Log file that logs all activities of syctask like creation of tasks
16
+ TASKS_LOG = SYC_DIR + "/tasks.log"
17
+ # File that holds the tracked task
18
+ TRACKED_TASK = SYC_DIR + "/tracked_tasks"
19
+ # If files are re-indexed during re-indexing these tasks are save here
20
+ RIDX_LOG = SYC_DIR + "/reindex.log"
21
+
22
+ # Reads the default task directory from the DEFAULT_TASKS_DIR file if it
23
+ # exists. If it exist but doesn't contain a valid directory ~/.tasks is
24
+ # returned as default tasks directory
25
+ dir = File.read(DEFAULT_TASKS_DIR) if File.exists? DEFAULT_TASKS_DIR
26
+ # User specified default working directory
27
+ work_dir = dir if not dir.nil? and not dir.empty? and File.exists? dir
28
+ # Set eather user defined work directory or default
29
+ WORK_DIR = work_dir.nil? ? File.expand_path('~/.tasks') : work_dir
30
+
31
+ # Logs a task regarding create, update, done, delete
32
+ def log_task(type, task)
33
+ File.open(TASKS_LOG, 'a') do |file|
34
+ log_entry = "#{type.to_s};"
35
+ log_entry += "#{task.id};#{task.dir};"
36
+ log_entry += "#{task.title.gsub(';', '\'semicolon\'')};"
37
+ log_entry += "#{Time.now};"
38
+ log_entry += "#{Time.now}"
39
+ file.puts log_entry
40
+ end
41
+ end
42
+
43
+ # Logs the work time
44
+ def log_work_time(type, work_time)
45
+ today = Time.now
46
+ begins = Time.local(today.year,
47
+ today.mon,
48
+ today.day,
49
+ work_time[0],
50
+ work_time[1],
51
+ 0)
52
+ ends = Time.local(today.year,
53
+ today.mon,
54
+ today.day,
55
+ work_time[2],
56
+ work_time[3],
57
+ 0)
58
+ entry = "#{type};-1;;work;#{begins};#{ends}\n"
59
+ logs = File.read(TASKS_LOG)
60
+ return if logs.scan(entry)[0]
61
+ time_pat = "#{today.strftime("%Y-%m-%d")} \\d{2}:\\d{2}:\\d{2} [+-]\\d{4}"
62
+ pattern = %r{#{type};-1;;work;#{time_pat};#{time_pat}\n}
63
+ log = logs.scan(pattern)[0]
64
+ if log and logs.sub!(log, entry)
65
+ File.write(TASKS_LOG, logs)
66
+ else
67
+ File.open(TASKS_LOG, 'a') {|f| f.puts entry}
68
+ end
69
+ end
70
+
71
+ # Logs meeting times
72
+ def log_meetings(type, busy_time, meetings)
73
+ today = Time.now
74
+ logs = File.read(TASKS_LOG)
75
+ time_pat = "#{today.strftime("%Y-%m-%d")} \\d{2}:\\d{2}:\\d{2} [+-]\\d{4}"
76
+ pattern = %r{#{type};-2;;.*?;#{time_pat};#{time_pat}\n}
77
+ logs.gsub!(pattern, "")
78
+ busy_time.each_with_index do |busy,i|
79
+ begins = Time.local(today.year,today.mon,today.day,busy[0],busy[1],0)
80
+ ends = Time.local(today.year,today.mon,today.day,busy[2],busy[3],0)
81
+ meeting = meetings[i] ? meetings[i] : "Meeting #{i}"
82
+ logs << "#{type};-2;;#{meeting};#{begins};#{ends}\n"
83
+ end
84
+ File.write(TASKS_LOG, logs)
85
+ end
86
+
87
+ # Checks whether all files are available that are needed for syctask's
88
+ # operation
89
+ def check_environment
90
+ FileUtils.mkdir_p WORK_DIR unless File.exists? WORK_DIR
91
+ unless viable?
92
+ unless get_files(File.expand_path("~"), "*.task").empty?
93
+ # Backup ARGV content
94
+ args = []
95
+ ARGV.each {|arg| args << arg} unless ARGV.empty?
96
+ ARGV.clear
97
+ puts
98
+ puts "Warning:"
99
+ puts "-------"
100
+ puts "There are missing system files of syc-task, even though tasks "+
101
+ "are available."
102
+ puts "If you have upgraded from version 0.0.7 or below than this is "+
103
+ "due to a changed\nfile structure. For changes in version "+
104
+ "greater 0.0.7 see"
105
+ puts "--> https://rubygems.org/gems/syc-task"
106
+ puts "Or you have accidentially deleted system files. In both cases "+
107
+ "re-indexing\nwill recover syc-task."
108
+ print "Do you want to recover syc-task (y/n)? "
109
+ answer = gets.chomp
110
+ exit -1 unless answer.downcase == "y"
111
+ reindex_tasks(File.expand_path("~"))
112
+ puts "Successfully recovered syc-task"
113
+ puts "-> A log file of re-indexed tasks can be found at\n"+
114
+ "#{RIDX_LOG}" if File.exists? RIDX_LOG
115
+ print "Press any key to continue "
116
+ gets
117
+ # Restore ARGV content
118
+ args.each {|arg| ARGV << arg} unless args.empty?
119
+ else
120
+ FileUtils.mkdir_p SYC_DIR unless File.exists? SYC_DIR
121
+ File.write(ID, "0")
122
+ end
123
+ end
124
+ end
125
+
126
+ # Checks if system files are available that are needed for running syc-task.
127
+ # Returns true if neccessary system files are available, otherwise false.
128
+ def viable?
129
+ File.exists? SYC_DIR and File.exists? ID
130
+ end
131
+
132
+ # Re-indexing of tasks is done when tasks are available but SYC_DIR or ID file
133
+ # is missing. The ID file contains the last issued task ID. The ID file is
134
+ # referenced for obtaining the next ID for a new task. Re-indexing is done as
135
+ # follows:
136
+ # * Retrieve all tasks in and below the given directory *root*
137
+ # * Determine the highest ID number and add it to the ID file
138
+ # * Determine all tasks that don't have a unique ID
139
+ # * Re-index all tasks not having a unique ID and rename the file names
140
+ # accordingly
141
+ # * Adjust the IDs in the planned_tasks, tasks.log and tracked_tasks files
142
+ # * Copy all system files planned_tasks, time_schedule, tasks.log, id to the
143
+ # SYC_DIR directory if not already in the SYC_DIR directory. This should
144
+ # only be if upgrading from version 0.0.7 and below.
145
+ def reindex_tasks(root)
146
+ FileUtils.mkdir_p SYC_DIR unless File.exists? SYC_DIR
147
+ new_id = {}
148
+ to_be_renamed = {}
149
+ root = File.expand_path(root)
150
+ puts "-> Collect task files..."
151
+ task_files = task_files(root)
152
+ puts "-> Restore ID counter..."
153
+ initialize_id(task_files)
154
+ print "-> Start re-indexing now..."
155
+ collect_by_id(task_files).each do |id, files|
156
+ next if files.size < 2
157
+ files.each_with_index do |file,i|
158
+ next if i == 0 # need to re-index only second and following tasks
159
+ result = reindex_task(file)
160
+ # associate old id to new id and dir name
161
+ if new_id[result[:old_id]].nil?
162
+ new_id[result[:old_id]] = {result[:dirname] => result[:new_id]}
163
+ else
164
+ new_id[result[:old_id]][result[:dirname]] = result[:new_id]
165
+ end
166
+ # assign tmp_file to new_file for later renaming
167
+ to_be_renamed[result[:tmp_file]] = result[:new_file]
168
+ # document the re-indexing of tasks
169
+ log_reindexing(result[:old_id], result[:new_id], result[:new_file])
170
+ end
171
+ end
172
+ to_be_renamed.each {|old_name,new_name| File.rename(old_name, new_name)}
173
+ puts
174
+ puts "-> Update task log file"
175
+ update_tasks_log(root, new_id)
176
+ puts "-> Update planned tasks files"
177
+ update_planned_tasks(root, new_id)
178
+ puts "-> Move schedule files..."
179
+ move_time_schedule_files(root)
180
+ puts "-> Update tracked task file..."
181
+ update_tracked_task(root)
182
+ end
183
+
184
+ # Re-indexes the tasks' IDs and renames the task files to match the new ID.
185
+ # The orginal file is deleted. Returns old_id, new_id, tmp_file_name and
186
+ # new_file_name. The task is save with the tmp_file_name in case the new ID
187
+ # and hence the new_file_name exists already from a not yet re-indexed task.
188
+ # After all tasks are re-indexed the tmp_file_names have to be renamed to the
189
+ # new_file_names. The renaming is in the responsibility of the calling method.
190
+ def reindex_task(file)
191
+ print "."
192
+ task = File.read(file)
193
+ old_id = task.scan(/(?<=^id: )\d+$/)[0]
194
+ new_id = next_id.to_s
195
+ task.gsub!(/(?<=^id: )\d+$/, new_id)
196
+ dirname = File.dirname(file)
197
+ new_file = "#{dirname}/#{new_id}.task"
198
+ tmp_file = "#{new_file}_"
199
+ File.write(tmp_file, task)
200
+ File.delete(file)
201
+ {old_id: old_id,
202
+ new_id: new_id,
203
+ tmp_file: tmp_file,
204
+ new_file: new_file,
205
+ dirname: dirname}
206
+ end
207
+
208
+ # Determines the greatest task ID out of the provided tasks and saves it to
209
+ # the ID file
210
+ def initialize_id(tasks)
211
+ pattern = %r{(?<=\/)\d+(?=\.task)}
212
+ tasks.sort_by! {|t| t.scan(pattern)[0].to_i}
213
+ save_id(tasks[tasks.size-1].scan(pattern)[0].to_i)
214
+ end
215
+
216
+ # Saves the ids to ids file
217
+ def save_ids(id, file)
218
+ entry = "#{id},#{file}"
219
+ return if File.exists? IDS and not File.read(IDS).scan(entry).empty?
220
+ File.open(IDS, 'a') {|f| f.puts entry}
221
+ end
222
+
223
+ # Save the id to the ID file. Returns the id when save was successful
224
+ def save_id(id)
225
+ File.write(ID,id)
226
+ id
227
+ end
228
+
229
+ # Retrieve the next unassigned task id
230
+ def next_id
231
+ id = File.read(ID).to_i + 1
232
+ save_id(id)
233
+ id
234
+ end
235
+
236
+ # Logs if a task is re-indexed
237
+ def log_reindexing(old_id, new_id, file)
238
+ entry = "#{old_id},#{new_id},#{file}"
239
+ return if File.exists? RIDX_LOG and not File.read(RIDX_LOG).
240
+ scan(entry).empty?
241
+ File.open(RIDX_LOG, 'a') {|f| f.puts entry}
242
+ end
243
+
244
+ # Updates the tasks.log file if tasks are re-indexed with the task's new ids
245
+ def update_tasks_log(dir, new_ids)
246
+ tasks_log_files(dir).each do |file|
247
+ logs = File.readlines(file)
248
+ logs.each_with_index do |log,i|
249
+ type = log.scan(/^.*?(?=;)/)[0]
250
+ logs[i] = log.sub!("-",";") if log.scan(/(?<=^#{type};)\d+-/)[0]
251
+ old_id = log.scan(/(?<=^#{type};)\d+(?=;)/)[0]
252
+ next unless new_ids[old_id]
253
+ task_dir = log.scan(/(?<=^#{type};#{old_id};).*?(?=;)/)[0]
254
+ next unless new_ids[old_id][task_dir]
255
+ logs[i] = log.sub("#{old_id};#{task_dir}",
256
+ "#{new_ids[old_id][task_dir]};#{task_dir}")
257
+ end
258
+ if file == TASKS_LOG
259
+ File.write(TASKS_LOG, logs.join)
260
+ else
261
+ #TODO only append a line if it is not already available in TASKS_LOG
262
+ File.open(TASKS_LOG, 'a') {|f| f.puts logs.join}
263
+ FileUtils.rm file
264
+ end
265
+ end
266
+ end
267
+
268
+ # TODO delete
269
+ def update_tasks_log_old(dir, old_id, new_id, file)
270
+ old_entry = "#{old_id}-#{File.dirname(file)}"
271
+ # Append '/' to dir name so already updated task is not subsequently updated
272
+ new_entry = "#{new_id}-#{File.dirname(file)}/"
273
+ @tasks_log_files = tasks_log_files(dir) if @tasks_log_files.nil?
274
+ @tasks_log_files.each do |f|
275
+ tasks_log = File.read(f).gsub(old_entry, new_entry)
276
+ File.write(f, tasks_log)
277
+ end
278
+ end
279
+
280
+ # Replaces the old ids with the new ids in the planned tasks files. A planned
281
+ # tasks file has the form '2013-03-03_planned_tasks' and lives until syctask's
282
+ # version 0.0.7 in ~/.tasks directory. From version 0.1.0 on the planned tasks
283
+ # files live in the ~/.syc/syctask directory. So the calling method has the
284
+ # responsibility to copy or move the planned tasks files after they have been
285
+ # updated to the new planned tasks directory.
286
+ def update_planned_tasks(dir, new_ids)
287
+ planned_tasks_files(dir).each do |file|
288
+ tasks = File.readlines(file)
289
+ tasks.each_with_index do |task,i|
290
+ task_dir, old_id = task.chomp.split(',')
291
+ next unless new_ids[old_id]
292
+ next unless new_ids[old_id][task_dir]
293
+ tasks[i] = "#{task_dir},#{new_ids[old_id][task_dir]}"
294
+ end
295
+ File.write("#{SYC_DIR}/#{File.basename(file)}", tasks.join("\n"))
296
+ end
297
+ end
298
+
299
+ # TODO delete
300
+ def update_planned_tasks_old(dir, old_id, new_id, file)
301
+ old_entry = "#{File.dirname(file)},#{old_id}"
302
+ # Append '/' to dir name so already updated task is not subsequently updated
303
+ new_entry = "#{File.dirname(file)}/,#{new_id}"
304
+ @planned_tasks_files = planned_tasks_files(dir) if @planned_tasks_files.nil?
305
+ @planned_tasks_files.each do |file|
306
+ planned_tasks = File.read(file).gsub(old_entry, new_entry)
307
+ File.write(file, planned_tasks)
308
+ end
309
+ end
310
+
311
+ # Updates tracked_tasks file if task has been re-indexed with new ID
312
+ def update_tracked_task(dir)
313
+ @tracked = get_files(dir, "tracked_tasks") if @tracked.nil?
314
+ return if @tracked.empty?
315
+ task = File.read(@tracked[0])
316
+ if File.exists? RIDX_LOG
317
+ old_id = task.scan(/(?<=id: )\d+$/)
318
+ old_dir = task.scan(/(?<=dir: ).*$/)
319
+ return if old_id.empty? or old_dir.empty?
320
+ pattern = %r{(?<=#{old_id[0]},)\d+(?=,#{old_dir[0]}\/\d+\.task)}
321
+ new_id = File.read(RIDX_LOG).scan(pattern)
322
+ task.gsub!("id: #{old_id}", "id: #{new_id}")
323
+ end
324
+ File.write(TRACKED_TASK, task)
325
+ FileUtils.rm @tracked[0] unless TRACKED_TASK == @tracked[0]
326
+ end
327
+
328
+ # Extracts tasks that have no unique id
329
+ def collect_by_id(tasks)
330
+ extract = {}
331
+ tasks.each do |task|
332
+ id = task.scan(/(?<=\/)\d+(?=\.task$)/)[0]
333
+ extract[id].nil? ? extract[id] = [task] : extract[id] << task
334
+ end
335
+ extract
336
+ end
337
+
338
+ # Retrieves all task files in and below the provided dir. Returns an array of
339
+ # task files
340
+ def task_files(dir)
341
+ get_files(dir, "*.task").keep_if {|file| file.match /\d+\.task$/}
342
+ end
343
+
344
+ # Retrieves all planned task files in and below the given directory
345
+ def planned_tasks_files(dir)
346
+ pattern = %r{\d{4}-\d{2}-\d{2}_planned_tasks}
347
+ get_files(dir, "*planned_tasks").keep_if {|f| f.match(pattern)}
348
+ end
349
+
350
+ # Retrieves all schedule files in and below the given directory
351
+ def time_schedule_files(dir)
352
+ pattern = %r{\d{4}-\d{2}-\d{2}_time_schedule}
353
+ get_files(dir, "*time_schedule").keep_if {|f| f.match(pattern)}
354
+ end
355
+
356
+ # Retrieves als tasks.log files in and below the given directory
357
+ def tasks_log_files(dir)
358
+ get_files(dir, "tasks.log")
359
+ end
360
+
361
+ # Retrieves all files that meet the pattern in and below the given directory
362
+ def get_files(dir, pattern)
363
+ original_dir = File.expand_path(".")
364
+ Dir.chdir(dir)
365
+ files = Dir.glob("**/#{pattern}", File::FNM_DOTMATCH).map do |f|
366
+ File.expand_path(f)
367
+ end
368
+ Dir.chdir(original_dir)
369
+ files
370
+ end
371
+
372
+ # Retrieve all directories that contain tasks
373
+ def get_task_dirs(dir)
374
+ original_dir = File.expand_path(".")
375
+ Dir.chdir(dir)
376
+ dirs = Dir.glob("**/*.task", File::FNM_DOTMATCH).map do |f|
377
+ File.dirname(File.expand_path(f))
378
+ end
379
+ Dir.chdir(original_dir)
380
+ dirs.uniq
381
+ end
382
+
383
+ # Retrieves all directories that contain tasks and the count of contained
384
+ # tasks in and below the provided directory
385
+ def get_task_dirs_and_count(dir)
386
+ original_dir = File.expand_path(".")
387
+ Dir.chdir(dir)
388
+ dirs_and_count = Hash.new(0)
389
+ Dir.glob("**/*.task", File::FNM_DOTMATCH).each do |f|
390
+ dirname = File.dirname(File.expand_path(f))
391
+ dirs_and_count[dirname] += 1
392
+ end
393
+ Dir.chdir(original_dir)
394
+ dirs_and_count
395
+ end
396
+
397
+ # Moves the tasks.log file to the system directory if not there. Should only
398
+ # be if upgrading from version 0.0.7 and below
399
+ def move_task_log_file(dir)
400
+ @tasks_log_files = tasks_log_files(dir) if @tasks_log_files.nil?
401
+ @tasks_log_files.each do |f|
402
+ next if f == TASKS_LOG
403
+ tasks_log = File.read(f)
404
+ File.open(TASKS_LOG, 'a') {|t| t.puts tasks_log}
405
+ FileUtils.mv(f, "#{f}_#{Time.now.strftime("%y%m%d")}")
406
+ end
407
+ end
408
+
409
+ # Moves the planned tasks file to the system directory if not there. Should
410
+ # only be if upgrading from version 0.0.7 and below
411
+ def move_planned_tasks_files(dir)
412
+ @planned_tasks_files = planned_tasks_files(dir) if @planned_tasks_files.nil?
413
+ @planned_tasks_files.each do |file|
414
+ to_file = "#{SYC_DIR}/#{File.basename(file)}"
415
+ next if file == to_file
416
+ FileUtils.mv file, to_file
417
+ end
418
+ end
419
+
420
+ # Moves the schedule file to the system directory if not there. Should
421
+ # only be if upgrading from version 0.0.7 and below
422
+ def move_time_schedule_files(dir)
423
+ @time_schedule_files = time_schedule_files(dir) if @time_schedule_files.nil?
424
+ @time_schedule_files.each do |file|
425
+ to_file = "#{SYC_DIR}/#{File.basename(file)}"
426
+ next if file == to_file
427
+ FileUtils.mv file, to_file
428
+ end
429
+ end
5
430
 
6
431
  end
@@ -1,7 +1,10 @@
1
1
  require_relative 'times.rb'
2
2
  require_relative 'meeting.rb'
3
3
  require_relative '../sycstring/string_util.rb'
4
+ require_relative '../syctime/time_util.rb'
5
+
4
6
  include Sycstring
7
+ include Syctime
5
8
 
6
9
  module Syctask
7
10
 
@@ -85,7 +88,10 @@ module Syctask
85
88
  title = titles[index] ? titles[index] : "Meeting #{index}"
86
89
  @meetings << Syctask::Meeting.new(busy, title)
87
90
  end
88
- raise Exception, "Busy times have to be within work time" unless within?(@meetings, @starts, @ends)
91
+ raise Exception,
92
+ "Busy times have to be within work time" unless within?(@meetings,
93
+ @starts,
94
+ @ends)
89
95
  @tasks = tasks
90
96
  end
91
97
 
@@ -95,8 +101,10 @@ module Syctask
95
101
  assignments.each do |assignment|
96
102
  number = assignment[0].upcase.ord - "A".ord
97
103
  return false if number < 0 or number > @meetings.size
98
- assignment[1].split(',').each do |index|
99
- @meetings[number].tasks << @tasks[index.to_i] if @tasks[index.to_i]
104
+ @meetings[number].tasks.clear
105
+ assignment[1].split(',').each do |id|
106
+ index = @tasks.find_index{|task| task.id == id.to_i}
107
+ @meetings[number].tasks << @tasks[index] if index and @tasks[index]
100
108
  end
101
109
  @meetings[number].tasks.uniq!
102
110
  end
@@ -109,7 +117,13 @@ module Syctask
109
117
  list << sprintf("%s", "--------\n").color(:red)
110
118
  meeting_number = "A"
111
119
  @meetings.each do |meeting|
112
- list << sprintf("%s - %s\n", meeting_number, meeting.title).color(:red)
120
+ hint = "-"
121
+ hint = "*" if time_between?(Time.now,
122
+ meeting.starts.time,
123
+ meeting.ends.time)
124
+ list << sprintf("%s %s %s\n", meeting_number,
125
+ hint,
126
+ meeting.title).color(:red)
113
127
  meeting_number.next!
114
128
  meeting.tasks.each do |task|
115
129
  task_color = task.done? ? :green : :blue
@@ -156,14 +170,19 @@ module Syctask
156
170
  # list
157
171
  def graph
158
172
  work_time, meeting_times = get_times
173
+
174
+ heading = sprintf("+++ %s - %s-%s +++", Time.now.strftime("%Y-%m-%d"),
175
+ @starts.time.strftime("%H:%M"),
176
+ @ends.time.strftime("%H:%M")).color(:blue)
177
+
159
178
  time_line = "|---" * (work_time[1]-work_time[0]) + "|"
160
179
  meeting_times.each do |time|
161
- time_line[time[0]..time[1]] = '/' * (time[1] - time[0]+1)
180
+ time_line[time[0]..time[1]-1] = '/' * (time[1] - time[0])
162
181
  end
163
182
 
164
183
  task_list, task_caption = assign_tasks_to_graph(time_line)
165
184
 
166
- [meeting_list, meeting_caption,
185
+ [heading.center(80), meeting_list, meeting_caption,
167
186
  colorize(time_line), time_caption,
168
187
  task_caption, task_list]
169
188
  end
@@ -214,23 +233,50 @@ module Syctask
214
233
  # Assigns the tasks to the timeline in alternation x and o subsequent tasks.
215
234
  # Returns the task list and the task caption
216
235
  def assign_tasks_to_graph(time_line)
236
+ done_tasks = []
217
237
  unscheduled_tasks = []
218
238
  signs = ['x','o']
219
239
  positions = {}
220
- position = 0
240
+ current_time = Time.now
221
241
  unassigned_tasks.each.with_index do |task, index|
222
- duration = task.duration.to_i
223
- free_time = scan_free(time_line, duration, position)
224
- position = free_time[0]
225
- if position.nil?
242
+ if task.done? or not task.today?
243
+ done_tasks << task
244
+ next
245
+ else
246
+ round = task.remaining.to_i % 900 == 0 ? 0 : 0.5
247
+ duration = [(task.remaining.to_i/900+round).round, 1].max
248
+ position = [0, position_for_time(current_time)].max
249
+ end
250
+ free_time = scan_free(time_line, 1, position)
251
+ if free_time[0].nil?
226
252
  unscheduled_tasks << task
227
253
  next
228
254
  end
229
- time_line[position..(position + duration-1)] =
230
- signs[index%2] * duration
231
- positions[position] = task.id
255
+ 0.upto(duration-1) do |i|
256
+ break unless free_time[i]
257
+ time_line[free_time[i]] = signs[index%2]
258
+ end
259
+ positions[free_time[0]] = task.id
232
260
  end
233
261
 
262
+ unless done_tasks.empty?
263
+ end_position = position_for_time(current_time)
264
+ total_duration = 0
265
+ done_tasks.each_with_index do |task,index|
266
+ free_time = scan_free(time_line, 1, 0, end_position)
267
+ lead_time = task.duration.to_i - task.remaining.to_i + 0.0
268
+ max_duration = [free_time.size - (done_tasks.size - index - 1), 1].max
269
+ duration = [(lead_time/900).round, 1].max
270
+ total_duration += duration = [duration, max_duration].min
271
+ 0.upto(duration-1) do |i|
272
+ break unless free_time[i]
273
+ time_line[free_time[i]] = signs[index%2]
274
+ end
275
+ positions[free_time[0]] = task.id if free_time[0]
276
+ end
277
+ end
278
+
279
+ # Create task list
234
280
  max_id_size = 1
235
281
  @tasks.each {|task| max_id_size = [task.id.to_s.size, max_id_size].max}
236
282
  max_ord_size = (@tasks.size - 1).to_s.size
@@ -238,20 +284,26 @@ module Syctask
238
284
  task_list = sprintf("%s", "Tasks\n").color(:blue)
239
285
  task_list << sprintf("%s", "-----\n").color(:blue)
240
286
  @tasks.each.with_index do |task, i|
241
- if task.done?
287
+ if task.done? or not task.today?
242
288
  color = :green
243
289
  elsif unscheduled_tasks.find_index(task)
244
290
  color = UNSCHEDULED_COLOR
245
291
  else
246
292
  color = WORK_COLOR
247
293
  end
294
+
295
+ hint = "-"
296
+ hint = "~" unless task.today?
297
+ hint = "*" if task.tracked?
298
+
248
299
  offset = max_ord_size + max_id_size + 5
249
300
  title = split_lines(task.title, 80-offset)
250
301
  title = title.chomp.gsub(/\n/, "\n#{' '*offset}")
251
- task_list << sprintf("%#{max_ord_size}d: %#{max_id_size}s - %s\n",
252
- i, task.id, title).color(color)
302
+ task_list << sprintf("%#{max_ord_size}d: %#{max_id_size}s %s %s\n",
303
+ i, task.id, hint, title).color(color)
253
304
  end
254
305
 
306
+ # Create task caption
255
307
  task_caption = ""
256
308
  create_caption(positions).each do |caption|
257
309
  task_caption << sprintf("%s\n", caption).color(WORK_COLOR)
@@ -267,6 +319,7 @@ module Syctask
267
319
  counter = 0
268
320
  lines = [""]
269
321
  positions.each do |position,id|
322
+ next unless position
270
323
  line_id = next_line(position,lines,counter)
271
324
  legend = ' ' * [0, position - lines[line_id].size].max + id.to_s
272
325
  lines[line_id] += legend
@@ -295,18 +348,28 @@ module Syctask
295
348
  return lines.size - 1
296
349
  end
297
350
 
351
+ # Determines the position within the time line for the given time. Each
352
+ # position represents a 15 minute duration. Minutes below 8 will be rounded
353
+ # down otherwise rounded up.
354
+ def position_for_time(time)
355
+ diff = @starts.diff(time)
356
+ # as the time line is always rounded down to the full hour we have to add
357
+ # the minutes with @starts.m / 15 to get the position for the current time
358
+ ((diff[0] * 60 + diff[1]) / 15.0).round + @starts.m / 15
359
+ end
360
+
298
361
  # Scans the schedule for free time where a task can be added to. Count
299
362
  # specifies the length of the free time and the position where to start
300
363
  # scanning within the graph
301
- def scan_free(graph, count, position)
364
+ def scan_free(graph, count, starts, ends=graph.size)
302
365
  pattern = /(?!\/)[\|-]{#{count}}(?<=-|\||\/)/
303
366
 
304
367
  positions = []
305
- index = position
306
- while index and index < graph.size
368
+ index = starts
369
+ while index and index < ends
307
370
  index = graph.index(pattern, index)
308
371
  if index
309
- positions << index
372
+ positions << index if index < ends
310
373
  index += 1
311
374
  end
312
375
  end