Skip to content

Instantly share code, notes, and snippets.

@sczizzo
Created June 9, 2017 03:12
Show Gist options
  • Save sczizzo/4493609c96090d237756e8d22df431f9 to your computer and use it in GitHub Desktop.
Save sczizzo/4493609c96090d237756e8d22df431f9 to your computer and use it in GitHub Desktop.
Sketching out a tmux layout utility
+------------+
| | 2 |
| |--------|
| 1 | 3 |4 |
| |--------|
| | 5 |
+------------+
+------------+
| 6 | 0 |
|------------|
| | 9 |
| 8 |-------|
| | 7 |
+------------+
+--------+
| | | |
+--------+
0: echo "hi mom"
1: vim muxt.rb
2: t
3: ls -la
4: date
5: htop
6: cal
7: uname -a
8: apt
9: cat example.muxt
#!/usr/bin/env ruby
require 'ostruct'
require 'shellwords'
# Pane P may have up to one dominant edge and
# subordinate edge. Only the eastern and
# southern edges may be marked as such. The
# longer of the two is dominant. If neither
# is longer, the eastern edge prevails.
#
# init: current pane P is top left of window
#
# loop:
# find dominant, subordinate edges Ed, Es of P
#
# if Ed and Ed has not been drawn
# draw Ed
# set P to neighbor over Ed
# next
#
# if Es and Es has not been drawn
# draw Es
# set P to neighbor over Es
# next
#
# if northern neighbor N
# flip north
# set P to N
# next
#
# if western neighbor W
# flip west
# set P to W
# next
#
# break
#
class Window
attr_reader :panes, :edges, :grid, :term, :tmux_window
def initialize(commands, session, idx, panes, edges, grid, term)
@commands = {}
commands.each { |c| @commands[c.label] = c.command }
@panes = {}
panes.each { |p| @panes[p.idx] = p }
@edges = {}
edges.each { |e| @edges[e.idx] = e }
@grid = grid
@term = term
@drawn = {}
if session.nil?
session = tmux('display-message', '-p', '#{session_name}') + ':'
index = tmux 'display-message', '-p', '#{window_index}'
idx = idx + index.to_i
end
@tmux_window = tmux 'new-window', '-d', '-P', '-k', '-t', "#{session}#{idx+1}"
@tmux_panes = {}
create
end
private
attr_reader :commands, :tmux_panes
def create
current = panes.find { |_, p| p.row.zero? && p.col.zero? }.last
loop do
dominant_edge = find_dominant_edge(current)
if dominant_edge && !drawn?(dominant_edge)
next_pane = pane_over(dominant_edge, current)
draw(:dominant, dominant_edge, current, next_pane)
current = next_pane
next
end
subordinate_edge = find_subordinate_edge(current, dominant_edge)
if subordinate_edge && !drawn?(subordinate_edge)
next_pane = pane_over(subordinate_edge, current)
draw(:subordinate, subordinate_edge, current, next_pane)
current = next_pane
next
end
if north_pane = northern_neighbor(current)
select('-U', current, north_pane)
current = north_pane
next
end
if west_pane = western_neighbor(current)
select('-L', current, west_pane)
current = west_pane
next
end
break
end
run_commands
end
def run_commands
panes.each do |idx, pane|
if command = commands[pane.label]
tmux 'send-keys', '-t', tmux_pane(pane), 'C-l'
tmux 'send-keys', '-t', tmux_pane(pane), command, 'C-m'
end
end
end
def tmux_pane(pane)
@tmux_panes.fetch(pane.idx, tmux_window)
end
def select(dir, pane, next_pane)
resize(pane)
tmux 'select-pane', dir, '-t', tmux_pane(pane)
resize(next_pane)
end
def resize(pane)
height = scale(pane.height, :height).to_s
width = scale(pane.width, :width).to_s
tmux 'resize-pane', '-y', height, '-x', width, '-t', tmux_pane(pane)
end
def drawn?(edge)
!!@drawn[edge.idx]
end
def draw(kind, edge, pane, next_pane)
raise if @drawn[edge.idx]
@drawn[edge.idx] = true
dir = edge.orientation == :vertical ? '-h' : '-v'
tp = tmux 'split-window', '-P', dir, '-t', tmux_pane(pane)
@tmux_panes[next_pane.idx] = tp
end
def scale(num, kind)
grid_size = grid.send(kind).to_f
term_size = term.send(kind).to_f
out = num.to_f * (term_size / grid_size)
out.round
end
def find_dominant_edge(pane)
east_edge = edges[pane.east]
south = if south_edge = edges[pane.south]
pane_over(south_edge, pane)
end
south && (south.width > pane.width) ? south_edge : east_edge
end
def find_subordinate_edge(pane, dominant_edge)
es = [edges[pane.east], edges[pane.south]]
es.delete(dominant_edge)
es.first
end
def pane_over(edge, pane)
other_panes = edge.
neighbors.
reject { |idx| idx == pane.idx }.
map { |idx| panes[idx] }
if edge.orientation == :vertical
other_panes.sort_by(&:row).first
else # horizontal
other_panes.sort_by(&:col).first
end
end
def northern_neighbor(pane)
if north_edge = edges[pane.north]
pane_over(north_edge, pane)
end
end
def western_neighbor(pane)
if west_edge = edges[pane.west]
pane_over(west_edge, pane)
end
end
end
def remove_frame(input)
width = nil
height = 0
grid = input.split("\n").map do |row|
height += 1
chars = row.split('')
width ||= chars.size
chars.map do |char|
char =~ /(\d| )/ ? ' ' : '.'
end
end
rmax = height - 1
cmax = width - 1
rmax.times do |i|
next if i == 0 || i == rmax
if grid[i][0] == '.' && grid[i][1] == ' '
grid[i][0] = ' '
end
end
rmax.times do |i|
next if i == 0 || i == rmax
if grid[i][cmax-1] == ' ' && grid[i][cmax] == '.'
grid[i][cmax] = ' '
end
end
width.times do |j|
if grid[0][j] == '.' && grid[1][j] == ' '
grid[0][j] = ' '
end
end
width.times do |j|
if grid[rmax-1][j] == ' ' && grid[rmax][j] == '.'
grid[rmax][j] = ' '
end
end
grid
end
def delineate_panes(grid)
height = grid.size
width = grid.first.size
seen = height.times.map do
width.times.map do
false
end
end
panes = []
idx = 0
(height-1).times do |i|
(width-1).times do |j|
next if seen[i][j]
seen[i][j] = true
if grid[i][j] == ' '
# find width
w = 0
(width-j).times do
jj = j + w
seen[i][jj] = true
break if grid[i][jj] == '.'
w += 1
end
# find height
h = 0
(height-i).times do
ii = i + h
seen[ii][j] = true
break if grid[ii][j] == '.'
h += 1
end
# Mark whole pane seen
h.times do |ii|
w.times do |jj|
seen[i+ii][j+jj] = true
end
end
panes << { idx: idx, row: i, col: j, width: w, height: h }
idx += 1
end
end
end
panes
end
def delineate_edges(grid)
height = grid.size
width = grid.first.size
idx = 0
edges = []
(width - 2).times do |x|
seen = height.times.map do
width.times.map do
false
end
end
height.times do |i|
j = x + 1
next if seen[i][j]
seen[i][j] = true
if grid[i][j] == '.' && (grid[i][j-1] == ' ' || grid[i][j+1] == ' ')
h = 1
(height-i-1).times do |y|
ii = y + i
seen[ii][j] = true
h += 1
break if grid[ii][j] == '.' && grid[ii+1][j] == ' '
end
edges << { idx: idx, orientation: :vertical, row: i, col: j, height: h }
idx += 1
end
end
end
(height - 2).times do |y|
seen = height.times.map do
width.times.map do
false
end
end
width.times do |j|
i = y + 1
next if seen[i][j]
seen[i][j] = true
if grid[i][j] == '.' && (grid[i][j-1] == '.' || grid[i][j+1] == '.')
w = 1
(width-j-1).times do |x|
jj = x + j
seen[i][jj] = true
w += 1
break if grid[i][jj] == '.' && grid[i][jj+1] == ' '
end
edges << { idx: idx, orientation: :horizontal, row: i, col: j, width: w }
idx += 1
end
end
end
edges
end
def make_associations(panes, edges)
panes.each do |pane|
edges.each do |edge|
edge[:neighbors] ||= []
edge[:len] ||= edge[:height] || edge[:width]
if edge[:orientation] == :vertical
at_left = edge[:col] == pane[:col] - 1
at_right = edge[:col] == pane[:col] + pane[:width]
at_height = pane[:row] >= edge[:row] && pane[:row] <= (edge[:row] + edge[:height])
next unless (at_left || at_right) && at_height
edge[:neighbors] << pane[:idx]
pane[:west] = edge[:idx] if at_left
pane[:east] = edge[:idx] if at_right
else # horizontal
at_top = edge[:row] == pane[:row] - 1
at_bottom = edge[:row] == pane[:row] + pane[:height]
at_width = pane[:col] >= edge[:col] && pane[:col] <= (edge[:col] + edge[:width])
next unless (at_top || at_bottom) && at_width
edge[:neighbors] << pane[:idx]
pane[:north] = edge[:idx] if at_top
pane[:south] = edge[:idx] if at_bottom
end
end
end
end
def read_labels(panes, input)
grid = input.split("\n").map { |r| r.split('') }
panes.each do |pane|
pane[:height].times do |y|
pane[:width].times do |x|
next if pane[:label]
i = pane[:row] + y
j = pane[:col] + x
c = grid[i][j]
pane[:label] = c if c =~ /\d/
end
end
end
end
begin
ARGF.seek(1)
ARGF.rewind
rescue
$stderr.puts 'Oopsie, no input provided!'
exit 1
end
def tmux(*cmd)
cmd = "tmux #{Shellwords.join(cmd)}"
out = `#{cmd}`.strip
puts "#{cmd} => #{out}"
out
end
session = ENV['TMUX'] ? nil : tmux('new-session', '-d', '-P')
commands = []
lines = ARGF.read.split("\n").map do |l|
if l =~ /^(\d):\s*(.*)$/
commands << OpenStruct.new(label: $1, command: $2)
nil
else
l
end
end
inputs = lines.compact.join("\n").split(/\n{2,}/).map(&:strip)
windows = inputs.each_with_index.map do |input, i|
grid = remove_frame(input)
panes = delineate_panes(grid)
edges = delineate_edges(grid)
read_labels(panes, input)
make_associations(panes, edges)
edges.map! { |edge| OpenStruct.new(edge) }
panes.map! { |pane| OpenStruct.new(pane) }
term_width = `tput cols`.strip.to_i
term_height = `tput lines`.strip.to_i
term_size = OpenStruct.new(width: 170, height: 50)
grid_size = OpenStruct.new(width: grid.first.size, height: grid.size)
Window.new(commands, session, i, panes, edges, grid_size, term_size)
end
tmux 'select-window', '-t', windows.first.tmux_window
tmux 'attach-session', '-t', session if session
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment