Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/jakobeha/devolve-ui.js
One UI for both terminal (TUI) and browser
https://github.com/jakobeha/devolve-ui.js
Last synced: about 1 month ago
JSON representation
One UI for both terminal (TUI) and browser
- Host: GitHub
- URL: https://github.com/jakobeha/devolve-ui.js
- Owner: Jakobeha
- License: apache-2.0
- Created: 2022-01-28T22:51:51.000Z (about 3 years ago)
- Default Branch: master
- Last Pushed: 2022-12-09T21:40:16.000Z (about 2 years ago)
- Last Synced: 2024-12-07T03:39:02.720Z (about 2 months ago)
- Language: TypeScript
- Size: 4.3 MB
- Stars: 0
- Watchers: 2
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
# devolve-ui: super simple reactive graphics for browser and terminal
*Disclaimer: this is an early project. It's subject to change and have bugs*
**[Live demo](https://jakobeha.github.io/devolve-ui-demos/index.html) | [Clone to get started](https://github.com/Jakobeha/devolve-ui-demos)**
devolve-ui is a super simple graphics library for canvas-based websites (games) and TUIs. A single devolve-ui app can be embedded in a website *and* run on the command line via `node`.
devolve-ui is JSX-based like React, but renders to canvas or terminal instead of DOM. It should not be used for traditional web pages.
Example:
```jsx
// https://github.com/Jakobeha/devolve-ui-demos/src/readme.tsx
import { DevolveUI, useState, useInterval } from '@raycenity/devolve-ui'const App = ({ name }) => {
const counter = useState(0)
useInterval(1000, () => {
counter.v++
})return (
Hello {name}
{counter.v} seconds
)
}new DevolveUI(App, { name: 'devolve-ui' }).show()
// Works in node or browser (with additional pixi.js script)
```
**Important setup information:** if adding to an existing project, besides installing, you *must* add this to your tsconfig.json for JSX support:
```json5
{
"include": [
/* you probably have this */
"src/**/*.ts",
/* but make sure to also add this */
"src/**/*.tsx"
],
"compilerOptions": {
/* ... */
/** if using esbuild, otherwise "jsx": "react" */
"jsx": "preserve",
"jsxImportSource": "@raycenity/devolve-ui",
}
}
```**Pro tip:** If you debug TUIs in IntelliJ, you can see `console` outputs in a separate tab from the terminal output!
## Installing (with pnpm)
```bash
# if you don't have pnpm installed, uncomment the next line
# curl -fsSL https://get.pnpm.io/install.sh | sh -
pnpm add @raycenity/devolve-ui
```### Repository info
devolve-ui is built using [esbuild](https://esbuild.org/). The package manager used is [pnpm](https://pnpm.io/). Linting is done by [standard](https://standardjs.com/), however we use a slightly modified version removing some warnings (`ts-standardx.mjs`). Docs are generated by [mkdocs](https://mkdocs.org). Feel free to submit issues / pull requests on the [Github](https://github.com/Jakobeha/devolve-ui).
## Features
### Cross-platform
devolve-ui is *cross-platform* (isomorphic): a devolve-ui application may run in both web browsers and terminals (via node.js). When the application is run in the terminal, graphics are much simpler and certain effects and animations are removed, hence the name "devolve"-ui.
When a devolve-ui application is run in the web browser, it uses pixi.js for rendering.
### Super simple
devolve-ui uses JSX and React-style **components**: you write your UI declaratively and use hooks (useState, useEffect, useLazy, useInput) for local state and side-effects. Your UI is literally a function which takes the global state, and returns a render of your application.
Unlike React, the lowercase JSX nodes (**views**) which devolve-ui uses are not HTML elements, they are:
- `hbox`, `vbox`, `zbox`: Layout child views
- `hbox`: Places children horizontally
- `vbox`: Places children vertically
- `zbox`: Places children on top of each other (no position offsets)
- `text`: Contains text
- `solid`: Renders a solid color
- `border`: Renders a border
- `source`: Renders an image, video, or other external graphic
- (WIP unstable) `pixi`: Can only be created via `PixiComponent`. These contain custom pixi components in the browser, and are invisible in TUIs.Another notable difference is the layout system. devolve-ui does not use CSS, instead all node bounds are calculated using only the parent and previous child. As a result, you must specify bounds much more explicitly. See the [Implementation](#Implementation) section for more.
State and contexts are also handled differently. Essentially, `useState` returns a proxy instead of a getter / setter array. The code in React:
```typescript
const [value, setValue] = useState(0)
setValue(value + 5)
```translates in devolve-ui to:
```typescript
const value = useState(0)
value.v += 5 // or value.v = value.v + 5
```See the [State / Lenses](#State-/-Lenses) and [Contexts](#Contexts) sections for more detail.
## Concepts
### Prompt-based GUI
Prompt-based GUI is a new-ish paradigm where your application interfaces with the UI via **prompts**. devolve-ui has built-in support for prompt-based GUI via the `PromptDevolveUI` class. Read [*this article*](https://jakobeha.github.io/devolve-ui/docs/prompt-based-gui.md) for more.
### State / Lenses
Instead of `useState` returning a getter/setter array (`[value, setValue]`), it returns a **lens**. You can get the value of the lens with `lens.v`, and set the value with `lens.v = newValue`.
The key advantage of lenses is that if the lens contains an object, you can get a lens to its property `foo` via `lens.foo`. For example:
```typescript
const parentLens = useState({ foo: { bar: { baz: 0 } } })
parentLens = { foo: { bar: { baz: 5 } } }
```is equivalent to
```typescript
const parentLens = useState({ foo: { bar: { baz: 0 } } })
const childLens = parentLens.foo.bar.baz
childLens.v = 5
```This is particularly useful when you pass the child lens to a child component, like so:
```jsx
const Parent = () => {
const lens = useState({foo: {bar: {baz: 'hello'}}})
// This prints 'hello world' for 2 seconds,
// then prints 'goodbye world'
return (
{lens.foo.bar.baz.v}
)
}const Child = ({lens}) => {
useDelay(2000, () => {
lens.v = 'goodbye'
})
return (
world
)
}
```### Contexts
Contexts allow you to pass props implicitly, similar to React contexts.
However, contexts in devolve-ui work slightly different: they are hooks instead of components.```jsx
// const fooBarContext = createContext() in TypeScript
const fooBarContext = createContext()const Parent = () => {
fooBarContext.useProvide({ foo: 'bar' })
return
}const Child = () => {
const value = fooBarContext.useConsume()
// value is { foo: 'bar' }
return {value.foo}
}
```There are also **state contexts**, which combine the functionality of contexts and states: a state context is a context which provides a **[state lens](#State-/-Lenses)** instead of a value. Children can mutate the state, and the mutation will affect other children who use the same provided context, but not children who use a different provided context.
```jsx
// const fooBarContext = createStateContext() in TypeScript
const fooBarContext = createStateContext()const Grandparent = () => {
// In the first parent, value is { foo: 'bar' } for the first 5 seconds, and { foo: 'baz' } after
// because MutatingChild sets it
// In the second parent, value remains { foo: 'bar' } because that child doesn't set it
return (
)
}const Parent = ({ children }) => {
fooBarContext.useProvide({ foo: 'bar' })
return {children}
}const MutatingChild = () => {
const value = fooBarContext.useConsume()
// value is { foo: 'bar' } for the first 5 seconds, and { foo: 'baz' } after
useDelay(5000, () => { value.foo.v = 'baz' })
return {value.foo.v}
}const Child = () => {
const value = fooBarContext.useConsume()
// value is { foo: 'bar' } forever
return {value.foo.v}
}
```Regular (non-state) contexts are called **props contexts** and are essentially implicit `props` passed from parents to their children. State contexts are essentially implicit `state` passed from parents to their children. See [this article](https://jakobeha.github.io/devolve-ui/docs/model-in-ui.md) for more explanation.
## Implementation
[Source](https://github.com/Jakobeha/devolve-ui)
### Directory overview
- `core`: The main code of devolve-ui
- `core/hooks`: Built-in hooks
- `core/hooks/intrinsic`: Hooks requiring package-private functions and support in `VComponent`
- `core/hooks/extra`: Hooks that you could create from the intrinsic ones
- `core/vdom`: The "DOM" in devolve-ui: nodes, attributes, and JSX.
- `renderer`: Platform-specific rendering
- `prompt`: [Prompt-based GUI](https://jakobeha.github.io/devolve-ui/docs/prompt-based-gui.md) helpers.### Notable types
- `VView`: Virtual "DOM" node, e.g. `box`, `text`, `color`. Immutable: a new view is created on change.
- `VComponent`: Component. Manages state, effects, input, etc. and renders a `VView`
- `Bounds`: A view's bounds depend on the parent and previous view: therefore `Bounds` are literally a function from parent and previous view properties to a `BoundingBox`. See [src/core/vdom/bounds.ts](https://github.com/Jakobeha/devolve-ui/blob/tree/master/core/vdom/bounds.ts)