Author SHA1 Message Date
dependabot[bot] 49aeca68f1 Bump golang.org/x/net from 0.39.0 to 0.55.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.39.0 to 0.55.0.
- [Commits](https://github.com/golang/net/compare/v0.39.0...v0.55.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.55.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-07-03 04:12:30 +00:00
Hadi f3a1877832 edit flake
Signed-off-by: Hadi <[email protected]>
2026-05-26 17:55:09 +02:00
114 changed files with 1091 additions and 4444 deletions
+1
View File
@@ -0,0 +1 @@
use flake
+24
View File
@@ -0,0 +1,24 @@
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:"
+10
View File
@@ -0,0 +1,10 @@
# 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
View File
@@ -0,0 +1 @@
ko_fi: anotherhadi
+28
View File
@@ -0,0 +1,28 @@
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 }}
+4 -1
View File
@@ -1 +1,4 @@
node_modules/
.claude/
CLAUDE.md
result/
.pre-commit-config.yaml
+21
View File
@@ -0,0 +1,21 @@
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.
+81 -15
View File
@@ -1,29 +1,95 @@
# I Love TUI
# Ilovetui
Shared OpenTUI/Solid theme, components, and config helpers for my TUIs.
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).
## Install
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.
```bash
bun add github:anotherhadi/ilovetui
## 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)
```
Peer dependencies, install whichever you actually use: `@opentui/core`, `@opentui/solid`, `@opentui/keymap`, `solid-js`, `opentui-spinner`.
No setup required — just import and use.
## Usage
## Installation
```ts
import { theme, presets } from "ilovetui";
import "ilovetui/solid";
```sh
go get github.com/anotherhadi/ilovetui
```
Entry points: `.`, `./solid`, `./context`, `./project-config`, `./keymap`, `./components`, `./components/help`, `./components/spinner`, `./components/section-header`. See the source for what each exports.
## Theme
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.
The theme follows the [Base16](https://github.com/tinted-theming/home) standard (16 colors). The library exposes both the raw palette and semantic aliases:
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.
| 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 |
## Focus and keymap scoping
The default theme is `./default.yaml`. Copy it and edit to customize:
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.
```sh
mkdir -p ~/.config/ilovetui
cp $(go env GOPATH)/pkg/mod/github.com/anotherhadi/ilovetui*/default.yaml ~/.config/ilovetui/config.yaml
```
Or let your app write it on first run:
```go
ilovetui.WriteDefaultConfig(ilovetui.DefaultConfigPath())
```
## Pre-built styles
`S` ships with a few ready-to-use lipgloss styles:
| Field | Description |
| ---------------- | ---------------------------------------- |
| `S.Bold` | Bold text |
| `S.Faint` | Muted / dimmed text |
| `S.Panel` | Rounded border, unfocused (Subtle color) |
| `S.PanelFocused` | Rounded border, focused (Primary color) |
## Helpers
```go
// Inner usable height of a bordered panel with outer height h
inner := ilovetui.ContentHeight(h)
// Render a box with a title embedded in the top border
box := ilovetui.RenderWithTitle(ilovetui.S.PanelFocused, "Header", content, w, h)
```
## API
```go
ilovetui.Init() // Reload from default config path
ilovetui.InitFrom(path string) // Reload from a custom path
ilovetui.InitFromBytes(data []byte) // Parse raw YAML
ilovetui.DefaultConfigPath() string // ~/.config/ilovetui/config.yaml
ilovetui.WriteDefaultConfig(path) // Write default config if missing
```
## Projects using ilovetui
- [anotherhadi/spilltea](https://github.com/anotherhadi/spilltea): A minimal, terminal-based HTTP(S) proxy for pentesters and CTF players. Think Burp Suite or Caido, but entirely in your terminal.
- [anotherhadi/usbguard-tui](https://github.com/anotherhadi/usbguard-tui): A terminal UI for managing USB devices via usbguard. TUI built with golang & bubbletea.
- [anotherhadi/jwt-tui](https://github.com/anotherhadi/jwt-tui): A terminal UI for inspecting, editing, and signing JSON Web Tokens (JWTs).
+44
View File
@@ -0,0 +1,44 @@
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)
}
-273
View File
@@ -1,273 +0,0 @@
{
"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=="],
}
}
+96
View File
@@ -0,0 +1,96 @@
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")
}
+51
View File
@@ -0,0 +1,51 @@
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),
}
}
+1 -17
View File
@@ -1,21 +1,5 @@
# ilovetui default config
# ilovetui default theme
# 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
-25
View File
@@ -1,25 +0,0 @@
// @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 };
-21
View File
@@ -1,21 +0,0 @@
// @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 };
-23
View File
@@ -1,23 +0,0 @@
// @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 };
-79
View File
@@ -1,79 +0,0 @@
// @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 };
-121
View File
@@ -1,121 +0,0 @@
// @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 };
-44
View File
@@ -1,44 +0,0 @@
// @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 };
-9
View File
@@ -1,9 +0,0 @@
// @bun
// src/context/help.ts
import { createSignal } from "solid-js";
var [helpOpen2, setHelpOpen] = createSignal(false);
function toggleHelp2() {
setHelpOpen((open) => !open);
}
export { helpOpen2, toggleHelp2 };
-87
View File
@@ -1,87 +0,0 @@
// @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 };
-26
View File
@@ -1,26 +0,0 @@
// @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 };
-35
View File
@@ -1,35 +0,0 @@
// @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 };
-65
View File
@@ -1,65 +0,0 @@
// @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 };
-79
View File
@@ -1,79 +0,0 @@
// @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 };
-54
View File
@@ -1,54 +0,0 @@
// @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 };
-61
View File
@@ -1,61 +0,0 @@
// @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 };
-155
View File
@@ -1,155 +0,0 @@
// @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 };
-71
View File
@@ -1,71 +0,0 @@
// @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 };
-140
View File
@@ -1,140 +0,0 @@
// @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 };
-32
View File
@@ -1,32 +0,0 @@
// @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 };
-83
View File
@@ -1,83 +0,0 @@
// @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 };
-42
View File
@@ -1,42 +0,0 @@
// @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 };
-168
View File
@@ -1,168 +0,0 @@
// @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 };
-52
View File
@@ -1,52 +0,0 @@
// @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 };
-10
View File
@@ -1,10 +0,0 @@
// @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 };
-44
View File
@@ -1,44 +0,0 @@
// @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 };
-58
View File
@@ -1,58 +0,0 @@
// @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 };
-12
View File
@@ -1,12 +0,0 @@
// @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
};
-12
View File
@@ -1,12 +0,0 @@
// @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
};
-12
View File
@@ -1,12 +0,0 @@
// @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
};
-14
View File
@@ -1,14 +0,0 @@
// @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
};
-12
View File
@@ -1,12 +0,0 @@
// @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
};
-12
View File
@@ -1,12 +0,0 @@
// @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
};
-12
View File
@@ -1,12 +0,0 @@
// @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
};
-12
View File
@@ -1,12 +0,0 @@
// @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
};
-12
View File
@@ -1,12 +0,0 @@
// @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
};
-10
View File
@@ -1,10 +0,0 @@
// @bun
import {
Pane2,
usePane2
} from "../chunk-vazj633p.js";
import"../chunk-59dzyjgd.js";
export {
Pane2 as Pane,
usePane2 as usePane
};
-12
View File
@@ -1,12 +0,0 @@
// @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
};
-81
View File
@@ -1,81 +0,0 @@
// @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
};
-12
View File
@@ -1,12 +0,0 @@
// @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
};
-12
View File
@@ -1,12 +0,0 @@
// @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
};
-12
View File
@@ -1,12 +0,0 @@
// @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
};
-14
View File
@@ -1,14 +0,0 @@
// @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
};
-14
View File
@@ -1,14 +0,0 @@
// @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
};
-13
View File
@@ -1,13 +0,0 @@
// @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
};
-18
View File
@@ -1,18 +0,0 @@
// @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
};
-69
View File
@@ -1,69 +0,0 @@
// @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
};
-17
View File
@@ -1,17 +0,0 @@
// @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
};
-9
View File
@@ -1,9 +0,0 @@
// @bun
import {
isInside2,
useFocusWithin2
} from "../chunk-59dzyjgd.js";
export {
isInside2 as isInside,
useFocusWithin2 as useFocusWithin
};
-9
View File
@@ -1,9 +0,0 @@
// @bun
import {
helpOpen2,
toggleHelp2
} from "../chunk-ee2qaswx.js";
export {
helpOpen2 as helpOpen,
toggleHelp2 as toggleHelp
};
-7
View File
@@ -1,7 +0,0 @@
// @bun
import {
createSimpleContext2
} from "../chunk-jza406qg.js";
export {
createSimpleContext2 as createSimpleContext
};
-27
View File
@@ -1,27 +0,0 @@
// @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
};
-11
View File
@@ -1,11 +0,0 @@
// @bun
import {
notifications2,
notify2,
dismiss2
} from "../chunk-6ggk5fg7.js";
export {
dismiss2 as dismiss,
notifications2 as notifications,
notify2 as notify
};
-23
View File
@@ -1,23 +0,0 @@
// @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
};
-27
View File
@@ -1,27 +0,0 @@
// @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
};
-7
View File
@@ -1,7 +0,0 @@
// @bun
import {
buildPresets2
} from "./chunk-yyg967q8.js";
export {
buildPresets2 as buildPresets
};
-117
View File
@@ -1,117 +0,0 @@
// @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
};
-48
View File
@@ -1,48 +0,0 @@
// @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 }))
});
-7
View File
@@ -1,7 +0,0 @@
// @bun
import {
configPath2
} from "./chunk-xykm10y9.js";
export {
configPath2 as configPath
};
Generated
+87
View File
@@ -0,0 +1,87 @@
{
"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
}
+30
View File
@@ -0,0 +1,30 @@
{
description = "A minimal Go library that provides a shared Base16color theme for terminal UIs built with bubbletea and lipgloss.";
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
View File
@@ -0,0 +1,242 @@
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> ",
},
}
}
+40
View File
@@ -0,0 +1,40 @@
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.55.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/text v0.37.0 // indirect
)
+79
View File
@@ -0,0 +1,79 @@
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.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
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
View File
@@ -0,0 +1,116 @@
// 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")
}
+22
View File
@@ -0,0 +1,22 @@
{
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;
}
-61
View File
@@ -1,61 +0,0 @@
{
"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"
}
}
-25
View File
@@ -1,25 +0,0 @@
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/`);
-40
View File
@@ -1,40 +0,0 @@
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>
);
}
-12
View File
@@ -1,12 +0,0 @@
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} />;
}
-94
View File
@@ -1,94 +0,0 @@
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>
);
}
-57
View File
@@ -1,57 +0,0 @@
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>
);
}
-23
View File
@@ -1,23 +0,0 @@
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>
);
}
-49
View File
@@ -1,49 +0,0 @@
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>
);
}
-41
View File
@@ -1,41 +0,0 @@
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>
</>
);
}
-125
View File
@@ -1,125 +0,0 @@
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>
);
}
-51
View File
@@ -1,51 +0,0 @@
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>
);
}
-48
View File
@@ -1,48 +0,0 @@
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;
}
-51
View File
@@ -1,51 +0,0 @@
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>
);
}
-40
View File
@@ -1,40 +0,0 @@
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>
);
}
-143
View File
@@ -1,143 +0,0 @@
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>
);
}
-23
View File
@@ -1,23 +0,0 @@
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>
);
}
-26
View File
@@ -1,26 +0,0 @@
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}
/>
);
}
-28
View File
@@ -1,28 +0,0 @@
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>
);
}
-167
View File
@@ -1,167 +0,0 @@
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>
);
}
-33
View File
@@ -1,33 +0,0 @@
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>
);
}
-2
View File
@@ -1,2 +0,0 @@
export { HelpBar, type HelpBarProps } from "./HelpBar.tsx";
export { HelpModal, type HelpModalProps } from "./HelpModal.tsx";
-14
View File
@@ -1,14 +0,0 @@
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";
-87
View File
@@ -1,87 +0,0 @@
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