Skip to content

Instantly share code, notes, and snippets.

@Quintus
Created April 30, 2012 18:43
Show Gist options
  • Save Quintus/2561002 to your computer and use it in GitHub Desktop.
Save Quintus/2561002 to your computer and use it in GitHub Desktop.
i3 workspaces
#!/usr/bin/env ruby
# -*- coding: utf-8 -*-
require "pp"
require "json"
require "socket"
module I3
class Error
end
class MsgError < Error
end
# Class representing i3 workspaces.
class Workspace
attr_reader :num
attr_reader :name
attr_reader :output
# Create a workspace from an i3 response. Internally used by Interface#workspaces.
def self.from_i3_hash(i3, hsh) # :nodoc:
ws = allocate
ws.instance_variable_set(:@i3, i3)
%w[num name visible focused urgent output].each do |str|
ws.instance_variable_set(:"@#{str}", hsh[str])
end
%w[x y width height].each do |str|
ws.instance_variable_set(:"@#{str}", hsh["rect"][str])
end
ws
end
def initialize(i3)
@i3 = i3
raise(NotImplementedError, "Not implemented yet")
end
def inspect
"#<#{self.class} #@num (#@name)>"
end
def visible?
@visible
end
def focused?
@focused
end
def urgent?
@urgent
end
end
class Interface
# The magic string marking each message as a i3
# message.
MAGIC_STRING = "i3-ipc".force_encoding("BINARY").freeze
# Map the human-readable command names to the
# identifiers i3 understands.
TYPE2INT = {
:command => 0,
:workspaces => 1,
:subscribe => 2,
:outputs => 3,
:tree => 4,
:marks => 5,
:bar_config => 6
}.freeze
# The inverse of MSG_TYPE2INT.
INT2TYPE = TYPE2INT.invert.freeze
# Establish a connection to i3.
# === Parameter
# [socket]
# (<tt>UNIXSocket.new(`i3 --get-socketpath`)</tt>)
# The socket i3 listens to. If not given, this
# method asks i3 for it’s socket path and creates
# a UNIX domain socket from it.
# === Return value
# Returns a new instance of this class.
def initialize(socket = nil)
@socket = socket ? socket : UNIXSocket.new(`i3 --get-socketpath`.chomp)
end
def command(cmd)
raw_message(:command, cmd)["success"]
end
# Get all workspaces i3 currently manages.
def workspaces
ary = raw_message(:workspaces)
ary.map{|hsh| Workspace.from_i3_hash(self, hsh)}
end
# Send a raw message to i3 and return i3’s reply.
#
# === Parameters
# [type] The type of the message, one of the keys of the
# TYPE2INT hash.
# [str] ("") The contents of the message, which may be
# empty if +type+ contains all necessary
# information.
# === Raises
# [ArgumentError] +type+ was invalid.
# [I3MsgError] i3’s answer was somehow invalid.
# === Return value
# The JSON stuff returned by i3, properly converted
# to a Ruby hash/array structure with all strings
# encoded in UTF-8.
def raw_message(type, str = "")
msg = format_i3_message(TYPE2INT[type], str)
@socket.write(msg)
# First read the magic string
mstr = @socket.read(MAGIC_STRING.bytes.count)
if mstr != MAGIC_STRING
raise(MsgError, "Invalid reply from i3! Expected '#{MAGIC_STRING}', got '#{mstr}'!")
end
# Now read two 32-bit integers, the length and the type
length, ident = @socket.read(8).unpack("LL")
reply_type = INT2TYPE[ident] || raise(I3MsgError, "Invalid reply type #{ident} from i3!")
if reply_type != type
raise(MsgError, "i3 answered with #{reply_type} to a #{type} message!")
end
# Finally read the message contents (i3 operates
# on UTF-8 strings, so we can force the encoding here).
content = @socket.read(length).force_encoding("UTF-8")
# i3 always answers with JSON. Return that.
JSON[content]
end
private
# Takes what you want to send to i3 and converts it
# to the binary blob i3 expects. +str+ is the message
# payload, i.e. its contents (which may be empty),
# +typeident+ is one of the type identifiers known
# to i3 (you can get a list of possible types numbers
# by printing out the INT2TYPE hash).
def format_i3_message(typeident, str = "")
cmd = MAGIC_STRING.dup
cmd << [str.bytes.count, typeident].pack("LL")
cmd << str.encode("UTF-8").force_encoding("BINARY")
cmd
end
end
end
if $0 == __FILE__
i3 = I3::Interface.new
i3.workspaces.each_with_index do |ws, i|
print(" | ") unless i.zero?
if ws.urgent?
print("¡#{ws.name}!")
elsif ws.focused?
print("[#{ws.name}]")
else
print(ws.name)
end
end
puts # Final newline
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment