Developer Documentation¶
Welcome to the Substation Developer Guide.
Translation: You're about to build forms. Let's make sure they don't suck.
The Real Talk¶
You're here because you need to create a form in Substation. Maybe it's a network creation form. Maybe it's server deployment. Maybe it's that weird edge case your PM just threw at you at 4:30 PM on Friday.
Good news: We've already solved the hard parts. You just need to assemble the pieces.
Better news: The components are actually well-designed, documented, and battle-tested.
Best news: You won't need to reinvent text field editing for the 47th time.
Documentation Overview¶
FormBuilder Guide¶
What it does: Creates complete forms with validation, navigation, and state management.
When to use it: Every single time you need a form. No exceptions. Don't be clever.
Read this first - FormBuilder is the brain of your forms. It handles:
- Form layout and rendering
- Field navigation (Tab, Shift-Tab, arrow keys)
- Validation and error display
- State management
- Submission handling
FormTextField Guide¶
What it does: Text input with cursor, history, validation, and all the keyboard shortcuts users expect.
When to use it: Any time you need string input. Names, IPs, CIDRs, descriptions, UUIDs.
Read this when you need:
- Text input fields
- Proper cursor handling
- Input history (UP/DOWN arrows)
- Inline validation
- Home/End/Left/Right navigation
FormSelector Guide¶
What it does: Select from large lists (images, networks, flavors) with search, multi-column display, and scrolling.
When to use it: Selecting resources. Any resource. Images, networks, flavors, security groups, availability zones.
Read this when you need:
- Resource selection from large lists
- Multi-column display
- Search/filter functionality
- Multi-select capability
- Handling 10,000+ items
StatusListView Guide¶
What it does: Renders primary resource lists with StatusIcon indicators and multi-column display.
When to use it: Building primary list views for resources (servers, volumes, networks, images).
Read this when you need:
- Primary resource list views
- StatusIcon + text columns
- Automatic filtering and scrolling
- Consistent list rendering
- 80-90% code reduction
DetailView Guide¶
What it does: Renders detailed information screens with section-based layouts and scrollable content.
When to use it: Building detail screens for resources (server details, volume details, network details).
Read this when you need:
- Detail screens with consistent formatting
- Section-based information organization
- Automatic field label and value formatting
- Scrolling for large content
- 70-85% code reduction
Testing Guide¶
What it does: Comprehensive guide to running tests, writing tests, and understanding the testing infrastructure.
When to use it: Before contributing code, after making changes, when adding new features.
Read this when you need:
- Run the test suite
- Write new tests
- Understand test coverage
- Debug failing tests
- Set up CI/CD
Quick Start: Create Your First Form in 5 Minutes¶
Scenario: You need a network creation form. Name, MTU, port security. Let's go.
Step 1: Define Your Form Model¶
struct NetworkCreateForm {
var networkName: String = ""
var mtu: String = "1500"
var portSecurityEnabled: Bool = true
var currentFieldId: String = "name"
// State management
var nameFieldState = FormTextFieldState(initialValue: "")
var mtuFieldState = FormTextFieldState(initialValue: "1500")
}
Step 2: Build Your Fields¶
let fields: [FormField] = [
.text(FormFieldText(
id: "name",
label: "Network Name",
value: form.networkName,
isRequired: true,
isSelected: form.currentFieldId == "name",
isActive: form.isEditingName
)),
.int(FormFieldNumber(
id: "mtu",
label: "MTU",
value: form.mtu,
minValue: 68,
maxValue: 9000,
unit: "bytes",
isRequired: true,
isSelected: form.currentFieldId == "mtu"
)),
.bool(FormFieldToggle(
id: "portSecurity",
label: "Port Security",
value: form.portSecurityEnabled,
isSelected: form.currentFieldId == "portSecurity"
))
]
Step 3: Create and Render¶
let formBuilder = FormBuilder(
title: "Create Network",
fields: fields,
selectedFieldId: form.currentFieldId,
validationErrors: form.validate()
)
// Render it
await SwiftTUI.render(formBuilder.render(), on: surface, in: bounds)
Step 4: Handle Input¶
switch keyCode {
case Int32(9): // TAB
form.nextField()
case Int32(32): // SPACE
if form.currentFieldId == "name" {
form.nameFieldState.activate()
}
case Int32(10): // ENTER
if form.isEditingName {
form.networkName = form.nameFieldState.value
form.nameFieldState.confirm()
} else {
// Submit the form
await createNetwork(form)
}
}
Done. You have a functional form with validation, navigation, and state management.
Field Types Reference¶
Need This | Use This | Example |
---|---|---|
Server name | .text |
String input, 1-255 chars |
MTU value | .number |
Number with range (68-9000) |
Port security | .toggle |
Enable/disable toggle |
Accept terms | .checkbox |
Checkbox with help text |
IP version | .select |
IPv4 vs IPv6 |
Select image | .selector |
Pick from 1000s of images |
Security groups | .multiSelect |
Pick multiple |
Current status | .info |
Read-only display |
Something weird | .custom |
Build your own |
Detailed field documentation: See component guides for comprehensive field type documentation and examples.
Best Practices¶
Use FormBuilder for Everything¶
Why: Consistency. One API. One behavior. Users learn once, use everywhere.
Bad: Custom form rendering with hand-rolled navigation and validation.
Good: FormBuilder(title: "Create Thing", fields: fields)
Validate Early¶
Why: Users want immediate feedback. Waiting until submit is 1990s web form behavior.
// Good - validate as they type
mutating func handleNameInput(_ char: Character) {
nameFieldState.handleCharacterInput(char)
validateName() // Immediate feedback
}
Provide Clear Error Messages¶
Good: "Network name must be between 1 and 255 characters"
Bad: "Invalid input"
Worse: "Error code: NET_NAME_ERR_001"
Make Resources Searchable¶
Why: Users will have 1000+ images. Scrolling is death.
// Good - search name, ID, tags
func matchesSearch(_ query: String) -> Bool {
let q = query.lowercased()
return (name?.lowercased().contains(q) ?? false) ||
id.lowercased().contains(q) ||
(tags?.joined().lowercased().contains(q) ?? false)
}
Don't Reinvent Components¶
Why: We already solved cursor movement, history, validation, scrolling, search. Don't rebuild them badly.
Bad: Writing your own text input with cursor tracking.
Good: Using FormTextField
which already handles this correctly.
Component Source Code¶
Study these when the guides aren't enough:
/Sources/Substation/Components/FormBuilder.swift
- Main form builder/Sources/Substation/Components/FormBuilderState.swift
- State management/Sources/Substation/Components/FormTextField.swift
- Text input component/Sources/Substation/Components/FormSelector.swift
- Selection component/Sources/Substation/Components/FormSelectorRenderer.swift
- Type-specific selector rendering/Sources/Substation/Components/StatusListView.swift
- Primary list view component
Example Forms in the Codebase¶
Simple forms:
/Sources/Substation/ViewModels/NetworkCreateForm.swift
- Name, MTU, port security/Sources/Substation/ViewModels/RouterCreateForm.swift
- Name, gateway, conditional fields
Complex forms:
/Sources/Substation/Views/ServerCreateView.swift
- Images, flavors, networks, security groups/Sources/Substation/ViewModels/SubnetCreateForm.swift
- IP validation, CIDR, allocation pools
Troubleshooting¶
"My field isn't accepting input!"¶
Check:
- Is
isActive
true? - Is
isSelected
true? - Are you calling
handleCharacterInput()
? - Is your event loop actually calling your input handler?
Fix: Set both isSelected: true
and isActive: true
when editing.
"Search doesn't work!"¶
Check:
- Does your type implement
FormSelectableItem
? - Is
matchesSearch()
actually implemented? - Are you updating
searchQuery
in the selector state?
Fix: Implement the protocol correctly. See the guide.
"Validation errors don't show!"¶
Check:
- Is
validationError
set on the field? - Is
showValidationErrors: true
on FormBuilder? - Is your validation function actually running?
Fix: Set validationError
on the field AND showValidationErrors
on the builder.
"My selector shows nothing!"¶
Check:
- Is
items
array empty? - Did you forget to set
isVisible: true
? - Did your search filter everything out?
Fix: Debug your data flow. Items need to exist and be visible.
Related Documentation¶
- API Reference - Complete API documentation for OSClient, SwiftTUI, CrossPlatformTimer
- Architecture Overview - Overall system architecture
- Performance Documentation - Benchmarking and optimization
Questions?¶
- Read the component guides first - They have detailed examples
- Look at existing forms - Copy patterns that work
- Check the source - It's well-commented
- Ask in PR - We're here to help
Remember: Building forms doesn't have to be painful. Use the components. Follow the patterns. Ship features instead of debugging cursor position at 2 AM.
Now go build something.