timr 0.1.0.pre.dev.1
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 +7 -0
- data/.editorconfig +10 -0
- data/.gitignore +5 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +29 -0
- data/Makefile +12 -0
- data/Makefile.common +58 -0
- data/README.md +11 -0
- data/bin/dev +13 -0
- data/bin/timr +38 -0
- data/lib/timr.rb +10 -0
- data/lib/timr/stack.rb +76 -0
- data/lib/timr/task.rb +144 -0
- data/lib/timr/timr.rb +392 -0
- data/lib/timr/version.rb +23 -0
- data/lib/timr/window.rb +202 -0
- data/lib/timr/window_help.rb +36 -0
- data/lib/timr/window_tasks.rb +36 -0
- data/lib/timr/window_test.rb +18 -0
- data/lib/timr/window_timeline.rb +14 -0
- data/timr.gemspec +32 -0
- data/timr.sublime-project +10 -0
- metadata +122 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: b78ed2b17c8e1324c1e8ce4ea8d331955c861227
|
4
|
+
data.tar.gz: cc2b02d5c1d05dd690ad117e76b5b0cc3e17df1f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 984d1c45cf0e32ef972ff0f94c427fa792071c5f6520235b97acc35952b151ee2619ca00025ad5da692793e8d6a1eb890d3874e0f21941ac5d5b2f6cc9ae87c0
|
7
|
+
data.tar.gz: 5bcbacb70f1b9412213d3b7b7214e38c7b081ef2357de42be7594f70e25d7571a36b4e982c80df9d0b18cc7760d1c57a8a7b416eb77d5ad0cda249330ae53ddc
|
data/.editorconfig
ADDED
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
timr (0.1.0.pre.dev.1)
|
5
|
+
curses (~> 1.0)
|
6
|
+
thefox-ext (~> 1.4)
|
7
|
+
uuid (~> 2.3)
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: https://rubygems.org/
|
11
|
+
specs:
|
12
|
+
curses (1.0.2)
|
13
|
+
macaddr (1.7.1)
|
14
|
+
systemu (~> 2.6.2)
|
15
|
+
minitest (5.8.4)
|
16
|
+
systemu (2.6.5)
|
17
|
+
thefox-ext (1.4.1)
|
18
|
+
uuid (2.3.8)
|
19
|
+
macaddr (~> 1.0)
|
20
|
+
|
21
|
+
PLATFORMS
|
22
|
+
ruby
|
23
|
+
|
24
|
+
DEPENDENCIES
|
25
|
+
minitest (~> 5.8)
|
26
|
+
timr!
|
27
|
+
|
28
|
+
BUNDLED WITH
|
29
|
+
1.11.2
|
data/Makefile
ADDED
data/Makefile.common
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
|
2
|
+
# Ruby Common Big
|
3
|
+
# 2016-04-09
|
4
|
+
|
5
|
+
MV = mv -nv
|
6
|
+
RM = rm -rf
|
7
|
+
MKDIR = mkdir -p
|
8
|
+
CHMOD = chmod
|
9
|
+
BUNDLER = bundle
|
10
|
+
BUNDLER_OPTIONS = --jobs=5 --retry=3
|
11
|
+
GEMSPEC_FILE = $(GEM_NAME).gemspec
|
12
|
+
|
13
|
+
.PHONY: all
|
14
|
+
all: setup $(ALL_TARGETS_EXT)
|
15
|
+
|
16
|
+
.PHONY: setup
|
17
|
+
setup: .setup
|
18
|
+
|
19
|
+
.setup:
|
20
|
+
$(BUNDLER) install $(BUNDLER_OPTIONS)
|
21
|
+
touch $@
|
22
|
+
|
23
|
+
.PHONY: install
|
24
|
+
install:
|
25
|
+
gem_file=$$(gem build $(GEMSPEC_FILE) | grep 'File:' | tail -1 | awk '{ print $$2 }'); \
|
26
|
+
sudo gem install $$gem_file; \
|
27
|
+
$(RM) $$gem_file
|
28
|
+
|
29
|
+
.PHONY: uninstall
|
30
|
+
uninstall:
|
31
|
+
sudo gem uninstall $(GEM_NAME)
|
32
|
+
|
33
|
+
.PHONY: update
|
34
|
+
update:
|
35
|
+
$(BUNDLER) update
|
36
|
+
|
37
|
+
.PHONY: clean
|
38
|
+
clean:
|
39
|
+
$(RM) .bundle
|
40
|
+
$(RM) .setup
|
41
|
+
$(RM) Gemfile.lock
|
42
|
+
|
43
|
+
.PHONY: release
|
44
|
+
release: | releases
|
45
|
+
set -e; \
|
46
|
+
gem_file=$$(gem build $(GEMSPEC_FILE) | grep 'File:' | tail -1 | awk '{ print $$2 }'); \
|
47
|
+
dst="releases/$$gem_file"; \
|
48
|
+
[ ! -f $$dst ]; \
|
49
|
+
$(MV) $$gem_file releases; \
|
50
|
+
gem push $$dst; \
|
51
|
+
echo 'done'
|
52
|
+
|
53
|
+
releases:
|
54
|
+
$(MKDIR) $@
|
55
|
+
|
56
|
+
tmp:
|
57
|
+
$(MKDIR) $@
|
58
|
+
$(CHMOD) u=rwx,go-rwx $@
|
data/README.md
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# Timr
|
2
|
+
|
3
|
+
A Time Tracking Tool for the Command-line.
|
4
|
+
|
5
|
+
## License
|
6
|
+
|
7
|
+
Copyright (C) 2016 Christian Mayer <http://fox21.at>
|
8
|
+
|
9
|
+
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
10
|
+
|
11
|
+
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>.
|
data/bin/dev
ADDED
data/bin/timr
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# coding: UTF-8
|
3
|
+
|
4
|
+
require 'optparse'
|
5
|
+
require 'timr'
|
6
|
+
|
7
|
+
|
8
|
+
@options = {
|
9
|
+
:dir => nil,
|
10
|
+
}
|
11
|
+
opts = OptionParser.new do |o|
|
12
|
+
o.banner = 'Usage: timr [options]'
|
13
|
+
o.separator('')
|
14
|
+
|
15
|
+
o.on_tail('-d', '--dir <path>', 'Path to a timr directory.') do |path|
|
16
|
+
@options[:dir] = path
|
17
|
+
end
|
18
|
+
|
19
|
+
o.on_tail('-v', '--version', 'Show version.') do
|
20
|
+
puts "#{TheFox::Timr::NAME} #{TheFox::Timr::VERSION} (#{TheFox::Timr::DATE})"
|
21
|
+
puts "#{TheFox::Timr::HOMEPAGE}"
|
22
|
+
exit
|
23
|
+
end
|
24
|
+
|
25
|
+
o.on_tail('-h', '--help', 'Show this message.') do
|
26
|
+
puts o
|
27
|
+
puts
|
28
|
+
exit 3
|
29
|
+
end
|
30
|
+
end
|
31
|
+
ARGV << '-h' if ARGV.count == 0
|
32
|
+
opts.parse(ARGV)
|
33
|
+
|
34
|
+
puts 'currently under heavy development'
|
35
|
+
|
36
|
+
# timr = TheFox::Timr::Timr.new(@options[:dir])
|
37
|
+
# timr.run
|
38
|
+
# timr.close
|
data/lib/timr.rb
ADDED
data/lib/timr/stack.rb
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
|
2
|
+
module TheFox
|
3
|
+
module Timr
|
4
|
+
|
5
|
+
class Stack
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@tasks = []
|
9
|
+
@task = nil
|
10
|
+
end
|
11
|
+
|
12
|
+
def task
|
13
|
+
@task
|
14
|
+
end
|
15
|
+
|
16
|
+
def has_task?
|
17
|
+
!@task.nil?
|
18
|
+
end
|
19
|
+
|
20
|
+
def create(name, description)
|
21
|
+
@task = Task.new
|
22
|
+
@task.name = name
|
23
|
+
@task.description = description
|
24
|
+
|
25
|
+
@task
|
26
|
+
end
|
27
|
+
|
28
|
+
def length
|
29
|
+
@tasks.length
|
30
|
+
end
|
31
|
+
|
32
|
+
def tasks_texts
|
33
|
+
show_star = length > 1
|
34
|
+
@tasks.map{ |task|
|
35
|
+
status = task.status
|
36
|
+
status = '*' if task == @task
|
37
|
+
"#{status} #{task.name}"
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
def pop
|
42
|
+
old = @tasks.pop
|
43
|
+
if !old.nil?
|
44
|
+
old.stop
|
45
|
+
@task = @tasks.last
|
46
|
+
@task.start if has_task?
|
47
|
+
true
|
48
|
+
else
|
49
|
+
false
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def pop_all(new_task = nil)
|
54
|
+
@tasks.each do |task|
|
55
|
+
task.stop
|
56
|
+
end
|
57
|
+
@tasks = []
|
58
|
+
@task = nil
|
59
|
+
|
60
|
+
if !new_task.nil?
|
61
|
+
push(new_task)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def push(task)
|
66
|
+
@task.stop if has_task?
|
67
|
+
|
68
|
+
@task = task
|
69
|
+
@task.start
|
70
|
+
@tasks << @task
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|
data/lib/timr/task.rb
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
|
2
|
+
require 'time'
|
3
|
+
#require 'yaml'
|
4
|
+
require 'yaml/store'
|
5
|
+
require 'uuid'
|
6
|
+
|
7
|
+
module TheFox
|
8
|
+
module Timr
|
9
|
+
|
10
|
+
class Task
|
11
|
+
|
12
|
+
def initialize(path = nil)
|
13
|
+
# Status
|
14
|
+
# :running
|
15
|
+
# :stop
|
16
|
+
@status = :stop
|
17
|
+
@changed = false
|
18
|
+
|
19
|
+
@meta = {
|
20
|
+
'id' => UUID.new.generate,
|
21
|
+
'name' => nil,
|
22
|
+
'description' => nil,
|
23
|
+
'created' => Time.now.strftime(TIME_FORMAT),
|
24
|
+
'modified' => Time.now.strftime(TIME_FORMAT),
|
25
|
+
}
|
26
|
+
@time = nil
|
27
|
+
@timeline = []
|
28
|
+
|
29
|
+
@path = path
|
30
|
+
if !@path.nil?
|
31
|
+
load_from_file(@path)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def load_from_file(path)
|
36
|
+
content = YAML::load_file(path)
|
37
|
+
@meta = content['meta']
|
38
|
+
@timeline = content['timeline']
|
39
|
+
.map{ |time_raw|
|
40
|
+
time = {
|
41
|
+
'b' => Time.parse(time_raw['b']),
|
42
|
+
'e' => Time.parse(time_raw['e']),
|
43
|
+
}
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
def save_to_file(basepath)
|
48
|
+
if @changed
|
49
|
+
path = File.expand_path("task_#{@meta['id']}.yml", basepath)
|
50
|
+
|
51
|
+
timeline_c = @timeline
|
52
|
+
.clone
|
53
|
+
.map{ |time|
|
54
|
+
time = time.clone
|
55
|
+
if !time['b'].nil?
|
56
|
+
time['b'] = time['b'].strftime(TIME_FORMAT)
|
57
|
+
end
|
58
|
+
if !time['e'].nil?
|
59
|
+
time['e'] = time['e'].strftime(TIME_FORMAT)
|
60
|
+
end
|
61
|
+
time
|
62
|
+
}
|
63
|
+
|
64
|
+
store = YAML::Store.new(path)
|
65
|
+
store.transaction do
|
66
|
+
store['meta'] = @meta
|
67
|
+
store['timeline'] = timeline_c
|
68
|
+
end
|
69
|
+
@changed = false
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def running?
|
74
|
+
@status == :running
|
75
|
+
end
|
76
|
+
|
77
|
+
def status
|
78
|
+
case @status
|
79
|
+
when :running
|
80
|
+
'>'
|
81
|
+
when :stop
|
82
|
+
'|'
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def changed
|
87
|
+
@changed = true
|
88
|
+
@meta['modified'] = Time.now.strftime(TIME_FORMAT)
|
89
|
+
end
|
90
|
+
|
91
|
+
def id
|
92
|
+
@meta['id']
|
93
|
+
end
|
94
|
+
|
95
|
+
def name
|
96
|
+
@meta['name']
|
97
|
+
end
|
98
|
+
|
99
|
+
def name=(name)
|
100
|
+
changed
|
101
|
+
@meta['name'] = name
|
102
|
+
end
|
103
|
+
|
104
|
+
def description=(description)
|
105
|
+
changed
|
106
|
+
@meta['description'] = description == '' ? nil : description
|
107
|
+
end
|
108
|
+
|
109
|
+
def start
|
110
|
+
if !running?
|
111
|
+
changed
|
112
|
+
@time = {
|
113
|
+
'b' => Time.now, # begin
|
114
|
+
'e' => nil, # end
|
115
|
+
}
|
116
|
+
@timeline << @time
|
117
|
+
end
|
118
|
+
@status = :running
|
119
|
+
end
|
120
|
+
|
121
|
+
def stop
|
122
|
+
if running? && !@time.nil?
|
123
|
+
changed
|
124
|
+
@time['e'] = Time.now
|
125
|
+
end
|
126
|
+
@status = :stop
|
127
|
+
end
|
128
|
+
|
129
|
+
def toggle
|
130
|
+
if running?
|
131
|
+
stop
|
132
|
+
else
|
133
|
+
start
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def to_s
|
138
|
+
name
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
144
|
+
end
|
data/lib/timr/timr.rb
ADDED
@@ -0,0 +1,392 @@
|
|
1
|
+
|
2
|
+
require 'curses'
|
3
|
+
require 'time'
|
4
|
+
|
5
|
+
module TheFox
|
6
|
+
module Timr
|
7
|
+
|
8
|
+
class Timr
|
9
|
+
|
10
|
+
#include Curses
|
11
|
+
|
12
|
+
def initialize(path)
|
13
|
+
@base_dir_path = File.expand_path(path)
|
14
|
+
@base_dir_name = File.basename(@base_dir_path)
|
15
|
+
@data_dir_path = "#{@base_dir_path}/data"
|
16
|
+
|
17
|
+
puts "base: #{@base_dir_path}"
|
18
|
+
puts "name: #{@base_dir_name}"
|
19
|
+
puts "data: #{@data_dir_path}"
|
20
|
+
|
21
|
+
@stack = Stack.new
|
22
|
+
@tasks = {}
|
23
|
+
|
24
|
+
init_dirs
|
25
|
+
tasks_load
|
26
|
+
|
27
|
+
@window = nil
|
28
|
+
@window_timeline = TimelineWindow.new
|
29
|
+
@window_help = HelpWindow.new
|
30
|
+
@window_test = TestWindow.new
|
31
|
+
|
32
|
+
@window_tasks = TasksWindow.new
|
33
|
+
@window_tasks.tasks = @tasks
|
34
|
+
end
|
35
|
+
|
36
|
+
def init_dirs
|
37
|
+
if !Dir.exist?(@base_dir_path)
|
38
|
+
Dir.mkdir(@base_dir_path)
|
39
|
+
end
|
40
|
+
if !Dir.exist?(@data_dir_path)
|
41
|
+
Dir.mkdir(@data_dir_path)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def tasks_load
|
46
|
+
Dir.chdir(@data_dir_path) do
|
47
|
+
Dir['task_*.yml'].each do |file_name|
|
48
|
+
puts "file: '#{file_name}'"
|
49
|
+
task = Task.new(file_name)
|
50
|
+
@tasks[task.id] = task
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def tasks_stop
|
56
|
+
@tasks.each do |task_id, task|
|
57
|
+
task.stop
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def tasks_save
|
62
|
+
Dir.chdir(@data_dir_path) do
|
63
|
+
@tasks.each do |task_id, task|
|
64
|
+
task.save_to_file('.')
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def init_curses
|
70
|
+
Curses.noecho
|
71
|
+
Curses.timeout = TIMEOUT
|
72
|
+
Curses.curs_set(0)
|
73
|
+
Curses.init_screen
|
74
|
+
Curses.start_color
|
75
|
+
Curses.use_default_colors
|
76
|
+
Curses.crmode
|
77
|
+
Curses.stdscr.keypad(true)
|
78
|
+
|
79
|
+
Curses.init_pair(Curses::COLOR_BLUE, Curses::COLOR_WHITE, Curses::COLOR_BLUE)
|
80
|
+
Curses.init_pair(Curses::COLOR_RED, Curses::COLOR_WHITE, Curses::COLOR_RED)
|
81
|
+
Curses.init_pair(Curses::COLOR_YELLOW, Curses::COLOR_BLACK, Curses::COLOR_YELLOW)
|
82
|
+
end
|
83
|
+
|
84
|
+
def title_line
|
85
|
+
title = "#{NAME} #{VERSION} -- #{@base_dir_name}"
|
86
|
+
if Curses.cols <= title.length + 1
|
87
|
+
title = "#{NAME} #{VERSION}"
|
88
|
+
if Curses.cols <= title.length + 1
|
89
|
+
title = NAME
|
90
|
+
end
|
91
|
+
end
|
92
|
+
rest = Curses.cols - title.length - COL
|
93
|
+
|
94
|
+
Curses.setpos(0, 0)
|
95
|
+
Curses.attron(Curses.color_pair(Curses::COLOR_BLUE) | Curses::A_BOLD) do
|
96
|
+
Curses.addstr(' ' * COL + title + ' ' * rest)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def status_text(text, attrn = Curses::A_NORMAL)
|
101
|
+
line_nr = Curses.lines - 1
|
102
|
+
|
103
|
+
Curses.setpos(line_nr, 0)
|
104
|
+
Curses.clrtoeol
|
105
|
+
Curses.setpos(line_nr, COL)
|
106
|
+
Curses.attron(attrn) do
|
107
|
+
Curses.addstr(text)
|
108
|
+
end
|
109
|
+
Curses.refresh
|
110
|
+
end
|
111
|
+
|
112
|
+
def status_input(text)
|
113
|
+
Curses.echo
|
114
|
+
Curses.timeout = -1
|
115
|
+
Curses.curs_set(1)
|
116
|
+
|
117
|
+
status_text(text)
|
118
|
+
|
119
|
+
input = ''
|
120
|
+
abort = false
|
121
|
+
loop do
|
122
|
+
key_pressed = Curses.getch
|
123
|
+
case key_pressed
|
124
|
+
when 27
|
125
|
+
abort = true
|
126
|
+
break
|
127
|
+
when Curses::Key::BACKSPACE
|
128
|
+
Curses.stdscr.delch
|
129
|
+
input = input[0..-2]
|
130
|
+
when 10
|
131
|
+
break
|
132
|
+
else
|
133
|
+
input += key_pressed
|
134
|
+
end
|
135
|
+
end
|
136
|
+
if abort
|
137
|
+
input = nil
|
138
|
+
end
|
139
|
+
|
140
|
+
Curses.noecho
|
141
|
+
Curses.timeout = TIMEOUT
|
142
|
+
Curses.curs_set(0)
|
143
|
+
|
144
|
+
input
|
145
|
+
end
|
146
|
+
|
147
|
+
def status_line(init = false)
|
148
|
+
line_nr = Curses.lines - 2
|
149
|
+
|
150
|
+
Curses.attron(Curses.color_pair(Curses::COLOR_YELLOW) | Curses::A_NORMAL) do
|
151
|
+
if init
|
152
|
+
Curses.setpos(line_nr, 0)
|
153
|
+
Curses.clrtoeol
|
154
|
+
Curses.addstr(' ' * Curses.cols)
|
155
|
+
end
|
156
|
+
|
157
|
+
Curses.setpos(line_nr, COL)
|
158
|
+
if @stack.has_task?
|
159
|
+
Curses.addstr(@stack.task.status)
|
160
|
+
else
|
161
|
+
Curses.addstr(TASK_NO_TASK_LOADED_C)
|
162
|
+
end
|
163
|
+
|
164
|
+
if Curses.cols > MIN_COLS
|
165
|
+
time_format = '%F %R %Z'
|
166
|
+
if Curses.cols <= 30
|
167
|
+
time_format = '%R'
|
168
|
+
elsif Curses.cols <= 40
|
169
|
+
time_format = '%m-%d %R'
|
170
|
+
elsif Curses.cols <= 50
|
171
|
+
time_format = '%y-%m-%d %R'
|
172
|
+
elsif Curses.cols <= 60
|
173
|
+
time_format = '%F %R'
|
174
|
+
elsif Curses.cols > 80
|
175
|
+
time_format = '%F %T %Z'
|
176
|
+
end
|
177
|
+
time_str = Time.now.strftime(time_format)
|
178
|
+
Curses.setpos(line_nr, Curses.cols - time_str.length - 1)
|
179
|
+
Curses.addstr(time_str)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
Curses.refresh
|
184
|
+
end
|
185
|
+
|
186
|
+
def window_show(window)
|
187
|
+
@window = window
|
188
|
+
window_refresh
|
189
|
+
end
|
190
|
+
|
191
|
+
def window_refresh
|
192
|
+
max_lines = content_length
|
193
|
+
(1..max_lines).each do |line_nr|
|
194
|
+
Curses.setpos(line_nr, 0)
|
195
|
+
Curses.clrtoeol
|
196
|
+
end
|
197
|
+
|
198
|
+
if !@window.nil?
|
199
|
+
line_nr = 1
|
200
|
+
@window.content_refresh
|
201
|
+
page_length = @window.page_length
|
202
|
+
current_line = @window.current_line
|
203
|
+
max_line_len = Curses.cols - 2
|
204
|
+
@window.page.each do |line_object|
|
205
|
+
is_cursor = line_nr == @window.cursor
|
206
|
+
|
207
|
+
line_text = line_object.to_s
|
208
|
+
#line_text = "#{line_text} #{is_cursor ? 'X' : ''}"
|
209
|
+
if line_text.length > max_line_len
|
210
|
+
cut = line_text.length - max_line_len + 4
|
211
|
+
line_text = "#{line_text[0..-cut]}..."
|
212
|
+
end
|
213
|
+
|
214
|
+
rest = Curses.cols - line_text.length - COL
|
215
|
+
|
216
|
+
if is_cursor
|
217
|
+
Curses.setpos(line_nr, 0)
|
218
|
+
Curses.attron(Curses.color_pair(Curses::COLOR_BLUE) | Curses::A_BOLD) do
|
219
|
+
Curses.addstr(' ' * COL + line_text + ' ' * rest)
|
220
|
+
end
|
221
|
+
else
|
222
|
+
Curses.setpos(line_nr, COL)
|
223
|
+
Curses.addstr(line_text)
|
224
|
+
end
|
225
|
+
|
226
|
+
line_nr += 1
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
Curses.refresh
|
231
|
+
end
|
232
|
+
|
233
|
+
def content_length
|
234
|
+
Curses.lines - RESERVED_LINES - @stack.length
|
235
|
+
end
|
236
|
+
|
237
|
+
def update_content_length
|
238
|
+
cl = content_length
|
239
|
+
|
240
|
+
@window_timeline.content_length = cl
|
241
|
+
@window_help.content_length = cl
|
242
|
+
@window_test.content_length = cl
|
243
|
+
@window_tasks.content_length = cl
|
244
|
+
end
|
245
|
+
|
246
|
+
def refresh
|
247
|
+
update_content_length
|
248
|
+
title_line
|
249
|
+
status_line(true)
|
250
|
+
stack_lines
|
251
|
+
window_refresh
|
252
|
+
end
|
253
|
+
|
254
|
+
def stack_lines
|
255
|
+
line_nr = Curses.lines - (3 + (@stack.length - 1))
|
256
|
+
|
257
|
+
Curses.attron(Curses.color_pair(Curses::COLOR_BLUE)) do
|
258
|
+
@stack.tasks_texts.reverse.each do |line_text|
|
259
|
+
rest = Curses.cols - line_text.length - COL
|
260
|
+
|
261
|
+
Curses.setpos(line_nr, 0)
|
262
|
+
Curses.clrtoeol
|
263
|
+
Curses.addstr(' ' * COL + line_text + ' ' * rest)
|
264
|
+
|
265
|
+
line_nr += 1
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
Curses.refresh
|
270
|
+
end
|
271
|
+
|
272
|
+
def run
|
273
|
+
init_curses
|
274
|
+
update_content_length
|
275
|
+
title_line
|
276
|
+
status_line(true)
|
277
|
+
|
278
|
+
loop do
|
279
|
+
key_pressed = Curses.getch
|
280
|
+
|
281
|
+
case key_pressed
|
282
|
+
when Curses::Key::NPAGE
|
283
|
+
@window.next_page if !@window.nil?
|
284
|
+
window_refresh
|
285
|
+
when Curses::Key::PPAGE
|
286
|
+
@window.previous_page if !@window.nil?
|
287
|
+
window_refresh
|
288
|
+
when Curses::Key::DOWN
|
289
|
+
#@window.next_line if !@window.nil?
|
290
|
+
@window.cursor_next_line if !@window.nil?
|
291
|
+
#status_text("Cursor: #{@window.cursor} c=#{content_length} l=#{@window.current_line} pr=#{@window.page_refreshes} cr=#{@window.content_refreshes}") if !@window.nil?
|
292
|
+
window_refresh
|
293
|
+
when Curses::Key::UP
|
294
|
+
#@window.previous_line if !@window.nil?
|
295
|
+
@window.cursor_previous_line if !@window.nil?
|
296
|
+
#status_text("Cursor: #{@window.cursor} c=#{content_length} l=#{@window.current_line} pr=#{@window.page_refreshes} cr=#{@window.content_refreshes}") if !@window.nil?
|
297
|
+
window_refresh
|
298
|
+
when Curses::Key::HOME
|
299
|
+
@window.first_page if !@window.nil?
|
300
|
+
window_refresh
|
301
|
+
when Curses::Key::END
|
302
|
+
@window.last_page if !@window.nil?
|
303
|
+
window_refresh
|
304
|
+
when Curses::Key::RESIZE
|
305
|
+
update_content_length
|
306
|
+
status_text("Resizing: #{Curses.lines}x#{Curses.cols}")
|
307
|
+
|
308
|
+
# Refreshing the complete screen while resizing
|
309
|
+
# can make everything slower. So for fast resizing
|
310
|
+
# comment this line.
|
311
|
+
refresh
|
312
|
+
when 10
|
313
|
+
object = @window.page_object if !@window.nil?
|
314
|
+
if object.is_a?(Task)
|
315
|
+
status_text("Object: #{object.class} #{object}")
|
316
|
+
end
|
317
|
+
when 'r'
|
318
|
+
refresh
|
319
|
+
status_text('')
|
320
|
+
when 'n'
|
321
|
+
task_name = status_input('New task: ')
|
322
|
+
if task_name.nil?
|
323
|
+
status_text('Aborted.')
|
324
|
+
else
|
325
|
+
task_description = status_input('Description: ')
|
326
|
+
|
327
|
+
task = @stack.create(task_name, task_description)
|
328
|
+
@tasks[task.id] = task
|
329
|
+
@stack.pop_all(task)
|
330
|
+
@window_tasks.content_changed
|
331
|
+
|
332
|
+
status_text("Task '#{task_name}' created: #{task.id}")
|
333
|
+
|
334
|
+
stack_lines
|
335
|
+
window_refresh
|
336
|
+
end
|
337
|
+
when 'p'
|
338
|
+
task = Task.new
|
339
|
+
task.name = "task #{Time.now.strftime(TIME_FORMAT)}"
|
340
|
+
task.description = 'description1'
|
341
|
+
|
342
|
+
@tasks[task.id] = task
|
343
|
+
@stack.push(task)
|
344
|
+
|
345
|
+
status_text("Task '#{task_name}' created: #{task.id}")
|
346
|
+
|
347
|
+
stack_lines
|
348
|
+
when 'x'
|
349
|
+
@stack.task.stop if @stack.has_task?
|
350
|
+
when 'c'
|
351
|
+
@stack.task.toggle if @stack.has_task?
|
352
|
+
when 'v'
|
353
|
+
if @stack.pop
|
354
|
+
stack_lines
|
355
|
+
window_refresh
|
356
|
+
end
|
357
|
+
when 'h', '?'
|
358
|
+
window_show(@window_help)
|
359
|
+
when 't' # Test Windows
|
360
|
+
window_show(@window_test)
|
361
|
+
when '1'
|
362
|
+
window_show(@window_timeline)
|
363
|
+
when '2'
|
364
|
+
window_show(@window_tasks)
|
365
|
+
when 'w'
|
366
|
+
tasks_save
|
367
|
+
when 'q'
|
368
|
+
break
|
369
|
+
when nil
|
370
|
+
# Do some work.
|
371
|
+
status_line
|
372
|
+
else
|
373
|
+
status_text("Invalid key '#{key_pressed}' (#{Curses.keyname(key_pressed)})", Curses.color_pair(Curses::COLOR_RED) | Curses::A_BOLD)
|
374
|
+
end
|
375
|
+
end
|
376
|
+
end
|
377
|
+
|
378
|
+
def close
|
379
|
+
Curses.stdscr.clear
|
380
|
+
Curses.stdscr.refresh
|
381
|
+
Curses.stdscr.close
|
382
|
+
|
383
|
+
Curses.close_screen
|
384
|
+
|
385
|
+
tasks_stop
|
386
|
+
tasks_save
|
387
|
+
end
|
388
|
+
|
389
|
+
end
|
390
|
+
|
391
|
+
end
|
392
|
+
end
|
data/lib/timr/version.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
|
2
|
+
module TheFox
|
3
|
+
module Timr
|
4
|
+
NAME = 'Timr'
|
5
|
+
VERSION = '0.1.0-dev.1'
|
6
|
+
DATE = '2016-05-08'
|
7
|
+
HOMEPAGE = 'https://github.com/TheFox/timr'
|
8
|
+
|
9
|
+
COL = 1
|
10
|
+
MIN_COLS = 20
|
11
|
+
TIMEOUT = 250
|
12
|
+
|
13
|
+
# Reserved Lines
|
14
|
+
# - Title
|
15
|
+
# - Status Time Line
|
16
|
+
# - Status Text Line
|
17
|
+
RESERVED_LINES = 3
|
18
|
+
|
19
|
+
TIME_FORMAT = '%FT%T%z'
|
20
|
+
|
21
|
+
TASK_NO_TASK_LOADED_C = ?-
|
22
|
+
end
|
23
|
+
end
|
data/lib/timr/window.rb
ADDED
@@ -0,0 +1,202 @@
|
|
1
|
+
|
2
|
+
module TheFox
|
3
|
+
module Timr
|
4
|
+
|
5
|
+
class Window
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@content_length = 0
|
9
|
+
@current_line = 0
|
10
|
+
@cursor = 1
|
11
|
+
@content_changed = true
|
12
|
+
@content_refreshes = 1
|
13
|
+
@page = nil
|
14
|
+
@page_changed = true
|
15
|
+
@page_refreshes = 1
|
16
|
+
|
17
|
+
content_refresh
|
18
|
+
end
|
19
|
+
|
20
|
+
def content_length=(content_length)
|
21
|
+
@content_length = content_length
|
22
|
+
end
|
23
|
+
|
24
|
+
def content
|
25
|
+
[]
|
26
|
+
end
|
27
|
+
|
28
|
+
def content_changed
|
29
|
+
@content_changed = true
|
30
|
+
end
|
31
|
+
|
32
|
+
def content_refreshes
|
33
|
+
@content_refreshes
|
34
|
+
end
|
35
|
+
|
36
|
+
def content_refresh
|
37
|
+
if @content_changed
|
38
|
+
@content = content
|
39
|
+
@content_refreshes += 1
|
40
|
+
@content_changed = false
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def page_changed
|
45
|
+
@page_changed = true
|
46
|
+
end
|
47
|
+
|
48
|
+
def page_refreshes
|
49
|
+
@page_refreshes
|
50
|
+
end
|
51
|
+
|
52
|
+
def page
|
53
|
+
if @content.nil?
|
54
|
+
[]
|
55
|
+
else
|
56
|
+
if @page.nil? || @page_changed
|
57
|
+
@page = @content[@current_line, @content_length]
|
58
|
+
@page_refreshes += 1
|
59
|
+
@page_changed = false
|
60
|
+
end
|
61
|
+
@page
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def page_object
|
66
|
+
page[@cursor - 1]
|
67
|
+
end
|
68
|
+
|
69
|
+
def page_length
|
70
|
+
page.length
|
71
|
+
end
|
72
|
+
|
73
|
+
def next_page?
|
74
|
+
has = false
|
75
|
+
new_current_line = @current_line + @content_length
|
76
|
+
new_page = @content[new_current_line, @content_length]
|
77
|
+
if !new_page.nil?
|
78
|
+
new_page_length = new_page.length
|
79
|
+
if new_page_length > 0
|
80
|
+
has = true
|
81
|
+
end
|
82
|
+
end
|
83
|
+
has
|
84
|
+
end
|
85
|
+
|
86
|
+
def next_page(add_lines = nil)
|
87
|
+
if add_lines.nil?
|
88
|
+
add_lines = @content_length
|
89
|
+
end
|
90
|
+
|
91
|
+
page_changed
|
92
|
+
@current_line += add_lines if next_page?
|
93
|
+
cursor_set_to_last_if_out_of_range
|
94
|
+
end
|
95
|
+
|
96
|
+
def previous_page?
|
97
|
+
@current_line > 0
|
98
|
+
end
|
99
|
+
|
100
|
+
def previous_page
|
101
|
+
if previous_page?
|
102
|
+
page_changed
|
103
|
+
@current_line -= @content_length
|
104
|
+
if @current_line < 0
|
105
|
+
@current_line = 0
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def first_page
|
111
|
+
page_changed
|
112
|
+
@current_line = 0
|
113
|
+
cursor_first_line
|
114
|
+
end
|
115
|
+
|
116
|
+
def last_page?
|
117
|
+
!next_page?
|
118
|
+
end
|
119
|
+
|
120
|
+
def last_page
|
121
|
+
page_changed
|
122
|
+
@current_line = @content.length - @content_length
|
123
|
+
cursor_last_line
|
124
|
+
end
|
125
|
+
|
126
|
+
def next_line
|
127
|
+
page_changed
|
128
|
+
next_page(1)
|
129
|
+
end
|
130
|
+
|
131
|
+
def previous_line
|
132
|
+
page_changed
|
133
|
+
previous_page(1)
|
134
|
+
end
|
135
|
+
|
136
|
+
def current_line
|
137
|
+
@current_line
|
138
|
+
end
|
139
|
+
|
140
|
+
def cursor
|
141
|
+
@cursor
|
142
|
+
end
|
143
|
+
|
144
|
+
def cursor_next_line
|
145
|
+
@cursor += 1
|
146
|
+
border = @content_length - 2
|
147
|
+
if @cursor > border
|
148
|
+
if last_page?
|
149
|
+
if @cursor > @content_length
|
150
|
+
@cursor = @content_length
|
151
|
+
end
|
152
|
+
else
|
153
|
+
@cursor = border
|
154
|
+
next_line
|
155
|
+
end
|
156
|
+
else
|
157
|
+
|
158
|
+
end
|
159
|
+
|
160
|
+
cursor_set_to_last_if_out_of_range
|
161
|
+
end
|
162
|
+
|
163
|
+
def cursor_previous_line
|
164
|
+
@cursor -= 1
|
165
|
+
border = 3
|
166
|
+
if @cursor < 1
|
167
|
+
@cursor = 1
|
168
|
+
|
169
|
+
if previous_page?
|
170
|
+
previous_line
|
171
|
+
else
|
172
|
+
|
173
|
+
end
|
174
|
+
elsif @cursor <= border
|
175
|
+
if previous_page?
|
176
|
+
@cursor = border
|
177
|
+
previous_line
|
178
|
+
end
|
179
|
+
else
|
180
|
+
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def cursor_last_line
|
185
|
+
@cursor = @content_length
|
186
|
+
end
|
187
|
+
|
188
|
+
def cursor_first_line
|
189
|
+
@cursor = 1
|
190
|
+
end
|
191
|
+
|
192
|
+
def cursor_set_to_last_if_out_of_range
|
193
|
+
plength = page_length
|
194
|
+
if @cursor > plength
|
195
|
+
@cursor = plength
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
end
|
200
|
+
|
201
|
+
end
|
202
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
|
2
|
+
module TheFox
|
3
|
+
module Timr
|
4
|
+
|
5
|
+
class HelpWindow < Window
|
6
|
+
|
7
|
+
def content
|
8
|
+
[
|
9
|
+
'#### Help ####',
|
10
|
+
'',
|
11
|
+
' n .. Create a New Task',
|
12
|
+
' c .. Current Task: Start/Continue',
|
13
|
+
' x .. Current Task: Stop',
|
14
|
+
' v .. Current Task: Stop and Pop from Stack',
|
15
|
+
' r .. Refresh Window',
|
16
|
+
' w .. Write all changes.',
|
17
|
+
' q .. Exit',
|
18
|
+
' h .. Help',
|
19
|
+
' 1 .. Timeline Window',
|
20
|
+
' 2 .. Tasks Window',
|
21
|
+
' KEY UP .. Move Cursor up.',
|
22
|
+
' KEY DOWN .. Move Cursor down.',
|
23
|
+
'',
|
24
|
+
'Current Task Status',
|
25
|
+
'',
|
26
|
+
" #{TASK_NO_TASK_LOADED_C} .. No task loaded.",
|
27
|
+
' | .. Task stopped.',
|
28
|
+
' > .. Task is running.',
|
29
|
+
#'',
|
30
|
+
]
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
|
2
|
+
require 'pp'
|
3
|
+
|
4
|
+
module TheFox
|
5
|
+
module Timr
|
6
|
+
|
7
|
+
class TasksWindow < Window
|
8
|
+
|
9
|
+
@tasks = nil
|
10
|
+
|
11
|
+
def tasks=(tasks)
|
12
|
+
content_changed
|
13
|
+
@tasks = tasks
|
14
|
+
end
|
15
|
+
|
16
|
+
def content
|
17
|
+
if @tasks.nil? || @tasks.length == 0
|
18
|
+
[
|
19
|
+
'',
|
20
|
+
'#### NO TASKS YET ####',
|
21
|
+
'',
|
22
|
+
"Press 'n' to create a new task.",
|
23
|
+
]
|
24
|
+
else
|
25
|
+
@tasks
|
26
|
+
.sort_by{ |k, v|
|
27
|
+
v.name
|
28
|
+
}
|
29
|
+
.map{ |a| a[1] }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
data/timr.gemspec
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# coding: UTF-8
|
2
|
+
|
3
|
+
lib = File.expand_path('../lib', __FILE__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
|
6
|
+
require 'timr/version'
|
7
|
+
|
8
|
+
Gem::Specification.new do |spec|
|
9
|
+
spec.name = 'timr'
|
10
|
+
spec.version = TheFox::Timr::VERSION
|
11
|
+
spec.date = TheFox::Timr::DATE
|
12
|
+
spec.author = 'Christian Mayer'
|
13
|
+
spec.email = 'christian@fox21.at'
|
14
|
+
|
15
|
+
spec.summary = %q{Timr}
|
16
|
+
spec.description = %q{Time Tracking Tool for the Command-line.}
|
17
|
+
spec.homepage = TheFox::Timr::HOMEPAGE
|
18
|
+
spec.license = 'GPL-3.0'
|
19
|
+
|
20
|
+
spec.files = `git ls-files -z`.split("\x0").reject{ |f| f.match(%r{^(test|spec|features)/}) }
|
21
|
+
spec.bindir = 'bin'
|
22
|
+
spec.executables = ['timr']
|
23
|
+
spec.require_paths = ['lib']
|
24
|
+
spec.required_ruby_version = '>=2.1.0'
|
25
|
+
|
26
|
+
spec.add_development_dependency 'minitest', '~>5.8'
|
27
|
+
|
28
|
+
spec.add_dependency 'curses', '~>1.0'
|
29
|
+
spec.add_dependency 'uuid', '~>2.3'
|
30
|
+
|
31
|
+
spec.add_dependency 'thefox-ext', '~>1.4'
|
32
|
+
end
|
metadata
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: timr
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0.pre.dev.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Christian Mayer
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-05-08 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: minitest
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '5.8'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '5.8'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: curses
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: uuid
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '2.3'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '2.3'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: thefox-ext
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.4'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.4'
|
69
|
+
description: Time Tracking Tool for the Command-line.
|
70
|
+
email: christian@fox21.at
|
71
|
+
executables:
|
72
|
+
- timr
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- ".editorconfig"
|
77
|
+
- ".gitignore"
|
78
|
+
- Gemfile
|
79
|
+
- Gemfile.lock
|
80
|
+
- Makefile
|
81
|
+
- Makefile.common
|
82
|
+
- README.md
|
83
|
+
- bin/dev
|
84
|
+
- bin/timr
|
85
|
+
- lib/timr.rb
|
86
|
+
- lib/timr/stack.rb
|
87
|
+
- lib/timr/task.rb
|
88
|
+
- lib/timr/timr.rb
|
89
|
+
- lib/timr/version.rb
|
90
|
+
- lib/timr/window.rb
|
91
|
+
- lib/timr/window_help.rb
|
92
|
+
- lib/timr/window_tasks.rb
|
93
|
+
- lib/timr/window_test.rb
|
94
|
+
- lib/timr/window_timeline.rb
|
95
|
+
- timr.gemspec
|
96
|
+
- timr.sublime-project
|
97
|
+
homepage: https://github.com/TheFox/timr
|
98
|
+
licenses:
|
99
|
+
- GPL-3.0
|
100
|
+
metadata: {}
|
101
|
+
post_install_message:
|
102
|
+
rdoc_options: []
|
103
|
+
require_paths:
|
104
|
+
- lib
|
105
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - ">="
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: 2.1.0
|
110
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
111
|
+
requirements:
|
112
|
+
- - ">"
|
113
|
+
- !ruby/object:Gem::Version
|
114
|
+
version: 1.3.1
|
115
|
+
requirements: []
|
116
|
+
rubyforge_project:
|
117
|
+
rubygems_version: 2.4.7
|
118
|
+
signing_key:
|
119
|
+
specification_version: 4
|
120
|
+
summary: Timr
|
121
|
+
test_files: []
|
122
|
+
has_rdoc:
|