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
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ FetchContent_MakeAvailable(hjson)
FetchContent_Declare(
litehtml
GIT_REPOSITORY https://github.com/litehtml/litehtml.git
GIT_TAG d4453f5d4e03cd4d902b867ca553d0ad81b09939 # I had some problems with the latest version, so I used this one
GIT_TAG 8836bc1bc35ca0cfd71dc0386ef841d5cbc3bd5e
)
set(LITEHTML_BUILD_TESTING OFF CACHE BOOL "Skip building tests" FORCE)
FetchContent_MakeAvailable(litehtml)
Expand Down
26 changes: 22 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ The goal is to provide a simple way to build native desktop applications with si
## Features

- Cross-platform (Windows, macOS, Linux)
- Language agnostic (communicates via stdin/stdout)
- Language agnostic (communicates via stdin/stdout, Unix sockets, or named pipes)
- Supports a reduced sub-set of HTML/CSS for UI layouting and styling
- Pane-based layout system
- Interactive widgets:
Expand Down Expand Up @@ -58,6 +58,7 @@ The goal is to provide a simple way to build native desktop applications with si
- [Todo List](./example/todo): Simple todo list app
- [Chat](./example/chat): Simple chat interface with message input and display (no real networking, just simulates a conversation)
- [IRC Client](./example/irc): A working, but simple IRC client
- [IPC Transports](./example/ipc): Demonstrates connecting via Unix domain socket or named pipe instead of stdin/stdout

All these examples are based on the Go SDK.

Expand All @@ -67,12 +68,13 @@ This uses litehtml for html/css layouting. This means that only a reduced sub-se

## Usage

stdui is a **compiled C++ binary** that opens a native OS window and renders HTML with interactive widgets. Your application controls it by spawning it as a child process and exchanging **newline-delimited JSON** over stdin/stdout. Any language that can start a process and read/write pipes can drive it.
stdui is a **compiled C++ binary** that opens a native OS window and renders HTML with interactive widgets. Your application controls it by spawning it as a child process and exchanging **newline-delimited JSON**. By default communication uses stdin/stdout; Unix domain sockets and named pipes are also supported. Any language that can start a process and read/write pipes (or connect to a socket) can drive it.

```
Your App (Go, Python, anything)
│ stdin → JSON commands
│ stdout ← JSON events
│ JSON commands →
│ ← JSON events
│ (stdin/stdout, Unix socket, or named pipe)
stdui binary (C++)
┌-----------------------------─┐
Expand Down Expand Up @@ -166,6 +168,22 @@ stdui → You {"action":"window-closed"}

That's the entire model. Every interaction follows this same pattern: send action, receive events, send new action.

### Alternative Transports

By default stdui reads/writes via stdin/stdout. You can also connect over a **Unix domain socket** or a **named pipe** (Windows named pipe on Windows, Unix domain socket alias on Unix/macOS) by passing a CLI flag when spawning the binary:

```sh
# Unix / macOS
./stdui --socket /tmp/myapp.sock # Unix domain socket
./stdui --pipe /tmp/myapp.pipe # named pipe (alias for Unix domain socket)

# Windows
stdui.exe --socket /tmp/myapp.sock # Unix domain socket (Windows 10 1803+)
stdui.exe --pipe \\.\pipe\myapp # Windows named pipe
```

stdui creates the socket/pipe file itself. You do not need to create it beforehand. The Go SDK exposes `StartWithSocket` and `StartWithNamedPipe` as drop-in replacements for `Start`. See the [IPC example](./example/ipc) and the [IPC Transports docs](https://bigjk.github.io/StdUI/docs/ipc-transports) for details.

**You can learn more about the protocol in the [protocol specification](./PROTOCOL.md).**

## SDKs
Expand Down
9 changes: 5 additions & 4 deletions docs/docs/intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ sidebar_position: 1

# Introduction

StdUI is a lightweight cross-platform UI engine that can be used with any programming language. It spawns as a child process and communicates with your application via stdin/stdout using newline-delimited JSON. This allows easy integration with any language that can start a process and read/write pipes.
StdUI is a lightweight cross-platform UI engine that can be used with any programming language. It spawns as a child process and communicates with your application via newline-delimited JSON. By default it uses stdin/stdout, but Unix domain sockets and named pipes are also supported for cases where subprocess pipes are inconvenient.

:::warning
This project is experimental. Expect bugs, missing features, and breaking changes. The API is not stable and may change without deprecation.
Expand All @@ -14,8 +14,9 @@ This project is experimental. Expect bugs, missing features, and breaking change

```
Your App (Go, Python, anything)
│ stdin → JSON commands
│ stdout ← JSON events
│ JSON commands →
│ ← JSON events
│ (stdin/stdout, Unix socket, or named pipe)
stdui binary (C++)
┌──────────────────────────────┐
Expand All @@ -31,7 +32,7 @@ StdUI uses a reduced subset of HTML/CSS (via [litehtml](https://github.com/liteh
## Features

- Cross-platform (Windows, macOS, Linux)
- Language agnostic (communicates via stdin/stdout)
- Language agnostic (communicates via stdin/stdout, Unix sockets, or named pipes)
- Supports a reduced subset of HTML/CSS for layout and styling
- Pane-based layout system
- Interactive widgets: buttons, text/number/password inputs, checkboxes, sliders, progress bars, color picker
Expand Down
121 changes: 121 additions & 0 deletions docs/docs/ipc-transports.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
---
sidebar_position: 3
sidebar_label: IPC Transports
---

# IPC Transports

By default stdui communicates with the controlling application via **stdin/stdout**. Two alternative transports are available for cases where subprocess pipes are inconvenient — for example, when the controlling process is not the direct parent of stdui, or when you want to connect to an already-running stdui instance.

| Transport | CLI flag | Platform |
| --------- | -------- | -------- |
| stdin/stdout | _(default, no flag needed)_ | All |
| Unix domain socket | `--socket <path>` | All (Windows 10 1803+) |
| Named pipe | `--pipe <path>` | All |

:::note Platform note — named pipes on Unix
On Unix/macOS, `--pipe` is an alias for a Unix domain socket. FIFOs cannot carry reliable bidirectional IPC, so stdui creates a Unix domain socket at the given path regardless. On Windows, `--pipe` uses a real Windows named pipe (`CreateNamedPipe`).
:::

## How it works

When either flag is passed, stdui **creates** the socket or pipe itself before entering its main loop. The controlling application connects after the file appears on disk. The socket/pipe file is removed by stdui on shutdown.

```
App (Go, Python, anything)
│ JSON commands →
│ ← JSON events
▼ (over socket or pipe instead of stdin/stdout)
stdui binary (C++)
```

The message format and protocol are identical to stdin/stdout — every message is a single line of JSON terminated by a newline.

## CLI flags

### `--socket <path>`

Starts stdui in Unix domain socket mode. stdui creates and listens on a Unix domain socket at `<path>`. The controlling application dials that path after the file is visible.

```sh
./stdui --socket /tmp/myapp.sock
```

### `--pipe <path>`

Starts stdui in named-pipe mode.

- **Unix/macOS**: creates a Unix domain socket at `<path>` (same as `--socket`).
- **Windows**: creates a Windows named pipe at `<path>` (must be of the form `\\.\pipe\<name>`).

```sh
# Unix / macOS
./stdui --pipe /tmp/myapp.pipe

# Windows
stdui.exe --pipe \\.\pipe\myapp
```

## Go SDK

The Go SDK provides two convenience functions that spawn stdui with the appropriate flag and connect to it automatically. Both functions retry the connection with exponential back-off for up to 5 seconds while stdui is starting up.

### `StartWithSocket`

```go
import stdui "github.com/BigJk/stdui/sdk/go"

client, err := stdui.StartWithSocket(
"./build/stdui", // path to stdui binary
"/tmp/myapp.sock", // socket path — created by stdui
stdui.Settings{
Title: "My App",
WindowWidth: stdui.Ptr(800),
WindowHeight: stdui.Ptr(600),
},
)
if err != nil {
log.Fatal(err)
}
```

### `StartWithNamedPipe`

```go
client, err := stdui.StartWithNamedPipe(
"./build/stdui", // path to stdui binary
"/tmp/myapp.pipe", // pipe path — created by stdui
stdui.Settings{
Title: "My App",
WindowWidth: stdui.Ptr(800),
WindowHeight: stdui.Ptr(600),
},
)
if err != nil {
log.Fatal(err)
}
```

On Windows pass a named-pipe path:

```go
client, err := stdui.StartWithNamedPipe(
`C:\path\to\stdui.exe`,
`\\.\pipe\myapp`,
settings,
)
```

After obtaining the `*Client`, usage is identical to `Start()` — register handlers and call `client.Wait()`.

## Full example

A runnable example is in [`example/ipc/main.go`](https://github.com/BigJk/stdui/blob/main/example/ipc/main.go). It accepts `-socket` and `-pipe` flags to select the transport at runtime:

```sh
# Use Unix domain socket
go run ./example/ipc -socket /tmp/stdui-demo.sock

# Use named pipe (default when no -socket flag is given)
go run ./example/ipc
```
4 changes: 3 additions & 1 deletion docs/docs/protocol/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ sidebar_label: Protocol

# Protocol

StdUI communicates with the controlling application via **stdin/stdout** using line-delimited JSON. Every message is a single line of JSON terminated by a newline character.
StdUI communicates with the controlling application using line-delimited JSON. Every message is a single line of JSON terminated by a newline character.

By default the channel is **stdin/stdout**, but Unix domain sockets and named pipes are also available — see [IPC Transports](/docs/ipc-transports). The message format is identical regardless of the transport.

## Message Format

Expand Down
31 changes: 31 additions & 0 deletions example/ipc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# IPC Transports

Demonstrates connecting to stdui via a **Unix domain socket** or a **named pipe** instead of the default stdin/stdout transport.

## Usage

```sh
# Named pipe (default — Unix domain socket on non-Windows, Windows named pipe on Windows)
go run . -binary ../../build/stdui

# Unix domain socket
go run . -binary ../../build/stdui -socket /tmp/stdui-ipc-example.sock
```

## Flags

| Flag | Default | Description |
| --------- | ----------------------------------------------------------------------------- | ------------------------------------------- |
| `-binary` | `./build/stdui` | Path to the stdui binary |
| `-socket` | `/tmp/stdui-ipc-example.sock` | Connect via Unix domain socket at this path |
| `-pipe` | `/tmp/stdui-ipc-example.pipe` (Unix) / `\\.\pipe\stdui-ipc-example` (Windows) | Connect via named pipe |

When `-socket` is passed explicitly it takes priority. Otherwise `-pipe` is used.

## Notes

- stdui **creates** the socket/pipe itself — you do not need to create it beforehand.
- On Unix/macOS `--pipe` is backed by a Unix domain socket (FIFOs cannot carry bidirectional IPC reliably).
- On Windows `--pipe` uses a real Windows named pipe (`CreateNamedPipe`).

See the [IPC Transports docs](https://bigjk.github.io/StdUI/docs/ipc-transports) for a full explanation.
7 changes: 7 additions & 0 deletions example/ipc/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module github.com/BigJk/stdui/example/ipc

go 1.21

require github.com/BigJk/stdui/sdk/go v0.0.0

replace github.com/BigJk/stdui/sdk/go => ../../sdk/go
124 changes: 124 additions & 0 deletions example/ipc/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package main

import (
"flag"
"fmt"
"os"
"runtime"

stdui "github.com/BigJk/stdui/sdk/go"
)

const contentTpl = `
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: sans-serif;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
height: 100%%;
}
.title {
font-size: 1.2rem;
font-weight: bold;
margin-bottom: 8px;
}
.subtitle {
color: #666;
font-size: 0.9rem;
margin-bottom: 16px;
}
.button {
width: 150px;
}
</style>

<div class="title">IPC transport example</div>
<div class="subtitle">%s</div>
<div class="button">
<ui-button text="Click me" action="ping"></ui-button>
</div>
`

func main() {
socket := flag.String("socket", defaultSocketPath(), "connect via Unix domain socket at this path")
pipe := flag.String("pipe", defaultPipePath(), "connect via named pipe (Unix domain socket on non-Windows, Windows named pipe on Windows)")
binary := flag.String("binary", "./build/stdui", "path to the stdui binary")
flag.Parse()

settings := stdui.Settings{
Title: "IPC Example",
WindowWidth: stdui.Ptr(500),
WindowHeight: stdui.Ptr(300),
Resizable: stdui.Ptr(false),
}

var (
client *stdui.Client
err error
transportID string
)

switch {
case isFlagSet("socket"):
transportID = "Unix domain socket: " + *socket
client, err = stdui.StartWithSocket(*binary, *socket, settings)
default:
transportID = "Named pipe: " + *pipe
client, err = stdui.StartWithNamedPipe(*binary, *pipe, settings)
}

if err != nil {
fmt.Fprintf(os.Stderr, "Failed to start stdui: %v\n", err)
os.Exit(1)
}

client.OnReady(func() {
_ = client.UpdateContent(fmt.Sprintf(contentTpl, transportID))
})

client.OnButtonClicked(func(attrs map[string]string, _ string) {
if attrs["action"] == "ping" {
fmt.Println("pong")
}
})

client.OnLog(func(namespace, message string) {
fmt.Fprintf(os.Stderr, "[log] %s: %s\n", namespace, message)
})

client.OnError(func(err error) {
fmt.Fprintf(os.Stderr, "[error] %v\n", err)
})

client.Wait()
}

// defaultSocketPath returns the default Unix domain socket path.
func defaultSocketPath() string {
return "/tmp/stdui-ipc-example.sock"
}

// defaultPipePath returns a sensible default named-pipe path for the current
// platform.
func defaultPipePath() string {
if runtime.GOOS == "windows" {
return `\\.\pipe\stdui-ipc-example`
}
return "/tmp/stdui-ipc-example.pipe"
}

// isFlagSet reports whether the flag with the given name was explicitly
// provided on the command line.
func isFlagSet(name string) bool {
found := false
flag.Visit(func(f *flag.Flag) {
if f.Name == name {
found = true
}
})
return found
}
Loading
Loading