i3ipc 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/.travis.yml ADDED
@@ -0,0 +1,9 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ - 2.1
6
+ - 2.2
7
+
8
+ script:
9
+ - bundle exec rspec
data/.yardopts ADDED
@@ -0,0 +1,2 @@
1
+ --markup markdown
2
+ --hide-void-return
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in i3ipc.gemspec
4
+ gemspec
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
@@ -0,0 +1,7 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: [:spec]
7
+
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,4 @@
1
+ require 'i3ipc/version'
2
+ require 'i3ipc/protocol'
3
+ require 'i3ipc/reply'
4
+ require 'i3ipc/connection'
@@ -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
@@ -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,3 @@
1
+ module I3ipc
2
+ VERSION = "0.1.0"
3
+ 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
@@ -0,0 +1,8 @@
1
+ require 'i3ipc'
2
+
3
+ require 'i3ipc/protocol'
4
+ require 'i3ipc/reply'
5
+ require 'i3ipc/connection'
6
+
7
+ require 'i3_mock_server'
8
+
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: