ifocustt 0.0.1a
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/LICENSE.txt +20 -0
- data/assets/example.png +0 -0
- data/bin/focus +8 -0
- data/bin/focus.sh +136 -0
- data/config/default.yml +13 -0
- data/lib/focus/action.rb +39 -0
- data/lib/focus/actions/change_blink_color.rb +15 -0
- data/lib/focus/actions/change_slack_do_not_disturb.rb +29 -0
- data/lib/focus/actions/change_slack_presence.rb +24 -0
- data/lib/focus/actions/log_focus_time.rb +40 -0
- data/lib/focus/actions/log_work_for_jira.rb +38 -0
- data/lib/focus/actions/run_shell_command.rb +7 -0
- data/lib/focus/actions/start_blink_server.rb +29 -0
- data/lib/focus/actions/start_focus_time.rb +136 -0
- data/lib/focus/actions/start_rescue_time.rb +23 -0
- data/lib/focus/actions/strobe_blink_color.rb +15 -0
- data/lib/focus/actions/toggle_mac_notification_center.rb +27 -0
- data/lib/focus/actions.rb +5 -0
- data/lib/focus/cli.rb +59 -0
- data/lib/focus/config.rb +65 -0
- data/lib/focus/config_loader.rb +29 -0
- data/lib/focus/formatter.rb +31 -0
- data/lib/focus/stdout.rb +26 -0
- data/lib/focus/string.rb +9 -0
- data/lib/focus/util.rb +5 -0
- data/lib/focus/utils/parse_jira_ticket_from_git_branch.rb +23 -0
- data/lib/focus/utils.rb +5 -0
- data/lib/focus/version.rb +3 -0
- data/lib/focus.rb +19 -0
- metadata +145 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: 853ef834580edb4e7919a959fe054a533b3d1e6b
|
|
4
|
+
data.tar.gz: 42ed12e13f63862715ab0df293659d1bd3ffe7a2
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: cacb2b8a84e8e0fd33a0c9e44bcb69789ad5f2dbcbe5d4517419d37f7cb3e174dd55a434b226f70b2f72f29bdee2899a9a48261548d0072bf734eecde457391e
|
|
7
|
+
data.tar.gz: 522c829bd63e1162f07147eeafcb9097dc7aae06c4305181b6a882c82ff1c6b746a777deaa15eefd70b11f6922c97d1c7704fa2b603b037e2530711a853bd7d8
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright (c) 2017 John Mason
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/assets/example.png
ADDED
|
Binary file
|
data/bin/focus
ADDED
data/bin/focus.sh
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
#
|
|
3
|
+
# USAGE:
|
|
4
|
+
#
|
|
5
|
+
# focus [minutes=25] [target=?|JIRA ticket in git branch]
|
|
6
|
+
|
|
7
|
+
git_branch_name() { git rev-parse --abbrev-ref HEAD 2>/dev/null; }
|
|
8
|
+
jira_ticket() {
|
|
9
|
+
local branch=$(git_branch_name)
|
|
10
|
+
local ticket=$(echo $branch | sed 's/.*\([a-zA-Z]\{2\}-[0-9]\{2\}\)/\1/') # TODO
|
|
11
|
+
if [ "$ticket" == "$branch" ]
|
|
12
|
+
then
|
|
13
|
+
echo "?"
|
|
14
|
+
else
|
|
15
|
+
echo "$ticket"
|
|
16
|
+
fi
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
FOCUS_MINUTES="${1-25}"
|
|
20
|
+
FOCUS_TARGET="${2-$(jira_ticket)}"
|
|
21
|
+
FOCUS_HISTORY_FILE="$HOME/.focus_history"
|
|
22
|
+
|
|
23
|
+
info() { printf -- "\033[00;34m..\033[0m %s " "$*"; }
|
|
24
|
+
ok() { echo "\033[00;32m✓\033[0m"; }
|
|
25
|
+
abort() { echo "!!! $*" >&2; exit 1; }
|
|
26
|
+
epoch() { date +%s; }
|
|
27
|
+
print_line() { printf "%-15s %-15s %-15s\n" "$1" "$2" "$3"; }
|
|
28
|
+
title() { print_line "EPOCH" "FOCUS_TIME" "TARGET"; }
|
|
29
|
+
event() { print_line "$(epoch)" "$FOCUS_MINUTES" "$@"; }
|
|
30
|
+
add_to_history() { "$@" >> "$FOCUS_HISTORY_FILE"; }
|
|
31
|
+
|
|
32
|
+
log_focus() {
|
|
33
|
+
info "Updating ~/.focus_history"
|
|
34
|
+
if [ ! -f "$FOCUS_HISTORY_FILE" ]; then add_to_history title; fi
|
|
35
|
+
add_to_history event "$FOCUS_TARGET"
|
|
36
|
+
ok
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
quietly() {
|
|
40
|
+
"$@" &>/dev/null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
SLACK_TOKEN=$(cat ~/.secrets/slack-token 2>/dev/null)
|
|
44
|
+
SLACK_AWAY="away"
|
|
45
|
+
SLACK_AVAILABLE="auto"
|
|
46
|
+
|
|
47
|
+
change_slack_presence() {
|
|
48
|
+
if [ -n "$SLACK_TOKEN" ]
|
|
49
|
+
then
|
|
50
|
+
local presence="$1"
|
|
51
|
+
info "Changing slack presence to $presence"
|
|
52
|
+
local setpresenceurl="https://slack.com/api/users.setPresence?token=$SLACK_TOKEN&presence=$presence&pretty=1"
|
|
53
|
+
curl -X POST "$setpresenceurl" &>/dev/null && ok
|
|
54
|
+
fi
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
BLINK_PORT="8754"
|
|
58
|
+
BLINK_SERVER="http://localhost:$BLINK_PORT/blink1"
|
|
59
|
+
BLINK_RED="23EA5B5B"
|
|
60
|
+
BLINK_GREEN="233AF23A"
|
|
61
|
+
BLINK_ORANGE="23FFBF00"
|
|
62
|
+
start_blink_server() {
|
|
63
|
+
if ! command -v blink1-server &> /dev/null
|
|
64
|
+
then
|
|
65
|
+
abort 'Blink server not installed. Run `npm install -g node-blink1-server`.'
|
|
66
|
+
else
|
|
67
|
+
quietly curl "$BLINK_SERVER" || blink1-server "$BLINK_PORT" &
|
|
68
|
+
fi
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
change_blink_color() {
|
|
72
|
+
local color="$1"
|
|
73
|
+
info "Changing blink to $color"
|
|
74
|
+
curl "$BLINK_SERVER/fadeToRGB?rgb=%$color" &>/dev/null && ok
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
strobe_blink_color() {
|
|
78
|
+
local color="$1"
|
|
79
|
+
local seconds="$2"
|
|
80
|
+
local blink_rate="0.5" # both on and off together finishes in one second
|
|
81
|
+
curl "$BLINK_SERVER/blink?rgb=%$color&time=$blink_rate&repeats=$seconds" &>/dev/null && ok
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
IFTTT_MAKER_KEY=$(cat ~/.secrets/ifttt-maker 2>/dev/null)
|
|
85
|
+
start_rescue_time() {
|
|
86
|
+
if [ -n "$IFTTT_MAKER_KEY" ]
|
|
87
|
+
then
|
|
88
|
+
local event="rescue_time_focus_start"
|
|
89
|
+
info "Firing $event blink(1) event to IFTT"
|
|
90
|
+
curl -X POST https://maker.ifttt.com/trigger/${event}/with/key/${IFTTT_MAKER_KEY} &>/dev/null && ok
|
|
91
|
+
fi
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
start_slack_do_not_disturb() {
|
|
95
|
+
if [ -n "$SLACK_TOKEN" ]
|
|
96
|
+
then
|
|
97
|
+
local url="https://slack.com/api/dnd.setSnooze?token=$SLACK_TOKEN&num_minutes=$FOCUS_MINUTES&pretty=1"
|
|
98
|
+
info "Changing slack to do not disturb for $FOCUS_MINUTES minutes"
|
|
99
|
+
curl -X POST "$url" &>/dev/null && ok
|
|
100
|
+
fi
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
disable_mac_notification_center() {
|
|
104
|
+
if [ "$(uname)" == "Darwin" ]
|
|
105
|
+
then
|
|
106
|
+
info "Disabling Mac OSX notification center"
|
|
107
|
+
quietly launchctl unload -w /System/Library/LaunchAgents/com.apple.notificationcenterui.plist
|
|
108
|
+
ok
|
|
109
|
+
fi
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
enable_mac_notification_center() {
|
|
113
|
+
if [ "$(uname)" == "Darwin" ]
|
|
114
|
+
then
|
|
115
|
+
launchctl load -w /System/Library/LaunchAgents/com.apple.notificationcenterui.plist
|
|
116
|
+
fi
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
cleanup(){
|
|
120
|
+
local focus_seconds=$(echo "$FOCUS_MINUTES * 60" | bc)
|
|
121
|
+
local break_seconds=$(echo "$focus_seconds * .2" | bc)
|
|
122
|
+
sleep "$focus_seconds" && quietly strobe_blink_color "$BLINK_ORANGE" "$break_seconds"
|
|
123
|
+
sleep "$break_seconds" && \
|
|
124
|
+
quietly change_slack_presence "$SLACK_AVAILABLE"
|
|
125
|
+
quietly enable_mac_notification_center
|
|
126
|
+
quietly change_blink_color "$BLINK_GREEN"
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
disable_mac_notification_center
|
|
130
|
+
start_rescue_time
|
|
131
|
+
start_blink_server
|
|
132
|
+
change_slack_presence "$SLACK_AWAY"
|
|
133
|
+
start_slack_do_not_disturb
|
|
134
|
+
log_focus
|
|
135
|
+
change_blink_color "$BLINK_RED"
|
|
136
|
+
cleanup &
|
data/config/default.yml
ADDED
data/lib/focus/action.rb
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
require "httparty"
|
|
2
|
+
require "interactor"
|
|
3
|
+
|
|
4
|
+
module Focus
|
|
5
|
+
class Action
|
|
6
|
+
include Interactor
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def focus_target
|
|
11
|
+
context.focus_target || context.target || "?"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def focus_minutes
|
|
15
|
+
context.focus_minutes || context.minutes.to_i
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def focus_seconds
|
|
19
|
+
context.minutes.to_f * 60
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def break_seconds
|
|
23
|
+
focus_seconds * 0.2
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def evaluate_step(klass, args)
|
|
27
|
+
action = klass.to_s.gsub(/^.*::/, "")
|
|
28
|
+
step = "Running #{action}..."
|
|
29
|
+
|
|
30
|
+
Focus::STDOUT.step(step, quiet: context.quiet) do
|
|
31
|
+
klass.call(args)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def debug_output(*args)
|
|
36
|
+
Focus::STDOUT.debug_output args
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Focus
|
|
2
|
+
class ChangeBlinkColor < Action
|
|
3
|
+
def call
|
|
4
|
+
context.fail! unless perform
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def perform
|
|
10
|
+
url = "#{Config.blink_server}/fadeToRGB?rgb=%#{context.color}"
|
|
11
|
+
res = HTTParty.get(url)
|
|
12
|
+
res.code == 200
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Focus
|
|
2
|
+
class ChangeSlackDoNotDisturb < Action
|
|
3
|
+
def call
|
|
4
|
+
context.fail! unless perform
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def perform
|
|
10
|
+
Config.slack_token ? HTTParty.post(url) : true
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def url
|
|
14
|
+
if context.enabled
|
|
15
|
+
"#{base_url}&num_minutes=#{focus_minutes}"
|
|
16
|
+
else
|
|
17
|
+
base_url
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def base_url
|
|
22
|
+
"#{Config.slack_api_url}/#{action}?token=#{Config.slack_token}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def action
|
|
26
|
+
context.enabled ? "dnd.setSnooze" : "dnd.endSnooze"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Focus
|
|
2
|
+
class ChangeSlackPresence < Action
|
|
3
|
+
def call
|
|
4
|
+
context.fail! unless perform
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def perform
|
|
10
|
+
token = Config.slack_token
|
|
11
|
+
if token
|
|
12
|
+
url = "#{Config.slack_api_url}/users.setPresence?token=#{token}&presence=#{presence}"
|
|
13
|
+
res = HTTParty.post url
|
|
14
|
+
res.code == 200
|
|
15
|
+
else
|
|
16
|
+
true
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def presence
|
|
21
|
+
context.presence
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
module Focus
|
|
3
|
+
class LogFocusTime < Action
|
|
4
|
+
def call
|
|
5
|
+
update_log
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def update_log
|
|
11
|
+
file = File.expand_path Config.focus_history_file
|
|
12
|
+
new_file = File.exist?(file)
|
|
13
|
+
|
|
14
|
+
File.open(file, "a") do |fo|
|
|
15
|
+
fo.puts title unless new_file
|
|
16
|
+
fo.puts entry(Time.now.to_i, focus_minutes, focus_target)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def entry(epoch, min, target)
|
|
21
|
+
line % [epoch, min, target]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def line
|
|
25
|
+
"%-20s %-20s %s"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def title
|
|
29
|
+
entry "EPOCH", "FOCUS_MINUTES", "TARGET"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def focus_target
|
|
33
|
+
jira_ticket || super
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def jira_ticket
|
|
37
|
+
@ticket ||= Utils::ParseJiraTicketFromGitBranch.call.jira_ticket
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module Focus
|
|
2
|
+
class PostWorkLogToJira < Action
|
|
3
|
+
def call
|
|
4
|
+
return unless jira_ticket && focus_seconds >= 60
|
|
5
|
+
HTTParty.post(issue_url, options)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def issue_url
|
|
11
|
+
"#{Config.jira_url}/issue/#{jira_ticket}/worklog"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def options
|
|
15
|
+
{
|
|
16
|
+
body: body.to_json,
|
|
17
|
+
basic_auth: auth,
|
|
18
|
+
headers: headers
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def body
|
|
23
|
+
{ timeSpentSeconds: focus_seconds, comment: context.comment.to_s }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def auth
|
|
27
|
+
{ username: Config.jira_username, password: Config.jira_password }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def headers
|
|
31
|
+
{ "Content-Type" => "application/json" }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def jira_ticket
|
|
35
|
+
context.jira_ticket ||= Utils::ParseJiraTicketFromGitBranch.call.jira_ticket
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Focus
|
|
2
|
+
class StartBlinkServer < Action
|
|
3
|
+
def call
|
|
4
|
+
verify_blink_server_installed
|
|
5
|
+
HTTParty.get(Config.blink_server)
|
|
6
|
+
rescue Errno::ECONNREFUSED
|
|
7
|
+
start_blink_server
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def verify_blink_server_installed
|
|
13
|
+
error_msg = \
|
|
14
|
+
"Blink server not installed. Run `npm install -g node-blink1-server`."
|
|
15
|
+
|
|
16
|
+
raise ArgumentError, error_msg unless blink_server_installed?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def blink_server_installed?
|
|
20
|
+
system("command -v blink1-server &>/dev/null")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def start_blink_server
|
|
24
|
+
fork do
|
|
25
|
+
system("blink1-server #{Config.blink_port} &>/dev/null")
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
require "yaml"
|
|
2
|
+
require "tty"
|
|
3
|
+
|
|
4
|
+
module Focus
|
|
5
|
+
class StartFocusTime < Action
|
|
6
|
+
DEFAULT_CONTEXT_KEYS = %i(minutes target quiet daemonize).freeze
|
|
7
|
+
|
|
8
|
+
attr_reader :action
|
|
9
|
+
|
|
10
|
+
def call
|
|
11
|
+
context.actions = ConfigLoader.load("actions")
|
|
12
|
+
focus
|
|
13
|
+
take_break
|
|
14
|
+
cleanup
|
|
15
|
+
happy_message
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def focus
|
|
21
|
+
@action = :focus
|
|
22
|
+
|
|
23
|
+
if context.daemonize
|
|
24
|
+
fork { perform_actions "OnFocus" }
|
|
25
|
+
else
|
|
26
|
+
perform_actions "OnFocus"
|
|
27
|
+
handle_progress_bar
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def take_break
|
|
32
|
+
@action = :break
|
|
33
|
+
if context.daemonize
|
|
34
|
+
fork { perform_actions "OnBreak" }
|
|
35
|
+
else
|
|
36
|
+
perform_actions "OnBreak"
|
|
37
|
+
handle_progress_bar
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def cleanup
|
|
42
|
+
if context.daemonize
|
|
43
|
+
fork { perform_actions "OnCompletion" }
|
|
44
|
+
else
|
|
45
|
+
perform_actions "OnCompletion"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def progress_bar
|
|
50
|
+
bar_type = @bar ? @bar.format.split.first : :not_defined
|
|
51
|
+
return @bar if bar_type == action.to_s
|
|
52
|
+
|
|
53
|
+
@bar = build_progress_bar
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def build_progress_bar
|
|
57
|
+
TTY::ProgressBar.new "#{action} [:bar] :elapsed" do |config|
|
|
58
|
+
config.total = seconds_for_action
|
|
59
|
+
config.interval = 1
|
|
60
|
+
config.width = 40
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def seconds_for_action
|
|
65
|
+
case action
|
|
66
|
+
when :focus then focus_seconds
|
|
67
|
+
when :break then break_seconds
|
|
68
|
+
else
|
|
69
|
+
raise "Unknown action: '#{action}'"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def every_second
|
|
74
|
+
end_time = Time.now + seconds_for_action
|
|
75
|
+
|
|
76
|
+
while Time.now < end_time
|
|
77
|
+
timestamp = Time.now
|
|
78
|
+
yield
|
|
79
|
+
interval = 1 - (Time.now - timestamp)
|
|
80
|
+
sleep(interval) if interval.positive?
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def handle_progress_bar
|
|
85
|
+
every_second { progress_bar.advance unless context.daemonize }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def perform_actions(event)
|
|
89
|
+
actions = context.actions.shift[event]
|
|
90
|
+
return unless actions
|
|
91
|
+
Focus::STDOUT.print_line "Starting #{event}...\r", quiet: context.quiet
|
|
92
|
+
|
|
93
|
+
actions.each do |hsh|
|
|
94
|
+
hsh.each do |action, keyword_arguments|
|
|
95
|
+
klass = constantize(action)
|
|
96
|
+
args = downcase(keyword_arguments)
|
|
97
|
+
|
|
98
|
+
evaluate_step klass, with_default_context(args)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def with_default_context(args)
|
|
104
|
+
hsh = args.to_h
|
|
105
|
+
hsh.merge default_hsh
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def default_hsh
|
|
109
|
+
slice_hash context.to_h, DEFAULT_CONTEXT_KEYS
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def slice_hash(hsh, *args)
|
|
113
|
+
keys = args.flatten
|
|
114
|
+
hsh.select { |k, _v| keys.include? k }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def downcase(thing)
|
|
118
|
+
return unless thing
|
|
119
|
+
return thing.underscore if thing.respond_to? :underscore
|
|
120
|
+
|
|
121
|
+
thing.each_with_object({}) do |(key, value), obj|
|
|
122
|
+
obj[key.underscore] = value
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def happy_message
|
|
127
|
+
return if context.quiet
|
|
128
|
+
Focus::STDOUT.puts_line nil
|
|
129
|
+
Focus::STDOUT.puts_line "Complete!"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def constantize(str)
|
|
133
|
+
Object.const_get "Focus::#{str}"
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Focus
|
|
2
|
+
class StartRescueTime < Action
|
|
3
|
+
def call
|
|
4
|
+
context.fail! unless perform
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def perform
|
|
10
|
+
token = Config.ifttt_maker_key
|
|
11
|
+
if token
|
|
12
|
+
url = "https://maker.ifttt.com/trigger/#{event}/with/key/#{token}"
|
|
13
|
+
HTTParty.post url
|
|
14
|
+
else
|
|
15
|
+
true
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def event
|
|
20
|
+
"rescue_time_focus_start"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Focus
|
|
2
|
+
class StrobeBlinkColor < Action
|
|
3
|
+
def call
|
|
4
|
+
context.fail! unless perform
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def perform
|
|
10
|
+
url = "#{Config.blink_server}/pattern?rgb=%#{context.color}&time=1.5&repeats=0"
|
|
11
|
+
res = HTTParty.get(url)
|
|
12
|
+
res.code == 200
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module Focus
|
|
2
|
+
class ToggleMacNotificationCenter < Action
|
|
3
|
+
def call
|
|
4
|
+
toggle if uname == "Darwin"
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def uname
|
|
10
|
+
`uname`.chomp
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def toggle
|
|
14
|
+
`launchctl #{setting} -w /System/Library/LaunchAgents/com.apple.notificationcenterui.plist &>/dev/null`
|
|
15
|
+
|
|
16
|
+
if context.enabled?
|
|
17
|
+
`killall NotificationCenter &>/dev/null`
|
|
18
|
+
else
|
|
19
|
+
`open -a NotificationCenter &>/dev/null`
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def setting
|
|
24
|
+
context.enabled ? "load" : "unload"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/focus/cli.rb
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
require "optparse"
|
|
2
|
+
require "ostruct"
|
|
3
|
+
require "pp"
|
|
4
|
+
|
|
5
|
+
module Focus
|
|
6
|
+
class Parser
|
|
7
|
+
DEFAULT_VALUES = {
|
|
8
|
+
minutes: 25,
|
|
9
|
+
quiet: true
|
|
10
|
+
}.freeze
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
def parse(options)
|
|
14
|
+
args = OpenStruct.new(DEFAULT_VALUES)
|
|
15
|
+
parser = build_parser(args)
|
|
16
|
+
parser.parse!(options)
|
|
17
|
+
args
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def build_parser(args) # rubocop:disable MethodLength
|
|
23
|
+
OptionParser.new do |opts|
|
|
24
|
+
opts.banner = "Usage: example.rb [options]"
|
|
25
|
+
|
|
26
|
+
opts.on("-v", "--verbose", "Run focus with more verbose STDOUT") do
|
|
27
|
+
args.quiet = false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
opts.on("-d", "--daemonize", "Allow focus to be forked to the background") do
|
|
31
|
+
args.daemonize = true
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
opts.on("-tTIME", "--time=TIME", "Alias to minutes") do |t|
|
|
35
|
+
args.minutes = t
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
opts.on("-mMINUTES", "--minutes=MINUTES", "How many minutes to focus.") do |m|
|
|
39
|
+
args.minutes = m
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
opts.on("-h", "--help", "Prints this help") do
|
|
43
|
+
puts opts
|
|
44
|
+
exit
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
class CLI
|
|
52
|
+
class << self
|
|
53
|
+
def run!(argv)
|
|
54
|
+
options = Parser.parse(argv)
|
|
55
|
+
StartFocusTime.call(options.to_h)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
data/lib/focus/config.rb
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
require "dotenv"
|
|
2
|
+
|
|
3
|
+
module Focus
|
|
4
|
+
class Config < OpenStruct
|
|
5
|
+
class << self
|
|
6
|
+
def method_missing(m, *args, &block) # rubocop:disable MethodMissing
|
|
7
|
+
ENV[m.to_s.upcase] || config.send(m, *args, &block)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def config
|
|
11
|
+
@config ||= new
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize
|
|
16
|
+
source_env
|
|
17
|
+
super(configurations)
|
|
18
|
+
ingest _hardcoded
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def configurations
|
|
24
|
+
defaults.merge(custom_config)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def ingest(hsh)
|
|
28
|
+
hsh.each do |key, value|
|
|
29
|
+
send("#{key}=", value)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def custom_config
|
|
34
|
+
@custom_config ||= Focus::ConfigLoader.load("config")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def defaults
|
|
38
|
+
{
|
|
39
|
+
"blink_port" => 8754,
|
|
40
|
+
"focus_history_file" => "#{ENV['HOME']}/.focus_history",
|
|
41
|
+
"focus_minutes" => 25,
|
|
42
|
+
"slack_api_url" => "https://slack.com/api"
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def _hardcoded
|
|
47
|
+
raise "`blink_port` was not defined" unless blink_port
|
|
48
|
+
|
|
49
|
+
{
|
|
50
|
+
"blink_server" => "http://localhost:#{blink_port}/blink1",
|
|
51
|
+
"slack_available" => "auto",
|
|
52
|
+
"slack_away" => "away"
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def source_env
|
|
57
|
+
return unless custom_config.respond_to? :keys
|
|
58
|
+
relative_path = custom_config["env_file"] || ".env"
|
|
59
|
+
path = File.expand_path relative_path
|
|
60
|
+
|
|
61
|
+
return unless File.exist?(path)
|
|
62
|
+
Dotenv.load path
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Focus
|
|
2
|
+
class ConfigLoader
|
|
3
|
+
class << self
|
|
4
|
+
def load(config)
|
|
5
|
+
YAML.load_file(config_file)[config]
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def config_file
|
|
11
|
+
opts = {
|
|
12
|
+
project_file: "./.focus.yml",
|
|
13
|
+
focus_repo_file: "#{Focus.root}/.focus.yml",
|
|
14
|
+
home_file: "#{ENV['HOME']}/.focus.yml",
|
|
15
|
+
default_file: "#{Focus.root}/config/default.yml"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
opts.each do |_k, file|
|
|
19
|
+
break file if exist?(file)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def exist?(path)
|
|
24
|
+
full_path = File.expand_path(path)
|
|
25
|
+
File.exist?(full_path) || File.symlink?(full_path)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
require "paint"
|
|
2
|
+
|
|
3
|
+
module Focus
|
|
4
|
+
module Formatter
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def step(step)
|
|
8
|
+
blue("==>") + " #{step}"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def error(error)
|
|
12
|
+
red("!!! Error:") + " #{error}"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def blue(string)
|
|
16
|
+
Paint[string, :blue]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def red(string)
|
|
20
|
+
Paint[string, :red, :bright]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def ok
|
|
24
|
+
" " + Paint["OK", :green]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def pastel
|
|
28
|
+
@pastel ||= Pastel.new
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
data/lib/focus/stdout.rb
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Focus
|
|
2
|
+
class STDOUT
|
|
3
|
+
class << self
|
|
4
|
+
def puts_line(str, opts = { quiet: false })
|
|
5
|
+
quiet = opts[:quiet]
|
|
6
|
+
::STDOUT.puts str unless quiet
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def print_line(str, opts = { quiet: false })
|
|
10
|
+
quiet = opts[:quiet]
|
|
11
|
+
::STDOUT.print str unless quiet
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def step(string, opts = { quiet: false })
|
|
15
|
+
quiet = opts[:quiet]
|
|
16
|
+
print_line(Focus::Formatter.step(string), quiet: quiet)
|
|
17
|
+
yield
|
|
18
|
+
puts_line Focus::Formatter.ok, quiet: quiet
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def debug_output(str)
|
|
22
|
+
::STDOUT.puts str if $DEBUG
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
data/lib/focus/string.rb
ADDED
data/lib/focus/util.rb
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Focus
|
|
2
|
+
module Utils
|
|
3
|
+
class ParseJiraTicketFromGitBranch < Util
|
|
4
|
+
JIRA_TICKET_RGX = /[a-z]+-\d+/i
|
|
5
|
+
|
|
6
|
+
def call
|
|
7
|
+
context.jira_ticket = jira_ticket
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def jira_ticket
|
|
13
|
+
git_branch = parse_git_branch
|
|
14
|
+
git_branch.scan(JIRA_TICKET_RGX).first
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def parse_git_branch
|
|
18
|
+
branch = `git rev-parse --abbrev-ref HEAD 2>/dev/null`.chomp
|
|
19
|
+
branch.empty? ? nil : branch
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
data/lib/focus/utils.rb
ADDED
data/lib/focus.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require "focus/action"
|
|
2
|
+
require "focus/actions"
|
|
3
|
+
require "focus/cli"
|
|
4
|
+
require "focus/config"
|
|
5
|
+
require "focus/config_loader"
|
|
6
|
+
require "focus/formatter"
|
|
7
|
+
require "focus/stdout"
|
|
8
|
+
require "focus/string"
|
|
9
|
+
require "focus/util"
|
|
10
|
+
require "focus/utils"
|
|
11
|
+
require "focus/version"
|
|
12
|
+
|
|
13
|
+
module Focus
|
|
14
|
+
class << self
|
|
15
|
+
def root
|
|
16
|
+
File.expand_path("../..", __FILE__)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ifocustt
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1a
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- John Mason
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2017-03-15 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: httparty
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0.14'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0.14'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: tty
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: 0.6.1
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: 0.6.1
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: paint
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '2.0'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '2.0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: dotenv
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '2.2'
|
|
62
|
+
type: :runtime
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '2.2'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: interactor
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '3.0'
|
|
76
|
+
type: :runtime
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '3.0'
|
|
83
|
+
description: |2
|
|
84
|
+
If Focus, Then That.
|
|
85
|
+
Do stuff when you need to focus.
|
|
86
|
+
email: binarymason@users.noreply.github.com
|
|
87
|
+
executables:
|
|
88
|
+
- focus
|
|
89
|
+
extensions: []
|
|
90
|
+
extra_rdoc_files: []
|
|
91
|
+
files:
|
|
92
|
+
- LICENSE.txt
|
|
93
|
+
- assets/example.png
|
|
94
|
+
- bin/focus
|
|
95
|
+
- bin/focus.sh
|
|
96
|
+
- config/default.yml
|
|
97
|
+
- lib/focus.rb
|
|
98
|
+
- lib/focus/action.rb
|
|
99
|
+
- lib/focus/actions.rb
|
|
100
|
+
- lib/focus/actions/change_blink_color.rb
|
|
101
|
+
- lib/focus/actions/change_slack_do_not_disturb.rb
|
|
102
|
+
- lib/focus/actions/change_slack_presence.rb
|
|
103
|
+
- lib/focus/actions/log_focus_time.rb
|
|
104
|
+
- lib/focus/actions/log_work_for_jira.rb
|
|
105
|
+
- lib/focus/actions/run_shell_command.rb
|
|
106
|
+
- lib/focus/actions/start_blink_server.rb
|
|
107
|
+
- lib/focus/actions/start_focus_time.rb
|
|
108
|
+
- lib/focus/actions/start_rescue_time.rb
|
|
109
|
+
- lib/focus/actions/strobe_blink_color.rb
|
|
110
|
+
- lib/focus/actions/toggle_mac_notification_center.rb
|
|
111
|
+
- lib/focus/cli.rb
|
|
112
|
+
- lib/focus/config.rb
|
|
113
|
+
- lib/focus/config_loader.rb
|
|
114
|
+
- lib/focus/formatter.rb
|
|
115
|
+
- lib/focus/stdout.rb
|
|
116
|
+
- lib/focus/string.rb
|
|
117
|
+
- lib/focus/util.rb
|
|
118
|
+
- lib/focus/utils.rb
|
|
119
|
+
- lib/focus/utils/parse_jira_ticket_from_git_branch.rb
|
|
120
|
+
- lib/focus/version.rb
|
|
121
|
+
homepage: http://github.com/binarymason/ifocustt
|
|
122
|
+
licenses:
|
|
123
|
+
- MIT
|
|
124
|
+
metadata: {}
|
|
125
|
+
post_install_message:
|
|
126
|
+
rdoc_options: []
|
|
127
|
+
require_paths:
|
|
128
|
+
- lib
|
|
129
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
130
|
+
requirements:
|
|
131
|
+
- - ">="
|
|
132
|
+
- !ruby/object:Gem::Version
|
|
133
|
+
version: 2.0.0
|
|
134
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
135
|
+
requirements:
|
|
136
|
+
- - ">"
|
|
137
|
+
- !ruby/object:Gem::Version
|
|
138
|
+
version: 1.3.1
|
|
139
|
+
requirements: []
|
|
140
|
+
rubyforge_project:
|
|
141
|
+
rubygems_version: 2.6.10
|
|
142
|
+
signing_key:
|
|
143
|
+
specification_version: 4
|
|
144
|
+
summary: If Focus, Then That
|
|
145
|
+
test_files: []
|