Skip to content
Draft
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
165 changes: 165 additions & 0 deletions packages/signals_nocterm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# Signals Nocterm

Reactive state management for [Nocterm](https://nocterm.dev) terminal UIs using [Signals](https://dartsignals.dev).

## Features

- **SignalsMixin** - Mixin for `State` classes to automatically manage signal lifecycle
- **Watch** - Component for fine-grained rebuilds
- **WatchBuilder** - Builder pattern for watching a single signal
- **watchSignal** - Function to watch signals in any build method

## Installation

```yaml
dependencies:
signals_nocterm: ^0.1.0
```

## Quick Start

### Using SignalsMixin

The most common way to use signals in Nocterm is with the `SignalsMixin`:

```dart
import 'package:nocterm/nocterm.dart';
import 'package:signals_nocterm/signals_nocterm.dart';

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

@override
State<Counter> createState() => _CounterState();
}

class _CounterState extends State<Counter> with SignalsMixin {
// Create signals - they're automatically disposed
late final _count = createSignal(0);
late final _doubled = createComputed(() => _count() * 2);

@override
void initState() {
super.initState();
// Create effects - also automatically disposed
createEffect(() {
print('Count: ${_count()}, Doubled: ${_doubled()}');
});
}

@override
Component build(BuildContext context) {
return Focusable(
focused: true,
onKeyEvent: (event) {
if (event.logicalKey == LogicalKey.space) {
_count.value++;
return true;
}
return false;
},
child: Center(
child: Text('Count: ${_count()}, Doubled: ${_doubled()}'),
),
);
}
}
```

### Using Watch for Fine-Grained Rebuilds

Use `Watch` to only rebuild specific parts of your component tree:

```dart
final count = signal(0);

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

@override
Component build(BuildContext context) {
return Column(
children: [
// Only this part rebuilds when count changes
Watch((context) => Text('Count: ${count()}')),
// This part never rebuilds
const Text('Static text'),
],
);
}
}
```

### Using WatchBuilder

`WatchBuilder` provides a builder pattern for watching a single signal:

```dart
final count = signal(0);

WatchBuilder<int>(
signal: count,
builder: (context, value, child) {
return Text('Count: $value');
},
// Optional child that doesn't rebuild
child: const Text('Static child'),
)
```

### Using watchSignal Extension

You can also use the `watch` extension on any signal:

```dart
class MyComponent extends StatelessComponent {
final count = signal(0);

@override
Component build(BuildContext context) {
// This component rebuilds when count changes
final value = count.watch(context);
return Text('Count: $value');
}
}
```

## API Reference

### SignalsMixin

Methods available when using `SignalsMixin`:

| Method | Description |
|--------|-------------|
| `createSignal<T>(value)` | Create a signal that's automatically disposed |
| `createComputed<T>(fn)` | Create a computed signal |
| `createEffect(fn)` | Create an effect that's automatically disposed |
| `createListSignal<T>(list)` | Create a list signal |
| `createMapSignal<K,V>(map)` | Create a map signal |
| `createSetSignal<T>(set)` | Create a set signal |
| `createFutureSignal<T>(fn)` | Create a future-based signal |
| `createStreamSignal<T>(fn)` | Create a stream-based signal |
| `bindSignal(signal)` | Watch an external signal |
| `unbindSignal(signal)` | Stop watching a signal |
| `watchSignal(signal)` | Get value and watch signal |
| `listenSignal(signal, callback)` | Listen to signal changes |
| `disposeSignal(id)` | Manually dispose a signal |

### Components

| Component | Description |
|-----------|-------------|
| `Watch` | Rebuilds its child when any accessed signal changes |
| `WatchBuilder<T>` | Builder pattern for a single signal |

### Functions

| Function | Description |
|----------|-------------|
| `watchSignal<T>(context, signal)` | Watch a signal in any build method |
| `unwatchSignal<T>(context, signal)` | Stop watching a signal |

## License

MIT
76 changes: 76 additions & 0 deletions packages/signals_nocterm/example/counter.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import 'package:nocterm/nocterm.dart';
import 'package:signals_nocterm/signals_nocterm.dart';

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

/// A simple counter example using signals.
///
/// Press SPACE to increment the counter.
/// Press 'r' to reset the counter.
class Counter extends StatefulComponent {
const Counter({super.key});

@override
State<Counter> createState() => _CounterState();
}

class _CounterState extends State<Counter> with SignalsMixin {
// Create a signal for the count
late final _count = createSignal(0);

// Create a computed signal for the doubled value
late final _doubled = createComputed(() => _count() * 2);

// Create a computed signal for checking if even
late final _isEven = createComputed(() => _count() % 2 == 0);

@override
void initState() {
super.initState();

// Create an effect that logs when count changes
createEffect(() {
print('Count changed to: ${_count()}');
});
}

@override
Component build(BuildContext context) {
return Focusable(
focused: true,
onKeyEvent: (event) {
if (event.logicalKey == LogicalKey.space) {
_count.value++;
return true;
}
if (event.character == 'r') {
_count.value = 0;
return true;
}
if (event.logicalKey == LogicalKey.escape) {
shutdownApp();
return true;
}
return false;
},
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Signals + Nocterm Counter'),
const Text(''),
Text('Count: ${_count()}'),
Text('Doubled: ${_doubled()}'),
Text('Is Even: ${_isEven()}'),
const Text(''),
const Text('Press SPACE to increment'),
const Text('Press R to reset'),
const Text('Press ESC to quit'),
],
),
),
);
}
}
4 changes: 4 additions & 0 deletions packages/signals_nocterm/lib/signals_core.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/// Re-export signals_core for convenience.
library;

export 'package:signals_core/signals_core.dart';
78 changes: 78 additions & 0 deletions packages/signals_nocterm/lib/signals_nocterm.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/// Signals integration for Nocterm - reactive state management for terminal UIs.
///
/// This library provides seamless integration between the signals reactive
/// programming library and Nocterm's terminal UI framework.
///
/// ## Getting Started
///
/// Add signals_nocterm to your pubspec.yaml:
///
/// ```yaml
/// dependencies:
/// signals_nocterm: ^0.1.0
/// ```
///
/// ## Usage with SignalsMixin
///
/// Use [SignalsMixin] with your [State] class to automatically manage signals:
///
/// ```dart
/// class Counter extends StatefulComponent {
/// const Counter({super.key});
///
/// @override
/// State<Counter> createState() => _CounterState();
/// }
///
/// class _CounterState extends State<Counter> with SignalsMixin {
/// late final _count = createSignal(0);
/// late final _doubled = createComputed(() => _count() * 2);
///
/// @override
/// void initState() {
/// super.initState();
/// createEffect(() {
/// print('Count: ${_count()}, Doubled: ${_doubled()}');
/// });
/// }
///
/// @override
/// Component build(BuildContext context) {
/// return Center(
/// child: Text('Count: ${_count()}, Doubled: ${_doubled()}'),
/// );
/// }
/// }
/// ```
///
/// ## Usage with Watch
///
/// Use [Watch] component or [watchSignal] function for fine-grained rebuilds:
///
/// ```dart
/// class MyComponent extends StatelessComponent {
/// MyComponent({super.key});
///
/// final count = signal(0);
///
/// @override
/// Component build(BuildContext context) {
/// return Watch((context) => Text('Count: ${count()}'));
/// }
/// }
/// ```
///
/// Or use [watchSignal] in your build method:
///
/// ```dart
/// @override
/// Component build(BuildContext context) {
/// final value = watchSignal(context, mySignal);
/// return Text('Value: $value');
/// }
/// ```
library;

export 'signals_core.dart';
export 'src/mixins/signals.dart';
export 'src/watch/watch.dart';
Loading