Skip to main content

Page Structure

SSH Portfolio has a fixed layout structure with four main pages:
ui/model.go
type Page int

const (
    PageHome Page = iota
    PageSkills
    PageProjects
    PageContact
)

var pageNames = []string{"Home", "Skills", "Projects", "Contact"}
Each page is a separate view that occupies the full content area.

Layout Components

The UI is divided into three main sections:
ui/model.go
const (
    tabBarHeight   = 3
    statusBarHeight = 1
)

1. Tab Bar (Top)

The tab bar shows navigation between pages:
┌──────────────────────────────────────────────┐
│                                              │
│  Home   Skills   Projects   Contact          │
│──────────────────────────────────────────────│
Height: 3 lines (tabs + border) The active tab is highlighted in blue, inactive tabs in gray:
ui/model.go
// Active tab
tab := r.NewStyle().
    Foreground(lipgloss.Color(Base)).
    Background(lipgloss.Color(Blue)).
    Bold(true).
    Padding(0, 2).
    Render(name)

// Inactive tab
tab := r.NewStyle().
    Foreground(lipgloss.Color(Subtext0)).
    Background(lipgloss.Color(Surface0)).
    Padding(0, 2).
    Render(name)

2. Content Area (Middle)

The main content area displays the current page:
ui/model.go
func (m Model) contentHeight() int {
    h := m.height - tabBarHeight - statusBarHeight
    if h < 1 {
        return 1
    }
    return h
}

func (m Model) contentWidth() int {
    return m.width
}
Height: Terminal height - 4 (3 for tab bar + 1 for status bar) Width: Full terminal width

3. Status Bar (Bottom)

The status bar shows keyboard shortcuts:
└──────────────────────────────────────────────┘
  ← → pages  │  ↑ ↓ select  │  q quit
Height: 1 line Hints change based on the active page:
ui/model.go
hints := "  ← → pages"
switch m.activePage {
case PageSkills, PageProjects:
    hints += "  │  ↑ ↓ select"
}
hints += "  │  q quit"
Move between pages using arrow keys or vim keys:
ui/model.go
case tea.KeyMsg:
    switch msg.String() {
    case "left", "h":
        if m.activePage > PageHome {
            m.activePage--
        }
        return m, nil
    case "right", "l":
        if m.activePage < PageContact {
            m.activePage++
        }
        return m, nil
    }
  • or h: Previous page
  • or l: Next page
  • Cannot go left from Home or right from Contact

Vertical Navigation (Up/Down)

Skills and Projects pages support vertical scrolling:
ui/skills.go
case tea.KeyMsg:
    switch msg.String() {
    case "up", "k":
        if m.cursor > 0 {
            m.cursor--
        }
    case "down", "j":
        if m.cursor < len(m.cfg.Skills)-1 {
            m.cursor++
        }
    }
  • or k: Previous item
  • or j: Next item

Quit

ui/model.go
case "ctrl+c", "q":
    return m, tea.Quit
  • q: Quit application
  • Ctrl+C: Force quit

Page Layouts

Home Page (Centered)

The home page centers all content vertically and horizontally:
ui/home.go
func (m HomeModel) View(width, height int, r *lipgloss.Renderer) string {
    // ... create art, title, intro ...
    
    content := lipgloss.JoinVertical(lipgloss.Center, art, title, intro)
    return lipgloss.Place(width, height, lipgloss.Center, lipgloss.Center, content)
}
Layout:
┌────────────────────────────┐
│                            │
│         ASCII ART          │
│                            │
│         Your Title         │
│                            │
│      Your intro text       │
│                            │
└────────────────────────────┘

Skills Page (Two-Column)

The skills page uses a two-column layout with a divider:
ui/skills.go
leftWidth := width*3/10 - 2
rightWidth := width - leftWidth - 5

return lipgloss.JoinHorizontal(lipgloss.Top, leftPane, divider, rightPane)
Layout:
┌─────────────┬──────────────────────┐
│ Languages   │ Languages            │
│▸Frontend    │ ──────────────       │
│ Backend     │ ● React / Next.js    │
│ DevOps      │ ● Electron           │
│ Tools       │ ● Material UI        │
│             │                      │
└─────────────┴──────────────────────┘
Left column: 30% width (category list) Right column: 70% width (skill details) Divider: Single vertical bar ()

Projects Page (Two-Column)

Similar to skills, projects use a two-column layout:
ui/projects.go
leftWidth := width*3/10 - 2
rightWidth := width - leftWidth - 5

return lipgloss.JoinHorizontal(lipgloss.Top, leftPane, divider, rightPane)
Layout:
┌─────────────┬──────────────────────┐
│▸2ndLock     │ 2ndLock              │
│ LogUI       │ ──────────────       │
│ Hjälp       │ Cross-platform...    │
│ Zexcore SDK │                      │
│             │ ⚡ NodeJS · React    │
│             │ 🔗 https://...       │
└─────────────┴──────────────────────┘
Left column: 30% width (project list) Right column: 70% width (project details)

Contact Page (Centered Card)

The contact page centers a bordered card:
ui/contact.go
card := r.NewStyle().
    Border(lipgloss.RoundedBorder()).
    BorderForeground(lipgloss.Color(Surface1)).
    Padding(2, 4).
    Render(content)

return lipgloss.Place(width, height, lipgloss.Center, lipgloss.Center, card)
Layout:
┌────────────────────────────┐
│                            │
│   ╭────────────────────╮   │
│   │  Get In Touch      │   │
│   │                    │   │
│   │ ✉  Email:  ...     │   │
│   │   GitHub:  ...     │   │
│   ╰────────────────────╯   │
│                            │
└────────────────────────────┘

Terminal Size Adaptation

The UI adapts to different terminal sizes:

Window Size Changes

ui/model.go
case tea.WindowSizeMsg:
    m.width = msg.Width
    m.height = msg.Height
    return m, nil
When the terminal is resized, the UI automatically recalculates:
  • Content width and height
  • Tab bar fill spacing
  • Status bar width
  • Text wrapping and alignment

Minimum Size Handling

ui/model.go
func (m Model) View() string {
    if m.width == 0 || m.height == 0 {
        return ""
    }
    // ... render UI
}

func (m Model) contentHeight() int {
    h := m.height - tabBarHeight - statusBarHeight
    if h < 1 {
        return 1
    }
    return h
}
The UI ensures minimum dimensions to prevent rendering errors.
  • Width: 80 columns (minimum), 100+ recommended
  • Height: 24 rows (minimum), 30+ recommended
Test your layout in small terminals:
# Set terminal to minimum size
stty rows 24 cols 80
ssh localhost -p 2222

Content Rendering

Each page receives the content dimensions and renders accordingly:
ui/model.go
cw, ch := m.contentWidth(), m.contentHeight()
switch m.activePage {
case PageHome:
    content = m.home.View(cw, ch, m.renderer)
case PageSkills:
    content = m.skills.View(cw, ch, m.renderer)
case PageProjects:
    content = m.projects.View(cw, ch, m.renderer)
case PageContact:
    content = m.contact.View(cw, ch, m.renderer)
}

// Ensure content fills available height
contentStyle := m.renderer.NewStyle().
    Width(m.width).
    Height(ch)
content = contentStyle.Render(content)
The content is styled to fill the exact available space, ensuring consistent layout.

Customizing Layout

Changing Column Widths

Modify the width calculations in ui/skills.go or ui/projects.go:
// Default: 30% left, 70% right
leftWidth := width*3/10 - 2
rightWidth := width - leftWidth - 5

// Example: 40% left, 60% right
leftWidth := width*4/10 - 2
rightWidth := width - leftWidth - 5

Adjusting Padding

Change padding values in style definitions:
leftPane := r.NewStyle().
    Width(leftWidth).
    Height(height - 2).
    Padding(1, 1).  // Vertical, Horizontal padding
    Render(content)

Modifying Borders

Change border styles:
// Rounded border (default for contact)
Border(lipgloss.RoundedBorder())

// Other options:
Border(lipgloss.NormalBorder())
Border(lipgloss.ThickBorder())
Border(lipgloss.DoubleBorder())
Border(lipgloss.HiddenBorder())

Adding Pages

To add a new page:
  1. Update the Page enum in ui/model.go:
const (
    PageHome Page = iota
    PageSkills
    PageProjects
    PageBlog      // New page
    PageContact
)

var pageNames = []string{"Home", "Skills", "Projects", "Blog", "Contact"}
  1. Create a new model file (e.g., ui/blog.go)
  2. Add the model to Model struct
  3. Update navigation logic and view rendering
  4. Rebuild the application

Best Practices

  1. Test multiple sizes: Always test your layout in various terminal sizes
  2. Keep content readable: Ensure sufficient width for text content
  3. Respect boundaries: Don’t exceed content width/height
  4. Use consistent spacing: Maintain padding consistency across pages
  5. Handle edge cases: Test with very small terminals (80x24)
  6. Consider accessibility: Use clear visual hierarchy and readable text

Build docs developers (and LLMs) love