timr 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.ackrc +9 -0
- data/.editorconfig +1 -0
- data/.env.example +7 -0
- data/.github/CONTRIBUTING.md +32 -0
- data/.github/ISSUE_TEMPLATE.md +13 -0
- data/.gitignore +8 -2
- data/.rdoc_options +21 -0
- data/.travis.yml +10 -7
- data/Gemfile +8 -0
- data/README.md +216 -3
- data/bin/.gitignore +2 -0
- data/bin/README.md +17 -0
- data/bin/build.sh +14 -0
- data/bin/build_api.sh +14 -0
- data/bin/build_coverage.sh +23 -0
- data/bin/build_info.sh +27 -0
- data/bin/build_man.sh +41 -0
- data/bin/clean.sh +14 -0
- data/bin/dev_setup.sh +19 -0
- data/bin/install.sh +49 -0
- data/bin/publish +38 -0
- data/bin/release.sh +35 -0
- data/bin/test.sh +19 -0
- data/bin/timr +20 -40
- data/bin/timr_bash_completion.sh +337 -0
- data/bin/uninstall.sh +24 -0
- data/lib/timr.rb +36 -8
- data/lib/timr/command/basic_command.rb +170 -0
- data/lib/timr/command/continue_command.rb +86 -0
- data/lib/timr/command/help_command.rb +137 -0
- data/lib/timr/command/log_command.rb +297 -0
- data/lib/timr/command/pause_command.rb +89 -0
- data/lib/timr/command/pop_command.rb +176 -0
- data/lib/timr/command/push_command.rb +141 -0
- data/lib/timr/command/report_command.rb +689 -0
- data/lib/timr/command/start_command.rb +172 -0
- data/lib/timr/command/status_command.rb +198 -0
- data/lib/timr/command/stop_command.rb +127 -0
- data/lib/timr/command/task_command.rb +318 -0
- data/lib/timr/command/track_command.rb +381 -0
- data/lib/timr/command/version_command.rb +18 -0
- data/lib/timr/duration.rb +159 -0
- data/lib/timr/exception/timr_error.rb +113 -0
- data/lib/timr/ext/time.rb +12 -0
- data/lib/timr/helper/datetime_helper.rb +128 -0
- data/lib/timr/helper/terminal_helper.rb +58 -0
- data/lib/timr/helper/translation_helper.rb +45 -0
- data/lib/timr/model/basic_model.rb +287 -0
- data/lib/timr/model/config.rb +48 -0
- data/lib/timr/model/foreign_id_db.rb +84 -0
- data/lib/timr/model/stack.rb +161 -0
- data/lib/timr/model/task.rb +1039 -0
- data/lib/timr/model/track.rb +589 -0
- data/lib/timr/progressbar.rb +41 -0
- data/lib/timr/simple_opt_parser.rb +230 -0
- data/lib/timr/status.rb +70 -0
- data/lib/timr/table.rb +88 -0
- data/lib/timr/timr.rb +500 -558
- data/lib/timr/version.rb +4 -15
- data/man/.gitignore +2 -0
- data/man/_footer +3 -0
- data/man/timr-continue.1 +48 -0
- data/man/timr-continue.1.ronn +39 -0
- data/man/timr-ftime.7 +77 -0
- data/man/timr-ftime.7.ronn +57 -0
- data/man/timr-log.1 +109 -0
- data/man/timr-log.1.ronn +87 -0
- data/man/timr-pause.1 +56 -0
- data/man/timr-pause.1.ronn +45 -0
- data/man/timr-pop.1 +66 -0
- data/man/timr-pop.1.ronn +53 -0
- data/man/timr-push.1 +25 -0
- data/man/timr-push.1.ronn +20 -0
- data/man/timr-report.1 +228 -0
- data/man/timr-report.1.ronn +193 -0
- data/man/timr-start.1 +100 -0
- data/man/timr-start.1.ronn +82 -0
- data/man/timr-status.1 +53 -0
- data/man/timr-status.1.ronn +42 -0
- data/man/timr-stop.1 +75 -0
- data/man/timr-stop.1.ronn +60 -0
- data/man/timr-task.1 +147 -0
- data/man/timr-task.1.ronn +115 -0
- data/man/timr-track.1 +109 -0
- data/man/timr-track.1.ronn +89 -0
- data/man/timr.1 +119 -0
- data/man/timr.1.ronn +68 -0
- data/timr.gemspec +18 -3
- data/timr.sublime-project +20 -1
- metadata +142 -23
- data/Makefile +0 -12
- data/Makefile.common +0 -56
- data/lib/timr/stack.rb +0 -81
- data/lib/timr/task.rb +0 -258
- data/lib/timr/track.rb +0 -167
- data/lib/timr/window.rb +0 -259
- data/lib/timr/window_help.rb +0 -41
- data/lib/timr/window_tasks.rb +0 -30
- data/lib/timr/window_test.rb +0 -20
- data/lib/timr/window_timeline.rb +0 -33
- data/tests/tc_stack.rb +0 -121
- data/tests/tc_task.rb +0 -190
- data/tests/tc_track.rb +0 -144
- data/tests/tc_window.rb +0 -428
- data/tests/ts_all.rb +0 -6
@@ -0,0 +1,41 @@
|
|
1
|
+
|
2
|
+
module TheFox
|
3
|
+
module Timr
|
4
|
+
|
5
|
+
# See [ruby-progressbar Issue #131](https://github.com/jfelchner/ruby-progressbar/issues/131).
|
6
|
+
class ProgressBar
|
7
|
+
|
8
|
+
def initialize(options = Hash.new)
|
9
|
+
@total = options.fetch(:total, 100)
|
10
|
+
@progress = options.fetch(:progress, 0)
|
11
|
+
@length = options.fetch(:length, 10)
|
12
|
+
@progress_mark = options.fetch(:progress_mark, ?#)
|
13
|
+
@remainder_mark = options.fetch(:remainder_mark, ?-)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Render ProgressBar as String.
|
17
|
+
def render(progress = nil)
|
18
|
+
if progress
|
19
|
+
@progress = progress
|
20
|
+
end
|
21
|
+
|
22
|
+
progress_f = @progress.to_f / @total.to_f
|
23
|
+
if progress_f > 1.0
|
24
|
+
progress_f = 1.0
|
25
|
+
end
|
26
|
+
|
27
|
+
progress_f = @length.to_f * progress_f
|
28
|
+
|
29
|
+
progress_s = @progress_mark * progress_f
|
30
|
+
|
31
|
+
remain_l = @length - progress_s.length
|
32
|
+
|
33
|
+
remain_s = @remainder_mark * remain_l
|
34
|
+
|
35
|
+
'%s%s' % [progress_s, remain_s]
|
36
|
+
end
|
37
|
+
|
38
|
+
end # class ProgressBar
|
39
|
+
|
40
|
+
end # module Timr
|
41
|
+
end # module TheFox
|
@@ -0,0 +1,230 @@
|
|
1
|
+
|
2
|
+
module TheFox
|
3
|
+
module Timr
|
4
|
+
|
5
|
+
# Simple Option Parser
|
6
|
+
#
|
7
|
+
# Because OptParse sucks. Parsing arguments should be easy. :(
|
8
|
+
#
|
9
|
+
# Covered Usecases
|
10
|
+
#
|
11
|
+
# - -a
|
12
|
+
# - --all
|
13
|
+
# - -a --all
|
14
|
+
# - -a -b val
|
15
|
+
# - -a -b val cmd
|
16
|
+
# - -a -b val cmd1 cmd2
|
17
|
+
# - -a -b val1 val2
|
18
|
+
# - -a -b val1 val2 cmd1 cmd2
|
19
|
+
# - -ab
|
20
|
+
# - -ab val
|
21
|
+
#
|
22
|
+
# ### Usage
|
23
|
+
# ```
|
24
|
+
# optparser = SimpleOptParser.new
|
25
|
+
# optparser.register_option(['-a'])
|
26
|
+
# optparser.register_option(['-b'])
|
27
|
+
# optparser.register_option(['-c'], 1)
|
28
|
+
# opts = optparser.parse('-a -b -c val')
|
29
|
+
# ```
|
30
|
+
class SimpleOptParser
|
31
|
+
|
32
|
+
# Parsed Options (+Commands)
|
33
|
+
attr_reader :options
|
34
|
+
|
35
|
+
# Not Recognized Options
|
36
|
+
attr_reader :unknown_options
|
37
|
+
|
38
|
+
def initialize
|
39
|
+
# -a => 0
|
40
|
+
# --all => 0
|
41
|
+
# -b => 1
|
42
|
+
@valid_options = Hash.new
|
43
|
+
|
44
|
+
# a => 0
|
45
|
+
# all => 0
|
46
|
+
# b => 1
|
47
|
+
# @valid_options_without_prefix = Hash.new
|
48
|
+
|
49
|
+
# Parsed Options (+Commands)
|
50
|
+
@options = Array.new
|
51
|
+
|
52
|
+
# Not Recognized Options
|
53
|
+
@unknown_options = Array.new
|
54
|
+
end
|
55
|
+
|
56
|
+
# `aliases` (Array)
|
57
|
+
#
|
58
|
+
# `number_values`
|
59
|
+
#
|
60
|
+
# - 0 = no value, just a switch
|
61
|
+
# - -1 = unlimited
|
62
|
+
# - n = n values
|
63
|
+
def register_option(aliases, number_values = 0)
|
64
|
+
aliases.each do |ali|
|
65
|
+
@valid_options[ali] = number_values
|
66
|
+
|
67
|
+
# if ali[0] == '-'
|
68
|
+
# if ali[1] == '-'
|
69
|
+
# # --arg
|
70
|
+
# # Cut the two first characters.
|
71
|
+
# @valid_options_without_prefix[ali[2..-1]] = number_values
|
72
|
+
# else
|
73
|
+
# # -a
|
74
|
+
# # Cut only the first character.
|
75
|
+
# @valid_options_without_prefix[ali[1..-1]] = number_values
|
76
|
+
# end
|
77
|
+
# end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# `args` (Array|String)
|
82
|
+
def parse(args)
|
83
|
+
# Reset previous options.
|
84
|
+
@options = []
|
85
|
+
|
86
|
+
if args.is_a?(Array)
|
87
|
+
pre_argv = args
|
88
|
+
else
|
89
|
+
pre_argv = args.split(' ')
|
90
|
+
end
|
91
|
+
|
92
|
+
argv = Array.new
|
93
|
+
|
94
|
+
# Pre-process Special Argument (Compact)
|
95
|
+
# For example '-abcd' is '-a -b -c -d'.
|
96
|
+
pre_argv.each do |arg|
|
97
|
+
if arg[0] == '-'
|
98
|
+
if arg[1] == '-'
|
99
|
+
# Normal --arg
|
100
|
+
argv << arg
|
101
|
+
else
|
102
|
+
if arg.length > 2
|
103
|
+
# Special -ab
|
104
|
+
arg = arg[1..-1]
|
105
|
+
|
106
|
+
# Add each single argument separate.
|
107
|
+
# Array ['-abc'] will become ['-a', '-b', '-c'].
|
108
|
+
arg.length.times do |n|
|
109
|
+
argv << "-#{arg[n]}"
|
110
|
+
end
|
111
|
+
else
|
112
|
+
# Normal -a
|
113
|
+
argv << arg
|
114
|
+
end
|
115
|
+
end
|
116
|
+
else
|
117
|
+
# Commands, Values, etc
|
118
|
+
argv << arg
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
loop_c = 0 # Limit the loop.
|
123
|
+
while argv.length > 0 && loop_c < 1024
|
124
|
+
loop_c += 1
|
125
|
+
|
126
|
+
arg = argv.shift
|
127
|
+
|
128
|
+
if arg[0] == '-'
|
129
|
+
if @valid_options[arg]
|
130
|
+
# Recognized Argument
|
131
|
+
|
132
|
+
number_values = @valid_options[arg]
|
133
|
+
|
134
|
+
if number_values == 0
|
135
|
+
# No values. Like Command.
|
136
|
+
@options << [arg]
|
137
|
+
elsif number_values == -1
|
138
|
+
# Eat all the arguments. Nom nom nom.
|
139
|
+
arg_values = []
|
140
|
+
sub_loop_c = 0 # Limit the loop.
|
141
|
+
while argv.length > 0 && sub_loop_c < 1024
|
142
|
+
sub_loop_c += 1
|
143
|
+
|
144
|
+
arg = argv.shift
|
145
|
+
|
146
|
+
# When reaching the end.
|
147
|
+
unless arg
|
148
|
+
break
|
149
|
+
end
|
150
|
+
|
151
|
+
arg_values << arg
|
152
|
+
end
|
153
|
+
@options << arg_values
|
154
|
+
else
|
155
|
+
# n values.
|
156
|
+
|
157
|
+
arg_values = []
|
158
|
+
sub_loop_c = 0 # Limit the loop.
|
159
|
+
while argv.length > 0 && sub_loop_c < number_values && sub_loop_c < 1024
|
160
|
+
sub_loop_c += 1
|
161
|
+
|
162
|
+
val = argv.shift
|
163
|
+
|
164
|
+
# When reaching the end.
|
165
|
+
unless val
|
166
|
+
raise ArgumentError, "SimpleOptParser: Argument '#{arg}' expects #{number_values} value(s). Found #{sub_loop_c}."
|
167
|
+
end
|
168
|
+
|
169
|
+
if @valid_options[val]
|
170
|
+
raise ArgumentError, "SimpleOptParser: Argument '#{arg}' expects #{number_values} value(s). Found another Argument '#{val}'."
|
171
|
+
end
|
172
|
+
|
173
|
+
arg_values << val
|
174
|
+
end
|
175
|
+
|
176
|
+
if arg_values.length != number_values
|
177
|
+
raise ArgumentError, "SimpleOptParser: Argument '#{arg}' expects #{number_values} value(s), #{arg_values.length} given."
|
178
|
+
end
|
179
|
+
|
180
|
+
arg_values.unshift(arg)
|
181
|
+
@options << arg_values
|
182
|
+
end
|
183
|
+
else
|
184
|
+
# Unknown Arguments
|
185
|
+
# '-ab' arguments will be processed in pre_argv.
|
186
|
+
@unknown_options << arg
|
187
|
+
end
|
188
|
+
else
|
189
|
+
# Command
|
190
|
+
@options << [arg]
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
@options
|
195
|
+
end
|
196
|
+
|
197
|
+
# Do not re-parse. Verify an already parsed array.
|
198
|
+
def verify(opts)
|
199
|
+
opts.each do |opt_ar|
|
200
|
+
arg = opt_ar.first
|
201
|
+
|
202
|
+
number_values = @valid_options[arg]
|
203
|
+
if number_values.nil?
|
204
|
+
# Unknown Arguments
|
205
|
+
else
|
206
|
+
# Argument is valid.
|
207
|
+
|
208
|
+
# Check length.
|
209
|
+
if number_values == 0
|
210
|
+
if opt_ar.length > 0
|
211
|
+
raise ArgumentError, "SimpleOptParser: Argument '#{arg}' expects no values, #{opt_ar.length} given."
|
212
|
+
end
|
213
|
+
elsif number_values == -1
|
214
|
+
# Always OK. Keep eating, Pacman.
|
215
|
+
else
|
216
|
+
if number_values != opt_ar.length
|
217
|
+
raise ArgumentError, "SimpleOptParser: Argument '#{arg}' expects #{number_values} value(s), #{opt_ar.length} given."
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
true
|
224
|
+
end
|
225
|
+
|
226
|
+
end # class SimpleOptParser
|
227
|
+
|
228
|
+
end # module Timr
|
229
|
+
end # module TheFox
|
230
|
+
|
data/lib/timr/status.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
|
2
|
+
require 'term/ansicolor'
|
3
|
+
|
4
|
+
module TheFox
|
5
|
+
module Timr
|
6
|
+
|
7
|
+
# Used as [Task](rdoc-ref:TheFox::Timr::Model::Task) and [Track](rdoc-ref:TheFox::Timr::Model::Track) Status.
|
8
|
+
#
|
9
|
+
# - `R` running
|
10
|
+
# - `S` stopped
|
11
|
+
# - `P` paused. It's actually stopped but with an additional flag.
|
12
|
+
# - `-` (dash) not started
|
13
|
+
# - `U` unknown
|
14
|
+
class Status
|
15
|
+
|
16
|
+
include Term::ANSIColor
|
17
|
+
|
18
|
+
# Source Data
|
19
|
+
attr_reader :short_status
|
20
|
+
|
21
|
+
# Resolved by `short_status`. See `set_long_status` method.
|
22
|
+
attr_reader :long_status
|
23
|
+
|
24
|
+
def initialize(short_status)
|
25
|
+
@short_status = short_status
|
26
|
+
|
27
|
+
@long_status = nil
|
28
|
+
set_long_status
|
29
|
+
end
|
30
|
+
|
31
|
+
# Use `term/ansicolor` to colorize the Long Status.
|
32
|
+
def colorized
|
33
|
+
case @short_status
|
34
|
+
when ?R
|
35
|
+
green(@long_status)
|
36
|
+
when ?S
|
37
|
+
red(@long_status)
|
38
|
+
else
|
39
|
+
@long_status
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# To String
|
44
|
+
def to_s
|
45
|
+
long_status
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def set_long_status
|
51
|
+
@long_status = case @short_status
|
52
|
+
when ?-
|
53
|
+
'not started'
|
54
|
+
when ?R
|
55
|
+
'running'
|
56
|
+
when ?S
|
57
|
+
'stopped'
|
58
|
+
when ?P
|
59
|
+
'paused'
|
60
|
+
when ?U
|
61
|
+
'unknown'
|
62
|
+
else
|
63
|
+
'unknown'
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
end # class Status
|
68
|
+
|
69
|
+
end # module Timr
|
70
|
+
end # module TheFox
|
data/lib/timr/table.rb
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
|
2
|
+
module TheFox
|
3
|
+
module Timr
|
4
|
+
|
5
|
+
class Table
|
6
|
+
|
7
|
+
# Holds all rows.
|
8
|
+
attr_reader :rows
|
9
|
+
|
10
|
+
def initialize(options = Hash.new)
|
11
|
+
@headings = options.fetch(:headings, Array.new)
|
12
|
+
|
13
|
+
@rows = Array.new
|
14
|
+
end
|
15
|
+
|
16
|
+
# Append a row.
|
17
|
+
def <<(row)
|
18
|
+
col_n = 0
|
19
|
+
row.each do |col|
|
20
|
+
header = @headings[col_n]
|
21
|
+
if header
|
22
|
+
header[:format] ||= '%s'
|
23
|
+
header[:label] ||= ''
|
24
|
+
unless header.has_key?(:empty)
|
25
|
+
header[:empty] = true
|
26
|
+
end
|
27
|
+
header[:max_length] ||= 0
|
28
|
+
header[:padding_left] ||= ''
|
29
|
+
header[:padding_right] ||= ''
|
30
|
+
else
|
31
|
+
header = {
|
32
|
+
:format => '%s',
|
33
|
+
:label => '',
|
34
|
+
:empty => true,
|
35
|
+
:max_length => 0,
|
36
|
+
:padding_left => '',
|
37
|
+
:padding_right => '',
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
unless col.nil?
|
42
|
+
if header[:empty]
|
43
|
+
header[:empty] = false
|
44
|
+
end
|
45
|
+
col_s = col.to_s
|
46
|
+
if col_s.length > header[:max_length]
|
47
|
+
header[:max_length] = (header[:format] % [col_s]).length + header[:padding_left].length + header[:padding_right].length
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
col_n += 1
|
52
|
+
end
|
53
|
+
@rows << row
|
54
|
+
end
|
55
|
+
|
56
|
+
# Render Table to String.
|
57
|
+
def to_s
|
58
|
+
s = ''
|
59
|
+
|
60
|
+
s << @headings.map{ |header|
|
61
|
+
unless header[:empty]
|
62
|
+
"%s#{header[:format]}%s" % [header[:padding_left], header[:label], header[:padding_right]]
|
63
|
+
end
|
64
|
+
}.select{ |ts| !ts.nil? }.join(' ')
|
65
|
+
s << "\n"
|
66
|
+
|
67
|
+
@rows.each do |row|
|
68
|
+
col_n = 0
|
69
|
+
columns = []
|
70
|
+
row.each do |col|
|
71
|
+
header = @headings[col_n]
|
72
|
+
unless header[:empty]
|
73
|
+
col_s = "%s#{header[:format]}%s" % [header[:padding_left], col, header[:padding_right]]
|
74
|
+
|
75
|
+
columns << col_s
|
76
|
+
end
|
77
|
+
col_n += 1
|
78
|
+
end
|
79
|
+
s << columns.join(' ') << "\n"
|
80
|
+
end
|
81
|
+
|
82
|
+
s
|
83
|
+
end
|
84
|
+
|
85
|
+
end # class Task
|
86
|
+
|
87
|
+
end # module Timr
|
88
|
+
end #module TheFox
|
data/lib/timr/timr.rb
CHANGED
@@ -1,659 +1,601 @@
|
|
1
1
|
|
2
|
-
require '
|
3
|
-
require 'time'
|
4
|
-
require 'fileutils'
|
5
|
-
require 'yaml/store'
|
6
|
-
require 'thefox-ext'
|
2
|
+
require 'pathname'
|
7
3
|
|
8
4
|
module TheFox
|
9
5
|
module Timr
|
10
6
|
|
7
|
+
# Core Class
|
8
|
+
#
|
9
|
+
# Loads and saves Models to files. Tasks are loaded to `@tasks`.
|
10
|
+
#
|
11
|
+
# Holds the [Stack](rdoc-ref:TheFox::Timr::Model::Stack) instance. Responsible to call Stack methods.
|
11
12
|
class Timr
|
12
13
|
|
13
|
-
|
14
|
+
include Model
|
15
|
+
include Error
|
14
16
|
|
15
|
-
|
16
|
-
|
17
|
-
@base_dir_name = File.basename(@base_dir_path)
|
18
|
-
@data_dir_path = "#{@base_dir_path}/data"
|
19
|
-
@config_path = config_path
|
20
|
-
|
21
|
-
puts "base: #{@base_dir_path}"
|
22
|
-
puts "name: #{@base_dir_name}"
|
23
|
-
puts "data: #{@data_dir_path}"
|
24
|
-
puts "config: #{@config_path}"
|
25
|
-
|
26
|
-
@stack = Stack.new
|
27
|
-
@tasks = {}
|
28
|
-
@last_write = nil
|
29
|
-
@config = {
|
30
|
-
'clock' => {
|
31
|
-
'default' => '%F %R',
|
32
|
-
'large' => '%F %T',
|
33
|
-
'short' => '%R',
|
34
|
-
},
|
35
|
-
}
|
36
|
-
#@ui_window_refresh_last = nil
|
37
|
-
#@ui_status_line_last = nil
|
38
|
-
|
39
|
-
config_read
|
40
|
-
init_dirs
|
41
|
-
tasks_load
|
42
|
-
|
43
|
-
@window = nil
|
44
|
-
@window_help = HelpWindow.new
|
45
|
-
@window_test = TestWindow.new
|
46
|
-
|
47
|
-
@window_tasks = TasksWindow.new(@tasks)
|
48
|
-
@window_timeline = TimelineWindow.new(@tasks)
|
49
|
-
end
|
17
|
+
# ForeignIdDb instance.
|
18
|
+
attr_reader :foreign_id_db
|
50
19
|
|
51
|
-
|
52
|
-
|
53
|
-
content = YAML::load_file(path)
|
54
|
-
if content
|
55
|
-
@config.merge_recursive!(content)
|
56
|
-
end
|
57
|
-
end
|
58
|
-
end
|
20
|
+
# Stack instance.
|
21
|
+
attr_reader :stack
|
59
22
|
|
60
|
-
def
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
23
|
+
def initialize(cwd)
|
24
|
+
# Current Working Directory
|
25
|
+
case cwd
|
26
|
+
when String
|
27
|
+
@cwd = Pathname.new(cwd)
|
28
|
+
else
|
29
|
+
@cwd = cwd
|
66
30
|
end
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
Dir['task_*.yml'].each do |file_name|
|
72
|
-
#puts "file: '#{file_name}'"
|
73
|
-
task = Task.new(file_name)
|
74
|
-
@tasks[task.id] = task
|
75
|
-
end
|
31
|
+
|
32
|
+
if @cwd && !@cwd.exist?
|
33
|
+
puts "Initialize Timr in #{@cwd}"
|
34
|
+
@cwd.mkpath
|
76
35
|
end
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
@
|
81
|
-
|
36
|
+
|
37
|
+
@config = Config.new
|
38
|
+
@config.file_path = Pathname.new('config.yml').expand_path(@cwd)
|
39
|
+
if @config.file_path.exist?
|
40
|
+
@config.load_from_file
|
41
|
+
else
|
42
|
+
#@config.save_to_file
|
43
|
+
@config.save_to_file(nil, true)
|
82
44
|
end
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
@
|
87
|
-
|
88
|
-
ui_status_text("Store files ... #{Time.now.strftime('%T')}")
|
45
|
+
|
46
|
+
@foreign_id_db = ForeignIdDb.new
|
47
|
+
@foreign_id_db.file_path = Pathname.new('foreign_id_db.yml').expand_path(@cwd)
|
48
|
+
if @foreign_id_db.file_path.exist?
|
49
|
+
@foreign_id_db.load_from_file
|
89
50
|
end
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
51
|
+
|
52
|
+
@tasks_path = Pathname.new('tasks').expand_path(@cwd)
|
53
|
+
unless @tasks_path.exist?
|
54
|
+
@tasks_path.mkpath
|
94
55
|
end
|
95
|
-
|
96
|
-
|
56
|
+
|
57
|
+
# Holds all loaded Tasks.
|
58
|
+
@tasks = Hash.new
|
59
|
+
|
60
|
+
# Stack Path
|
61
|
+
stack_path = Pathname.new('stack.yml').expand_path(@cwd)
|
62
|
+
|
63
|
+
# Stack
|
64
|
+
@stack = Stack.new
|
65
|
+
@stack.timr = self
|
66
|
+
@stack.file_path = stack_path
|
67
|
+
if stack_path.exist?
|
68
|
+
@stack.load_from_file
|
97
69
|
end
|
98
70
|
end
|
99
71
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
72
|
+
# Removes all previous [Tracks](rdoc-ref:TheFox::Timr::Model::Track) and starts a new one.
|
73
|
+
#
|
74
|
+
# Options:
|
75
|
+
#
|
76
|
+
# - `:foreign_id` (String)
|
77
|
+
# - `:task_id` (String)
|
78
|
+
# - `:track_id` (String)
|
79
|
+
def start(options = Hash.new)
|
80
|
+
foreign_id_opt = options.fetch(:foreign_id, nil)
|
81
|
+
task_id_opt = options.fetch(:task_id, nil)
|
82
|
+
track_id_opt = options.fetch(:track_id, nil)
|
83
|
+
|
84
|
+
# Get current Track from Stack.
|
85
|
+
old_track = @stack.current_track
|
86
|
+
|
87
|
+
# Stop current running Track.
|
88
|
+
if old_track
|
89
|
+
# Get Task from Track.
|
90
|
+
old_task = old_track.task
|
91
|
+
unless old_task
|
92
|
+
raise TrackError, "Track #{old_track.short_id} has no Task."
|
121
93
|
end
|
94
|
+
|
95
|
+
# Stop Task
|
96
|
+
old_task.stop
|
97
|
+
|
98
|
+
# Save Task
|
99
|
+
old_task.save_to_file
|
100
|
+
|
101
|
+
old_task = nil
|
122
102
|
end
|
123
|
-
rest = Curses.cols - title.length - COL
|
124
103
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
line_nr = Curses.lines - 1
|
134
|
-
|
135
|
-
Curses.setpos(line_nr, 0)
|
136
|
-
Curses.clrtoeol
|
137
|
-
if !text.nil?
|
138
|
-
Curses.setpos(line_nr, COL)
|
139
|
-
Curses.attron(attrn) do
|
140
|
-
Curses.addstr(text)
|
104
|
+
if task_id_opt
|
105
|
+
task = get_task_by_id(task_id_opt)
|
106
|
+
|
107
|
+
if foreign_id_opt
|
108
|
+
# Throws exception when Foreign ID already exists in DB.
|
109
|
+
# Break before new Track creation.
|
110
|
+
@foreign_id_db.add_task(task, foreign_id_opt)
|
111
|
+
@foreign_id_db.save_to_file
|
141
112
|
end
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
113
|
+
|
114
|
+
track = task.start(options)
|
115
|
+
|
116
|
+
task.save_to_file
|
117
|
+
|
118
|
+
@stack.start(track)
|
119
|
+
@stack.save_to_file
|
120
|
+
else
|
121
|
+
if track_id_opt
|
122
|
+
# Seach Track ID the long way. Should be avoided.
|
123
|
+
# Searches all files.
|
124
|
+
|
125
|
+
track = Track.find_track_by_id(@tasks_path, track_id_opt)
|
126
|
+
if track
|
127
|
+
options[:track_id] = track.id
|
128
|
+
|
129
|
+
# Get Task from Track.
|
130
|
+
task = track.task
|
131
|
+
unless task
|
132
|
+
raise TrackError, "Track #{track.short_id} has no Task."
|
133
|
+
end
|
134
|
+
|
135
|
+
# Start Task
|
136
|
+
track = task.start(options)
|
137
|
+
|
138
|
+
# Save Task
|
139
|
+
task.save_to_file
|
140
|
+
|
141
|
+
@stack.start(track)
|
142
|
+
@stack.save_to_file
|
170
143
|
end
|
171
|
-
when 10
|
172
|
-
break
|
173
|
-
when nil
|
174
|
-
# Do nothing.
|
175
|
-
#sleep TIMEOUT.to_f / 1000
|
176
144
|
else
|
177
|
-
|
145
|
+
# Create completely new Task.
|
146
|
+
task = Task.create_task_from_hash(options)
|
147
|
+
|
148
|
+
if foreign_id_opt
|
149
|
+
# Throws exception when Foreign ID already exists in DB.
|
150
|
+
# Break before new Track creation.
|
151
|
+
@foreign_id_db.add_task(task, foreign_id_opt)
|
152
|
+
@foreign_id_db.save_to_file
|
153
|
+
end
|
154
|
+
|
155
|
+
# Start Task
|
156
|
+
track = task.start(options)
|
157
|
+
|
158
|
+
# Task Path
|
159
|
+
task_file_path = BasicModel.create_path_by_id(@tasks_path, task.id)
|
160
|
+
|
161
|
+
# Save Track to file.
|
162
|
+
task.save_to_file(task_file_path)
|
163
|
+
|
164
|
+
@stack.start(track)
|
165
|
+
@stack.save_to_file
|
178
166
|
end
|
179
167
|
end
|
180
|
-
if abort
|
181
|
-
input = nil
|
182
|
-
end
|
183
168
|
|
184
|
-
|
185
|
-
Curses.timeout = TIMEOUT
|
186
|
-
Curses.curs_set(0)
|
187
|
-
|
188
|
-
input
|
169
|
+
track
|
189
170
|
end
|
190
171
|
|
191
|
-
#
|
192
|
-
|
193
|
-
|
194
|
-
|
172
|
+
# Stops the current running [Track](rdoc-ref:TheFox::Timr::Model::Track) and removes it from the Stack.
|
173
|
+
def stop(options = Hash.new)
|
174
|
+
# Get current Track from Stack.
|
175
|
+
track = @stack.current_track
|
176
|
+
unless track
|
177
|
+
return
|
178
|
+
end
|
195
179
|
|
196
|
-
|
197
|
-
|
180
|
+
# Get Task from Track.
|
181
|
+
task = track.task
|
198
182
|
|
199
|
-
|
200
|
-
|
201
|
-
run_time_track_m = 0
|
202
|
-
run_time_track_s = 0
|
183
|
+
# Stop Task
|
184
|
+
task.stop(options)
|
203
185
|
|
204
|
-
|
205
|
-
|
206
|
-
run_time_total_m = 0
|
207
|
-
run_time_total_s = 0
|
186
|
+
# Save Task
|
187
|
+
task.save_to_file
|
208
188
|
|
209
|
-
|
189
|
+
@stack.stop
|
190
|
+
@stack.save_to_file
|
210
191
|
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
run_time_track_h, run_time_track_m, run_time_track_s = @stack.task.run_time_track
|
221
|
-
run_time_total_h, run_time_total_m, run_time_total_s = @stack.task.run_time_total
|
222
|
-
else
|
223
|
-
status_s = TASK_NO_TASK_LOADED_CHAR
|
192
|
+
track
|
193
|
+
end
|
194
|
+
|
195
|
+
# Stops the current running [Track](rdoc-ref:TheFox::Timr::Model::Track) but does not remove it from the Stack.
|
196
|
+
def pause(options = Hash.new)
|
197
|
+
# Get current Track from Stack.
|
198
|
+
track = @stack.current_track
|
199
|
+
unless track
|
200
|
+
return
|
224
201
|
end
|
225
202
|
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
run_time_total_str = '%2d:%02d:%02d' % [run_time_total_h, run_time_total_m, run_time_total_s]
|
230
|
-
end
|
231
|
-
|
232
|
-
time_format = @config['clock']['default']
|
233
|
-
if Curses.cols <= 50
|
234
|
-
if stack_has_task
|
235
|
-
run_time_track_str = '%2d:%02d' % [run_time_track_h, run_time_track_m]
|
236
|
-
end
|
237
|
-
run_time_total_str = ''
|
238
|
-
|
239
|
-
time_format = nil
|
240
|
-
elsif Curses.cols <= 60
|
241
|
-
if stack_has_task
|
242
|
-
run_time_track_str = '%2d:%02d' % [run_time_track_h, run_time_track_m]
|
243
|
-
run_time_total_str = '%2d:%02d' % [run_time_total_h, run_time_total_m]
|
244
|
-
end
|
245
|
-
|
246
|
-
time_format = @config['clock']['short']
|
247
|
-
elsif Curses.cols > 80
|
248
|
-
time_format = @config['clock']['large']
|
249
|
-
end
|
250
|
-
if !time_format.nil?
|
251
|
-
time_s = Time.now.strftime(time_format)
|
252
|
-
end
|
203
|
+
# Track Status
|
204
|
+
if track.stopped?
|
205
|
+
raise TrackError, "Cannot pause current Track #{track.short_id}, is not running."
|
253
206
|
end
|
254
207
|
|
255
|
-
|
208
|
+
# Get Task from Track.
|
209
|
+
task = track.task
|
256
210
|
|
257
|
-
|
258
|
-
|
211
|
+
# Pause Task
|
212
|
+
track = task.pause(options)
|
259
213
|
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
Curses.addstr(' ' * Curses.cols)
|
265
|
-
end
|
266
|
-
|
267
|
-
Curses.setpos(line_nr, COL)
|
268
|
-
Curses.addstr(line)
|
269
|
-
end
|
214
|
+
# Save Task
|
215
|
+
task.save_to_file
|
216
|
+
|
217
|
+
# Do nothing on the Stack.
|
270
218
|
|
271
|
-
|
219
|
+
track
|
272
220
|
end
|
273
221
|
|
274
|
-
|
275
|
-
|
276
|
-
|
222
|
+
# Continues the Top [Track](rdoc-ref:TheFox::Timr::Model::Track).
|
223
|
+
#
|
224
|
+
# Options:
|
225
|
+
#
|
226
|
+
# - `:track` (Track)
|
227
|
+
def continue(options = Hash.new)
|
228
|
+
# Get current Track from Stack.
|
229
|
+
track = @stack.current_track
|
230
|
+
unless track
|
231
|
+
return
|
232
|
+
end
|
233
|
+
options[:track] = track
|
234
|
+
|
235
|
+
# Get Task from Track.
|
236
|
+
task = track.task
|
237
|
+
|
238
|
+
# Continue Task
|
239
|
+
track = task.continue(options)
|
240
|
+
|
241
|
+
# Save Task
|
242
|
+
task.save_to_file
|
243
|
+
|
244
|
+
@stack.stop
|
245
|
+
@stack.push(track)
|
246
|
+
@stack.save_to_file
|
247
|
+
|
248
|
+
track
|
277
249
|
end
|
278
250
|
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
251
|
+
# Starts a new [Track](rdoc-ref:TheFox::Timr::Model::Track) and pauses the underlying one.
|
252
|
+
#
|
253
|
+
# Options:
|
254
|
+
#
|
255
|
+
# - `:foreign_id` (String)
|
256
|
+
# - `:task_id` (String)
|
257
|
+
# - `:track_id` (String)
|
258
|
+
def push(options = Hash.new)
|
259
|
+
foreign_id_opt = options.fetch(:foreign_id, nil)
|
260
|
+
task_id_opt = options.fetch(:task_id, nil)
|
261
|
+
track_id_opt = options.fetch(:track_id, nil)
|
262
|
+
|
263
|
+
# Get current Track from Stack.
|
264
|
+
old_track = @stack.current_track
|
265
|
+
|
266
|
+
# Stop current running Track.
|
267
|
+
if old_track
|
268
|
+
# Get Task from Track.
|
269
|
+
old_task = old_track.task
|
270
|
+
unless old_task
|
271
|
+
raise TrackError, "Track #{old_track.short_id} has no Task."
|
272
|
+
end
|
273
|
+
|
274
|
+
# Stop Task here because on pop we need to take the
|
275
|
+
# current Track from Stack instead from Task.
|
276
|
+
# You can push another Track from an already existing
|
277
|
+
# Task on the Stack. A Task can hold only one current Track.
|
278
|
+
old_task.stop
|
279
|
+
|
280
|
+
# Save Task
|
281
|
+
old_task.save_to_file
|
282
|
+
|
283
|
+
old_task = nil
|
286
284
|
end
|
287
|
-
out = ' ' * COL + text + ' ' * rest
|
288
285
|
|
289
|
-
if
|
290
|
-
|
291
|
-
|
286
|
+
if task_id_opt
|
287
|
+
task = get_task_by_id(task_id_opt)
|
288
|
+
|
289
|
+
if foreign_id_opt
|
290
|
+
# Throws exception when Foreign ID already exists in DB.
|
291
|
+
# Break before new Track creation.
|
292
|
+
@foreign_id_db.add_task(task, foreign_id_opt)
|
293
|
+
@foreign_id_db.save_to_file
|
292
294
|
end
|
295
|
+
|
296
|
+
# Start Task
|
297
|
+
track = task.start(options)
|
298
|
+
|
299
|
+
# Save Task
|
300
|
+
task.save_to_file
|
301
|
+
|
302
|
+
@stack.push(track)
|
303
|
+
@stack.save_to_file
|
293
304
|
else
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
+
if track_id_opt
|
306
|
+
# The long way. Should be avoided.
|
307
|
+
# Search all files.
|
308
|
+
|
309
|
+
track = Track.find_track_by_id(@tasks_path, track_id_opt)
|
310
|
+
if track
|
311
|
+
options[:track_id] = track.id
|
312
|
+
|
313
|
+
# Get Task from Track.
|
314
|
+
task = track.task
|
315
|
+
unless task
|
316
|
+
raise TrackError, "Track #{track.short_id} has no Task."
|
317
|
+
end
|
318
|
+
|
319
|
+
# Start Task
|
320
|
+
track = task.start(options)
|
321
|
+
|
322
|
+
# Save Task
|
323
|
+
task.save_to_file
|
324
|
+
|
325
|
+
@stack.push(track)
|
326
|
+
@stack.save_to_file
|
327
|
+
end
|
328
|
+
else
|
329
|
+
# Create completely new Task.
|
330
|
+
task = Task.create_task_from_hash(options)
|
305
331
|
|
306
|
-
if
|
307
|
-
|
308
|
-
|
332
|
+
if foreign_id_opt
|
333
|
+
# Throws exception when Foreign ID already exists in DB.
|
334
|
+
# Break before new Track creation.
|
335
|
+
@foreign_id_db.add_task(task, foreign_id_opt)
|
336
|
+
@foreign_id_db.save_to_file
|
309
337
|
end
|
310
338
|
|
311
|
-
|
339
|
+
# Start Task
|
340
|
+
track = task.start(options)
|
312
341
|
|
313
|
-
|
342
|
+
# Task Path
|
343
|
+
task_file_path = BasicModel.create_path_by_id(@tasks_path, task.id)
|
314
344
|
|
315
|
-
#
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
window_page_length = @window.page_length
|
322
|
-
content_length = ui_content_length
|
323
|
-
if window_page_length < content_length
|
324
|
-
((window_page_length + 1)..content_length).to_a.each do |rest_line_nr|
|
325
|
-
Curses.setpos(rest_line_nr, 0)
|
326
|
-
Curses.clrtoeol
|
327
|
-
|
328
|
-
# if $DEBUG
|
329
|
-
# Curses.addstr("-- CLEAR -- #{rest_line_nr} #{Time.now.strftime('%T')}")
|
330
|
-
# Curses.refresh
|
331
|
-
# sleep 0.01
|
332
|
-
# end
|
333
|
-
end
|
345
|
+
# Save Track to file.
|
346
|
+
task.save_to_file(task_file_path)
|
347
|
+
|
348
|
+
@stack.push(track)
|
349
|
+
@stack.save_to_file
|
334
350
|
end
|
335
351
|
end
|
336
352
|
|
337
|
-
|
353
|
+
track
|
338
354
|
end
|
339
355
|
|
340
|
-
|
341
|
-
|
356
|
+
# Stops the Top [Track](rdoc-ref:TheFox::Timr::Model::Track), removes it from the [Stack](rdoc-ref:TheFox::Timr::Model::Stack) and
|
357
|
+
# continues the next underlying (new Top) Track.
|
358
|
+
def pop(options = Hash.new)
|
359
|
+
stop(options)
|
360
|
+
continue(options)
|
342
361
|
end
|
343
362
|
|
344
|
-
|
345
|
-
|
363
|
+
# Create a new [Task](rdoc-ref:TheFox::Timr::Model::Task) based on the `options` Hash. Will not be started or something else.
|
364
|
+
#
|
365
|
+
# Uses [Task#create_task_from_hash](rdoc-ref:TheFox::Timr::Model::Task::create_task_from_hash) to create a new Task instance and [BasicModel#create_path_by_id](rdoc-ref:TheFox::Timr::Model::BasicModel.create_path_by_id) to create a new file path.
|
366
|
+
#
|
367
|
+
# Returns the new created Task instance.
|
368
|
+
#
|
369
|
+
# Options:
|
370
|
+
#
|
371
|
+
# - `:foreign_id` (String)
|
372
|
+
def add_task(options = Hash.new)
|
373
|
+
foreign_id_opt = options.fetch(:foreign_id, nil)
|
374
|
+
|
375
|
+
task = Task.create_task_from_hash(options)
|
376
|
+
|
377
|
+
if foreign_id_opt
|
378
|
+
# Throws exception when Foreign ID already exists in DB.
|
379
|
+
# Break before Task save.
|
380
|
+
@foreign_id_db.add_task(task, foreign_id_opt)
|
381
|
+
@foreign_id_db.save_to_file
|
382
|
+
end
|
346
383
|
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
384
|
+
# Task Path
|
385
|
+
task_file_path = BasicModel.create_path_by_id(@tasks_path, task.id)
|
386
|
+
|
387
|
+
# Save Track to file.
|
388
|
+
task.save_to_file(task_file_path)
|
389
|
+
|
390
|
+
# Leave Stack untouched.
|
391
|
+
|
392
|
+
task
|
356
393
|
end
|
357
394
|
|
358
|
-
|
359
|
-
|
395
|
+
# Remove a [Task](rdoc-ref:TheFox::Timr::Model::Task).
|
396
|
+
#
|
397
|
+
# Options:
|
398
|
+
#
|
399
|
+
# - `:task_id` (String) Can be either a internal ID (hex) or Foreign ID, because `get_task_by_id` searches also the Foreign ID DB.
|
400
|
+
def remove_task(options = Hash.new)
|
401
|
+
task_id_opt = options.fetch(:task_id, nil)
|
402
|
+
|
403
|
+
unless task_id_opt
|
404
|
+
raise TaskError, 'No Task ID given.'
|
405
|
+
end
|
360
406
|
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
407
|
+
task = get_task_by_id(task_id_opt)
|
408
|
+
|
409
|
+
@tasks.delete(task.id)
|
410
|
+
|
411
|
+
# Get running Tracks and remove these from Stack.
|
412
|
+
task.tracks({:status => ?R}).each do |track_id, track|
|
413
|
+
@stack.remove(track)
|
414
|
+
end
|
415
|
+
@stack.save_to_file
|
416
|
+
|
417
|
+
# Remove Task from Foreign ID DB.
|
418
|
+
@foreign_id_db.remove_task(task)
|
419
|
+
@foreign_id_db.save_to_file
|
420
|
+
|
421
|
+
task.delete_file
|
422
|
+
|
423
|
+
task
|
365
424
|
end
|
366
425
|
|
367
|
-
|
368
|
-
|
426
|
+
# Remove a Track.
|
427
|
+
#
|
428
|
+
# Options:
|
429
|
+
#
|
430
|
+
# - `:track_id` (String)
|
431
|
+
def remove_track(options = Hash.new)
|
432
|
+
track_id_opt = options.fetch(:track_id, nil)
|
369
433
|
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
line_nr += 1
|
379
|
-
end
|
434
|
+
unless track_id_opt
|
435
|
+
raise TrackError, 'No Track ID given.'
|
436
|
+
end
|
437
|
+
|
438
|
+
track = get_track_by_id(track_id_opt)
|
439
|
+
unless track
|
440
|
+
raise TrackError, "Track for ID '#{track_id_opt}' not found."
|
380
441
|
end
|
381
442
|
|
382
|
-
|
443
|
+
task = track.task
|
444
|
+
|
445
|
+
track.remove
|
446
|
+
|
447
|
+
task.save_to_file
|
448
|
+
|
449
|
+
# Remove Track from Stack.
|
450
|
+
@stack.remove(track)
|
451
|
+
@stack.save_to_file
|
452
|
+
|
453
|
+
{
|
454
|
+
:task => task,
|
455
|
+
:track => track,
|
456
|
+
}
|
383
457
|
end
|
384
458
|
|
385
|
-
|
386
|
-
|
387
|
-
|
459
|
+
# Find a [Task](rdoc-ref:TheFox::Timr::Model::Task) by ID (internal or foreign).
|
460
|
+
#
|
461
|
+
# Tasks always should be loaded with this methods to check if a Task instance already exist at `@tasks`. This is like a cache.
|
462
|
+
#
|
463
|
+
# `task_id` can be a short ID or a Foreign ID. If a Task is already loaded with full ID another search by short ID would lead to generate a new object_id. Then there would be two Tasks instances loaded for the same Task ID. The Check Cache if condition prohibits this.
|
464
|
+
def get_task_by_id(task_id)
|
465
|
+
# First search in Foreign ID DB.
|
466
|
+
tmp_task_id = @foreign_id_db.get_task_id(task_id)
|
467
|
+
if tmp_task_id
|
468
|
+
task_id = tmp_task_id
|
469
|
+
end
|
470
|
+
|
471
|
+
task = @tasks[task_id]
|
472
|
+
|
473
|
+
if task
|
474
|
+
# Take Task from cache.
|
388
475
|
else
|
389
|
-
|
390
|
-
|
391
|
-
|
476
|
+
task = Task.load_task_from_file_with_id(@tasks_path, task_id)
|
477
|
+
|
478
|
+
# Check cache.
|
479
|
+
if @tasks[task.id]
|
480
|
+
# Task already loaded.
|
481
|
+
task = @tasks[task.id]
|
392
482
|
else
|
393
|
-
|
483
|
+
# Set new loaded Task.
|
484
|
+
@tasks[task.id] = task
|
394
485
|
end
|
395
486
|
end
|
396
487
|
|
397
|
-
|
398
|
-
window_content_changed
|
399
|
-
ui_stack_lines_refresh
|
400
|
-
ui_window_refresh_all
|
488
|
+
task
|
401
489
|
end
|
402
490
|
|
403
|
-
|
404
|
-
|
405
|
-
|
491
|
+
# Find a [Track](rdoc-ref:TheFox::Timr::Model::Track) by ID.
|
492
|
+
def get_track_by_id(track_id)
|
493
|
+
@tasks.each do |task_id, task|
|
494
|
+
track = task.find_track_by_id(track_id)
|
495
|
+
if track
|
496
|
+
return track
|
497
|
+
end
|
406
498
|
end
|
499
|
+
|
500
|
+
nil
|
407
501
|
end
|
408
502
|
|
409
|
-
|
410
|
-
|
503
|
+
# Get a Track by a specific Task ID and Track ID.
|
504
|
+
def get_track_by_task_id(task_id, track_id)
|
505
|
+
if task_id && track_id
|
506
|
+
task = get_task_by_id(task_id)
|
507
|
+
if task
|
508
|
+
return task.find_track_by_id(track_id)
|
509
|
+
end
|
510
|
+
end
|
511
|
+
|
512
|
+
nil
|
411
513
|
end
|
412
514
|
|
413
|
-
|
414
|
-
|
515
|
+
# Get all [Tasks](rdoc-ref:TheFox::Timr::Model::Task).
|
516
|
+
def tasks
|
517
|
+
load_all_tracks
|
518
|
+
|
519
|
+
@tasks
|
415
520
|
end
|
416
521
|
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
522
|
+
# Get all [Tracks](rdoc-ref:TheFox::Timr::Model::Track).
|
523
|
+
#
|
524
|
+
# Options:
|
525
|
+
#
|
526
|
+
# - `:sort` (Boolean)
|
527
|
+
def tracks(options = Hash.new)
|
528
|
+
sort_opt = options.fetch(:sort, true)
|
529
|
+
|
530
|
+
load_all_tracks
|
531
|
+
|
532
|
+
filtered_tracks = Hash.new
|
533
|
+
@tasks.each do |task_id, task|
|
534
|
+
tracks = task.tracks(options)
|
535
|
+
filtered_tracks.merge!(tracks)
|
536
|
+
end
|
537
|
+
|
538
|
+
if sort_opt
|
539
|
+
# Sort ASC by Begin DateTime, End DateTime.
|
540
|
+
filtered_tracks.sort{ |t1, t2|
|
541
|
+
t1 = t1.last
|
542
|
+
t2 = t2.last
|
543
|
+
|
544
|
+
cmp1 = t1.begin_datetime <=> t2.begin_datetime
|
545
|
+
|
546
|
+
if cmp1 == 0
|
547
|
+
t1.end_datetime <=> t2.end_datetime
|
548
|
+
else
|
549
|
+
cmp1
|
550
|
+
end
|
551
|
+
}.to_h
|
552
|
+
else
|
553
|
+
filtered_tracks
|
421
554
|
end
|
422
555
|
end
|
423
556
|
|
424
|
-
#
|
425
|
-
#
|
426
|
-
#
|
427
|
-
# Not only the length of the rows can change, but also
|
428
|
-
# the actual an change.
|
429
|
-
def window_content_changed
|
430
|
-
@window_tasks.content_changed
|
431
|
-
@window_timeline.content_changed
|
432
|
-
end
|
557
|
+
# Alias for `ForeignIdDb#match_task_with_foreign_id`.
|
558
|
+
# def match_task_with_foreign_id(task, foreign_id)
|
559
|
+
# end
|
433
560
|
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
elsif object.is_a?(Track)
|
442
|
-
task = object.task
|
443
|
-
track = object
|
444
|
-
end
|
561
|
+
# Save [Stack](rdoc-ref:TheFox::Timr::Model::Stack) and [Config](rdoc-ref:TheFox::Timr::Model::Config).
|
562
|
+
def shutdown
|
563
|
+
# Save Stack
|
564
|
+
@stack.save_to_file
|
565
|
+
|
566
|
+
# Save config
|
567
|
+
@config.save_to_file
|
445
568
|
|
446
|
-
|
569
|
+
# Save Foreign ID DB
|
570
|
+
@foreign_id_db.save_to_file
|
447
571
|
end
|
448
572
|
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
573
|
+
# Load all [Tracks](rdoc-ref:TheFox::Timr::Model::Track) using `get_task_by_id`.
|
574
|
+
def load_all_tracks
|
575
|
+
# Iterate all files.
|
576
|
+
@tasks_path.find.each do |file|
|
577
|
+
# Filter all directories.
|
578
|
+
unless file.file?
|
579
|
+
next
|
455
580
|
end
|
456
|
-
end
|
457
|
-
end
|
458
|
-
|
459
|
-
def run
|
460
|
-
ui_init_curses
|
461
|
-
update_content_length
|
462
|
-
ui_title_line
|
463
|
-
ui_status_line(true)
|
464
|
-
ui_window_show(@window_timeline)
|
465
|
-
|
466
|
-
loop do
|
467
|
-
key_pressed = Curses.getch
|
468
581
|
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
# if $DEBUG
|
474
|
-
# ui_status_text('DEBUG: %03d C=%03d L=%03d pr=%03d cr=%03d' % [@window.cursor, ui_content_length, @window.current_line, @window.page_refreshes, @window.content_refreshes])
|
475
|
-
# end
|
476
|
-
|
477
|
-
ui_window_refresh_all
|
478
|
-
when Curses::Key::PPAGE
|
479
|
-
@window.previous_page if !@window.nil?
|
480
|
-
|
481
|
-
# if $DEBUG
|
482
|
-
# ui_status_text('DEBUG: %03d C=%03d L=%03d pr=%03d cr=%03d' % [@window.cursor, ui_content_length, @window.current_line, @window.page_refreshes, @window.content_refreshes])
|
483
|
-
# end
|
484
|
-
|
485
|
-
ui_window_refresh_all
|
486
|
-
when Curses::Key::DOWN
|
487
|
-
if !@window.nil? && @window.has_cursor?
|
488
|
-
simple_refresh_b = @window.cursor_on_inner_range?
|
489
|
-
|
490
|
-
@window.cursor_next_line
|
491
|
-
|
492
|
-
simple_refresh_a = @window.cursor_on_inner_range?
|
493
|
-
|
494
|
-
# if $DEBUG
|
495
|
-
# ui_status_text('DEBUG: %03d C=%03d L=%03d pr=%03d cr=%03d %02d %02d %s %s' % [@window.cursor, ui_content_length, @window.current_line, @window.page_refreshes, @window.content_refreshes, @window.cursor_border_top, @window.cursor_border_bottom, simple_refresh_b ? 'SIMPLE' : 'FULL', simple_refresh_a ? 'SIMPLE' : 'FULL'])
|
496
|
-
#end
|
497
|
-
|
498
|
-
ui_window_refresh_all
|
499
|
-
end
|
500
|
-
when Curses::Key::UP
|
501
|
-
if !@window.nil? && @window.has_cursor?
|
502
|
-
simple_refresh_b = @window.cursor_on_inner_range?
|
503
|
-
|
504
|
-
@window.cursor_previous_line
|
505
|
-
|
506
|
-
simple_refresh_a = @window.cursor_on_inner_range?
|
507
|
-
|
508
|
-
# if $DEBUG
|
509
|
-
# ui_status_text('DEBUG: %03d C=%03d L=%03d pr=%03d cr=%03d %02d %02d %s %s' % [@window.cursor, ui_content_length, @window.current_line, @window.page_refreshes, @window.content_refreshes, @window.cursor_border_top, @window.cursor_border_bottom, simple_refresh_b ? 'SIMPLE' : 'FULL', simple_refresh_a ? 'SIMPLE' : 'FULL'])
|
510
|
-
# end
|
511
|
-
|
512
|
-
ui_window_refresh_all
|
513
|
-
end
|
514
|
-
when Curses::Key::HOME
|
515
|
-
@window.first_page if !@window.nil?
|
516
|
-
|
517
|
-
if $DEBUG
|
518
|
-
ui_status_text('DEBUG: %03d C=%03d L=%03d pr=%03d cr=%03d' % [@window.cursor, ui_content_length, @window.current_line, @window.page_refreshes, @window.content_refreshes])
|
519
|
-
end
|
520
|
-
|
521
|
-
ui_window_refresh_all
|
522
|
-
when Curses::Key::END
|
523
|
-
@window.last_page if !@window.nil?
|
524
|
-
|
525
|
-
# if $DEBUG
|
526
|
-
# ui_status_text('DEBUG: %03d C=%03d L=%03d pr=%03d cr=%03d' % [@window.cursor, ui_content_length, @window.current_line, @window.page_refreshes, @window.content_refreshes])
|
527
|
-
# end
|
528
|
-
|
529
|
-
ui_window_refresh_all
|
530
|
-
when Curses::Key::DC
|
531
|
-
task, track = window_page_object
|
532
|
-
|
533
|
-
if track.nil?
|
534
|
-
# Remove Task
|
535
|
-
else
|
536
|
-
# Remove Track
|
537
|
-
end
|
538
|
-
task.remove_track(track)
|
539
|
-
|
540
|
-
ui_status_text("Del '#{task.class}' '#{track.class}'")
|
541
|
-
when Curses::Key::RESIZE
|
542
|
-
update_content_length
|
543
|
-
ui_status_text("Window size: #{Curses.cols}x#{Curses.lines}")
|
544
|
-
|
545
|
-
# Refreshing the complete screen while resizing
|
546
|
-
# can make everything slower. So for fast resizing
|
547
|
-
# comment this line.
|
548
|
-
ui_refresh_all
|
549
|
-
when 10
|
550
|
-
task, track = window_page_object
|
551
|
-
|
552
|
-
# if track.nil?
|
553
|
-
# track = Track.new
|
554
|
-
# track.task = task
|
555
|
-
# end
|
556
|
-
|
557
|
-
task_apply_replace_stack(task, track)
|
558
|
-
when '#'
|
559
|
-
task, * = window_page_object
|
560
|
-
|
561
|
-
track_description = ui_status_input('Track Description: ')
|
562
|
-
|
563
|
-
track = Track.new
|
564
|
-
track.task = task
|
565
|
-
track.description = track_description
|
566
|
-
|
567
|
-
task_apply_replace_stack(task, track)
|
568
|
-
ui_status_text('OK')
|
569
|
-
when 'b', 'p'
|
570
|
-
task, track = window_page_object
|
571
|
-
|
572
|
-
if task.nil?
|
573
|
-
ui_status_text("Unrecognized object: #{object.class}")
|
574
|
-
else
|
575
|
-
task_apply_push(task, track)
|
576
|
-
end
|
577
|
-
when 'r'
|
578
|
-
ui_refresh_all
|
579
|
-
ui_status_text
|
580
|
-
when 'n'
|
581
|
-
task_name = ui_status_input('New task: ')
|
582
|
-
if task_name.nil?
|
583
|
-
ui_status_text('Aborted.')
|
584
|
-
else
|
585
|
-
task = Task.new
|
586
|
-
task.name = task_name
|
587
|
-
task.save_to_file(@data_dir_path)
|
588
|
-
|
589
|
-
task_apply_replace_stack(task)
|
590
|
-
|
591
|
-
ui_status_text("Task '#{task_name}' created.")
|
592
|
-
end
|
593
|
-
when 't'
|
594
|
-
task_name = ui_status_input('New task: ')
|
595
|
-
if task_name.nil?
|
596
|
-
ui_status_text('Aborted.')
|
597
|
-
else
|
598
|
-
task = Task.new
|
599
|
-
task.name = task_name
|
600
|
-
task.save_to_file(@data_dir_path)
|
601
|
-
|
602
|
-
@tasks[task.id] = task
|
603
|
-
|
604
|
-
update_content_length
|
605
|
-
window_content_changed
|
606
|
-
ui_stack_lines_refresh
|
607
|
-
ui_window_refresh_all
|
608
|
-
|
609
|
-
ui_status_text("Task '#{task_name}' created.")
|
610
|
-
end
|
611
|
-
when 'x'
|
612
|
-
@stack.task.pause if @stack.has_task?
|
613
|
-
window_content_changed
|
614
|
-
ui_refresh_simple
|
615
|
-
when 'c'
|
616
|
-
@stack.task.toggle if @stack.has_task?
|
617
|
-
window_content_changed
|
618
|
-
ui_refresh_simple
|
619
|
-
when 'v'
|
620
|
-
task_apply_pop
|
621
|
-
when 'f'
|
622
|
-
task_apply_stack_pop_all
|
623
|
-
when 'h', '?'
|
624
|
-
ui_window_show(@window_help)
|
625
|
-
when '1'
|
626
|
-
ui_window_show(@window_timeline)
|
627
|
-
when '2'
|
628
|
-
ui_window_show(@window_tasks)
|
629
|
-
when 'z' # Test Windows
|
630
|
-
ui_window_show(@window_test)
|
631
|
-
when 'w'
|
632
|
-
tasks_save(true)
|
633
|
-
when 'q'
|
634
|
-
break
|
635
|
-
when nil
|
636
|
-
# Do some work.
|
637
|
-
ui_status_line
|
638
|
-
write_all_data
|
639
|
-
else
|
640
|
-
ui_status_text_error("Invalid key '#{key_pressed}' (#{Curses.keyname(key_pressed)})")
|
582
|
+
# Filter all non-yaml files.
|
583
|
+
unless file.basename.fnmatch('*.yml')
|
584
|
+
next
|
641
585
|
end
|
586
|
+
|
587
|
+
track_id = BasicModel.get_id_from_path(@tasks_path, file)
|
588
|
+
|
589
|
+
# Loads the Task from file into @tasks.
|
590
|
+
get_task_by_id(track_id)
|
642
591
|
end
|
643
592
|
end
|
644
593
|
|
645
|
-
def
|
646
|
-
|
647
|
-
Curses.stdscr.refresh
|
648
|
-
Curses.stdscr.close
|
649
|
-
|
650
|
-
Curses.close_screen
|
651
|
-
|
652
|
-
tasks_stop
|
653
|
-
tasks_save
|
594
|
+
def to_s
|
595
|
+
'Timr'
|
654
596
|
end
|
655
597
|
|
656
|
-
end
|
657
|
-
|
658
|
-
end
|
659
|
-
end
|
598
|
+
end # class Timr
|
599
|
+
|
600
|
+
end # module Timr
|
601
|
+
end # module TheFox
|