Module: Ace::Tmux::Atoms::LayoutStringBuilder

Defined in:
lib/ace/tmux/atoms/layout_string_builder.rb

Overview

Pure function that builds a tmux custom layout string from a LayoutNode tree.

Takes a tree of LayoutNode objects, window dimensions, and pane IDs, then produces a tmux layout string like:

"a]b4,200x50,0,0{80x50,0,0,0,119x50,81,0[119x25,81,0,1,119x24,81,26,2]}"

tmux layout string format:

- `{...}` = horizontal split (children side by side)
- `[...]` = vertical split (children stacked)
- Leaf: `WxH,xoff,yoff,pane_id`
- Container: `WxH,xoff,yoff{child1,child2,...}` or `WxH,xoff,yoff[child1,child2,...]`

Class Method Summary collapse

Class Method Details

.allocate_sizes(children, total:) ⇒ Array<Integer>

Allocate cell sizes for children along the split axis.

Handles explicit sizes (percentage or absolute) and distributes remaining space evenly among auto-sized children. Accounts for 1-cell separators between adjacent panes.

Parameters:

  • children (Array<Models::LayoutNode>)

    Child nodes

  • total (Integer)

    Total cells available along the split axis

Returns:

  • (Array<Integer>)

    Cell sizes for each child



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/ace/tmux/atoms/layout_string_builder.rb', line 79

def allocate_sizes(children, total:)
  separator_count = children.length - 1
  available = total - separator_count

  sizes = children.map { |child| parse_size(child.size, available) }

  # Distribute remaining space among auto-sized children
  claimed = sizes.compact.sum
  auto_count = sizes.count(&:nil?)

  if auto_count > 0
    remaining = available - claimed
    base = remaining / auto_count
    extra = remaining % auto_count

    auto_index = 0
    sizes = sizes.map do |s|
      next s unless s.nil?

      # Give extra cells to the first auto children
      cell_size = base + ((auto_index < extra) ? 1 : 0)
      auto_index += 1
      cell_size
    end
  end

  sizes
end

.brackets_for(direction) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



138
139
140
# File 'lib/ace/tmux/atoms/layout_string_builder.rb', line 138

def brackets_for(direction)
  (direction == :horizontal) ? ["{", "}"] : ["[", "]"]
end

.build(root, width:, height:, pane_ids:) ⇒ String

Build a complete tmux layout string with checksum.

Parameters:

  • root (Models::LayoutNode)

    Root of the layout tree

  • width (Integer)

    Window width in cells

  • height (Integer)

    Window height in cells

  • pane_ids (Array<Integer>)

    Pane IDs in DFS leaf order

Returns:

  • (String)

    Complete tmux layout string with checksum



27
28
29
30
31
32
33
34
35
36
# File 'lib/ace/tmux/atoms/layout_string_builder.rb', line 27

def build(root, width:, height:, pane_ids:)
  # Assign pane IDs to leaves in DFS order
  # Fall back to sequential index if pane_ids is shorter than leaves
  leaves = root.leaves
  leaves.each_with_index { |leaf, i| leaf.pane_id = pane_ids[i] || i }

  body = generate_node(root, x: 0, y: 0, width: width, height: height)
  checksum = layout_checksum(body)
  "#{checksum},#{body}"
end

.child_dimensions(direction, parent_w, parent_h, size) ⇒ Object



153
154
155
156
157
158
159
# File 'lib/ace/tmux/atoms/layout_string_builder.rb', line 153

def child_dimensions(direction, parent_w, parent_h, size)
  if direction == :horizontal
    [size, parent_h]
  else
    [parent_w, size]
  end
end

.child_offsets(direction, parent_x, parent_y, offset) ⇒ Object



162
163
164
165
166
167
168
# File 'lib/ace/tmux/atoms/layout_string_builder.rb', line 162

def child_offsets(direction, parent_x, parent_y, offset)
  if direction == :horizontal
    [offset, parent_y]
  else
    [parent_x, offset]
  end
end

.generate_node(node, x:, y:, width:, height:) ⇒ String

Recursively generate the layout string for a node.

Parameters:

  • node (Models::LayoutNode)

    Current node

  • x (Integer)

    X offset

  • y (Integer)

    Y offset

  • width (Integer)

    Available width

  • height (Integer)

    Available height

Returns:

  • (String)

    Layout string fragment



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/ace/tmux/atoms/layout_string_builder.rb', line 46

def generate_node(node, x:, y:, width:, height:)
  if node.leaf?
    "#{width}x#{height},#{x},#{y},#{node.pane_id}"
  else
    sizes = allocate_sizes(node.children, total: split_dimension(node.direction, width, height))
    open_bracket, close_bracket = brackets_for(node.direction)

    child_strings = []
    offset = split_offset(node.direction, x, y)

    node.children.each_with_index do |child, i|
      child_w, child_h = child_dimensions(node.direction, width, height, sizes[i])
      child_x, child_y = child_offsets(node.direction, x, y, offset)

      child_strings << generate_node(child, x: child_x, y: child_y, width: child_w, height: child_h)

      # Advance offset past this child + 1-cell separator (except after last)
      offset += sizes[i] + 1
    end

    "#{width}x#{height},#{x},#{y}#{open_bracket}#{child_strings.join(",")}#{close_bracket}"
  end
end

.layout_checksum(str) ⇒ String

Compute tmux layout checksum (CRC16 variant: rotate-right-and-add).

Parameters:

  • str (String)

    Layout string body (without checksum prefix)

Returns:

  • (String)

    4-digit hex checksum



128
129
130
131
132
133
134
135
# File 'lib/ace/tmux/atoms/layout_string_builder.rb', line 128

def layout_checksum(str)
  csum = 0
  str.each_byte do |byte|
    csum = ((csum >> 1) | ((csum & 1) << 15)) + byte
    csum &= 0xffff
  end
  format("%04x", csum)
end

.parse_size(size, total) ⇒ Integer?

Parse an explicit size value into cells.

Parameters:

  • size (String, nil)

    Size string (e.g., “40%”, “80”) or nil

  • total (Integer)

    Total available cells (for percentage calculation)

Returns:

  • (Integer, nil)

    Size in cells, or nil if auto



113
114
115
116
117
118
119
120
121
122
# File 'lib/ace/tmux/atoms/layout_string_builder.rb', line 113

def parse_size(size, total)
  return nil if size.nil?

  size = size.to_s
  if size.end_with?("%")
    (total * size.to_f / 100).round
  else
    size.to_i
  end
end

.split_dimension(direction, width, height) ⇒ Object



143
144
145
# File 'lib/ace/tmux/atoms/layout_string_builder.rb', line 143

def split_dimension(direction, width, height)
  (direction == :horizontal) ? width : height
end

.split_offset(direction, x, y) ⇒ Object



148
149
150
# File 'lib/ace/tmux/atoms/layout_string_builder.rb', line 148

def split_offset(direction, x, y)
  (direction == :horizontal) ? x : y
end