volay 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +7 -0
- data/.rubocop.yml +10 -0
- data/.travis.yml +11 -0
- data/Gemfile +4 -0
- data/LICENSE +674 -0
- data/MANIFEST +38 -0
- data/README.md +52 -0
- data/Rakefile +9 -0
- data/bin/volay +34 -0
- data/glade/icons/volume-high.png +0 -0
- data/glade/icons/volume-low.png +0 -0
- data/glade/icons/volume-medium.png +0 -0
- data/glade/icons/volume-muted.png +0 -0
- data/glade/volay.glade +99 -0
- data/lib/volay/app.rb +62 -0
- data/lib/volay/cli.rb +44 -0
- data/lib/volay/config.rb +91 -0
- data/lib/volay/exceptions.rb +6 -0
- data/lib/volay/mixer/alsa.rb +48 -0
- data/lib/volay/mixer/default.rb +72 -0
- data/lib/volay/utils.rb +40 -0
- data/lib/volay/version.rb +4 -0
- data/lib/volay/volay.rb +11 -0
- data/lib/volay/widget/events.rb +22 -0
- data/lib/volay/widget/system_tray.rb +91 -0
- data/lib/volay/widget/volume_control.rb +37 -0
- data/lib/volay.rb +20 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/volay/app_spec.rb +56 -0
- data/spec/volay/config_spec.rb +64 -0
- data/spec/volay/mixer/alsa_spec.rb +56 -0
- data/spec/volay/mixer/default_spec.rb +44 -0
- data/spec/volay/utils_spec.rb +52 -0
- data/spec/volay/widget/volume_control_spec.rb +27 -0
- data/task/manifest.rake +9 -0
- data/task/rubocop.rake +5 -0
- data/task/spec.rake +6 -0
- data/volay.gemspec +27 -0
- metadata +208 -0
@@ -0,0 +1,91 @@
|
|
1
|
+
# Volay module
|
2
|
+
module Volay
|
3
|
+
# Widgets components
|
4
|
+
module Widget
|
5
|
+
# Events class
|
6
|
+
class SystemTray < Events
|
7
|
+
LEFT_CLICK = 1
|
8
|
+
RIGHT_CLICK = 3
|
9
|
+
|
10
|
+
##
|
11
|
+
# When left click on the status icon, popup the window menu
|
12
|
+
#
|
13
|
+
# @param [Gtk::Widget] widget Widget
|
14
|
+
# @param [Gtk::Event] event Event
|
15
|
+
#
|
16
|
+
def on_status_icon_button_press_event(_widget, event)
|
17
|
+
return unless event.is_a?(Gdk::EventButton) &&
|
18
|
+
event.button == LEFT_CLICK
|
19
|
+
window = @app.get_object('system_tray_window')
|
20
|
+
posx, posy = get_position(window)
|
21
|
+
window.move(posx, posy)
|
22
|
+
@app.get_object('system_tray_window').show_all
|
23
|
+
end
|
24
|
+
|
25
|
+
##
|
26
|
+
# When click outside the window
|
27
|
+
#
|
28
|
+
def on_system_tray_window_focus_out_event
|
29
|
+
@app.get_object('system_tray_window').hide
|
30
|
+
end
|
31
|
+
|
32
|
+
##
|
33
|
+
# When right click on the status icon
|
34
|
+
#
|
35
|
+
# @param [Gtk::Widget] widget Widget
|
36
|
+
# @param [Gtk::Event] event Event
|
37
|
+
# @param [Integer] time Time
|
38
|
+
def on_status_icon_popup_menu(_widget, _event, time)
|
39
|
+
popup_menu = @app.get_object('popup_menu')
|
40
|
+
popup_menu.show_all
|
41
|
+
popup_menu.popup(nil, nil, 0, time)
|
42
|
+
end
|
43
|
+
|
44
|
+
##
|
45
|
+
# When quit button is clicked
|
46
|
+
#
|
47
|
+
def on_popup_menu_quit_activate
|
48
|
+
Gtk.main_quit
|
49
|
+
end
|
50
|
+
|
51
|
+
##
|
52
|
+
# Retrieve the good position to be above
|
53
|
+
# the status icon
|
54
|
+
#
|
55
|
+
# @param [Gtk::Window] window Window
|
56
|
+
#
|
57
|
+
def get_position(window)
|
58
|
+
screen, rectangle, orientation = @app.get_object('status_icon').geometry
|
59
|
+
window.set_screen(screen)
|
60
|
+
monitor_num = screen.get_monitor_at_point(rectangle.x, rectangle.y)
|
61
|
+
monitor = screen.get_monitor_geometry(monitor_num)
|
62
|
+
window_width, window_height = window.size
|
63
|
+
|
64
|
+
if orientation == Gtk::Orientation::VERTICAL
|
65
|
+
if monitor.width - rectangle.x == rectangle.width
|
66
|
+
# right panel
|
67
|
+
posx = monitor.x + monitor.width - window_width - rectangle.width
|
68
|
+
else
|
69
|
+
# left panel
|
70
|
+
posx = rectangle.x + rectangle.width
|
71
|
+
posy = rectangle.y
|
72
|
+
end
|
73
|
+
else
|
74
|
+
if (rectangle.y + rectangle.height + window_height <=
|
75
|
+
monitor.y + monitor.height)
|
76
|
+
posy = rectangle.y + rectangle.height
|
77
|
+
else
|
78
|
+
posy = rectangle.y - window_height
|
79
|
+
if (rectangle.x + window_width <= monitor.x + monitor.width)
|
80
|
+
posx = rectangle.x
|
81
|
+
else
|
82
|
+
posx = monitor.x + monitor.width - window_width
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
[posx, posy]
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# Volay module
|
2
|
+
module Volay
|
3
|
+
# Widgets components
|
4
|
+
module Widget
|
5
|
+
# Events class
|
6
|
+
class VolumeControl < Events
|
7
|
+
##
|
8
|
+
# When system tray window is showed
|
9
|
+
#
|
10
|
+
def on_system_tray_window_show
|
11
|
+
@app.get_object('volume_adjustement')
|
12
|
+
.value = @app.mixer.percent
|
13
|
+
@app.utils.update_status_icon
|
14
|
+
end
|
15
|
+
|
16
|
+
##
|
17
|
+
# When slider have its adjustement value changed
|
18
|
+
#
|
19
|
+
def on_volume_adjustement_value_changed(widget)
|
20
|
+
Thread.new do
|
21
|
+
@app.mixer.value = widget.value
|
22
|
+
@app.utils.update_status_icon
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
##
|
27
|
+
# When mute toggle image is clicked
|
28
|
+
#
|
29
|
+
def on_toggle_mute_toggled
|
30
|
+
Thread.new do
|
31
|
+
@app.mixer.toggle
|
32
|
+
@app.utils.update_status_icon
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/volay.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'gtk3'
|
2
|
+
require 'logger'
|
3
|
+
require 'mixlib/cli'
|
4
|
+
require 'mixlib/shellout'
|
5
|
+
|
6
|
+
unless $LOAD_PATH.include?(File.expand_path('../', __FILE__))
|
7
|
+
$LOAD_PATH.unshift(File.expand_path('../', __FILE__))
|
8
|
+
end
|
9
|
+
|
10
|
+
require 'volay/version'
|
11
|
+
require 'volay/config'
|
12
|
+
require 'volay/exceptions'
|
13
|
+
require 'volay/app'
|
14
|
+
require 'volay/cli'
|
15
|
+
require 'volay/utils'
|
16
|
+
require 'volay/widget/events'
|
17
|
+
require 'volay/widget/system_tray'
|
18
|
+
require 'volay/widget/volume_control'
|
19
|
+
require 'volay/mixer/default'
|
20
|
+
require 'volay/mixer/alsa'
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'fakefs/spec_helpers'
|
3
|
+
require 'gtk3'
|
4
|
+
require 'volay/app'
|
5
|
+
require 'volay/utils'
|
6
|
+
require 'volay/widget/events'
|
7
|
+
require 'volay/widget/system_tray'
|
8
|
+
require 'volay/widget/volume_control'
|
9
|
+
require 'volay/mixer/default'
|
10
|
+
require 'volay/mixer/alsa'
|
11
|
+
require 'volay/exceptions'
|
12
|
+
|
13
|
+
describe 'Volay::App' do
|
14
|
+
include FakeFS::SpecHelpers
|
15
|
+
|
16
|
+
context '#events' do
|
17
|
+
def app_events
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'should return alsa' do
|
21
|
+
allow_any_instance_of(Volay::App).to receive(:initialize_mixer)
|
22
|
+
allow_any_instance_of(Volay::App).to receive(:initialize_ui)
|
23
|
+
allow_any_instance_of(Volay::Utils).to receive(:update_status_icon)
|
24
|
+
allow_any_instance_of(Volay::App).to receive(:connect_signals)
|
25
|
+
.and_yield('on_status_icon_button_press_event')
|
26
|
+
|
27
|
+
File.write('something', '')
|
28
|
+
app = Volay::App.new('something')
|
29
|
+
expect(app.signals_list).to be_a(Hash)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context '#ui' do
|
34
|
+
it 'should return alsa' do
|
35
|
+
allow_any_instance_of(Volay::App).to receive(:initialize_mixer)
|
36
|
+
allow_any_instance_of(Volay::App).to receive(:initialize_events)
|
37
|
+
|
38
|
+
xml = <<-EOF
|
39
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
40
|
+
<!-- Generated with glade 3.18.3 -->
|
41
|
+
<interface>
|
42
|
+
<requires lib="gtk+" version="3.2"/>
|
43
|
+
<object class="GtkIconFactory" id="icon_factory">
|
44
|
+
<sources>
|
45
|
+
<source stock-id="volume-muted" filename="/glade/icons/volume-muted.png"/>
|
46
|
+
</sources>
|
47
|
+
</object>
|
48
|
+
</interface>
|
49
|
+
EOF
|
50
|
+
|
51
|
+
File.write('something', xml)
|
52
|
+
app = Volay::App.new('something')
|
53
|
+
expect(app.get_object('icon_factory')).to be_a(Gtk::IconFactory)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'logger'
|
3
|
+
require 'volay/config'
|
4
|
+
require 'volay/mixer/default'
|
5
|
+
require 'volay/mixer/alsa'
|
6
|
+
require 'volay/exceptions'
|
7
|
+
|
8
|
+
describe 'Volay::Config' do
|
9
|
+
include FakeFS::SpecHelpers
|
10
|
+
|
11
|
+
it 'should return config file' do
|
12
|
+
config_file = Volay::Config.config_file
|
13
|
+
expect(config_file).to be_a(String)
|
14
|
+
expect(config_file).to match(/.volay$/)
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'should return logger' do
|
18
|
+
expect(Volay::Config.logger).to be_a(Logger)
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'should set and get config' do
|
22
|
+
Volay::Config.set('test', 'something')
|
23
|
+
expect(Volay::Config.get('test')).to eq('something')
|
24
|
+
expect(Volay::Config.get(:test)).to eq('something')
|
25
|
+
expect(Volay::Config.get(:something)).to be_nil
|
26
|
+
end
|
27
|
+
|
28
|
+
context '#which' do
|
29
|
+
let(:config) do
|
30
|
+
FileUtils.mkdir_p('/usr/bin')
|
31
|
+
File.write('/usr/bin/ruby', '')
|
32
|
+
File.chmod(0777, '/usr/bin/ruby')
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'should find ruby executable' do
|
36
|
+
config
|
37
|
+
expect(Volay::Config.which('ruby')).to eq('/usr/bin/ruby')
|
38
|
+
end
|
39
|
+
|
40
|
+
it "shouldn't find amixer executable" do
|
41
|
+
config
|
42
|
+
expect(Volay::Config.which('amixer')).to be_nil
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
context '#mixer' do
|
47
|
+
def app_mixer(prog)
|
48
|
+
FileUtils.mkdir_p('/usr/bin')
|
49
|
+
File.write("/usr/bin/#{prog}", '')
|
50
|
+
File.chmod(0777, "/usr/bin/#{prog}")
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'should not return pulseaudio' do
|
54
|
+
app_mixer('pulseaudio-ctl')
|
55
|
+
expect { Volay::Config.mixer }
|
56
|
+
.to raise_error(Volay::MixerNotFound)
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'should return alsa' do
|
60
|
+
app_mixer('amixer')
|
61
|
+
expect(Volay::Config.mixer).to be_a(Volay::Mixer::Alsa)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'mixlib/shellout'
|
3
|
+
require 'volay/mixer/default'
|
4
|
+
|
5
|
+
describe 'Volay::Mixer::Alsa' do
|
6
|
+
let(:mixer) do
|
7
|
+
Volay::Mixer::Alsa.new
|
8
|
+
end
|
9
|
+
|
10
|
+
def stub_shellout(cmd, stdout = '', stderr = '')
|
11
|
+
allow_any_instance_of(Mixlib::ShellOut).to receive(:run_command)
|
12
|
+
allow_any_instance_of(Mixlib::ShellOut).to receive(:new)
|
13
|
+
.with("amixer #{cmd}")
|
14
|
+
allow_any_instance_of(Mixlib::ShellOut).to receive(:stdout)
|
15
|
+
.and_return(stdout)
|
16
|
+
allow_any_instance_of(Mixlib::ShellOut).to receive(:stderr)
|
17
|
+
.and_return(stderr)
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'should raise error when calling up' do
|
21
|
+
stub_shellout('-q set Master 2%+', 'Yes')
|
22
|
+
expect(mixer.up).to eq('Yes')
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'should raise error when calling down' do
|
26
|
+
stub_shellout('-q set Master 2%-', 'Yes')
|
27
|
+
expect(mixer.down).to eq('Yes')
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'should raise error when calling value=' do
|
31
|
+
stub_shellout('-q set Master 50%', 'Yes')
|
32
|
+
expect(mixer.value = '50').to eq('50')
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'should raise error when calling toggle' do
|
36
|
+
stub_shellout('-q set Master toggle', 'Yes')
|
37
|
+
expect(mixer.toggle).to eq('Yes')
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'should raise error when calling current' do
|
41
|
+
stdout = <<-EOF
|
42
|
+
Simple mixer control 'Master',0
|
43
|
+
Capabilities: pvolume pswitch pswitch-joined
|
44
|
+
Playback channels: Front Left - Front Right
|
45
|
+
Limits: Playback 0 - 65536
|
46
|
+
Mono:
|
47
|
+
Front Left: Playback 13108 [20%] [off]
|
48
|
+
Front Right: Playback 13108 [20%] [off]
|
49
|
+
EOF
|
50
|
+
stub_shellout('get Master', stdout)
|
51
|
+
expect(mixer.current).to eq(value: 13_107,
|
52
|
+
max_value: 65_536,
|
53
|
+
percent: 20,
|
54
|
+
muted: true)
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'volay/mixer/default'
|
3
|
+
|
4
|
+
describe 'Volay::Mixer::Default' do
|
5
|
+
let(:mixer) do
|
6
|
+
Volay::Mixer::Default.new
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'should raise error when calling up' do
|
10
|
+
expect { mixer.up }.to raise_error(NotImplementedError)
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'should raise error when calling down' do
|
14
|
+
expect { mixer.down }.to raise_error(NotImplementedError)
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'should raise error when calling value=' do
|
18
|
+
expect { mixer.value = '50' }.to raise_error(NotImplementedError)
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'should raise error when calling toggle' do
|
22
|
+
expect { mixer.toggle }.to raise_error(NotImplementedError)
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'should raise error when calling current' do
|
26
|
+
expect { mixer.current }.to raise_error(NotImplementedError)
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'should raise error when calling value' do
|
30
|
+
expect { mixer.value }.to raise_error(NotImplementedError)
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'should raise error when calling max_value' do
|
34
|
+
expect { mixer.max_value }.to raise_error(NotImplementedError)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'should raise error when calling percent' do
|
38
|
+
expect { mixer.percent }.to raise_error(NotImplementedError)
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'should raise error when calling muted' do
|
42
|
+
expect { mixer.muted? }.to raise_error(NotImplementedError)
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'volay/utils'
|
3
|
+
|
4
|
+
describe 'Volay::Utils' do
|
5
|
+
context 'status_icon' do
|
6
|
+
def prepare(image, muted, volume_percent)
|
7
|
+
app = double
|
8
|
+
toggle_image = double
|
9
|
+
status_icon = double
|
10
|
+
mixer = double
|
11
|
+
allow(app).to receive(:mixer).and_return(mixer)
|
12
|
+
allow(app).to receive(:get_object)
|
13
|
+
.with('status_icon').and_return(status_icon)
|
14
|
+
allow(app).to receive(:get_object)
|
15
|
+
.with('toggle_mute_image').and_return(toggle_image)
|
16
|
+
allow(mixer).to receive(:muted?)
|
17
|
+
.and_return(muted)
|
18
|
+
allow(mixer).to receive(:percent)
|
19
|
+
.and_return(volume_percent)
|
20
|
+
[status_icon, toggle_image].each do |element|
|
21
|
+
allow(element).to receive(:set_stock)
|
22
|
+
.with(image).once.and_return(true)
|
23
|
+
end
|
24
|
+
|
25
|
+
app
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'should change icon to low' do
|
29
|
+
app = prepare('volume-low', false, 20)
|
30
|
+
utils = Volay::Utils.new(app)
|
31
|
+
expect(utils.update_status_icon).to be_truthy
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'should change icon to medium' do
|
35
|
+
app = prepare('volume-medium', false, 40)
|
36
|
+
utils = Volay::Utils.new(app)
|
37
|
+
expect(utils.update_status_icon).to be_truthy
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'should change icon to high' do
|
41
|
+
app = prepare('volume-high', false, 80)
|
42
|
+
utils = Volay::Utils.new(app)
|
43
|
+
expect(utils.update_status_icon).to be_truthy
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'should change icon to muted' do
|
47
|
+
app = prepare('volume-muted', true, 0)
|
48
|
+
utils = Volay::Utils.new(app)
|
49
|
+
expect(utils.update_status_icon).to be_truthy
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'volay/widget/volume_control'
|
3
|
+
|
4
|
+
describe 'Volay::Widget::VolumeControl' do
|
5
|
+
let(:app) do
|
6
|
+
app = double
|
7
|
+
allow(app).to receive(:mixer).and_return(double)
|
8
|
+
allow(app).to receive(:signals_list).and_return({})
|
9
|
+
app
|
10
|
+
end
|
11
|
+
|
12
|
+
let(:vc) do
|
13
|
+
Volay::Widget::VolumeControl.new(app)
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'on volume scale' do
|
17
|
+
utils = double
|
18
|
+
volume = double
|
19
|
+
allow(volume).to receive(:value=).and_return(20)
|
20
|
+
allow(app).to receive(:get_object).once.with('volume_adjustement')
|
21
|
+
.and_return(volume)
|
22
|
+
allow(app).to receive(:utils).once.and_return(utils)
|
23
|
+
allow(utils).to receive(:update_status_icon).once.and_return(true)
|
24
|
+
allow(app.mixer).to receive(:percent).and_return(20)
|
25
|
+
expect(vc.on_system_tray_window_show).to be_truthy
|
26
|
+
end
|
27
|
+
end
|
data/task/manifest.rake
ADDED
data/task/rubocop.rake
ADDED
data/task/spec.rake
ADDED
data/volay.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require File.expand_path('../lib/volay/version', __FILE__)
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = 'volay'
|
5
|
+
s.version = Volay::VERSION
|
6
|
+
s.authors = ['Pierre Rambaud']
|
7
|
+
s.email = 'pierre.rambaud86@gmail.com'
|
8
|
+
s.license = 'GPL-3.0'
|
9
|
+
s.summary = 'The Volume system tray'
|
10
|
+
s.homepage = 'http://github.com/PierreRambaud/volay'
|
11
|
+
s.description = 'Really simple volume system tray written in ruby.'
|
12
|
+
s.executables = ['volay']
|
13
|
+
s.files = File.read(File.expand_path('../MANIFEST', __FILE__)).split("\n")
|
14
|
+
|
15
|
+
s.required_ruby_version = '>= 1.9.3'
|
16
|
+
|
17
|
+
s.add_dependency 'mixlib-cli', '~>1.5.0'
|
18
|
+
s.add_dependency 'mixlib-shellout', '~>2.0.1'
|
19
|
+
s.add_dependency 'gtk3', '~>2.2'
|
20
|
+
|
21
|
+
s.add_development_dependency 'fakefs', '~>0.6.0'
|
22
|
+
s.add_development_dependency 'rake', '~>10.0'
|
23
|
+
s.add_development_dependency 'rack-test', '~>0.6'
|
24
|
+
s.add_development_dependency 'rspec', '~>3.0'
|
25
|
+
s.add_development_dependency 'simplecov', '~>0.9'
|
26
|
+
s.add_development_dependency 'rubocop', '~>0.25'
|
27
|
+
end
|