syc-task 0.0.7 → 0.1.15

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