mirror of
https://github.com/anotherhadi/ilovetui.git
synced 2026-10-05 19:08:25 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18668f4aea | ||
|
|
238d764b0a | ||
|
|
b8dc56adb1 | ||
|
|
ad619d23d5 | ||
|
|
fc5e87c028 | ||
|
|
4dada889cb | ||
|
|
080ed34fe4 | ||
|
|
1c1d8ead1f | ||
|
|
3d6bcdf0f2 | ||
|
|
280507f7e0 | ||
|
|
6d858380c7 | ||
|
|
edf72ab183 | ||
|
|
a1d368088c | ||
|
|
20b4e7a0a5 | ||
|
|
21e2039433 | ||
|
|
9158a070f3 | ||
|
|
5e66c5b303 | ||
|
|
b329bdc074 | ||
|
|
038842878a | ||
|
|
12305ba1f2 | ||
|
|
000f9997fd | ||
|
|
df7bd3df0a | ||
|
|
68bf97efd6 | ||
|
|
1bc3ee2821 | ||
|
|
2fd1861339 | ||
|
|
b22ce7cbf7 | ||
|
|
0648e7a761 | ||
|
|
55cb4be01d | ||
|
|
21b574eb6f |
@@ -1,24 +0,0 @@
|
|||||||
version: 2
|
|
||||||
|
|
||||||
before:
|
|
||||||
hooks:
|
|
||||||
- go mod tidy
|
|
||||||
|
|
||||||
builds:
|
|
||||||
- skip: true
|
|
||||||
|
|
||||||
archives:
|
|
||||||
- formats:
|
|
||||||
- tar.gz
|
|
||||||
name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}"
|
|
||||||
|
|
||||||
checksum:
|
|
||||||
name_template: checksums.txt
|
|
||||||
|
|
||||||
changelog:
|
|
||||||
sort: asc
|
|
||||||
filters:
|
|
||||||
exclude:
|
|
||||||
- "^docs:"
|
|
||||||
- "^test:"
|
|
||||||
- "^ci:"
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
# Contributing
|
|
||||||
|
|
||||||
Everybody is invited and welcome to contribute. There is a lot to do... Check the issues!
|
|
||||||
|
|
||||||
The process is straight-forward.
|
|
||||||
|
|
||||||
- Read [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#best-practices-for-faster-reviews) by Kubernetes (but skip step 0 and 1)
|
|
||||||
- Fork this git repository
|
|
||||||
- Write your changes (bug fixes, new features, ...).
|
|
||||||
- Create a Pull Request against the main branch.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ko_fi: anotherhadi
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
name: Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "v*"
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version-file: go.mod
|
|
||||||
|
|
||||||
- uses: goreleaser/goreleaser-action@v6
|
|
||||||
with:
|
|
||||||
version: "~> v2"
|
|
||||||
args: release --clean --config .github/.goreleaser.yaml
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
+1
-4
@@ -1,4 +1 @@
|
|||||||
.claude/
|
node_modules/
|
||||||
CLAUDE.md
|
|
||||||
result/
|
|
||||||
.pre-commit-config.yaml
|
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2026 Hadi
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,95 +1,29 @@
|
|||||||
# Ilovetui
|
# I Love TUI
|
||||||
|
|
||||||
A minimal Go library that provides a shared [Base16](https://github.com/tinted-theming/home) color theme for terminal UIs built with [bubbletea](https://github.com/charmbracelet/bubbletea) and [lipgloss](https://github.com/charmbracelet/lipgloss).
|
Shared OpenTUI/Solid theme, components, and config helpers for my TUIs.
|
||||||
|
|
||||||
The idea is simple: instead of every TUI app managing its own colors, they all share one theme file so the user customizes once and every app looks consistent.
|
## Install
|
||||||
|
|
||||||
## How it works
|
```bash
|
||||||
|
bun add github:anotherhadi/ilovetui
|
||||||
On import, `ilovetui` automatically loads the user's theme from `~/.config/ilovetui/config.yaml` (respecting `$XDG_CONFIG_HOME`).
|
|
||||||
If no config exists, it falls back to the embedded default. The active theme is exposed as the package-level variable `S`.
|
|
||||||
|
|
||||||
```go
|
|
||||||
import "github.com/anotherhadi/ilovetui"
|
|
||||||
|
|
||||||
// Use colors directly
|
|
||||||
style := lipgloss.NewStyle().Foreground(ilovetui.S.Primary)
|
|
||||||
|
|
||||||
// Use pre-built panel styles
|
|
||||||
box := ilovetui.RenderWithTitle(ilovetui.S.PanelFocused, "Title", content, w, h)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
No setup required — just import and use.
|
Peer dependencies, install whichever you actually use: `@opentui/core`, `@opentui/solid`, `@opentui/keymap`, `solid-js`, `opentui-spinner`.
|
||||||
|
|
||||||
## Installation
|
## Usage
|
||||||
|
|
||||||
```sh
|
```ts
|
||||||
go get github.com/anotherhadi/ilovetui
|
import { theme, presets } from "ilovetui";
|
||||||
|
import "ilovetui/solid";
|
||||||
```
|
```
|
||||||
|
|
||||||
## Theme
|
Entry points: `.`, `./solid`, `./context`, `./project-config`, `./keymap`, `./components`, `./components/help`, `./components/spinner`, `./components/section-header`. See the source for what each exports.
|
||||||
|
|
||||||
The theme follows the [Base16](https://github.com/tinted-theming/home) standard (16 colors). The library exposes both the raw palette and semantic aliases:
|
The theme is base16 and user-overridable via `~/.config/ilovetui/config.yaml`. Copy [`src/default.yaml`](https://github.com/anotherhadi/ilovetui/blob/main/src/default.yaml) there as a starting point.
|
||||||
|
|
||||||
| Alias | Base16 | Meaning |
|
An app can layer its own theme overrides on top of that file (e.g. a `theme:` section of its own config, validated with `ThemeConfigSchema`) by calling `configureTheme(overrides)` once at startup, before the first render. Invalid theme files are reported as warning toasts and skipped; unknown keys are ignored, so other TUIs can keep their own keys in the shared file.
|
||||||
| ------------ | ------ | --------------------------------------- |
|
|
||||||
| `Background` | Base00 | Background |
|
|
||||||
| `SubtleBg` | Base01 | Lighter Background / Status Bars |
|
|
||||||
| `Selection` | Base02 | Selection Background |
|
|
||||||
| `Subtle` | Base03 | Comments / Invisibles |
|
|
||||||
| `Muted` | Base04 | Dark Foreground / Status Bars |
|
|
||||||
| `Text` | Base05 | Default Foreground |
|
|
||||||
| `Primary` | Base0D | Functions / Methods / Headings / Accent |
|
|
||||||
| `Success` | Base0B | Strings / Success / Diff Inserted |
|
|
||||||
| `Warning` | Base09 | Integers / Constants / Booleans |
|
|
||||||
| `Error` | Base08 | Variables / Errors / Diff Deleted |
|
|
||||||
|
|
||||||
The default theme is `./default.yaml`. Copy it and edit to customize:
|
## Focus and keymap scoping
|
||||||
|
|
||||||
```sh
|
Wrap each focus region in a `<Pane>` (`focusable` when the pane itself is the focus target). Inside it, `usePane()` gives `target` — pass it to `useBindings` so the layer is only active while focus is within the pane — and `focused()` for styling. `<Modal>` takes focus while open, restores it on close, and binds its `dismissKey` (default `escape`) plus any `commands`/`bindings` you pass, scoped to itself. Note that keymap layers are ordered by `priority` then registration order, not by tree depth: keep overlapping layers on disjoint targets, or give them explicit priorities.
|
||||||
mkdir -p ~/.config/ilovetui
|
|
||||||
cp $(go env GOPATH)/pkg/mod/github.com/anotherhadi/ilovetui*/default.yaml ~/.config/ilovetui/config.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
Or let your app write it on first run:
|
|
||||||
|
|
||||||
```go
|
|
||||||
ilovetui.WriteDefaultConfig(ilovetui.DefaultConfigPath())
|
|
||||||
```
|
|
||||||
|
|
||||||
## Pre-built styles
|
|
||||||
|
|
||||||
`S` ships with a few ready-to-use lipgloss styles:
|
|
||||||
|
|
||||||
| Field | Description |
|
|
||||||
| ---------------- | ---------------------------------------- |
|
|
||||||
| `S.Bold` | Bold text |
|
|
||||||
| `S.Faint` | Muted / dimmed text |
|
|
||||||
| `S.Panel` | Rounded border, unfocused (Subtle color) |
|
|
||||||
| `S.PanelFocused` | Rounded border, focused (Primary color) |
|
|
||||||
|
|
||||||
## Helpers
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Inner usable height of a bordered panel with outer height h
|
|
||||||
inner := ilovetui.ContentHeight(h)
|
|
||||||
|
|
||||||
// Render a box with a title embedded in the top border
|
|
||||||
box := ilovetui.RenderWithTitle(ilovetui.S.PanelFocused, "Header", content, w, h)
|
|
||||||
```
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
```go
|
|
||||||
ilovetui.Init() // Reload from default config path
|
|
||||||
ilovetui.InitFrom(path string) // Reload from a custom path
|
|
||||||
ilovetui.InitFromBytes(data []byte) // Parse raw YAML
|
|
||||||
ilovetui.DefaultConfigPath() string // ~/.config/ilovetui/config.yaml
|
|
||||||
ilovetui.WriteDefaultConfig(path) // Write default config if missing
|
|
||||||
```
|
|
||||||
|
|
||||||
## Projects using ilovetui
|
|
||||||
|
|
||||||
- [anotherhadi/spilltea](https://github.com/anotherhadi/spilltea): A minimal, terminal-based HTTP(S) proxy for pentesters and CTF players. Think Burp Suite or Caido, but entirely in your terminal.
|
|
||||||
- [anotherhadi/usbguard-tui](https://github.com/anotherhadi/usbguard-tui): A terminal UI for managing USB devices via usbguard. TUI built with golang & bubbletea.
|
|
||||||
- [anotherhadi/jwt-tui](https://github.com/anotherhadi/jwt-tui): A terminal UI for inspecting, editing, and signing JSON Web Tokens (JWTs).
|
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
package ilovetui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"charm.land/lipgloss/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ContentHeight returns the usable inner height for a bordered panel of totalH rows.
|
|
||||||
func ContentHeight(totalH int) int {
|
|
||||||
h := totalH - 2
|
|
||||||
if h < 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return h
|
|
||||||
}
|
|
||||||
|
|
||||||
// RenderWithTitle renders a bordered box with a title embedded in the top border.
|
|
||||||
// title may contain ANSI color codes. width and height are the total outer dimensions.
|
|
||||||
//
|
|
||||||
// Example:
|
|
||||||
//
|
|
||||||
// box := ilovetui.RenderWithTitle(theme.Styles.PanelFocused, "Header", content, w, h)
|
|
||||||
func RenderWithTitle(border lipgloss.Style, title, content string, width, height int) string {
|
|
||||||
boxH := height - 1
|
|
||||||
if contentH := boxH - 1; contentH > 0 {
|
|
||||||
lines := strings.Split(content, "\n")
|
|
||||||
if len(lines) > contentH {
|
|
||||||
content = strings.Join(lines[:contentH], "\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
box := border.BorderTop(false).Width(width).Height(boxH).Render(content)
|
|
||||||
|
|
||||||
boxWidth := lipgloss.Width(strings.SplitN(box, "\n", 2)[0])
|
|
||||||
titleW := lipgloss.Width(title)
|
|
||||||
fillW := boxWidth - titleW - 4 // 4 = "╭ " + " " + "╮"
|
|
||||||
if fillW < 0 {
|
|
||||||
fillW = 0
|
|
||||||
}
|
|
||||||
bc := lipgloss.NewStyle().Foreground(border.GetBorderTopForeground())
|
|
||||||
topLine := bc.Render("╭ ") + bc.Render(title) + bc.Render(" "+strings.Repeat("─", fillW)+"╮")
|
|
||||||
|
|
||||||
return lipgloss.JoinVertical(lipgloss.Left, topLine, box)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,273 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "ilovetui-opentui",
|
||||||
|
"dependencies": {
|
||||||
|
"yaml": "^2.8.1",
|
||||||
|
"zod": "^4.5.4",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@opentui/core": "^0.5.9",
|
||||||
|
"@opentui/keymap": "^0.5.9",
|
||||||
|
"@opentui/solid": "^0.5.9",
|
||||||
|
"@types/bun": "^1.3.14",
|
||||||
|
"opentui-spinner": "^0.0.7",
|
||||||
|
"solid-js": "1.9.12",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@opentui/core": ">=0.5.0",
|
||||||
|
"@opentui/keymap": ">=0.5.0",
|
||||||
|
"@opentui/solid": ">=0.5.0",
|
||||||
|
"opentui-spinner": ">=0.0.7",
|
||||||
|
"solid-js": ">=1.9.0",
|
||||||
|
},
|
||||||
|
"optionalPeers": [
|
||||||
|
"@opentui/core",
|
||||||
|
"@opentui/keymap",
|
||||||
|
"@opentui/solid",
|
||||||
|
"opentui-spinner",
|
||||||
|
"solid-js",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@ampproject/remapping": ["@ampproject/[email protected]", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
|
||||||
|
|
||||||
|
"@babel/code-frame": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
|
||||||
|
|
||||||
|
"@babel/compat-data": ["@babel/[email protected]", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="],
|
||||||
|
|
||||||
|
"@babel/core": ["@babel/[email protected]", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
|
||||||
|
|
||||||
|
"@babel/generator": ["@babel/[email protected]", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="],
|
||||||
|
|
||||||
|
"@babel/helper-annotate-as-pure": ["@babel/[email protected]", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw=="],
|
||||||
|
|
||||||
|
"@babel/helper-compilation-targets": ["@babel/[email protected]", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="],
|
||||||
|
|
||||||
|
"@babel/helper-create-class-features-plugin": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/traverse": "^7.29.7", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg=="],
|
||||||
|
|
||||||
|
"@babel/helper-globals": ["@babel/[email protected]", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="],
|
||||||
|
|
||||||
|
"@babel/helper-member-expression-to-functions": ["@babel/[email protected]", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg=="],
|
||||||
|
|
||||||
|
"@babel/helper-module-imports": ["@babel/[email protected]", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="],
|
||||||
|
|
||||||
|
"@babel/helper-module-transforms": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="],
|
||||||
|
|
||||||
|
"@babel/helper-optimise-call-expression": ["@babel/[email protected]", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong=="],
|
||||||
|
|
||||||
|
"@babel/helper-plugin-utils": ["@babel/[email protected]", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="],
|
||||||
|
|
||||||
|
"@babel/helper-replace-supers": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ=="],
|
||||||
|
|
||||||
|
"@babel/helper-skip-transparent-expression-wrappers": ["@babel/[email protected]", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ=="],
|
||||||
|
|
||||||
|
"@babel/helper-string-parser": ["@babel/[email protected]", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
|
||||||
|
|
||||||
|
"@babel/helper-validator-identifier": ["@babel/[email protected]", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
||||||
|
|
||||||
|
"@babel/helper-validator-option": ["@babel/[email protected]", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="],
|
||||||
|
|
||||||
|
"@babel/helpers": ["@babel/[email protected]", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="],
|
||||||
|
|
||||||
|
"@babel/parser": ["@babel/[email protected]", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="],
|
||||||
|
|
||||||
|
"@babel/plugin-syntax-jsx": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A=="],
|
||||||
|
|
||||||
|
"@babel/plugin-syntax-typescript": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-modules-commonjs": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-module-transforms": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ=="],
|
||||||
|
|
||||||
|
"@babel/plugin-transform-typescript": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/plugin-syntax-typescript": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw=="],
|
||||||
|
|
||||||
|
"@babel/preset-typescript": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ=="],
|
||||||
|
|
||||||
|
"@babel/template": ["@babel/[email protected]", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="],
|
||||||
|
|
||||||
|
"@babel/traverse": ["@babel/[email protected]", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg=="],
|
||||||
|
|
||||||
|
"@babel/types": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
|
||||||
|
|
||||||
|
"@jridgewell/gen-mapping": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||||
|
|
||||||
|
"@jridgewell/resolve-uri": ["@jridgewell/[email protected]", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||||
|
|
||||||
|
"@jridgewell/sourcemap-codec": ["@jridgewell/[email protected]", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="],
|
||||||
|
|
||||||
|
"@jridgewell/trace-mapping": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||||
|
|
||||||
|
"@opentui/core": ["@opentui/[email protected]", "", { "dependencies": { "bun-ffi-structs": "0.3.1", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.5.9", "@opentui/core-darwin-x64": "0.5.9", "@opentui/core-linux-arm64": "0.5.9", "@opentui/core-linux-arm64-musl": "0.5.9", "@opentui/core-linux-x64": "0.5.9", "@opentui/core-linux-x64-musl": "0.5.9", "@opentui/core-win32-arm64": "0.5.9", "@opentui/core-win32-x64": "0.5.9" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-d0EWYyp6djitu1N1R0o75NrLl4TxY3oJEmRNrX9vSKKC5/jriGQdSV6lJmwiB77O0cxtBnPzWztAV79vA1J2fA=="],
|
||||||
|
|
||||||
|
"@opentui/core-darwin-arm64": ["@opentui/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-YcpWGD8GwoO3UYYw0kLDI4qof3ElSwWW5M3fNG+Kw87BSjCa5frZqak5xebdZ/XU5Xsk+BjgbBsxbzo9yrIjZQ=="],
|
||||||
|
|
||||||
|
"@opentui/core-darwin-x64": ["@opentui/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-xgdZwgcwlDCqi0WPCS1d0GVSd+oKDFX+UVE1ZPei8u8W3pirXGSExxVHNku7QbEYCRvpCeMtoKIa6xN9cVTsoQ=="],
|
||||||
|
|
||||||
|
"@opentui/core-linux-arm64": ["@opentui/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-vHBiJp69wMaL+fRMDpnXCCIpPyQRPZEWCWBatnblUUGIWUseqeQlaPow6ljHSbgAd/GjygAf4tFCyTqNT2ezZA=="],
|
||||||
|
|
||||||
|
"@opentui/core-linux-arm64-musl": ["@opentui/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-A7GW4NCTInj2vy3X2gVO2Sm8jllgdLxwPdOklPqyl+xqivQtyG9qwS5YXoCk9fBJt10ORhYKETEjYEnIE5xvMQ=="],
|
||||||
|
|
||||||
|
"@opentui/core-linux-x64": ["@opentui/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-rdw7QVopVHwj6fMrJc7hPGVaYg2V5NBlW3MUfkrQBlUlo/87OC4aYDAh5oRIL1CIFD9LjDdh7hwmUNbn/K9Pcw=="],
|
||||||
|
|
||||||
|
"@opentui/core-linux-x64-musl": ["@opentui/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-J4wQs1OMPZ4hR93Op1C/BLFmIta2mUJm4M7djevlgcWcal4RSNO4V8QvUXfogWGci+ADZqEmX2osQQDY5NVJpw=="],
|
||||||
|
|
||||||
|
"@opentui/core-win32-arm64": ["@opentui/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-OnmEsGLWwsHrm6sTsWSSesu4LGjSV1JNWqCsEItbuitWaqH6rXdSahSc5jMFsS57bZxUyaRz01yAqiZzUSXnVQ=="],
|
||||||
|
|
||||||
|
"@opentui/core-win32-x64": ["@opentui/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-/CnAIfKL7+ZeGLyZoXV5zS71Nd8Zn97RUir2DAIY05MJozmfg5s7XOAdUNYuK1cg5mbwlAuqeAsfbuWUAEe15g=="],
|
||||||
|
|
||||||
|
"@opentui/keymap": ["@opentui/[email protected]", "", { "dependencies": { "@opentui/core": "0.5.9" }, "peerDependencies": { "@opentui/react": "0.5.9", "@opentui/solid": "0.5.9", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-ZcRNeuCDv+LJ89BS5xBw90e0S1Etn6j41jflg6LZlWj4bnqjCU55dq0+zERUdkkC4rqD4SikM8q7V3ZfkwzyyQ=="],
|
||||||
|
|
||||||
|
"@opentui/solid": ["@opentui/[email protected]", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.5.9", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-zGSP/ia9ww+TTMvQMZjJW2+h7fg9YkVRIM723EgvP+v7ZBZ/0vMzPNh7FJ0jET3ysw0h5hsbvcYISs4sUcf+UA=="],
|
||||||
|
|
||||||
|
"@types/bun": ["@types/[email protected]", "", { "dependencies": { "bun-types": "1.4.0" } }, "sha512-K+lZULY23vRgK/CfTjFIV+tyifaNdSMlPh9j+6mQ/cLfpOznLyAuzgV/JQysyECpkBQLVMSyvjlr2fBUSA9wFQ=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ=="],
|
||||||
|
|
||||||
|
"ansi-regex": ["[email protected]", "", {}, "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ=="],
|
||||||
|
|
||||||
|
"babel-plugin-jsx-dom-expressions": ["[email protected]", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-lxve6Y02YiZTldB7efKpnbf1BH00XCFZNYYW235jSGsYaJNFtHrYlKV6/O+miHbjqpIr9FTe5+0no4hofAMbfA=="],
|
||||||
|
|
||||||
|
"babel-plugin-module-resolver": ["[email protected]", "", { "dependencies": { "find-babel-config": "^2.1.1", "glob": "^9.3.3", "pkg-up": "^3.1.0", "reselect": "^4.1.7", "resolve": "^1.22.8" } }, "sha512-9KtaCazHee2xc0ibfqsDeamwDps6FZNo5S0Q81dUqEuFzVwPhcT4J5jOqIVvgCA3Q/wO9hKYxN/Ds3tIsp5ygg=="],
|
||||||
|
|
||||||
|
"babel-preset-solid": ["[email protected]", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.6" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.12" }, "optionalPeers": ["solid-js"] }, "sha512-LLqnuKVDlKpyBlMPcH6qEvs/wmS9a+NczppxJ3ryS/c0O5IiSFOIBQi9GzyiGDSbcJpx4Gr87jyFTos1MyEuWg=="],
|
||||||
|
|
||||||
|
"balanced-match": ["[email protected]", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||||
|
|
||||||
|
"baseline-browser-mapping": ["[email protected]", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw=="],
|
||||||
|
|
||||||
|
"brace-expansion": ["[email protected]", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="],
|
||||||
|
|
||||||
|
"browserslist": ["[email protected]", "", { "dependencies": { "baseline-browser-mapping": "^2.11.12", "caniuse-lite": "^1.0.30001809", "electron-to-chromium": "^1.5.402", "node-releases": "^2.0.53", "update-browserslist-db": "^1.3.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA=="],
|
||||||
|
|
||||||
|
"bun-ffi-structs": ["[email protected]", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-3gM7PpVWLyrwxWjcilSiGuhWanhZivvo6l0u573NziPH6f/gwk6McbaYgn7oJWov6pKGRTDbrg94W5DcJsKTtQ=="],
|
||||||
|
|
||||||
|
"bun-types": ["[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-iIKw23BspnQQYd3prITOBxeUsxBHnwzX6YJfGMuNOZzeNcMmVqzIIVGRm1l69ogaPQmb4wB6BN8mA5bE9YuC5Q=="],
|
||||||
|
|
||||||
|
"caniuse-lite": ["[email protected]", "", {}, "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg=="],
|
||||||
|
|
||||||
|
"cli-spinners": ["[email protected]", "", {}, "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw=="],
|
||||||
|
|
||||||
|
"convert-source-map": ["[email protected]", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||||
|
|
||||||
|
"csstype": ["[email protected]", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||||
|
|
||||||
|
"debug": ["[email protected]", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
|
"diff": ["[email protected]", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="],
|
||||||
|
|
||||||
|
"electron-to-chromium": ["[email protected]", "", {}, "sha512-4T+DTDWuMPM4aHlHwWdAVCVWwp7LDilnhzkj+c/Lbj91XSQrLuOmZSLtS9Q4iIqjlPUbPOnC624zDVVHCHaolQ=="],
|
||||||
|
|
||||||
|
"emoji-regex": ["[email protected]", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
|
||||||
|
|
||||||
|
"entities": ["[email protected]", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
||||||
|
|
||||||
|
"es-errors": ["[email protected]", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||||
|
|
||||||
|
"escalade": ["[email protected]", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||||
|
|
||||||
|
"find-babel-config": ["[email protected]", "", { "dependencies": { "json5": "^2.2.3" } }, "sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg=="],
|
||||||
|
|
||||||
|
"find-up": ["[email protected]", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="],
|
||||||
|
|
||||||
|
"fs.realpath": ["[email protected]", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
|
||||||
|
|
||||||
|
"function-bind": ["[email protected]", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||||
|
|
||||||
|
"gensync": ["[email protected]", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
||||||
|
|
||||||
|
"get-east-asian-width": ["[email protected]", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="],
|
||||||
|
|
||||||
|
"glob": ["[email protected]", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="],
|
||||||
|
|
||||||
|
"hasown": ["[email protected]", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="],
|
||||||
|
|
||||||
|
"html-entities": ["[email protected]", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="],
|
||||||
|
|
||||||
|
"is-core-module": ["[email protected]", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="],
|
||||||
|
|
||||||
|
"js-tokens": ["[email protected]", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||||
|
|
||||||
|
"jsesc": ["[email protected]", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||||
|
|
||||||
|
"json5": ["[email protected]", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||||
|
|
||||||
|
"locate-path": ["[email protected]", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="],
|
||||||
|
|
||||||
|
"lru-cache": ["[email protected]", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||||
|
|
||||||
|
"marked": ["[email protected]", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="],
|
||||||
|
|
||||||
|
"minimatch": ["[email protected]", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg=="],
|
||||||
|
|
||||||
|
"minipass": ["[email protected]", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="],
|
||||||
|
|
||||||
|
"ms": ["[email protected]", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
|
"node-releases": ["[email protected]", "", {}, "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ=="],
|
||||||
|
|
||||||
|
"opentui-spinner": ["[email protected]", "", { "dependencies": { "cli-spinners": "^3.3.0" }, "peerDependencies": { "@opentui/core": "^0.3.4", "@opentui/react": "^0.3.4", "@opentui/solid": "^0.3.4", "typescript": "^5" }, "optionalPeers": ["@opentui/react", "@opentui/solid"] }, "sha512-nPzwAvJG+y9rVEwwHLHqbsMzLnIk2zw+F9LqwA7aYJvpM5gsrKC2rrGi36A+tZpA+1RnWxXeWEgVZMchnaH18Q=="],
|
||||||
|
|
||||||
|
"p-limit": ["[email protected]", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
|
||||||
|
|
||||||
|
"p-locate": ["[email protected]", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="],
|
||||||
|
|
||||||
|
"p-try": ["[email protected]", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="],
|
||||||
|
|
||||||
|
"parse5": ["[email protected]", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
|
||||||
|
|
||||||
|
"path-exists": ["[email protected]", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="],
|
||||||
|
|
||||||
|
"path-parse": ["[email protected]", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
|
||||||
|
|
||||||
|
"path-scurry": ["[email protected]", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
|
||||||
|
|
||||||
|
"picocolors": ["[email protected]", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
|
"pkg-up": ["[email protected]", "", { "dependencies": { "find-up": "^3.0.0" } }, "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA=="],
|
||||||
|
|
||||||
|
"reselect": ["[email protected]", "", {}, "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ=="],
|
||||||
|
|
||||||
|
"resolve": ["[email protected]", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="],
|
||||||
|
|
||||||
|
"s-js": ["[email protected]", "", {}, "sha512-RtpOm+cM6O0sHg6IA70wH+UC3FZcND+rccBZpBAHzlUgNO2Bm5BN+FnM8+OBxzXdwpKWFwX11JGF0MFRkhSoIQ=="],
|
||||||
|
|
||||||
|
"semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
|
|
||||||
|
"seroval": ["[email protected]", "", {}, "sha512-rVQVWjjSvlINzaQPZH5JFqsqEsIWdTxY3iJZCnTL/5gQbXIRooVZKI60tVCkOVfzcRPejboxO2t0P89dg5mQaA=="],
|
||||||
|
|
||||||
|
"seroval-plugins": ["[email protected]", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-HXuLAX2pu/UByPpaeo/TaMfvMIi+1QqIoPJYCcAtU8QkVNwgR6MPlGuCQTErV1JwraaMbYaWVIBX7mppzGLATQ=="],
|
||||||
|
|
||||||
|
"solid-js": ["[email protected]", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-QzKaSJq2/iDrWR1As6MHZQ8fQkdOBf8GReYb7L5iKwMGceg7HxDcaOHk0at66tNgn9U2U7dXo8ZZpLIAmGMzgw=="],
|
||||||
|
|
||||||
|
"string-width": ["[email protected]", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||||
|
|
||||||
|
"strip-ansi": ["[email protected]", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
|
||||||
|
|
||||||
|
"supports-preserve-symlinks-flag": ["[email protected]", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
||||||
|
|
||||||
|
"typescript": ["[email protected]", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
|
"undici-types": ["[email protected]", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
|
||||||
|
|
||||||
|
"update-browserslist-db": ["[email protected]", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw=="],
|
||||||
|
|
||||||
|
"web-tree-sitter": ["[email protected]", "", { "peerDependencies": { "@types/emscripten": "^1.40.0" }, "optionalPeers": ["@types/emscripten"] }, "sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA=="],
|
||||||
|
|
||||||
|
"yallist": ["[email protected]", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||||
|
|
||||||
|
"yaml": ["[email protected]", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
|
||||||
|
|
||||||
|
"zod": ["[email protected]", "", {}, "sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA=="],
|
||||||
|
|
||||||
|
"babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/[email protected]", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="],
|
||||||
|
|
||||||
|
"parse5/entities": ["[email protected]", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
|
||||||
|
|
||||||
|
"path-scurry/lru-cache": ["[email protected]", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||||
|
|
||||||
|
"path-scurry/minipass": ["[email protected]", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
package ilovetui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"charm.land/bubbles/v2/help"
|
|
||||||
"charm.land/bubbles/v2/paginator"
|
|
||||||
"charm.land/bubbles/v2/textarea"
|
|
||||||
"charm.land/bubbles/v2/viewport"
|
|
||||||
"charm.land/lipgloss/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewHelp returns a help.Model styled with the active theme.
|
|
||||||
func NewHelp() help.Model {
|
|
||||||
h := help.New()
|
|
||||||
h.Styles.ShortKey = lipgloss.NewStyle().Foreground(S.Primary)
|
|
||||||
h.Styles.ShortDesc = lipgloss.NewStyle().Foreground(S.Muted)
|
|
||||||
h.Styles.ShortSeparator = lipgloss.NewStyle().Foreground(S.Subtle)
|
|
||||||
h.Styles.FullKey = lipgloss.NewStyle().Foreground(S.Primary)
|
|
||||||
h.Styles.FullDesc = lipgloss.NewStyle().Foreground(S.Muted)
|
|
||||||
h.Styles.FullSeparator = lipgloss.NewStyle().Foreground(S.Subtle)
|
|
||||||
h.Styles.Ellipsis = lipgloss.NewStyle().Foreground(S.Subtle)
|
|
||||||
return h
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewTextarea returns a textarea.Model styled with the active theme.
|
|
||||||
// Set showLineNumbers to true to display line numbers in the gutter.
|
|
||||||
func NewTextarea(showLineNumbers bool) textarea.Model {
|
|
||||||
ta := textarea.New()
|
|
||||||
ta.Prompt = ""
|
|
||||||
ta.ShowLineNumbers = showLineNumbers
|
|
||||||
ta.CharLimit = 0
|
|
||||||
ta.EndOfBufferCharacter = '~'
|
|
||||||
ts := ta.Styles()
|
|
||||||
ts.Focused.Base = lipgloss.NewStyle()
|
|
||||||
ts.Blurred.Base = lipgloss.NewStyle()
|
|
||||||
ts.Focused.Text = lipgloss.NewStyle().Foreground(S.Text)
|
|
||||||
ts.Focused.CursorLine = lipgloss.NewStyle().Background(S.Selection).Foreground(S.Text)
|
|
||||||
ts.Focused.CursorLineNumber = lipgloss.NewStyle().Background(S.Selection).Foreground(S.Primary).Bold(true)
|
|
||||||
ts.Focused.LineNumber = lipgloss.NewStyle().Foreground(S.Subtle)
|
|
||||||
ts.Focused.Placeholder = lipgloss.NewStyle().Foreground(S.Subtle)
|
|
||||||
ts.Focused.EndOfBuffer = lipgloss.NewStyle().Foreground(S.SubtleBg)
|
|
||||||
ts.Blurred.Text = lipgloss.NewStyle().Foreground(S.Muted)
|
|
||||||
ts.Blurred.LineNumber = lipgloss.NewStyle().Foreground(S.SubtleBg)
|
|
||||||
ts.Blurred.Placeholder = lipgloss.NewStyle().Foreground(S.Subtle)
|
|
||||||
ts.Blurred.EndOfBuffer = lipgloss.NewStyle().Foreground(S.SubtleBg)
|
|
||||||
ta.SetStyles(ts)
|
|
||||||
return ta
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewPaginator returns a dot-style paginator.Model styled with the active theme.
|
|
||||||
func NewPaginator() paginator.Model {
|
|
||||||
p := paginator.New()
|
|
||||||
p.Type = paginator.Dots
|
|
||||||
p.ActiveDot = S.PagerDotActive
|
|
||||||
p.InactiveDot = S.PagerDotInactive
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
// SplitH splits totalHeight into top and bottom sections, accounting for the
|
|
||||||
// height of statusBar (measured by newline count).
|
|
||||||
func SplitH(totalHeight int, statusBar string, ratio float64) (top, bottom int) {
|
|
||||||
statusH := strings.Count(statusBar, "\n") + 1
|
|
||||||
available := totalHeight - statusH
|
|
||||||
top = int(float64(available) * ratio)
|
|
||||||
bottom = available - top
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewViewport returns a viewport.Model with mouse wheel disabled.
|
|
||||||
func NewViewport() viewport.Model {
|
|
||||||
vp := viewport.New()
|
|
||||||
vp.MouseWheelEnabled = false
|
|
||||||
return vp
|
|
||||||
}
|
|
||||||
|
|
||||||
// ViewportView renders the viewport and appends a subtle scroll indicator
|
|
||||||
// on the last visible line when the user has not reached the bottom.
|
|
||||||
func ViewportView(vp *viewport.Model) string {
|
|
||||||
v := vp.View()
|
|
||||||
if vp.AtBottom() {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
lines := strings.Split(v, "\n")
|
|
||||||
if len(lines) == 0 {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
arrow := lipgloss.NewStyle().Foreground(S.Subtle).Render("↓")
|
|
||||||
arrowW := lipgloss.Width(arrow)
|
|
||||||
inner := vp.Width() - 2*arrowW
|
|
||||||
if inner < 0 {
|
|
||||||
inner = 0
|
|
||||||
}
|
|
||||||
lines[len(lines)-1] = arrow + strings.Repeat(" ", inner) + arrow
|
|
||||||
return strings.Join(lines, "\n")
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
package ilovetui
|
|
||||||
|
|
||||||
type colorsYAML struct {
|
|
||||||
Base00 string `yaml:"base00"`
|
|
||||||
Base01 string `yaml:"base01"`
|
|
||||||
Base02 string `yaml:"base02"`
|
|
||||||
Base03 string `yaml:"base03"`
|
|
||||||
Base04 string `yaml:"base04"`
|
|
||||||
Base05 string `yaml:"base05"`
|
|
||||||
Base06 string `yaml:"base06"`
|
|
||||||
Base07 string `yaml:"base07"`
|
|
||||||
Base08 string `yaml:"base08"`
|
|
||||||
Base09 string `yaml:"base09"`
|
|
||||||
Base0A string `yaml:"base0a"`
|
|
||||||
Base0B string `yaml:"base0b"`
|
|
||||||
Base0C string `yaml:"base0c"`
|
|
||||||
Base0D string `yaml:"base0d"`
|
|
||||||
Base0E string `yaml:"base0e"`
|
|
||||||
Base0F string `yaml:"base0f"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type configYAML struct {
|
|
||||||
Colors colorsYAML `yaml:"colors"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func mergeColors(base, user colorsYAML) colorsYAML {
|
|
||||||
pick := func(b, u string) string {
|
|
||||||
if u != "" {
|
|
||||||
return u
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
return colorsYAML{
|
|
||||||
Base00: pick(base.Base00, user.Base00),
|
|
||||||
Base01: pick(base.Base01, user.Base01),
|
|
||||||
Base02: pick(base.Base02, user.Base02),
|
|
||||||
Base03: pick(base.Base03, user.Base03),
|
|
||||||
Base04: pick(base.Base04, user.Base04),
|
|
||||||
Base05: pick(base.Base05, user.Base05),
|
|
||||||
Base06: pick(base.Base06, user.Base06),
|
|
||||||
Base07: pick(base.Base07, user.Base07),
|
|
||||||
Base08: pick(base.Base08, user.Base08),
|
|
||||||
Base09: pick(base.Base09, user.Base09),
|
|
||||||
Base0A: pick(base.Base0A, user.Base0A),
|
|
||||||
Base0B: pick(base.Base0B, user.Base0B),
|
|
||||||
Base0C: pick(base.Base0C, user.Base0C),
|
|
||||||
Base0D: pick(base.Base0D, user.Base0D),
|
|
||||||
Base0E: pick(base.Base0E, user.Base0E),
|
|
||||||
Base0F: pick(base.Base0F, user.Base0F),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Vendored
+25
@@ -0,0 +1,25 @@
|
|||||||
|
// @bun
|
||||||
|
// src/context/focus.ts
|
||||||
|
import { CliRenderEvents } from "@opentui/core";
|
||||||
|
import { useRenderer } from "@opentui/solid";
|
||||||
|
import { createEffect, createSignal, onCleanup } from "solid-js";
|
||||||
|
function isInside2(node, root) {
|
||||||
|
if (!root)
|
||||||
|
return false;
|
||||||
|
for (let current = node;current; current = current.parent) {
|
||||||
|
if (current === root)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
function useFocusWithin2(target) {
|
||||||
|
const renderer = useRenderer();
|
||||||
|
const [focused, setFocused] = createSignal(false);
|
||||||
|
const sync = () => setFocused(isInside2(renderer.currentFocusedRenderable, target() ?? null));
|
||||||
|
renderer.on(CliRenderEvents.FOCUSED_RENDERABLE, sync);
|
||||||
|
onCleanup(() => renderer.off(CliRenderEvents.FOCUSED_RENDERABLE, sync));
|
||||||
|
createEffect(sync);
|
||||||
|
return focused;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { isInside2, useFocusWithin2 };
|
||||||
Vendored
+21
@@ -0,0 +1,21 @@
|
|||||||
|
// @bun
|
||||||
|
// src/context/notifications.ts
|
||||||
|
import { createSignal } from "solid-js";
|
||||||
|
var DEFAULT_DURATION = 3000;
|
||||||
|
var [notifications2, setNotifications] = createSignal([]);
|
||||||
|
var nextId = 0;
|
||||||
|
function notify2(message, options = {}) {
|
||||||
|
const id = options.id ?? `toast-${nextId++}`;
|
||||||
|
const toast = { id, title: options.title, message, kind: options.kind ?? "info" };
|
||||||
|
setNotifications((prev) => [...prev.filter((n) => n.id !== id), toast]);
|
||||||
|
const duration = options.duration ?? DEFAULT_DURATION;
|
||||||
|
if (duration > 0) {
|
||||||
|
setTimeout(() => dismiss2(id), duration);
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
function dismiss2(id) {
|
||||||
|
setNotifications((prev) => prev.filter((n) => n.id !== id));
|
||||||
|
}
|
||||||
|
|
||||||
|
export { notifications2, notify2, dismiss2 };
|
||||||
Vendored
+23
@@ -0,0 +1,23 @@
|
|||||||
|
// @bun
|
||||||
|
import {
|
||||||
|
theme2
|
||||||
|
} from "./chunk-p6jvscj3.js";
|
||||||
|
|
||||||
|
// src/components/Box.tsx
|
||||||
|
import { spread as _$spread } from "@opentui/solid";
|
||||||
|
import { mergeProps as _$mergeProps } from "@opentui/solid";
|
||||||
|
import { memo as _$memo } from "@opentui/solid";
|
||||||
|
import { createElement as _$createElement } from "@opentui/solid";
|
||||||
|
function Box2(props) {
|
||||||
|
return (() => {
|
||||||
|
var _el$ = _$createElement("box");
|
||||||
|
_$spread(_el$, _$mergeProps(props, {
|
||||||
|
get borderStyle() {
|
||||||
|
return props.borderStyle ?? theme2.borderStyle;
|
||||||
|
}
|
||||||
|
}), false);
|
||||||
|
return _el$;
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Box2 };
|
||||||
Vendored
+79
@@ -0,0 +1,79 @@
|
|||||||
|
// @bun
|
||||||
|
import {
|
||||||
|
notifications2,
|
||||||
|
dismiss2
|
||||||
|
} from "./chunk-6ggk5fg7.js";
|
||||||
|
import {
|
||||||
|
theme2
|
||||||
|
} from "./chunk-p6jvscj3.js";
|
||||||
|
|
||||||
|
// src/components/NotificationHost.tsx
|
||||||
|
import { effect as _$effect } from "@opentui/solid";
|
||||||
|
import { insertNode as _$insertNode } from "@opentui/solid";
|
||||||
|
import { memo as _$memo } from "@opentui/solid";
|
||||||
|
import { insert as _$insert } from "@opentui/solid";
|
||||||
|
import { createComponent as _$createComponent } from "@opentui/solid";
|
||||||
|
import { setProp as _$setProp } from "@opentui/solid";
|
||||||
|
import { createElement as _$createElement } from "@opentui/solid";
|
||||||
|
import { Portal } from "@opentui/solid";
|
||||||
|
import { For } from "solid-js";
|
||||||
|
var KIND_LABEL = {
|
||||||
|
info: "Info",
|
||||||
|
success: "Success",
|
||||||
|
warning: "Warning",
|
||||||
|
error: "Error"
|
||||||
|
};
|
||||||
|
function NotificationHost2(props = {}) {
|
||||||
|
const defaultColor = () => ({
|
||||||
|
info: theme2.primary,
|
||||||
|
success: theme2.success,
|
||||||
|
warning: theme2.warning,
|
||||||
|
error: theme2.error
|
||||||
|
});
|
||||||
|
const colorFor = (kind) => props.colors?.[kind] ?? defaultColor()[kind];
|
||||||
|
return _$createComponent(Portal, {
|
||||||
|
get children() {
|
||||||
|
var _el$ = _$createElement("box");
|
||||||
|
_$setProp(_el$, "position", "absolute");
|
||||||
|
_$setProp(_el$, "top", 1);
|
||||||
|
_$setProp(_el$, "right", 1);
|
||||||
|
_$setProp(_el$, "flexDirection", "column");
|
||||||
|
_$setProp(_el$, "zIndex", 1000);
|
||||||
|
_$insert(_el$, _$createComponent(For, {
|
||||||
|
get each() {
|
||||||
|
return notifications2();
|
||||||
|
},
|
||||||
|
children: (toast) => (() => {
|
||||||
|
var _el$2 = _$createElement("box"), _el$3 = _$createElement("text"), _el$4 = _$createElement("text");
|
||||||
|
_$insertNode(_el$2, _el$3);
|
||||||
|
_$insertNode(_el$2, _el$4);
|
||||||
|
_$setProp(_el$2, "border", true);
|
||||||
|
_$setProp(_el$2, "marginBottom", 1);
|
||||||
|
_$insert(_el$3, () => toast.title ?? KIND_LABEL[toast.kind]);
|
||||||
|
_$insert(_el$4, () => toast.message);
|
||||||
|
_$effect((_p$) => {
|
||||||
|
var _v$ = theme2.borderStyle, _v$2 = colorFor(toast.kind), _v$3 = props.backgroundColor ?? theme2.background, _v$4 = props.width ?? 32, _v$5 = theme2.mouse ? () => dismiss2(toast.id) : undefined, _v$6 = colorFor(toast.kind);
|
||||||
|
_v$ !== _p$.e && (_p$.e = _$setProp(_el$2, "borderStyle", _v$, _p$.e));
|
||||||
|
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$2, "borderColor", _v$2, _p$.t));
|
||||||
|
_v$3 !== _p$.a && (_p$.a = _$setProp(_el$2, "backgroundColor", _v$3, _p$.a));
|
||||||
|
_v$4 !== _p$.o && (_p$.o = _$setProp(_el$2, "width", _v$4, _p$.o));
|
||||||
|
_v$5 !== _p$.i && (_p$.i = _$setProp(_el$2, "onMouseDown", _v$5, _p$.i));
|
||||||
|
_v$6 !== _p$.n && (_p$.n = _$setProp(_el$3, "fg", _v$6, _p$.n));
|
||||||
|
return _p$;
|
||||||
|
}, {
|
||||||
|
e: undefined,
|
||||||
|
t: undefined,
|
||||||
|
a: undefined,
|
||||||
|
o: undefined,
|
||||||
|
i: undefined,
|
||||||
|
n: undefined
|
||||||
|
});
|
||||||
|
return _el$2;
|
||||||
|
})()
|
||||||
|
}));
|
||||||
|
return _el$;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { NotificationHost2 };
|
||||||
Vendored
+121
@@ -0,0 +1,121 @@
|
|||||||
|
// @bun
|
||||||
|
import {
|
||||||
|
theme2
|
||||||
|
} from "./chunk-p6jvscj3.js";
|
||||||
|
|
||||||
|
// src/components/HelpBar.tsx
|
||||||
|
import { createTextNode as _$createTextNode } from "@opentui/solid";
|
||||||
|
import { insertNode as _$insertNode } from "@opentui/solid";
|
||||||
|
import { effect as _$effect } from "@opentui/solid";
|
||||||
|
import { memo as _$memo } from "@opentui/solid";
|
||||||
|
import { insert as _$insert } from "@opentui/solid";
|
||||||
|
import { createComponent as _$createComponent } from "@opentui/solid";
|
||||||
|
import { use as _$use } from "@opentui/solid";
|
||||||
|
import { setProp as _$setProp } from "@opentui/solid";
|
||||||
|
import { createElement as _$createElement } from "@opentui/solid";
|
||||||
|
import { CliRenderEvents, TextAttributes } from "@opentui/core";
|
||||||
|
import { onResize, useRenderer } from "@opentui/solid";
|
||||||
|
import { formatCommandBindings } from "@opentui/keymap/extras";
|
||||||
|
import { useKeymapSelector } from "@opentui/keymap/solid";
|
||||||
|
import { createMemo, createSignal, For } from "solid-js";
|
||||||
|
var KEY_DISPLAY = {
|
||||||
|
up: "\u2191",
|
||||||
|
down: "\u2193",
|
||||||
|
left: "\u2190",
|
||||||
|
right: "\u2192",
|
||||||
|
enter: "Enter",
|
||||||
|
escape: "Esc"
|
||||||
|
};
|
||||||
|
var SEPARATOR = " ";
|
||||||
|
var ELLIPSIS = "\u2026";
|
||||||
|
function HelpBar2(props = {}) {
|
||||||
|
const accent = () => props.accentColor ?? theme2.primary;
|
||||||
|
const muted = () => props.mutedColor ?? theme2.muted;
|
||||||
|
const renderer = useRenderer();
|
||||||
|
const [width, setWidth] = createSignal(0);
|
||||||
|
let container;
|
||||||
|
const measure = () => {
|
||||||
|
if (!container)
|
||||||
|
return;
|
||||||
|
setWidth(container.width);
|
||||||
|
};
|
||||||
|
const remeasureNextFrame = () => renderer.once(CliRenderEvents.FRAME, measure);
|
||||||
|
onResize(remeasureNextFrame);
|
||||||
|
const entries = useKeymapSelector((keymap) => keymap.getCommandEntries({
|
||||||
|
visibility: "active"
|
||||||
|
}).filter((entry) => entry.command.shortHelp === true).map((entry) => ({
|
||||||
|
command: entry.command.name,
|
||||||
|
keys: formatCommandBindings(entry.bindings, {
|
||||||
|
keyNameAliases: KEY_DISPLAY
|
||||||
|
}) ?? "",
|
||||||
|
label: typeof entry.command.desc === "string" ? entry.command.desc : entry.command.name
|
||||||
|
})));
|
||||||
|
const visible = createMemo(() => {
|
||||||
|
const available = width() - 2;
|
||||||
|
const shown = [];
|
||||||
|
let used = 0;
|
||||||
|
for (const entry of entries()) {
|
||||||
|
const sepWidth = shown.length > 0 ? SEPARATOR.length : 0;
|
||||||
|
const entryWidth = entry.keys.length + 1 + entry.label.length;
|
||||||
|
if (used + sepWidth + entryWidth <= available) {
|
||||||
|
used += sepWidth + entryWidth;
|
||||||
|
shown.push(entry);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const remaining = available - used - sepWidth;
|
||||||
|
const prefix = `${entry.keys} `;
|
||||||
|
if (remaining > prefix.length + ELLIPSIS.length) {
|
||||||
|
const labelBudget = remaining - prefix.length - ELLIPSIS.length;
|
||||||
|
shown.push({
|
||||||
|
...entry,
|
||||||
|
label: `${entry.label.slice(0, labelBudget)}${ELLIPSIS}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return shown;
|
||||||
|
});
|
||||||
|
return (() => {
|
||||||
|
var _el$ = _$createElement("box");
|
||||||
|
_$use((el) => {
|
||||||
|
container = el;
|
||||||
|
remeasureNextFrame();
|
||||||
|
}, _el$);
|
||||||
|
_$setProp(_el$, "flexDirection", "row");
|
||||||
|
_$setProp(_el$, "paddingLeft", 1);
|
||||||
|
_$setProp(_el$, "paddingRight", 1);
|
||||||
|
_$setProp(_el$, "overflow", "hidden");
|
||||||
|
_$insert(_el$, _$createComponent(For, {
|
||||||
|
get each() {
|
||||||
|
return visible();
|
||||||
|
},
|
||||||
|
children: (entry, i) => [_$memo(() => _$memo(() => i() > 0)() && (() => {
|
||||||
|
var _el$4 = _$createElement("text");
|
||||||
|
_$insertNode(_el$4, _$createTextNode(` `));
|
||||||
|
_$effect((_$p) => _$setProp(_el$4, "fg", muted(), _$p));
|
||||||
|
return _el$4;
|
||||||
|
})()), (() => {
|
||||||
|
var _el$2 = _$createElement("text");
|
||||||
|
_$insert(_el$2, () => entry.keys);
|
||||||
|
_$effect((_p$) => {
|
||||||
|
var _v$ = accent(), _v$2 = TextAttributes.BOLD;
|
||||||
|
_v$ !== _p$.e && (_p$.e = _$setProp(_el$2, "fg", _v$, _p$.e));
|
||||||
|
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$2, "attributes", _v$2, _p$.t));
|
||||||
|
return _p$;
|
||||||
|
}, {
|
||||||
|
e: undefined,
|
||||||
|
t: undefined
|
||||||
|
});
|
||||||
|
return _el$2;
|
||||||
|
})(), (() => {
|
||||||
|
var _el$3 = _$createElement("text");
|
||||||
|
_$insert(_el$3, () => ` ${entry.label}`);
|
||||||
|
_$effect((_$p) => _$setProp(_el$3, "fg", muted(), _$p));
|
||||||
|
return _el$3;
|
||||||
|
})()]
|
||||||
|
}));
|
||||||
|
return _el$;
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
export { HelpBar2 };
|
||||||
Vendored
+44
@@ -0,0 +1,44 @@
|
|||||||
|
// @bun
|
||||||
|
import {
|
||||||
|
theme2
|
||||||
|
} from "./chunk-p6jvscj3.js";
|
||||||
|
|
||||||
|
// src/components/KeyValue.tsx
|
||||||
|
import { memo as _$memo } from "@opentui/solid";
|
||||||
|
import { effect as _$effect } from "@opentui/solid";
|
||||||
|
import { insertNode as _$insertNode } from "@opentui/solid";
|
||||||
|
import { createComponent as _$createComponent } from "@opentui/solid";
|
||||||
|
import { insert as _$insert } from "@opentui/solid";
|
||||||
|
import { setProp as _$setProp } from "@opentui/solid";
|
||||||
|
import { createElement as _$createElement } from "@opentui/solid";
|
||||||
|
import { Show } from "solid-js";
|
||||||
|
function KeyValue2(props) {
|
||||||
|
return (() => {
|
||||||
|
var _el$ = _$createElement("box"), _el$2 = _$createElement("text");
|
||||||
|
_$insertNode(_el$, _el$2);
|
||||||
|
_$setProp(_el$, "flexShrink", 0);
|
||||||
|
_$setProp(_el$, "flexDirection", "row");
|
||||||
|
_$setProp(_el$, "justifyContent", "space-between");
|
||||||
|
_$insert(_el$2, () => props.label);
|
||||||
|
_$insert(_el$, _$createComponent(Show, {
|
||||||
|
get when() {
|
||||||
|
return props.children;
|
||||||
|
},
|
||||||
|
get fallback() {
|
||||||
|
return (() => {
|
||||||
|
var _el$3 = _$createElement("text");
|
||||||
|
_$insert(_el$3, () => props.value ?? "");
|
||||||
|
_$effect((_$p) => _$setProp(_el$3, "fg", props.valueColor, _$p));
|
||||||
|
return _el$3;
|
||||||
|
})();
|
||||||
|
},
|
||||||
|
get children() {
|
||||||
|
return props.children;
|
||||||
|
}
|
||||||
|
}), null);
|
||||||
|
_$effect((_$p) => _$setProp(_el$2, "fg", theme2.muted, _$p));
|
||||||
|
return _el$;
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
export { KeyValue2 };
|
||||||
Vendored
+9
@@ -0,0 +1,9 @@
|
|||||||
|
// @bun
|
||||||
|
// src/context/help.ts
|
||||||
|
import { createSignal } from "solid-js";
|
||||||
|
var [helpOpen2, setHelpOpen] = createSignal(false);
|
||||||
|
function toggleHelp2() {
|
||||||
|
setHelpOpen((open) => !open);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { helpOpen2, toggleHelp2 };
|
||||||
Vendored
+87
@@ -0,0 +1,87 @@
|
|||||||
|
// @bun
|
||||||
|
import {
|
||||||
|
helpOpen2,
|
||||||
|
toggleHelp2
|
||||||
|
} from "./chunk-ee2qaswx.js";
|
||||||
|
import {
|
||||||
|
Modal2
|
||||||
|
} from "./chunk-t2cszvff.js";
|
||||||
|
|
||||||
|
// src/components/HelpModal.tsx
|
||||||
|
import { createComponent as _$createComponent } from "@opentui/solid";
|
||||||
|
import { insert as _$insert } from "@opentui/solid";
|
||||||
|
import { setProp as _$setProp } from "@opentui/solid";
|
||||||
|
import { effect as _$effect } from "@opentui/solid";
|
||||||
|
import { createTextNode as _$createTextNode } from "@opentui/solid";
|
||||||
|
import { insertNode as _$insertNode } from "@opentui/solid";
|
||||||
|
import { createElement as _$createElement } from "@opentui/solid";
|
||||||
|
import { TextAttributes } from "@opentui/core";
|
||||||
|
import { formatCommandBindings } from "@opentui/keymap/extras";
|
||||||
|
import { useKeymapSelector } from "@opentui/keymap/solid";
|
||||||
|
import { createSignal } from "solid-js";
|
||||||
|
var KEY_DISPLAY = {
|
||||||
|
up: "\u2191",
|
||||||
|
down: "\u2193",
|
||||||
|
left: "\u2190",
|
||||||
|
right: "\u2192",
|
||||||
|
enter: "Enter",
|
||||||
|
escape: "Esc"
|
||||||
|
};
|
||||||
|
function HelpModal2(props = {}) {
|
||||||
|
const [scope, setScope] = createSignal(null);
|
||||||
|
const entries = useKeymapSelector((keymap) => keymap.getCommandEntries({
|
||||||
|
visibility: "active",
|
||||||
|
focused: scope()
|
||||||
|
}).map((entry) => ({
|
||||||
|
command: entry.command.name,
|
||||||
|
keys: formatCommandBindings(entry.bindings, {
|
||||||
|
keyNameAliases: KEY_DISPLAY
|
||||||
|
}) ?? "",
|
||||||
|
label: typeof entry.command.desc === "string" ? entry.command.desc : entry.command.name
|
||||||
|
})));
|
||||||
|
return _$createComponent(Modal2, {
|
||||||
|
get open() {
|
||||||
|
return helpOpen2();
|
||||||
|
},
|
||||||
|
onDismiss: toggleHelp2,
|
||||||
|
onOpen: (previous) => setScope(() => previous),
|
||||||
|
get dismissKey() {
|
||||||
|
return props.dismissKey;
|
||||||
|
},
|
||||||
|
get accentColor() {
|
||||||
|
return props.accentColor;
|
||||||
|
},
|
||||||
|
get backgroundColor() {
|
||||||
|
return props.backgroundColor;
|
||||||
|
},
|
||||||
|
get backdropColor() {
|
||||||
|
return props.backdropColor;
|
||||||
|
},
|
||||||
|
get width() {
|
||||||
|
return props.width;
|
||||||
|
},
|
||||||
|
get maxHeight() {
|
||||||
|
return entries().length + 6;
|
||||||
|
},
|
||||||
|
scrollable: true,
|
||||||
|
get children() {
|
||||||
|
return [(() => {
|
||||||
|
var _el$ = _$createElement("text");
|
||||||
|
_$insertNode(_el$, _$createTextNode(`Keybindings`));
|
||||||
|
_$effect((_$p) => _$setProp(_el$, "attributes", TextAttributes.BOLD, _$p));
|
||||||
|
return _el$;
|
||||||
|
})(), (() => {
|
||||||
|
var _el$3 = _$createElement("text");
|
||||||
|
_$insertNode(_el$3, _$createTextNode(` `));
|
||||||
|
return _el$3;
|
||||||
|
})(), (() => {
|
||||||
|
var _el$5 = _$createElement("text");
|
||||||
|
_$insert(_el$5, () => entries().map((entry) => `${entry.keys.padEnd(10)}${entry.label}`).join(`
|
||||||
|
`));
|
||||||
|
return _el$5;
|
||||||
|
})()];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { HelpModal2 };
|
||||||
Vendored
+26
@@ -0,0 +1,26 @@
|
|||||||
|
// @bun
|
||||||
|
// src/context/helper.tsx
|
||||||
|
import { createComponent as _$createComponent } from "@opentui/solid";
|
||||||
|
import { createContext, useContext } from "solid-js";
|
||||||
|
function createSimpleContext2(input) {
|
||||||
|
const ctx = createContext();
|
||||||
|
return {
|
||||||
|
provider: (props) => {
|
||||||
|
const value = input.init(props);
|
||||||
|
return _$createComponent(ctx.Provider, {
|
||||||
|
value,
|
||||||
|
get children() {
|
||||||
|
return props.children;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
use() {
|
||||||
|
const value = useContext(ctx);
|
||||||
|
if (!value)
|
||||||
|
throw new Error(`${input.name} context must be used within its provider`);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { createSimpleContext2 };
|
||||||
Vendored
+35
@@ -0,0 +1,35 @@
|
|||||||
|
// @bun
|
||||||
|
import {
|
||||||
|
theme2
|
||||||
|
} from "./chunk-p6jvscj3.js";
|
||||||
|
|
||||||
|
// src/components/Spinner.tsx
|
||||||
|
import { setProp as _$setProp } from "@opentui/solid";
|
||||||
|
import { effect as _$effect } from "@opentui/solid";
|
||||||
|
import { createElement as _$createElement } from "@opentui/solid";
|
||||||
|
import"opentui-spinner/solid";
|
||||||
|
function Spinner2(props) {
|
||||||
|
return (() => {
|
||||||
|
var _el$ = _$createElement("spinner");
|
||||||
|
_$effect((_p$) => {
|
||||||
|
var { name: _v$, frames: _v$2, interval: _v$3 } = props, _v$4 = props.autoplay ?? !theme2.reducedMotion, _v$5 = props.color ?? theme2.primary, _v$6 = props.backgroundColor;
|
||||||
|
_v$ !== _p$.e && (_p$.e = _$setProp(_el$, "name", _v$, _p$.e));
|
||||||
|
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$, "frames", _v$2, _p$.t));
|
||||||
|
_v$3 !== _p$.a && (_p$.a = _$setProp(_el$, "interval", _v$3, _p$.a));
|
||||||
|
_v$4 !== _p$.o && (_p$.o = _$setProp(_el$, "autoplay", _v$4, _p$.o));
|
||||||
|
_v$5 !== _p$.i && (_p$.i = _$setProp(_el$, "color", _v$5, _p$.i));
|
||||||
|
_v$6 !== _p$.n && (_p$.n = _$setProp(_el$, "backgroundColor", _v$6, _p$.n));
|
||||||
|
return _p$;
|
||||||
|
}, {
|
||||||
|
e: undefined,
|
||||||
|
t: undefined,
|
||||||
|
a: undefined,
|
||||||
|
o: undefined,
|
||||||
|
i: undefined,
|
||||||
|
n: undefined
|
||||||
|
});
|
||||||
|
return _el$;
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Spinner2 };
|
||||||
Vendored
+65
@@ -0,0 +1,65 @@
|
|||||||
|
// @bun
|
||||||
|
// src/config.ts
|
||||||
|
import { z } from "zod";
|
||||||
|
var COLOR_KEYS2 = [
|
||||||
|
"base00",
|
||||||
|
"base01",
|
||||||
|
"base02",
|
||||||
|
"base03",
|
||||||
|
"base04",
|
||||||
|
"base05",
|
||||||
|
"base06",
|
||||||
|
"base07",
|
||||||
|
"base08",
|
||||||
|
"base09",
|
||||||
|
"base0a",
|
||||||
|
"base0b",
|
||||||
|
"base0c",
|
||||||
|
"base0d",
|
||||||
|
"base0e",
|
||||||
|
"base0f"
|
||||||
|
];
|
||||||
|
var OPENTUI_BORDER_STYLES2 = ["single", "double", "rounded", "heavy"];
|
||||||
|
var hexColor = z.string().regex(/^#?[0-9a-fA-F]{6}$/, 'expected a hex color like "#1e1e2e"');
|
||||||
|
var colorsShape = {};
|
||||||
|
for (const key of COLOR_KEYS2)
|
||||||
|
colorsShape[key] = hexColor.optional();
|
||||||
|
var ThemeConfigSchema2 = z.object({
|
||||||
|
nerd_fonts: z.boolean().optional(),
|
||||||
|
emoji_fonts: z.boolean().optional(),
|
||||||
|
reduced_motion: z.boolean().optional(),
|
||||||
|
mouse: z.boolean().optional(),
|
||||||
|
border: z.enum(OPENTUI_BORDER_STYLES2).optional(),
|
||||||
|
colors: z.object(colorsShape).optional()
|
||||||
|
});
|
||||||
|
function layerConfigs2(layers) {
|
||||||
|
const out = {};
|
||||||
|
for (const layer of layers) {
|
||||||
|
const { colors, ...rest } = layer;
|
||||||
|
Object.assign(out, rest);
|
||||||
|
if (colors)
|
||||||
|
out.colors = { ...out.colors, ...colors };
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
function resolveConfig2(config) {
|
||||||
|
const colors = {};
|
||||||
|
for (const key of COLOR_KEYS2)
|
||||||
|
colors[key] = normalizeColor2(config.colors?.[key] ?? "");
|
||||||
|
return {
|
||||||
|
nerdFonts: config.nerd_fonts ?? false,
|
||||||
|
emojiFonts: config.emoji_fonts ?? false,
|
||||||
|
reducedMotion: config.reduced_motion ?? false,
|
||||||
|
mouse: config.mouse ?? true,
|
||||||
|
border: config.border ?? "rounded",
|
||||||
|
colors
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function normalizeColor2(hex) {
|
||||||
|
const trimmed = hex.trim();
|
||||||
|
if (trimmed && !trimmed.startsWith("#"))
|
||||||
|
return `#${trimmed}`;
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { COLOR_KEYS2, OPENTUI_BORDER_STYLES2, ThemeConfigSchema2, layerConfigs2, resolveConfig2, normalizeColor2 };
|
||||||
Vendored
+79
@@ -0,0 +1,79 @@
|
|||||||
|
// @bun
|
||||||
|
import {
|
||||||
|
ThemeConfigSchema2,
|
||||||
|
layerConfigs2,
|
||||||
|
resolveConfig2
|
||||||
|
} from "./chunk-njvj322f.js";
|
||||||
|
import {
|
||||||
|
notify2
|
||||||
|
} from "./chunk-6ggk5fg7.js";
|
||||||
|
import {
|
||||||
|
buildPresets2
|
||||||
|
} from "./chunk-yyg967q8.js";
|
||||||
|
import {
|
||||||
|
configPath2
|
||||||
|
} from "./chunk-xykm10y9.js";
|
||||||
|
|
||||||
|
// src/index.ts
|
||||||
|
import { existsSync, readFileSync } from "fs";
|
||||||
|
import { join } from "path";
|
||||||
|
import { parse as parseYAML } from "yaml";
|
||||||
|
import { z } from "zod";
|
||||||
|
function reportThemeError(path, message) {
|
||||||
|
const text = `${path}: ${message}`;
|
||||||
|
console.error(`[ilovetui theme] ${text}`);
|
||||||
|
notify2(text, { kind: "warning", duration: 0 });
|
||||||
|
}
|
||||||
|
function readThemeFile(path) {
|
||||||
|
if (!existsSync(path))
|
||||||
|
return {};
|
||||||
|
let raw;
|
||||||
|
try {
|
||||||
|
raw = parseYAML(readFileSync(path, "utf8"));
|
||||||
|
} catch (error) {
|
||||||
|
reportThemeError(path, error instanceof Error ? error.message : String(error));
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const result = ThemeConfigSchema2.safeParse(raw ?? {});
|
||||||
|
if (!result.success) {
|
||||||
|
reportThemeError(path, z.prettifyError(result.error));
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
var fileLayers = [readThemeFile(join(import.meta.dir, "default.yaml")), readThemeFile(configPath2())];
|
||||||
|
function buildTheme(overrides) {
|
||||||
|
const resolved = resolveConfig2(layerConfigs2([...fileLayers, overrides]));
|
||||||
|
const colors = resolved.colors;
|
||||||
|
return {
|
||||||
|
...colors,
|
||||||
|
background: colors.base00,
|
||||||
|
subtleBg: colors.base01,
|
||||||
|
selection: colors.base02,
|
||||||
|
subtle: colors.base03,
|
||||||
|
muted: colors.base04,
|
||||||
|
text: colors.base05,
|
||||||
|
primary: colors.base0d,
|
||||||
|
success: colors.base0b,
|
||||||
|
warning: colors.base09,
|
||||||
|
error: colors.base08,
|
||||||
|
nerdFonts: resolved.nerdFonts,
|
||||||
|
emojiFonts: resolved.emojiFonts,
|
||||||
|
reducedMotion: resolved.reducedMotion,
|
||||||
|
mouse: resolved.mouse,
|
||||||
|
borderStyle: resolved.border
|
||||||
|
};
|
||||||
|
}
|
||||||
|
var theme2 = buildTheme({});
|
||||||
|
var presets2 = buildPresets2(theme2);
|
||||||
|
function configureTheme2(overrides = {}) {
|
||||||
|
Object.assign(theme2, buildTheme(overrides));
|
||||||
|
const next = buildPresets2(theme2);
|
||||||
|
for (const key of Object.keys(next))
|
||||||
|
Object.assign(presets2[key], next[key]);
|
||||||
|
}
|
||||||
|
function icon2(codepoint) {
|
||||||
|
return theme2.nerdFonts ? `${codepoint} ` : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export { theme2, presets2, configureTheme2, icon2 };
|
||||||
Vendored
+54
@@ -0,0 +1,54 @@
|
|||||||
|
// @bun
|
||||||
|
import {
|
||||||
|
theme2
|
||||||
|
} from "./chunk-p6jvscj3.js";
|
||||||
|
|
||||||
|
// src/components/MinSizeGuard.tsx
|
||||||
|
import { createComponent as _$createComponent } from "@opentui/solid";
|
||||||
|
import { effect as _$effect } from "@opentui/solid";
|
||||||
|
import { insertNode as _$insertNode } from "@opentui/solid";
|
||||||
|
import { insert as _$insert } from "@opentui/solid";
|
||||||
|
import { setProp as _$setProp } from "@opentui/solid";
|
||||||
|
import { createElement as _$createElement } from "@opentui/solid";
|
||||||
|
import { memo as _$memo } from "@opentui/solid";
|
||||||
|
import { useTerminalDimensions } from "@opentui/solid";
|
||||||
|
import { Show } from "solid-js";
|
||||||
|
function MinSizeGuard2(props) {
|
||||||
|
const dimensions = useTerminalDimensions();
|
||||||
|
const minWidth = () => props.minWidth ?? 80;
|
||||||
|
const minHeight = () => props.minHeight ?? 24;
|
||||||
|
const tooSmall = () => dimensions().width < minWidth() || dimensions().height < minHeight();
|
||||||
|
return [_$memo(() => props.children), _$createComponent(Show, {
|
||||||
|
get when() {
|
||||||
|
return tooSmall();
|
||||||
|
},
|
||||||
|
get children() {
|
||||||
|
var _el$ = _$createElement("box"), _el$2 = _$createElement("text");
|
||||||
|
_$insertNode(_el$, _el$2);
|
||||||
|
_$setProp(_el$, "position", "absolute");
|
||||||
|
_$setProp(_el$, "top", 0);
|
||||||
|
_$setProp(_el$, "left", 0);
|
||||||
|
_$setProp(_el$, "alignItems", "center");
|
||||||
|
_$setProp(_el$, "justifyContent", "center");
|
||||||
|
_$setProp(_el$, "zIndex", 9999);
|
||||||
|
_$insert(_el$2, () => `Terminal too small
|
||||||
|
Minimum size: ${minWidth()}x${minHeight()} \u2014 current: ${dimensions().width}x${dimensions().height}`);
|
||||||
|
_$effect((_p$) => {
|
||||||
|
var _v$ = dimensions().width, _v$2 = dimensions().height, _v$3 = props.backgroundColor ?? theme2.background, _v$4 = props.textColor ?? theme2.muted;
|
||||||
|
_v$ !== _p$.e && (_p$.e = _$setProp(_el$, "width", _v$, _p$.e));
|
||||||
|
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$, "height", _v$2, _p$.t));
|
||||||
|
_v$3 !== _p$.a && (_p$.a = _$setProp(_el$, "backgroundColor", _v$3, _p$.a));
|
||||||
|
_v$4 !== _p$.o && (_p$.o = _$setProp(_el$2, "fg", _v$4, _p$.o));
|
||||||
|
return _p$;
|
||||||
|
}, {
|
||||||
|
e: undefined,
|
||||||
|
t: undefined,
|
||||||
|
a: undefined,
|
||||||
|
o: undefined
|
||||||
|
});
|
||||||
|
return _el$;
|
||||||
|
}
|
||||||
|
})];
|
||||||
|
}
|
||||||
|
|
||||||
|
export { MinSizeGuard2 };
|
||||||
Vendored
+61
@@ -0,0 +1,61 @@
|
|||||||
|
// @bun
|
||||||
|
import {
|
||||||
|
theme2
|
||||||
|
} from "./chunk-p6jvscj3.js";
|
||||||
|
import {
|
||||||
|
Badge2
|
||||||
|
} from "./chunk-v9cetqzr.js";
|
||||||
|
|
||||||
|
// src/components/Toggle.tsx
|
||||||
|
import { setProp as _$setProp } from "@opentui/solid";
|
||||||
|
import { effect as _$effect } from "@opentui/solid";
|
||||||
|
import { insert as _$insert } from "@opentui/solid";
|
||||||
|
import { createComponent as _$createComponent } from "@opentui/solid";
|
||||||
|
import { memo as _$memo } from "@opentui/solid";
|
||||||
|
import { createElement as _$createElement } from "@opentui/solid";
|
||||||
|
import { TextAttributes } from "@opentui/core";
|
||||||
|
import { Show } from "solid-js";
|
||||||
|
function Toggle2(props) {
|
||||||
|
const onColor = () => props.onColor ?? theme2.success;
|
||||||
|
const offColor = () => props.offColor ?? theme2.muted;
|
||||||
|
const nerdFonts = () => props.withNerdfont ?? theme2.nerdFonts;
|
||||||
|
return (() => {
|
||||||
|
var _el$ = _$createElement("box");
|
||||||
|
_$insert(_el$, _$createComponent(Show, {
|
||||||
|
get when() {
|
||||||
|
return nerdFonts();
|
||||||
|
},
|
||||||
|
get fallback() {
|
||||||
|
return (() => {
|
||||||
|
var _el$2 = _$createElement("text");
|
||||||
|
_$insert(_el$2, () => props.on ? "on" : "off");
|
||||||
|
_$effect((_p$) => {
|
||||||
|
var _v$ = props.on ? onColor() : offColor(), _v$2 = TextAttributes.BOLD;
|
||||||
|
_v$ !== _p$.e && (_p$.e = _$setProp(_el$2, "fg", _v$, _p$.e));
|
||||||
|
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$2, "attributes", _v$2, _p$.t));
|
||||||
|
return _p$;
|
||||||
|
}, {
|
||||||
|
e: undefined,
|
||||||
|
t: undefined
|
||||||
|
});
|
||||||
|
return _el$2;
|
||||||
|
})();
|
||||||
|
},
|
||||||
|
get children() {
|
||||||
|
return _$createComponent(Badge2, {
|
||||||
|
get label() {
|
||||||
|
return props.on ? " \u25CF" : "\u25CF ";
|
||||||
|
},
|
||||||
|
get color() {
|
||||||
|
return _$memo(() => !!props.on)() ? onColor() : offColor();
|
||||||
|
},
|
||||||
|
withNerdfont: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
_$effect((_$p) => _$setProp(_el$, "onMouseDown", theme2.mouse ? props.onToggle : undefined, _$p));
|
||||||
|
return _el$;
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Toggle2 };
|
||||||
Vendored
+155
@@ -0,0 +1,155 @@
|
|||||||
|
// @bun
|
||||||
|
import {
|
||||||
|
theme2,
|
||||||
|
presets2
|
||||||
|
} from "./chunk-p6jvscj3.js";
|
||||||
|
|
||||||
|
// src/components/Sidebar.tsx
|
||||||
|
import { use as _$use } from "@opentui/solid";
|
||||||
|
import { createComponent as _$createComponent } from "@opentui/solid";
|
||||||
|
import { createTextNode as _$createTextNode } from "@opentui/solid";
|
||||||
|
import { insertNode as _$insertNode } from "@opentui/solid";
|
||||||
|
import { effect as _$effect } from "@opentui/solid";
|
||||||
|
import { insert as _$insert } from "@opentui/solid";
|
||||||
|
import { setProp as _$setProp } from "@opentui/solid";
|
||||||
|
import { createElement as _$createElement } from "@opentui/solid";
|
||||||
|
import { TextAttributes } from "@opentui/core";
|
||||||
|
import { createEffect, Show } from "solid-js";
|
||||||
|
function truncateWithEllipsis(text, maxWidth) {
|
||||||
|
if (maxWidth <= 0)
|
||||||
|
return "";
|
||||||
|
if (text.length <= maxWidth)
|
||||||
|
return text;
|
||||||
|
return text.substring(0, Math.max(0, maxWidth - 1)) + "\u2026";
|
||||||
|
}
|
||||||
|
function itemIndexAtScreenY(el, screenY) {
|
||||||
|
const internals = el;
|
||||||
|
if (!internals.linesPerItem)
|
||||||
|
return null;
|
||||||
|
const localY = screenY - el.screenY;
|
||||||
|
if (localY < 0)
|
||||||
|
return null;
|
||||||
|
const index = internals.scrollOffset + Math.floor(localY / internals.linesPerItem);
|
||||||
|
return index >= 0 && index < el.options.length ? index : null;
|
||||||
|
}
|
||||||
|
var BORDER_WIDTH = 2;
|
||||||
|
var ITEM_LEFT_PADDING = 1;
|
||||||
|
var ITEM_INDICATOR_WIDTH = 2;
|
||||||
|
function Sidebar2(props) {
|
||||||
|
let select;
|
||||||
|
const contentWidth = () => Math.max(0, (props.width ?? 24) - BORDER_WIDTH);
|
||||||
|
const itemTextWidth = () => Math.max(0, contentWidth() - ITEM_LEFT_PADDING - ITEM_INDICATOR_WIDTH);
|
||||||
|
const displayTitle = () => props.title ? truncateWithEllipsis(props.title, contentWidth()) : props.title;
|
||||||
|
const displayItems = () => props.items.map((item) => ({
|
||||||
|
...item,
|
||||||
|
name: truncateWithEllipsis(item.name, itemTextWidth()),
|
||||||
|
description: truncateWithEllipsis(item.description, itemTextWidth())
|
||||||
|
}));
|
||||||
|
const handleMouseDown = (event) => {
|
||||||
|
if (!select)
|
||||||
|
return;
|
||||||
|
const index = itemIndexAtScreenY(select, event.y);
|
||||||
|
if (index !== null) {
|
||||||
|
select.focus();
|
||||||
|
select.setSelectedIndex(index);
|
||||||
|
select.selectCurrent();
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleMouseScroll = (event) => {
|
||||||
|
if (!select || !event.scroll)
|
||||||
|
return;
|
||||||
|
const steps = Math.max(1, event.scroll.delta);
|
||||||
|
if (event.scroll.direction === "down")
|
||||||
|
select.moveDown(steps);
|
||||||
|
else if (event.scroll.direction === "up")
|
||||||
|
select.moveUp(steps);
|
||||||
|
};
|
||||||
|
createEffect(() => {
|
||||||
|
const index = props.items.findIndex((item) => item.value === props.value);
|
||||||
|
if (select && index !== -1 && select.getSelectedIndex() !== index)
|
||||||
|
select.setSelectedIndex(index);
|
||||||
|
});
|
||||||
|
return (() => {
|
||||||
|
var _el$ = _$createElement("box"), _el$5 = _$createElement("select");
|
||||||
|
_$insertNode(_el$, _el$5);
|
||||||
|
_$setProp(_el$, "border", true);
|
||||||
|
_$setProp(_el$, "flexDirection", "column");
|
||||||
|
_$insert(_el$, _$createComponent(Show, {
|
||||||
|
get when() {
|
||||||
|
return props.title;
|
||||||
|
},
|
||||||
|
get children() {
|
||||||
|
return [(() => {
|
||||||
|
var _el$2 = _$createElement("text");
|
||||||
|
_$insert(_el$2, displayTitle);
|
||||||
|
_$effect((_$p) => _$setProp(_el$2, "attributes", TextAttributes.BOLD, _$p));
|
||||||
|
return _el$2;
|
||||||
|
})(), (() => {
|
||||||
|
var _el$3 = _$createElement("text");
|
||||||
|
_$insertNode(_el$3, _$createTextNode(` `));
|
||||||
|
return _el$3;
|
||||||
|
})()];
|
||||||
|
}
|
||||||
|
}), _el$5);
|
||||||
|
_$use((el) => {
|
||||||
|
select = el ?? undefined;
|
||||||
|
props.ref?.(el && {
|
||||||
|
moveUp: () => el.moveUp(),
|
||||||
|
moveDown: () => el.moveDown(),
|
||||||
|
selectCurrent: () => el.selectCurrent(),
|
||||||
|
focus: () => el.focus()
|
||||||
|
});
|
||||||
|
}, _el$5);
|
||||||
|
_$setProp(_el$5, "flexGrow", 1);
|
||||||
|
_$setProp(_el$5, "showScrollIndicator", true);
|
||||||
|
_$setProp(_el$5, "onChange", (_index, option) => {
|
||||||
|
if (!option)
|
||||||
|
return;
|
||||||
|
props.onSelect(option);
|
||||||
|
});
|
||||||
|
_$setProp(_el$5, "onSelect", (_index, option) => {
|
||||||
|
if (!option)
|
||||||
|
return;
|
||||||
|
props.onConfirm?.(option);
|
||||||
|
});
|
||||||
|
_$effect((_p$) => {
|
||||||
|
var _v$ = props.width ?? 24, _v$2 = theme2.borderStyle, _v$3 = props.focused ? props.accentColor ?? theme2.primary : props.mutedColor ?? theme2.muted, _v$4 = props.focused, _v$5 = displayItems(), _v$6 = theme2.mouse ? handleMouseDown : undefined, _v$7 = theme2.mouse ? handleMouseScroll : undefined, _v$8 = props.backgroundColor ?? "transparent", _v$9 = props.textColor ?? presets2.select.textColor, _v$0 = props.focusedBackgroundColor ?? "transparent", _v$1 = props.focusedTextColor ?? presets2.select.focusedTextColor, _v$10 = props.selectedBackgroundColor ?? presets2.select.selectedBackgroundColor, _v$11 = props.selectedTextColor ?? presets2.select.selectedTextColor, _v$12 = props.descriptionColor ?? presets2.select.descriptionColor, _v$13 = props.selectedDescriptionColor ?? presets2.select.selectedDescriptionColor;
|
||||||
|
_v$ !== _p$.e && (_p$.e = _$setProp(_el$, "width", _v$, _p$.e));
|
||||||
|
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$, "borderStyle", _v$2, _p$.t));
|
||||||
|
_v$3 !== _p$.a && (_p$.a = _$setProp(_el$, "borderColor", _v$3, _p$.a));
|
||||||
|
_v$4 !== _p$.o && (_p$.o = _$setProp(_el$5, "focused", _v$4, _p$.o));
|
||||||
|
_v$5 !== _p$.i && (_p$.i = _$setProp(_el$5, "options", _v$5, _p$.i));
|
||||||
|
_v$6 !== _p$.n && (_p$.n = _$setProp(_el$5, "onMouseDown", _v$6, _p$.n));
|
||||||
|
_v$7 !== _p$.s && (_p$.s = _$setProp(_el$5, "onMouseScroll", _v$7, _p$.s));
|
||||||
|
_v$8 !== _p$.h && (_p$.h = _$setProp(_el$5, "backgroundColor", _v$8, _p$.h));
|
||||||
|
_v$9 !== _p$.r && (_p$.r = _$setProp(_el$5, "textColor", _v$9, _p$.r));
|
||||||
|
_v$0 !== _p$.d && (_p$.d = _$setProp(_el$5, "focusedBackgroundColor", _v$0, _p$.d));
|
||||||
|
_v$1 !== _p$.l && (_p$.l = _$setProp(_el$5, "focusedTextColor", _v$1, _p$.l));
|
||||||
|
_v$10 !== _p$.u && (_p$.u = _$setProp(_el$5, "selectedBackgroundColor", _v$10, _p$.u));
|
||||||
|
_v$11 !== _p$.c && (_p$.c = _$setProp(_el$5, "selectedTextColor", _v$11, _p$.c));
|
||||||
|
_v$12 !== _p$.w && (_p$.w = _$setProp(_el$5, "descriptionColor", _v$12, _p$.w));
|
||||||
|
_v$13 !== _p$.m && (_p$.m = _$setProp(_el$5, "selectedDescriptionColor", _v$13, _p$.m));
|
||||||
|
return _p$;
|
||||||
|
}, {
|
||||||
|
e: undefined,
|
||||||
|
t: undefined,
|
||||||
|
a: undefined,
|
||||||
|
o: undefined,
|
||||||
|
i: undefined,
|
||||||
|
n: undefined,
|
||||||
|
s: undefined,
|
||||||
|
h: undefined,
|
||||||
|
r: undefined,
|
||||||
|
d: undefined,
|
||||||
|
l: undefined,
|
||||||
|
u: undefined,
|
||||||
|
c: undefined,
|
||||||
|
w: undefined,
|
||||||
|
m: undefined
|
||||||
|
});
|
||||||
|
return _el$;
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Sidebar2 };
|
||||||
Vendored
+71
@@ -0,0 +1,71 @@
|
|||||||
|
// @bun
|
||||||
|
import {
|
||||||
|
theme2,
|
||||||
|
presets2
|
||||||
|
} from "./chunk-p6jvscj3.js";
|
||||||
|
|
||||||
|
// src/components/ListRow.tsx
|
||||||
|
import { effect as _$effect } from "@opentui/solid";
|
||||||
|
import { createComponent as _$createComponent } from "@opentui/solid";
|
||||||
|
import { insertNode as _$insertNode } from "@opentui/solid";
|
||||||
|
import { insert as _$insert } from "@opentui/solid";
|
||||||
|
import { setProp as _$setProp } from "@opentui/solid";
|
||||||
|
import { createElement as _$createElement } from "@opentui/solid";
|
||||||
|
import { TextAttributes } from "@opentui/core";
|
||||||
|
import { Show } from "solid-js";
|
||||||
|
function ListRow2(props) {
|
||||||
|
const tone = (fallback) => props.selected ? presets2.select.selectedTextColor : fallback;
|
||||||
|
return (() => {
|
||||||
|
var _el$ = _$createElement("box"), _el$2 = _$createElement("box"), _el$3 = _$createElement("box"), _el$4 = _$createElement("text");
|
||||||
|
_$insertNode(_el$, _el$2);
|
||||||
|
_$setProp(_el$, "flexShrink", 0);
|
||||||
|
_$setProp(_el$, "flexDirection", "column");
|
||||||
|
_$setProp(_el$, "paddingLeft", 1);
|
||||||
|
_$setProp(_el$, "paddingRight", 1);
|
||||||
|
_$insertNode(_el$2, _el$3);
|
||||||
|
_$setProp(_el$2, "flexDirection", "row");
|
||||||
|
_$setProp(_el$2, "justifyContent", "space-between");
|
||||||
|
_$setProp(_el$2, "alignItems", "center");
|
||||||
|
_$setProp(_el$2, "height", 1);
|
||||||
|
_$insertNode(_el$3, _el$4);
|
||||||
|
_$setProp(_el$3, "flexGrow", 1);
|
||||||
|
_$setProp(_el$3, "flexShrink", 1);
|
||||||
|
_$setProp(_el$3, "minWidth", 0);
|
||||||
|
_$setProp(_el$3, "marginRight", 1);
|
||||||
|
_$setProp(_el$3, "overflow", "hidden");
|
||||||
|
_$setProp(_el$4, "wrapMode", "none");
|
||||||
|
_$insert(_el$4, () => props.label);
|
||||||
|
_$insert(_el$2, _$createComponent(Show, {
|
||||||
|
get when() {
|
||||||
|
return props.right;
|
||||||
|
},
|
||||||
|
get children() {
|
||||||
|
var _el$5 = _$createElement("box");
|
||||||
|
_$setProp(_el$5, "flexShrink", 0);
|
||||||
|
_$setProp(_el$5, "flexDirection", "row");
|
||||||
|
_$setProp(_el$5, "alignItems", "center");
|
||||||
|
_$insert(_el$5, () => props.right?.(tone));
|
||||||
|
return _el$5;
|
||||||
|
}
|
||||||
|
}), null);
|
||||||
|
_$insert(_el$, () => props.below?.(tone), null);
|
||||||
|
_$effect((_p$) => {
|
||||||
|
var _v$ = props.id, _v$2 = props.selected ? presets2.select.selectedBackgroundColor : "transparent", _v$3 = theme2.mouse && props.onClick ? () => props.onClick?.() : undefined, _v$4 = tone(), _v$5 = props.bold ? TextAttributes.BOLD : undefined;
|
||||||
|
_v$ !== _p$.e && (_p$.e = _$setProp(_el$, "id", _v$, _p$.e));
|
||||||
|
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$, "backgroundColor", _v$2, _p$.t));
|
||||||
|
_v$3 !== _p$.a && (_p$.a = _$setProp(_el$, "onMouseDown", _v$3, _p$.a));
|
||||||
|
_v$4 !== _p$.o && (_p$.o = _$setProp(_el$4, "fg", _v$4, _p$.o));
|
||||||
|
_v$5 !== _p$.i && (_p$.i = _$setProp(_el$4, "attributes", _v$5, _p$.i));
|
||||||
|
return _p$;
|
||||||
|
}, {
|
||||||
|
e: undefined,
|
||||||
|
t: undefined,
|
||||||
|
a: undefined,
|
||||||
|
o: undefined,
|
||||||
|
i: undefined
|
||||||
|
});
|
||||||
|
return _el$;
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ListRow2 };
|
||||||
Vendored
+140
@@ -0,0 +1,140 @@
|
|||||||
|
// @bun
|
||||||
|
import {
|
||||||
|
theme2
|
||||||
|
} from "./chunk-p6jvscj3.js";
|
||||||
|
|
||||||
|
// src/components/Modal.tsx
|
||||||
|
import { insertNode as _$insertNode } from "@opentui/solid";
|
||||||
|
import { effect as _$effect } from "@opentui/solid";
|
||||||
|
import { insert as _$insert } from "@opentui/solid";
|
||||||
|
import { use as _$use } from "@opentui/solid";
|
||||||
|
import { setProp as _$setProp } from "@opentui/solid";
|
||||||
|
import { createElement as _$createElement } from "@opentui/solid";
|
||||||
|
import { createComponent as _$createComponent } from "@opentui/solid";
|
||||||
|
import { RGBA } from "@opentui/core";
|
||||||
|
import { useBindings } from "@opentui/keymap/solid";
|
||||||
|
import { Portal, useRenderer, useTerminalDimensions } from "@opentui/solid";
|
||||||
|
import { createSignal, onCleanup, onMount, Show } from "solid-js";
|
||||||
|
function Modal2(props) {
|
||||||
|
return _$createComponent(Portal, {
|
||||||
|
get children() {
|
||||||
|
return _$createComponent(Show, {
|
||||||
|
get when() {
|
||||||
|
return props.open;
|
||||||
|
},
|
||||||
|
get children() {
|
||||||
|
return _$createComponent(ModalPanel, props);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function ModalPanel(props) {
|
||||||
|
const renderer = useRenderer();
|
||||||
|
const dimensions = useTerminalDimensions();
|
||||||
|
const [panel, setPanel] = createSignal();
|
||||||
|
const screenCap = () => Math.max(5, dimensions().height - 4);
|
||||||
|
const maxPanelHeight = () => Math.min(props.maxHeight ?? screenCap(), screenCap());
|
||||||
|
const maxContentHeight = () => Math.max(1, maxPanelHeight() - 4);
|
||||||
|
const maxPanelWidth = () => Math.max(10, dimensions().width - 4);
|
||||||
|
const panelWidth = () => Math.min(props.width ?? 50, maxPanelWidth());
|
||||||
|
let previousFocus = null;
|
||||||
|
onMount(() => {
|
||||||
|
previousFocus = renderer.currentFocusedRenderable;
|
||||||
|
props.onOpen?.(previousFocus);
|
||||||
|
panel()?.focus();
|
||||||
|
});
|
||||||
|
onCleanup(() => {
|
||||||
|
if (previousFocus && !previousFocus.isDestroyed)
|
||||||
|
previousFocus.focus();
|
||||||
|
});
|
||||||
|
const dismissKey = props.dismissKey ?? "escape";
|
||||||
|
if (dismissKey !== false || props.commands || props.bindings) {
|
||||||
|
useBindings(() => ({
|
||||||
|
target: panel,
|
||||||
|
commands: [...dismissKey !== false ? [{
|
||||||
|
name: "modal_dismiss",
|
||||||
|
desc: "close",
|
||||||
|
shortHelp: true,
|
||||||
|
run: () => props.onDismiss?.()
|
||||||
|
}] : [], ...props.commands ?? []],
|
||||||
|
bindings: [...dismissKey !== false ? [{
|
||||||
|
key: dismissKey,
|
||||||
|
cmd: "modal_dismiss"
|
||||||
|
}] : [], ...props.bindings ?? []]
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return (() => {
|
||||||
|
var _el$ = _$createElement("box"), _el$2 = _$createElement("box");
|
||||||
|
_$insertNode(_el$, _el$2);
|
||||||
|
_$setProp(_el$, "position", "absolute");
|
||||||
|
_$setProp(_el$, "top", 0);
|
||||||
|
_$setProp(_el$, "left", 0);
|
||||||
|
_$setProp(_el$, "alignItems", "center");
|
||||||
|
_$setProp(_el$, "justifyContent", "center");
|
||||||
|
_$setProp(_el$, "zIndex", 2000);
|
||||||
|
_$use(setPanel, _el$2);
|
||||||
|
_$setProp(_el$2, "focusable", true);
|
||||||
|
_$setProp(_el$2, "border", true);
|
||||||
|
_$setProp(_el$2, "flexDirection", "column");
|
||||||
|
_$setProp(_el$2, "paddingLeft", 2);
|
||||||
|
_$setProp(_el$2, "paddingRight", 2);
|
||||||
|
_$setProp(_el$2, "paddingTop", 1);
|
||||||
|
_$setProp(_el$2, "paddingBottom", 1);
|
||||||
|
_$insert(_el$2, _$createComponent(Show, {
|
||||||
|
get when() {
|
||||||
|
return props.scrollable;
|
||||||
|
},
|
||||||
|
get fallback() {
|
||||||
|
return (() => {
|
||||||
|
var _el$4 = _$createElement("box");
|
||||||
|
_$setProp(_el$4, "flexDirection", "column");
|
||||||
|
_$setProp(_el$4, "overflow", "hidden");
|
||||||
|
_$insert(_el$4, () => props.children);
|
||||||
|
_$effect((_$p) => _$setProp(_el$4, "maxHeight", maxContentHeight(), _$p));
|
||||||
|
return _el$4;
|
||||||
|
})();
|
||||||
|
},
|
||||||
|
get children() {
|
||||||
|
var _el$3 = _$createElement("scrollbox");
|
||||||
|
_$setProp(_el$3, "flexShrink", 1);
|
||||||
|
_$setProp(_el$3, "scrollY", true);
|
||||||
|
_$setProp(_el$3, "scrollX", false);
|
||||||
|
_$setProp(_el$3, "focusable", false);
|
||||||
|
_$insert(_el$3, () => props.children);
|
||||||
|
_$effect((_$p) => _$setProp(_el$3, "maxHeight", maxContentHeight(), _$p));
|
||||||
|
return _el$3;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
_$effect((_p$) => {
|
||||||
|
var _v$ = dimensions().width, _v$2 = dimensions().height, _v$3 = props.backdropColor ?? RGBA.fromInts(0, 0, 0, 150), _v$4 = theme2.mouse ? props.onDismiss : undefined, _v$5 = panelWidth(), _v$6 = maxPanelHeight(), _v$7 = theme2.borderStyle, _v$8 = props.accentColor ?? theme2.primary, _v$9 = props.accentColor ?? theme2.primary, _v$0 = props.backgroundColor ?? theme2.background, _v$1 = theme2.mouse ? (event) => event.stopPropagation() : undefined;
|
||||||
|
_v$ !== _p$.e && (_p$.e = _$setProp(_el$, "width", _v$, _p$.e));
|
||||||
|
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$, "height", _v$2, _p$.t));
|
||||||
|
_v$3 !== _p$.a && (_p$.a = _$setProp(_el$, "backgroundColor", _v$3, _p$.a));
|
||||||
|
_v$4 !== _p$.o && (_p$.o = _$setProp(_el$, "onMouseDown", _v$4, _p$.o));
|
||||||
|
_v$5 !== _p$.i && (_p$.i = _$setProp(_el$2, "width", _v$5, _p$.i));
|
||||||
|
_v$6 !== _p$.n && (_p$.n = _$setProp(_el$2, "maxHeight", _v$6, _p$.n));
|
||||||
|
_v$7 !== _p$.s && (_p$.s = _$setProp(_el$2, "borderStyle", _v$7, _p$.s));
|
||||||
|
_v$8 !== _p$.h && (_p$.h = _$setProp(_el$2, "borderColor", _v$8, _p$.h));
|
||||||
|
_v$9 !== _p$.r && (_p$.r = _$setProp(_el$2, "focusedBorderColor", _v$9, _p$.r));
|
||||||
|
_v$0 !== _p$.d && (_p$.d = _$setProp(_el$2, "backgroundColor", _v$0, _p$.d));
|
||||||
|
_v$1 !== _p$.l && (_p$.l = _$setProp(_el$2, "onMouseDown", _v$1, _p$.l));
|
||||||
|
return _p$;
|
||||||
|
}, {
|
||||||
|
e: undefined,
|
||||||
|
t: undefined,
|
||||||
|
a: undefined,
|
||||||
|
o: undefined,
|
||||||
|
i: undefined,
|
||||||
|
n: undefined,
|
||||||
|
s: undefined,
|
||||||
|
h: undefined,
|
||||||
|
r: undefined,
|
||||||
|
d: undefined,
|
||||||
|
l: undefined
|
||||||
|
});
|
||||||
|
return _el$;
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Modal2 };
|
||||||
Vendored
+32
@@ -0,0 +1,32 @@
|
|||||||
|
// @bun
|
||||||
|
import {
|
||||||
|
theme2
|
||||||
|
} from "./chunk-p6jvscj3.js";
|
||||||
|
|
||||||
|
// src/components/SignalBars.tsx
|
||||||
|
import { effect as _$effect } from "@opentui/solid";
|
||||||
|
import { insert as _$insert } from "@opentui/solid";
|
||||||
|
import { createComponent as _$createComponent } from "@opentui/solid";
|
||||||
|
import { setProp as _$setProp } from "@opentui/solid";
|
||||||
|
import { createElement as _$createElement } from "@opentui/solid";
|
||||||
|
import { For } from "solid-js";
|
||||||
|
var BARS = ["\u2581", "\u2583", "\u2585", "\u2587"];
|
||||||
|
function SignalBars2(props) {
|
||||||
|
const level = () => Math.floor(props.signal * BARS.length / 100);
|
||||||
|
return (() => {
|
||||||
|
var _el$ = _$createElement("box");
|
||||||
|
_$setProp(_el$, "flexDirection", "row");
|
||||||
|
_$insert(_el$, _$createComponent(For, {
|
||||||
|
each: BARS,
|
||||||
|
children: (bar, i) => (() => {
|
||||||
|
var _el$2 = _$createElement("text");
|
||||||
|
_$insert(_el$2, bar);
|
||||||
|
_$effect((_$p) => _$setProp(_el$2, "fg", i() < level() ? props.filledColor : props.emptyColor ?? theme2.muted, _$p));
|
||||||
|
return _el$2;
|
||||||
|
})()
|
||||||
|
}));
|
||||||
|
return _el$;
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
export { SignalBars2 };
|
||||||
Vendored
+83
@@ -0,0 +1,83 @@
|
|||||||
|
// @bun
|
||||||
|
import {
|
||||||
|
theme2
|
||||||
|
} from "./chunk-p6jvscj3.js";
|
||||||
|
|
||||||
|
// src/components/Badge.tsx
|
||||||
|
import { insert as _$insert } from "@opentui/solid";
|
||||||
|
import { memo as _$memo } from "@opentui/solid";
|
||||||
|
import { createComponent as _$createComponent } from "@opentui/solid";
|
||||||
|
import { effect as _$effect } from "@opentui/solid";
|
||||||
|
import { createTextNode as _$createTextNode } from "@opentui/solid";
|
||||||
|
import { insertNode as _$insertNode } from "@opentui/solid";
|
||||||
|
import { setProp as _$setProp } from "@opentui/solid";
|
||||||
|
import { createElement as _$createElement } from "@opentui/solid";
|
||||||
|
import { TextAttributes } from "@opentui/core";
|
||||||
|
import { Show } from "solid-js";
|
||||||
|
function Badge2(props) {
|
||||||
|
const textColor = () => props.textColor ?? theme2.background;
|
||||||
|
const backgroundColor = () => props.backgroundColor ?? theme2.background;
|
||||||
|
const attributes = () => props.bold === false ? undefined : TextAttributes.BOLD;
|
||||||
|
const nerdFonts = () => props.withNerdfont ?? theme2.nerdFonts;
|
||||||
|
return (() => {
|
||||||
|
var _el$ = _$createElement("box"), _el$4 = _$createElement("text");
|
||||||
|
_$insertNode(_el$, _el$4);
|
||||||
|
_$setProp(_el$, "flexDirection", "row");
|
||||||
|
_$insert(_el$, _$createComponent(Show, {
|
||||||
|
get when() {
|
||||||
|
return nerdFonts();
|
||||||
|
},
|
||||||
|
get children() {
|
||||||
|
var _el$2 = _$createElement("text");
|
||||||
|
_$insertNode(_el$2, _$createTextNode(`\uE0B6`));
|
||||||
|
_$effect((_p$) => {
|
||||||
|
var _v$ = props.color, _v$2 = backgroundColor();
|
||||||
|
_v$ !== _p$.e && (_p$.e = _$setProp(_el$2, "fg", _v$, _p$.e));
|
||||||
|
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$2, "bg", _v$2, _p$.t));
|
||||||
|
return _p$;
|
||||||
|
}, {
|
||||||
|
e: undefined,
|
||||||
|
t: undefined
|
||||||
|
});
|
||||||
|
return _el$2;
|
||||||
|
}
|
||||||
|
}), _el$4);
|
||||||
|
_$insert(_el$4, (() => {
|
||||||
|
var _c$ = _$memo(() => !!nerdFonts());
|
||||||
|
return () => _c$() ? props.label : ` ${props.label} `;
|
||||||
|
})());
|
||||||
|
_$insert(_el$, _$createComponent(Show, {
|
||||||
|
get when() {
|
||||||
|
return nerdFonts();
|
||||||
|
},
|
||||||
|
get children() {
|
||||||
|
var _el$5 = _$createElement("text");
|
||||||
|
_$insertNode(_el$5, _$createTextNode(`\uE0B4`));
|
||||||
|
_$effect((_p$) => {
|
||||||
|
var _v$3 = props.color, _v$4 = backgroundColor();
|
||||||
|
_v$3 !== _p$.e && (_p$.e = _$setProp(_el$5, "fg", _v$3, _p$.e));
|
||||||
|
_v$4 !== _p$.t && (_p$.t = _$setProp(_el$5, "bg", _v$4, _p$.t));
|
||||||
|
return _p$;
|
||||||
|
}, {
|
||||||
|
e: undefined,
|
||||||
|
t: undefined
|
||||||
|
});
|
||||||
|
return _el$5;
|
||||||
|
}
|
||||||
|
}), null);
|
||||||
|
_$effect((_p$) => {
|
||||||
|
var _v$5 = textColor(), _v$6 = props.color, _v$7 = attributes();
|
||||||
|
_v$5 !== _p$.e && (_p$.e = _$setProp(_el$4, "fg", _v$5, _p$.e));
|
||||||
|
_v$6 !== _p$.t && (_p$.t = _$setProp(_el$4, "bg", _v$6, _p$.t));
|
||||||
|
_v$7 !== _p$.a && (_p$.a = _$setProp(_el$4, "attributes", _v$7, _p$.a));
|
||||||
|
return _p$;
|
||||||
|
}, {
|
||||||
|
e: undefined,
|
||||||
|
t: undefined,
|
||||||
|
a: undefined
|
||||||
|
});
|
||||||
|
return _el$;
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge2 };
|
||||||
Vendored
+42
@@ -0,0 +1,42 @@
|
|||||||
|
// @bun
|
||||||
|
import {
|
||||||
|
useFocusWithin2
|
||||||
|
} from "./chunk-59dzyjgd.js";
|
||||||
|
|
||||||
|
// src/components/Pane.tsx
|
||||||
|
import { createComponent as _$createComponent } from "@opentui/solid";
|
||||||
|
import { insert as _$insert } from "@opentui/solid";
|
||||||
|
import { use as _$use } from "@opentui/solid";
|
||||||
|
import { spread as _$spread } from "@opentui/solid";
|
||||||
|
import { createElement as _$createElement } from "@opentui/solid";
|
||||||
|
import { createContext, createSignal, splitProps, useContext } from "solid-js";
|
||||||
|
var PaneContext = createContext();
|
||||||
|
function Pane2(props) {
|
||||||
|
const [local, rest] = splitProps(props, ["handle", "children"]);
|
||||||
|
const [el, setEl] = createSignal();
|
||||||
|
const focused = useFocusWithin2(el);
|
||||||
|
const handle = {
|
||||||
|
target: el,
|
||||||
|
focused,
|
||||||
|
focus: () => el()?.focus()
|
||||||
|
};
|
||||||
|
local.handle?.(handle);
|
||||||
|
return _$createComponent(PaneContext.Provider, {
|
||||||
|
value: handle,
|
||||||
|
get children() {
|
||||||
|
var _el$ = _$createElement("box");
|
||||||
|
_$use(setEl, _el$);
|
||||||
|
_$spread(_el$, rest, true);
|
||||||
|
_$insert(_el$, () => local.children);
|
||||||
|
return _el$;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function usePane2() {
|
||||||
|
const pane = useContext(PaneContext);
|
||||||
|
if (!pane)
|
||||||
|
throw new Error("usePane() must be used inside a <Pane>");
|
||||||
|
return pane;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Pane2, usePane2 };
|
||||||
Vendored
+168
@@ -0,0 +1,168 @@
|
|||||||
|
// @bun
|
||||||
|
import {
|
||||||
|
theme2
|
||||||
|
} from "./chunk-p6jvscj3.js";
|
||||||
|
|
||||||
|
// src/components/Tabs.tsx
|
||||||
|
import { effect as _$effect } from "@opentui/solid";
|
||||||
|
import { insertNode as _$insertNode } from "@opentui/solid";
|
||||||
|
import { createComponent as _$createComponent } from "@opentui/solid";
|
||||||
|
import { insert as _$insert } from "@opentui/solid";
|
||||||
|
import { use as _$use } from "@opentui/solid";
|
||||||
|
import { setProp as _$setProp } from "@opentui/solid";
|
||||||
|
import { createElement as _$createElement } from "@opentui/solid";
|
||||||
|
import { BorderChars, CliRenderEvents, TextAttributes } from "@opentui/core";
|
||||||
|
import { onResize, useRenderer } from "@opentui/solid";
|
||||||
|
import { createMemo, createSignal, Index } from "solid-js";
|
||||||
|
function stepTabValue2(items, value, delta) {
|
||||||
|
const index = items.findIndex((t) => t.value === value);
|
||||||
|
if (index === -1)
|
||||||
|
return value;
|
||||||
|
return items[(index + delta + items.length) % items.length].value;
|
||||||
|
}
|
||||||
|
function innerWidth(label) {
|
||||||
|
return [...label].length + 2;
|
||||||
|
}
|
||||||
|
function segmentWidth(label) {
|
||||||
|
return innerWidth(label) + 2;
|
||||||
|
}
|
||||||
|
function topEdge(chars, label) {
|
||||||
|
return `${chars.topLeft}${chars.horizontal.repeat(innerWidth(label))}${chars.topRight}`;
|
||||||
|
}
|
||||||
|
function bottomEdge(chars, label, isFirst, isLast, isActive) {
|
||||||
|
const left = isFirst ? isActive ? chars.vertical : chars.leftT : isActive ? chars.bottomRight : chars.bottomT;
|
||||||
|
const right = isLast ? isActive ? chars.vertical : chars.rightT : isActive ? chars.bottomLeft : chars.bottomT;
|
||||||
|
const fill = isActive ? " " : chars.horizontal;
|
||||||
|
return left + fill.repeat(innerWidth(label)) + right;
|
||||||
|
}
|
||||||
|
function buildBottomRow(chars, segments, available) {
|
||||||
|
const tabsWidth = segments.reduce((sum, s) => sum + segmentWidth(s.label), 0);
|
||||||
|
const remainder = available - tabsWidth;
|
||||||
|
const extending = remainder > 0;
|
||||||
|
const tabs = segments.map((s, i) => bottomEdge(chars, s.label, i === 0, i === segments.length - 1 && !extending, s.isActive)).join("");
|
||||||
|
return extending ? tabs + chars.horizontal.repeat(remainder - 1) + chars.topRight : tabs;
|
||||||
|
}
|
||||||
|
function fitCount(items, start, available) {
|
||||||
|
const hiddenBefore = start;
|
||||||
|
let used = 0;
|
||||||
|
let count = 0;
|
||||||
|
for (let i = start;i < items.length; i++) {
|
||||||
|
const width = segmentWidth(items[i].label);
|
||||||
|
const hiddenAfter = items.length - start - count - 1;
|
||||||
|
const totalHidden = hiddenBefore + hiddenAfter;
|
||||||
|
const reserve = totalHidden > 0 ? segmentWidth(`+${totalHidden}`) : 0;
|
||||||
|
if (used + width + reserve > available)
|
||||||
|
break;
|
||||||
|
used += width;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
return Math.max(count, 1);
|
||||||
|
}
|
||||||
|
function Tabs2(props) {
|
||||||
|
const renderer = useRenderer();
|
||||||
|
const [width, setWidth] = createSignal(0);
|
||||||
|
let container;
|
||||||
|
const measure = () => {
|
||||||
|
if (!container)
|
||||||
|
return;
|
||||||
|
setWidth(container.width);
|
||||||
|
};
|
||||||
|
const remeasureNextFrame = () => renderer.once(CliRenderEvents.FRAME, measure);
|
||||||
|
onResize(remeasureNextFrame);
|
||||||
|
const segments = createMemo(() => {
|
||||||
|
const items = props.items;
|
||||||
|
const activeIndex = Math.max(0, items.findIndex((t) => t.value === props.value));
|
||||||
|
let start = 0;
|
||||||
|
let count = fitCount(items, start, width());
|
||||||
|
if (activeIndex >= start + count) {
|
||||||
|
start = activeIndex;
|
||||||
|
count = fitCount(items, start, width());
|
||||||
|
}
|
||||||
|
const shownItems = items.slice(start, start + count);
|
||||||
|
const shown = shownItems.map((item) => ({
|
||||||
|
value: item.value,
|
||||||
|
label: item.label,
|
||||||
|
isActive: item.value === props.value
|
||||||
|
}));
|
||||||
|
const hidden = items.length - count;
|
||||||
|
if (hidden > 0) {
|
||||||
|
const used = shownItems.reduce((sum, item) => sum + segmentWidth(item.label), 0);
|
||||||
|
const tag = `+${hidden}`;
|
||||||
|
if (shown.length === 0 || used + segmentWidth(tag) <= width()) {
|
||||||
|
shown.push({
|
||||||
|
value: undefined,
|
||||||
|
label: tag,
|
||||||
|
isActive: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return shown;
|
||||||
|
});
|
||||||
|
const accent = () => props.accentColor ?? theme2.primary;
|
||||||
|
const muted = () => props.mutedColor ?? theme2.muted;
|
||||||
|
const borderColor = () => props.focused ? accent() : muted();
|
||||||
|
const chars = BorderChars[theme2.borderStyle];
|
||||||
|
return (() => {
|
||||||
|
var _el$ = _$createElement("box"), _el$2 = _$createElement("text"), _el$3 = _$createElement("box"), _el$4 = _$createElement("text");
|
||||||
|
_$insertNode(_el$, _el$2);
|
||||||
|
_$insertNode(_el$, _el$3);
|
||||||
|
_$insertNode(_el$, _el$4);
|
||||||
|
_$use((el) => {
|
||||||
|
container = el;
|
||||||
|
remeasureNextFrame();
|
||||||
|
}, _el$);
|
||||||
|
_$setProp(_el$, "flexDirection", "column");
|
||||||
|
_$setProp(_el$, "height", 3);
|
||||||
|
_$setProp(_el$2, "selectable", false);
|
||||||
|
_$insert(_el$2, () => segments().map((s) => topEdge(chars, s.label)).join(""));
|
||||||
|
_$setProp(_el$3, "flexDirection", "row");
|
||||||
|
_$insert(_el$3, _$createComponent(Index, {
|
||||||
|
get each() {
|
||||||
|
return segments();
|
||||||
|
},
|
||||||
|
children: (s) => [(() => {
|
||||||
|
var _el$5 = _$createElement("text");
|
||||||
|
_$setProp(_el$5, "selectable", false);
|
||||||
|
_$insert(_el$5, () => chars.vertical);
|
||||||
|
_$effect((_$p) => _$setProp(_el$5, "fg", borderColor(), _$p));
|
||||||
|
return _el$5;
|
||||||
|
})(), (() => {
|
||||||
|
var _el$6 = _$createElement("text");
|
||||||
|
_$setProp(_el$6, "selectable", false);
|
||||||
|
_$insert(_el$6, () => ` ${s().label} `);
|
||||||
|
_$effect((_p$) => {
|
||||||
|
var _v$3 = s().isActive ? accent() : muted(), _v$4 = s().isActive ? TextAttributes.BOLD : undefined, _v$5 = theme2.mouse && s().value !== undefined ? () => props.onChange(s().value) : undefined;
|
||||||
|
_v$3 !== _p$.e && (_p$.e = _$setProp(_el$6, "fg", _v$3, _p$.e));
|
||||||
|
_v$4 !== _p$.t && (_p$.t = _$setProp(_el$6, "attributes", _v$4, _p$.t));
|
||||||
|
_v$5 !== _p$.a && (_p$.a = _$setProp(_el$6, "onMouseDown", _v$5, _p$.a));
|
||||||
|
return _p$;
|
||||||
|
}, {
|
||||||
|
e: undefined,
|
||||||
|
t: undefined,
|
||||||
|
a: undefined
|
||||||
|
});
|
||||||
|
return _el$6;
|
||||||
|
})(), (() => {
|
||||||
|
var _el$7 = _$createElement("text");
|
||||||
|
_$setProp(_el$7, "selectable", false);
|
||||||
|
_$insert(_el$7, () => chars.vertical);
|
||||||
|
_$effect((_$p) => _$setProp(_el$7, "fg", borderColor(), _$p));
|
||||||
|
return _el$7;
|
||||||
|
})()]
|
||||||
|
}));
|
||||||
|
_$setProp(_el$4, "selectable", false);
|
||||||
|
_$insert(_el$4, () => buildBottomRow(chars, segments(), width()));
|
||||||
|
_$effect((_p$) => {
|
||||||
|
var _v$ = borderColor(), _v$2 = borderColor();
|
||||||
|
_v$ !== _p$.e && (_p$.e = _$setProp(_el$2, "fg", _v$, _p$.e));
|
||||||
|
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$4, "fg", _v$2, _p$.t));
|
||||||
|
return _p$;
|
||||||
|
}, {
|
||||||
|
e: undefined,
|
||||||
|
t: undefined
|
||||||
|
});
|
||||||
|
return _el$;
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
export { stepTabValue2, Tabs2 };
|
||||||
Vendored
+52
@@ -0,0 +1,52 @@
|
|||||||
|
// @bun
|
||||||
|
import {
|
||||||
|
theme2
|
||||||
|
} from "./chunk-p6jvscj3.js";
|
||||||
|
import {
|
||||||
|
Box2
|
||||||
|
} from "./chunk-8r4rajaz.js";
|
||||||
|
import {
|
||||||
|
Tabs2
|
||||||
|
} from "./chunk-vf2ybhtx.js";
|
||||||
|
|
||||||
|
// src/components/TabbedPanel.tsx
|
||||||
|
import { insert as _$insert } from "@opentui/solid";
|
||||||
|
import { memo as _$memo } from "@opentui/solid";
|
||||||
|
import { createComponent as _$createComponent } from "@opentui/solid";
|
||||||
|
import { setProp as _$setProp } from "@opentui/solid";
|
||||||
|
import { createElement as _$createElement } from "@opentui/solid";
|
||||||
|
function TabbedPanel2(props) {
|
||||||
|
return (() => {
|
||||||
|
var _el$ = _$createElement("box");
|
||||||
|
_$setProp(_el$, "flexDirection", "column");
|
||||||
|
_$setProp(_el$, "flexGrow", 1);
|
||||||
|
_$insert(_el$, _$createComponent(Tabs2, {
|
||||||
|
get items() {
|
||||||
|
return props.items;
|
||||||
|
},
|
||||||
|
get value() {
|
||||||
|
return props.value;
|
||||||
|
},
|
||||||
|
get onChange() {
|
||||||
|
return props.onChange;
|
||||||
|
},
|
||||||
|
get focused() {
|
||||||
|
return props.focused;
|
||||||
|
}
|
||||||
|
}), null);
|
||||||
|
_$insert(_el$, _$createComponent(Box2, {
|
||||||
|
flexGrow: 1,
|
||||||
|
border: ["left", "right", "bottom"],
|
||||||
|
get borderColor() {
|
||||||
|
return _$memo(() => !!props.focused)() ? theme2.primary : theme2.muted;
|
||||||
|
},
|
||||||
|
flexDirection: "column",
|
||||||
|
get children() {
|
||||||
|
return props.children;
|
||||||
|
}
|
||||||
|
}), null);
|
||||||
|
return _el$;
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
export { TabbedPanel2 };
|
||||||
Vendored
+10
@@ -0,0 +1,10 @@
|
|||||||
|
// @bun
|
||||||
|
// src/yaml.ts
|
||||||
|
import { homedir } from "os";
|
||||||
|
import { join } from "path";
|
||||||
|
function configPath2() {
|
||||||
|
const configDir = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
||||||
|
return join(configDir, "ilovetui", "config.yaml");
|
||||||
|
}
|
||||||
|
|
||||||
|
export { configPath2 };
|
||||||
Vendored
+44
@@ -0,0 +1,44 @@
|
|||||||
|
// @bun
|
||||||
|
// src/presets.ts
|
||||||
|
function buildPresets2(theme) {
|
||||||
|
const select = {
|
||||||
|
backgroundColor: theme.background,
|
||||||
|
textColor: theme.text,
|
||||||
|
focusedBackgroundColor: theme.subtleBg,
|
||||||
|
focusedTextColor: theme.text,
|
||||||
|
selectedBackgroundColor: theme.selection,
|
||||||
|
selectedTextColor: theme.primary,
|
||||||
|
descriptionColor: theme.muted,
|
||||||
|
selectedDescriptionColor: theme.text
|
||||||
|
};
|
||||||
|
const tabSelect = {
|
||||||
|
backgroundColor: theme.background,
|
||||||
|
textColor: theme.text,
|
||||||
|
focusedBackgroundColor: theme.subtleBg,
|
||||||
|
focusedTextColor: theme.text,
|
||||||
|
selectedBackgroundColor: theme.selection,
|
||||||
|
selectedTextColor: theme.primary,
|
||||||
|
selectedDescriptionColor: theme.text
|
||||||
|
};
|
||||||
|
const input = {
|
||||||
|
backgroundColor: theme.background,
|
||||||
|
textColor: theme.text,
|
||||||
|
focusedBackgroundColor: theme.subtleBg,
|
||||||
|
focusedTextColor: theme.text,
|
||||||
|
placeholderColor: theme.muted
|
||||||
|
};
|
||||||
|
const box = {
|
||||||
|
border: true,
|
||||||
|
borderStyle: theme.borderStyle,
|
||||||
|
borderColor: theme.subtle,
|
||||||
|
focusedBorderColor: theme.primary,
|
||||||
|
backgroundColor: theme.background
|
||||||
|
};
|
||||||
|
const slider = {
|
||||||
|
backgroundColor: theme.subtleBg,
|
||||||
|
foregroundColor: theme.primary
|
||||||
|
};
|
||||||
|
return { select, tabSelect, input, textarea: input, box, slider };
|
||||||
|
}
|
||||||
|
|
||||||
|
export { buildPresets2 };
|
||||||
Vendored
+58
@@ -0,0 +1,58 @@
|
|||||||
|
// @bun
|
||||||
|
import {
|
||||||
|
theme2
|
||||||
|
} from "./chunk-p6jvscj3.js";
|
||||||
|
|
||||||
|
// src/components/ProgressBar.tsx
|
||||||
|
import { effect as _$effect } from "@opentui/solid";
|
||||||
|
import { memo as _$memo } from "@opentui/solid";
|
||||||
|
import { insert as _$insert } from "@opentui/solid";
|
||||||
|
import { createComponent as _$createComponent } from "@opentui/solid";
|
||||||
|
import { setProp as _$setProp } from "@opentui/solid";
|
||||||
|
import { createElement as _$createElement } from "@opentui/solid";
|
||||||
|
import { parseColor, RGBA } from "@opentui/core";
|
||||||
|
import { createMemo, For } from "solid-js";
|
||||||
|
function sampleGradient(stops, t) {
|
||||||
|
if (stops.length === 1)
|
||||||
|
return stops[0];
|
||||||
|
const clamped = Math.min(Math.max(t, 0), 1);
|
||||||
|
const segment = clamped * (stops.length - 1);
|
||||||
|
const i = Math.min(Math.floor(segment), stops.length - 2);
|
||||||
|
const localT = segment - i;
|
||||||
|
const [ar, ag, ab, aa] = parseColor(stops[i]).toInts();
|
||||||
|
const [br, bg, bb, ba] = parseColor(stops[i + 1]).toInts();
|
||||||
|
return RGBA.fromInts(Math.round(ar + (br - ar) * localT), Math.round(ag + (bg - ag) * localT), Math.round(ab + (bb - ab) * localT), Math.round(aa + (ba - aa) * localT));
|
||||||
|
}
|
||||||
|
function ProgressBar2(props) {
|
||||||
|
const width = () => props.width ?? 20;
|
||||||
|
const stops = createMemo(() => Array.isArray(props.color) ? props.color : [props.color ?? theme2.primary]);
|
||||||
|
const filled = createMemo(() => Math.round(width() * Math.min(Math.max(props.value, 0), 100) / 100));
|
||||||
|
const cells = createMemo(() => Array.from({
|
||||||
|
length: width()
|
||||||
|
}, (_, i) => i));
|
||||||
|
return (() => {
|
||||||
|
var _el$ = _$createElement("box");
|
||||||
|
_$setProp(_el$, "flexDirection", "row");
|
||||||
|
_$insert(_el$, _$createComponent(For, {
|
||||||
|
get each() {
|
||||||
|
return cells();
|
||||||
|
},
|
||||||
|
children: (i) => {
|
||||||
|
const isFilled = () => i < filled();
|
||||||
|
const t = width() > 1 ? i / (width() - 1) : 0;
|
||||||
|
return (() => {
|
||||||
|
var _el$2 = _$createElement("text");
|
||||||
|
_$insert(_el$2, (() => {
|
||||||
|
var _c$ = _$memo(() => !!isFilled());
|
||||||
|
return () => _c$() ? props.fillChar ?? "\u2588" : props.trackChar ?? "\u2591";
|
||||||
|
})());
|
||||||
|
_$effect((_$p) => _$setProp(_el$2, "fg", isFilled() ? sampleGradient(stops(), t) : props.trackColor ?? theme2.muted, _$p));
|
||||||
|
return _el$2;
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
return _el$;
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ProgressBar2 };
|
||||||
Vendored
+12
@@ -0,0 +1,12 @@
|
|||||||
|
// @bun
|
||||||
|
import {
|
||||||
|
Badge2
|
||||||
|
} from "../chunk-v9cetqzr.js";
|
||||||
|
import"../chunk-p6jvscj3.js";
|
||||||
|
import"../chunk-yyg967q8.js";
|
||||||
|
import"../chunk-njvj322f.js";
|
||||||
|
import"../chunk-6ggk5fg7.js";
|
||||||
|
import"../chunk-xykm10y9.js";
|
||||||
|
export {
|
||||||
|
Badge2 as Badge
|
||||||
|
};
|
||||||
Vendored
+12
@@ -0,0 +1,12 @@
|
|||||||
|
// @bun
|
||||||
|
import {
|
||||||
|
Box2
|
||||||
|
} from "../chunk-8r4rajaz.js";
|
||||||
|
import"../chunk-p6jvscj3.js";
|
||||||
|
import"../chunk-yyg967q8.js";
|
||||||
|
import"../chunk-njvj322f.js";
|
||||||
|
import"../chunk-6ggk5fg7.js";
|
||||||
|
import"../chunk-xykm10y9.js";
|
||||||
|
export {
|
||||||
|
Box2 as Box
|
||||||
|
};
|
||||||
Vendored
+12
@@ -0,0 +1,12 @@
|
|||||||
|
// @bun
|
||||||
|
import {
|
||||||
|
HelpBar2
|
||||||
|
} from "../chunk-c1dx8cn3.js";
|
||||||
|
import"../chunk-p6jvscj3.js";
|
||||||
|
import"../chunk-yyg967q8.js";
|
||||||
|
import"../chunk-njvj322f.js";
|
||||||
|
import"../chunk-6ggk5fg7.js";
|
||||||
|
import"../chunk-xykm10y9.js";
|
||||||
|
export {
|
||||||
|
HelpBar2 as HelpBar
|
||||||
|
};
|
||||||
Vendored
+14
@@ -0,0 +1,14 @@
|
|||||||
|
// @bun
|
||||||
|
import {
|
||||||
|
HelpModal2
|
||||||
|
} from "../chunk-ersd5rrs.js";
|
||||||
|
import"../chunk-t2cszvff.js";
|
||||||
|
import"../chunk-p6jvscj3.js";
|
||||||
|
import"../chunk-yyg967q8.js";
|
||||||
|
import"../chunk-njvj322f.js";
|
||||||
|
import"../chunk-6ggk5fg7.js";
|
||||||
|
import"../chunk-xykm10y9.js";
|
||||||
|
import"../chunk-ee2qaswx.js";
|
||||||
|
export {
|
||||||
|
HelpModal2 as HelpModal
|
||||||
|
};
|
||||||
Vendored
+12
@@ -0,0 +1,12 @@
|
|||||||
|
// @bun
|
||||||
|
import {
|
||||||
|
KeyValue2
|
||||||
|
} from "../chunk-c4gp0x16.js";
|
||||||
|
import"../chunk-p6jvscj3.js";
|
||||||
|
import"../chunk-yyg967q8.js";
|
||||||
|
import"../chunk-njvj322f.js";
|
||||||
|
import"../chunk-6ggk5fg7.js";
|
||||||
|
import"../chunk-xykm10y9.js";
|
||||||
|
export {
|
||||||
|
KeyValue2 as KeyValue
|
||||||
|
};
|
||||||
Vendored
+12
@@ -0,0 +1,12 @@
|
|||||||
|
// @bun
|
||||||
|
import {
|
||||||
|
ListRow2
|
||||||
|
} from "../chunk-sh9n4zha.js";
|
||||||
|
import"../chunk-p6jvscj3.js";
|
||||||
|
import"../chunk-yyg967q8.js";
|
||||||
|
import"../chunk-njvj322f.js";
|
||||||
|
import"../chunk-6ggk5fg7.js";
|
||||||
|
import"../chunk-xykm10y9.js";
|
||||||
|
export {
|
||||||
|
ListRow2 as ListRow
|
||||||
|
};
|
||||||
Vendored
+12
@@ -0,0 +1,12 @@
|
|||||||
|
// @bun
|
||||||
|
import {
|
||||||
|
MinSizeGuard2
|
||||||
|
} from "../chunk-qdyd8q9r.js";
|
||||||
|
import"../chunk-p6jvscj3.js";
|
||||||
|
import"../chunk-yyg967q8.js";
|
||||||
|
import"../chunk-njvj322f.js";
|
||||||
|
import"../chunk-6ggk5fg7.js";
|
||||||
|
import"../chunk-xykm10y9.js";
|
||||||
|
export {
|
||||||
|
MinSizeGuard2 as MinSizeGuard
|
||||||
|
};
|
||||||
Vendored
+12
@@ -0,0 +1,12 @@
|
|||||||
|
// @bun
|
||||||
|
import {
|
||||||
|
Modal2
|
||||||
|
} from "../chunk-t2cszvff.js";
|
||||||
|
import"../chunk-p6jvscj3.js";
|
||||||
|
import"../chunk-yyg967q8.js";
|
||||||
|
import"../chunk-njvj322f.js";
|
||||||
|
import"../chunk-6ggk5fg7.js";
|
||||||
|
import"../chunk-xykm10y9.js";
|
||||||
|
export {
|
||||||
|
Modal2 as Modal
|
||||||
|
};
|
||||||
Vendored
+12
@@ -0,0 +1,12 @@
|
|||||||
|
// @bun
|
||||||
|
import {
|
||||||
|
NotificationHost2
|
||||||
|
} from "../chunk-ajb193xj.js";
|
||||||
|
import"../chunk-p6jvscj3.js";
|
||||||
|
import"../chunk-yyg967q8.js";
|
||||||
|
import"../chunk-njvj322f.js";
|
||||||
|
import"../chunk-6ggk5fg7.js";
|
||||||
|
import"../chunk-xykm10y9.js";
|
||||||
|
export {
|
||||||
|
NotificationHost2 as NotificationHost
|
||||||
|
};
|
||||||
Vendored
+10
@@ -0,0 +1,10 @@
|
|||||||
|
// @bun
|
||||||
|
import {
|
||||||
|
Pane2,
|
||||||
|
usePane2
|
||||||
|
} from "../chunk-vazj633p.js";
|
||||||
|
import"../chunk-59dzyjgd.js";
|
||||||
|
export {
|
||||||
|
Pane2 as Pane,
|
||||||
|
usePane2 as usePane
|
||||||
|
};
|
||||||
Vendored
+12
@@ -0,0 +1,12 @@
|
|||||||
|
// @bun
|
||||||
|
import {
|
||||||
|
ProgressBar2
|
||||||
|
} from "../chunk-z734n0wr.js";
|
||||||
|
import"../chunk-p6jvscj3.js";
|
||||||
|
import"../chunk-yyg967q8.js";
|
||||||
|
import"../chunk-njvj322f.js";
|
||||||
|
import"../chunk-6ggk5fg7.js";
|
||||||
|
import"../chunk-xykm10y9.js";
|
||||||
|
export {
|
||||||
|
ProgressBar2 as ProgressBar
|
||||||
|
};
|
||||||
Vendored
+81
@@ -0,0 +1,81 @@
|
|||||||
|
// @bun
|
||||||
|
import"../chunk-njvj322f.js";
|
||||||
|
import"../chunk-6ggk5fg7.js";
|
||||||
|
import"../chunk-yyg967q8.js";
|
||||||
|
import"../chunk-xykm10y9.js";
|
||||||
|
import {
|
||||||
|
theme2
|
||||||
|
} from "../chunk-p6jvscj3.js";
|
||||||
|
import {
|
||||||
|
Spinner2
|
||||||
|
} from "../chunk-kmqqzz9e.js";
|
||||||
|
|
||||||
|
// src/components/SectionHeader.tsx
|
||||||
|
import { createTextNode as _$createTextNode } from "@opentui/solid";
|
||||||
|
import { createComponent as _$createComponent } from "@opentui/solid";
|
||||||
|
import { effect as _$effect } from "@opentui/solid";
|
||||||
|
import { insertNode as _$insertNode } from "@opentui/solid";
|
||||||
|
import { insert as _$insert } from "@opentui/solid";
|
||||||
|
import { memo as _$memo } from "@opentui/solid";
|
||||||
|
import { setProp as _$setProp } from "@opentui/solid";
|
||||||
|
import { createElement as _$createElement } from "@opentui/solid";
|
||||||
|
import { TextAttributes } from "@opentui/core";
|
||||||
|
import { Show } from "solid-js";
|
||||||
|
function SectionHeader(props) {
|
||||||
|
return (() => {
|
||||||
|
var _el$ = _$createElement("box"), _el$2 = _$createElement("box"), _el$3 = _$createElement("text"), _el$4 = _$createElement("box");
|
||||||
|
_$insertNode(_el$, _el$2);
|
||||||
|
_$insertNode(_el$, _el$4);
|
||||||
|
_$setProp(_el$, "flexShrink", 0);
|
||||||
|
_$setProp(_el$, "flexDirection", "row");
|
||||||
|
_$setProp(_el$, "justifyContent", "space-between");
|
||||||
|
_$setProp(_el$, "alignItems", "center");
|
||||||
|
_$setProp(_el$, "height", 1);
|
||||||
|
_$insertNode(_el$2, _el$3);
|
||||||
|
_$setProp(_el$2, "flexGrow", 1);
|
||||||
|
_$setProp(_el$2, "flexShrink", 1);
|
||||||
|
_$setProp(_el$2, "minWidth", 0);
|
||||||
|
_$setProp(_el$2, "marginRight", 1);
|
||||||
|
_$setProp(_el$2, "overflow", "hidden");
|
||||||
|
_$setProp(_el$3, "wrapMode", "none");
|
||||||
|
_$insert(_el$3, () => props.title ?? "");
|
||||||
|
_$setProp(_el$4, "flexShrink", 0);
|
||||||
|
_$insert(_el$4, _$createComponent(Show, {
|
||||||
|
get when() {
|
||||||
|
return !props.loading;
|
||||||
|
},
|
||||||
|
get fallback() {
|
||||||
|
return (() => {
|
||||||
|
var _el$6 = _$createElement("box"), _el$7 = _$createElement("text"), _el$8 = _$createTextNode(` `);
|
||||||
|
_$insertNode(_el$6, _el$7);
|
||||||
|
_$setProp(_el$6, "flexDirection", "row");
|
||||||
|
_$setProp(_el$6, "alignItems", "center");
|
||||||
|
_$insert(_el$6, _$createComponent(Spinner2, {}), _el$7);
|
||||||
|
_$insertNode(_el$7, _el$8);
|
||||||
|
_$insert(_el$7, () => props.loadingLabel ?? "Loading\u2026", null);
|
||||||
|
_$effect((_$p) => _$setProp(_el$7, "fg", theme2.muted, _$p));
|
||||||
|
return _el$6;
|
||||||
|
})();
|
||||||
|
},
|
||||||
|
get children() {
|
||||||
|
var _el$5 = _$createElement("text");
|
||||||
|
_$insert(_el$5, () => props.hint ?? "");
|
||||||
|
_$effect((_$p) => _$setProp(_el$5, "fg", theme2.muted, _$p));
|
||||||
|
return _el$5;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
_$effect((_p$) => {
|
||||||
|
var _v$ = theme2.muted, _v$2 = TextAttributes.BOLD;
|
||||||
|
_v$ !== _p$.e && (_p$.e = _$setProp(_el$3, "fg", _v$, _p$.e));
|
||||||
|
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$3, "attributes", _v$2, _p$.t));
|
||||||
|
return _p$;
|
||||||
|
}, {
|
||||||
|
e: undefined,
|
||||||
|
t: undefined
|
||||||
|
});
|
||||||
|
return _el$;
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
export {
|
||||||
|
SectionHeader
|
||||||
|
};
|
||||||
Vendored
+12
@@ -0,0 +1,12 @@
|
|||||||
|
// @bun
|
||||||
|
import {
|
||||||
|
Sidebar2
|
||||||
|
} from "../chunk-rcywynbw.js";
|
||||||
|
import"../chunk-p6jvscj3.js";
|
||||||
|
import"../chunk-yyg967q8.js";
|
||||||
|
import"../chunk-njvj322f.js";
|
||||||
|
import"../chunk-6ggk5fg7.js";
|
||||||
|
import"../chunk-xykm10y9.js";
|
||||||
|
export {
|
||||||
|
Sidebar2 as Sidebar
|
||||||
|
};
|
||||||
Vendored
+12
@@ -0,0 +1,12 @@
|
|||||||
|
// @bun
|
||||||
|
import {
|
||||||
|
SignalBars2
|
||||||
|
} from "../chunk-tz3vvffs.js";
|
||||||
|
import"../chunk-p6jvscj3.js";
|
||||||
|
import"../chunk-yyg967q8.js";
|
||||||
|
import"../chunk-njvj322f.js";
|
||||||
|
import"../chunk-6ggk5fg7.js";
|
||||||
|
import"../chunk-xykm10y9.js";
|
||||||
|
export {
|
||||||
|
SignalBars2 as SignalBars
|
||||||
|
};
|
||||||
Vendored
+12
@@ -0,0 +1,12 @@
|
|||||||
|
// @bun
|
||||||
|
import {
|
||||||
|
Spinner2
|
||||||
|
} from "../chunk-kmqqzz9e.js";
|
||||||
|
import"../chunk-p6jvscj3.js";
|
||||||
|
import"../chunk-yyg967q8.js";
|
||||||
|
import"../chunk-njvj322f.js";
|
||||||
|
import"../chunk-6ggk5fg7.js";
|
||||||
|
import"../chunk-xykm10y9.js";
|
||||||
|
export {
|
||||||
|
Spinner2 as Spinner
|
||||||
|
};
|
||||||
Vendored
+14
@@ -0,0 +1,14 @@
|
|||||||
|
// @bun
|
||||||
|
import {
|
||||||
|
TabbedPanel2
|
||||||
|
} from "../chunk-wtbg0e39.js";
|
||||||
|
import"../chunk-vf2ybhtx.js";
|
||||||
|
import"../chunk-8r4rajaz.js";
|
||||||
|
import"../chunk-p6jvscj3.js";
|
||||||
|
import"../chunk-yyg967q8.js";
|
||||||
|
import"../chunk-njvj322f.js";
|
||||||
|
import"../chunk-6ggk5fg7.js";
|
||||||
|
import"../chunk-xykm10y9.js";
|
||||||
|
export {
|
||||||
|
TabbedPanel2 as TabbedPanel
|
||||||
|
};
|
||||||
Vendored
+14
@@ -0,0 +1,14 @@
|
|||||||
|
// @bun
|
||||||
|
import {
|
||||||
|
stepTabValue2,
|
||||||
|
Tabs2
|
||||||
|
} from "../chunk-vf2ybhtx.js";
|
||||||
|
import"../chunk-p6jvscj3.js";
|
||||||
|
import"../chunk-yyg967q8.js";
|
||||||
|
import"../chunk-njvj322f.js";
|
||||||
|
import"../chunk-6ggk5fg7.js";
|
||||||
|
import"../chunk-xykm10y9.js";
|
||||||
|
export {
|
||||||
|
Tabs2 as Tabs,
|
||||||
|
stepTabValue2 as stepTabValue
|
||||||
|
};
|
||||||
Vendored
+13
@@ -0,0 +1,13 @@
|
|||||||
|
// @bun
|
||||||
|
import {
|
||||||
|
Toggle2
|
||||||
|
} from "../chunk-qhp6k176.js";
|
||||||
|
import"../chunk-v9cetqzr.js";
|
||||||
|
import"../chunk-p6jvscj3.js";
|
||||||
|
import"../chunk-yyg967q8.js";
|
||||||
|
import"../chunk-njvj322f.js";
|
||||||
|
import"../chunk-6ggk5fg7.js";
|
||||||
|
import"../chunk-xykm10y9.js";
|
||||||
|
export {
|
||||||
|
Toggle2 as Toggle
|
||||||
|
};
|
||||||
Vendored
+18
@@ -0,0 +1,18 @@
|
|||||||
|
// @bun
|
||||||
|
import"../chunk-njvj322f.js";
|
||||||
|
import"../chunk-6ggk5fg7.js";
|
||||||
|
import"../chunk-yyg967q8.js";
|
||||||
|
import"../chunk-xykm10y9.js";
|
||||||
|
import"../chunk-p6jvscj3.js";
|
||||||
|
import {
|
||||||
|
HelpBar2
|
||||||
|
} from "../chunk-c1dx8cn3.js";
|
||||||
|
import"../chunk-ee2qaswx.js";
|
||||||
|
import"../chunk-t2cszvff.js";
|
||||||
|
import {
|
||||||
|
HelpModal2
|
||||||
|
} from "../chunk-ersd5rrs.js";
|
||||||
|
export {
|
||||||
|
HelpBar2 as HelpBar,
|
||||||
|
HelpModal2 as HelpModal
|
||||||
|
};
|
||||||
Vendored
+69
@@ -0,0 +1,69 @@
|
|||||||
|
// @bun
|
||||||
|
import"../chunk-njvj322f.js";
|
||||||
|
import"../chunk-6ggk5fg7.js";
|
||||||
|
import"../chunk-yyg967q8.js";
|
||||||
|
import"../chunk-xykm10y9.js";
|
||||||
|
import"../chunk-p6jvscj3.js";
|
||||||
|
import {
|
||||||
|
Badge2
|
||||||
|
} from "../chunk-v9cetqzr.js";
|
||||||
|
import {
|
||||||
|
Box2
|
||||||
|
} from "../chunk-8r4rajaz.js";
|
||||||
|
import {
|
||||||
|
KeyValue2
|
||||||
|
} from "../chunk-c4gp0x16.js";
|
||||||
|
import {
|
||||||
|
ListRow2
|
||||||
|
} from "../chunk-sh9n4zha.js";
|
||||||
|
import {
|
||||||
|
MinSizeGuard2
|
||||||
|
} from "../chunk-qdyd8q9r.js";
|
||||||
|
import {
|
||||||
|
Modal2
|
||||||
|
} from "../chunk-t2cszvff.js";
|
||||||
|
import {
|
||||||
|
NotificationHost2
|
||||||
|
} from "../chunk-ajb193xj.js";
|
||||||
|
import"../chunk-59dzyjgd.js";
|
||||||
|
import {
|
||||||
|
Pane2,
|
||||||
|
usePane2
|
||||||
|
} from "../chunk-vazj633p.js";
|
||||||
|
import {
|
||||||
|
ProgressBar2
|
||||||
|
} from "../chunk-z734n0wr.js";
|
||||||
|
import {
|
||||||
|
Sidebar2
|
||||||
|
} from "../chunk-rcywynbw.js";
|
||||||
|
import {
|
||||||
|
SignalBars2
|
||||||
|
} from "../chunk-tz3vvffs.js";
|
||||||
|
import {
|
||||||
|
stepTabValue2,
|
||||||
|
Tabs2
|
||||||
|
} from "../chunk-vf2ybhtx.js";
|
||||||
|
import {
|
||||||
|
TabbedPanel2
|
||||||
|
} from "../chunk-wtbg0e39.js";
|
||||||
|
import {
|
||||||
|
Toggle2
|
||||||
|
} from "../chunk-qhp6k176.js";
|
||||||
|
export {
|
||||||
|
Badge2 as Badge,
|
||||||
|
Box2 as Box,
|
||||||
|
KeyValue2 as KeyValue,
|
||||||
|
ListRow2 as ListRow,
|
||||||
|
MinSizeGuard2 as MinSizeGuard,
|
||||||
|
Modal2 as Modal,
|
||||||
|
NotificationHost2 as NotificationHost,
|
||||||
|
Pane2 as Pane,
|
||||||
|
ProgressBar2 as ProgressBar,
|
||||||
|
Sidebar2 as Sidebar,
|
||||||
|
SignalBars2 as SignalBars,
|
||||||
|
TabbedPanel2 as TabbedPanel,
|
||||||
|
Tabs2 as Tabs,
|
||||||
|
Toggle2 as Toggle,
|
||||||
|
stepTabValue2 as stepTabValue,
|
||||||
|
usePane2 as usePane
|
||||||
|
};
|
||||||
Vendored
+17
@@ -0,0 +1,17 @@
|
|||||||
|
// @bun
|
||||||
|
import {
|
||||||
|
COLOR_KEYS2,
|
||||||
|
OPENTUI_BORDER_STYLES2,
|
||||||
|
ThemeConfigSchema2,
|
||||||
|
layerConfigs2,
|
||||||
|
resolveConfig2,
|
||||||
|
normalizeColor2
|
||||||
|
} from "./chunk-njvj322f.js";
|
||||||
|
export {
|
||||||
|
COLOR_KEYS2 as COLOR_KEYS,
|
||||||
|
OPENTUI_BORDER_STYLES2 as OPENTUI_BORDER_STYLES,
|
||||||
|
ThemeConfigSchema2 as ThemeConfigSchema,
|
||||||
|
layerConfigs2 as layerConfigs,
|
||||||
|
normalizeColor2 as normalizeColor,
|
||||||
|
resolveConfig2 as resolveConfig
|
||||||
|
};
|
||||||
Vendored
+9
@@ -0,0 +1,9 @@
|
|||||||
|
// @bun
|
||||||
|
import {
|
||||||
|
isInside2,
|
||||||
|
useFocusWithin2
|
||||||
|
} from "../chunk-59dzyjgd.js";
|
||||||
|
export {
|
||||||
|
isInside2 as isInside,
|
||||||
|
useFocusWithin2 as useFocusWithin
|
||||||
|
};
|
||||||
Vendored
+9
@@ -0,0 +1,9 @@
|
|||||||
|
// @bun
|
||||||
|
import {
|
||||||
|
helpOpen2,
|
||||||
|
toggleHelp2
|
||||||
|
} from "../chunk-ee2qaswx.js";
|
||||||
|
export {
|
||||||
|
helpOpen2 as helpOpen,
|
||||||
|
toggleHelp2 as toggleHelp
|
||||||
|
};
|
||||||
Vendored
+7
@@ -0,0 +1,7 @@
|
|||||||
|
// @bun
|
||||||
|
import {
|
||||||
|
createSimpleContext2
|
||||||
|
} from "../chunk-jza406qg.js";
|
||||||
|
export {
|
||||||
|
createSimpleContext2 as createSimpleContext
|
||||||
|
};
|
||||||
Vendored
+27
@@ -0,0 +1,27 @@
|
|||||||
|
// @bun
|
||||||
|
import {
|
||||||
|
createSimpleContext2
|
||||||
|
} from "../chunk-jza406qg.js";
|
||||||
|
import {
|
||||||
|
notifications2,
|
||||||
|
notify2,
|
||||||
|
dismiss2
|
||||||
|
} from "../chunk-6ggk5fg7.js";
|
||||||
|
import {
|
||||||
|
helpOpen2,
|
||||||
|
toggleHelp2
|
||||||
|
} from "../chunk-ee2qaswx.js";
|
||||||
|
import {
|
||||||
|
isInside2,
|
||||||
|
useFocusWithin2
|
||||||
|
} from "../chunk-59dzyjgd.js";
|
||||||
|
export {
|
||||||
|
createSimpleContext2 as createSimpleContext,
|
||||||
|
dismiss2 as dismiss,
|
||||||
|
helpOpen2 as helpOpen,
|
||||||
|
isInside2 as isInside,
|
||||||
|
notifications2 as notifications,
|
||||||
|
notify2 as notify,
|
||||||
|
toggleHelp2 as toggleHelp,
|
||||||
|
useFocusWithin2 as useFocusWithin
|
||||||
|
};
|
||||||
Vendored
+11
@@ -0,0 +1,11 @@
|
|||||||
|
// @bun
|
||||||
|
import {
|
||||||
|
notifications2,
|
||||||
|
notify2,
|
||||||
|
dismiss2
|
||||||
|
} from "../chunk-6ggk5fg7.js";
|
||||||
|
export {
|
||||||
|
dismiss2 as dismiss,
|
||||||
|
notifications2 as notifications,
|
||||||
|
notify2 as notify
|
||||||
|
};
|
||||||
Vendored
+17
-1
@@ -1,5 +1,21 @@
|
|||||||
# ilovetui default theme
|
# ilovetui default config
|
||||||
# Copy to ~/.config/ilovetui/config.yaml and edit to customize.
|
# Copy to ~/.config/ilovetui/config.yaml and edit to customize.
|
||||||
|
|
||||||
|
# Whether components may use Nerd Font glyphs (requires a patched font).
|
||||||
|
nerd_fonts: true
|
||||||
|
|
||||||
|
# Whether the terminal font can render emoji glyphs.
|
||||||
|
emoji_fonts: true
|
||||||
|
|
||||||
|
# Disable spinner animation and other motion across all ilovetui-based TUIs.
|
||||||
|
reduced_motion: false
|
||||||
|
|
||||||
|
# Whether components may register mouse handlers (e.g. click-to-dismiss).
|
||||||
|
mouse: true
|
||||||
|
|
||||||
|
# Border style used by panels across all ilovetui-based TUIs.
|
||||||
|
border: rounded
|
||||||
|
|
||||||
colors:
|
colors:
|
||||||
base00: "#110F12" # Background
|
base00: "#110F12" # Background
|
||||||
base01: "#1C1920" # Lighter Background / Status Bars
|
base01: "#1C1920" # Lighter Background / Status Bars
|
||||||
Vendored
+23
@@ -0,0 +1,23 @@
|
|||||||
|
// @bun
|
||||||
|
import {
|
||||||
|
theme2,
|
||||||
|
presets2,
|
||||||
|
configureTheme2,
|
||||||
|
icon2
|
||||||
|
} from "./chunk-p6jvscj3.js";
|
||||||
|
import"./chunk-yyg967q8.js";
|
||||||
|
import {
|
||||||
|
ThemeConfigSchema2
|
||||||
|
} from "./chunk-njvj322f.js";
|
||||||
|
import"./chunk-6ggk5fg7.js";
|
||||||
|
import {
|
||||||
|
configPath2
|
||||||
|
} from "./chunk-xykm10y9.js";
|
||||||
|
export {
|
||||||
|
ThemeConfigSchema2 as ThemeConfigSchema,
|
||||||
|
configPath2 as configPath,
|
||||||
|
configureTheme2 as configureTheme,
|
||||||
|
icon2 as icon,
|
||||||
|
presets2 as presets,
|
||||||
|
theme2 as theme
|
||||||
|
};
|
||||||
Vendored
+27
@@ -0,0 +1,27 @@
|
|||||||
|
// @bun
|
||||||
|
// src/keymap/provider.tsx
|
||||||
|
import { createComponent as _$createComponent } from "@opentui/solid";
|
||||||
|
import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui";
|
||||||
|
import { registerCommaBindings } from "@opentui/keymap/addons";
|
||||||
|
import { KeymapProvider as BaseKeymapProvider } from "@opentui/keymap/solid";
|
||||||
|
import { useRenderer } from "@opentui/solid";
|
||||||
|
import { onCleanup } from "solid-js";
|
||||||
|
import { useKeymap, useBindings, useKeymapSelector, reactiveMatcherFromSignal } from "@opentui/keymap/solid";
|
||||||
|
function KeymapProvider(props) {
|
||||||
|
const renderer = useRenderer();
|
||||||
|
const keymap = createDefaultOpenTuiKeymap(renderer);
|
||||||
|
onCleanup(registerCommaBindings(keymap));
|
||||||
|
return _$createComponent(BaseKeymapProvider, {
|
||||||
|
keymap,
|
||||||
|
get children() {
|
||||||
|
return props.children;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export {
|
||||||
|
KeymapProvider,
|
||||||
|
reactiveMatcherFromSignal,
|
||||||
|
useBindings,
|
||||||
|
useKeymap,
|
||||||
|
useKeymapSelector
|
||||||
|
};
|
||||||
Vendored
+7
@@ -0,0 +1,7 @@
|
|||||||
|
// @bun
|
||||||
|
import {
|
||||||
|
buildPresets2
|
||||||
|
} from "./chunk-yyg967q8.js";
|
||||||
|
export {
|
||||||
|
buildPresets2 as buildPresets
|
||||||
|
};
|
||||||
Vendored
+117
@@ -0,0 +1,117 @@
|
|||||||
|
// @bun
|
||||||
|
import {
|
||||||
|
notify2
|
||||||
|
} from "./chunk-6ggk5fg7.js";
|
||||||
|
|
||||||
|
// src/project-config.ts
|
||||||
|
import { existsSync, readFileSync } from "fs";
|
||||||
|
import { homedir } from "os";
|
||||||
|
import { join } from "path";
|
||||||
|
import { parse as parseYAML } from "yaml";
|
||||||
|
import { z } from "zod";
|
||||||
|
function userConfigPath(projectName) {
|
||||||
|
const configHome = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
||||||
|
return join(configHome, projectName, "config.yaml");
|
||||||
|
}
|
||||||
|
function readYaml(path) {
|
||||||
|
return parseYAML(readFileSync(path, "utf-8"));
|
||||||
|
}
|
||||||
|
function warn(projectName, path, message) {
|
||||||
|
const text = `${path}: ${message}`;
|
||||||
|
console.error(`[${projectName} config] ${text}`);
|
||||||
|
notify2(text, { kind: "warning", duration: 0 });
|
||||||
|
}
|
||||||
|
function loadProjectConfig(options) {
|
||||||
|
const { projectName, defaultConfigPath, defaultSchema, userSchema } = options;
|
||||||
|
let rawDefaults;
|
||||||
|
try {
|
||||||
|
rawDefaults = readYaml(defaultConfigPath);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to read ${defaultConfigPath}: ${error instanceof Error ? error.message : error}`);
|
||||||
|
}
|
||||||
|
const defaultsResult = defaultSchema.safeParse(rawDefaults);
|
||||||
|
if (!defaultsResult.success) {
|
||||||
|
throw new Error(`${defaultConfigPath} is invalid:
|
||||||
|
${z.prettifyError(defaultsResult.error)}`);
|
||||||
|
}
|
||||||
|
const path = userConfigPath(projectName);
|
||||||
|
const user = (() => {
|
||||||
|
if (!existsSync(path))
|
||||||
|
return {};
|
||||||
|
let raw;
|
||||||
|
try {
|
||||||
|
raw = readYaml(path);
|
||||||
|
} catch (error) {
|
||||||
|
warn(projectName, path, error instanceof Error ? error.message : String(error));
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const result = userSchema.safeParse(raw ?? {});
|
||||||
|
if (!result.success) {
|
||||||
|
warn(projectName, path, z.prettifyError(result.error));
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return result.data;
|
||||||
|
})();
|
||||||
|
return { defaults: defaultsResult.data, user };
|
||||||
|
}
|
||||||
|
function isPlainObject(value) {
|
||||||
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
function deepMerge(base, over) {
|
||||||
|
if (!isPlainObject(base) || !isPlainObject(over))
|
||||||
|
return over === undefined ? base : over;
|
||||||
|
const out = { ...base };
|
||||||
|
for (const [key, value] of Object.entries(over))
|
||||||
|
out[key] = deepMerge(base[key], value);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
function loadMergedProjectConfig(options) {
|
||||||
|
const { projectName, defaultConfigPath, schema } = options;
|
||||||
|
let rawDefaults;
|
||||||
|
try {
|
||||||
|
rawDefaults = readYaml(defaultConfigPath);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to read ${defaultConfigPath}: ${error instanceof Error ? error.message : error}`);
|
||||||
|
}
|
||||||
|
const defaults = schema.safeParse(rawDefaults);
|
||||||
|
if (!defaults.success) {
|
||||||
|
throw new Error(`${defaultConfigPath} is invalid:
|
||||||
|
${z.prettifyError(defaults.error)}`);
|
||||||
|
}
|
||||||
|
const path = userConfigPath(projectName);
|
||||||
|
if (!existsSync(path))
|
||||||
|
return defaults.data;
|
||||||
|
let rawUser;
|
||||||
|
try {
|
||||||
|
rawUser = readYaml(path);
|
||||||
|
} catch (error) {
|
||||||
|
warn(projectName, path, error instanceof Error ? error.message : String(error));
|
||||||
|
return defaults.data;
|
||||||
|
}
|
||||||
|
const merged = schema.safeParse(deepMerge(rawDefaults, rawUser ?? {}));
|
||||||
|
if (!merged.success) {
|
||||||
|
warn(projectName, path, z.prettifyError(merged.error));
|
||||||
|
return defaults.data;
|
||||||
|
}
|
||||||
|
return merged.data;
|
||||||
|
}
|
||||||
|
function keybindsSchema(names, required) {
|
||||||
|
const shape = {};
|
||||||
|
for (const name of names) {
|
||||||
|
shape[name] = required ? z.string().min(1) : z.string().min(1).optional();
|
||||||
|
}
|
||||||
|
return z.object(shape).strict();
|
||||||
|
}
|
||||||
|
function mergeKeybinds(names, defaults, user) {
|
||||||
|
const merged = {};
|
||||||
|
for (const name of names) {
|
||||||
|
merged[name] = user?.[name] ?? defaults[name];
|
||||||
|
}
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
export {
|
||||||
|
keybindsSchema,
|
||||||
|
loadMergedProjectConfig,
|
||||||
|
loadProjectConfig,
|
||||||
|
mergeKeybinds
|
||||||
|
};
|
||||||
Vendored
+48
@@ -0,0 +1,48 @@
|
|||||||
|
// @bun
|
||||||
|
import"./chunk-njvj322f.js";
|
||||||
|
import"./chunk-6ggk5fg7.js";
|
||||||
|
import"./chunk-yyg967q8.js";
|
||||||
|
import"./chunk-xykm10y9.js";
|
||||||
|
import {
|
||||||
|
theme2,
|
||||||
|
presets2
|
||||||
|
} from "./chunk-p6jvscj3.js";
|
||||||
|
|
||||||
|
// src/solid.ts
|
||||||
|
import { extend } from "@opentui/solid";
|
||||||
|
import {
|
||||||
|
ASCIIFontRenderable,
|
||||||
|
BoxRenderable,
|
||||||
|
InputRenderable,
|
||||||
|
SelectRenderable,
|
||||||
|
SliderRenderable,
|
||||||
|
TabSelectRenderable,
|
||||||
|
TextareaRenderable,
|
||||||
|
TextRenderable
|
||||||
|
} from "@opentui/core";
|
||||||
|
function withThemeDefaults(Ctor, defaults, after) {
|
||||||
|
class Themed extends Ctor {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
Object.assign(this, defaults());
|
||||||
|
after?.(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Themed;
|
||||||
|
}
|
||||||
|
var boxDefaults = () => {
|
||||||
|
const { borderStyle, borderColor, focusedBorderColor } = presets2.box;
|
||||||
|
return { borderStyle, borderColor, focusedBorderColor };
|
||||||
|
};
|
||||||
|
extend({
|
||||||
|
box: withThemeDefaults(BoxRenderable, boxDefaults, (instance) => {
|
||||||
|
instance.border = false;
|
||||||
|
}),
|
||||||
|
select: withThemeDefaults(SelectRenderable, () => presets2.select),
|
||||||
|
tab_select: withThemeDefaults(TabSelectRenderable, () => presets2.tabSelect),
|
||||||
|
input: withThemeDefaults(InputRenderable, () => presets2.input),
|
||||||
|
textarea: withThemeDefaults(TextareaRenderable, () => presets2.textarea),
|
||||||
|
slider: withThemeDefaults(SliderRenderable, () => presets2.slider),
|
||||||
|
text: withThemeDefaults(TextRenderable, () => ({ fg: theme2.text })),
|
||||||
|
ascii_font: withThemeDefaults(ASCIIFontRenderable, () => ({ color: theme2.primary }))
|
||||||
|
});
|
||||||
Vendored
+7
@@ -0,0 +1,7 @@
|
|||||||
|
// @bun
|
||||||
|
import {
|
||||||
|
configPath2
|
||||||
|
} from "./chunk-xykm10y9.js";
|
||||||
|
export {
|
||||||
|
configPath2 as configPath
|
||||||
|
};
|
||||||
Generated
-87
@@ -1,87 +0,0 @@
|
|||||||
{
|
|
||||||
"nodes": {
|
|
||||||
"flake-compat": {
|
|
||||||
"flake": false,
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1767039857,
|
|
||||||
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "flake-compat",
|
|
||||||
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "flake-compat",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"git-hooks": {
|
|
||||||
"inputs": {
|
|
||||||
"flake-compat": "flake-compat",
|
|
||||||
"gitignore": "gitignore",
|
|
||||||
"nixpkgs": [
|
|
||||||
"nixpkgs"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1778507602,
|
|
||||||
"narHash": "sha256-kTwur1wV+01SdqskVMSo6JMEpg71ps3HpbFY2GsflKs=",
|
|
||||||
"owner": "cachix",
|
|
||||||
"repo": "git-hooks.nix",
|
|
||||||
"rev": "61ab0e80d9c7ab14c256b5b453d8b3fb0189ba0a",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "cachix",
|
|
||||||
"repo": "git-hooks.nix",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"gitignore": {
|
|
||||||
"inputs": {
|
|
||||||
"nixpkgs": [
|
|
||||||
"git-hooks",
|
|
||||||
"nixpkgs"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1709087332,
|
|
||||||
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
|
|
||||||
"owner": "hercules-ci",
|
|
||||||
"repo": "gitignore.nix",
|
|
||||||
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "hercules-ci",
|
|
||||||
"repo": "gitignore.nix",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1779560665,
|
|
||||||
"narHash": "sha256-tpyBcxPpcQb8ukyNF7DoCwfSY3VPsxHoYwj00Cayv5o=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "64c08a7ca051951c8eae34e3e3cb1e202fe36786",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "NixOS",
|
|
||||||
"ref": "nixos-unstable",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": {
|
|
||||||
"inputs": {
|
|
||||||
"git-hooks": "git-hooks",
|
|
||||||
"nixpkgs": "nixpkgs"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": "root",
|
|
||||||
"version": 7
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
description = "";
|
|
||||||
|
|
||||||
inputs = {
|
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
|
||||||
git-hooks = {
|
|
||||||
url = "github:cachix/git-hooks.nix";
|
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
outputs = {
|
|
||||||
self,
|
|
||||||
nixpkgs,
|
|
||||||
git-hooks,
|
|
||||||
}: let
|
|
||||||
supportedSystems = ["x86_64-linux" "aarch64-linux"];
|
|
||||||
|
|
||||||
forAllSystems = f:
|
|
||||||
nixpkgs.lib.genAttrs supportedSystems
|
|
||||||
(system: f system (import nixpkgs {inherit system;}));
|
|
||||||
in {
|
|
||||||
devShells = forAllSystems (system: pkgs: {
|
|
||||||
default = import ./nix/shell.nix {
|
|
||||||
inherit pkgs;
|
|
||||||
gitHooksLib = git-hooks.lib.${system};
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
-242
@@ -1,242 +0,0 @@
|
|||||||
package ilovetui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"image/color"
|
|
||||||
|
|
||||||
"charm.land/glamour/v2/ansi"
|
|
||||||
)
|
|
||||||
|
|
||||||
func hexColor(c color.Color) *string {
|
|
||||||
r, g, b, _ := c.RGBA()
|
|
||||||
s := fmt.Sprintf("#%02X%02X%02X", r>>8, g>>8, b>>8)
|
|
||||||
return &s
|
|
||||||
}
|
|
||||||
|
|
||||||
// GlamourStyleConfig returns a glamour ansi.StyleConfig using the active theme.
|
|
||||||
func GlamourStyleConfig() ansi.StyleConfig {
|
|
||||||
str := func(s string) *string { return &s }
|
|
||||||
boolPtr := func(b bool) *bool { return &b }
|
|
||||||
uintPtr := func(u uint) *uint { return &u }
|
|
||||||
hex := hexColor
|
|
||||||
|
|
||||||
return ansi.StyleConfig{
|
|
||||||
Document: ansi.StyleBlock{
|
|
||||||
StylePrimitive: ansi.StylePrimitive{
|
|
||||||
BlockPrefix: "\n",
|
|
||||||
BlockSuffix: "\n",
|
|
||||||
Color: hex(S.Base05),
|
|
||||||
},
|
|
||||||
Margin: uintPtr(2),
|
|
||||||
},
|
|
||||||
BlockQuote: ansi.StyleBlock{
|
|
||||||
StylePrimitive: ansi.StylePrimitive{
|
|
||||||
Color: hex(S.Base03),
|
|
||||||
Italic: boolPtr(true),
|
|
||||||
},
|
|
||||||
Indent: uintPtr(1),
|
|
||||||
IndentToken: str("│ "),
|
|
||||||
},
|
|
||||||
List: ansi.StyleList{
|
|
||||||
LevelIndent: 2,
|
|
||||||
},
|
|
||||||
Heading: ansi.StyleBlock{
|
|
||||||
StylePrimitive: ansi.StylePrimitive{
|
|
||||||
BlockSuffix: "\n",
|
|
||||||
Color: hex(S.Base0D),
|
|
||||||
Bold: boolPtr(true),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
H1: ansi.StyleBlock{
|
|
||||||
StylePrimitive: ansi.StylePrimitive{
|
|
||||||
Prefix: " ",
|
|
||||||
Suffix: " ",
|
|
||||||
Color: hex(S.Base07),
|
|
||||||
BackgroundColor: hex(S.Base0D),
|
|
||||||
Bold: boolPtr(true),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
H2: ansi.StyleBlock{
|
|
||||||
StylePrimitive: ansi.StylePrimitive{
|
|
||||||
Prefix: "## ",
|
|
||||||
Color: hex(S.Base0D),
|
|
||||||
Bold: boolPtr(true),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
H3: ansi.StyleBlock{
|
|
||||||
StylePrimitive: ansi.StylePrimitive{
|
|
||||||
Prefix: "### ",
|
|
||||||
Color: hex(S.Base0C),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
H4: ansi.StyleBlock{
|
|
||||||
StylePrimitive: ansi.StylePrimitive{
|
|
||||||
Prefix: "#### ",
|
|
||||||
Color: hex(S.Base0B),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
H5: ansi.StyleBlock{
|
|
||||||
StylePrimitive: ansi.StylePrimitive{
|
|
||||||
Prefix: "##### ",
|
|
||||||
Color: hex(S.Base09),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
H6: ansi.StyleBlock{
|
|
||||||
StylePrimitive: ansi.StylePrimitive{
|
|
||||||
Prefix: "###### ",
|
|
||||||
Color: hex(S.Base08),
|
|
||||||
Bold: boolPtr(false),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Strikethrough: ansi.StylePrimitive{
|
|
||||||
CrossedOut: boolPtr(true),
|
|
||||||
},
|
|
||||||
Emph: ansi.StylePrimitive{
|
|
||||||
Italic: boolPtr(true),
|
|
||||||
},
|
|
||||||
Strong: ansi.StylePrimitive{
|
|
||||||
Bold: boolPtr(true),
|
|
||||||
},
|
|
||||||
HorizontalRule: ansi.StylePrimitive{
|
|
||||||
Color: hex(S.Base03),
|
|
||||||
Format: "\n--------\n",
|
|
||||||
},
|
|
||||||
Item: ansi.StylePrimitive{
|
|
||||||
BlockPrefix: "• ",
|
|
||||||
},
|
|
||||||
Enumeration: ansi.StylePrimitive{
|
|
||||||
BlockPrefix: ". ",
|
|
||||||
},
|
|
||||||
Task: ansi.StyleTask{
|
|
||||||
Ticked: "[✓] ",
|
|
||||||
Unticked: "[ ] ",
|
|
||||||
},
|
|
||||||
Link: ansi.StylePrimitive{
|
|
||||||
Color: hex(S.Base0C),
|
|
||||||
Underline: boolPtr(true),
|
|
||||||
},
|
|
||||||
LinkText: ansi.StylePrimitive{
|
|
||||||
Color: hex(S.Base0D),
|
|
||||||
Bold: boolPtr(true),
|
|
||||||
},
|
|
||||||
Image: ansi.StylePrimitive{
|
|
||||||
Color: hex(S.Base0C),
|
|
||||||
Underline: boolPtr(true),
|
|
||||||
},
|
|
||||||
ImageText: ansi.StylePrimitive{
|
|
||||||
Color: hex(S.Base04),
|
|
||||||
Format: "Image: {{.text}} ->",
|
|
||||||
},
|
|
||||||
Code: ansi.StyleBlock{
|
|
||||||
StylePrimitive: ansi.StylePrimitive{
|
|
||||||
Prefix: " ",
|
|
||||||
Suffix: " ",
|
|
||||||
Color: hex(S.Base0B),
|
|
||||||
BackgroundColor: hex(S.Base01),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
CodeBlock: ansi.StyleCodeBlock{
|
|
||||||
StyleBlock: ansi.StyleBlock{
|
|
||||||
StylePrimitive: ansi.StylePrimitive{
|
|
||||||
Color: hex(S.Base04),
|
|
||||||
},
|
|
||||||
Margin: uintPtr(2),
|
|
||||||
},
|
|
||||||
Chroma: &ansi.Chroma{
|
|
||||||
Text: ansi.StylePrimitive{
|
|
||||||
Color: hex(S.Base05),
|
|
||||||
},
|
|
||||||
Error: ansi.StylePrimitive{
|
|
||||||
Color: hex(S.Base07),
|
|
||||||
BackgroundColor: hex(S.Base08),
|
|
||||||
},
|
|
||||||
Comment: ansi.StylePrimitive{
|
|
||||||
Color: hex(S.Base03),
|
|
||||||
Italic: boolPtr(true),
|
|
||||||
},
|
|
||||||
CommentPreproc: ansi.StylePrimitive{
|
|
||||||
Color: hex(S.Base09),
|
|
||||||
},
|
|
||||||
Keyword: ansi.StylePrimitive{
|
|
||||||
Color: hex(S.Base0E),
|
|
||||||
},
|
|
||||||
KeywordReserved: ansi.StylePrimitive{
|
|
||||||
Color: hex(S.Base0E),
|
|
||||||
},
|
|
||||||
KeywordNamespace: ansi.StylePrimitive{
|
|
||||||
Color: hex(S.Base0D),
|
|
||||||
},
|
|
||||||
KeywordType: ansi.StylePrimitive{
|
|
||||||
Color: hex(S.Base0A),
|
|
||||||
},
|
|
||||||
Operator: ansi.StylePrimitive{
|
|
||||||
Color: hex(S.Base05),
|
|
||||||
},
|
|
||||||
Punctuation: ansi.StylePrimitive{
|
|
||||||
Color: hex(S.Base05),
|
|
||||||
},
|
|
||||||
Name: ansi.StylePrimitive{
|
|
||||||
Color: hex(S.Base05),
|
|
||||||
},
|
|
||||||
NameBuiltin: ansi.StylePrimitive{
|
|
||||||
Color: hex(S.Base0D),
|
|
||||||
},
|
|
||||||
NameTag: ansi.StylePrimitive{
|
|
||||||
Color: hex(S.Base08),
|
|
||||||
},
|
|
||||||
NameAttribute: ansi.StylePrimitive{
|
|
||||||
Color: hex(S.Base0A),
|
|
||||||
},
|
|
||||||
NameClass: ansi.StylePrimitive{
|
|
||||||
Color: hex(S.Base0A),
|
|
||||||
Bold: boolPtr(true),
|
|
||||||
Underline: boolPtr(true),
|
|
||||||
},
|
|
||||||
NameConstant: ansi.StylePrimitive{
|
|
||||||
Color: hex(S.Base09),
|
|
||||||
},
|
|
||||||
NameDecorator: ansi.StylePrimitive{
|
|
||||||
Color: hex(S.Base0C),
|
|
||||||
},
|
|
||||||
NameFunction: ansi.StylePrimitive{
|
|
||||||
Color: hex(S.Base0D),
|
|
||||||
},
|
|
||||||
LiteralNumber: ansi.StylePrimitive{
|
|
||||||
Color: hex(S.Base09),
|
|
||||||
},
|
|
||||||
LiteralString: ansi.StylePrimitive{
|
|
||||||
Color: hex(S.Base0B),
|
|
||||||
},
|
|
||||||
LiteralStringEscape: ansi.StylePrimitive{
|
|
||||||
Color: hex(S.Base0C),
|
|
||||||
},
|
|
||||||
GenericDeleted: ansi.StylePrimitive{
|
|
||||||
Color: hex(S.Base08),
|
|
||||||
},
|
|
||||||
GenericEmph: ansi.StylePrimitive{
|
|
||||||
Italic: boolPtr(true),
|
|
||||||
},
|
|
||||||
GenericInserted: ansi.StylePrimitive{
|
|
||||||
Color: hex(S.Base0B),
|
|
||||||
},
|
|
||||||
GenericStrong: ansi.StylePrimitive{
|
|
||||||
Bold: boolPtr(true),
|
|
||||||
},
|
|
||||||
GenericSubheading: ansi.StylePrimitive{
|
|
||||||
Color: hex(S.Base04),
|
|
||||||
},
|
|
||||||
Background: ansi.StylePrimitive{
|
|
||||||
BackgroundColor: hex(S.Base01),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Table: ansi.StyleTable{
|
|
||||||
StyleBlock: ansi.StyleBlock{
|
|
||||||
StylePrimitive: ansi.StylePrimitive{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
DefinitionDescription: ansi.StylePrimitive{
|
|
||||||
BlockPrefix: "\n> ",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
module github.com/anotherhadi/ilovetui
|
|
||||||
|
|
||||||
go 1.25.8
|
|
||||||
|
|
||||||
require (
|
|
||||||
charm.land/bubbles/v2 v2.1.0
|
|
||||||
charm.land/glamour/v2 v2.0.0
|
|
||||||
charm.land/lipgloss/v2 v2.0.3
|
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
charm.land/bubbletea/v2 v2.0.2 // indirect
|
|
||||||
github.com/alecthomas/chroma/v2 v2.14.0 // indirect
|
|
||||||
github.com/atotto/clipboard v0.1.4 // indirect
|
|
||||||
github.com/aymerick/douceur v0.2.0 // indirect
|
|
||||||
github.com/charmbracelet/colorprofile v0.4.3 // indirect
|
|
||||||
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect
|
|
||||||
github.com/charmbracelet/x/ansi v0.11.7 // indirect
|
|
||||||
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
|
|
||||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
|
||||||
github.com/charmbracelet/x/termios v0.1.1 // indirect
|
|
||||||
github.com/charmbracelet/x/windows v0.2.2 // indirect
|
|
||||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
|
||||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
|
||||||
github.com/dlclark/regexp2 v1.11.0 // indirect
|
|
||||||
github.com/gorilla/css v1.0.1 // indirect
|
|
||||||
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
|
|
||||||
github.com/mattn/go-runewidth v0.0.23 // indirect
|
|
||||||
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
|
|
||||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
|
||||||
github.com/yuin/goldmark v1.7.8 // indirect
|
|
||||||
github.com/yuin/goldmark-emoji v1.0.5 // indirect
|
|
||||||
golang.org/x/net v0.39.0 // indirect
|
|
||||||
golang.org/x/sync v0.19.0 // indirect
|
|
||||||
golang.org/x/sys v0.43.0 // indirect
|
|
||||||
golang.org/x/text v0.24.0 // indirect
|
|
||||||
)
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
charm.land/bubbles/v2 v2.1.0 h1:YSnNh5cPYlYjPxRrzs5VEn3vwhtEn3jVGRBT3M7/I0g=
|
|
||||||
charm.land/bubbles/v2 v2.1.0/go.mod h1:l97h4hym2hvWBVfmJDtrEHHCtkIKeTEb3TTJ4ZOB3wY=
|
|
||||||
charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0=
|
|
||||||
charm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=
|
|
||||||
charm.land/glamour/v2 v2.0.0 h1:IDBoqLEy7Hdpb9VOXN+khLP/XSxtJy1VsHuW/yF87+U=
|
|
||||||
charm.land/glamour/v2 v2.0.0/go.mod h1:kjq9WB0s8vuUYZNYey2jp4Lgd9f4cKdzAw88FZtpj/w=
|
|
||||||
charm.land/lipgloss/v2 v2.0.3 h1:yM2zJ4Cf5Y51b7RHIwioil4ApI/aypFXXVHSwlM6RzU=
|
|
||||||
charm.land/lipgloss/v2 v2.0.3/go.mod h1:7myLU9iG/3xluAWzpY/fSxYYHCgoKTie7laxk6ATwXA=
|
|
||||||
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
|
||||||
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
|
||||||
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
|
|
||||||
github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
|
||||||
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
|
|
||||||
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
|
|
||||||
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
|
||||||
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
|
||||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
|
||||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
|
||||||
github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
|
|
||||||
github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
|
|
||||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
|
||||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
|
||||||
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
|
|
||||||
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
|
|
||||||
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA=
|
|
||||||
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98=
|
|
||||||
github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI=
|
|
||||||
github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ=
|
|
||||||
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
|
|
||||||
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
|
|
||||||
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI=
|
|
||||||
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU=
|
|
||||||
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
|
||||||
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
|
||||||
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
|
|
||||||
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
|
|
||||||
github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
|
|
||||||
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
|
|
||||||
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
|
|
||||||
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
|
||||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
|
||||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
|
||||||
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
|
||||||
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
|
||||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
|
||||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
|
||||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
|
||||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
|
||||||
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
|
|
||||||
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
|
||||||
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
|
|
||||||
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
|
||||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
|
||||||
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
|
||||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
|
||||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
|
||||||
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
|
||||||
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
|
||||||
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
|
||||||
github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
|
|
||||||
github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
|
|
||||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
|
||||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
|
||||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
|
||||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
|
||||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
|
||||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
|
||||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
|
||||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
|
||||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
|
||||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
-116
@@ -1,116 +0,0 @@
|
|||||||
// Package ilovetui provides a shared Base16 color theme for bubbletea/lipgloss
|
|
||||||
// applications. The theme is loaded automatically on import from
|
|
||||||
// ~/.config/ilovetui/config.yaml (falling back to the embedded
|
|
||||||
// default config). Access colors and styles via the package-level variable S.
|
|
||||||
//
|
|
||||||
// import "github.com/anotherhadi/ilovetui"
|
|
||||||
//
|
|
||||||
// style := lipgloss.NewStyle().Foreground(ilovetui.S.Primary)
|
|
||||||
// box := ilovetui.RenderWithTitle(ilovetui.S.PanelFocused, "Title", content, w, h)
|
|
||||||
package ilovetui
|
|
||||||
|
|
||||||
import (
|
|
||||||
_ "embed"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed default.yaml
|
|
||||||
var DefaultConfig []byte
|
|
||||||
|
|
||||||
// S is the active theme. It is populated automatically at import time and can
|
|
||||||
// be reloaded at any point by calling Init, InitFrom, or InitFromBytes.
|
|
||||||
var S Styles
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
path := DefaultConfigPath()
|
|
||||||
if data, err := os.ReadFile(path); err == nil {
|
|
||||||
if s, err := stylesFromBytes(data); err == nil {
|
|
||||||
S = s
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Silent fallback: embedded default always works.
|
|
||||||
s, _ := stylesFromBytes(DefaultConfig)
|
|
||||||
S = s
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init reloads S from the user config file, falling back to the embedded
|
|
||||||
// default if the file is missing. Returns an error only on parse failures.
|
|
||||||
func Init() error {
|
|
||||||
path := DefaultConfigPath()
|
|
||||||
data, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
s, e := stylesFromBytes(DefaultConfig)
|
|
||||||
if e != nil {
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
S = s
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return InitFromBytes(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InitFrom reloads S from an explicit file path.
|
|
||||||
func InitFrom(path string) error {
|
|
||||||
data, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("ilovetui: read config: %w", err)
|
|
||||||
}
|
|
||||||
return InitFromBytes(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InitFromBytes reloads S from raw YAML. Accepts hex strings with or without
|
|
||||||
// the leading '#'.
|
|
||||||
func InitFromBytes(data []byte) error {
|
|
||||||
s, err := stylesFromBytes(data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
S = s
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteDefaultConfig writes the embedded default config to path, creating
|
|
||||||
// parent directories as needed. No-op if the file already exists.
|
|
||||||
func WriteDefaultConfig(path string) error {
|
|
||||||
if _, err := os.Stat(path); err == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
||||||
return fmt.Errorf("ilovetui: create config dir: %w", err)
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(path, DefaultConfig, 0o600); err != nil {
|
|
||||||
return fmt.Errorf("ilovetui: write config: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DefaultConfigPath returns the canonical user config path,
|
|
||||||
// respecting $XDG_CONFIG_HOME.
|
|
||||||
func DefaultConfigPath() string {
|
|
||||||
return filepath.Join(configDir(), "ilovetui", "config.yaml")
|
|
||||||
}
|
|
||||||
|
|
||||||
func stylesFromBytes(data []byte) (Styles, error) {
|
|
||||||
var base configYAML
|
|
||||||
if err := yaml.Unmarshal(DefaultConfig, &base); err != nil {
|
|
||||||
return Styles{}, fmt.Errorf("ilovetui: parse default config: %w", err)
|
|
||||||
}
|
|
||||||
var user configYAML
|
|
||||||
if err := yaml.Unmarshal(data, &user); err != nil {
|
|
||||||
return Styles{}, fmt.Errorf("ilovetui: parse config: %w", err)
|
|
||||||
}
|
|
||||||
return newStyles(mergeColors(base.Colors, user.Colors)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func configDir() string {
|
|
||||||
if dir := os.Getenv("XDG_CONFIG_HOME"); dir != "" {
|
|
||||||
return dir
|
|
||||||
}
|
|
||||||
home, _ := os.UserHomeDir()
|
|
||||||
return filepath.Join(home, ".config")
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
pkgs,
|
|
||||||
gitHooksLib,
|
|
||||||
}: let
|
|
||||||
hooks = gitHooksLib.run {
|
|
||||||
src = ../.;
|
|
||||||
hooks = {
|
|
||||||
gofmt.enable = true;
|
|
||||||
govet.enable = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
in
|
|
||||||
pkgs.mkShell {
|
|
||||||
packages = with pkgs;
|
|
||||||
[
|
|
||||||
go
|
|
||||||
doctoc
|
|
||||||
]
|
|
||||||
++ hooks.enabledPackages;
|
|
||||||
|
|
||||||
shellHook = hooks.shellHook;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
"name": "ilovetui",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"module": "dist/index.js",
|
||||||
|
"type": "module",
|
||||||
|
"types": "./src/index.ts",
|
||||||
|
"exports": {
|
||||||
|
".": { "types": "./src/index.ts", "default": "./dist/index.js" },
|
||||||
|
"./solid": { "types": "./src/solid.ts", "default": "./dist/solid.js" },
|
||||||
|
"./context": { "types": "./src/context/index.ts", "default": "./dist/context/index.js" },
|
||||||
|
"./project-config": { "types": "./src/project-config.ts", "default": "./dist/project-config.js" },
|
||||||
|
"./keymap": { "types": "./src/keymap/provider.tsx", "default": "./dist/keymap/provider.js" },
|
||||||
|
"./components": { "types": "./src/components/index.ts", "default": "./dist/components/index.js" },
|
||||||
|
"./components/spinner": { "types": "./src/components/Spinner.tsx", "default": "./dist/components/Spinner.js" },
|
||||||
|
"./components/help": { "types": "./src/components/help.ts", "default": "./dist/components/help.js" },
|
||||||
|
"./components/section-header": { "types": "./src/components/SectionHeader.tsx", "default": "./dist/components/SectionHeader.js" }
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"files": ["dist", "src"],
|
||||||
|
"scripts": {
|
||||||
|
"build": "bun run scripts/build.ts",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@opentui/core": ">=0.5.0",
|
||||||
|
"@opentui/solid": ">=0.5.0",
|
||||||
|
"@opentui/keymap": ">=0.5.0",
|
||||||
|
"solid-js": ">=1.9.0",
|
||||||
|
"opentui-spinner": ">=0.0.7"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@opentui/core": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@opentui/solid": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@opentui/keymap": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"solid-js": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"opentui-spinner": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"yaml": "^2.8.1",
|
||||||
|
"zod": "^4.5.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@opentui/core": "^0.5.9",
|
||||||
|
"@opentui/keymap": "^0.5.9",
|
||||||
|
"@opentui/solid": "^0.5.9",
|
||||||
|
"@types/bun": "^1.3.14",
|
||||||
|
"opentui-spinner": "^0.0.7",
|
||||||
|
"solid-js": "1.9.12",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { createSolidTransformPlugin } from "@opentui/solid/bun-plugin";
|
||||||
|
|
||||||
|
const entrypoints = await Array.fromAsync(new Bun.Glob("**/*.{ts,tsx}").scan({ cwd: "src" }));
|
||||||
|
|
||||||
|
await Bun.$`rm -rf dist`;
|
||||||
|
|
||||||
|
const result = await Bun.build({
|
||||||
|
entrypoints: entrypoints.map((path) => `src/${path}`),
|
||||||
|
outdir: "dist",
|
||||||
|
root: "src",
|
||||||
|
target: "bun",
|
||||||
|
format: "esm",
|
||||||
|
splitting: true,
|
||||||
|
plugins: [createSolidTransformPlugin()],
|
||||||
|
external: ["@opentui/core", "@opentui/solid", "@opentui/keymap", "solid-js", "opentui-spinner", "yaml", "zod"],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
for (const message of result.logs) console.error(message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Bun.write("dist/default.yaml", Bun.file("src/default.yaml"));
|
||||||
|
|
||||||
|
console.log(`Built ${result.outputs.length} files to dist/`);
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { type ColorInput, TextAttributes } from "@opentui/core";
|
||||||
|
import { Show } from "solid-js";
|
||||||
|
import { theme } from "../index.ts";
|
||||||
|
|
||||||
|
const CAP_LEFT = "";
|
||||||
|
const CAP_RIGHT = "";
|
||||||
|
|
||||||
|
export interface BadgeProps {
|
||||||
|
label: string;
|
||||||
|
color: ColorInput;
|
||||||
|
textColor?: ColorInput;
|
||||||
|
backgroundColor?: ColorInput;
|
||||||
|
bold?: boolean;
|
||||||
|
withNerdfont?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Badge(props: BadgeProps) {
|
||||||
|
const textColor = () => props.textColor ?? theme.background;
|
||||||
|
const backgroundColor = () => props.backgroundColor ?? theme.background;
|
||||||
|
const attributes = () => (props.bold === false ? undefined : TextAttributes.BOLD);
|
||||||
|
const nerdFonts = () => props.withNerdfont ?? theme.nerdFonts;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box flexDirection="row">
|
||||||
|
<Show when={nerdFonts()}>
|
||||||
|
<text fg={props.color} bg={backgroundColor()}>
|
||||||
|
{CAP_LEFT}
|
||||||
|
</text>
|
||||||
|
</Show>
|
||||||
|
<text fg={textColor()} bg={props.color} attributes={attributes()}>
|
||||||
|
{nerdFonts() ? props.label : ` ${props.label} `}
|
||||||
|
</text>
|
||||||
|
<Show when={nerdFonts()}>
|
||||||
|
<text fg={props.color} bg={backgroundColor()}>
|
||||||
|
{CAP_RIGHT}
|
||||||
|
</text>
|
||||||
|
</Show>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import type { BoxProps as OpenTUIBoxProps } from "@opentui/solid";
|
||||||
|
import { theme } from "../index.ts";
|
||||||
|
|
||||||
|
export interface BoxProps extends OpenTUIBoxProps {}
|
||||||
|
|
||||||
|
// The raw <box> intrinsic has no idea ilovetui's theme exists — its own
|
||||||
|
// borderStyle default ("single") is hardcoded in @opentui/core. Use <Box>
|
||||||
|
// instead of <box> wherever you want theme.borderStyle applied by default,
|
||||||
|
// same as Sidebar/Modal/Tabs already do internally.
|
||||||
|
export function Box(props: BoxProps) {
|
||||||
|
return <box {...props} borderStyle={props.borderStyle ?? theme.borderStyle} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { type ColorInput, CliRenderEvents, TextAttributes, type BoxRenderable } from "@opentui/core";
|
||||||
|
import { onResize, useRenderer } from "@opentui/solid";
|
||||||
|
import { formatCommandBindings } from "@opentui/keymap/extras";
|
||||||
|
import { useKeymapSelector } from "@opentui/keymap/solid";
|
||||||
|
import { createMemo, createSignal, For } from "solid-js";
|
||||||
|
import { theme } from "../index.ts";
|
||||||
|
|
||||||
|
const KEY_DISPLAY = { up: "↑", down: "↓", left: "←", right: "→", enter: "Enter", escape: "Esc" };
|
||||||
|
const SEPARATOR = " ";
|
||||||
|
const ELLIPSIS = "…";
|
||||||
|
|
||||||
|
export interface HelpBarProps {
|
||||||
|
accentColor?: ColorInput;
|
||||||
|
mutedColor?: ColorInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HelpEntry {
|
||||||
|
command: string;
|
||||||
|
keys: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HelpBar(props: HelpBarProps = {}) {
|
||||||
|
const accent = () => props.accentColor ?? theme.primary;
|
||||||
|
const muted = () => props.mutedColor ?? theme.muted;
|
||||||
|
const renderer = useRenderer();
|
||||||
|
const [width, setWidth] = createSignal(0);
|
||||||
|
let container: BoxRenderable | undefined;
|
||||||
|
const measure = () => {
|
||||||
|
if (!container) return;
|
||||||
|
setWidth(container.width);
|
||||||
|
};
|
||||||
|
const remeasureNextFrame = () => renderer.once(CliRenderEvents.FRAME, measure);
|
||||||
|
onResize(remeasureNextFrame);
|
||||||
|
|
||||||
|
const entries = useKeymapSelector((keymap): HelpEntry[] =>
|
||||||
|
keymap
|
||||||
|
.getCommandEntries({ visibility: "active" })
|
||||||
|
.filter((entry) => entry.command.shortHelp === true)
|
||||||
|
.map((entry) => ({
|
||||||
|
command: entry.command.name,
|
||||||
|
keys: formatCommandBindings(entry.bindings, { keyNameAliases: KEY_DISPLAY }) ?? "",
|
||||||
|
label: typeof entry.command.desc === "string" ? entry.command.desc : entry.command.name,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const visible = createMemo((): HelpEntry[] => {
|
||||||
|
const available = width() - 2;
|
||||||
|
const shown: HelpEntry[] = [];
|
||||||
|
let used = 0;
|
||||||
|
for (const entry of entries()) {
|
||||||
|
const sepWidth = shown.length > 0 ? SEPARATOR.length : 0;
|
||||||
|
const entryWidth = entry.keys.length + 1 + entry.label.length;
|
||||||
|
if (used + sepWidth + entryWidth <= available) {
|
||||||
|
used += sepWidth + entryWidth;
|
||||||
|
shown.push(entry);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const remaining = available - used - sepWidth;
|
||||||
|
const prefix = `${entry.keys} `;
|
||||||
|
if (remaining > prefix.length + ELLIPSIS.length) {
|
||||||
|
const labelBudget = remaining - prefix.length - ELLIPSIS.length;
|
||||||
|
shown.push({ ...entry, label: `${entry.label.slice(0, labelBudget)}${ELLIPSIS}` });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return shown;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box
|
||||||
|
flexDirection="row"
|
||||||
|
paddingLeft={1}
|
||||||
|
paddingRight={1}
|
||||||
|
overflow="hidden"
|
||||||
|
ref={(el) => {
|
||||||
|
container = el;
|
||||||
|
remeasureNextFrame();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<For each={visible()}>
|
||||||
|
{(entry, i) => (
|
||||||
|
<>
|
||||||
|
{i() > 0 && <text fg={muted()}>{SEPARATOR}</text>}
|
||||||
|
<text fg={accent()} attributes={TextAttributes.BOLD}>
|
||||||
|
{entry.keys}
|
||||||
|
</text>
|
||||||
|
<text fg={muted()}>{` ${entry.label}`}</text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { type ColorInput, type Renderable, TextAttributes } from "@opentui/core";
|
||||||
|
import { formatCommandBindings } from "@opentui/keymap/extras";
|
||||||
|
import { useKeymapSelector } from "@opentui/keymap/solid";
|
||||||
|
import { createSignal } from "solid-js";
|
||||||
|
import { helpOpen, toggleHelp } from "../context/help.ts";
|
||||||
|
import { Modal } from "./Modal.tsx";
|
||||||
|
|
||||||
|
const KEY_DISPLAY = { up: "↑", down: "↓", left: "←", right: "→", enter: "Enter", escape: "Esc" };
|
||||||
|
|
||||||
|
export interface HelpModalProps {
|
||||||
|
accentColor?: ColorInput;
|
||||||
|
backgroundColor?: ColorInput;
|
||||||
|
backdropColor?: ColorInput;
|
||||||
|
width?: number;
|
||||||
|
/** See ModalProps.dismissKey. */
|
||||||
|
dismissKey?: string | false;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HelpEntry {
|
||||||
|
command: string;
|
||||||
|
keys: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HelpModal(props: HelpModalProps = {}) {
|
||||||
|
// The modal takes focus when it opens, which would make its own "close"
|
||||||
|
// the only active command. List what was active where focus was *before*
|
||||||
|
// it opened instead.
|
||||||
|
const [scope, setScope] = createSignal<Renderable | null>(null);
|
||||||
|
|
||||||
|
const entries = useKeymapSelector((keymap): HelpEntry[] =>
|
||||||
|
keymap.getCommandEntries({ visibility: "active", focused: scope() }).map((entry) => ({
|
||||||
|
command: entry.command.name,
|
||||||
|
keys: formatCommandBindings(entry.bindings, { keyNameAliases: KEY_DISPLAY }) ?? "",
|
||||||
|
label: typeof entry.command.desc === "string" ? entry.command.desc : entry.command.name,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={helpOpen()}
|
||||||
|
onDismiss={toggleHelp}
|
||||||
|
onOpen={(previous) => setScope(() => previous)}
|
||||||
|
dismissKey={props.dismissKey}
|
||||||
|
accentColor={props.accentColor}
|
||||||
|
backgroundColor={props.backgroundColor}
|
||||||
|
backdropColor={props.backdropColor}
|
||||||
|
width={props.width}
|
||||||
|
maxHeight={entries().length + 6}
|
||||||
|
scrollable
|
||||||
|
>
|
||||||
|
<text attributes={TextAttributes.BOLD}>Keybindings</text>
|
||||||
|
<text> </text>
|
||||||
|
<text>{entries().map((entry) => `${entry.keys.padEnd(10)}${entry.label}`).join("\n")}</text>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import type { ColorInput } from "@opentui/core";
|
||||||
|
import { Show, type JSX } from "solid-js";
|
||||||
|
import { theme } from "../index.ts";
|
||||||
|
|
||||||
|
export interface KeyValueProps {
|
||||||
|
label: string;
|
||||||
|
/** Plain value; ignored when children are given. */
|
||||||
|
value?: string | number;
|
||||||
|
valueColor?: ColorInput;
|
||||||
|
children?: JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A "Label ........ value" line, as used in details panels and modals. */
|
||||||
|
export function KeyValue(props: KeyValueProps) {
|
||||||
|
return (
|
||||||
|
<box flexShrink={0} flexDirection="row" justifyContent="space-between">
|
||||||
|
<text fg={theme.muted}>{props.label}</text>
|
||||||
|
<Show when={props.children} fallback={<text fg={props.valueColor}>{props.value ?? ""}</text>}>
|
||||||
|
{props.children}
|
||||||
|
</Show>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { type ColorInput, TextAttributes } from "@opentui/core";
|
||||||
|
import { Show, type JSX } from "solid-js";
|
||||||
|
import { presets, theme } from "../index.ts";
|
||||||
|
|
||||||
|
/** Returns the selected-row text color when the row is selected, `fallback` otherwise. */
|
||||||
|
export type RowTone = (fallback?: ColorInput) => ColorInput | undefined;
|
||||||
|
|
||||||
|
export interface ListRowProps {
|
||||||
|
id?: string;
|
||||||
|
label: string;
|
||||||
|
selected: boolean;
|
||||||
|
bold?: boolean;
|
||||||
|
onClick?: () => void;
|
||||||
|
/** Right-aligned content; use `tone` for its colors so it follows the selection. */
|
||||||
|
right?: (tone: RowTone) => JSX.Element;
|
||||||
|
/** Extra lines under the main one. */
|
||||||
|
below?: (tone: RowTone) => JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One row of a keyboard-navigable list: truncated label on the left, details on the right. */
|
||||||
|
export function ListRow(props: ListRowProps) {
|
||||||
|
const tone: RowTone = (fallback) => (props.selected ? presets.select.selectedTextColor : fallback);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box
|
||||||
|
id={props.id}
|
||||||
|
flexShrink={0}
|
||||||
|
flexDirection="column"
|
||||||
|
paddingLeft={1}
|
||||||
|
paddingRight={1}
|
||||||
|
backgroundColor={props.selected ? presets.select.selectedBackgroundColor : "transparent"}
|
||||||
|
onMouseDown={theme.mouse && props.onClick ? () => props.onClick?.() : undefined}
|
||||||
|
>
|
||||||
|
<box flexDirection="row" justifyContent="space-between" alignItems="center" height={1}>
|
||||||
|
<box flexGrow={1} flexShrink={1} minWidth={0} marginRight={1} overflow="hidden">
|
||||||
|
<text fg={tone()} attributes={props.bold ? TextAttributes.BOLD : undefined} wrapMode="none">
|
||||||
|
{props.label}
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
<Show when={props.right}>
|
||||||
|
<box flexShrink={0} flexDirection="row" alignItems="center">
|
||||||
|
{props.right?.(tone)}
|
||||||
|
</box>
|
||||||
|
</Show>
|
||||||
|
</box>
|
||||||
|
{props.below?.(tone)}
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import type { ColorInput } from "@opentui/core";
|
||||||
|
import { useTerminalDimensions } from "@opentui/solid";
|
||||||
|
import { Show, type ParentProps } from "solid-js";
|
||||||
|
import { theme } from "../index.ts";
|
||||||
|
|
||||||
|
export interface MinSizeGuardProps extends ParentProps {
|
||||||
|
minWidth?: number;
|
||||||
|
minHeight?: number;
|
||||||
|
backgroundColor?: ColorInput;
|
||||||
|
textColor?: ColorInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MinSizeGuard(props: MinSizeGuardProps) {
|
||||||
|
const dimensions = useTerminalDimensions();
|
||||||
|
const minWidth = () => props.minWidth ?? 80;
|
||||||
|
const minHeight = () => props.minHeight ?? 24;
|
||||||
|
const tooSmall = () => dimensions().width < minWidth() || dimensions().height < minHeight();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{props.children}
|
||||||
|
<Show when={tooSmall()}>
|
||||||
|
<box
|
||||||
|
position="absolute"
|
||||||
|
top={0}
|
||||||
|
left={0}
|
||||||
|
width={dimensions().width}
|
||||||
|
height={dimensions().height}
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
backgroundColor={props.backgroundColor ?? theme.background}
|
||||||
|
zIndex={9999}
|
||||||
|
>
|
||||||
|
<text fg={props.textColor ?? theme.muted}>
|
||||||
|
{`Terminal too small\nMinimum size: ${minWidth()}x${minHeight()} — current: ${dimensions().width}x${dimensions().height}`}
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
</Show>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import { type BoxRenderable, type ColorInput, type KeyEvent, type MouseEvent, type Renderable, RGBA } from "@opentui/core";
|
||||||
|
import type { Binding, Command } from "@opentui/keymap";
|
||||||
|
import { useBindings } from "@opentui/keymap/solid";
|
||||||
|
import { Portal, useRenderer, useTerminalDimensions } from "@opentui/solid";
|
||||||
|
import { createSignal, onCleanup, onMount, Show, type ParentProps } from "solid-js";
|
||||||
|
import { theme } from "../index.ts";
|
||||||
|
|
||||||
|
export type ModalCommand = Command<Renderable, KeyEvent>;
|
||||||
|
export type ModalBinding = Binding<Renderable, KeyEvent>;
|
||||||
|
|
||||||
|
export interface ModalProps extends ParentProps {
|
||||||
|
open: boolean;
|
||||||
|
onDismiss?: () => void;
|
||||||
|
/**
|
||||||
|
* Key that calls onDismiss while the modal has focus (default "escape").
|
||||||
|
* `false` disables it. Needs a KeymapProvider unless disabled.
|
||||||
|
*/
|
||||||
|
dismissKey?: string | false;
|
||||||
|
/** Extra keymap commands/bindings, active only while the modal has focus. */
|
||||||
|
commands?: ModalCommand[];
|
||||||
|
bindings?: ModalBinding[];
|
||||||
|
/** Called on open with whatever had focus before the modal took it. */
|
||||||
|
onOpen?: (previousFocus: Renderable | null) => void;
|
||||||
|
accentColor?: ColorInput;
|
||||||
|
backgroundColor?: ColorInput;
|
||||||
|
backdropColor?: ColorInput;
|
||||||
|
width?: number;
|
||||||
|
maxHeight?: number;
|
||||||
|
scrollable?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Modal(props: ModalProps) {
|
||||||
|
return (
|
||||||
|
<Portal>
|
||||||
|
<Show when={props.open}>
|
||||||
|
<ModalPanel {...props} />
|
||||||
|
</Show>
|
||||||
|
</Portal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mounted only while open: takes native focus on mount and hands it back to
|
||||||
|
// whatever had it on unmount, so keymap layers scoped to the page underneath
|
||||||
|
// go quiet while the modal is up and come back on their own afterwards.
|
||||||
|
function ModalPanel(props: ModalProps) {
|
||||||
|
const renderer = useRenderer();
|
||||||
|
const dimensions = useTerminalDimensions();
|
||||||
|
const [panel, setPanel] = createSignal<BoxRenderable>();
|
||||||
|
|
||||||
|
const screenCap = () => Math.max(5, dimensions().height - 4);
|
||||||
|
const maxPanelHeight = () => Math.min(props.maxHeight ?? screenCap(), screenCap());
|
||||||
|
const maxContentHeight = () => Math.max(1, maxPanelHeight() - 4);
|
||||||
|
const maxPanelWidth = () => Math.max(10, dimensions().width - 4);
|
||||||
|
const panelWidth = () => Math.min(props.width ?? 50, maxPanelWidth());
|
||||||
|
|
||||||
|
let previousFocus: Renderable | null = null;
|
||||||
|
onMount(() => {
|
||||||
|
previousFocus = renderer.currentFocusedRenderable;
|
||||||
|
props.onOpen?.(previousFocus);
|
||||||
|
panel()?.focus();
|
||||||
|
});
|
||||||
|
onCleanup(() => {
|
||||||
|
if (previousFocus && !previousFocus.isDestroyed) previousFocus.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
const dismissKey = props.dismissKey ?? "escape";
|
||||||
|
if (dismissKey !== false || props.commands || props.bindings) {
|
||||||
|
useBindings(() => ({
|
||||||
|
target: panel,
|
||||||
|
commands: [
|
||||||
|
...(dismissKey !== false
|
||||||
|
? [{ name: "modal_dismiss", desc: "close", shortHelp: true, run: () => props.onDismiss?.() }]
|
||||||
|
: []),
|
||||||
|
...(props.commands ?? []),
|
||||||
|
],
|
||||||
|
bindings: [...(dismissKey !== false ? [{ key: dismissKey, cmd: "modal_dismiss" }] : []), ...(props.bindings ?? [])],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box
|
||||||
|
width={dimensions().width}
|
||||||
|
height={dimensions().height}
|
||||||
|
position="absolute"
|
||||||
|
top={0}
|
||||||
|
left={0}
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
backgroundColor={props.backdropColor ?? RGBA.fromInts(0, 0, 0, 150)}
|
||||||
|
zIndex={2000}
|
||||||
|
onMouseDown={theme.mouse ? props.onDismiss : undefined}
|
||||||
|
>
|
||||||
|
<box
|
||||||
|
ref={setPanel}
|
||||||
|
focusable
|
||||||
|
width={panelWidth()}
|
||||||
|
maxHeight={maxPanelHeight()}
|
||||||
|
border
|
||||||
|
borderStyle={theme.borderStyle}
|
||||||
|
borderColor={props.accentColor ?? theme.primary}
|
||||||
|
focusedBorderColor={props.accentColor ?? theme.primary}
|
||||||
|
backgroundColor={props.backgroundColor ?? theme.background}
|
||||||
|
flexDirection="column"
|
||||||
|
paddingLeft={2}
|
||||||
|
paddingRight={2}
|
||||||
|
paddingTop={1}
|
||||||
|
paddingBottom={1}
|
||||||
|
onMouseDown={theme.mouse ? (event: MouseEvent) => event.stopPropagation() : undefined}
|
||||||
|
>
|
||||||
|
<Show
|
||||||
|
when={props.scrollable}
|
||||||
|
fallback={
|
||||||
|
<box flexDirection="column" maxHeight={maxContentHeight()} overflow="hidden">
|
||||||
|
{props.children}
|
||||||
|
</box>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<scrollbox maxHeight={maxContentHeight()} flexShrink={1} scrollY scrollX={false} focusable={false}>
|
||||||
|
{props.children}
|
||||||
|
</scrollbox>
|
||||||
|
</Show>
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import type { ColorInput } from "@opentui/core";
|
||||||
|
import { Portal } from "@opentui/solid";
|
||||||
|
import { For } from "solid-js";
|
||||||
|
import { dismiss, notifications, type NotificationKind } from "../context/notifications.ts";
|
||||||
|
import { theme } from "../index.ts";
|
||||||
|
|
||||||
|
const KIND_LABEL: Record<NotificationKind, string> = {
|
||||||
|
info: "Info",
|
||||||
|
success: "Success",
|
||||||
|
warning: "Warning",
|
||||||
|
error: "Error",
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface NotificationHostProps {
|
||||||
|
width?: number;
|
||||||
|
colors?: Partial<Record<NotificationKind, ColorInput>>;
|
||||||
|
backgroundColor?: ColorInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NotificationHost(props: NotificationHostProps = {}) {
|
||||||
|
const defaultColor = (): Record<NotificationKind, ColorInput> => ({
|
||||||
|
info: theme.primary,
|
||||||
|
success: theme.success,
|
||||||
|
warning: theme.warning,
|
||||||
|
error: theme.error,
|
||||||
|
});
|
||||||
|
const colorFor = (kind: NotificationKind) => props.colors?.[kind] ?? defaultColor()[kind];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Portal>
|
||||||
|
<box position="absolute" top={1} right={1} flexDirection="column" zIndex={1000}>
|
||||||
|
<For each={notifications()}>
|
||||||
|
{(toast) => (
|
||||||
|
<box
|
||||||
|
border
|
||||||
|
borderStyle={theme.borderStyle}
|
||||||
|
borderColor={colorFor(toast.kind)}
|
||||||
|
backgroundColor={props.backgroundColor ?? theme.background}
|
||||||
|
width={props.width ?? 32}
|
||||||
|
marginBottom={1}
|
||||||
|
onMouseDown={theme.mouse ? () => dismiss(toast.id) : undefined}
|
||||||
|
>
|
||||||
|
<text fg={colorFor(toast.kind)}>{toast.title ?? KIND_LABEL[toast.kind]}</text>
|
||||||
|
<text>{toast.message}</text>
|
||||||
|
</box>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</box>
|
||||||
|
</Portal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import type { BoxRenderable } from "@opentui/core";
|
||||||
|
import type { BoxProps } from "@opentui/solid";
|
||||||
|
import { type Accessor, createContext, createSignal, splitProps, useContext } from "solid-js";
|
||||||
|
import { useFocusWithin } from "../context/focus.ts";
|
||||||
|
|
||||||
|
export interface PaneHandle {
|
||||||
|
/** The pane's root renderable — use it as a keymap layer `target`. */
|
||||||
|
target: Accessor<BoxRenderable | undefined>;
|
||||||
|
/** True while native focus is anywhere inside the pane. */
|
||||||
|
focused: Accessor<boolean>;
|
||||||
|
focus(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PaneContext = createContext<PaneHandle>();
|
||||||
|
|
||||||
|
export interface PaneProps extends Omit<BoxProps, "ref"> {
|
||||||
|
/** Receives the pane's handle, e.g. to focus it or target it from outside. */
|
||||||
|
handle?: (handle: PaneHandle) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A focus region. Wrap each independently focusable area (sidebar, content,
|
||||||
|
* …) in a Pane; anything inside can call usePane() to scope keymap layers to
|
||||||
|
* it (`useBindings(() => ({ target: pane.target, … }))`) and to style itself
|
||||||
|
* from `pane.focused()`. Pass `focusable` when the pane itself should be the
|
||||||
|
* focus target (rather than e.g. a <select> inside it).
|
||||||
|
*/
|
||||||
|
export function Pane(props: PaneProps) {
|
||||||
|
const [local, rest] = splitProps(props, ["handle", "children"]);
|
||||||
|
const [el, setEl] = createSignal<BoxRenderable>();
|
||||||
|
const focused = useFocusWithin(el);
|
||||||
|
const handle: PaneHandle = { target: el, focused, focus: () => el()?.focus() };
|
||||||
|
local.handle?.(handle);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PaneContext.Provider value={handle}>
|
||||||
|
<box {...rest} ref={setEl}>
|
||||||
|
{local.children}
|
||||||
|
</box>
|
||||||
|
</PaneContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePane(): PaneHandle {
|
||||||
|
const pane = useContext(PaneContext);
|
||||||
|
if (!pane) throw new Error("usePane() must be used inside a <Pane>");
|
||||||
|
return pane;
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { type ColorInput, parseColor, RGBA } from "@opentui/core";
|
||||||
|
import { createMemo, For } from "solid-js";
|
||||||
|
import { theme } from "../index.ts";
|
||||||
|
|
||||||
|
function sampleGradient(stops: ColorInput[], t: number): ColorInput {
|
||||||
|
if (stops.length === 1) return stops[0]!;
|
||||||
|
const clamped = Math.min(Math.max(t, 0), 1);
|
||||||
|
const segment = clamped * (stops.length - 1);
|
||||||
|
const i = Math.min(Math.floor(segment), stops.length - 2);
|
||||||
|
const localT = segment - i;
|
||||||
|
const [ar, ag, ab, aa] = parseColor(stops[i]!).toInts();
|
||||||
|
const [br, bg, bb, ba] = parseColor(stops[i + 1]!).toInts();
|
||||||
|
return RGBA.fromInts(
|
||||||
|
Math.round(ar + (br - ar) * localT),
|
||||||
|
Math.round(ag + (bg - ag) * localT),
|
||||||
|
Math.round(ab + (bb - ab) * localT),
|
||||||
|
Math.round(aa + (ba - aa) * localT),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProgressBarProps {
|
||||||
|
value: number;
|
||||||
|
width?: number;
|
||||||
|
color?: ColorInput | ColorInput[];
|
||||||
|
trackColor?: ColorInput;
|
||||||
|
fillChar?: string;
|
||||||
|
trackChar?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProgressBar(props: ProgressBarProps) {
|
||||||
|
const width = () => props.width ?? 20;
|
||||||
|
const stops = createMemo(() => (Array.isArray(props.color) ? props.color : [props.color ?? theme.primary]));
|
||||||
|
const filled = createMemo(() => Math.round((width() * Math.min(Math.max(props.value, 0), 100)) / 100));
|
||||||
|
const cells = createMemo(() => Array.from({ length: width() }, (_, i) => i));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box flexDirection="row">
|
||||||
|
<For each={cells()}>
|
||||||
|
{(i) => {
|
||||||
|
const isFilled = () => i < filled();
|
||||||
|
const t = width() > 1 ? i / (width() - 1) : 0;
|
||||||
|
return (
|
||||||
|
<text fg={isFilled() ? sampleGradient(stops(), t) : (props.trackColor ?? theme.muted)}>
|
||||||
|
{isFilled() ? (props.fillChar ?? "█") : (props.trackChar ?? "░")}
|
||||||
|
</text>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { TextAttributes } from "@opentui/core";
|
||||||
|
import { Show } from "solid-js";
|
||||||
|
import { theme } from "../index.ts";
|
||||||
|
import { Spinner } from "./Spinner.tsx";
|
||||||
|
|
||||||
|
export interface SectionHeaderProps {
|
||||||
|
title?: string;
|
||||||
|
loading?: boolean;
|
||||||
|
/** Shown next to the spinner while loading (default "Loading…"). */
|
||||||
|
loadingLabel?: string;
|
||||||
|
/** Shown on the right when not loading, e.g. "r refresh". */
|
||||||
|
hint?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lives in its own entry point (ilovetui/components/section-header) because
|
||||||
|
// it pulls in the optional opentui-spinner peer dependency.
|
||||||
|
export function SectionHeader(props: SectionHeaderProps) {
|
||||||
|
return (
|
||||||
|
<box flexShrink={0} flexDirection="row" justifyContent="space-between" alignItems="center" height={1}>
|
||||||
|
<box flexGrow={1} flexShrink={1} minWidth={0} marginRight={1} overflow="hidden">
|
||||||
|
<text fg={theme.muted} attributes={TextAttributes.BOLD} wrapMode="none">
|
||||||
|
{props.title ?? ""}
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
<box flexShrink={0}>
|
||||||
|
<Show
|
||||||
|
when={!props.loading}
|
||||||
|
fallback={
|
||||||
|
<box flexDirection="row" alignItems="center">
|
||||||
|
<Spinner />
|
||||||
|
<text fg={theme.muted}> {props.loadingLabel ?? "Loading…"}</text>
|
||||||
|
</box>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<text fg={theme.muted}>{props.hint ?? ""}</text>
|
||||||
|
</Show>
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
import { type ColorInput, type MouseEvent, type SelectOption, type SelectRenderable, TextAttributes } from "@opentui/core";
|
||||||
|
import { createEffect, Show } from "solid-js";
|
||||||
|
import { presets, theme } from "../index.ts";
|
||||||
|
|
||||||
|
export interface SidebarHandle {
|
||||||
|
moveUp(): void;
|
||||||
|
moveDown(): void;
|
||||||
|
selectCurrent(): void;
|
||||||
|
focus(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function truncateWithEllipsis(text: string, maxWidth: number): string {
|
||||||
|
if (maxWidth <= 0) return "";
|
||||||
|
if (text.length <= maxWidth) return text;
|
||||||
|
return text.substring(0, Math.max(0, maxWidth - 1)) + "…";
|
||||||
|
}
|
||||||
|
|
||||||
|
function itemIndexAtScreenY(el: SelectRenderable, screenY: number): number | null {
|
||||||
|
const internals = el as unknown as { scrollOffset: number; linesPerItem: number };
|
||||||
|
if (!internals.linesPerItem) return null;
|
||||||
|
const localY = screenY - el.screenY;
|
||||||
|
if (localY < 0) return null;
|
||||||
|
const index = internals.scrollOffset + Math.floor(localY / internals.linesPerItem);
|
||||||
|
return index >= 0 && index < el.options.length ? index : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SidebarProps {
|
||||||
|
title?: string;
|
||||||
|
items: SelectOption[];
|
||||||
|
/** Value of the item to keep highlighted (e.g. the current route), if controlled. */
|
||||||
|
value?: string;
|
||||||
|
width?: number;
|
||||||
|
focused?: boolean;
|
||||||
|
onSelect: (option: SelectOption) => void;
|
||||||
|
onConfirm?: (option: SelectOption) => void;
|
||||||
|
ref?: (handle: SidebarHandle | null) => void;
|
||||||
|
|
||||||
|
accentColor?: ColorInput;
|
||||||
|
mutedColor?: ColorInput;
|
||||||
|
|
||||||
|
backgroundColor?: ColorInput;
|
||||||
|
textColor?: ColorInput;
|
||||||
|
focusedBackgroundColor?: ColorInput;
|
||||||
|
focusedTextColor?: ColorInput;
|
||||||
|
selectedBackgroundColor?: ColorInput;
|
||||||
|
selectedTextColor?: ColorInput;
|
||||||
|
descriptionColor?: ColorInput;
|
||||||
|
selectedDescriptionColor?: ColorInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BORDER_WIDTH = 2;
|
||||||
|
const ITEM_LEFT_PADDING = 1;
|
||||||
|
const ITEM_INDICATOR_WIDTH = 2;
|
||||||
|
|
||||||
|
export function Sidebar(props: SidebarProps) {
|
||||||
|
let select: SelectRenderable | undefined;
|
||||||
|
|
||||||
|
const contentWidth = () => Math.max(0, (props.width ?? 24) - BORDER_WIDTH);
|
||||||
|
const itemTextWidth = () => Math.max(0, contentWidth() - ITEM_LEFT_PADDING - ITEM_INDICATOR_WIDTH);
|
||||||
|
|
||||||
|
const displayTitle = () => (props.title ? truncateWithEllipsis(props.title, contentWidth()) : props.title);
|
||||||
|
|
||||||
|
const displayItems = () =>
|
||||||
|
props.items.map((item) => ({
|
||||||
|
...item,
|
||||||
|
name: truncateWithEllipsis(item.name, itemTextWidth()),
|
||||||
|
description: truncateWithEllipsis(item.description, itemTextWidth()),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const handleMouseDown = (event: MouseEvent) => {
|
||||||
|
if (!select) return;
|
||||||
|
const index = itemIndexAtScreenY(select, event.y);
|
||||||
|
if (index !== null) {
|
||||||
|
select.focus();
|
||||||
|
select.setSelectedIndex(index);
|
||||||
|
select.selectCurrent();
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseScroll = (event: MouseEvent) => {
|
||||||
|
if (!select || !event.scroll) return;
|
||||||
|
const steps = Math.max(1, event.scroll.delta);
|
||||||
|
if (event.scroll.direction === "down") select.moveDown(steps);
|
||||||
|
else if (event.scroll.direction === "up") select.moveUp(steps);
|
||||||
|
};
|
||||||
|
|
||||||
|
// A user effect, so it runs after the `options` prop has been applied.
|
||||||
|
createEffect(() => {
|
||||||
|
const index = props.items.findIndex((item) => item.value === props.value);
|
||||||
|
if (select && index !== -1 && select.getSelectedIndex() !== index) select.setSelectedIndex(index);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box
|
||||||
|
width={props.width ?? 24}
|
||||||
|
border
|
||||||
|
borderStyle={theme.borderStyle}
|
||||||
|
borderColor={props.focused ? (props.accentColor ?? theme.primary) : (props.mutedColor ?? theme.muted)}
|
||||||
|
flexDirection="column"
|
||||||
|
>
|
||||||
|
<Show when={props.title}>
|
||||||
|
<text attributes={TextAttributes.BOLD}>{displayTitle()}</text>
|
||||||
|
<text> </text>
|
||||||
|
</Show>
|
||||||
|
<select
|
||||||
|
ref={(el) => {
|
||||||
|
select = el ?? undefined;
|
||||||
|
props.ref?.(
|
||||||
|
el && {
|
||||||
|
moveUp: () => el.moveUp(),
|
||||||
|
moveDown: () => el.moveDown(),
|
||||||
|
selectCurrent: () => el.selectCurrent(),
|
||||||
|
focus: () => el.focus(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
flexGrow={1}
|
||||||
|
focused={props.focused}
|
||||||
|
showScrollIndicator
|
||||||
|
options={displayItems()}
|
||||||
|
onMouseDown={theme.mouse ? handleMouseDown : undefined}
|
||||||
|
onMouseScroll={theme.mouse ? handleMouseScroll : undefined}
|
||||||
|
backgroundColor={props.backgroundColor ?? "transparent"}
|
||||||
|
textColor={props.textColor ?? presets.select.textColor}
|
||||||
|
focusedBackgroundColor={props.focusedBackgroundColor ?? "transparent"}
|
||||||
|
focusedTextColor={props.focusedTextColor ?? presets.select.focusedTextColor}
|
||||||
|
selectedBackgroundColor={props.selectedBackgroundColor ?? presets.select.selectedBackgroundColor}
|
||||||
|
selectedTextColor={props.selectedTextColor ?? presets.select.selectedTextColor}
|
||||||
|
descriptionColor={props.descriptionColor ?? presets.select.descriptionColor}
|
||||||
|
selectedDescriptionColor={props.selectedDescriptionColor ?? presets.select.selectedDescriptionColor}
|
||||||
|
onChange={(_index, option) => {
|
||||||
|
if (!option) return;
|
||||||
|
props.onSelect(option);
|
||||||
|
}}
|
||||||
|
onSelect={(_index, option) => {
|
||||||
|
if (!option) return;
|
||||||
|
props.onConfirm?.(option);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import type { ColorInput } from "@opentui/core";
|
||||||
|
import { For } from "solid-js";
|
||||||
|
import { theme } from "../index.ts";
|
||||||
|
|
||||||
|
const BARS = ["▁", "▃", "▅", "▇"] as const;
|
||||||
|
|
||||||
|
export interface SignalBarsProps {
|
||||||
|
signal: number;
|
||||||
|
filledColor?: ColorInput;
|
||||||
|
emptyColor?: ColorInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SignalBars(props: SignalBarsProps) {
|
||||||
|
const level = () => Math.floor((props.signal * BARS.length) / 100);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box flexDirection="row">
|
||||||
|
<For each={BARS}>
|
||||||
|
{(bar, i) => <text fg={i() < level() ? props.filledColor : (props.emptyColor ?? theme.muted)}>{bar}</text>}
|
||||||
|
</For>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import type { ColorInput } from "@opentui/core";
|
||||||
|
import "opentui-spinner/solid";
|
||||||
|
import type { ColorGenerator, SpinnerOptions } from "opentui-spinner";
|
||||||
|
import { theme } from "../index.ts";
|
||||||
|
|
||||||
|
export interface SpinnerProps {
|
||||||
|
name?: SpinnerOptions["name"];
|
||||||
|
frames?: string[];
|
||||||
|
interval?: number;
|
||||||
|
autoplay?: boolean;
|
||||||
|
color?: ColorInput | ColorGenerator;
|
||||||
|
backgroundColor?: ColorInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Spinner(props: SpinnerProps) {
|
||||||
|
return (
|
||||||
|
<spinner
|
||||||
|
name={props.name}
|
||||||
|
frames={props.frames}
|
||||||
|
interval={props.interval}
|
||||||
|
autoplay={props.autoplay ?? !theme.reducedMotion}
|
||||||
|
color={props.color ?? theme.primary}
|
||||||
|
backgroundColor={props.backgroundColor}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import type { ParentProps } from "solid-js";
|
||||||
|
import { theme } from "../index.ts";
|
||||||
|
import { Box } from "./Box.tsx";
|
||||||
|
import { type TabItem, Tabs } from "./Tabs.tsx";
|
||||||
|
|
||||||
|
export interface TabbedPanelProps extends ParentProps {
|
||||||
|
items: TabItem[];
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
focused?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** <Tabs/> on top of a bordered panel whose top edge is drawn by the tabs. */
|
||||||
|
export function TabbedPanel(props: TabbedPanelProps) {
|
||||||
|
return (
|
||||||
|
<box flexDirection="column" flexGrow={1}>
|
||||||
|
<Tabs items={props.items} value={props.value} onChange={props.onChange} focused={props.focused} />
|
||||||
|
<Box
|
||||||
|
flexGrow={1}
|
||||||
|
border={["left", "right", "bottom"]}
|
||||||
|
borderColor={props.focused ? theme.primary : theme.muted}
|
||||||
|
flexDirection="column"
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</Box>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
import { type BorderCharacters, type BoxRenderable, type ColorInput, BorderChars, CliRenderEvents, TextAttributes } from "@opentui/core";
|
||||||
|
import { onResize, useRenderer } from "@opentui/solid";
|
||||||
|
import { createMemo, createSignal, Index } from "solid-js";
|
||||||
|
import { theme } from "../index.ts";
|
||||||
|
|
||||||
|
export interface TabItem {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TabsProps {
|
||||||
|
items: TabItem[];
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
focused?: boolean;
|
||||||
|
accentColor?: ColorInput;
|
||||||
|
mutedColor?: ColorInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stepTabValue(items: TabItem[], value: string, delta: number): string {
|
||||||
|
const index = items.findIndex((t) => t.value === value);
|
||||||
|
if (index === -1) return value;
|
||||||
|
return items[(index + delta + items.length) % items.length]!.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function innerWidth(label: string): number {
|
||||||
|
return [...label].length + 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
function segmentWidth(label: string): number {
|
||||||
|
return innerWidth(label) + 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
function topEdge(chars: BorderCharacters, label: string): string {
|
||||||
|
return `${chars.topLeft}${chars.horizontal.repeat(innerWidth(label))}${chars.topRight}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bottomEdge(chars: BorderCharacters, label: string, isFirst: boolean, isLast: boolean, isActive: boolean): string {
|
||||||
|
const left = isFirst ? (isActive ? chars.vertical : chars.leftT) : isActive ? chars.bottomRight : chars.bottomT;
|
||||||
|
const right = isLast ? (isActive ? chars.vertical : chars.rightT) : isActive ? chars.bottomLeft : chars.bottomT;
|
||||||
|
const fill = isActive ? " " : chars.horizontal;
|
||||||
|
return left + fill.repeat(innerWidth(label)) + right;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildBottomRow(
|
||||||
|
chars: BorderCharacters,
|
||||||
|
segments: { label: string; isActive: boolean }[],
|
||||||
|
available: number,
|
||||||
|
): string {
|
||||||
|
const tabsWidth = segments.reduce((sum, s) => sum + segmentWidth(s.label), 0);
|
||||||
|
const remainder = available - tabsWidth;
|
||||||
|
const extending = remainder > 0;
|
||||||
|
|
||||||
|
const tabs = segments
|
||||||
|
.map((s, i) => bottomEdge(chars, s.label, i === 0, i === segments.length - 1 && !extending, s.isActive))
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
return extending ? tabs + chars.horizontal.repeat(remainder - 1) + chars.topRight : tabs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fitCount(items: TabItem[], start: number, available: number): number {
|
||||||
|
const hiddenBefore = start;
|
||||||
|
let used = 0;
|
||||||
|
let count = 0;
|
||||||
|
for (let i = start; i < items.length; i++) {
|
||||||
|
const width = segmentWidth(items[i]!.label);
|
||||||
|
const hiddenAfter = items.length - start - count - 1;
|
||||||
|
const totalHidden = hiddenBefore + hiddenAfter;
|
||||||
|
const reserve = totalHidden > 0 ? segmentWidth(`+${totalHidden}`) : 0;
|
||||||
|
if (used + width + reserve > available) break;
|
||||||
|
used += width;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
return Math.max(count, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Tabs(props: TabsProps) {
|
||||||
|
const renderer = useRenderer();
|
||||||
|
const [width, setWidth] = createSignal(0);
|
||||||
|
let container: BoxRenderable | undefined;
|
||||||
|
const measure = () => {
|
||||||
|
if (!container) return;
|
||||||
|
setWidth(container.width);
|
||||||
|
};
|
||||||
|
const remeasureNextFrame = () => renderer.once(CliRenderEvents.FRAME, measure);
|
||||||
|
onResize(remeasureNextFrame);
|
||||||
|
|
||||||
|
const segments = createMemo(() => {
|
||||||
|
const items = props.items;
|
||||||
|
const activeIndex = Math.max(
|
||||||
|
0,
|
||||||
|
items.findIndex((t) => t.value === props.value),
|
||||||
|
);
|
||||||
|
|
||||||
|
let start = 0;
|
||||||
|
let count = fitCount(items, start, width());
|
||||||
|
if (activeIndex >= start + count) {
|
||||||
|
start = activeIndex;
|
||||||
|
count = fitCount(items, start, width());
|
||||||
|
}
|
||||||
|
|
||||||
|
const shownItems = items.slice(start, start + count);
|
||||||
|
const shown = shownItems.map((item) => ({
|
||||||
|
value: item.value as string | undefined,
|
||||||
|
label: item.label,
|
||||||
|
isActive: item.value === props.value,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const hidden = items.length - count;
|
||||||
|
if (hidden > 0) {
|
||||||
|
const used = shownItems.reduce((sum, item) => sum + segmentWidth(item.label), 0);
|
||||||
|
const tag = `+${hidden}`;
|
||||||
|
if (shown.length === 0 || used + segmentWidth(tag) <= width()) {
|
||||||
|
shown.push({ value: undefined, label: tag, isActive: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return shown;
|
||||||
|
});
|
||||||
|
|
||||||
|
const accent = () => props.accentColor ?? theme.primary;
|
||||||
|
const muted = () => props.mutedColor ?? theme.muted;
|
||||||
|
|
||||||
|
const borderColor = () => (props.focused ? accent() : muted());
|
||||||
|
const chars = BorderChars[theme.borderStyle];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box
|
||||||
|
flexDirection="column"
|
||||||
|
height={3}
|
||||||
|
ref={(el) => {
|
||||||
|
container = el;
|
||||||
|
remeasureNextFrame();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<text selectable={false} fg={borderColor()}>
|
||||||
|
{segments()
|
||||||
|
.map((s) => topEdge(chars, s.label))
|
||||||
|
.join("")}
|
||||||
|
</text>
|
||||||
|
<box flexDirection="row">
|
||||||
|
<Index each={segments()}>
|
||||||
|
{(s) => (
|
||||||
|
<>
|
||||||
|
<text selectable={false} fg={borderColor()}>
|
||||||
|
{chars.vertical}
|
||||||
|
</text>
|
||||||
|
<text
|
||||||
|
selectable={false}
|
||||||
|
fg={s().isActive ? accent() : muted()}
|
||||||
|
attributes={s().isActive ? TextAttributes.BOLD : undefined}
|
||||||
|
onMouseDown={theme.mouse && s().value !== undefined ? () => props.onChange(s().value!) : undefined}
|
||||||
|
>
|
||||||
|
{` ${s().label} `}
|
||||||
|
</text>
|
||||||
|
<text selectable={false} fg={borderColor()}>
|
||||||
|
{chars.vertical}
|
||||||
|
</text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Index>
|
||||||
|
</box>
|
||||||
|
<text selectable={false} fg={borderColor()}>
|
||||||
|
{buildBottomRow(chars, segments(), width())}
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { type ColorInput, TextAttributes } from "@opentui/core";
|
||||||
|
import { Show } from "solid-js";
|
||||||
|
import { theme } from "../index.ts";
|
||||||
|
import { Badge } from "./Badge.tsx";
|
||||||
|
|
||||||
|
export interface ToggleProps {
|
||||||
|
on: boolean;
|
||||||
|
onToggle?: () => void;
|
||||||
|
onColor?: ColorInput;
|
||||||
|
offColor?: ColorInput;
|
||||||
|
withNerdfont?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Toggle(props: ToggleProps) {
|
||||||
|
const onColor = () => props.onColor ?? theme.success;
|
||||||
|
const offColor = () => props.offColor ?? theme.muted;
|
||||||
|
const nerdFonts = () => props.withNerdfont ?? theme.nerdFonts;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box onMouseDown={theme.mouse ? props.onToggle : undefined}>
|
||||||
|
<Show
|
||||||
|
when={nerdFonts()}
|
||||||
|
fallback={
|
||||||
|
<text fg={props.on ? onColor() : offColor()} attributes={TextAttributes.BOLD}>
|
||||||
|
{props.on ? "on" : "off"}
|
||||||
|
</text>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Badge label={props.on ? " ●" : "● "} color={props.on ? onColor() : offColor()} withNerdfont />
|
||||||
|
</Show>
|
||||||
|
</box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { HelpBar, type HelpBarProps } from "./HelpBar.tsx";
|
||||||
|
export { HelpModal, type HelpModalProps } from "./HelpModal.tsx";
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
export { Badge, type BadgeProps } from "./Badge.tsx";
|
||||||
|
export { Box, type BoxProps } from "./Box.tsx";
|
||||||
|
export { KeyValue, type KeyValueProps } from "./KeyValue.tsx";
|
||||||
|
export { ListRow, type ListRowProps, type RowTone } from "./ListRow.tsx";
|
||||||
|
export { MinSizeGuard, type MinSizeGuardProps } from "./MinSizeGuard.tsx";
|
||||||
|
export { Modal, type ModalBinding, type ModalCommand, type ModalProps } from "./Modal.tsx";
|
||||||
|
export { NotificationHost, type NotificationHostProps } from "./NotificationHost.tsx";
|
||||||
|
export { Pane, usePane, type PaneHandle, type PaneProps } from "./Pane.tsx";
|
||||||
|
export { ProgressBar, type ProgressBarProps } from "./ProgressBar.tsx";
|
||||||
|
export { Sidebar, type SidebarHandle, type SidebarProps } from "./Sidebar.tsx";
|
||||||
|
export { SignalBars, type SignalBarsProps } from "./SignalBars.tsx";
|
||||||
|
export { TabbedPanel, type TabbedPanelProps } from "./TabbedPanel.tsx";
|
||||||
|
export { stepTabValue, Tabs, type TabItem, type TabsProps } from "./Tabs.tsx";
|
||||||
|
export { Toggle, type ToggleProps } from "./Toggle.tsx";
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const COLOR_KEYS = [
|
||||||
|
"base00",
|
||||||
|
"base01",
|
||||||
|
"base02",
|
||||||
|
"base03",
|
||||||
|
"base04",
|
||||||
|
"base05",
|
||||||
|
"base06",
|
||||||
|
"base07",
|
||||||
|
"base08",
|
||||||
|
"base09",
|
||||||
|
"base0a",
|
||||||
|
"base0b",
|
||||||
|
"base0c",
|
||||||
|
"base0d",
|
||||||
|
"base0e",
|
||||||
|
"base0f",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type ColorKey = (typeof COLOR_KEYS)[number];
|
||||||
|
|
||||||
|
export const OPENTUI_BORDER_STYLES = ["single", "double", "rounded", "heavy"] as const;
|
||||||
|
|
||||||
|
export type OpenTUIBorderStyle = (typeof OPENTUI_BORDER_STYLES)[number];
|
||||||
|
|
||||||
|
const hexColor = z.string().regex(/^#?[0-9a-fA-F]{6}$/, 'expected a hex color like "#1e1e2e"');
|
||||||
|
|
||||||
|
const colorsShape = {} as Record<ColorKey, z.ZodOptional<z.ZodString>>;
|
||||||
|
for (const key of COLOR_KEYS) colorsShape[key] = hexColor.optional();
|
||||||
|
|
||||||
|
// Not .strict(): ~/.config/ilovetui/config.yaml is shared by every
|
||||||
|
// ilovetui-based TUI, and some of them read extra keys of their own from it
|
||||||
|
// (e.g. `layout_border`). Unknown keys are stripped, known ones validated.
|
||||||
|
export const ThemeConfigSchema = z.object({
|
||||||
|
nerd_fonts: z.boolean().optional(),
|
||||||
|
emoji_fonts: z.boolean().optional(),
|
||||||
|
reduced_motion: z.boolean().optional(),
|
||||||
|
mouse: z.boolean().optional(),
|
||||||
|
border: z.enum(OPENTUI_BORDER_STYLES).optional(),
|
||||||
|
colors: z.object(colorsShape).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ConfigYAML = z.infer<typeof ThemeConfigSchema>;
|
||||||
|
export type ColorsYAML = NonNullable<ConfigYAML["colors"]>;
|
||||||
|
|
||||||
|
export type ResolvedColors = Record<ColorKey, string>;
|
||||||
|
|
||||||
|
export interface ResolvedConfig {
|
||||||
|
nerdFonts: boolean;
|
||||||
|
emojiFonts: boolean;
|
||||||
|
reducedMotion: boolean;
|
||||||
|
mouse: boolean;
|
||||||
|
border: OpenTUIBorderStyle;
|
||||||
|
colors: ResolvedColors;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Later layers win, key by key (colors included).
|
||||||
|
export function layerConfigs(layers: ConfigYAML[]): ConfigYAML {
|
||||||
|
const out: ConfigYAML = {};
|
||||||
|
for (const layer of layers) {
|
||||||
|
const { colors, ...rest } = layer;
|
||||||
|
Object.assign(out, rest);
|
||||||
|
if (colors) out.colors = { ...out.colors, ...colors };
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveConfig(config: ConfigYAML): ResolvedConfig {
|
||||||
|
const colors = {} as ResolvedColors;
|
||||||
|
for (const key of COLOR_KEYS) colors[key] = normalizeColor(config.colors?.[key] ?? "");
|
||||||
|
return {
|
||||||
|
nerdFonts: config.nerd_fonts ?? false,
|
||||||
|
emojiFonts: config.emoji_fonts ?? false,
|
||||||
|
reducedMotion: config.reduced_motion ?? false,
|
||||||
|
mouse: config.mouse ?? true,
|
||||||
|
border: config.border ?? "rounded",
|
||||||
|
colors,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeColor(hex: string): string {
|
||||||
|
const trimmed = hex.trim();
|
||||||
|
if (trimmed && !trimmed.startsWith("#")) return `#${trimmed}`;
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user