i3ipc 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.travis.yml +9 -0
- data/.yardopts +2 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +177 -0
- data/Rakefile +7 -0
- data/i3ipc.gemspec +31 -0
- data/lib/i3ipc.rb +4 -0
- data/lib/i3ipc/connection.rb +66 -0
- data/lib/i3ipc/protocol.rb +136 -0
- data/lib/i3ipc/reply.rb +116 -0
- data/lib/i3ipc/version.rb +3 -0
- data/spec/i3_mock_server.rb +57 -0
- data/spec/i3ipc/connection_spec.rb +46 -0
- data/spec/i3ipc/protocol_spec.rb +132 -0
- data/spec/i3ipc/reply_spec.rb +117 -0
- data/spec/spec_helper.rb +8 -0
- metadata +114 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: e40ae80557aa74e152aa616ae813744d8b755586
|
4
|
+
data.tar.gz: a5f833254c367b6d9bc4b4bdecd8303020388194
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4af0b717ee2d06d19d858694305c21eef45c2ef0b1e74427acafd18c32c9db0835694c9c84fadc5456d1a55bb51a7b1e5664d58f50f564ba4aed75095a1769de
|
7
|
+
data.tar.gz: 38fef1144589bacb4c4b7edcae7427f435aeaa95fca85a0b45789f4805f2e9b21deade91c6f396096bf4e201a8cd0603b0207e21dc25787f0a50f10113403208
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/.yardopts
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 veelenga
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,177 @@
|
|
1
|
+
# I3ipc [![Gem Version](https://badge.fury.io/rb/i3ipc.svg)](https://rubygems.org/gems/i3ipc) [![Build Status](https://api.travis-ci.org/veelenga/i3ipc-ruby.svg?branch=master)](https://travis-ci.org/veelenga/i3ipc-ruby)
|
2
|
+
|
3
|
+
- [Installation](#installation)
|
4
|
+
- [Usage](#usage)
|
5
|
+
- [Command](#command)
|
6
|
+
- [Workspaces](#workspaces)
|
7
|
+
- [Subscribe](#subscribe)
|
8
|
+
- [Outputs](#outputs)
|
9
|
+
- [Tree](#tree)
|
10
|
+
- [Marks](#marks)
|
11
|
+
- [Bar config](#bar-config)
|
12
|
+
- [Version](#version)
|
13
|
+
- [Contributing](#contributing)
|
14
|
+
|
15
|
+
|
16
|
+
An improved Ruby library to control [i3wm](http://i3wm.org/).
|
17
|
+
|
18
|
+
i3's interprocess communication (or [ipc](http://i3wm.org/docs/ipc.html)) is the interface i3 wm uses to receive commands from the clients. It also features a publish/subscribe mechanism for notifying interested parties of window manager events.
|
19
|
+
|
20
|
+
This gem will be useful for example for controlling i3 windows manager or to get various information like the current workspaces or to implemennt external workspace bar in `Ruby` language.
|
21
|
+
|
22
|
+
Inspired by [i3ipc-python](https://github.com/acrisci/i3ipc-python), [i3ipc-gjs](https://github.com/acrisci/i3ipc-gjs), [i3ipc-lua](https://github.com/acrisci/i3ipc-lua) and reworked mainly from [i3-ipc](https://github.com/badboy/i3-ipc) (thanks to [@badboy](https://github.com/badboy) for this gem).
|
23
|
+
|
24
|
+
## Installation
|
25
|
+
|
26
|
+
Nothing special here:
|
27
|
+
```sh
|
28
|
+
$ gem install i3ipc
|
29
|
+
```
|
30
|
+
## Usage
|
31
|
+
|
32
|
+
Usage is very simple and straightforward:
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
require 'i3ipc'
|
36
|
+
|
37
|
+
i3 = I3Ipc::Connection.new
|
38
|
+
# communicate with i3 server...
|
39
|
+
# ...
|
40
|
+
i3.close
|
41
|
+
|
42
|
+
```
|
43
|
+
|
44
|
+
Below you can find examples of usage some replies from local i3 wm. Output depend on my config and will be different in other env. A list of messages to send and replies you can find in [Receiving replies from i3](https://i3wm.org/docs/ipc.html#_receiving_replies_from_i3).
|
45
|
+
|
46
|
+
Each reply from i3 wm will be parsed and packed in a special object. That object responds to any method with a name of an original name of i3 wm attribute in the reply. So you can access attributes in a very useful way. Find examples below.
|
47
|
+
|
48
|
+
### Command
|
49
|
+
|
50
|
+
Executes one or more command at a time. Reply contains the property `success (bool)` for each command:
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
>> command = i3.command('workspace 0; focus left')
|
54
|
+
>> puts command[0]
|
55
|
+
{
|
56
|
+
"success": true
|
57
|
+
}
|
58
|
+
>> puts command[0].success
|
59
|
+
true
|
60
|
+
```
|
61
|
+
|
62
|
+
or a human readable error message in the property `error (string)`
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
>> command = i3.command('this a bad command')
|
66
|
+
>> puts command[0].success
|
67
|
+
false
|
68
|
+
>> puts command[0].error
|
69
|
+
Expected one of these tokens: <end>, '[', 'move', 'exec', 'exit', 'restart', 'reload', 'shmlog', 'debuglog', 'border', 'layout', 'append_layout', 'workspace', 'focus', 'kill', 'open', 'fullscreen', 'split', 'floating', 'mark', 'unmark', 'resize', 'rename', 'nop', 'scratchpad', 'mode', 'bar'
|
70
|
+
```
|
71
|
+
|
72
|
+
### Workspaces
|
73
|
+
|
74
|
+
Reply consists of a list of workspaces. Each workspace has some properties:
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
>> workspaces = i3.workspaces
|
78
|
+
>> puts workspaces[0]
|
79
|
+
{
|
80
|
+
"num": 1,
|
81
|
+
"name": "1 Browse",
|
82
|
+
"visible": true,
|
83
|
+
"focused": false,
|
84
|
+
"rect": {
|
85
|
+
"x": 1366,
|
86
|
+
"y": 20,
|
87
|
+
"width": 1920,
|
88
|
+
"height": 1060
|
89
|
+
},
|
90
|
+
"output": "VGA1",
|
91
|
+
"urgent": false
|
92
|
+
}
|
93
|
+
>> puts workspaces[0].name
|
94
|
+
1 Browse
|
95
|
+
>> puts workspaces[0].rect.width
|
96
|
+
1920
|
97
|
+
```
|
98
|
+
|
99
|
+
### Subscribe
|
100
|
+
|
101
|
+
To be done (not implemented yet)
|
102
|
+
|
103
|
+
### Outputs
|
104
|
+
|
105
|
+
Reply consists of a list of outputs:
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
>> outputs = i3.outputs
|
109
|
+
>> puts oututs[0].name
|
110
|
+
LVDS1
|
111
|
+
```
|
112
|
+
|
113
|
+
### Tree
|
114
|
+
|
115
|
+
The reply consists information about i3 tree. Each node in the tree (representing one container) has some properties:
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
>> tree = i3.tree
|
119
|
+
>> puts tree.id
|
120
|
+
8214416
|
121
|
+
>> puts tree.nodes[0].name
|
122
|
+
VGA1
|
123
|
+
```
|
124
|
+
|
125
|
+
### Marks
|
126
|
+
|
127
|
+
Reply consists of a single array of string for each container that has a mark.
|
128
|
+
|
129
|
+
First we need to create some marks:
|
130
|
+
|
131
|
+
```ruby
|
132
|
+
>> i3.command('mark terminal; focus right; mark vim')
|
133
|
+
```
|
134
|
+
|
135
|
+
Then can get a list of available marks:
|
136
|
+
|
137
|
+
```ruby
|
138
|
+
>> puts i3.marks
|
139
|
+
terminal
|
140
|
+
vim
|
141
|
+
```
|
142
|
+
|
143
|
+
And use those marks:
|
144
|
+
|
145
|
+
```ruby
|
146
|
+
>> i3.command("focus right; [con_mark=\"terminal\"] focus")
|
147
|
+
```
|
148
|
+
|
149
|
+
### Bar config
|
150
|
+
|
151
|
+
```ruby
|
152
|
+
>> puts i3.bar_config
|
153
|
+
bar-0
|
154
|
+
```
|
155
|
+
|
156
|
+
### Version
|
157
|
+
|
158
|
+
Reply describes a current version of i3 windows manager:
|
159
|
+
|
160
|
+
```ruby
|
161
|
+
>> puts i3.version
|
162
|
+
{
|
163
|
+
"major": 4,
|
164
|
+
"minor": 10,
|
165
|
+
"patch": 2,
|
166
|
+
"human_readable": "4.10.2 (2015-04-16, branch \"4.10.2\")"
|
167
|
+
}
|
168
|
+
```
|
169
|
+
|
170
|
+
## Contributing
|
171
|
+
|
172
|
+
1. Fork it
|
173
|
+
1. Create your feature branch (`git checkout -b my-new-feature`)
|
174
|
+
1. Commit your changes (`git commit -am 'Add some feature'`)
|
175
|
+
1. Run tests (`bundle exec rspec`)
|
176
|
+
1. Push to the branch (`git push origin my-new-feature`)
|
177
|
+
1. Create a new Pull Request
|
data/Rakefile
ADDED
data/i3ipc.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'i3ipc/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'i3ipc'
|
8
|
+
spec.version = I3ipc::VERSION
|
9
|
+
spec.authors = ['Vitalii Elengaupt']
|
10
|
+
spec.email = ['velenhaupt@gmail.com']
|
11
|
+
spec.summary = 'Interprocess communication with i3 wm'
|
12
|
+
spec.description = <<-DESC
|
13
|
+
Implementation of interface for i3 tiling window manager.
|
14
|
+
Useful for example to remote-control i3 or to get various
|
15
|
+
information like the current workspace to implement an
|
16
|
+
external workspace bar etc. in Ruby language.
|
17
|
+
DESC
|
18
|
+
spec.homepage = 'https://github.com/veelenga/i3ipc-ruby'
|
19
|
+
spec.license = 'MIT'
|
20
|
+
|
21
|
+
spec.files = `git ls-files`.split($RS)
|
22
|
+
spec.executables = spec.files.grep(/^bin\//) { |f| File.basename(f) }
|
23
|
+
spec.test_files = spec.files.grep(/^spec\//)
|
24
|
+
spec.require_paths = ['lib']
|
25
|
+
|
26
|
+
spec.required_ruby_version = '>= 1.9.3'
|
27
|
+
|
28
|
+
spec.add_development_dependency 'bundler', '~> 1.7'
|
29
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
30
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
31
|
+
end
|
data/lib/i3ipc.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'i3ipc/protocol'
|
2
|
+
require 'i3ipc/reply'
|
3
|
+
|
4
|
+
module I3Ipc
|
5
|
+
# Entry point for communication with i3-ipc.
|
6
|
+
# Able to send/receive messages and convert
|
7
|
+
# responses.
|
8
|
+
#
|
9
|
+
# Usage example:
|
10
|
+
# con = Connection.new
|
11
|
+
# p con.version.human_readable # => 4.10.2 (2015-0...
|
12
|
+
# p con.command('focus left').success? # => true
|
13
|
+
# p con.workspaces[0].name # => 0 Term
|
14
|
+
# # ...
|
15
|
+
# con.close
|
16
|
+
class Connection
|
17
|
+
|
18
|
+
def initialize(connect = true)
|
19
|
+
@protocol = Protocol.new
|
20
|
+
connect && @protocol.connect
|
21
|
+
end
|
22
|
+
|
23
|
+
def connect
|
24
|
+
@protocol.connect
|
25
|
+
end
|
26
|
+
|
27
|
+
def disconnect
|
28
|
+
@protocol.disconnect
|
29
|
+
end
|
30
|
+
|
31
|
+
def command(cmds)
|
32
|
+
reply_for(0, cmds)
|
33
|
+
end
|
34
|
+
|
35
|
+
def workspaces
|
36
|
+
reply_for(1)
|
37
|
+
end
|
38
|
+
|
39
|
+
def outputs
|
40
|
+
reply_for(3)
|
41
|
+
end
|
42
|
+
|
43
|
+
def tree
|
44
|
+
reply_for(4)
|
45
|
+
end
|
46
|
+
|
47
|
+
def marks
|
48
|
+
reply_for(5)
|
49
|
+
end
|
50
|
+
|
51
|
+
def bar_config
|
52
|
+
reply_for(6)
|
53
|
+
end
|
54
|
+
|
55
|
+
def version
|
56
|
+
reply_for(7)
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def reply_for(type, message = nil)
|
62
|
+
@protocol.send(type, message)
|
63
|
+
Reply.parse(@protocol.receive type)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'socket'
|
2
|
+
|
3
|
+
module I3Ipc
|
4
|
+
# Communication interface with i3-ipc.
|
5
|
+
#
|
6
|
+
# Can connect to i3-ipc socket, disconnect, send and receive messages.
|
7
|
+
#
|
8
|
+
# Usage example:
|
9
|
+
# protocol = Protocol.new
|
10
|
+
# protocol.send(7)
|
11
|
+
# puts protocol.receive
|
12
|
+
# protocol.disconnect
|
13
|
+
#
|
14
|
+
# For i3-ipc interface details refer to https://i3wm.org/docs/ipc.html.
|
15
|
+
class Protocol
|
16
|
+
# Magic string for i3-ipc protocol to ensure the integrity of messages.
|
17
|
+
MAGIC_STRING = 'i3-ipc'
|
18
|
+
|
19
|
+
# Throws when received data with not expected magic string.
|
20
|
+
# Usually this means that protocol is not compatible with
|
21
|
+
# current i3-ipc version.
|
22
|
+
class WrongMagicString < RuntimeError
|
23
|
+
def initialize(magic_string)
|
24
|
+
@magic_string = magic_string
|
25
|
+
end
|
26
|
+
|
27
|
+
def message
|
28
|
+
%Q{Magic code expected '#{MAGIC_STRING}', but was '#{@magic_string}'}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Throws when received data with not expected type.
|
33
|
+
class WrongType < RuntimeError
|
34
|
+
def initialize(expected, actual)
|
35
|
+
@expected = expected
|
36
|
+
@actual = actual
|
37
|
+
end
|
38
|
+
|
39
|
+
def message
|
40
|
+
%Q{Message type expected '#{@expected}', but was '#{@actual}'}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Throws when protocol trying to do some action
|
45
|
+
# on non-connected channel.
|
46
|
+
class NotConnected < RuntimeError; end
|
47
|
+
|
48
|
+
def initialize(socketpath = nil)
|
49
|
+
@socketpath = socketpath ? socketpath : get_socketpath
|
50
|
+
end
|
51
|
+
|
52
|
+
# Connects to i3-ipc server socket using Socket::UNIXSocket.
|
53
|
+
# Does nothing if already connected.
|
54
|
+
def connect
|
55
|
+
@socket = UNIXSocket.new(@socketpath) unless @socket
|
56
|
+
end
|
57
|
+
|
58
|
+
# Disconnects from i3-ipc server socket.
|
59
|
+
# Does nothing if not connected.
|
60
|
+
def disconnect
|
61
|
+
@socket && @socket.close
|
62
|
+
@socket = nil
|
63
|
+
end
|
64
|
+
|
65
|
+
# Sends packed message to i3-ipc server socket.
|
66
|
+
#
|
67
|
+
# Throws:
|
68
|
+
# * NotConnected if protocol is not connected.
|
69
|
+
#
|
70
|
+
# +type+: type of the message.
|
71
|
+
# +payload+: payload of the message
|
72
|
+
def send(type, payload = nil)
|
73
|
+
check_connected
|
74
|
+
@socket.write(pack(type, payload))
|
75
|
+
end
|
76
|
+
|
77
|
+
# Receives message from i3-ipc server socket.
|
78
|
+
#
|
79
|
+
# Throws:
|
80
|
+
# * NotConnected if protocol is not connected.
|
81
|
+
# * WrongMagicString if got message with not expected magic string.
|
82
|
+
# * WrongType if got message with not expected magic type.
|
83
|
+
#
|
84
|
+
# +type+: expected type of the message.
|
85
|
+
def receive(type = nil)
|
86
|
+
check_connected
|
87
|
+
# length of "i3-ipc" + 4 bytes length + 4 bytes type
|
88
|
+
data = @socket.read 14
|
89
|
+
magic, len, recv_type = unpack_header(data)
|
90
|
+
|
91
|
+
raise WrongMagicString.new(magic) unless MAGIC_STRING.eql? magic
|
92
|
+
type && (raise WrongType.new(type, recv_type) unless type == recv_type)
|
93
|
+
|
94
|
+
@socket.read(len)
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
# Packs the message.
|
100
|
+
# A typical message looks like:
|
101
|
+
# <header><payload>
|
102
|
+
# where a header is:
|
103
|
+
# <magic string><message length><message type>
|
104
|
+
#
|
105
|
+
# +type+: type of the message
|
106
|
+
# +payload+: patload of the message
|
107
|
+
def pack(type, payload=nil)
|
108
|
+
size = payload ? payload.to_s.bytes.count : 0
|
109
|
+
msg = MAGIC_STRING + [size, type].pack("LL")
|
110
|
+
msg << payload.to_s if payload
|
111
|
+
msg
|
112
|
+
end
|
113
|
+
|
114
|
+
# Unpacks the header.
|
115
|
+
# A typical header looks like:
|
116
|
+
# <magic_string><message length><message type>
|
117
|
+
#
|
118
|
+
# +data+: data to be unpacked.
|
119
|
+
def unpack_header(data)
|
120
|
+
struct_header_len = MAGIC_STRING.size
|
121
|
+
magic_message = data[0, struct_header_len]
|
122
|
+
len, type = data[struct_header_len..-1].unpack("LL")
|
123
|
+
[magic_message, len, type]
|
124
|
+
end
|
125
|
+
|
126
|
+
def get_socketpath
|
127
|
+
path = `i3 --get-socketpath`.chomp!
|
128
|
+
raise 'Unable to get i3 socketpath' unless path
|
129
|
+
path
|
130
|
+
end
|
131
|
+
|
132
|
+
def check_connected
|
133
|
+
raise NotConnected unless @socket
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
data/lib/i3ipc/reply.rb
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module I3Ipc
|
4
|
+
# Wrapper for reply from i3-ipc.
|
5
|
+
#
|
6
|
+
# Represents response from i3 as object tree.
|
7
|
+
#
|
8
|
+
# Able to parse Numeric, String, TrueClass, FalseClass,
|
9
|
+
# Array, Hash from passed JSON string.
|
10
|
+
#
|
11
|
+
# For example:
|
12
|
+
# response = Reply.parse(
|
13
|
+
# %Q{
|
14
|
+
# {
|
15
|
+
# "name": "LVDS1",
|
16
|
+
# "active": true,
|
17
|
+
# "current_workspace": "4",
|
18
|
+
# "rect": {
|
19
|
+
# "x": 0,
|
20
|
+
# "y": 0,
|
21
|
+
# "width": 1280,
|
22
|
+
# "height": 800
|
23
|
+
# }
|
24
|
+
# }
|
25
|
+
# }
|
26
|
+
# )
|
27
|
+
#
|
28
|
+
# p response.name # => "LVDS1"
|
29
|
+
# p response.active # => true
|
30
|
+
# p response.rect.width # => 1280
|
31
|
+
# # ...
|
32
|
+
#
|
33
|
+
# response = Reply.parse(%Q{ {"data": [{"key1": true}, {"key2": false}]} })
|
34
|
+
# p response.data[0].key1 # => true
|
35
|
+
# p response.data[0].key2 # => false
|
36
|
+
class Reply
|
37
|
+
def initialize(data)
|
38
|
+
@data = data.dup
|
39
|
+
end
|
40
|
+
|
41
|
+
# Parses response from I3-ipc protocol.
|
42
|
+
#
|
43
|
+
# Returns Reply object with dynamically accessed values.
|
44
|
+
#
|
45
|
+
# +response+: string, that represents response from i3 in json format.
|
46
|
+
def self.parse(response)
|
47
|
+
parse_data JSON.parse(response)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns false if this reply represents and error
|
51
|
+
# from i3-ipc protocol. Otherwise returns true, which
|
52
|
+
# meens that request is successful and reply has some
|
53
|
+
# data.
|
54
|
+
def success?
|
55
|
+
not self.respond_to? :error
|
56
|
+
end
|
57
|
+
|
58
|
+
def method_missing(name, *args, &block)
|
59
|
+
if @data.include?(name)
|
60
|
+
raise ArgumentError.new('wrong number of arguments (%d for 0)' % args.length) if args.length > 0
|
61
|
+
return @data[name]
|
62
|
+
else
|
63
|
+
super
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def respond_to?(method_sym, include_private = false)
|
68
|
+
if @data.include?(method_sym)
|
69
|
+
true
|
70
|
+
else
|
71
|
+
super
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def to_s
|
76
|
+
JSON.pretty_generate(to_h)
|
77
|
+
end
|
78
|
+
|
79
|
+
def to_h
|
80
|
+
data = @data.dup
|
81
|
+
data.each do |k, v|
|
82
|
+
data[k] = Reply.unparse_data v
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def self.parse_data(data)
|
89
|
+
case data
|
90
|
+
when Numeric, String, TrueClass, FalseClass, NilClass
|
91
|
+
return data
|
92
|
+
when Array
|
93
|
+
return data.map {|v| parse_data(v)}
|
94
|
+
when Hash
|
95
|
+
data.each do |k, v|
|
96
|
+
data[k] = parse_data v
|
97
|
+
end
|
98
|
+
return Reply.new(Hash[data.map {|k, v| [k.to_sym, v]}])
|
99
|
+
else
|
100
|
+
raise "Unable to parse data of type #{data.class}"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def self.unparse_data(data)
|
105
|
+
case data
|
106
|
+
when Numeric, String, TrueClass, FalseClass, NilClass
|
107
|
+
data
|
108
|
+
when Reply
|
109
|
+
data.to_h
|
110
|
+
when Array
|
111
|
+
data.map! {|x| self.unparse_data(x)}
|
112
|
+
data
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
module I3Ipc
|
5
|
+
# Simple socket server that communicates with
|
6
|
+
# client simulating i3-ipc messages.
|
7
|
+
class I3MockServer
|
8
|
+
SOCKET_PATH = '/tmp/i3-mock-server.sock'
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
remove_sock
|
12
|
+
@server = UNIXServer.new(SOCKET_PATH)
|
13
|
+
end
|
14
|
+
|
15
|
+
def accept_client
|
16
|
+
@client = @server.accept_nonblock
|
17
|
+
self
|
18
|
+
rescue IO::WaitReadable, Errno::EINTR
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
|
22
|
+
def client_alive?
|
23
|
+
return false unless @client
|
24
|
+
@client.write 'hi'
|
25
|
+
true
|
26
|
+
rescue Errno::EPIPE
|
27
|
+
false
|
28
|
+
end
|
29
|
+
|
30
|
+
def receive(len)
|
31
|
+
raise 'Client not accepted yet' unless @client
|
32
|
+
@client.read(len)
|
33
|
+
end
|
34
|
+
|
35
|
+
def send(data)
|
36
|
+
raise 'Client not accepted yet' unless @client
|
37
|
+
@client.write(data)
|
38
|
+
end
|
39
|
+
|
40
|
+
def close_client
|
41
|
+
@client.close if @client
|
42
|
+
@client = nil
|
43
|
+
end
|
44
|
+
|
45
|
+
def close
|
46
|
+
close_client
|
47
|
+
@server.close unless @server.closed?
|
48
|
+
remove_sock
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def remove_sock
|
54
|
+
FileUtils.rm(SOCKET_PATH) if File.exist?(SOCKET_PATH)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module I3Ipc
|
4
|
+
describe Connection do
|
5
|
+
|
6
|
+
describe '#command' do
|
7
|
+
|
8
|
+
before(:all) do
|
9
|
+
class Connection
|
10
|
+
def initialize
|
11
|
+
@protocol = Protocol.new ''
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'returns Reply object' do
|
17
|
+
allow_any_instance_of(Protocol).to receive(:send)
|
18
|
+
allow_any_instance_of(Protocol).to receive(:receive)
|
19
|
+
.and_return("{}")
|
20
|
+
|
21
|
+
expect(Connection.new.command('cmd')).to be_a Reply
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'sends correct command and receives success response' do
|
25
|
+
allow_any_instance_of(Protocol).to receive(:send)
|
26
|
+
allow_any_instance_of(Protocol).to receive(:receive)
|
27
|
+
.and_return(%Q[{"success": true}])
|
28
|
+
|
29
|
+
connection = Connection.new
|
30
|
+
reply = connection.command('focus left')
|
31
|
+
expect(reply.success).to be true
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'sends incorrect command and receives error response' do
|
35
|
+
allow_any_instance_of(Protocol).to receive(:send)
|
36
|
+
allow_any_instance_of(Protocol).to receive(:receive)
|
37
|
+
.and_return(%Q[{"success": false, "error": "wrong command"}])
|
38
|
+
|
39
|
+
connection = Connection.new
|
40
|
+
reply = connection.command('my command')
|
41
|
+
expect(reply.success).to be false
|
42
|
+
expect(reply.error).to eql 'wrong command'
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module I3Ipc
|
4
|
+
describe Protocol do
|
5
|
+
|
6
|
+
before(:all) do
|
7
|
+
# open internal methods for verification needs
|
8
|
+
class Protocol
|
9
|
+
public :pack, :unpack_header
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
before(:each, :i3 => :simulate) do
|
14
|
+
@i3_srv = I3MockServer.new
|
15
|
+
subject.connect
|
16
|
+
@i3_srv.accept_client
|
17
|
+
end
|
18
|
+
|
19
|
+
after(:each, :i3 => :simulate) do
|
20
|
+
@i3_srv.close
|
21
|
+
subject.disconnect
|
22
|
+
end
|
23
|
+
|
24
|
+
subject { Protocol.new(I3MockServer::SOCKET_PATH) }
|
25
|
+
|
26
|
+
it 'has MAGIC_STRING string constant' do
|
27
|
+
expect(Protocol::MAGIC_STRING).to be_a String
|
28
|
+
end
|
29
|
+
|
30
|
+
describe '#connect' do
|
31
|
+
it 'fails to connect if server not running' do
|
32
|
+
expect { subject.connect }.to raise_error Errno::ENOENT
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'connects if server running', :i3 => :simulate do
|
36
|
+
expect(@i3_srv.client_alive?).to be true
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'reconnects if disconnected', :i3 => :simulate do
|
40
|
+
subject.disconnect
|
41
|
+
subject.connect
|
42
|
+
expect(@i3_srv.accept_client.client_alive?).to be true
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'does nothing if already connected', :i3 => :simulate do
|
46
|
+
subject.connect
|
47
|
+
expect(@i3_srv.accept_client).to be nil
|
48
|
+
expect(@i3_srv.client_alive?).to be true
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe '#disconnect', :i3 => :simulate do
|
53
|
+
it 'disconnects if connected' do
|
54
|
+
subject.disconnect
|
55
|
+
expect(@i3_srv.client_alive?).to be false
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'does nothing if aready disconnected' do
|
59
|
+
subject.disconnect
|
60
|
+
subject.disconnect
|
61
|
+
expect(@i3_srv.client_alive?).to be false
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe '#send', :i3 => :simulate do
|
66
|
+
let(:type) { 42 }
|
67
|
+
let(:message) { 'test_send' }
|
68
|
+
|
69
|
+
it 'is able to send packed type' do
|
70
|
+
subject.send(type)
|
71
|
+
|
72
|
+
data = @i3_srv.receive(14)
|
73
|
+
header = subject.unpack_header data
|
74
|
+
expect(header).to match_array [Protocol::MAGIC_STRING, 0, type]
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'is able to send packed type and payload' do
|
78
|
+
subject.send(type, message)
|
79
|
+
|
80
|
+
data = @i3_srv.receive(14)
|
81
|
+
header = subject.unpack_header data
|
82
|
+
expect(header).to match_array [Protocol::MAGIC_STRING, message.size, type]
|
83
|
+
|
84
|
+
data = @i3_srv.receive(header[1])
|
85
|
+
expect(data).to eq message
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'throws error if not connected' do
|
89
|
+
subject.disconnect
|
90
|
+
expect { subject.send(type) }.to raise_error
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
describe '#receive', :i3 => :simulate do
|
95
|
+
let(:type) { 40 }
|
96
|
+
let(:message) { 'test_receive' }
|
97
|
+
|
98
|
+
context 'if server send packed message' do
|
99
|
+
before (:each) do
|
100
|
+
data = subject.pack(type, message)
|
101
|
+
@i3_srv.send(data)
|
102
|
+
end
|
103
|
+
|
104
|
+
it 'returns unpacked message' do
|
105
|
+
recv_message = subject.receive
|
106
|
+
expect(recv_message).to eq(message)
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'throws WrongType if received type does not match expected one' do
|
110
|
+
expect { subject.receive(type + 1) }.to raise_error Protocol::WrongType
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
context 'if server send wrong message' do
|
115
|
+
before (:each) do
|
116
|
+
data = 'i4-ipc' + [message.size, type].pack("LL") + message
|
117
|
+
@i3_srv.send(data)
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'throws WrongMagicString' do
|
121
|
+
expect { subject.receive(type) }.to raise_error Protocol::WrongMagicString
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
it 'throws error if not connected' do
|
126
|
+
subject.disconnect
|
127
|
+
expect { subject.receive }.to raise_error Protocol::NotConnected
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module I3Ipc
|
4
|
+
describe Reply do
|
5
|
+
|
6
|
+
describe '.parse' do
|
7
|
+
context 'when valid JSON string passed' do
|
8
|
+
|
9
|
+
it 'properly parses boolean attributes' do
|
10
|
+
expect(Reply.parse('{"success": true}').success).to be true
|
11
|
+
expect(Reply.parse('{"success": false}').success).to be false
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'properly parses numeric attributes' do
|
15
|
+
reply = Reply.parse(%Q[{ "int": 2, "float": 4.2 }])
|
16
|
+
expect(reply.int).to eql 2
|
17
|
+
expect(reply.float).to eql 4.2
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'properly parses string attributes' do
|
21
|
+
expect(Reply.parse('{"output": "LVDS1"}').output).to eql "LVDS1"
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'properly parses array of hashes' do
|
25
|
+
reply = Reply.parse( %Q[{ "arr": [{"key1": true}, {"key2": false}] }])
|
26
|
+
expect(reply.arr[0].key1).to be true
|
27
|
+
expect(reply.arr[1].key2).to be false
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'properly parses sub-hashes' do
|
31
|
+
reply = Reply.parse(%Q[{ "ha":{ "key1": "val1", "key2": "val2"} }])
|
32
|
+
expect(reply.ha.key1).to eql 'val1'
|
33
|
+
expect(reply.ha.key2).to eql 'val2'
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'properly parses sub-arrays' do
|
37
|
+
reply = Reply.parse(%Q[{ "arr": [[ 1, 2 ]] }])
|
38
|
+
expect(reply.arr[0]).to match_array [1, 2]
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'properly parses empty array' do
|
42
|
+
reply = Reply.parse(%Q[{"ar" : []}])
|
43
|
+
expect(reply.ar).to be_a Array
|
44
|
+
expect(reply.ar).to be_empty
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'property parses empty hash' do
|
48
|
+
reply = Reply.parse(%Q[{"ha": {}}]);
|
49
|
+
expect(reply.ha).to be_a Reply
|
50
|
+
expect(reply.ha.to_s).to eq "{\n}"
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'returns new Reply object' do
|
54
|
+
expect(Reply.parse('{}')).to be_a Reply
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
context 'when NOT valid JSON string passed' do
|
59
|
+
it 'raise JSON::ParserError' do
|
60
|
+
expect{Reply.parse(%Q[{"data": }])}.to raise_error JSON::ParserError
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe '#success?' do
|
66
|
+
it 'returns true if response without error' do
|
67
|
+
reply = Reply.parse(%Q[{}])
|
68
|
+
expect(reply.success?).to be true
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'returns false if response with error' do
|
72
|
+
reply = Reply.parse(%Q[{"error": "Wrong command"}])
|
73
|
+
expect(reply.success?).to be false
|
74
|
+
expect(reply.error).to eql 'Wrong command'
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe '#to_h' do
|
79
|
+
it 'converts it back to hash' do
|
80
|
+
hash = {:f => 1, :a => 2, :inner => {:ar => [true, false, "v"]}}
|
81
|
+
reply = Reply.parse(hash.to_json)
|
82
|
+
expect(reply.to_h). to eql hash
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
describe '#to_s' do
|
87
|
+
it 'returns property formatter structure with hash, arrays and primitives' do
|
88
|
+
reply = Reply.parse(%Q[{"d":{"1":[{"k":"v"}]} }])
|
89
|
+
expect(reply.to_s).to eql "{\n \"d\": {\n \"1\": [\n {\n \"k\": \"v\"\n }\n ]\n }\n}"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
describe '#method_missing' do
|
94
|
+
it 'returns value with dynamic method based on input data' do
|
95
|
+
expect(Reply.new({:meth => 'val'}).meth).to eql 'val'
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'throws ArgumentError if one or more parameters passed to dynamic method' do
|
99
|
+
expect{ Reply.new({:meth => 'val'}).meth(0) }.to raise_error ArgumentError
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'still throws NoMethodError if not existed method called' do
|
103
|
+
expect{ Reply.new({:meth => 'val'}).no_such_method }.to raise_error NoMethodError
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
describe '#respond_to' do
|
108
|
+
it 'responds to method from input data' do
|
109
|
+
expect(Reply.new({:meth => 'val'})).to respond_to :meth
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'still does not respond to not existed methods' do
|
113
|
+
expect(Reply.new({:meth => 'val'})).not_to respond_to :no_such_method
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: i3ipc
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Vitalii Elengaupt
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-05-23 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.7'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.7'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
55
|
+
description: |2
|
56
|
+
Implementation of interface for i3 tiling window manager.
|
57
|
+
Useful for example to remote-control i3 or to get various
|
58
|
+
information like the current workspace to implement an
|
59
|
+
external workspace bar etc. in Ruby language.
|
60
|
+
email:
|
61
|
+
- velenhaupt@gmail.com
|
62
|
+
executables: []
|
63
|
+
extensions: []
|
64
|
+
extra_rdoc_files: []
|
65
|
+
files:
|
66
|
+
- ".gitignore"
|
67
|
+
- ".travis.yml"
|
68
|
+
- ".yardopts"
|
69
|
+
- Gemfile
|
70
|
+
- LICENSE.txt
|
71
|
+
- README.md
|
72
|
+
- Rakefile
|
73
|
+
- i3ipc.gemspec
|
74
|
+
- lib/i3ipc.rb
|
75
|
+
- lib/i3ipc/connection.rb
|
76
|
+
- lib/i3ipc/protocol.rb
|
77
|
+
- lib/i3ipc/reply.rb
|
78
|
+
- lib/i3ipc/version.rb
|
79
|
+
- spec/i3_mock_server.rb
|
80
|
+
- spec/i3ipc/connection_spec.rb
|
81
|
+
- spec/i3ipc/protocol_spec.rb
|
82
|
+
- spec/i3ipc/reply_spec.rb
|
83
|
+
- spec/spec_helper.rb
|
84
|
+
homepage: https://github.com/veelenga/i3ipc-ruby
|
85
|
+
licenses:
|
86
|
+
- MIT
|
87
|
+
metadata: {}
|
88
|
+
post_install_message:
|
89
|
+
rdoc_options: []
|
90
|
+
require_paths:
|
91
|
+
- lib
|
92
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 1.9.3
|
97
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - ">="
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
requirements: []
|
103
|
+
rubyforge_project:
|
104
|
+
rubygems_version: 2.4.6
|
105
|
+
signing_key:
|
106
|
+
specification_version: 4
|
107
|
+
summary: Interprocess communication with i3 wm
|
108
|
+
test_files:
|
109
|
+
- spec/i3_mock_server.rb
|
110
|
+
- spec/i3ipc/connection_spec.rb
|
111
|
+
- spec/i3ipc/protocol_spec.rb
|
112
|
+
- spec/i3ipc/reply_spec.rb
|
113
|
+
- spec/spec_helper.rb
|
114
|
+
has_rdoc:
|