Layouts

Layouts wrap screen views with shared UI such as sidebars, headers, footers, command palettes, and modal overlays.

Generated apps use Ruby layout classes and the declarative layout DSL:

class ApplicationController < Charming::Controller
  layout Layouts::ApplicationLayout
  focus_ring :sidebar, :content
end

That resolves:

app/views/layouts/application_layout.rb

ERB layouts remain available as a fallback with layout "layouts/application".

Layout Class

Layout classes inherit from Charming::View:

module MyApp
  module Layouts
    class ApplicationLayout < Charming::View
      def render
        screen_layout(background: theme.background) do
          split :horizontal, gap: 1 do
            pane(:sidebar, width: 24, border: :rounded, padding: [1, 2]) do
              navigation
            end

            pane(:content, grow: 1, border: :rounded, padding: [1, 2]) do
              yield_content
            end
          end
        end
      end

      private

      def navigation
        column("Home", "Settings", gap: 1)
      end
    end
  end
end

Layout Assigns

Layouts receive standard assigns:

Assign Purpose
content The already-rendered screen body.
screen Current terminal dimensions.
controller Current controller instance.
theme Active theme.

Any assigns passed to the screen view are also available to the layout.

Use yield_content to place the current screen inside the layout.

DSL Primitives

screen_layout creates a full-screen layout tree for the current terminal size.

screen_layout(background: theme.background) do
  split :horizontal, gap: 1 do
    pane(:sidebar, width: 24) { "Sidebar" }
    pane(:content, grow: 1) { |rect| "Content area: #{rect.width}x#{rect.height}" }
  end
end

Available primitives:

Primitive Purpose
screen_layout(background: nil) { ... } Render a full-screen layout using the current screen.
split(:horizontal, gap: n) { ... } Divide space into left-to-right panes.
split(:vertical, gap: n) { ... } Divide space into top-to-bottom panes.
pane(:name, **options) { ... } Render content into an assigned rectangle.
overlay(content, top: :center, left: :center) Draw content over the finished layout.

Pane sizing options:

Option Behavior
width: n Fixed outer width in a horizontal split.
height: n Fixed outer height in a vertical split.
grow: n Take remaining space, weighted by n.
min_width: n / max_width: n Clamp the computed width (horizontal splits).
min_height: n / max_height: n Clamp the computed height (vertical splits).
no size Equivalent to grow: 1 inside a split.

Constraints apply after grow distribution: a clamped pane’s surplus or deficit is re-balanced onto the remaining flexible children, so pane(:sidebar, width: 24, min_width: 18) keeps a usable sidebar on narrow terminals while grow panes absorb the difference.

Pane styling options:

Option Behavior
border: true Draw a normal border.
border: :rounded Draw a named border style.
padding: 1 Add equal padding on all sides.
padding: [1, 2] Add vertical and horizontal padding.
style: theme.title Apply a base style.
focus: true Include this pane in the layout’s Tab focus ring.
focused_style: theme.title Style the pane when it is focused. Defaults to theme.title.
clip: true Clip content to the pane. This is the default.
wrap: true Wrap long lines inside the pane.

Pane dimensions are outer dimensions. Borders and padding are included in the assigned width and height.

Pane blocks may accept a Charming::Layout::Rect argument. The yielded rect is the pane’s inner content area after border and padding are applied, so components can render to their actual available width and height:

pane(:content, grow: 1, border: :rounded, padding: [1, 2]) do |rect|
  render_component FeedList.new(width: rect.width, height: rect.height)
end

Focusable panes are opt-in. Named panes are not focusable unless focus: true is set:

screen_layout do
  split :horizontal, gap: 1 do
    pane(:files, width: 32, border: :rounded, focus: true) { files_panel }
    pane(:diff, grow: 1, border: :rounded, focus: true) { diff_panel }
  end
end

The first focusable pane starts focused. Tab cycles forward and Shift+Tab cycles backward. Command palettes and other modal scopes still take priority while open.

Screen Views

Screens are Ruby view classes by default:

module MyApp
  module Home
    class ShowView < Charming::View
      def render
        column(
          text(home.title, style: theme.title),
          text("Press ctrl+p for commands, q to quit.", style: theme.muted),
          gap: 1
        )
      end
    end
  end
end

The controller can still render by action name:

def show
  render :show, home: home, palette: command_palette
end

For HomeController, that resolves MyApp::Home::ShowView before falling back to ERB templates.

Responsive Layouts

Use normal Ruby methods and screen dimensions:

def render
  screen_layout do
    split(narrow? ? :vertical : :horizontal, gap: 1) do
      pane(:sidebar, **sidebar_options, border: :rounded, padding: [1, 2]) do
        navigation
      end

      pane(:content, grow: 1, border: :rounded, padding: [1, 2]) do
        yield_content
      end
    end
  end
end

private

def narrow?
  screen.narrow?(below: 72, min_height: 20)
end

def sidebar_options
  narrow? ? {height: [screen.height / 3, 5].max} : {width: 24}
end

Use overlay inside screen_layout for command palettes, dialogs, tooltips, or toasts:

def render
  screen_layout(background: theme.background) do
    split :horizontal, gap: 1 do
      pane(:sidebar, width: 24, border: :rounded) { navigation }
      pane(:content, grow: 1, border: :rounded) { yield_content }
    end

    overlay command_palette_modal if command_palette_modal
  end
end

private

def command_palette_modal
  return unless palette

  render_component Charming::Components::CommandPaletteModal.new(
    content: palette,
    theme: theme
  )
end

Overlays accept z_index: for stacking order — higher values composite on top, and ties keep registration order. Useful when a toast must float above an open modal:

overlay help_modal, z_index: 5 if help_modal
overlay toast, top: screen.height - 5, left: :center, z_index: 10 if toast

Status Bar

The classic bottom bar is a one-row pane wrapping the whole layout in an outer vertical split. (status_hints here is an app-defined controller method returning [key, description] pairs — each screen can override it.)

screen_layout(background: theme.background) do
  split :vertical do
    split :horizontal, gap: 1, grow: 1 do
      pane(:sidebar, width: 24, min_width: 18, border: :rounded) { navigation }
      pane(:content, grow: 1, border: :rounded) { yield_content }
    end

    pane(:status_bar, height: 1) do
      render_component Charming::Components::StatusBar.new(
        width: screen.width,
        left: " #{controller.route&.title}",
        hints: controller.status_hints,
        theme: theme
      )
    end
  end
end

Lower-Level Helpers

The DSL sits above the lower-level string helpers. Use these inside panes and screen views:

Helper Purpose
text(value, style: nil) Render styled text.
box(value, style: nil) Style or border a block manually.
row(*items, gap: 0) Join blocks side by side.
column(*items, gap: 0) Stack blocks vertically.
render_component(component) Render reusable components.

Use Charming::UI.place, center, and overlay only when you need lower-level canvas control.

Style Chaining

Charming::UI::Style objects are immutable and chainable:

panel_style = Charming::UI.style
  .foreground(:bright_cyan)
  .background("#101820")
  .bold
  .border(:rounded, foreground: :bright_magenta)
  .padding(1, 2)
  .width(40)
  .align(:center)

box("System ready", style: panel_style)

Common style methods:

Method Purpose
foreground(color) / fg(color) Set text color.
background(color) / bg(color) Set background color.
bold, faint, italic, underline, reverse, strikethrough Add text attributes.
padding(1), padding(1, 2), padding(1, 2, 1, 2) Add CSS-like padding.
border(:normal | :rounded | :thick | :double) Add a border.
border(..., sides: [:top, :bottom]) Draw only specific sides.
width(value) Set content width.
height(value) Set content height.
align(:left | :center | :right) Align content within width.

Colors can be named symbols (:cyan, :bright_white), 0-255 indexed colors, or truecolor hex strings ("#ff00aa").


This site uses Just the Docs, a documentation theme for Jekyll.