Skip to content

Instantly share code, notes, and snippets.

@quark-zju
Last active May 25, 2016 20:06
Show Gist options
  • Save quark-zju/ca7de775bc8584c5db90d326a53f5cfe to your computer and use it in GitHub Desktop.
Save quark-zju/ca7de775bc8584c5db90d326a53f5cfe to your computer and use it in GitHub Desktop.
i3-scriptable with auto-splitv script (emulating i3 3.x behavior)
# tl;dr Bring back most of the column behavior of i3 3.x / wmii, in i3 4.x
#
# Auto run "split v" on new (or moved) windows which is the child
# of the top-level container and has "splith" layout.
#
# For example,
#
# +---------+ +---------+ +---------+ +---------+ +----+----+
# | | |.........| | | | | | |....|
# | | 1 |.........| 2 | | 3 +---------+ 4 | |....|
# | | > |.........| > +---------+ > | | > +----+....|
# | | |.........| |.........| +---------+ | |....|
# | | |.........| |.........| |.........| | |....|
# +---------+ +=========+ +---------+ +---------+ +----+====+
#
# 1. Create a new window in an empty workspace, the window will have
# "split v" layout, regardless of "default_orientation" option.
# 2. Create a second window, no auto "split v" this time.
# 3. Create another one, works as expected.
# 4. Move the window right. It will have an auto "split v" since it is
# the direct child of the "split h" workspace container (new column).
#
# The difference with the column behavior is that moving a bottom window in
# a column down will implicitly create a "split v" parent container.
on_window_change ['new', 'move'] do |i3, reply|
node = reply.container
workspace = i3.workspace_of(node)
container = i3.inner_container_of(workspace)
parent = i3.parent_of(node)
if parent == container && parent.layout == 'splith'
i3.logger.info "split v for \"#{node.name}\""
i3.command "[con_id=#{node.id}] split v"
end
end
source 'http://rubygems.org'
gem 'i3ipc', '~> 0.2.0'
# Make i3 more flexible by executing custom scripts on window change.
# Support script live reloading.
#
# gem 'i3ipc', github: 'veelenga/i3ipc-ruby', ref: '3316cd03c3ff2a0f8d96c01cf7abef4e70d09a06'
require 'i3ipc'
require 'logger'
require 'forwardable'
class I3Script
attr_accessor :i3, :script_paths
extend Forwardable
def_delegators :@i3, :command, :workspaces, :outputs, :version, :bar_config
def initialize
self.i3 ||= I3Ipc::Connection.new
end
def tree
@tree ||= build_tree!
end
def parent_of(id)
get_tree_index(id, :parent)
end
def workspace_of(id)
get_tree_index(id, :workspace)
end
def focused
build_tree
@tree_focused
end
def windows
build_tree
@tree_windows
end
# get first container which has >= 2 nodes
def inner_container_of(node)
return nil if node.nil? || !node.respond_to?(:nodes)
if node.nodes.size == 1 && node.nodes.first.window.nil?
inner_container_of(node.nodes.first)
else
node
end
end
def run(script_paths)
self.script_paths = [*script_paths]
reload_scripts!
event_loop if !@on_window_change_handlers.empty?
end
def finalize
i3.close
self.i3 = nil
end
def logger
@logger ||= Logger.new(STDERR).tap do |l|
l.level = (ENV['LOG'] || Logger::INFO).to_i
end
end
def on_window_change(change = nil, &cb)
@on_window_change_handlers << {change: change, callback: cb} if cb
end
# human-friendly string for an i3 node
def inspect_node(node = tree, recursive = true, indent = 0)
result = "#{' ' * indent}#{node.id} \"#{node.name}\" layout=#{node.layout} type=#{node.type} window=#{node.window}"
node.nodes.each {|n| result += "\n" + inspect_node(n, true, indent + 1)} if recursive
result
end
private
def event_loop
# Use a new connection for window change event handling
I3Ipc::Connection.new.tap do |i|
pid = i.subscribe('window', self.method(:run_window_change_handlers))
pid.join
i.close
end
end
def reload_scripts!
@on_window_change_handlers = []
@script_mtimes = {}
script_paths.each do |path|
logger.debug "Loading #{path}"
@script_mtimes[path] = File.mtime(path)
binding.eval File.read(path), path
end
end
def reload_scripts
if script_paths.any? { |path| !File.exists?(path) || @script_mtimes[path] != File.mtime(path) }
logger.debug "Script change detected. Reloading."
reload_scripts!
end
end
def run_window_change_handlers reply
expire_tree!
reload_scripts
logger.debug "Handling #{reply.change} event"
@on_window_change_handlers.each do |h|
begin
if h[:change].nil? || [*h[:change]].include?(reply.change)
h[:callback][self, reply]
end
rescue => ex
logger.warn ex
end
end
end
def build_tree!
@tree = i3.tree
@tree_index = {}
@tree_windows = []
@tree_focused = nil
# make mappings not directly in the tree: id -> (parent, workspace)
dfs = proc do |node, parent, workspace|
@tree_index[node.id] = {parent: parent, node: node, workspace: workspace}
begin
@tree_focused = node if node.respond_to?(:focused) && node.focused
@tree_windows << node if node.respond_to?(:window) && node.window
workspace = node if node.type == 'workspace'
node.nodes.each {|n| dfs[n, node, workspace]}
node.floating_nodes.each {|n| dfs[n, node, workspace]}
rescue
end
end
dfs[@tree, nil]
@tree
end
def build_tree
build_tree! if @tree.nil?
end
def get_tree_index(id, field)
id = id.id if id.respond_to?(:id)
build_tree
info = @tree_index[id]
info && info[field]
end
def expire_tree!
@tree = @tree_index = @tree_focused = @tree_windows = @tree_focused = nil
end
end
I3Script.new.tap do |i3s|
begin
i3s.run ARGV
ensure
i3s.finalize
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment