├── .babelrc
├── .github
└── FUNDING.yml
├── .gitignore
├── .npmignore
├── .prettierignore
├── LICENSE.txt
├── README.md
├── examples
├── animation.jsx
├── components
│ └── AnimatedBox.jsx
├── context.jsx
├── dashboard.jsx
├── demo.jsx
├── dispatch.jsx
├── effects.jsx
├── flex.jsx
├── form.jsx
├── hooks.jsx
├── neo-blessed.jsx
├── progressbar.jsx
├── remove.jsx
├── teardown.jsx
└── utils
│ └── colors.js
├── img
├── demo.gif
└── example.png
├── package-lock.json
├── package.json
├── rollup.config.js
├── run.js
├── src
├── fiber
│ ├── devtools.js
│ ├── events.js
│ ├── fiber.js
│ └── index.js
├── index.js
└── shared
│ ├── solveClass.js
│ └── update.js
└── test
├── endpoint.js
└── suites
└── solveClass.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["@babel/preset-env"],
4 | ["@babel/preset-react"]
5 | ],
6 | "plugins": [
7 | "@babel/transform-flow-strip-types",
8 | "@babel/plugin-proposal-object-rest-spread",
9 | "@babel/plugin-proposal-class-properties"
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: Yomguithereal
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | *.log
3 | node_modules
4 | dist
5 | .idea
6 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | *.log
3 | node_modules
4 | test
5 | examples
6 | *.png
7 | .npmignore
8 | .gitignore
9 | /src
10 | /img
11 | /rollup.config.js
12 | /run.js
13 | /.idea
14 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | dist
2 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Guillaume Plique (Yomguithereal)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-blessed
2 |
3 | A [React](https://facebook.github.io/react/) custom renderer for the [blessed](https://github.com/chjj/blessed) library.
4 |
5 | This renderer should currently be considered as experimental, is subject to change and will only work with React's latest version (`17.x.x`, using Fiber).
6 |
7 | 
8 |
9 | ## Summary
10 |
11 | * [Installation](#installation)
12 | * [Demo](#demo)
13 | * [Usage](#usage)
14 | * [Rendering a basic application](#rendering-a-basic-application)
15 | * [Nodes & text nodes](#nodes--text-nodes)
16 | * [Refs](#refs)
17 | * [Events](#events)
18 | * [Classes](#classes)
19 | * [Using blessed forks](#using-blessed-forks)
20 | * [Using the devtools](#using-the-devtools)
21 | * [Roadmap](#roadmap)
22 | * [FAQ](#faq)
23 | * [Contribution](#contribution)
24 | * [License](#license)
25 |
26 | ## Installation
27 |
28 | You can install `react-blessed` through npm:
29 |
30 | ```bash
31 | # Be sure to install react>=17.0.0 & blessed>=0.1.81 before
32 | npm install blessed react
33 |
34 | # Then just install `react-blessed`
35 | npm install react-blessed
36 | ```
37 |
38 | ## Demo
39 |
40 | For a quick demo of what you could achieve with such a renderer you can clone this repository and check some of the [examples](./examples):
41 |
42 | ```bash
43 | git clone https://github.com/Yomguithereal/react-blessed
44 | cd react-blessed
45 | npm install
46 |
47 | # To see which examples you can run:
48 | npm run demo
49 |
50 | # Then choose one to run:
51 | npm run demo animation
52 | ```
53 |
54 | ## Usage
55 |
56 | ### Rendering a basic application
57 |
58 | ```jsx
59 | import React, {Component} from 'react';
60 | import blessed from 'blessed';
61 | import {render} from 'react-blessed';
62 |
63 | // Rendering a simple centered box
64 | class App extends Component {
65 | render() {
66 | return (
67 |
73 | Hello World!
74 |
75 | );
76 | }
77 | }
78 |
79 | // Creating our screen
80 | const screen = blessed.screen({
81 | autoPadding: true,
82 | smartCSR: true,
83 | title: 'react-blessed hello world'
84 | });
85 |
86 | // Adding a way to quit the program
87 | screen.key(['escape', 'q', 'C-c'], function(ch, key) {
88 | return process.exit(0);
89 | });
90 |
91 | // Rendering the React app using our screen
92 | const component = render(, screen);
93 | ```
94 |
95 | ### Nodes & text nodes
96 |
97 | Any of the blessed [widgets](https://github.com/chjj/blessed#widgets) can be rendered through `react-blessed` by using a lowercased tag title.
98 |
99 | Text nodes, on the other hand, will be rendered by applying the `setContent` method with the given text on the parent node.
100 |
101 | ### Refs
102 |
103 | As with React's DOM renderer, `react-blessed` lets you handle the original blessed nodes, if you ever need them, through refs.
104 |
105 | ```jsx
106 | class CustomList extends Component {
107 | componentDidMount() {
108 |
109 | // Focus on the first box
110 | this.refs.first.focus();
111 | }
112 |
113 | render() {
114 | return (
115 |
116 |
117 | First box.
118 |
119 |
120 | Second box.
121 |
122 |
123 | );
124 | }
125 | }
126 | ```
127 |
128 | ### Events
129 |
130 | Any blessed node event can be caught through a `on`-prefixed listener:
131 |
132 | ```jsx
133 | class Completion extends Component {
134 | constructor(props) {
135 | super(props);
136 |
137 | this.state = {progress: 0, color: 'blue'};
138 |
139 | const interval = setInterval(() => {
140 | if (this.state.progress >= 100)
141 | return clearInterval(interval);
142 |
143 | this.setState({progress: this.state.progress + 1});
144 | }, 50);
145 | }
146 |
147 | render() {
148 | const {progress} = this.state,
149 | label = `Progress - ${progress}%`;
150 |
151 | // See the `onComplete` prop
152 | return this.setState({color: 'green'})}
154 | filled={progress}
155 | style={{bar: {bg: this.state.color}}} />;
156 | }
157 | }
158 | ```
159 |
160 | ### Classes
161 |
162 | For convenience, `react-blessed` lets you handle classes looking like what [react-native](https://facebook.github.io/react-native/docs/style.html#content) proposes.
163 |
164 | Just pass object or an array of objects as the class of your components likewise:
165 |
166 | ```jsx
167 | // Let's say we want all our elements to have a fancy blue border
168 | const stylesheet = {
169 | bordered: {
170 | border: {
171 | type: 'line'
172 | },
173 | style: {
174 | border: {
175 | fg: 'blue'
176 | }
177 | }
178 | }
179 | };
180 |
181 | class App extends Component {
182 | render() {
183 | return (
184 |
185 |
186 | First box.
187 |
188 |
189 | Second box.
190 |
191 |
192 | );
193 | }
194 | }
195 | ```
196 |
197 | You can of course combine classes (note that the given array of classes will be compacted):
198 | ```jsx
199 | // Let's say we want all our elements to have a fancy blue border
200 | const stylesheet = {
201 | bordered: {
202 | border: {
203 | type: 'line'
204 | },
205 | style: {
206 | border: {
207 | fg: 'blue'
208 | }
209 | }
210 | },
211 | magentaBackground: {
212 | style: {
213 | bg: 'magenta'
214 | }
215 | }
216 | };
217 |
218 | class App extends Component {
219 | render() {
220 |
221 | // If this flag is false, then the class won't apply to the second box
222 | const backgroundForSecondBox = this.props.backgroundForSecondBox;
223 |
224 | return (
225 |
226 |
227 | First box.
228 |
229 |
233 | Second box.
234 |
235 |
236 | );
237 | }
238 | }
239 | ```
240 |
241 | ### Using blessed forks
242 |
243 | Because [blessed](https://github.com/chjj/blessed) is not actively maintained in quite a while, you might want to use one of it's forks. To do that, import `createBlessedRenderer` function instead:
244 |
245 | ```
246 | import React, {Component} from 'react';
247 | import blessed from 'neo-blessed';
248 | import {createBlessedRenderer} from 'react-blessed';
249 |
250 | const render = createBlessedRenderer(blessed);
251 | ```
252 |
253 | ### Using the devtools
254 |
255 | `react-blessed` can be used along with React's own devtools for convenience. To do so, just install [`react-devtools`](https://www.npmjs.com/package/react-devtools) in your project and all should work out of the box when running the Electron app, as soon as a `react-blessed` program is running on one of your shells.
256 |
257 | ## Roadmap
258 |
259 | * Full support (meaning every tags and options should be handled by the renderer).
260 | * `react-blessed-contrib` to add some sugar over the [blessed-contrib](https://github.com/yaronn/blessed-contrib) library (probably through full-fledged components).
261 |
262 | ## Faq
263 |
264 | * `
` : To enable interactions, add `mouse={ true }` and/or `keys={ true }`
265 |
266 | ## Contribution
267 |
268 | Contributions are obviously welcome.
269 |
270 | Be sure to add unit tests if relevant and pass them all before submitting your pull request.
271 |
272 | ```bash
273 | # Installing the dev environment
274 | git clone git@github.com:Yomguithereal/react-blessed.git
275 | cd react-blessed
276 | npm install
277 |
278 | # Running the tests
279 | npm test
280 | ```
281 |
282 | ## License
283 |
284 | MIT
285 |
--------------------------------------------------------------------------------
/examples/animation.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import blessed from 'blessed';
3 | import AnimatedBox from './components/AnimatedBox';
4 | import {render} from '../src';
5 |
6 | const screen = blessed.screen({
7 | autoPadding: true,
8 | smartCSR: true,
9 | title: 'react-blessed box animation'
10 | });
11 |
12 | screen.key(['escape', 'q', 'C-c'], function (ch, key) {
13 | return process.exit(0);
14 | });
15 |
16 | render(, screen, inst => console.log('Rendered AnimatedBox!'));
17 |
--------------------------------------------------------------------------------
/examples/components/AnimatedBox.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 |
3 | class AnimatedBox extends Component {
4 | constructor(props) {
5 | super(props);
6 |
7 | this.state = {
8 | position: props.initialPosition || 0,
9 | toRight: true
10 | };
11 |
12 | setInterval(() => {
13 | const {position, toRight} = this.state,
14 | newDirection = position === (toRight ? 90 : 0) ? !toRight : toRight,
15 | newPosition = newDirection ? position + 1 : position - 1;
16 |
17 | this.setState({
18 | position: newPosition,
19 | toRight: newDirection
20 | });
21 | }, props.time || 33.333333);
22 | }
23 | render() {
24 | const position = `${this.state.position}%`;
25 |
26 | return (
27 |
35 | );
36 | }
37 | }
38 |
39 | module.exports = AnimatedBox;
40 |
--------------------------------------------------------------------------------
/examples/context.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component, createContext, useContext} from 'react';
2 | import blessed from 'blessed';
3 | import {render} from '../src';
4 |
5 | // this is a bit weird, since the context provider & consumer and components are all in the same file
6 | // normally these would all be in different places, so the provider's children can grab the context from anywhere
7 |
8 | const DemoContext = createContext();
9 | const {Provider} = DemoContext;
10 |
11 | // app-level provider of demo context
12 | class DemoProvider extends Component {
13 | constructor(props) {
14 | super(props);
15 | this.state = {
16 | demo: 0
17 | };
18 | this.setDemo = this.setDemo.bind(this);
19 | }
20 |
21 | setDemo(value) {
22 | this.setState({...this.state, demo: value});
23 | }
24 |
25 | render() {
26 | return (
27 |
28 | {this.props.children}
29 |
30 | );
31 | }
32 | }
33 |
34 | // wrap a component with demo context consumer
35 | const withDemo = Component => props => {
36 | const context = useContext(DemoContext);
37 | return ;
38 | };
39 |
40 | class AppInner extends Component {
41 | render() {
42 | const {demo, setDemo} = this.props;
43 | return (
44 |
48 | {demo}
49 |
50 |
60 |
69 |
70 | );
71 | }
72 | }
73 |
74 | const App = withDemo(AppInner);
75 |
76 | const screen = blessed.screen({
77 | autoPadding: true,
78 | smartCSR: true,
79 | title: 'react-blessed context demo'
80 | });
81 |
82 | screen.key(['escape', 'q', 'C-c'], function (ch, key) {
83 | return process.exit(0);
84 | });
85 |
86 | render(
87 |
88 |
89 | ,
90 | screen
91 | );
92 |
--------------------------------------------------------------------------------
/examples/dashboard.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import blessed from 'blessed';
3 | import {render} from '../src';
4 |
5 | /**
6 | * Stylesheet
7 | */
8 | const stylesheet = {
9 | bordered: {
10 | border: {
11 | type: 'line'
12 | },
13 | style: {
14 | border: {
15 | fg: 'blue'
16 | }
17 | }
18 | }
19 | };
20 |
21 | /**
22 | * Top level component.
23 | */
24 | class Dashboard extends Component {
25 | render() {
26 | return (
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | );
36 | }
37 | }
38 |
39 | /**
40 | * Log component.
41 | */
42 | class Log extends Component {
43 | render() {
44 | return (
45 |
51 | {'Hello'}, {0}, {'World'}
52 |
53 | );
54 | }
55 | }
56 |
57 | /**
58 | * Request component.
59 | */
60 | class Request extends Component {
61 | render() {
62 | return (
63 |
64 | {0}
65 |
66 | );
67 | }
68 | }
69 |
70 | /**
71 | * Response component.
72 | */
73 | class Response extends Component {
74 | render() {
75 | return (
76 |
83 | );
84 | }
85 | }
86 |
87 | /**
88 | * Jobs component.
89 | */
90 | class Jobs extends Component {
91 | render() {
92 | return (
93 |
100 | );
101 | }
102 | }
103 |
104 | /**
105 | * Progress component.
106 | */
107 | class Progress extends Component {
108 | constructor(props) {
109 | super(props);
110 |
111 | this.state = {progress: 0, color: 'blue'};
112 |
113 | const interval = setInterval(() => {
114 | if (this.state.progress >= 100) return clearInterval(interval);
115 |
116 | this.setState({progress: this.state.progress + 1});
117 | }, 50);
118 | }
119 |
120 | render() {
121 | const {progress} = this.state,
122 | label = `Progress - ${progress}%`;
123 |
124 | return (
125 | this.setState({color: 'green'})}
128 | class={stylesheet.bordered}
129 | filled={progress}
130 | top="60%"
131 | left="60%"
132 | width="40%"
133 | height="10%"
134 | style={{bar: {bg: this.state.color}}}
135 | />
136 | );
137 | }
138 | }
139 |
140 | /**
141 | * Stats component.
142 | */
143 | class Stats extends Component {
144 | render() {
145 | return (
146 |
153 | Some stats
154 |
155 | );
156 | }
157 | }
158 |
159 | /**
160 | * Rendering the screen.
161 | */
162 | const screen = blessed.screen({
163 | autoPadding: true,
164 | smartCSR: true,
165 | title: 'react-blessed dashboard'
166 | });
167 |
168 | screen.key(['escape', 'q', 'C-c'], function (ch, key) {
169 | return process.exit(0);
170 | });
171 |
172 | render(, screen);
173 |
--------------------------------------------------------------------------------
/examples/demo.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import blessed from 'blessed';
3 | import {render} from '../src';
4 |
5 | class App extends Component {
6 | render() {
7 | return (
8 |
12 |
13 |
14 |
15 | Random text here...
16 |
17 | );
18 | }
19 | }
20 |
21 | class InnerBox extends Component {
22 | constructor(props) {
23 | super(props);
24 |
25 | this.state = {
26 | hey: true
27 | };
28 |
29 | setInterval(() => {
30 | this.setState({hey: !this.state.hey});
31 | }, 1000);
32 | }
33 |
34 | render() {
35 | const position = this.props.position;
36 |
37 | const left = position === 'left' ? '2%' : '53%';
38 |
39 | return (
40 |
48 | {this.state.hey ? 'Hey...' : 'Ho...'}
49 |
50 | );
51 | }
52 | }
53 |
54 | class ProgressBar extends Component {
55 | constructor(props) {
56 | super(props);
57 |
58 | this.state = {completion: 0};
59 |
60 | const interval = setInterval(() => {
61 | if (this.state.completion >= 100) return clearInterval(interval);
62 |
63 | this.setState({completion: this.state.completion + 10});
64 | }, 1000);
65 | }
66 |
67 | render() {
68 | return (
69 |
80 | );
81 | }
82 | }
83 |
84 | const screen = blessed.screen({
85 | autoPadding: true,
86 | smartCSR: true,
87 | title: 'react-blessed demo app'
88 | });
89 |
90 | screen.key(['escape', 'q', 'C-c'], function (ch, key) {
91 | return process.exit(0);
92 | });
93 |
94 | const component = render(, screen);
95 |
--------------------------------------------------------------------------------
/examples/dispatch.jsx:
--------------------------------------------------------------------------------
1 | import React, {useReducer} from 'react';
2 | import blessed from 'blessed';
3 | import {render} from '../src';
4 |
5 | const initialState = {count: 0};
6 |
7 | function reducer(state, action) {
8 | switch (action.type) {
9 | case 'increment':
10 | return {count: state.count + 1};
11 | case 'decrement':
12 | return {count: state.count - 1};
13 | default:
14 | throw new Error();
15 | }
16 | }
17 |
18 | const App = () => {
19 | const [demo, dispatch] = useReducer(reducer, initialState);
20 | return (
21 |
25 | {demo.count}
26 |
27 |
37 |
46 |
47 | );
48 | };
49 |
50 | const screen = blessed.screen({
51 | autoPadding: true,
52 | smartCSR: true,
53 | title: 'react-blessed hooks demo'
54 | });
55 |
56 | screen.key(['escape', 'q', 'C-c'], function (ch, key) {
57 | return process.exit(0);
58 | });
59 |
60 | render(, screen);
61 |
--------------------------------------------------------------------------------
/examples/effects.jsx:
--------------------------------------------------------------------------------
1 | import React, {useState, useEffect} from 'react';
2 | import blessed from 'blessed';
3 | import {render} from '../src';
4 |
5 | const WillUnmount = props => {
6 | useEffect(() => {
7 | // On unmount, we set the top level count to -5
8 | return () => props.setCount(-5);
9 | }, [props.setCount]);
10 |
11 | return I will be gone;
12 | };
13 |
14 | const App = () => {
15 | const [count, setCount] = useState(0);
16 |
17 | useEffect(() => {
18 | // Setup interval on mount
19 | const id = setInterval(() => setCount(count => count + 1), 1000);
20 |
21 | // Clear the interval on unmount
22 | return () => clearInterval(id);
23 | }, []);
24 |
25 | useEffect(() => {
26 | // On mount we set the count to 4
27 | setCount(4);
28 | }, []);
29 |
30 | return (
31 |
35 | {count}
36 | {count > 5 && }
37 |
38 | );
39 | };
40 |
41 | const screen = blessed.screen({
42 | autoPadding: true,
43 | smartCSR: true,
44 | title: 'react-blessed effects demo'
45 | });
46 |
47 | screen.key(['escape', 'q', 'C-c'], function (ch, key) {
48 | return process.exit(0);
49 | });
50 |
51 | render(, screen);
52 |
--------------------------------------------------------------------------------
/examples/flex.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import blessed from 'blessed';
3 | import {render} from '../src';
4 |
5 | class App extends Component {
6 | render() {
7 | return (
8 |
12 |
13 |
14 |
15 | Random text here...
16 |
17 | );
18 | }
19 | }
20 |
21 | class InnerBox extends Component {
22 | constructor(props) {
23 | super(props);
24 |
25 | this.state = {
26 | hey: true
27 | };
28 |
29 | setInterval(() => {
30 | this.setState({hey: !this.state.hey});
31 | }, 1000);
32 | }
33 |
34 | render() {
35 | const position = this.props.position;
36 |
37 | const left = position === 'left' ? '2%' : '53%';
38 |
39 | return (
40 |
49 | {this.state.hey ? 'Hey...' : 'Ho...'}
50 |
51 | );
52 | }
53 | }
54 |
55 | class ProgressBar extends Component {
56 | constructor(props) {
57 | super(props);
58 |
59 | this.state = {completion: 0};
60 |
61 | const interval = setInterval(() => {
62 | if (this.state.completion >= 100) return clearInterval(interval);
63 |
64 | this.setState({completion: this.state.completion + 10});
65 | }, 1000);
66 | }
67 |
68 | render() {
69 | return (
70 |
81 | );
82 | }
83 | }
84 |
85 | const screen = blessed.screen({
86 | autoPadding: true,
87 | smartCSR: true,
88 | title: 'react-blessed demo app'
89 | });
90 |
91 | screen.key(['escape', 'q', 'C-c'], function (ch, key) {
92 | return process.exit(0);
93 | });
94 |
95 | const component = render(, screen);
96 |
--------------------------------------------------------------------------------
/examples/form.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import blessed from 'blessed';
3 | import {render} from '../src';
4 | import AnimatedBox from './components/AnimatedBox';
5 |
6 | class Form extends Component {
7 | constructor(props) {
8 | super(props);
9 |
10 | this.state = {
11 | name: ''
12 | };
13 |
14 | this.submit = data => this.setState(state => ({name: data}));
15 | this.cancel = _ => console.log('Form canceled');
16 | }
17 | render() {
18 | return (
19 |
47 | );
48 | }
49 | }
50 |
51 | const screen = blessed.screen({
52 | autoPadding: true,
53 | // smartCSR: true,
54 | title: 'react-blessed form example'
55 | });
56 |
57 | screen.key(['escape', 'q', 'C-c'], function (ch, key) {
58 | return process.exit(0);
59 | });
60 |
61 | render(, screen, inst => console.log('Rendered Form!'));
62 |
--------------------------------------------------------------------------------
/examples/hooks.jsx:
--------------------------------------------------------------------------------
1 | import React, {useState} from 'react';
2 | import blessed from 'blessed';
3 | import {render} from '../src';
4 |
5 | // NOTE: hooks require react@next version, but will probly be in 16.7.0
6 |
7 | const App = () => {
8 | const [demo, setDemo] = useState(0);
9 | return (
10 |
14 | {demo}
15 |
16 |
26 |
35 |
36 | );
37 | };
38 |
39 | const screen = blessed.screen({
40 | autoPadding: true,
41 | smartCSR: true,
42 | title: 'react-blessed hooks demo'
43 | });
44 |
45 | screen.key(['escape', 'q', 'C-c'], function (ch, key) {
46 | return process.exit(0);
47 | });
48 |
49 | render(, screen);
50 |
--------------------------------------------------------------------------------
/examples/neo-blessed.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import blessed from 'neo-blessed';
3 | import {createBlessedRenderer} from '../src';
4 |
5 | const render = createBlessedRenderer(blessed);
6 |
7 | class App extends Component {
8 | render() {
9 | return (
10 |
14 | This example uses neo-blessed fork of blessed library.
15 |
16 | );
17 | }
18 | }
19 |
20 | const screen = blessed.screen({
21 | autoPadding: true,
22 | smartCSR: true,
23 | title: 'react-blessed demo app'
24 | });
25 |
26 | screen.key(['escape', 'q', 'C-c'], function (ch, key) {
27 | return process.exit(0);
28 | });
29 |
30 | const component = render(, screen);
31 |
--------------------------------------------------------------------------------
/examples/progressbar.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import blessed from 'blessed';
3 | import {render} from '../src';
4 |
5 | class ProgressBox extends Component {
6 | constructor(props) {
7 | super(props);
8 |
9 | this.state = {
10 | position: 0,
11 | toRight: true
12 | };
13 |
14 | setInterval(() => {
15 | const {position, toRight} = this.state,
16 | newDirection = position === (toRight ? 90 : 0) ? !toRight : toRight,
17 | newPosition = newDirection ? position + 1 : position - 1;
18 |
19 | this.setState({
20 | position: newPosition,
21 | toRight: newDirection
22 | });
23 | }, 30);
24 | }
25 | render() {
26 | const position = `${this.state.position}%`;
27 |
28 | return (
29 |
36 |
47 |
48 | );
49 | }
50 | }
51 |
52 | const screen = blessed.screen({
53 | autoPadding: true,
54 | smartCSR: true,
55 | title: 'react-blessed box animation'
56 | });
57 |
58 | screen.key(['escape', 'q', 'C-c'], function (ch, key) {
59 | return process.exit(0);
60 | });
61 |
62 | render(, screen, inst => console.log('Rendered ProgressBox!'));
63 |
--------------------------------------------------------------------------------
/examples/remove.jsx:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import blessed from 'blessed';
3 | import {render} from '../src';
4 |
5 | class RemovesChild extends Component {
6 | constructor(props) {
7 | super(props);
8 |
9 | this.state = {renderChild: true};
10 |
11 | setInterval(() => {
12 | this.setState(state => ({renderChild: !state.renderChild}));
13 | }, props.freq || 500);
14 | }
15 | render() {
16 | const {renderChild} = this.state;
17 |
18 | return (
19 |
27 | {renderChild && (
28 |
35 | I will be removed
36 |
37 | )}
38 |
39 | );
40 | }
41 | }
42 |
43 | const screen = blessed.screen({
44 | autoPadding: true,
45 | smartCSR: true,
46 | title: 'react-blessed box animation'
47 | });
48 |
49 | screen.key(['escape', 'q', 'C-c'], function (ch, key) {
50 | return process.exit(0);
51 | });
52 |
53 | render(, screen, inst => console.log('Rendered RemovesChild!'));
54 |
--------------------------------------------------------------------------------
/examples/teardown.jsx:
--------------------------------------------------------------------------------
1 | import {once} from 'events';
2 |
3 | import blessed from 'blessed';
4 | import React from 'react';
5 | import {render} from '../src';
6 |
7 | async function main() {
8 | const screen = blessed.screen({
9 | smartCSR: true
10 | });
11 |
12 | render(
13 |
14 |
20 | press q to try to exit
21 |
22 |
23 |
24 |
25 | ,
26 | screen
27 | );
28 |
29 | screen.key(['escape', 'q', 'C-c'], () => screen.destroy());
30 |
31 | await once(screen, 'destroy');
32 |
33 | console.error("look out for zombies; press Ctrl-C when you've seen enough");
34 | }
35 |
36 | function Ticker(props) {
37 | const tick = useTicker(props.ms);
38 |
39 | return {`tick: ${tick}`};
40 | }
41 |
42 | function useTicker(ms) {
43 | const [tick, set_tick] = React.useState(0);
44 | React.useEffect(() => {
45 | console.error('ticker starting');
46 | const timer = setTimeout(tick, ms);
47 | return () => {
48 | console.error('ticker ending');
49 | clearTimeout(timer);
50 | };
51 | function tick() {
52 | console.error('ticker ticking');
53 | timer.refresh();
54 | set_tick(tick => tick + 1);
55 | }
56 | }, []);
57 | return tick;
58 | }
59 |
60 | main().then(null, err => {
61 | console.error(err.stack || err);
62 | process.exit(1);
63 | });
64 |
--------------------------------------------------------------------------------
/examples/utils/colors.js:
--------------------------------------------------------------------------------
1 | // copied directly from https://github.com/facebook/rebound-js
2 | // rewritten for babel/.eslintrc
3 |
4 | // BSD License
5 |
6 | // For the rebound-js software
7 |
8 | // Copyright (c) 2014, Facebook, Inc. All rights reserved.
9 |
10 | // Redistribution and use in source and binary forms, with or without modification,
11 | // are permitted provided that the following conditions are met:
12 |
13 | // * Redistributions of source code must retain the above copyright notice, this
14 | // list of conditions and the following disclaimer.
15 |
16 | // * Redistributions in binary form must reproduce the above copyright notice,
17 | // this list of conditions and the following disclaimer in the documentation
18 | // and/or other materials provided with the distribution.
19 |
20 | // * Neither the name Facebook nor the names of its contributors may be used to
21 | // endorse or promote products derived from this software without specific
22 | // prior written permission.
23 |
24 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
25 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
26 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
27 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
28 | // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
29 | // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
30 | // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
31 | // ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
32 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
33 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34 |
35 | const colorCache = {};
36 | // Here are a couple of function to convert colors between hex codes and RGB
37 | // component values. These are handy when performing color
38 | // tweening animations.
39 |
40 | export function hexToRGB(c) {
41 | let color = c;
42 | if (colorCache[color]) {
43 | return colorCache[color];
44 | }
45 | color = color.replace('#', '');
46 | if (color.length === 3) {
47 | color = color[0] + color[0] + color[1] + color[1] + color[2] + color[2];
48 | }
49 | const parts = color.match(/.{2}/g);
50 |
51 | const ret = {
52 | r: parseInt(parts[0], 16),
53 | g: parseInt(parts[1], 16),
54 | b: parseInt(parts[2], 16)
55 | };
56 |
57 | colorCache[color] = ret;
58 | return ret;
59 | }
60 |
61 | export function rgbToHex(red, green, blue) {
62 | let r = red.toString(16);
63 | let g = green.toString(16);
64 | let b = blue.toString(16);
65 | r = r.length < 2 ? '0' + r : r;
66 | g = g.length < 2 ? '0' + g : g;
67 | b = b.length < 2 ? '0' + b : b;
68 | return '#' + r + g + b;
69 | }
70 |
71 | // This helper function does a linear interpolation of a value from
72 | // one range to another. This can be very useful for converting the
73 | // motion of a Spring to a range of UI property values. For example a
74 | // spring moving from position 0 to 1 could be interpolated to move a
75 | // view from pixel 300 to 350 and scale it from 0.5 to 1. The current
76 | // position of the `Spring` just needs to be run through this method
77 | // taking its input range in the _from_ parameters with the property
78 | // animation range in the _to_ parameters.
79 | export function mapValueInRange(value, fromLow, fromHigh, toLow, toHigh) {
80 | let fromRangeSize = fromHigh - fromLow;
81 | let toRangeSize = toHigh - toLow;
82 | let valueScale = (value - fromLow) / fromRangeSize;
83 | return toLow + valueScale * toRangeSize;
84 | }
85 |
86 | // Interpolate two hex colors in a 0 - 1 range or optionally provide a
87 | // custom range with fromLow,fromHight. The output will be in hex by default
88 | // unless asRGB is true in which case it will be returned as an rgb string.
89 | export function interpolateColor(val, start, end, low, high, asRGB) {
90 | let fromLow = low === undefined ? 0 : low;
91 | let fromHigh = high === undefined ? 1 : high;
92 | let startColor = hexToRGB(start);
93 | let endColor = hexToRGB(end);
94 | let r = Math.floor(
95 | mapValueInRange(val, fromLow, fromHigh, startColor.r, endColor.r)
96 | );
97 | let g = Math.floor(
98 | mapValueInRange(val, fromLow, fromHigh, startColor.g, endColor.g)
99 | );
100 | let b = Math.floor(
101 | mapValueInRange(val, fromLow, fromHigh, startColor.b, endColor.b)
102 | );
103 | if (asRGB) {
104 | return 'rgb(' + r + ',' + g + ',' + b + ')';
105 | }
106 | return rgbToHex(r, g, b);
107 | }
108 |
--------------------------------------------------------------------------------
/img/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yomguithereal/react-blessed/b686438af2726111637ef17b998f0f6967623640/img/demo.gif
--------------------------------------------------------------------------------
/img/example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Yomguithereal/react-blessed/b686438af2726111637ef17b998f0f6967623640/img/example.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-blessed",
3 | "version": "0.7.2",
4 | "description": "A react renderer for blessed.",
5 | "main": "./dist/index.js",
6 | "module": "./dist/index.es.js",
7 | "scripts": {
8 | "build": "npm run clean && rollup -c",
9 | "clean": "rimraf dist",
10 | "demo": "node run.js",
11 | "prepublish": "npm run build",
12 | "prepublishOnly": "npm run build",
13 | "prettier": "prettier --write './**/*.js' './**/*.jsx'",
14 | "test": "mocha -R spec --require @babel/register ./test/endpoint.js"
15 | },
16 | "repository": {
17 | "type": "git",
18 | "url": "git+https://github.com/yomguithereal/react-blessed.git"
19 | },
20 | "keywords": [
21 | "blessed",
22 | "react",
23 | "renderer",
24 | "cli",
25 | "interface"
26 | ],
27 | "author": "yomguithereal",
28 | "license": "MIT",
29 | "bugs": {
30 | "url": "https://github.com/yomguithereal/react-blessed/issues"
31 | },
32 | "browserslist": "maintained node versions",
33 | "homepage": "https://github.com/yomguithereal/react-blessed#readme",
34 | "devDependencies": {
35 | "@babel/cli": "^7.12.10",
36 | "@babel/core": "^7.12.10",
37 | "@babel/plugin-proposal-class-properties": "^7.12.1",
38 | "@babel/plugin-proposal-object-rest-spread": "^7.12.1",
39 | "@babel/plugin-transform-flow-strip-types": "^7.12.10",
40 | "@babel/preset-env": "^7.12.11",
41 | "@babel/preset-react": "^7.12.10",
42 | "@babel/register": "^7.12.10",
43 | "@yomguithereal/prettier-config": "^1.1.0",
44 | "blessed": "^0.1.81",
45 | "invariant": "^2.2.0",
46 | "lodash": "^4.17.20",
47 | "mocha": "^8.2.1",
48 | "neo-blessed": "^0.2.0",
49 | "prettier": "^2.2.1",
50 | "react": "^17.0.1",
51 | "react-devtools-core": "4.10.1",
52 | "rimraf": "^3.0.2",
53 | "rollup": "^2.38.5",
54 | "rollup-plugin-babel": "^4.4.0",
55 | "rollup-plugin-commonjs": "^10.1.0",
56 | "rollup-plugin-node-resolve": "^5.2.0"
57 | },
58 | "peerDependencies": {
59 | "blessed": ">=0.1.81 <0.2.0",
60 | "react": ">=17.0.1 <18.0.0",
61 | "react-devtools-core": ">=4.10.1 <5.0.0"
62 | },
63 | "peerDependenciesMeta": {
64 | "blessed": {
65 | "optional": true
66 | },
67 | "react-devtools-core": {
68 | "optional": true
69 | }
70 | },
71 | "dependencies": {
72 | "react-reconciler": "^0.26.1"
73 | },
74 | "prettier": "@yomguithereal/prettier-config"
75 | }
76 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import babel from 'rollup-plugin-babel';
2 | import commonjs from 'rollup-plugin-commonjs';
3 | import resolve from 'rollup-plugin-node-resolve';
4 |
5 | import pkg from './package.json';
6 |
7 | export default {
8 | input: 'src/index.js',
9 | output: [
10 | {
11 | file: pkg.main,
12 | format: 'cjs',
13 | sourcemap: true
14 | },
15 | {
16 | file: pkg.module,
17 | format: 'es',
18 | sourcemap: true
19 | }
20 | ],
21 | external: ['blessed', 'ws', 'react-devtools-core', 'react-reconciler'],
22 | plugins: [
23 | babel({
24 | exclude: 'node_modules/**'
25 | }),
26 | resolve(),
27 | commonjs({
28 | ignore: ['ws', 'react-devtools-core']
29 | })
30 | ]
31 | };
32 |
--------------------------------------------------------------------------------
/run.js:
--------------------------------------------------------------------------------
1 | require('@babel/register')({
2 | presets: [['@babel/preset-env'], ['@babel/preset-react']]
3 | });
4 | const argv = process.argv.slice(2);
5 | const example = argv[0];
6 | const examples = [
7 | 'animation',
8 | 'context',
9 | 'dashboard',
10 | 'demo',
11 | 'dispatch',
12 | 'effects',
13 | 'form',
14 | 'hooks',
15 | 'neo-blessed',
16 | 'progressbar',
17 | 'remove',
18 | 'teardown'
19 | ];
20 |
21 | if (examples.indexOf(example) === -1) {
22 | console.warn(
23 | 'Invalid example "%s" provided. Must be one of:\n *',
24 | example,
25 | examples.join('\n * ')
26 | );
27 | process.exit(0);
28 | }
29 |
30 | require('./examples/' + example);
31 |
--------------------------------------------------------------------------------
/src/fiber/devtools.js:
--------------------------------------------------------------------------------
1 | /* global __REACT_DEVTOOLS_GLOBAL_HOOK__ */
2 |
3 | module.exports = {
4 | version: require('react').version
5 | };
6 |
7 | try {
8 | require.resolve('react-devtools-core');
9 | const defineProperty = Object.defineProperty;
10 | defineProperty(global, 'WebSocket', {
11 | value: require('ws')
12 | });
13 | defineProperty(global, 'window', {
14 | value: global
15 | });
16 | const {connectToDevTools} = require('react-devtools-core');
17 |
18 | connectToDevTools({
19 | isAppActive() {
20 | // Don't steal the DevTools from currently active app.
21 | return true;
22 | },
23 | host: 'localhost',
24 | port: 8097,
25 | resolveRNStyle: null // TODO maybe: require('flattenStyle')
26 | });
27 | } catch (err) {
28 | // no devtools installed...
29 | }
30 |
--------------------------------------------------------------------------------
/src/fiber/events.js:
--------------------------------------------------------------------------------
1 | const startCase = string =>
2 | string
3 | .replace(/(?:^\w|[A-Z]|\b\w|\s+)/g, function (match) {
4 | return +match === 0 ? '' : match.toUpperCase();
5 | })
6 | .replace(/[^A-Za-z0-9 ]+/, '');
7 |
8 | const blacklist = [
9 | 'adopt',
10 | 'attach',
11 | 'destroy',
12 | 'reparent',
13 | 'parsed content',
14 | 'set content'
15 | ];
16 | const eventName = event => `on${startCase(event)}`;
17 |
18 | const eventListener = (node, event, ...args) => {
19 | if (node._updating) return;
20 |
21 | const handler = node.props[eventName(event)];
22 |
23 | /*
24 | if (blacklist.indexOf(event) === -1) {
25 | if (handler) {
26 | console.log(event, ': ', startCase(event).replace(/ /g, ''));
27 | }
28 | }
29 | */
30 |
31 | if (typeof handler === 'function') {
32 | if (event === 'focus' || event === 'blur') {
33 | args[0] = node;
34 | }
35 | handler(...args);
36 | }
37 | };
38 |
39 | export default eventListener;
40 |
--------------------------------------------------------------------------------
/src/fiber/fiber.js:
--------------------------------------------------------------------------------
1 | /* @flow */
2 | import type {HostConfig, Reconciler} from 'react-fiber-types';
3 | import ReactFiberReconciler from 'react-reconciler';
4 | import eventListener from './events';
5 | import update from '../shared/update';
6 | import solveClass from '../shared/solveClass';
7 | import debounce from 'lodash/debounce';
8 | import injectIntoDevToolsConfig from './devtools';
9 |
10 | const emptyObject = {};
11 | let runningEffects = [];
12 |
13 | const createBlessedRenderer = function (blessed) {
14 | type Instance = {
15 | type: string,
16 | props: Object,
17 | _eventListener: Function,
18 | _updating: boolean,
19 | screen: typeof blessed.Screen
20 | };
21 |
22 | let screenRef = null;
23 |
24 | const BlessedReconciler = ReactFiberReconciler({
25 | supportsMutation: true,
26 | supportsPersistence: false,
27 |
28 | getRootHostContext(rootContainerInstance: Container): HostContext {
29 | return emptyObject;
30 | },
31 | getChildHostContext(
32 | parentHostContext: HostContext,
33 | type: string
34 | ): HostContext {
35 | return emptyObject;
36 | },
37 | getPublicInstance(instance) {
38 | return instance;
39 | },
40 |
41 | createInstance(
42 | type: string,
43 | props: Props,
44 | rootContainerInstance: Container,
45 | hostContext: HostContext,
46 | internalInstanceHandle: Object
47 | ) {
48 | const {children, ...appliedProps} = solveClass(props);
49 | const blessedTypePrefix = 'blessed-';
50 | if (type.startsWith(blessedTypePrefix)) {
51 | type = type.slice(blessedTypePrefix.length);
52 | }
53 | const instance = blessed[type]({...appliedProps, screen: screenRef});
54 | instance.props = props;
55 | instance._eventListener = (...args) => eventListener(instance, ...args);
56 | instance.on('event', instance._eventListener);
57 |
58 | return instance;
59 | },
60 |
61 | appendInitialChild(
62 | parentInstance: Instance,
63 | child: Instance | TextInstance
64 | ): void {
65 | parentInstance.append(child);
66 | },
67 |
68 | finalizeInitialChildren(
69 | instance: Instance,
70 | type: string,
71 | props: Props,
72 | rootContainerInstance: Container
73 | ): boolean {
74 | const {children, ...appliedProps} = solveClass(props);
75 | update(instance, appliedProps);
76 | instance.props = props;
77 | return false;
78 | },
79 |
80 | prepareUpdate(
81 | instance: Instance,
82 | type: string,
83 | oldProps: Props,
84 | newProps: Props,
85 | rootContainerInstance: Container,
86 | hostContext: HostContext
87 | ): null | Array {
88 | return solveClass(newProps);
89 | },
90 |
91 | shouldSetTextContent(props: Props): boolean {
92 | return false;
93 | },
94 |
95 | now: Date.now,
96 |
97 | createTextInstance(
98 | text: string,
99 | rootContainerInstance: Container,
100 | hostContext: HostContext,
101 | internalInstanceHandle: OpaqueHandle
102 | ): TextInstance {
103 | return blessed.text({content: text, screen: screenRef});
104 | },
105 |
106 | prepareForCommit() {
107 | // noop but must return `null` to avoid issues related to node removal
108 | return null;
109 | },
110 |
111 | resetAfterCommit() {
112 | // noop
113 | },
114 |
115 | commitMount(
116 | instance: Instance,
117 | type: string,
118 | newProps: Props,
119 | internalInstanceHandle: Object
120 | ) {
121 | throw new Error(
122 | 'commitMount not implemented. Please post a reproducible use case that calls this method at https://github.com/Yomguithereal/react-blessed/issues/new'
123 | );
124 | instance.screen.debouncedRender();
125 | // noop
126 | },
127 |
128 | commitUpdate(
129 | instance: Instance,
130 | updatePayload: Array,
131 | type: string,
132 | oldProps: Props,
133 | newProps: Props,
134 | internalInstanceHandle: Object
135 | ): void {
136 | instance._updating = true;
137 | update(instance, updatePayload);
138 | // update event handler pointers
139 | instance.props = newProps;
140 | instance._updating = false;
141 | instance.screen.debouncedRender();
142 | },
143 |
144 | commitTextUpdate(
145 | textInstance: TextInstance,
146 | oldText: string,
147 | newText: string
148 | ): void {
149 | textInstance.setContent(newText);
150 | textInstance.screen.debouncedRender();
151 | },
152 |
153 | appendChild(
154 | parentInstance: Instance | Container,
155 | child: Instance | TextInstance
156 | ): void {
157 | parentInstance.append(child);
158 | },
159 |
160 | appendChildToContainer(
161 | parentInstance: Instance | Container,
162 | child: Instance | TextInstance
163 | ): void {
164 | parentInstance.append(child);
165 | },
166 |
167 | insertBefore(
168 | parentInstance: Instance | Container,
169 | child: Instance | TextInstance,
170 | beforeChild: Instance | TextInstance
171 | ): void {
172 | // pretty sure everything is absolutely positioned so insertBefore ~= append
173 | parentInstance.append(child);
174 | },
175 |
176 | insertInContainerBefore(
177 | parentInstance: Instance | Container,
178 | child: Instance | TextInstance,
179 | beforeChild: Instance | TextInstance
180 | ): void {
181 | // pretty sure everything is absolutely positioned so insertBefore ~= append
182 | parentInstance.append(child);
183 | },
184 |
185 | removeChild(
186 | parentInstance: Instance | Container,
187 | child: Instance | TextInstance
188 | ): void {
189 | parentInstance.remove(child);
190 | child.off('event', child._eventListener);
191 | child.forDescendants(function (el) {
192 | el.off('event', child._eventListener);
193 | });
194 | child.destroy();
195 | },
196 |
197 | removeChildFromContainer(
198 | parentInstance: Instance | Container,
199 | child: Instance | TextInstance
200 | ): void {
201 | parentInstance.remove(child);
202 | child.off('event', child._eventListener);
203 | child.forDescendants(function (el) {
204 | el.off('event', child._eventListener);
205 | });
206 | child.destroy();
207 | },
208 |
209 | resetTextContent(instance: Instance): void {
210 | instance.setContent('');
211 | },
212 |
213 | clearContainer(container: Container): void {
214 | container.render();
215 | }
216 | });
217 |
218 | BlessedReconciler.injectIntoDevTools(injectIntoDevToolsConfig);
219 |
220 | const roots = new Map();
221 |
222 | return function render(element, screen, callback) {
223 | screenRef = screen;
224 |
225 | // TODO: doesn't this leak? Shouldn't we use some weak map?
226 | let root = roots.get(screen);
227 | if (!root) {
228 | root = BlessedReconciler.createContainer(screen);
229 | roots.set(screen, root);
230 |
231 | screen.once('destroy', () => {
232 | BlessedReconciler.updateContainer(null, root, null);
233 | roots.delete(screen);
234 | });
235 | }
236 |
237 | // render at most every 16ms. Should sync this with the screen refresh rate
238 | // probably if possible
239 | screen.debouncedRender = debounce(() => screen.render(), 16);
240 | BlessedReconciler.updateContainer((element: any), root, null, callback);
241 | screen.debouncedRender();
242 | return BlessedReconciler.getPublicRootInstance(root);
243 | };
244 | };
245 |
246 | module.exports = {
247 | render: function render(element, screen, callback) {
248 | const blessed = require('blessed');
249 | const renderer = createBlessedRenderer(blessed);
250 | return renderer(element, screen, callback);
251 | },
252 | createBlessedRenderer: createBlessedRenderer
253 | };
254 |
--------------------------------------------------------------------------------
/src/fiber/index.js:
--------------------------------------------------------------------------------
1 | import './devtools';
2 | export * from './fiber';
3 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | export * from './fiber';
2 |
--------------------------------------------------------------------------------
/src/shared/solveClass.js:
--------------------------------------------------------------------------------
1 | /**
2 | * React Blessed Classes Solving
3 | * ==============================
4 | *
5 | * Solving a component's classes to apply correct props to an element.
6 | */
7 | import merge from 'lodash/merge';
8 | const emptyArray = [];
9 |
10 | /**
11 | * Solves the given props by applying classes.
12 | *
13 | * @param {object} props - The component's props.
14 | * @return {object} - The solved props.
15 | */
16 | export default function solveClass(props) {
17 | let {class: classes, ...rest} = props;
18 |
19 | const args = [{}];
20 |
21 | if (classes) args.push.apply(args, emptyArray.concat(classes));
22 |
23 | args.push(rest);
24 |
25 | return merge.apply(null, args);
26 | }
27 |
--------------------------------------------------------------------------------
/src/shared/update.js:
--------------------------------------------------------------------------------
1 | /**
2 | * React Blessed Update Schemes
3 | * =============================
4 | *
5 | * Applying updates to blessed nodes correctly.
6 | */
7 | import merge from 'lodash/merge';
8 |
9 | const RAW_ATTRIBUTES = new Set([
10 | // Alignment, Orientation & Presentation
11 | 'align',
12 | 'valign',
13 | 'orientation',
14 | 'shrink',
15 | 'padding',
16 | 'tags',
17 | 'shadow',
18 |
19 | // Font-related
20 | 'font',
21 | 'fontBold',
22 | 'fch',
23 | 'ch',
24 | 'bold',
25 | 'underline',
26 |
27 | // Flags
28 | 'clickable',
29 | 'input',
30 | 'keyable',
31 | 'hidden',
32 | 'visible',
33 | 'scrollable',
34 | 'draggable',
35 | 'interactive',
36 |
37 | // Position
38 | 'left',
39 | 'right',
40 | 'top',
41 | 'bottom',
42 | 'aleft',
43 | 'aright',
44 | 'atop',
45 | 'abottom',
46 |
47 | // Size
48 | 'width',
49 | 'height',
50 |
51 | // Checkbox
52 | 'checked',
53 |
54 | // Misc
55 | 'name'
56 | ]);
57 |
58 | /**
59 | * Updates the given blessed node.
60 | *
61 | * @param {BlessedNode} node - Node to update.
62 | * @param {object} options - Props of the component without children.
63 | */
64 | export default function update(node, options) {
65 | // TODO: enforce some kind of shallow equality?
66 | // TODO: handle position
67 |
68 | const selectQue = [];
69 |
70 | for (let key in options) {
71 | let value = options[key];
72 |
73 | if (key === 'selected' && node.select)
74 | selectQue.push({
75 | node,
76 | value: typeof value === 'string' ? +value : value
77 | });
78 | // Setting label
79 | else if (key === 'label') node.setLabel(value);
80 | // Removing hoverText
81 | else if (key === 'hoverText' && !value) node.removeHover();
82 | // Setting hoverText
83 | else if (key === 'hoverText' && value) node.setHover(value);
84 | // Setting content
85 | else if (key === 'content') node.setContent(value);
86 | // Updating style
87 | else if (key === 'style') node.style = merge({}, node.style, value);
88 | // Updating items
89 | else if (key === 'items') node.setItems(value);
90 | // Border edge case
91 | else if (key === 'border') node.border = merge({}, node.border, value);
92 | // Textarea value
93 | else if (key === 'value' && node.setValue) node.setValue(value);
94 | // Progress bar
95 | else if (key === 'filled' && node.filled !== value) node.setProgress(value);
96 | // Table / ListTable rows / data
97 | else if ((key === 'rows' || key === 'data') && node.setData)
98 | node.setData(value);
99 | else if (key === 'focused' && value && !node[key]) node.focus();
100 | // Raw attributes
101 | else if (RAW_ATTRIBUTES.has(key)) node[key] = value;
102 | }
103 |
104 | selectQue.forEach(({node, value}) => node.select(value));
105 | }
106 |
--------------------------------------------------------------------------------
/test/endpoint.js:
--------------------------------------------------------------------------------
1 | /**
2 | * React Blessed Unit Testing Endpoint
3 | * ====================================
4 | *
5 | * Requiring the tests.
6 | */
7 | require('./suites/solveClass');
8 |
--------------------------------------------------------------------------------
/test/suites/solveClass.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert';
2 | import solveClass from '../../src/shared/solveClass';
3 |
4 | describe('solveClass', function () {
5 | it('should merge a single class into props.', function () {
6 | assert.deepEqual(
7 | solveClass({
8 | class: {
9 | border: {
10 | type: 'line'
11 | },
12 | style: {
13 | border: {
14 | fg: 'red'
15 | }
16 | }
17 | },
18 | border: {
19 | type: 'dashed'
20 | },
21 | style: {
22 | bg: 'green'
23 | }
24 | }),
25 | {
26 | border: {
27 | type: 'dashed'
28 | },
29 | style: {
30 | border: {
31 | fg: 'red'
32 | },
33 | bg: 'green'
34 | }
35 | }
36 | );
37 |
38 | assert.deepEqual(
39 | solveClass({
40 | class: {
41 | border: {
42 | type: 'line'
43 | },
44 | style: {
45 | border: {
46 | fg: 'red'
47 | }
48 | }
49 | },
50 | style: {
51 | bg: 'green'
52 | }
53 | }),
54 | {
55 | border: {
56 | type: 'line'
57 | },
58 | style: {
59 | border: {
60 | fg: 'red'
61 | },
62 | bg: 'green'
63 | }
64 | }
65 | );
66 | });
67 |
68 | it('should be possible to merge several classes into props.', function () {
69 | assert.deepEqual(
70 | solveClass({
71 | class: [
72 | {
73 | style: {
74 | border: {
75 | fg: 'red'
76 | }
77 | }
78 | },
79 | {
80 | border: {
81 | type: 'line'
82 | }
83 | }
84 | ],
85 | border: {
86 | type: 'dashed'
87 | },
88 | style: {
89 | bg: 'green'
90 | }
91 | }),
92 | {
93 | border: {
94 | type: 'dashed'
95 | },
96 | style: {
97 | border: {
98 | fg: 'red'
99 | },
100 | bg: 'green'
101 | }
102 | }
103 | );
104 | });
105 |
106 | it('the given class array should be compacted.', function () {
107 | assert.deepEqual(
108 | solveClass({
109 | class: [
110 | {
111 | style: {
112 | border: {
113 | fg: 'red'
114 | }
115 | }
116 | },
117 | {
118 | border: {
119 | type: 'line'
120 | }
121 | },
122 | false && {
123 | fg: 'yellow'
124 | }
125 | ],
126 | border: {
127 | type: 'dashed'
128 | },
129 | style: {
130 | bg: 'green'
131 | }
132 | }),
133 | {
134 | border: {
135 | type: 'dashed'
136 | },
137 | style: {
138 | border: {
139 | fg: 'red'
140 | },
141 | bg: 'green'
142 | }
143 | }
144 | );
145 | });
146 | });
147 |
--------------------------------------------------------------------------------