timr 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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
|