ftg 2.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 +7 -0
- data/.gitignore +15 -0
- data/.idea/.ftg.iml +31 -0
- data/.idea/misc.xml +14 -0
- data/.idea/modules.xml +8 -0
- data/.idea/vcs.xml +6 -0
- data/.idea/workspace.xml +45 -0
- data/.ruby-version +1 -0
- data/Gemfile +17 -0
- data/LICENSE.txt +21 -0
- data/README.md +61 -0
- data/Rakefile +1 -0
- data/bin/console +14 -0
- data/bin/ftg +7 -0
- data/bin/setup +7 -0
- data/config/private.json.example +6 -0
- data/config/public.json +30 -0
- data/ftg.gemspec +32 -0
- data/lib/coffee.rb +79 -0
- data/lib/colors.rb +38 -0
- data/lib/ftg.rb +316 -0
- data/lib/ftg_logger.rb +67 -0
- data/lib/ftg_options.rb +28 -0
- data/lib/ftg_stats.rb +135 -0
- data/lib/ftg_sync.rb +112 -0
- data/lib/idle_logger.rb +9 -0
- data/lib/interactive.rb +115 -0
- data/lib/migrations/create_tasks.rb +21 -0
- data/lib/models/task.rb +5 -0
- data/lib/task_formatter.rb +51 -0
- data/lib/utils.rb +24 -0
- metadata +105 -0
data/lib/ftg_sync.rb
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
require 'uri'
|
|
2
|
+
require 'httparty'
|
|
3
|
+
|
|
4
|
+
class FtgSync
|
|
5
|
+
TOGGL_API_TOKEN = '317c14d9c290d3c6cc1e4f35a2ad8c80'
|
|
6
|
+
TIME_ENTRIES_URL = 'https://toggl.com/api/v8/time_entries'
|
|
7
|
+
WORKSPACE_ID = 939576
|
|
8
|
+
|
|
9
|
+
PIDS = {
|
|
10
|
+
autres: 9800260,
|
|
11
|
+
maintenance: 9800223,
|
|
12
|
+
projects: 10669186,
|
|
13
|
+
reu: 9800248,
|
|
14
|
+
sprint: 9800218,
|
|
15
|
+
support: 9800226,
|
|
16
|
+
technical: 9800254
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
def initialize
|
|
20
|
+
@headers = {
|
|
21
|
+
'Content-Type' => 'application/json'
|
|
22
|
+
}
|
|
23
|
+
@credentials = {
|
|
24
|
+
username: TOGGL_API_TOKEN,
|
|
25
|
+
password: 'api_token'
|
|
26
|
+
}
|
|
27
|
+
@base_query = {
|
|
28
|
+
user_agent: 'ftg',
|
|
29
|
+
workspace_id: WORKSPACE_ID
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@base_params = {
|
|
33
|
+
headers: @headers,
|
|
34
|
+
query: @base_query,
|
|
35
|
+
basic_auth: @credentials
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@base_params_jira = {
|
|
39
|
+
headers: @headers,
|
|
40
|
+
basic_auth: {
|
|
41
|
+
username: 'benjamin.crouzier',
|
|
42
|
+
password: 'morebabyplease'
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def run
|
|
48
|
+
# current_user_id = me['data']['id']
|
|
49
|
+
abort('no')
|
|
50
|
+
binding.pry
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# workspace_users
|
|
54
|
+
|
|
55
|
+
# between_time_range
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def create_entry(description, duration_sec, start_date, type)
|
|
59
|
+
puts "creating entry #{description}, #{duration_sec}, #{start_date}, #{type}"
|
|
60
|
+
params = { "time_entry" =>
|
|
61
|
+
{ "description" => description, "tags" => [],
|
|
62
|
+
"duration" => duration_sec,
|
|
63
|
+
"start" => start_date.iso8601,
|
|
64
|
+
"pid" => PIDS[type],
|
|
65
|
+
"created_with" => "ftg"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
HTTParty.post(TIME_ENTRIES_URL, @base_params.merge({body: params.to_json}))
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def delete_entry
|
|
72
|
+
|
|
73
|
+
response = HTTParty.delete(TIME_ENTRIES_URL + '/279467425', @base_params.merge({}))
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def maintenance?(jt)
|
|
77
|
+
get_jt_info(jt)['fields']['customfield_10400']['value'] == 'Maintenance' rescue nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def get_jt_info(jt)
|
|
81
|
+
HTTParty.get(
|
|
82
|
+
"https://jobteaser.atlassian.net/rest/api/2/issue/#{jt.upcase}",
|
|
83
|
+
@base_params_jira.merge({})
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def between_time_range
|
|
88
|
+
start_date = Time.new(2014, 01, 01).iso8601
|
|
89
|
+
end_date = Time.now.iso8601
|
|
90
|
+
|
|
91
|
+
# URI.encode(...) does not escape ':' for some reason
|
|
92
|
+
params = "?start_date=#{CGI.escape(start_date)}&end_date=#{CGI.escape(end_date)}"
|
|
93
|
+
response = HTTParty.get(TIME_ENTRIES_URL + params, @base_params.merge({body: params.to_json}))
|
|
94
|
+
binding.pry
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def workspace_projects
|
|
98
|
+
response = HTTParty.get(
|
|
99
|
+
"https://toggl.com/api/v8/workspaces/#{WORKSPACE_ID}/projects",
|
|
100
|
+
@base_params.merge({})
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def me
|
|
105
|
+
HTTParty.get(
|
|
106
|
+
'https://toggl.com/api/v8/me',
|
|
107
|
+
@base_params.merge({})
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# FtgSync.new.run
|
data/lib/idle_logger.rb
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
|
|
2
|
+
loop do
|
|
3
|
+
idle_cmd = "echo $((`ioreg -c IOHIDSystem | sed -e '/HIDIdleTime/!{ d' -e 't' -e '}' -e 's/.* = //g' -e 'q'` / 1000000000))"
|
|
4
|
+
idle_result = `#{idle_cmd}`
|
|
5
|
+
date_result = `date +%s`
|
|
6
|
+
|
|
7
|
+
`echo "#{idle_result.strip + '\t' + date_result.strip}" >> $HOME/.ftg/log/idle.log`
|
|
8
|
+
sleep 10
|
|
9
|
+
end
|
data/lib/interactive.rb
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
class Interactive
|
|
2
|
+
KEY_TOP = "\e[A"
|
|
3
|
+
KEY_RIGHT = "\e[C"
|
|
4
|
+
KEY_DOWN = "\e[B"
|
|
5
|
+
KEY_LEFT = "\e[D"
|
|
6
|
+
KEY_BACKSPACE = "\x7F"
|
|
7
|
+
KEY_ENTER = "\r"
|
|
8
|
+
KEY_CTRL_C = "\x03"
|
|
9
|
+
KEY_TAB = "\t"
|
|
10
|
+
KEY_ESCAPE = "\e"
|
|
11
|
+
|
|
12
|
+
SEQ_ERASE_LEFT = "\e[D"
|
|
13
|
+
SEQ_ERASE_TO_END_OF_LINE = "\033[K"
|
|
14
|
+
|
|
15
|
+
def initialize
|
|
16
|
+
@term_width = `/usr/bin/env tput cols`.to_i
|
|
17
|
+
@formatter = TaskFormatter.new
|
|
18
|
+
|
|
19
|
+
if @term_width < @formatter.max_width
|
|
20
|
+
message = " Your terminal must be at least #{@formatter.max_width} columns wide "
|
|
21
|
+
STDERR.puts('<' + message.
|
|
22
|
+
rjust(@formatter.max_width + 1 - message.length / 2, '-').
|
|
23
|
+
ljust(@formatter.max_width - 2, '-') + '>')
|
|
24
|
+
exit(1)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def read_char
|
|
29
|
+
begin
|
|
30
|
+
# save previous state of stty
|
|
31
|
+
old_state = `stty -g`
|
|
32
|
+
# disable echoing and enable raw (not having to press enter)
|
|
33
|
+
system "stty raw -echo"
|
|
34
|
+
c = STDIN.getc.chr
|
|
35
|
+
# gather next two characters of special keys
|
|
36
|
+
if (c=="\e")
|
|
37
|
+
extra_thread = Thread.new {
|
|
38
|
+
c = c + STDIN.getc.chr
|
|
39
|
+
c = c + STDIN.getc.chr
|
|
40
|
+
}
|
|
41
|
+
# wait just long enough for special keys to get swallowed
|
|
42
|
+
extra_thread.join(0.00001)
|
|
43
|
+
# kill thread so not-so-long special keys don't wait on getc
|
|
44
|
+
extra_thread.kill
|
|
45
|
+
end
|
|
46
|
+
rescue => ex
|
|
47
|
+
puts "#{ex.class}: #{ex.message}"
|
|
48
|
+
puts ex.backtrace
|
|
49
|
+
ensure
|
|
50
|
+
# restore previous state of stty
|
|
51
|
+
system "stty #{old_state}"
|
|
52
|
+
end
|
|
53
|
+
return c
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def print_tasks
|
|
57
|
+
task_len = TaskFormatter.max_length(@tasks)
|
|
58
|
+
|
|
59
|
+
puts @header
|
|
60
|
+
@tasks.each_with_index do |task, i|
|
|
61
|
+
@formatter.format(task, task_len)
|
|
62
|
+
|
|
63
|
+
print "\e[47m" + "\e[30m" if i == @task_selected
|
|
64
|
+
print @formatter.line_for_interactive
|
|
65
|
+
print "\e[0m" if i == @task_selected
|
|
66
|
+
print ' ' + @formatter.sync_status
|
|
67
|
+
print "\033[K"
|
|
68
|
+
puts ''
|
|
69
|
+
end
|
|
70
|
+
total_time = Utils.format_time(@tasks.map(&:duration).reduce(:+))
|
|
71
|
+
puts "\e[100m #{''.ljust(task_len, ' ')} #{total_time} #{''.rjust(@formatter.bar_size, ' ')} \e[0m\033[K"
|
|
72
|
+
|
|
73
|
+
puts "\033[K"
|
|
74
|
+
print "\033[#{@tasks.length + 3}A"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def interactive_edit(tasks)
|
|
79
|
+
@tasks = tasks
|
|
80
|
+
@deleted_tasks = []
|
|
81
|
+
@header = '2015-06-03 ' + '[↑|↓] navigate, [⇽|⇾] adjust time, [⇐ ] remove, [↵ |q] save, [esc] cancel'.grey
|
|
82
|
+
|
|
83
|
+
@task_selected = 0
|
|
84
|
+
|
|
85
|
+
loop do
|
|
86
|
+
print_tasks
|
|
87
|
+
input = read_char
|
|
88
|
+
|
|
89
|
+
exit if [KEY_CTRL_C, KEY_ESCAPE].include? input
|
|
90
|
+
return @deleted_tasks if [KEY_ENTER, 'q'].include? input
|
|
91
|
+
|
|
92
|
+
top_down = { KEY_DOWN => +1, KEY_TOP => -1 }
|
|
93
|
+
if top_down.keys.include? input
|
|
94
|
+
@task_selected += top_down[input]
|
|
95
|
+
@task_selected %= @tasks.length
|
|
96
|
+
end
|
|
97
|
+
left_right = { KEY_RIGHT => +300, KEY_LEFT => -300 }
|
|
98
|
+
if left_right.keys.include? input
|
|
99
|
+
time = @tasks[@task_selected].duration
|
|
100
|
+
time = [time - (time % 300) + left_right[input], 0].max
|
|
101
|
+
@tasks[@task_selected].duration = time
|
|
102
|
+
@tasks[@task_selected].edited_at = Time.now
|
|
103
|
+
end
|
|
104
|
+
if input == KEY_BACKSPACE
|
|
105
|
+
next if @tasks.length <= 1
|
|
106
|
+
@tasks[@task_selected].deleted_at = Time.now
|
|
107
|
+
@deleted_tasks << @tasks[@task_selected]
|
|
108
|
+
@tasks.delete_at(@task_selected)
|
|
109
|
+
@task_selected -= 1 if @task_selected == @tasks.length
|
|
110
|
+
@task_selected %= @tasks.length
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
class CreateTasks < ActiveRecord::Migration
|
|
2
|
+
def up
|
|
3
|
+
create_table :tasks do |t|
|
|
4
|
+
t.date :day
|
|
5
|
+
t.string :name
|
|
6
|
+
t.datetime :synced_at
|
|
7
|
+
t.datetime :edited_at
|
|
8
|
+
t.datetime :deleted_at
|
|
9
|
+
t.integer :duration
|
|
10
|
+
t.integer :toggl_activity_id
|
|
11
|
+
t.timestamps
|
|
12
|
+
end
|
|
13
|
+
puts 'Migrating up'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def down
|
|
17
|
+
drop_table :tasks
|
|
18
|
+
puts 'Migrating down'
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
end
|
data/lib/models/task.rb
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
class TaskFormatter
|
|
2
|
+
|
|
3
|
+
attr_accessor :bar_size, :max_width
|
|
4
|
+
|
|
5
|
+
def initialize(bar_size = 40, max_width = 94)
|
|
6
|
+
@bar_size = bar_size
|
|
7
|
+
@max_width = max_width
|
|
8
|
+
@task = nil
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def format(task, task_len)
|
|
12
|
+
@task = task
|
|
13
|
+
@task_len = task_len
|
|
14
|
+
self
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.max_length(tasks)
|
|
18
|
+
[tasks.map { |e| e.name.length }.max || 0, 60].min
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def time_bar(full_char = '=', empty_char = ' ', ellipsis = '...')
|
|
22
|
+
time_len = [@task.duration / 300, bar_size].min
|
|
23
|
+
if time_len >= bar_size
|
|
24
|
+
"[#{(full_char * (time_len - 2)) + ellipsis}"
|
|
25
|
+
else
|
|
26
|
+
"[#{full_char * time_len}#{empty_char * (bar_size - time_len)}]"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def sync_status
|
|
31
|
+
!!@task.synced_at ? '✔' : '✘'
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def line_for_interactive
|
|
35
|
+
task_formatted = @task.name[0, @task_len].ljust(@task_len, ' ')
|
|
36
|
+
" #{task_formatted} #{@task.duration_formatted} #{time_bar}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def extract_jt(name)
|
|
40
|
+
match = name[/^jt-[0-9]+/]
|
|
41
|
+
match ? match.upcase : nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def line_for_email
|
|
45
|
+
task_formatted = @task.name[0, @task_len]
|
|
46
|
+
jt = extract_jt(@task.name)
|
|
47
|
+
jira_link = jt ? "https://jobteaser.atlassian.net/browse/#{jt}" : ''
|
|
48
|
+
" #{time_bar('#', ' ', '..')} #{@task.duration_formatted} #{task_formatted} #{jira_link}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
end
|
data/lib/utils.rb
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
class ::Hash
|
|
2
|
+
def deep_merge(second)
|
|
3
|
+
merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
|
|
4
|
+
self.merge(second, &merger)
|
|
5
|
+
end
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
class Utils
|
|
9
|
+
# def self.format_time(seconds)
|
|
10
|
+
# seconds ||= 0
|
|
11
|
+
# Time.at(seconds.round).utc.strftime('%H:%M:%S')#%Y %M %D
|
|
12
|
+
# end
|
|
13
|
+
|
|
14
|
+
def self.format_time(secs)
|
|
15
|
+
# secs ||= 0
|
|
16
|
+
# Time.at(secs.round).utc.strftime('%Hh %Mm')
|
|
17
|
+
secs ||= 0
|
|
18
|
+
'%02sh %02dm' % [secs / 3600, (secs / 60) % 60]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.is_integer?(str)
|
|
22
|
+
str.to_i.to_s == str
|
|
23
|
+
end
|
|
24
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ftg
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: '2.0'
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- pinouchon
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2015-10-18 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: bundler
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.10'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.10'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rake
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '10.0'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '10.0'
|
|
41
|
+
description: Toggl replacement. Time tracking based on git branches
|
|
42
|
+
email:
|
|
43
|
+
- pinouchon@gmail.com
|
|
44
|
+
executables:
|
|
45
|
+
- ftg
|
|
46
|
+
extensions: []
|
|
47
|
+
extra_rdoc_files: []
|
|
48
|
+
files:
|
|
49
|
+
- ".gitignore"
|
|
50
|
+
- ".idea/.ftg.iml"
|
|
51
|
+
- ".idea/misc.xml"
|
|
52
|
+
- ".idea/modules.xml"
|
|
53
|
+
- ".idea/vcs.xml"
|
|
54
|
+
- ".idea/workspace.xml"
|
|
55
|
+
- ".ruby-version"
|
|
56
|
+
- Gemfile
|
|
57
|
+
- Gemfile.lock
|
|
58
|
+
- LICENSE.txt
|
|
59
|
+
- README.md
|
|
60
|
+
- Rakefile
|
|
61
|
+
- bin/console
|
|
62
|
+
- bin/ftg
|
|
63
|
+
- bin/setup
|
|
64
|
+
- config/private.json.example
|
|
65
|
+
- config/public.json
|
|
66
|
+
- ftg.gemspec
|
|
67
|
+
- lib/coffee.rb
|
|
68
|
+
- lib/colors.rb
|
|
69
|
+
- lib/ftg.rb
|
|
70
|
+
- lib/ftg_logger.rb
|
|
71
|
+
- lib/ftg_options.rb
|
|
72
|
+
- lib/ftg_stats.rb
|
|
73
|
+
- lib/ftg_sync.rb
|
|
74
|
+
- lib/idle_logger.rb
|
|
75
|
+
- lib/interactive.rb
|
|
76
|
+
- lib/migrations/create_tasks.rb
|
|
77
|
+
- lib/models/task.rb
|
|
78
|
+
- lib/task_formatter.rb
|
|
79
|
+
- lib/utils.rb
|
|
80
|
+
homepage: https://github.com/pinouchon/.ftg
|
|
81
|
+
licenses:
|
|
82
|
+
- MIT
|
|
83
|
+
metadata:
|
|
84
|
+
allowed_push_host: https://rubygems.org
|
|
85
|
+
post_install_message:
|
|
86
|
+
rdoc_options: []
|
|
87
|
+
require_paths:
|
|
88
|
+
- lib
|
|
89
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
90
|
+
requirements:
|
|
91
|
+
- - ">="
|
|
92
|
+
- !ruby/object:Gem::Version
|
|
93
|
+
version: '0'
|
|
94
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
95
|
+
requirements:
|
|
96
|
+
- - ">="
|
|
97
|
+
- !ruby/object:Gem::Version
|
|
98
|
+
version: '0'
|
|
99
|
+
requirements: []
|
|
100
|
+
rubyforge_project:
|
|
101
|
+
rubygems_version: 2.4.5.1
|
|
102
|
+
signing_key:
|
|
103
|
+
specification_version: 4
|
|
104
|
+
summary: Toggl replacement
|
|
105
|
+
test_files: []
|