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.
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
@@ -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
@@ -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
@@ -0,0 +1,5 @@
1
+ class Task < ActiveRecord::Base
2
+ def duration_formatted
3
+ Utils.format_time(duration)
4
+ end
5
+ end
@@ -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: []