Input
User input components
Input components handle user input in your terminal app. This guide covers text input and keyboard event handling.
TextField
TextField provides a text input field:
class MyForm extends StatefulComponent {
const MyForm({super.key});
@override
State<MyForm> createState() => _MyFormState();
}
class _MyFormState extends State<MyForm> {
final _controller = TextEditingController();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Component build(BuildContext context) {
return TextField(
controller: _controller,
decoration: BoxDecoration(
border: BoxBorder.all(color: Colors.gray),
),
);
}
}
TextEditingController
Use TextEditingController to read and control text:
final controller = TextEditingController();
// Get current text
String text = controller.text;
// Set text
controller.text = 'New text';
// Clear text
controller.clear();
// Listen to changes
controller.addListener(() {
print('Text changed: ${controller.text}');
});
// Always dispose when done
controller.dispose();
Multi-line input
TextField supports multi-line input:
TextField(
controller: controller,
maxLines: 5, // Allow 5 lines
)
Styling TextField
Style the text field:
TextField(
controller: controller,
style: TextStyle(color: Colors.green),
decoration: BoxDecoration(
border: BoxBorder.all(color: Colors.blue),
color: Colors.black,
),
padding: EdgeInsets.all(1),
)
TextField callbacks
React to text changes:
TextField(
controller: controller,
onChanged: (text) {
print('Text is now: $text');
},
onSubmitted: (text) {
print('User pressed Enter with: $text');
},
)
Focusable
Focusable makes any component respond to keyboard input:
class Counter extends StatefulComponent {
const Counter({super.key});
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int _count = 0;
@override
Component build(BuildContext context) {
return Focusable(
focused: true,
onKeyEvent: (event) {
if (event.logicalKey == LogicalKey.arrowUp) {
setState(() => _count++);
return true; // Event handled
}
if (event.logicalKey == LogicalKey.arrowDown) {
setState(() => _count--);
return true;
}
return false; // Not handled, pass to parent
},
child: Text('Count: $_count (use arrows)'),
);
}
}
KeyboardEvent
The onKeyEvent callback receives a KeyboardEvent:
onKeyEvent: (event) {
// Check specific keys
if (event.logicalKey == LogicalKey.enter) {
// Handle Enter
}
// Check for character input
if (event.character != null) {
print('User typed: ${event.character}');
}
// Check modifiers
if (event.isControlPressed && event.logicalKey == LogicalKey.keyC) {
// Handle Ctrl+C
}
return true; // Return true if handled
}
Common keys
Use LogicalKey constants:
LogicalKey.enter
LogicalKey.escape
LogicalKey.space
LogicalKey.arrowUp
LogicalKey.arrowDown
LogicalKey.arrowLeft
LogicalKey.arrowRight
LogicalKey.backspace
LogicalKey.delete
LogicalKey.tab
// Letter keys
LogicalKey.keyA
LogicalKey.keyB
// ... etc
// Number keys
LogicalKey.digit1
LogicalKey.digit2
// ... etc
Event handling return value
Return true if you handled the event, false otherwise:
onKeyEvent: (event) {
if (event.logicalKey == LogicalKey.space) {
doSomething();
return true; // Handled - stops propagation
}
return false; // Not handled - bubbles to parent
}
Events bubble up the component tree until a handler returns true.
Focus management
Control which component receives keyboard input:
class App extends StatefulComponent {
const App({super.key});
@override
State<App> createState() => _AppState();
}
class _AppState extends State<App> {
int _focusedIndex = 0;
@override
Component build(BuildContext context) {
return Column(
children: [
Focusable(
focused: _focusedIndex == 0,
onKeyEvent: (event) {
if (event.logicalKey == LogicalKey.tab) {
setState(() => _focusedIndex = 1);
return true;
}
// Handle input for first field
return false;
},
child: Text('Field 1 ${_focusedIndex == 0 ? '(focused)' : ''}'),
),
Focusable(
focused: _focusedIndex == 1,
onKeyEvent: (event) {
if (event.logicalKey == LogicalKey.tab) {
setState(() => _focusedIndex = 0);
return true;
}
// Handle input for second field
return false;
},
child: Text('Field 2 ${_focusedIndex == 1 ? '(focused)' : ''}'),
),
],
);
}
}
Only the Focusable with focused: true receives keyboard events.
Simple form
class LoginForm extends StatefulComponent {
const LoginForm({super.key});
@override
State<LoginForm> createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
int _focusedField = 0;
@override
void dispose() {
_usernameController.dispose();
_passwordController.dispose();
super.dispose();
}
void _submit() {
print('Username: ${_usernameController.text}');
print('Password: ${_passwordController.text}');
}
@override
Component build(BuildContext context) {
return Column(
children: [
Focusable(
focused: _focusedField == 0,
onKeyEvent: (event) {
if (event.logicalKey == LogicalKey.tab) {
setState(() => _focusedField = 1);
return true;
}
if (event.logicalKey == LogicalKey.enter) {
_submit();
return true;
}
return false;
},
child: TextField(
controller: _usernameController,
decoration: BoxDecoration(
border: _focusedField == 0
? BoxBorder.all(color: Colors.blue)
: BoxBorder.all(color: Colors.gray),
),
),
),
SizedBox(height: 1),
Focusable(
focused: _focusedField == 1,
onKeyEvent: (event) {
if (event.logicalKey == LogicalKey.tab) {
setState(() => _focusedField = 0);
return true;
}
if (event.logicalKey == LogicalKey.enter) {
_submit();
return true;
}
return false;
},
child: TextField(
controller: _passwordController,
obscureText: true,
decoration: BoxDecoration(
border: _focusedField == 1
? BoxBorder.all(color: Colors.blue)
: BoxBorder.all(color: Colors.gray),
),
),
),
],
);
}
}
Validation
Validate input before submission:
void _submit() {
if (_usernameController.text.isEmpty) {
setState(() => _error = 'Username required');
return;
}
if (_passwordController.text.length < 8) {
setState(() => _error = 'Password must be at least 8 characters');
return;
}
// Process valid input
_login(_usernameController.text, _passwordController.text);
}
Always dispose controllers
TextEditingController must be disposed:
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Provide visual focus indicators
Show users which field is focused:
TextField(
decoration: BoxDecoration(
border: BoxBorder.all(
color: isFocused ? Colors.blue : Colors.gray,
),
),
)
Handle keyboard shortcuts consistently
Use standard shortcuts:
Tab: Move to next fieldShift+Tab: Move to previous fieldEnter: Submit form or activateEscape: Cancel or close
Test on different terminals
Keyboard handling varies between terminals. Test your input on multiple terminals to ensure compatibility.
Next steps
- Keyboard Events - Deep dive into keyboard handling
- Focus Management - Advanced focus patterns
- Text & Styling - Style your text input