Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
232 changes: 232 additions & 0 deletions example/tab_demo.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import 'dart:async';
import 'package:nocterm/nocterm.dart';

void main() {
runApp(const TabDemoApp());
}

class TabDemoApp extends StatelessComponent {
const TabDemoApp({super.key});

@override
Component build(BuildContext context) {
return NoctermApp(
title: 'Tab Demo',
home: const MainScreen(),
);
}
}

class MainScreen extends StatefulComponent {
const MainScreen({super.key});

@override
State<MainScreen> createState() => _MainScreenState();
}

class _MainScreenState extends State<MainScreen> {
int _selectedTabIndex = 0;
int _focusedFieldIndex = 0;

double _progress1 = 0.0;
double _progress2 = 0.0;
Timer? _progressTimer;

@override
void initState() {
super.initState();
_progressTimer = Timer.periodic(const Duration(milliseconds: 100), (timer) {
if (mounted) {
setState(() {
_progress1 += 0.02;
if (_progress1 > 1.0) _progress1 = 0.0;

_progress2 += 0.005;
if (_progress2 > 1.0) _progress2 = 0.0;
});
}
});
}

@override
void dispose() {
_progressTimer?.cancel();
super.dispose();
}

@override
Component build(BuildContext context) {
return Column(
children: [
// Header
Container(
padding: const EdgeInsets.all(1),
color: Colors.blue,
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Tab Demo', style: TextStyle(color: Colors.white)),
],
),
),

// Tabs
TabComponent(
tabs: const ['Form Input', 'List View', 'Progress'],
selectedIndex: _selectedTabIndex,
onChanged: (index) {
setState(() {
_selectedTabIndex = index;
});
},
),

// Content Area
Expanded(
child: Container(
padding: const EdgeInsets.all(2),
child: _buildSelectedContent(),
),
),

// Footer
Container(
padding: const EdgeInsets.all(1),
color: Colors.brightBlack,
child: const Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
' Use Left/Right arrows or 1-3 to switch tabs. ',
style: TextStyle(color: Colors.grey),
),
Text(
' Press Ctrl+C or ESC or q to exit. ',
style: TextStyle(color: Colors.grey),
),
],
),
),
],
);
}

Component _buildSelectedContent() {
switch (_selectedTabIndex) {
case 0:
return _buildFormTab();
case 1:
return _buildListTab();
case 2:
return _buildProgressTab();
default:
return const Text('Unknown Tab');
}
}

Component _buildFormTab() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Enter your details:'),
const SizedBox(height: 1),
const Text('Name:'),
GestureDetector(
onTap: () {
setState(() {
_focusedFieldIndex = 0;
});
},
child: Container(
width: 30,
decoration: BoxDecoration(
border: BoxBorder.all(style: BoxBorderStyle.solid),
),
child: TextField(
placeholder: 'Type your name...',
focused: _focusedFieldIndex == 0,
),
),
),
const SizedBox(height: 1),
const Text('Email:'),
GestureDetector(
onTap: () {
setState(() {
_focusedFieldIndex = 1;
});
},
child: Container(
width: 30,
decoration: BoxDecoration(
border: BoxBorder.all(style: BoxBorderStyle.solid),
),
child: TextField(
placeholder: 'Type your email...',
focused: _focusedFieldIndex == 1,
),
),
),
],
);
}

Component _buildListTab() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Select an item:'),
const SizedBox(height: 1),
Expanded(
child: Container(
decoration: BoxDecoration(
border: BoxBorder.all(style: BoxBorderStyle.solid),
),
child: ListView.builder(
itemCount: 20,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 1),
child: Text('List Item ${index + 1}'),
);
},
),
),
),
],
);
}

Component _buildProgressTab() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Task Progress Tracker'),
const SizedBox(height: 2),
const Text('Downloading Files...'),
const SizedBox(height: 1),
Container(
width: 40,
child: ProgressBar(
value: _progress1,
valueColor: Colors.blue,
backgroundColor: Colors.brightBlack,
showPercentage: true,
),
),
const SizedBox(height: 2),
const Text('Processing Data...'),
const SizedBox(height: 1),
Container(
width: 40,
child: ProgressBar(
value: _progress2,
valueColor: Colors.magenta,
backgroundColor: Colors.brightBlack,
showPercentage: true,
),
),
],
);
}
}
1 change: 1 addition & 0 deletions lib/nocterm.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export 'src/framework/axis.dart';

export 'src/components/spacer.dart';
export 'src/components/divider.dart';
export 'src/components/tab.dart';
export 'src/process/pty_controller.dart';
export 'src/components/stack.dart';
export 'src/components/render_stack.dart' show Stack;
Expand Down
6 changes: 4 additions & 2 deletions lib/src/backend/terminal.dart
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,10 @@ class Terminal {

/// Restore terminal colors to defaults
void restoreColors() {
backend.writeRaw('\x1b]110'); // foreground
backend.writeRaw('\x1b]111'); // background
backend.writeRaw('\x1b]10;\x07'); // foreground reset (empty spec)
backend.writeRaw('\x1b]11;\x07'); // background reset (empty spec)
backend.writeRaw('\x1b]110\x07'); // foreground reset (OSC 110)
backend.writeRaw('\x1b]111\x07'); // background reset (OSC 111)
}

void reset() {
Expand Down
25 changes: 21 additions & 4 deletions lib/src/binding/terminal_binding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ class TerminalBinding extends NoctermBinding
// We also enable modifyOtherKeys as a fallback for terminals like xterm.
terminal.write(EscapeCodes.enable.kittyKeyboard);
terminal.write(EscapeCodes.enable.modifyOtherKeys);
terminal.write(EscapeCodes.enable.applicationCursorKeys);
terminal.flush();

// Store initial size
Expand Down Expand Up @@ -258,10 +259,21 @@ class TerminalBinding extends NoctermBinding
}

// Route the event through the component tree
_routeKeyboardEvent(keyEvent);
final handled = _routeKeyboardEvent(keyEvent);

// Note: Ctrl+C (SIGINT) is routed through the event system first,
// allowing components to intercept it. Falls back to shutdown if unhandled.
if (!handled) {
final isCtrlC = keyEvent.logicalKey == LogicalKey.keyC &&
keyEvent.isControlPressed;
final isEsc = keyEvent.logicalKey == LogicalKey.escape;
final isQ = keyEvent.character == 'q' || keyEvent.character == 'Q';

if (isCtrlC || isEsc || isQ) {
_performImmediateShutdown();
terminal.backend.requestExit(0);
}
}
} else if (event is MouseInputEvent) {
final mouseEvent = event.event;

Expand Down Expand Up @@ -564,6 +576,7 @@ class TerminalBinding extends NoctermBinding
// Pop kitty keyboard mode and reset modifyOtherKeys
terminal.backend.writeRaw(EscapeCodes.disable.kittyKeyboard);
terminal.backend.writeRaw(EscapeCodes.disable.modifyOtherKeys);
terminal.backend.writeRaw(EscapeCodes.disable.applicationCursorKeys);
terminal.restoreColors(); // Restore terminal colors
terminal.flush();

Expand Down Expand Up @@ -847,6 +860,10 @@ class TerminalBinding extends NoctermBinding
// Pop kitty keyboard mode and reset modifyOtherKeys
terminal.backend.writeRaw(EscapeCodes.disable.kittyKeyboard);
terminal.backend.writeRaw(EscapeCodes.disable.modifyOtherKeys);
terminal.backend.writeRaw(EscapeCodes.disable.applicationCursorKeys);

// Restore terminal colors to defaults
terminal.restoreColors();

// Restore terminal (this includes leaving alternate screen)
terminal.showCursor();
Expand All @@ -859,10 +876,10 @@ class TerminalBinding extends NoctermBinding
terminal.backend.writeRaw(EscapeCodes.disable.buttonEventTracking);
terminal.backend.writeRaw(EscapeCodes.disable.basicMouseTracking);

// Send a terminal reset sequence as a final safety measure
// This helps ensure the terminal is in a clean state
terminal.backend.writeRaw(EscapeCodes.resetDeviceAttributes);
// Send a soft terminal reset as a final safety measure
terminal.backend.writeRaw(EscapeCodes.softReset);

terminal.backend.writeRaw('\x1b[0m'); // Reset all SGR attributes
terminal.clear();

// Final flush to ensure all cleanup is complete
Expand Down
Loading
Loading