windows 0.0.1
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/.gitignore +20 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +64 -0
- data/Guardfile +11 -0
- data/LICENSE +22 -0
- data/README.md +26 -0
- data/Rakefile +2 -0
- data/lib/windows.rb +9 -0
- data/lib/windows/core_ext/numeric.rb +5 -0
- data/lib/windows/engines/wmctrl.rb +68 -0
- data/lib/windows/engines/xwindow.rb +82 -0
- data/lib/windows/project.rb +31 -0
- data/lib/windows/structures.rb +7 -0
- data/lib/windows/structures/collection.rb +19 -0
- data/lib/windows/structures/desktop.rb +21 -0
- data/lib/windows/structures/geometry.rb +6 -0
- data/lib/windows/structures/window.rb +6 -0
- data/lib/windows/units/converter.rb +64 -0
- data/lib/windows/units/recognizer.rb +68 -0
- data/lib/windows/units/unit_converter.rb +73 -0
- data/lib/windows/version.rb +3 -0
- data/samples/moving_windows.rb +22 -0
- data/samples/project.rb +10 -0
- data/spec/core_ext/numeric.rb +14 -0
- data/spec/engines/wmctrl_spec.rb +110 -0
- data/spec/engines/xwindow_spec.rb +171 -0
- data/spec/project_spec.rb +68 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/structures/collection_spec.rb +28 -0
- data/spec/structures/desktop_spec.rb +13 -0
- data/spec/support/matchers/delegate.rb +40 -0
- data/spec/unit_converter_spec.rb +80 -0
- data/spec/window_spec.rb +9 -0
- data/windows.gemspec +27 -0
- metadata +219 -0
@@ -0,0 +1,68 @@
|
|
1
|
+
module Windows
|
2
|
+
module Units
|
3
|
+
module Recognizer
|
4
|
+
module Formats
|
5
|
+
class Base < Struct.new(:el)
|
6
|
+
attr_accessor :unit
|
7
|
+
|
8
|
+
def match
|
9
|
+
raise "you should implement your match"
|
10
|
+
end
|
11
|
+
|
12
|
+
def format
|
13
|
+
self.class.to_s.split("::").last.downcase.to_sym
|
14
|
+
end
|
15
|
+
|
16
|
+
def return_match(matcher, data,&block)
|
17
|
+
data = if data
|
18
|
+
matcher.instance_eval &block
|
19
|
+
matcher
|
20
|
+
else
|
21
|
+
nil
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class Pixel < Base
|
27
|
+
# match 50, 50.5, 50.02
|
28
|
+
def match
|
29
|
+
data = el.kind_of?(Numeric)
|
30
|
+
item = return_match(self, data) { @unit = el }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class Percent < Base
|
35
|
+
# match 50px, 50PX, 50.5px, 50.1PX
|
36
|
+
def match
|
37
|
+
return false unless el.respond_to?(:match)
|
38
|
+
|
39
|
+
data = el.match(/(^[\d.]+)(%)/i)
|
40
|
+
return_match(self, data) { @unit = Float(data[1]) }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
RECOGNIZERS = [Pixel, Percent]
|
45
|
+
end
|
46
|
+
|
47
|
+
class Finder < Struct.new(:el)
|
48
|
+
include Formats
|
49
|
+
|
50
|
+
def run
|
51
|
+
item = RECOGNIZERS.map do |klass|
|
52
|
+
recognizer = klass.new(el)
|
53
|
+
recognizer.match
|
54
|
+
end.compact.first
|
55
|
+
|
56
|
+
raise "I don't know how to recognize this unit #{el}" unless item
|
57
|
+
item
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def recognize_unit(el)
|
62
|
+
@recognize_unit_engine ||= Finder.new
|
63
|
+
@recognize_unit_engine.el = el
|
64
|
+
@recognize_unit_engine.run
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'windows/units/converter'
|
2
|
+
require 'windows/units/recognizer'
|
3
|
+
require 'windows/structures'
|
4
|
+
|
5
|
+
module Windows
|
6
|
+
module Units
|
7
|
+
class UnitConverter
|
8
|
+
include Units::Converter
|
9
|
+
include Units::Recognizer
|
10
|
+
include Structures
|
11
|
+
|
12
|
+
NAMED_GEOMETRY = {
|
13
|
+
left: [0, 0, '50%', '100%'],
|
14
|
+
right: ['50%', 0, '50%', '100%'],
|
15
|
+
bottom: [0, '50%', '100%', '50%'],
|
16
|
+
top: [0, 0, '100%', '50%'],
|
17
|
+
max: [0, 0, '100%', '100%'],
|
18
|
+
bottom_right: ['50%', '50%', '50%', '50%'],
|
19
|
+
bottom_left: [0, '50%', '50%', '50%'],
|
20
|
+
top_right: ['50%', 0, '50%', '50%'],
|
21
|
+
top_left: [0, 0, '50%', '50%']
|
22
|
+
}
|
23
|
+
|
24
|
+
def initialize(desktop, *args)
|
25
|
+
@width = desktop.width
|
26
|
+
@height = desktop.height
|
27
|
+
@geometry = create_geometry(args)
|
28
|
+
@x_offset = desktop.x_offset
|
29
|
+
@y_offset = desktop.y_offset
|
30
|
+
@x_axis = [@geometry.x, @geometry.w]
|
31
|
+
@y_axis = [@geometry.y, @geometry.h]
|
32
|
+
end
|
33
|
+
|
34
|
+
def convert
|
35
|
+
x, w = @x_axis.map do |el|
|
36
|
+
item = recognize_unit(el)
|
37
|
+
converter(item.unit).to(item.format, base: @width)
|
38
|
+
end
|
39
|
+
|
40
|
+
y, h = @y_axis.map do |el|
|
41
|
+
item = recognize_unit(el)
|
42
|
+
converter(item.unit).to(item.format, base: @height)
|
43
|
+
end
|
44
|
+
|
45
|
+
x = x + @x_offset
|
46
|
+
y = y + @y_offset
|
47
|
+
|
48
|
+
[x, y, w, h]
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def create_geometry(args)
|
54
|
+
return Geometry.new(*args) if args.count > 1
|
55
|
+
|
56
|
+
if args[0].respond_to?(:to_sym) && geometry_exist?(args[0])
|
57
|
+
args = find_geometry(args[0].to_sym)
|
58
|
+
return Geometry.new(*args)
|
59
|
+
else
|
60
|
+
raise "Geometry with name #{args[0]} not exist. You can use #{NAMED_GEOMETRY.keys}"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def find_geometry(name)
|
65
|
+
NAMED_GEOMETRY[name.to_sym]
|
66
|
+
end
|
67
|
+
|
68
|
+
def geometry_exist?(name)
|
69
|
+
NAMED_GEOMETRY.keys.include?(name.to_sym)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path(File.join(File.dirname(__FILE__),'../lib'))
|
2
|
+
require 'windows'
|
3
|
+
|
4
|
+
terminal1 = Windows::Window.new('x-www-browser')
|
5
|
+
terminal2 = Windows::Window.new('x-www-browser')
|
6
|
+
terminal3 = Windows::Window.new('x-www-browser')
|
7
|
+
windows = [terminal1, terminal2, terminal3]
|
8
|
+
|
9
|
+
# first create all windows
|
10
|
+
windows.each(&:create)
|
11
|
+
|
12
|
+
# move with percentage values
|
13
|
+
terminal1.move '50%', '50%', '50%', '50%'
|
14
|
+
|
15
|
+
# move with special named argument
|
16
|
+
terminal2.move :top_right
|
17
|
+
|
18
|
+
# move with pixels
|
19
|
+
terminal3.move 0, 0, 300, 300
|
20
|
+
|
21
|
+
sleep 5
|
22
|
+
windows.each(&:close)
|
data/samples/project.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
$LOAD_PATH.unshift lib = File.expand_path(File.join(File.dirname(__FILE__),'../lib'))
|
2
|
+
require 'windows'
|
3
|
+
|
4
|
+
p = Windows::Project.new(:default,lib)
|
5
|
+
|
6
|
+
p.open_window('x-www-browser').move(:right)
|
7
|
+
p.open_window('x-terminal-emulator').move(:bottom)
|
8
|
+
|
9
|
+
sleep 5
|
10
|
+
p.close
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'windows/core_ext/numeric'
|
3
|
+
|
4
|
+
describe Numeric do
|
5
|
+
context "#percent" do
|
6
|
+
it 'return correct value' do
|
7
|
+
1000.percent(10).should == 100
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'floor result' do
|
11
|
+
999.percent(31.53).should == 314
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'windows/engines/wmctrl'
|
3
|
+
require 'time'
|
4
|
+
|
5
|
+
describe WMCtrl do
|
6
|
+
let(:fake_window_id) { 1234 }
|
7
|
+
let(:window) { Windows::Structures::Window.new(fake_window_id) }
|
8
|
+
let(:time) { Time.parse("Sep 13 2011") }
|
9
|
+
let(:command) { 'any_command' }
|
10
|
+
|
11
|
+
before :each do
|
12
|
+
create_windows
|
13
|
+
create_desktops
|
14
|
+
end
|
15
|
+
|
16
|
+
context "spawn_window" do
|
17
|
+
it 'should raise exception when command not exist' do
|
18
|
+
Process.stub(:spawn).and_raise(Errno::ENOENT)
|
19
|
+
expect { subject.spawn_window(command) }.to raise_error "Failed to create window with command: #{command}. Maybe a typo?"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
it "#desktops" do
|
24
|
+
list = subject.desktops
|
25
|
+
desktops.zip(list).each do |desktop, desktop_window|
|
26
|
+
desktop[:id].should == desktop_window.id
|
27
|
+
desktop[:workarea].should == desktop_window.geometry
|
28
|
+
end
|
29
|
+
|
30
|
+
list.should be_instance_of(Windows::Structures::Collection)
|
31
|
+
end
|
32
|
+
|
33
|
+
it "#find_desktops" do
|
34
|
+
desktop = subject.find_desktop(1)
|
35
|
+
desktop.id.should == 1
|
36
|
+
desktop.should be_instance_of(Windows::Structures::Desktop)
|
37
|
+
end
|
38
|
+
|
39
|
+
context "#windows" do
|
40
|
+
it 'return windows' do
|
41
|
+
list = subject.windows
|
42
|
+
|
43
|
+
windows(:sorted).zip(list).each do |stub, window|
|
44
|
+
desktop = subject.find_desktop(stub[:desktop])
|
45
|
+
|
46
|
+
stub[:id].should == window.id
|
47
|
+
stub[:title].should == window.title
|
48
|
+
stub[:desktop].should == desktop.id
|
49
|
+
stub[:geometry][0].should == window.x
|
50
|
+
stub[:geometry][1].should == window.y
|
51
|
+
stub[:geometry][2].should == window.width
|
52
|
+
stub[:geometry][3].should == window.height
|
53
|
+
end
|
54
|
+
|
55
|
+
list.should be_instance_of(Windows::Structures::Collection)
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'return sorted by :id' do
|
59
|
+
expected_ids = windows(:sorted).map {|w| w[:id]}
|
60
|
+
|
61
|
+
subject.windows.ids.should == expected_ids
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
context "#find_windows" do
|
66
|
+
it 'should find window by id' do
|
67
|
+
subject.find_window(2).id.should == 2
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'should return Window' do
|
71
|
+
subject.find_window(2).should be_instance_of Windows::Structures::Window
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
it '#active_window' do
|
76
|
+
subject.active_window.active.should be_true
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def desktops
|
82
|
+
unless @desktops
|
83
|
+
klass = Windows::Structures::Desktop
|
84
|
+
@desktops = []
|
85
|
+
@desktops.push({id: 1, workarea: [0, 0, 800, 600]})
|
86
|
+
@desktops.push({id: 2, workarea: [0, 0, 1200, 1000]})
|
87
|
+
end
|
88
|
+
@desktops
|
89
|
+
end
|
90
|
+
|
91
|
+
def windows(sorted = false)
|
92
|
+
unless @windows
|
93
|
+
klass = Windows::Structures::Window
|
94
|
+
@windows = []
|
95
|
+
@windows.push({id: 1, title: 'bash', desktop: 1, geometry: [0,0,100,200]})
|
96
|
+
@windows.push({id: 3, title: 'torr', desktop: 2, geometry: [100,100,400,800]})
|
97
|
+
@windows.push({id: 2, title: 'list', desktop: 1, geometry: [50,50,200,400], active: true})
|
98
|
+
end
|
99
|
+
sorted ? @windows.sort_by{|w| w[:id]} : @windows
|
100
|
+
end
|
101
|
+
|
102
|
+
def create_windows
|
103
|
+
subject.stub(:list_windows).and_return(windows)
|
104
|
+
end
|
105
|
+
|
106
|
+
def create_desktops
|
107
|
+
subject.stub(:list_desktops).and_return(desktops)
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'time'
|
3
|
+
require 'windows/engines/xwindow'
|
4
|
+
require 'windows/structures/window'
|
5
|
+
require 'windows/structures/desktop'
|
6
|
+
|
7
|
+
module Windows
|
8
|
+
module Engines
|
9
|
+
class WMCtrl
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class DummyEngine
|
15
|
+
def action(*args)
|
16
|
+
end
|
17
|
+
|
18
|
+
def spawn_window(command)
|
19
|
+
fake_window
|
20
|
+
end
|
21
|
+
|
22
|
+
def find_window(id)
|
23
|
+
fake_window
|
24
|
+
end
|
25
|
+
|
26
|
+
def fake_window
|
27
|
+
Windows::Structures::Window.new(1234, 'fake window', fake_desktop)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def fake_desktop
|
33
|
+
Windows::Structures::Desktop.new(9876, [0,0,800,600])
|
34
|
+
end
|
35
|
+
end
|
36
|
+
class DummyWindow < Struct.new(:id, :title);end
|
37
|
+
class DummyDesktop < Struct.new(:x_offset, :y_offset, :width, :height);end
|
38
|
+
|
39
|
+
describe Windows::Engines::XWindow do
|
40
|
+
subject { Windows::Engines::XWindow.new(command, options, engine)}
|
41
|
+
let(:engine) { DummyEngine.new }
|
42
|
+
let(:command) { 'ls' }
|
43
|
+
let(:time) { Time.parse("Sep 13 2011")}
|
44
|
+
let(:options) { Hash.new }
|
45
|
+
let(:window) { engine.fake_window }
|
46
|
+
let(:id) { window.id }
|
47
|
+
|
48
|
+
it { should delegate(:title).to(:window) }
|
49
|
+
it { should delegate(:desktop).to(:window) }
|
50
|
+
it { should delegate(:x).to(:window) }
|
51
|
+
it { should delegate(:y).to(:window) }
|
52
|
+
it { should delegate(:width).to(:window) }
|
53
|
+
it { should delegate(:height).to(:window) }
|
54
|
+
it { should respond_to :command= }
|
55
|
+
|
56
|
+
it '#move' do
|
57
|
+
args = [100, 200, 500, 400]
|
58
|
+
|
59
|
+
subject.should_receive(:undock).ordered
|
60
|
+
engine.should_receive(:action).with(id, :move_resize, 0, *args).ordered
|
61
|
+
subject.move(*args)
|
62
|
+
end
|
63
|
+
|
64
|
+
context 'close' do
|
65
|
+
it 'should delegate to engine' do
|
66
|
+
engine.should_receive(:action).with(id, :close)
|
67
|
+
subject.close
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'should :id, :created_at set to false' do
|
71
|
+
subject.create
|
72
|
+
subject.close
|
73
|
+
subject.id.should == false
|
74
|
+
subject.created_at.should == false
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
it '#focus' do
|
79
|
+
engine.should_receive(:action).with(id, :activate)
|
80
|
+
subject.focus
|
81
|
+
end
|
82
|
+
|
83
|
+
it '#undock' do
|
84
|
+
args = ["remove", "maximized_vert", "maximized_horz"]
|
85
|
+
|
86
|
+
engine.should_receive(:action).with(id, :change_state, *args)
|
87
|
+
subject.undock
|
88
|
+
end
|
89
|
+
|
90
|
+
context "#create" do
|
91
|
+
before(:each) do
|
92
|
+
Time.stub!(:now).and_return(time)
|
93
|
+
end
|
94
|
+
|
95
|
+
it 'should assign id' do
|
96
|
+
subject.create
|
97
|
+
subject.id.should == engine.fake_window.id
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'should assign created_at' do
|
101
|
+
subject.create
|
102
|
+
subject.created_at.should == time
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'should return false when window is already created' do
|
106
|
+
subject.create
|
107
|
+
subject.create.should be_false
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'should not spawn new window when it exist' do
|
111
|
+
engine.should_receive(:find_window).with(command).and_return(window)
|
112
|
+
engine.should_not_receive(:spawn_window).with(command)
|
113
|
+
subject.create
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
context '#window' do
|
118
|
+
context "when window is not created" do
|
119
|
+
it 'should create window' do
|
120
|
+
subject.should_receive(:create)
|
121
|
+
subject.window
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
context "when window is created" do
|
126
|
+
before do
|
127
|
+
subject.create
|
128
|
+
end
|
129
|
+
|
130
|
+
it 'should delegate to engine' do
|
131
|
+
engine.should_receive(:find_window).with(id)
|
132
|
+
subject.window
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
it '#on_top' do
|
138
|
+
engine.should_receive(:action).with(id, :change_state, "add", "above")
|
139
|
+
subject.on_top
|
140
|
+
end
|
141
|
+
|
142
|
+
it '#not_on_top' do
|
143
|
+
engine.should_receive(:action).with(id, :change_state, "remove", "above")
|
144
|
+
subject.not_on_top
|
145
|
+
end
|
146
|
+
|
147
|
+
context "#action" do
|
148
|
+
it 'should delegate to engine' do
|
149
|
+
args = [0, 0, 0, 100, 200]
|
150
|
+
|
151
|
+
engine.should_receive(:action).with(id, *args)
|
152
|
+
subject.action(*args)
|
153
|
+
end
|
154
|
+
|
155
|
+
it 'should return self' do
|
156
|
+
subject.action.should == subject
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
it '#maximize' do
|
161
|
+
engine.should_receive(:action).with(id, :change_state, "add", "maximized_vert", "maximized_horz")
|
162
|
+
subject.maximize
|
163
|
+
end
|
164
|
+
|
165
|
+
context "initialize" do
|
166
|
+
it "should use WMCtrl as default engine" do
|
167
|
+
object = subject.class.new(command, options, nil)
|
168
|
+
object.engine.should be_instance_of(Windows::Engines::WMCtrl)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|