i3ipc 0.1.0

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.
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: