achoo 0.3
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/CHANGES +46 -0
- data/COPYING +339 -0
- data/README.rdoc +100 -0
- data/Rakefile +64 -0
- data/bin/achoo +48 -0
- data/bin/awake +31 -0
- data/bin/ical +17 -0
- data/bin/vcs_commits +31 -0
- data/lib/achoo.rb +140 -0
- data/lib/achoo/awake.rb +130 -0
- data/lib/achoo/binary.rb +7 -0
- data/lib/achoo/binary/cstruct.rb +60 -0
- data/lib/achoo/binary/utmp_record.rb +59 -0
- data/lib/achoo/form.rb +18 -0
- data/lib/achoo/hour_administration_form.rb +131 -0
- data/lib/achoo/hour_registration_form.rb +227 -0
- data/lib/achoo/hour_registration_form_ranged.rb +45 -0
- data/lib/achoo/ical.rb +58 -0
- data/lib/achoo/lock_month_form.rb +40 -0
- data/lib/achoo/open_timespan.rb +13 -0
- data/lib/achoo/rc_loader.rb +55 -0
- data/lib/achoo/system.rb +6 -0
- data/lib/achoo/system/pm_suspend.rb +30 -0
- data/lib/achoo/system/wtmp.rb +18 -0
- data/lib/achoo/term.rb +76 -0
- data/lib/achoo/term/menu.rb +55 -0
- data/lib/achoo/term/table.rb +101 -0
- data/lib/achoo/timespan.rb +119 -0
- data/lib/achoo/ui.rb +10 -0
- data/lib/achoo/ui/commands.rb +51 -0
- data/lib/achoo/ui/common.rb +15 -0
- data/lib/achoo/ui/date_chooser.rb +75 -0
- data/lib/achoo/ui/date_choosers.rb +21 -0
- data/lib/achoo/ui/exception_handling.rb +20 -0
- data/lib/achoo/ui/month_chooser.rb +27 -0
- data/lib/achoo/ui/optionally_ranged_date_chooser.rb +33 -0
- data/lib/achoo/ui/register_hours.rb +137 -0
- data/lib/achoo/vcs.rb +43 -0
- data/lib/achoo/vcs/git.rb +20 -0
- data/lib/achoo/vcs/subversion.rb +32 -0
- metadata +116 -0
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'achoo/hour_registration_form'
|
2
|
+
|
3
|
+
class Achoo::HourRegistrationFormRanged < Achoo::HourRegistrationForm
|
4
|
+
|
5
|
+
def initialize(agent)
|
6
|
+
super
|
7
|
+
|
8
|
+
@page = @agent.get(atk_submit_to_url(@page.link_with(:text => 'Select range').href))
|
9
|
+
@form = @page.form('entryform')
|
10
|
+
end
|
11
|
+
|
12
|
+
def date=(date_range)
|
13
|
+
super(date_range[0])
|
14
|
+
|
15
|
+
to_day_field.value = date_range[1].strftime('%d')
|
16
|
+
to_month_field.value = date_range[1].strftime('%m')
|
17
|
+
to_year_field.value = date_range[1].year
|
18
|
+
end
|
19
|
+
|
20
|
+
def date
|
21
|
+
start = super
|
22
|
+
finish = Date.new(to_year_field.value.to_i, to_month_field.value.to_i,
|
23
|
+
to_day_field.value.to_i)
|
24
|
+
[start, finish]
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def to_day_field
|
30
|
+
@form.field_with(:name => 'todate[day]')
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_month_field
|
34
|
+
@form.field_with(:name => 'todate[month]')
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_year_field
|
38
|
+
@form.field_with(:name => 'todate[year]')
|
39
|
+
end
|
40
|
+
|
41
|
+
def date_to_s
|
42
|
+
date.map {|d| d.strftime("%Y-%m-%d")}.join(" -> ")
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
data/lib/achoo/ical.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'achoo/timespan'
|
2
|
+
require 'achoo/ui/exception_handling'
|
3
|
+
require 'net/https'
|
4
|
+
require 'ri_cal'
|
5
|
+
|
6
|
+
class Achoo; end
|
7
|
+
|
8
|
+
class Achoo::ICal
|
9
|
+
|
10
|
+
Achoo::UI::ExceptionHandling
|
11
|
+
|
12
|
+
def self.from_http_request(params)
|
13
|
+
http = Net::HTTP.new(params[:host], params[:port])
|
14
|
+
http.use_ssl = true
|
15
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
16
|
+
ics = http.start do |http|
|
17
|
+
request = Net::HTTP::Get.new(params[:path])
|
18
|
+
request.basic_auth(params[:user], params[:pass])
|
19
|
+
response = http.request(request)
|
20
|
+
response.body
|
21
|
+
end
|
22
|
+
self.new(ics)
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(ics_str)
|
26
|
+
@calendar = RiCal.parse_string(ics_str).first
|
27
|
+
end
|
28
|
+
|
29
|
+
def print_events(date, io=$stdout)
|
30
|
+
arg_start = date
|
31
|
+
arg_end = date + 1
|
32
|
+
|
33
|
+
@calendar.events.each do |e|
|
34
|
+
begin
|
35
|
+
if !e.x_properties['X-MICROSOFT-CDO-ALLDAYEVENT'].empty? && e.x_properties['X-MICROSOFT-CDO-ALLDAYEVENT'].first.value == 'TRUE'
|
36
|
+
# FIX handle this
|
37
|
+
elsif e.recurs?
|
38
|
+
e.occurrences({:overlapping => [arg_start, arg_end]}).each do |o|
|
39
|
+
print_event(o, io)
|
40
|
+
end
|
41
|
+
elsif e.dtstart >= arg_start && e.dtstart <= arg_end \
|
42
|
+
|| e.dtend >= arg_start && e.dtend <= arg_end
|
43
|
+
print_event(e, io)
|
44
|
+
end
|
45
|
+
rescue Exception => e
|
46
|
+
handle_exception("Failed to process calendar event", e)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def print_event(e, io)
|
54
|
+
dti = Achoo::Timespan.new(e.dtstart.to_s, e.dtend.to_s)
|
55
|
+
io.printf "%s: %s\n", dti, e.summary
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
@@ -0,0 +1,40 @@
|
|
1
|
+
class Achoo; end
|
2
|
+
|
3
|
+
class Achoo::LockMonthForm
|
4
|
+
|
5
|
+
def initialize(agent)
|
6
|
+
@agent = agent
|
7
|
+
end
|
8
|
+
|
9
|
+
def lock_month(period)
|
10
|
+
page = @agent.get(RC[:lock_months_url])
|
11
|
+
@form = page.form('entryform')
|
12
|
+
|
13
|
+
@form.period = period
|
14
|
+
unless user_select.nil?
|
15
|
+
user_select.options.each do |opt|
|
16
|
+
if opt.text.match(/\(#{RC[:user]}\)$/)
|
17
|
+
opt.select
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
def print_values
|
25
|
+
puts "Month: #{@form.period}"
|
26
|
+
puts " User: #{user_select.value}" unless user_select.nil?
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
def submit
|
31
|
+
@form.submit
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def user_select
|
37
|
+
@form.field_with(:name => 'userid')
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'achoo/term'
|
2
|
+
|
3
|
+
class Achoo; end
|
4
|
+
|
5
|
+
module Achoo::RCLoader
|
6
|
+
|
7
|
+
def load_rc(rc_file="#{ENV['HOME']}/.achoo")
|
8
|
+
#create_empty_rc_if_not_exists(rc_file)
|
9
|
+
file_permissions_secure?(rc_file)
|
10
|
+
|
11
|
+
load rc_file
|
12
|
+
|
13
|
+
verify_rc_contents(rc_file)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def file_permissions_secure?(rc_file)
|
19
|
+
# FIX test to is to strict
|
20
|
+
if File.stat(rc_file).mode != 0100600
|
21
|
+
puts Achoo::Term.fatal "Insecure permissions on #{rc_file}"
|
22
|
+
exit 1
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def create_empty_rc_if_not_exists(rc_file)
|
27
|
+
return if FileTest.exist?(rc_file)
|
28
|
+
|
29
|
+
FileUtils.touch(rc_file)
|
30
|
+
FileUtils.chmod(0600, rc_file)
|
31
|
+
end
|
32
|
+
|
33
|
+
def verify_rc_contents(rc_file)
|
34
|
+
unless Object.const_defined?('RC')
|
35
|
+
puts Achoo::Term.fatal "Malformed run control file: No RC constant defined"
|
36
|
+
exit 1
|
37
|
+
end
|
38
|
+
|
39
|
+
%w(url user password).each do |key|
|
40
|
+
unless RC.has_key?(key.to_sym)
|
41
|
+
puts Achoo::Term.fatal "Missing mandatory run control configuration variable: #{key}"
|
42
|
+
exit 1
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
%w(vcs_dirs ical).each do |key|
|
47
|
+
unless RC.has_key?(key.to_sym)
|
48
|
+
puts Achoo::Term.warn "Missing run control configuration variable: #{key}. " \
|
49
|
+
+ "Add it to #{rc_file} to get rid of this warning"
|
50
|
+
RC[key] = []
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
data/lib/achoo/system.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'achoo/system'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
class Achoo::System::PMSuspend < Array
|
5
|
+
|
6
|
+
class LogEntry
|
7
|
+
attr :time
|
8
|
+
attr :action
|
9
|
+
|
10
|
+
def initialize(time_str, action)
|
11
|
+
@action = action
|
12
|
+
@time = Time.parse(time_str)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(glob='/var/log/pm-suspend.log*')
|
17
|
+
super()
|
18
|
+
Dir.glob(glob).sort.reverse.each do |file|
|
19
|
+
next if file =~ /\.gz$/ # FIX uncompress?
|
20
|
+
File.open(file, 'r') do |fh|
|
21
|
+
fh.readlines.each do |l|
|
22
|
+
l.chop!
|
23
|
+
next unless l =~ /Awake|performing suspend/
|
24
|
+
self << LogEntry.new(*l.split(': '))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'achoo/binary'
|
2
|
+
require 'achoo/system'
|
3
|
+
|
4
|
+
class Achoo::System::Wtmp < Array
|
5
|
+
|
6
|
+
def initialize(glob='/var/log/wtmp*')
|
7
|
+
super()
|
8
|
+
chunk_size = Achoo::Binary::UTMPRecord.bin_size
|
9
|
+
Dir.glob(glob).sort.reverse.each do |file|
|
10
|
+
File.open(file, 'r') do |io|
|
11
|
+
while (bytes = io.read(chunk_size))
|
12
|
+
self << Achoo::Binary::UTMPRecord.new(bytes)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
data/lib/achoo/term.rb
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
class Achoo; class Term; end; end
|
4
|
+
|
5
|
+
Achoo::Term.autoload :Menu, 'achoo/term/menu'
|
6
|
+
Achoo::Term.autoload :Table, 'achoo/term/table'
|
7
|
+
|
8
|
+
class Achoo::Term
|
9
|
+
|
10
|
+
def self.bold(text); "\e[1m#{text}\e[0m"; end
|
11
|
+
|
12
|
+
def self.underline(text); "\e[4m#{text}\e[0m"; end
|
13
|
+
|
14
|
+
def self.warn(text); "\e[1;33m#{text}\e[0m"; end
|
15
|
+
|
16
|
+
def self.fatal(text); "\e[1;31m#{text}\e[0m"; end
|
17
|
+
|
18
|
+
def self.password
|
19
|
+
`stty -echo`
|
20
|
+
pas = ask('Password')
|
21
|
+
`stty echo`
|
22
|
+
pas
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.ask(question='')
|
26
|
+
answer = nil
|
27
|
+
loop do
|
28
|
+
print bold("#{question}> ")
|
29
|
+
$stdout.flush
|
30
|
+
answer = gets
|
31
|
+
|
32
|
+
# Answer is nil if user hits C-d on an empty input
|
33
|
+
if answer.nil?
|
34
|
+
puts
|
35
|
+
exit
|
36
|
+
end
|
37
|
+
|
38
|
+
answer.strip! unless answer.nil?
|
39
|
+
|
40
|
+
# FIX move this to achoo.rb?
|
41
|
+
unless $stdin.tty?
|
42
|
+
puts answer
|
43
|
+
end
|
44
|
+
break unless a_little_something(answer)
|
45
|
+
end
|
46
|
+
answer
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.choose(question, entries, special=nil, additional_valid_answers=[])
|
50
|
+
menu = Achoo::Term::Menu.new(question, entries, special, additional_valid_answers)
|
51
|
+
menu.print_ask_and_validate
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def self.a_little_something(answer)
|
57
|
+
return false if answer.nil?
|
58
|
+
|
59
|
+
case answer.downcase
|
60
|
+
when 'bless you!', 'gesundheit!'
|
61
|
+
puts "Thank you!"
|
62
|
+
return true
|
63
|
+
else
|
64
|
+
return false
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.shadowbox(text)
|
69
|
+
x = "┌──────────────────────────────────────────┐ \n"
|
70
|
+
x << "│ #{text.center(40)} " << "│▒\n"
|
71
|
+
x << "└──────────────────────────────────────────┘▒\n"
|
72
|
+
x << " ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒\n"
|
73
|
+
x
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'achoo/term'
|
2
|
+
|
3
|
+
class Achoo
|
4
|
+
class Term
|
5
|
+
class Menu
|
6
|
+
|
7
|
+
def initialize(question, entries, special=nil, additional_valid_answers=[])
|
8
|
+
@question = question
|
9
|
+
@entries = entries
|
10
|
+
@special = special
|
11
|
+
|
12
|
+
@valid = {}
|
13
|
+
@valid['0'] = true unless @special.nil?
|
14
|
+
1.upto(@entries.length).each {|i| @valid[i.to_s] = true}
|
15
|
+
additional_valid_answers.each {|a| @valid[a] = true}
|
16
|
+
end
|
17
|
+
|
18
|
+
def print_ask_and_validate()
|
19
|
+
return nil if @entries.empty?
|
20
|
+
|
21
|
+
print_menu()
|
22
|
+
return '1' if only_one_option?
|
23
|
+
|
24
|
+
loop do
|
25
|
+
answer = Achoo::Term.ask(@question)
|
26
|
+
if @valid[answer]
|
27
|
+
return answer
|
28
|
+
else
|
29
|
+
puts "Invalid value. Must be one of " << @valid.keys.sort.join(',')
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def print_menu
|
37
|
+
format = menu_item_format
|
38
|
+
@entries.each_with_index do |entry, i|
|
39
|
+
printf format, i+1, entry
|
40
|
+
end
|
41
|
+
printf format, 0, @special unless @special.nil?
|
42
|
+
end
|
43
|
+
|
44
|
+
def only_one_option?
|
45
|
+
@entries.length == 1 && @special.nil?
|
46
|
+
end
|
47
|
+
|
48
|
+
def menu_item_format
|
49
|
+
max_digits = Math.log10(@entries.length).floor + 1
|
50
|
+
" %#{max_digits}d. %s\n"
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
if RUBY_VERSION < "1.9"
|
4
|
+
$KCODE = 'u'
|
5
|
+
require 'jcode'
|
6
|
+
end
|
7
|
+
|
8
|
+
# Unicode box drawing characters
|
9
|
+
#
|
10
|
+
# http://en.wikipedia.org/wiki/Box-drawing_characters
|
11
|
+
#
|
12
|
+
#
|
13
|
+
# 0 1 2 3 4 5 6 7 8 9 A B C D E F
|
14
|
+
#
|
15
|
+
# 2500 ─ ━ │ ┃ ┄ ┅ ┆ ┇ ┈ ┉ ┊ ┋ ┌ ┍ ┎ ┏
|
16
|
+
#
|
17
|
+
# 2510 ┐ ┑ ┒ ┓ └ ┕ ┖ ┗ ┘ ┙ ┚ ┛ ├ ┝ ┞ ┟
|
18
|
+
#
|
19
|
+
# 2520 ┠ ┡ ┢ ┣ ┤ ┥ ┦ ┧ ┨ ┩ ┪ ┫ ┬ ┭ ┮ ┯
|
20
|
+
#
|
21
|
+
# 2530 ┰ ┱ ┲ ┳ ┴ ┵ ┶ ┷ ┸ ┹ ┺ ┻ ┼ ┽ ┾ ┿
|
22
|
+
#
|
23
|
+
# 2540 ╀ ╁ ╂ ╃ ╄ ╅ ╆ ╇ ╈ ╉ ╊ ╋ ╌ ╍ ╎ ╏
|
24
|
+
#
|
25
|
+
# 2550 ═ ║ ╒ ╓ ╔ ╕ ╖ ╗ ╘ ╙ ╚ ╛ ╜ ╝ ╞ ╟
|
26
|
+
#
|
27
|
+
# 2560 ╠ ╡ ╢ ╣ ╤ ╥ ╦ ╧ ╨ ╩ ╪ ╫ ╬ ╭ ╮ ╯
|
28
|
+
#
|
29
|
+
# 2570 ╰ ╱ ╲ ╳ ╴ ╵ ╶ ╷ ╸ ╹ ╺ ╻ ╼ ╽ ╾ ╿
|
30
|
+
|
31
|
+
class Achoo; class Term; end; end
|
32
|
+
|
33
|
+
class Achoo::Term::Table
|
34
|
+
|
35
|
+
def initialize(headers, data_rows, summaries=nil)
|
36
|
+
@headers = headers
|
37
|
+
@data_rows = data_rows
|
38
|
+
@summaries = summaries
|
39
|
+
end
|
40
|
+
|
41
|
+
def print(io=$stdout)
|
42
|
+
lengths = calculate_table_cell_widths
|
43
|
+
format = build_format(lengths)
|
44
|
+
headers = center_table_headers(lengths)
|
45
|
+
separator = lengths.map {|length| '─'*(length+2)}
|
46
|
+
|
47
|
+
|
48
|
+
io.print '┌' << separator.join('┬') << "┐\n"
|
49
|
+
io.print '│ ' << headers.join(' │ ') << " │\n"
|
50
|
+
io.print '├' << separator.join('┼') << "┤\n"
|
51
|
+
@data_rows.each {|r| io.printf format, *r }
|
52
|
+
unless @summaries.nil? || @data_rows.length == 1
|
53
|
+
io.print '├' << separator.join('┼') << "┤\n"
|
54
|
+
io.printf format, *@summaries
|
55
|
+
end
|
56
|
+
io.print '└' << separator.join('┴') << "┘\n"
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def center_table_headers(lengths)
|
62
|
+
headers = @headers.dup
|
63
|
+
lengths.each_with_index do |len,i|
|
64
|
+
headers[i] = headers[i].center(len)
|
65
|
+
end
|
66
|
+
headers
|
67
|
+
end
|
68
|
+
|
69
|
+
def calculate_table_cell_widths
|
70
|
+
lengths = []
|
71
|
+
@headers.each_with_index do |h, i|
|
72
|
+
lengths[i] = RUBY_VERSION < '1.9' ? h.jlength : h.length
|
73
|
+
end
|
74
|
+
@data_rows.each do |r|
|
75
|
+
r.each_with_index do |d, i|
|
76
|
+
len = RUBY_VERSION < '1.9' ? d.jlength : d.length
|
77
|
+
lengths[i] = [len, lengths[i]].max
|
78
|
+
end
|
79
|
+
end
|
80
|
+
lengths
|
81
|
+
end
|
82
|
+
|
83
|
+
def build_format(lengths)
|
84
|
+
is_column_left_justified = Array.new(lengths.count)
|
85
|
+
is_column_left_justified.fill(false)
|
86
|
+
|
87
|
+
@data_rows.each do |r|
|
88
|
+
r.each_index do |c|
|
89
|
+
if !r[c].strip.empty? && !r[c].match(/^\d+[:.,]?\d*$/)
|
90
|
+
is_column_left_justified[c] = true
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
lengths.reduce('│') do |f, l|
|
96
|
+
justify = is_column_left_justified.shift ? '-' : ''
|
97
|
+
f + " %#{justify}#{l}s │"
|
98
|
+
end + "\n"
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|