Created
June 9, 2017 03:12
-
-
Save sczizzo/4493609c96090d237756e8d22df431f9 to your computer and use it in GitHub Desktop.
Sketching out a tmux layout utility
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
+------------+ | |
| | 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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