volay 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/MANIFEST ADDED
@@ -0,0 +1,38 @@
1
+ .gitignore
2
+ .rubocop.yml
3
+ .travis.yml
4
+ Gemfile
5
+ LICENSE
6
+ MANIFEST
7
+ README.md
8
+ Rakefile
9
+ bin/volay
10
+ glade/icons/volume-high.png
11
+ glade/icons/volume-low.png
12
+ glade/icons/volume-medium.png
13
+ glade/icons/volume-muted.png
14
+ glade/volay.glade
15
+ lib/volay.rb
16
+ lib/volay/app.rb
17
+ lib/volay/cli.rb
18
+ lib/volay/config.rb
19
+ lib/volay/exceptions.rb
20
+ lib/volay/mixer/alsa.rb
21
+ lib/volay/mixer/default.rb
22
+ lib/volay/utils.rb
23
+ lib/volay/version.rb
24
+ lib/volay/volay.rb
25
+ lib/volay/widget/events.rb
26
+ lib/volay/widget/system_tray.rb
27
+ lib/volay/widget/volume_control.rb
28
+ spec/spec_helper.rb
29
+ spec/volay/app_spec.rb
30
+ spec/volay/config_spec.rb
31
+ spec/volay/mixer/alsa_spec.rb
32
+ spec/volay/mixer/default_spec.rb
33
+ spec/volay/utils_spec.rb
34
+ spec/volay/widget/volume_control_spec.rb
35
+ task/manifest.rake
36
+ task/rubocop.rake
37
+ task/spec.rake
38
+ volay.gemspec
data/README.md ADDED
@@ -0,0 +1,52 @@
1
+ #Volay, the Volume System Tray
2
+
3
+ [![Build Status](https://travis-ci.org/PierreRambaud/volay.svg)](https://travis-ci.org/PierreRambaud/volay)
4
+
5
+ ##Requirements
6
+
7
+ * Ruby 1.9.3 or newer
8
+
9
+ ##Installation
10
+
11
+ From Rubygems:
12
+
13
+ ```
14
+ $ gem install volay
15
+ ```
16
+
17
+ From Github:
18
+
19
+ ```
20
+ $ git clone https://github.com/PierreRambaud/volay.git
21
+ $ cd volay
22
+ $ bundle install
23
+ $ bundle exec rake install
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ ```bash
29
+ $ volay -h
30
+ Volay, the Volume System Tray.
31
+ -d, --down PERCENT Down volume
32
+ -l, --log_level LEVEL Set the log level (debug, info, warn, error, fatal)
33
+ -m, --toggle-mute Toggle mute
34
+ -u, --up PERCENT Up volume
35
+ -h, --help Show this message
36
+ ```
37
+
38
+ If no arguments are passed, the Gtk app is launched.
39
+
40
+ ## Running tests
41
+
42
+ To run unit tests:
43
+ `$ bundle exec rake spec`
44
+
45
+ To check code style:
46
+ `$ bundle exec rake rubocop`
47
+
48
+ To run all tests:
49
+ `$ bundle exec rake`
50
+
51
+ ## License
52
+ See [LICENSE](LICENSE) file
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+
2
+ require 'bundler/gem_tasks'
3
+ GEMSPEC = Gem::Specification.load('gemirro.gemspec')
4
+
5
+ Dir['./task/*.rake'].each do |task|
6
+ import(task)
7
+ end
8
+
9
+ task default: [:rubocop, :spec]
data/bin/volay ADDED
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env ruby
2
+ require File.expand_path('../../lib/volay', __FILE__)
3
+
4
+ # Module volay
5
+ module Volay
6
+ # Main class to display app or control volume
7
+ class Main
8
+ def initialize
9
+ cli = Volay::CLI.new
10
+ cli.parse_options
11
+ Volay::Config.set(:log_level, cli.config[:log_level])
12
+ Volay::Config.init_config
13
+ if [:up, :down, :mute].any? { |k| cli.config.key?(k) }
14
+ sound(cli.config)
15
+ else
16
+ app
17
+ end
18
+ end
19
+
20
+ def sound(config)
21
+ Volay::Config.mixer.up(config[:up]) unless config[:up].nil?
22
+ Volay::Config.mixer.down(config[:down]) unless config[:down].nil?
23
+ Volay::Config.mixer.toggle if config[:mute]
24
+ end
25
+
26
+ def app
27
+ Gtk.init
28
+ Volay::App.new('glade/volay.glade')
29
+ Gtk.main
30
+ end
31
+ end
32
+ end
33
+
34
+ Volay::Main.new
Binary file
Binary file
Binary file
Binary file
data/glade/volay.glade ADDED
@@ -0,0 +1,99 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!-- Generated with glade 3.18.3 -->
3
+ <interface>
4
+ <requires lib="gtk+" version="3.6"/>
5
+ <object class="GtkIconFactory" id="icon_factory">
6
+ <sources>
7
+ <source stock-id="volume-high" filename="/glade/icons/volume-high.png"/>
8
+ <source stock-id="volume-medium" filename="/glade/icons/volume-medium.png"/>
9
+ <source stock-id="volume-muted" filename="/glade/icons/volume-muted.png"/>
10
+ <source stock-id="volume-low" filename="/glade/icons/volume-low.png"/>
11
+ </sources>
12
+ </object>
13
+ <object class="GtkMenu" id="popup_menu">
14
+ <property name="visible">True</property>
15
+ <property name="can_focus">False</property>
16
+ <child>
17
+ <object class="GtkImageMenuItem" id="popup_menu_quit">
18
+ <property name="label">gtk-quit</property>
19
+ <property name="visible">True</property>
20
+ <property name="can_focus">False</property>
21
+ <property name="use_underline">True</property>
22
+ <property name="use_stock">True</property>
23
+ <property name="always_show_image">True</property>
24
+ <signal name="activate" handler="on_popup_menu_quit_activate" swapped="no"/>
25
+ </object>
26
+ </child>
27
+ </object>
28
+ <object class="GtkStatusIcon" id="status_icon">
29
+ <property name="stock">volume-muted</property>
30
+ <signal name="button-press-event" handler="on_status_icon_button_press_event" swapped="no"/>
31
+ <signal name="popup-menu" handler="on_status_icon_popup_menu" swapped="no"/>
32
+ </object>
33
+ <object class="GtkAdjustment" id="volume_adjustement">
34
+ <property name="upper">100</property>
35
+ <property name="step_increment">1</property>
36
+ <property name="page_increment">10</property>
37
+ <signal name="value-changed" handler="on_volume_adjustement_value_changed" swapped="no"/>
38
+ </object>
39
+ <object class="GtkWindow" id="system_tray_window">
40
+ <property name="width_request">25</property>
41
+ <property name="height_request">200</property>
42
+ <property name="can_focus">True</property>
43
+ <property name="events">GDK_FOCUS_CHANGE_MASK</property>
44
+ <property name="resizable">False</property>
45
+ <property name="modal">True</property>
46
+ <property name="window_position">mouse</property>
47
+ <property name="type_hint">popup-menu</property>
48
+ <property name="focus_on_map">False</property>
49
+ <property name="decorated">False</property>
50
+ <property name="deletable">False</property>
51
+ <signal name="focus-out-event" handler="on_system_tray_window_focus_out_event" swapped="no"/>
52
+ <signal name="show" handler="on_system_tray_window_show" swapped="no"/>
53
+ <child>
54
+ <object class="GtkBox" id="volume_control_box">
55
+ <property name="visible">True</property>
56
+ <property name="can_focus">False</property>
57
+ <property name="orientation">vertical</property>
58
+ <child>
59
+ <object class="GtkScale" id="volume_scale">
60
+ <property name="visible">True</property>
61
+ <property name="can_focus">True</property>
62
+ <property name="orientation">vertical</property>
63
+ <property name="adjustment">volume_adjustement</property>
64
+ <property name="inverted">True</property>
65
+ <property name="round_digits">0</property>
66
+ <property name="digits">0</property>
67
+ <signal name="realize" handler="on_volume_scale_realize" swapped="no"/>
68
+ </object>
69
+ <packing>
70
+ <property name="expand">True</property>
71
+ <property name="fill">True</property>
72
+ <property name="position">0</property>
73
+ </packing>
74
+ </child>
75
+ <child>
76
+ <object class="GtkToggleButton" id="toggle_mute">
77
+ <property name="visible">True</property>
78
+ <property name="can_focus">False</property>
79
+ <property name="receives_default">True</property>
80
+ <property name="always_show_image">True</property>
81
+ <signal name="toggled" handler="on_toggle_mute_toggled" swapped="no"/>
82
+ <child>
83
+ <object class="GtkImage" id="toggle_mute_image">
84
+ <property name="visible">True</property>
85
+ <property name="can_focus">False</property>
86
+ <property name="stock">volume-muted</property>
87
+ </object>
88
+ </child>
89
+ </object>
90
+ <packing>
91
+ <property name="expand">False</property>
92
+ <property name="fill">True</property>
93
+ <property name="position">1</property>
94
+ </packing>
95
+ </child>
96
+ </object>
97
+ </child>
98
+ </object>
99
+ </interface>
data/lib/volay/app.rb ADDED
@@ -0,0 +1,62 @@
1
+ # Volay module
2
+ module Volay
3
+ # App class
4
+ class App < Gtk::Builder
5
+ attr_accessor :signals_list
6
+ attr_reader :mixer, :utils
7
+
8
+ ##
9
+ # Initialize application
10
+ #
11
+ # @param [String] path Path to glade file
12
+ #
13
+ def initialize(path)
14
+ super()
15
+
16
+ Gtk::Settings.default.gtk_button_images = true
17
+
18
+ initialize_ui(path)
19
+ initialize_mixer
20
+ initialize_events
21
+ end
22
+
23
+ private
24
+
25
+ ##
26
+ # Initialize events
27
+ #
28
+ def initialize_events
29
+ @signals_list = {}
30
+ @utils = Volay::Utils.new(self)
31
+ @utils.update_status_icon
32
+ Volay::Widget::SystemTray.new(self)
33
+ Volay::Widget::VolumeControl.new(self)
34
+
35
+ connect_signals do |handler|
36
+ @signals_list[handler] if @signals_list.key?(handler)
37
+ end
38
+ end
39
+
40
+ ##
41
+ # Initialize mixer for controlling volume
42
+ #
43
+ def initialize_mixer
44
+ @mixer ||= Volay::Config.mixer
45
+ end
46
+
47
+ ##
48
+ # Intiailize ui by overriding icons path
49
+ #
50
+ # @param [String] path Path to glade file
51
+ #
52
+ def initialize_ui(path)
53
+ # Override Icon path to prevent
54
+ # Gtk-CRITICAL **:gtk_icon_source_set_filename:
55
+ ui = File.read(path)
56
+ ui.gsub!(%r{/glade/icons},
57
+ File.expand_path('../../glade/icons',
58
+ File.dirname(__FILE__)))
59
+ add_from_string(ui)
60
+ end
61
+ end
62
+ end
data/lib/volay/cli.rb ADDED
@@ -0,0 +1,44 @@
1
+ # Volay module
2
+ module Volay
3
+ # CLI implementation
4
+ class CLI
5
+ include Mixlib::CLI
6
+
7
+ banner('Volay, the Volume System Tray.')
8
+
9
+ option(:help,
10
+ short: '-h',
11
+ long: '--help',
12
+ description: 'Show this message',
13
+ on: :tail,
14
+ boolean: true,
15
+ show_options: true,
16
+ exit: 0)
17
+
18
+ option(:log_level,
19
+ short: '-l LEVEL',
20
+ long: '--log_level LEVEL',
21
+ description: 'Set the log level (debug, info, warn, error, fatal)',
22
+ in: %w(debug info warn error fatal),
23
+ default: :info,
24
+ proc: proc { |l| l.to_sym })
25
+
26
+ option(:up,
27
+ short: '-u PERCENT',
28
+ long: '--up PERCENT',
29
+ description: 'Up volume',
30
+ proc: proc { |l| l.to_i })
31
+
32
+ option(:down,
33
+ short: '-d PERCENT',
34
+ long: '--down PERCENT',
35
+ description: 'Down volume',
36
+ proc: proc { |l| l.to_i })
37
+
38
+ option(:mute,
39
+ short: '-m',
40
+ long: '--toggle-mute',
41
+ boolean: true,
42
+ description: 'Toggle mute')
43
+ end
44
+ end
@@ -0,0 +1,91 @@
1
+ # Volay module
2
+ module Volay
3
+ # Config class
4
+ class Config
5
+ attr_reader :logger, :mixer, :options
6
+
7
+ ##
8
+ # Get option
9
+ #
10
+ # @return [Mixed]
11
+ #
12
+ def self.init_config
13
+ File.write(config_file, '') unless File.exist?(config_file)
14
+ logger.level = get(:log_level)
15
+ end
16
+
17
+ ##
18
+ # Get option
19
+ #
20
+ # @return [Mixed]
21
+ #
22
+ def self.get(option)
23
+ @options ||= {}
24
+ @options[option.to_sym] if @options.key?(option.to_sym)
25
+ end
26
+
27
+ ##
28
+ # Set option
29
+ #
30
+ # @param [String|Symbol] option Option key
31
+ # @param [Mixed] value Option value
32
+ #
33
+ def self.set(option, value)
34
+ @options ||= {}
35
+ @options[option.to_sym] = value
36
+ end
37
+
38
+ ##
39
+ # Get config file
40
+ #
41
+ # @return [String]
42
+ #
43
+ def self.config_file
44
+ File.expand_path('~/.volay')
45
+ end
46
+
47
+ ##
48
+ # Get logger
49
+ #
50
+ # @return [Logger]
51
+ #
52
+ def self.logger
53
+ @logger ||= Logger.new(STDOUT)
54
+ end
55
+
56
+ ##
57
+ # Initialize mixer for controlling volume
58
+ #
59
+ def self.mixer
60
+ @mixer ||= begin
61
+ if which('amixer')
62
+ Volay::Mixer::Alsa.new
63
+ else
64
+ fail MixerNotFound
65
+ end
66
+ end
67
+ end
68
+
69
+ ##
70
+ # Cross-platform way of finding an executable in the $PATH.
71
+ #
72
+ # Example:
73
+ # which('ruby') #=> /usr/bin/ruby
74
+ # which('foo') #=> nil
75
+ #
76
+ # @param [String] cmd Which command
77
+ # @return [String|NilClass]
78
+ #
79
+ def self.which(cmd)
80
+ exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
81
+ ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
82
+ exts.each do |ext|
83
+ exe = File.join(path, "#{cmd}#{ext}")
84
+ return exe if File.executable?(exe) && !File.directory?(exe)
85
+ end
86
+ end
87
+
88
+ nil
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,6 @@
1
+ # Volay Module
2
+ module Volay
3
+ # Mixer not found
4
+ class MixerNotFound < StandardError
5
+ end
6
+ end
@@ -0,0 +1,48 @@
1
+ # Volay module
2
+ module Volay
3
+ # Mixer module
4
+ module Mixer
5
+ # Alsa class for playing with amixer
6
+ class Alsa < Default
7
+ DEFAULT_VALUE = 2
8
+
9
+ def up(value = DEFAULT_VALUE)
10
+ command("-q set Master #{value}%+")
11
+ end
12
+
13
+ def down(value = DEFAULT_VALUE)
14
+ command("-q set Master #{value}%-")
15
+ end
16
+
17
+ def value=(value)
18
+ command("-q set Master #{value}%")
19
+ end
20
+
21
+ def toggle
22
+ command('-q set Master toggle')
23
+ end
24
+
25
+ def current
26
+ result = command('get Master')
27
+ max_value = result.match(/ 0 - (\d+)/)[1].to_i
28
+ current_percent = result.match(/\[([0-9]+)%\]\s+\[o(?:n|ff)\]/)[1].to_i
29
+ current_value = ((current_percent * max_value) / 100).to_i
30
+ current_state = result.match(/\[([a-z]+)\]/)[1].to_s
31
+
32
+ { value: current_value,
33
+ max_value: max_value,
34
+ percent: current_percent,
35
+ muted: current_state == 'on' ? false : true }
36
+ end
37
+
38
+ private
39
+
40
+ def command(cmd)
41
+ result = Mixlib::ShellOut.new("amixer #{cmd}")
42
+ result.run_command
43
+ Volay::Config.logger.error(result.stderr) unless result.stderr.empty?
44
+ result.stdout
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,72 @@
1
+ # Volay module
2
+ module Volay
3
+ # Mixer module
4
+ module Mixer
5
+ # Default class
6
+ class Default
7
+ ##
8
+ # Up the volume
9
+ #
10
+ def up(*)
11
+ fail NotImplementedError
12
+ end
13
+
14
+ ##
15
+ # Down the volume
16
+ #
17
+ def down(*)
18
+ fail NotImplementedError
19
+ end
20
+
21
+ ##
22
+ # Set the volume
23
+ #
24
+ def value=(*)
25
+ fail NotImplementedError
26
+ end
27
+
28
+ ##
29
+ # Toggle mute
30
+ #
31
+ def toggle
32
+ fail NotImplementedError
33
+ end
34
+
35
+ ##
36
+ # Current data, value, max_value,
37
+ # percent and muted
38
+ #
39
+ def current
40
+ fail NotImplementedError
41
+ end
42
+
43
+ ##
44
+ # Current value
45
+ #
46
+ def max_value
47
+ current[:value]
48
+ end
49
+
50
+ ##
51
+ # Current value
52
+ #
53
+ def value
54
+ current[:value]
55
+ end
56
+
57
+ ##
58
+ # Current percent
59
+ #
60
+ def percent
61
+ current[:percent]
62
+ end
63
+
64
+ ##
65
+ # Is muted
66
+ #
67
+ def muted?
68
+ current[:muted]
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,40 @@
1
+ # Volay module
2
+ module Volay
3
+ # Utils class
4
+ class Utils
5
+ attr_reader :app
6
+
7
+ ##
8
+ # Initialize utils class
9
+ #
10
+ # @param [Gtk::Builder] app Application
11
+ #
12
+ def initialize(app)
13
+ @app = app
14
+ end
15
+
16
+ ##
17
+ # Change status icon stock
18
+ #
19
+ def update_status_icon
20
+ icon = status_icon
21
+ @app.get_object('status_icon')
22
+ .set_stock(icon)
23
+ @app.get_object('toggle_mute_image')
24
+ .set_stock(icon)
25
+ end
26
+
27
+ def status_icon
28
+ case @app.mixer.percent
29
+ when 66..100
30
+ icon = 'volume-high'
31
+ when 33..65
32
+ icon = 'volume-medium'
33
+ when 0..32
34
+ icon = 'volume-low'
35
+ end unless @app.mixer.muted?
36
+
37
+ icon.nil? ? 'volume-muted' : icon
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,4 @@
1
+ # Volay module
2
+ module Volay
3
+ VERSION = '0.0.1'
4
+ end
@@ -0,0 +1,11 @@
1
+ require 'gtk3'
2
+ require 'mixlib-cli'
3
+
4
+ unless $LOAD_PATH.include?(File.expand_path('../', __FILE__))
5
+ $LOAD_PATH.unshift(File.expand_path('../', __FILE__))
6
+ end
7
+
8
+ require 'volay/version'
9
+ require 'volay/app'
10
+ require 'volay/cli'
11
+ require 'volay/widget/events'
@@ -0,0 +1,22 @@
1
+ # Volay module
2
+ module Volay
3
+ # Widgets components
4
+ module Widget
5
+ # Events class
6
+ class Events
7
+ attr_reader :app
8
+
9
+ ##
10
+ # Initialize signals events
11
+ #
12
+ def initialize(app)
13
+ @app = app
14
+
15
+ methods.each do |name|
16
+ next unless name.match(/^on_/)
17
+ @app.signals_list[name.to_s] = method(name)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end