nocterm

repository·main·Indexed 18 days ago

https://github.com/norbert515/nocterm

A framework for building Terminal User Interfaces (TUIs) in Dart using a reactive, declarative programming model inspired by Flutter, featuring StatefulComponents, setState, and widget trees. The ecosystem includes the nocterm CLI for IDE debugging and log streaming, nocterm_riverpod for state management integration, and nocterm_web for rendering TUIs in web browsers via xterm.js.

Tokens
60.7K
Snippets
206
Records
271
Agent score
62%

What's inside nocterm

  1. What is Nocterm?

    main

    Nocterm is a framework for building Terminal User Interfaces (TUIs) in Dart. It implements a declarative UI model inspired by Flutter, allowing developers to use familiar patterns like StatefulComponent, setState(), and layout widgets (Column, Row, etc.) to build interactive terminal applications.

    Key Features

    • Familiar API: Uses Flutter-like components and state management.
    • Hot Reload: Supports instant updates during development.
    • Rich Components: Includes text styling, layouts, scrolling, input fields, and markdown rendering.
    • Cross-platform: Compatible with Windows, macOS, and Linux.
    • Testing: Provides specialized testing utilities for TUIs.
  2. Summary of Nocterm's automated terminal management

    main

    Nocterm abstracts away the low-level terminal mechanics so you can focus on UI development. The following tasks are handled automatically by the framework:

    • Alternate screen switching: Moving between the main and alternate buffers.
    • Raw mode setup and cleanup: Configuring input modes and restoring the terminal state on exit.
    • Resize event handling: Detecting window changes and triggering rebuilds.
    • Cursor positioning: Managing where the cursor appears on the grid.
    • Rendering optimization: Efficiently updating the character cell grid.
  3. Configure Image protocols

    main

    Nocterm supports several protocols for displaying images. While the component auto-detects the best available protocol (Priority: Kitty > iTerm2 > Sixel > Unicode Blocks), you can manually specify a protocol using the protocol parameter with ImageProtocol values.

    Supported Protocols

    • ImageProtocol.kitty: Most capable; supports transparency and animations. (Kitty, WezTerm, Ghostty)
    • ImageProtocol.iterm2: Proprietary protocol. (iTerm2, WezTerm)
    • ImageProtocol.sixel: Widely supported older protocol. (xterm with sixel, mlterm, Mintty, foot, WezTerm)
    • ImageProtocol.unicodeBlocks: Universal fallback using Unicode half-block characters. Works in any terminal but at lower resolution (2 vertical pixels per character cell).
    // Example: Explicitly using Sixel
    Image.file(
      '/path/to/image.png',
      protocol: ImageProtocol.sixel,
    )
  4. How navigation works in Nocterm

    main

    Nocterm uses a stack-based navigation system similar to Flutter. You manage screens by pushing routes onto a stack to navigate forward and popping them to go back. The Navigator component is the central hub for managing this stack, handling keyboard navigation (like the ESC key for going back), supporting named routes, and providing modal dialogs.

    import 'package:nocterm/nocterm.dart';
    
    void main() {
      runApp(
        Navigator(
          home: const HomePage(),
          routes: {
            '/settings': (context) => const SettingsPage(),
            '/about': (context) => const AboutPage(),
          },
        ),
      );
    }
  5. Understand the scroll acceleration algorithm

    main

    The acceleration logic in AcceleratedScrollController is based on a time-delta velocity model:

    1. Rapid Scrolling (< 100ms between events): The _currentVelocity is multiplied by accelerationFactor and clamped between minSpeed and maxSpeed.
    2. Moderate Scrolling (100ms - 300ms between events): The _currentVelocity is multiplied by decayRate to simulate natural deceleration.
    3. Slow Scrolling (> 300ms between events): The _currentVelocity is reset to minSpeed for precise control.

    This model ensures that fast trackpad swipes build up to maxSpeed while slow, deliberate movements remain precise at minSpeed.

  6. Understand the Component tree

    main

    Components in Nocterm form a hierarchical tree structure. Each component can contain children, which in turn can have their own children.

    Rebuild Behavior: Changes propagate down the tree. If a parent component rebuilds, all of its children will also rebuild.

    Container(
      child: Column(
        children: [
          Text('Title'),
          SizedBox(height: 1),
          Row(
            children: [
              Text('Left'),
              Text('Right'),
            ],
          ),
        ],
      ),
    )
  7. Understanding character cells and layout

    main

    Unlike GUI applications that use pixels, Nocterm renders text in a grid of character cells. Each cell consists of:

    • A character: A letter, number, emoji, or space.
    • Styling: Foreground color, background color, bold, italic, or underline.

    Key Layout Implications:

    • Positioning: Elements are positioned in rows and columns, not pixels.
    • Wrapping: Text wrapping occurs at character boundaries.
    • Wide characters: Certain characters (like emojis or Chinese characters) may occupy 2 cells instead of 1.

    When using layout components like Column and Row, Nocterm calculates positions based on these character cells.

  8. How event bubbling works in the component tree

    main

    Keyboard events bubble up the component tree. When a Focusable component receives an event, it decides whether to stop the event or let it pass to its parent based on the return value of onKeyEvent:

    1. If onKeyEvent returns true, the event is considered handled and stops bubbling.
    2. If onKeyEvent returns false, the event is not handled and bubbles up to the parent component.
    // Parent
    Focusable(
      focused: true,
      onKeyEvent: (event) {
        print('Parent received: ${event.logicalKey}');
        return false;  // Not handled, pass to parent
      },
      child: Focusable(
        focused: true,
        onKeyEvent: (event) {
          if (event.logicalKey == LogicalKey.enter) {
            print('Child handled Enter');
            return true;  // Handled, stop bubbling
          }
          return false;  // Not handled, bubble to parent
        },
        child: Text('Child'),
      ),
    )
  9. Use AnimatedBuilder for UI updates

    main

    AnimatedBuilder is the recommended way to rebuild parts of your UI when an animation changes. It takes an animation and a builder function.

    Optimization: The child parameter

    To prevent expensive subtrees from rebuilding on every frame, pass them as the child argument to AnimatedBuilder. The builder function receives this child and can use it without re-executing its own construction logic.

    AnimatedBuilder(
      animation: _controller,
      builder: (context, child) {
        return Opacity(
          opacity: _controller.value,
          child: child, // This subtree won't rebuild
        );
      },
      child: ExpensiveComponent(), // Built only once
    )

    ListenableBuilder

    If you are using a non-animation Listenable (like a ChangeNotifier), use ListenableBuilder instead:

    ListenableBuilder(
      listenable: myNotifier,
      builder: (context, child) {
        return Text('Value: ${myNotifier.value}');
      },
    )
  10. Lift state up to share data between components

    main

    When multiple components need to access or modify the same data, move that state to their closest common ancestor.

    The Pattern:

    1. Parent Component: Holds the actual state and defines methods to modify it.
    2. Child Components: Receive the state as parameters (props) and receive callbacks (e.g., VoidCallback) to notify the parent when an interaction occurs.

    This ensures a single source of truth and predictable data flow.

    class _AppState extends State<App> {
      int _count = 0;
    
      void _increment() => setState(() => _count++);
    
      @override
      Component build(BuildContext context) {
        return Column(
          children: [
            CounterDisplay(count: _count), // Data passed down
            CounterControls(
              onIncrement: _increment,   // Callback passed down
            ),
          ],
        );
      }
    }
  11. How Nocterm handles colors and styling

    main

    Instead of manually writing ANSI escape sequences (like \x1b[31m), Nocterm provides a declarative API using Colors constants and TextStyle objects. This abstraction ensures compatibility across different terminal color modes (16-color, 256-color, and True Color/24-bit RGB) and handles the necessary reset codes automatically.

    To apply color, use the Colors class. To apply text decorations like bold or underline, use the bold and underline properties within a TextStyle object.

    // Applying color and style declaratively
    Text('Hello', style: TextStyle(color: Colors.red, bold: true, underline: true))