| name | termstack-yaml-generator |
| description | This skill should be used when the user wants to create a TermStack TUI configuration, generate a YAML file for browsing APIs, create a terminal dashboard, or build a config-driven terminal UI. Use this when users mention TermStack, TUI configuration, API browser, terminal dashboard, or want to create YAML configs for data visualization in the terminal. |
| version | 2.0.0 |
TermStack YAML Configuration Generator
This skill helps generate TermStack YAML configuration files for creating terminal user interfaces (TUIs) that browse APIs, display data in tables, stream logs, and navigate between pages.
What is TermStack?
TermStack is a config-driven Terminal User Interface (TUI) framework. You define pages, data sources, views, and actions in YAML - no coding required. It supports:
- HTTP API calls with query parameters and headers
- CLI command execution with arguments
- Script execution for custom data processing
- Stream adapters for real-time data (logs, websocket)
- Table views with sortable, styled columns
- Text/YAML views for detailed data
- Logs views with filtering and search
- Multi-page navigation with context passing
- Conditional navigation based on data values
- Actions triggered by keyboard shortcuts
- Multiple data sources with merge capabilities
- Column transforms with Tera templates
- Validation rules for data integrity
YAML Configuration Structure
version: v1
app:
name: "App Name"
description: "App description"
theme: "default"
globals:
api_base: "https://api.example.com"
start: page_name
pages:
page_name:
title: "Page Title"
data:
adapter: http
url: "{{ api_base }}/endpoint"
method: GET
params:
key: "value"
headers:
Accept: "application/json"
items: "$.data[*]"
view:
type: table
columns:
- path: "$.field"
display: "Column Name"
width: 20
style:
- default: true
color: cyan
next:
page: detail_page
context:
item_id: "$.id"
actions:
- key: "ctrl+d"
name: "Details"
page: "detail_page"
context:
item_id: "$.id"
Key Concepts
1. Data Adapters
IMPORTANT: Choosing the Right Field
- Use
adapter: cli|http|script for single-fetch or periodic refresh data sources
- Use
type: stream for real-time streaming data sources (continuous output)
HTTP Adapter
data:
adapter: http
url: "{{ api_base }}/users"
method: GET
params:
limit: 10
status: "active"
headers:
Authorization: "Bearer {{ token }}"
Accept: "application/json"
items: "$.data[*]"
timeout: "30s"
refresh_interval: "5m"
CLI Adapter
data:
adapter: cli
command: "kubectl"
args: ["get", "pods", "-n", "{{ namespace }}", "-o", "json"]
items: "$.items[*]"
timeout: "10s"
refresh_interval: "30s"
Script Adapter
Execute external scripts for custom data processing:
data:
adapter: script
path: "./scripts/process_data.sh"
args: ["--env", "{{ environment }}"]
items: "$[*]"
timeout: "1m"
Stream Adapter
For real-time streaming data (logs, command output):
data:
type: stream
command: "kubectl"
args: ["logs", "-f", "pod-name", "-n", "namespace"]
buffer_size: 1000
buffer_time: "5m"
follow: true
timeout: "30s"
Important: Stream data sources use type: stream (not adapter: stream). The command output is streamed in real-time to the terminal.
2. View Types
Table View
Display data in columns with sorting and styling:
view:
type: table
columns:
- path: "$.name"
display: "Name"
width: 30
style:
- default: true
color: cyan
bold: true
- path: "$.status"
display: "Status"
width: 15
style:
- condition: "{{ value == 'active' }}"
color: green
- condition: "{{ value == 'inactive' }}"
color: red
- default: true
color: gray
- path: "$.created_at"
display: "Age"
width: 15
transform: "{{ value | timeago }}"
style:
- default: true
color: yellow
- path: "$.size"
display: "Size"
width: 12
transform: "{{ value | filesizeformat }}"
style:
- default: true
color: blue
Text View
Display single objects as formatted text:
view:
type: text
syntax: yaml
Logs View
Display streaming logs:
view:
type: logs
follow: true
wrap: true
show_line_numbers: true
show_timestamps: false
Note: Log filtering is defined in the schema but not yet fully implemented in the current version. Use the basic logs view shown above.
3. Navigation
Simple Navigation (Enter key)
next:
page: detail_page
context:
item_id: "$.id"
item_name: "$.name"
Conditional Navigation
Navigate to different pages based on data values:
next:
- condition: "{{ row.type == 'folder' }}"
page: folder_view
context:
folder_id: "$.id"
- condition: "{{ row.type == 'file' }}"
page: file_view
context:
file_id: "$.id"
- default: true
page: default_view
4. Actions
Actions are triggered via the Shift+A menu or direct Ctrl+key shortcuts:
actions:
- key: "ctrl+d"
name: "Delete"
description: "Delete this item"
confirm: "Are you sure you want to delete {{ name }}?"
command: "curl"
args: ["-X", "DELETE", "{{ api_base }}/items/{{ id }}"]
refresh: true
- key: "ctrl+v"
name: "View Details"
page: "detail_page"
context:
item_id: "$.id"
- key: "ctrl+o"
name: "Open in Browser"
command: "open"
args: ["{{ html_url }}"]
- key: "ctrl+y"
name: "YAML View"
builtin: yaml_view
5. Multiple Data Sources
Combine data from multiple sources:
data:
sources:
- name: users
adapter: http
url: "{{ api_base }}/users"
items: "$.data[*]"
- name: stats
adapter: http
url: "{{ api_base }}/stats"
items: "$.data[*]"
optional: true
merge: true
view:
type: table
columns:
- path: "$.name"
display: "User"
source: users
- path: "$.count"
display: "Stats"
source: stats
6. Context Variables and Navigation
CRITICAL: There are TWO different syntaxes for context depending on navigation type!
A. Next Navigation (Enter key) - Uses JSONPath
next:
page: detail
context:
user_id: "$.id"
user_name: "$.attributes.name"
detail:
title: "User: {{ user_name }}"
data:
url: "{{ api_base }}/users/{{ user_id }}"
B. Action Navigation (Shift+A menu / Ctrl+key) - Uses Templates
actions:
- key: "ctrl+d"
page: detail
context:
user_id: "{{ id }}"
user_name: "{{ attributes.name }}"
combined: "{{ namespace }}/{{ name }}"
C. Variable Scoping and Access
Variables available in templates ({{ }}):
-
Global variables - From globals section
globals:
api_base: "https://api.example.com"
-
Current row fields - Flattened to top level
-
Previous page data - Stored by page name
-
Context variables - From navigation context
-
Special variables
{{ row }} - The entire current row object
{{ value }} - Current value (in transforms/conditions)
{{ env.VAR_NAME }} - Environment variables
Variable Resolution Priority (high to low):
- Current row fields (flattened)
- Page contexts (from previous navigations)
- Global variables
- Environment variables
D. Common Patterns
Pattern 1: Multi-level Navigation
next:
page: pods
context:
namespace: "$.metadata.name"
title: "Pods in {{ namespace }}"
data:
command: "kubectl"
args: ["get", "pods", "-n", "{{ namespace }}"]
actions:
- key: "ctrl+l"
page: pod_logs
context:
pod_name: "{{ metadata.name }}"
namespace: "{{ namespace }}"
title: "Logs: {{ pod_name }}"
data:
type: stream
command: "kubectl"
args: ["logs", "-f", "{{ pod_name }}", "-n", "{{ namespace }}"]
Pattern 2: Accessing Previous Page Data
title: "Details - {{ users.attributes.name }}"
title: "Details - {{ user_name }}"
Pattern 3: Current Row Access
columns:
- path: "$.status"
style:
- condition: "{{ status == 'active' }}"
color: green
E. Key Rules Summary
| Location | Syntax | Example |
|---|
| next.context values | JSONPath only | pod_name: "$.metadata.name" |
| action.context values | Templates | pod_name: "{{ metadata.name }}" |
| Page titles | Templates | title: "{{ namespace }}" |
| Data commands/URLs | Templates | args: ["-n", "{{ namespace }}"] |
| Transforms | Templates | transform: "{{ value | timeago }}" |
| Conditions | Templates | condition: "{{ value > 5 }}" |
7. Styling
Available colors: black, red, green, yellow, blue, magenta, cyan, white, gray
Style modifiers: bold, dim
style:
- condition: "{{ value > 100 }}"
color: green
bold: true
- condition: "{{ value < 0 }}"
color: red
- default: true
color: white
8. Column Transforms
Use Tera templates to transform column values:
columns:
- path: "$.created_at"
display: "Age"
transform: "{{ value | timeago }}"
- path: "$.bytes"
display: "Size"
transform: "{{ value | filesizeformat }}"
- path: "$.status"
display: "Status"
transform: "{{ value | upper }}"
- path: "$.price"
display: "Price"
transform: "${{ value | round(precision=2) }}"
- path: "$.tags"
display: "Tags"
transform: "{{ value | join(sep=', ') }}"
Available Tera filters:
timeago - Convert timestamp to relative time
filesizeformat - Format bytes to human-readable size
upper, lower, capitalize - Case conversion
round - Round numbers
join - Join arrays
status_color - Color code status values
9. Validation Rules
Add validation to ensure data integrity:
validation:
rules:
- field: "$.email"
type: email
message: "Invalid email format"
- field: "$.age"
type: range
min: 0
max: 120
message: "Age must be between 0 and 120"
- field: "$.username"
type: regex
pattern: "^[a-zA-Z0-9_]+$"
message: "Username can only contain letters, numbers, and underscores"
- field: "$.status"
type: enum
values: ["active", "inactive", "pending"]
message: "Status must be active, inactive, or pending"
10. JSONPath Reference
Common patterns:
$[*] - All items in root array
$.data[*] - All items in data array
$.items[*] - All items in items array
$.attributes.name - Nested field access
$.data - Single object (for detail views)
$..name - Recursive descent (all name fields)
$[0] - First item
$[-1] - Last item
$[?(@.status == 'active')] - Filter items
Complete Examples
Example 1: Dog API Browser
version: v1
app:
name: "Dog Breeds Browser"
description: "Explore dog breeds and facts"
theme: "default"
globals:
api_base: "https://dogapi.dog/api/v2"
start: breeds
pages:
breeds:
title: "Dog Breeds"
data:
adapter: http
url: "{{ api_base }}/breeds"
method: GET
headers:
Accept: "application/json"
items: "$.data[*]"
view:
type: table
columns:
- path: "$.attributes.name"
display: "Breed"
width: 30
style:
- default: true
color: cyan
bold: true
- path: "$.attributes.life.min"
display: "Min Life"
width: 10
style:
- default: true
color: green
- path: "$.attributes.life.max"
display: "Max Life"
width: 10
style:
- default: true
color: green
- path: "$.attributes.hypoallergenic"
display: "Hypo"
width: 6
style:
- condition: "{{ value == 'true' }}"
color: yellow
bold: true
- default: true
color: gray
next:
page: breed_detail
context:
breed_id: "$.id"
breed_name: "$.attributes.name"
actions:
- key: "ctrl+f"
name: "View Facts"
page: "facts"
breed_detail:
title: "{{ breed_name }}"
data:
adapter: http
url: "{{ api_base }}/breeds/{{ breed_id }}"
method: GET
headers:
Accept: "application/json"
items: "$.data"
view:
type: table
columns:
- path: "$.attributes.name"
display: "Name"
width: 30
- path: "$.attributes.description"
display: "Description"
width: 80
facts:
title: "Dog Facts"
data:
adapter: http
url: "{{ api_base }}/facts"
method: GET
headers:
Accept: "application/json"
items: "$.data[*]"
view:
type: table
columns:
- path: "$.attributes.body"
display: "Fact"
width: 100
style:
- default: true
color: yellow
Example 2: Kubernetes Dashboard with Logs
version: v1
app:
name: "Kubernetes Dashboard"
description: "Browse pods and view logs"
theme: "default"
globals:
namespace: "default"
start: pods
pages:
pods:
title: "Pods in {{ namespace }}"
data:
adapter: cli
command: "kubectl"
args: ["get", "pods", "-n", "{{ namespace }}", "-o", "json"]
items: "$.items[*]"
refresh_interval: "10s"
view:
type: table
columns:
- path: "$.metadata.name"
display: "Name"
width: 40
style:
- default: true
color: cyan
- path: "$.status.phase"
display: "Status"
width: 15
style:
- condition: "{{ value == 'Running' }}"
color: green
- condition: "{{ value == 'Pending' }}"
color: yellow
- condition: "{{ value == 'Failed' }}"
color: red
- default: true
color: white
- path: "$.metadata.creationTimestamp"
display: "Age"
width: 15
transform: "{{ value | timeago }}"
next:
page: pod_detail
context:
pod_name: "$.metadata.name"
pod_namespace: "$.metadata.namespace"
actions:
- key: "ctrl+l"
name: "View Logs"
page: "pod_logs"
context:
pod_name: "{{ metadata.name }}"
pod_namespace: "{{ metadata.namespace }}"
pod_detail:
title: "Pod: {{ pod_name }}"
data:
adapter: cli
command: "kubectl"
args: ["get", "pod", "{{ pod_name }}", "-n", "{{ pod_namespace }}", "-o", "json"]
items: "$.data"
view:
type: text
syntax: yaml
pod_logs:
title: "Logs: {{ pod_name }}"
data:
type: stream
command: "kubectl"
args: ["logs", "-f", "{{ pod_name }}", "-n", "{{ pod_namespace }}"]
buffer_size: 1000
follow: true
view:
type: logs
follow: true
wrap: true
show_line_numbers: true
Example 3: File Browser with Conditional Navigation
version: v1
app:
name: "File Browser"
description: "Browse files and directories"
theme: "default"
globals:
base_path: "/Users/user/projects"
start: directory
pages:
directory:
title: "{{ current_path | default(value=base_path) }}"
data:
adapter: cli
command: "ls"
args: ["-la", "{{ current_path | default(value=base_path) }}"]
items: "$[*]"
view:
type: table
columns:
- path: "$.name"
display: "Name"
width: 40
style:
- condition: "{{ row.type == 'dir' }}"
color: blue
bold: true
- default: true
color: white
- path: "$.size"
display: "Size"
width: 12
transform: "{{ value | filesizeformat }}"
- path: "$.modified"
display: "Modified"
width: 20
transform: "{{ value | timeago }}"
next:
- condition: "{{ row.type == 'dir' }}"
page: directory
context:
current_path: "$.path"
- condition: "{{ row.type == 'file' }}"
page: file_content
context:
file_path: "$.path"
- default: true
page: directory
file_content:
title: "{{ file_path }}"
data:
adapter: cli
command: "cat"
args: ["{{ file_path }}"]
view:
type: text
syntax: auto
Example 4: REST API with Multiple Data Sources
version: v1
app:
name: "User Dashboard"
description: "View users with stats"
theme: "default"
globals:
api_base: "https://api.example.com"
start: users
pages:
users:
title: "Users with Activity"
data:
sources:
- name: users
adapter: http
url: "{{ api_base }}/users"
items: "$.data[*]"
- name: activity
adapter: http
url: "{{ api_base }}/activity"
items: "$.data[*]"
optional: true
merge: true
view:
type: table
columns:
- path: "$.name"
display: "User"
width: 30
source: users
style:
- default: true
color: cyan
- path: "$.email"
display: "Email"
width: 35
source: users
- path: "$.last_login"
display: "Last Login"
width: 20
source: activity
transform: "{{ value | timeago }}"
style:
- default: true
color: yellow
Generation Guidelines
When generating a TermStack YAML:
- Understand the data source - API endpoints, CLI commands, or scripts
- Define globals - API base URL and common variables
- Create the start page - Usually a list/table view
- Add detail pages - For viewing individual items
- Set up navigation - Use
next for Enter key, actions for shortcuts
- Add styling - Color code important fields
- Use correct context - Pass IDs/names via context, access directly by key name
- Choose the right data source type - Use
adapter: http|cli|script for single/periodic fetches, type: stream for real-time streaming
- Select appropriate view - Table for lists, Text for details, Logs for streaming
- Add transforms - Use Tera filters for formatting (timeago, filesizeformat)
- Implement validation - Add rules for data integrity
- Use conditional navigation - Route to different pages based on data type
Common Patterns
REST API Browser
List Page (table) → Detail Page (table/text) → Related Items (table)
File Browser
Directory (table) → Subdirectory (table) → File Content (text)
Use conditional navigation for files vs directories
Kubernetes Dashboard
Namespaces → Pods → Pod Details → Logs (streaming)
Log Viewer
Services (table) → Logs (logs view with filters)
Use stream adapter with follow mode
Multi-Source Dashboard
Users (merged from users + stats) → User Detail → User Activity
Timeout Formats
All timeout fields support these formats:
- Seconds:
"30s", "5s"
- Minutes:
"5m", "30m"
- Hours:
"1h", "2h"
- Combined:
"1h30m", "2m30s"
Running TermStack
termstack examples/your-config.yaml
Installation: If you don't have termstack installed, download it with:
curl -fsSL https://github.com/pa/termstack/releases/latest/download/termstack-macos-arm64.tar.gz \
| tar -xz \
&& chmod +x termstack \
&& sudo mv termstack /usr/local/bin/termstack
curl -fsSL https://github.com/pa/termstack/releases/latest/download/termstack-macos-amd64.tar.gz \
| tar -xz \
&& chmod +x termstack \
&& sudo mv termstack /usr/local/bin/termstack
curl -fsSL https://github.com/pa/termstack/releases/latest/download/termstack-linux-amd64.tar.gz \
| tar -xz \
&& chmod +x termstack \
&& sudo mv termstack /usr/local/bin/termstack
Navigation Keys
Enter - Navigate to next page (defined by next)
Esc - Go back
Shift+A - Open action menu
j/k or arrows - Move up/down
g - Go to top
G - Go to bottom
/ - Search (use %Column Name% term for column-specific search)
q - Quit
r - Refresh data
f - Toggle filter (in logs view)
Tips for Best Results
- Always specify
items JSONPath - This tells TermStack where to find the array
- Use descriptive context variable names - Makes templates easier to understand
- Add timeouts - Prevent hanging on slow APIs or commands
- Use refresh_interval for dashboards - Keep data current
- Add confirmation for destructive actions - Use
confirm field
- Style based on data values - Use conditional styles for status, severity, etc.
- Use transforms for readability - Format timestamps, file sizes, etc.
- Test JSONPath expressions - Use online tools to verify paths
- Add optional flag to non-critical data sources - Prevents failures
- Use builtin actions - Leverage built-in functionality (help, search, refresh)
Troubleshooting Common Issues
Issue: "Command exited with status: 1" in Stream
Cause: Template variables in stream command are not being resolved correctly.
Solution:
data:
type: stream
command: "kubectl"
args: ["logs", "-f", "{{ pod_name }}", "-n", "{{ namespace }}"]
actions:
- key: "ctrl+l"
page: pod_logs
context:
pod_name: "{{ metadata.name }}"
pod_namespace: "{{ metadata.namespace }}"
data:
type: stream
args: ["logs", "-f", "{{ pod_name }}", "-n", "{{ pod_namespace }}"]
Issue: Variables Not Available in Next Page
Cause: Using wrong syntax in context (templates in next.context or JSONPath in action.context).
Solution:
next:
context:
name: "{{ metadata.name }}"
next:
context:
name: "$.metadata.name"
actions:
- key: "ctrl+d"
context:
name: "$.metadata.name"
actions:
- key: "ctrl+d"
context:
name: "{{ metadata.name }}"
Issue: Can't Access Previous Page Data
Cause: Not understanding how page data is stored.
Solution:
title: "Pods in {{ namespaces.metadata.name }}"
next:
page: pods
context:
namespace: "$.metadata.name"
title: "Pods in {{ namespace }}"
Issue: Template Variables Empty/Undefined
Cause: Variable not in scope or wrong variable name.
Debug steps:
- Check if variable is passed in context
- Verify context syntax matches navigation type (next vs action)
- Use page name prefix for previous page data:
{{ pagename.field }}
- Check variable resolution priority (current row > page contexts > globals)
Example:
actions:
- key: "ctrl+l"
context:
pod_name: "{{ metadata.name }}"
pod_logs:
title: "{{ pod_name }}"
pod_logs:
title: "{{ pods.metadata.name }}"
Issue: JSONPath Not Extracting Data
Cause: Wrong JSONPath expression or data structure.
Solution:
context:
name: "$.metadata.name"
context:
name: "$.name"
name: "metadata.name"
Best Practices for Context
-
Always pass namespace with pod data:
context:
pod_name: "{{ metadata.name }}"
pod_namespace: "{{ metadata.namespace }}"
-
Use consistent variable names across pages:
namespaces → namespace
pods → pod_name, pod_namespace
deployments → deployment_name
-
Extract data early in navigation chain:
next:
context:
namespace: "$.metadata.name"
cluster: "$.metadata.clusterName"
-
Use descriptive context keys:
context:
pod_name: "{{ metadata.name }}"
pod_namespace: "{{ metadata.namespace }}"
context:
name: "{{ metadata.name }}"
ns: "{{ metadata.namespace }}"