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/
|
||||
CLAUDE.md
|
||||
result/
|
||||
.pre-commit-config.yaml
|
||||
node_modules/
|
||||
|
||||
@@ -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
|
||||
|
||||
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)
|
||||
```bash
|
||||
bun add github:anotherhadi/ilovetui
|
||||
```
|
||||
|
||||
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
|
||||
go get github.com/anotherhadi/ilovetui
|
||||
```ts
|
||||
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 |
|
||||
| ------------ | ------ | --------------------------------------- |
|
||||
| `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 |
|
||||
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.
|
||||
|
||||
The default theme is `./default.yaml`. Copy it and edit to customize:
|
||||
## Focus and keymap scoping
|
||||
|
||||
```sh
|
||||
mkdir -p ~/.config/ilovetui
|
||||
cp $(go env GOPATH)/pkg/mod/github.com/anotherhadi/ilovetui*/default.yaml ~/.config/ilovetui/config.yaml
|
||||
```
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
# 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:
|
||||
base00: "#110F12" # Background
|
||||
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