26 Commits
Author SHA1 Message Date
Hadi 18668f4aea fixes & update
Signed-off-by: Hadi <[email protected]>
2026-10-04 18:46:49 +02:00
Hadi 238d764b0a ellipsis
Signed-off-by: Hadi <[email protected]>
2026-09-07 14:36:17 +02:00
Hadi b8dc56adb1 fix notifications background
Signed-off-by: Hadi <[email protected]>
2026-09-07 10:47:22 +02:00
Hadi ad619d23d5 fix focus issue
Signed-off-by: Hadi <[email protected]>
2026-09-07 10:16:22 +02:00
Hadi fc5e87c028 rebuild
Signed-off-by: Hadi <[email protected]>
2026-09-07 10:09:59 +02:00
Hadi 4dada889cb change sidebar click behavior
Signed-off-by: Hadi <[email protected]>
2026-09-07 10:08:42 +02:00
Hadi 080ed34fe4 edit width & rebuild
Signed-off-by: Hadi <[email protected]>
2026-09-03 20:46:48 +02:00
Hadi 1c1d8ead1f add maxheight & rebuild
Signed-off-by: Hadi <[email protected]>
2026-09-03 20:42:48 +02:00
Hadi 3d6bcdf0f2 fix modal & rebuild
Signed-off-by: Hadi <[email protected]>
2026-09-03 20:21:04 +02:00
Hadi 280507f7e0 add preset & rebuild
Signed-off-by: Hadi <[email protected]>
2026-09-03 16:45:05 +02:00
Hadi 6d858380c7 fix border style
Signed-off-by: Hadi <[email protected]>
2026-09-02 17:13:05 +02:00
Hadi edf72ab183 rebuild
Signed-off-by: Hadi <[email protected]>
2026-09-02 16:44:33 +02:00
Hadi a1d368088c fix tabs size
Signed-off-by: Hadi <[email protected]>
2026-09-02 16:44:26 +02:00
Hadi 20b4e7a0a5 rebuild
Signed-off-by: Hadi <[email protected]>
2026-09-02 16:07:22 +02:00
Hadi 21e2039433 edit tab length
Signed-off-by: Hadi <[email protected]>
2026-09-02 16:07:14 +02:00
Hadi 9158a070f3 rebuild
Signed-off-by: Hadi <[email protected]>
2026-09-02 15:07:42 +02:00
Hadi 5e66c5b303 edit icons
Signed-off-by: Hadi <[email protected]>
2026-09-02 15:07:39 +02:00
Hadi b329bdc074 multiple fixes
Signed-off-by: Hadi <[email protected]>
2026-09-01 16:35:10 +02:00
Hadi 038842878a precompile
Signed-off-by: Hadi <[email protected]>
2026-09-01 10:57:30 +02:00
Hadi 12305ba1f2 rewrite bubbletea -> opentui 2026-09-01 10:23:42 +02:00
Hadi 000f9997fd change funding url
Signed-off-by: Hadi <[email protected]>
2026-08-28 15:03:37 +02:00
Hadi df7bd3df0a add SignalBars
Signed-off-by: Hadi <[email protected]>
2026-08-24 21:54:12 +02:00
Hadi 68bf97efd6 add basic app components
Signed-off-by: Hadi <[email protected]>
2026-08-24 18:57:11 +02:00
Hadi 1bc3ee2821 add border layout config
Signed-off-by: Hadi <[email protected]>
2026-08-24 14:08:36 +02:00
Hadi 2fd1861339 Change overlay display to allow bubblezone to work
Signed-off-by: Hadi <[email protected]>
2026-08-23 17:42:33 +02:00
Hadi b22ce7cbf7 minimum window size package
Signed-off-by: Hadi <[email protected]>
2026-08-23 10:50:15 +02:00
157 changed files with 4438 additions and 3517 deletions
-1
View File
@@ -1 +0,0 @@
use flake
-24
View File
@@ -1,24 +0,0 @@
version: 2
before:
hooks:
- go mod tidy
builds:
- skip: true
archives:
- formats:
- tar.gz
name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}"
checksum:
name_template: checksums.txt
changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
- "^ci:"
-10
View File
@@ -1,10 +0,0 @@
# Contributing
Everybody is invited and welcome to contribute. There is a lot to do... Check the issues!
The process is straight-forward.
- Read [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#best-practices-for-faster-reviews) by Kubernetes (but skip step 0 and 1)
- Fork this git repository
- Write your changes (bug fixes, new features, ...).
- Create a Pull Request against the main branch.
-1
View File
@@ -1 +0,0 @@
ko_fi: anotherhadi
-28
View File
@@ -1,28 +0,0 @@
name: Release
on:
push:
tags:
- "v*"
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- uses: goreleaser/goreleaser-action@v6
with:
version: "~> v2"
args: release --clean --config .github/.goreleaser.yaml
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+1 -4
View File
@@ -1,4 +1 @@
.claude/ node_modules/
CLAUDE.md
result/
.pre-commit-config.yaml
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2026 Hadi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+16 -23
View File
@@ -1,36 +1,29 @@
# I Love TUI # I Love TUI
A shared [Base16](https://github.com/tinted-theming/home) theme, a themed wrapper around every official component, and a small set of custom Bubble Tea components, in one Go module, so every TUI built with it shares one config file and looks consistent. Shared OpenTUI/Solid theme, components, and config helpers for my TUIs.
## Install ## Install
```sh ```bash
go get github.com/anotherhadi/ilovetui bun add github:anotherhadi/ilovetui
``` ```
## Packages Peer dependencies, install whichever you actually use: `@opentui/core`, `@opentui/solid`, `@opentui/keymap`, `solid-js`, `opentui-spinner`.
- [`style`](style/README.md): the theme itself. Colors, pre-built panel styles, config loading. ## Usage
- [`bubbles`](bubbles/README.md): themed constructors for official `bubbles/v2` components (`help`, `textarea`, `textinput`, `viewport`, ...).
- [`tabs`](tabs/README.md), [`modal`](modal/README.md), [`drawer`](drawer/README.md), [`notification`](notification/README.md), [`helpbar`](helpbar/README.md): custom components not found in the official `bubbles` library, styled from the same theme.
Each package has its own README and a runnable example under `examples/<package>`. ```ts
import { theme, presets } from "ilovetui";
## Quick start import "ilovetui/solid";
On import, `style` automatically loads the user's theme from `~/.config/ilovetui/config.yaml` (embedded default as fallback), exposed as the package-level `S`:
```go
import "github.com/anotherhadi/ilovetui/style"
s := lipgloss.NewStyle().Foreground(style.S.Primary)
box := style.RenderWithTitle(style.S.PanelFocused, "Title", content, w, h)
``` ```
No setup required. See [`style/README.md`](style/README.md) for config details. Entry points: `.`, `./solid`, `./context`, `./project-config`, `./keymap`, `./components`, `./components/help`, `./components/spinner`, `./components/section-header`. See the source for what each exports.
## Projects using ilovetui 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.
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.
## Focus and keymap scoping
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.
- [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).
-33
View File
@@ -1,33 +0,0 @@
package app
import tea "charm.land/bubbletea/v2"
type QuitMsg struct{}
func Quit() tea.Cmd {
return func() tea.Msg { return QuitMsg{} }
}
type FocusMsg struct{}
func Focus() tea.Cmd {
return func() tea.Msg { return FocusMsg{} }
}
type BlurMsg struct{}
func Blur() tea.Cmd {
return func() tea.Msg { return BlurMsg{} }
}
type RequestFocusMsg struct{}
func RequestFocus() tea.Cmd {
return func() tea.Msg { return RequestFocusMsg{} }
}
type SetTitleMsg struct{ Title string }
func SetTitle(title string) tea.Cmd {
return func() tea.Msg { return SetTitleMsg{Title: title} }
}
-17
View File
@@ -1,17 +0,0 @@
# bubbles
Themed constructors for every official `bubbles/v2` component: `help`, `textarea`, `textinput`,
`list`, `table`, `filepicker`, `spinner`, `progress`, `paginator`, `viewport`. Each wraps the
official `New`, then applies `style.S` colors on top.
- `spinner`, `table`, `progress` forward any `opts` to the upstream `New` before the theme is
applied, so the theme's colors always win over anything conflicting in `opts`.
- `ViewportView` renders a `viewport.Model` with a themed scrollbar thumb in the left gutter,
shown only when content overflows.
- `NewList`/`NewDefaultDelegate` theme both the list chrome and the selection/filter colors of its
default delegate.
Custom components in this repo (`tabs`, `modal`, `drawer`, `notification`, `helpbar`) build any
official component they need through here, never through `charm.land/bubbles/v2` directly.
See `examples/bubbles`.
-1
View File
@@ -1 +0,0 @@
package bubbles
-25
View File
@@ -1,25 +0,0 @@
package bubbles
import (
"charm.land/bubbles/v2/filepicker"
"github.com/anotherhadi/ilovetui/style"
)
func NewFilePicker() filepicker.Model {
f := filepicker.New()
s := filepicker.DefaultStyles()
s.Cursor = s.Cursor.Foreground(style.S.Primary)
s.DisabledCursor = s.DisabledCursor.Foreground(style.S.Subtle)
s.Symlink = s.Symlink.Foreground(style.S.Warning)
s.Directory = s.Directory.Foreground(style.S.Primary)
s.File = s.File.Foreground(style.S.Text)
s.DisabledFile = s.DisabledFile.Foreground(style.S.Subtle)
s.Permission = s.Permission.Foreground(style.S.Muted)
s.Selected = s.Selected.Foreground(style.S.Primary)
s.DisabledSelected = s.DisabledSelected.Foreground(style.S.Subtle)
s.FileSize = s.FileSize.Foreground(style.S.Muted)
s.EmptyDirectory = s.EmptyDirectory.Foreground(style.S.Subtle)
f.Styles = s
return f
}
-20
View File
@@ -1,20 +0,0 @@
package bubbles
import (
"charm.land/bubbles/v2/help"
"charm.land/lipgloss/v2"
"github.com/anotherhadi/ilovetui/style"
)
func NewHelp() help.Model {
h := help.New()
h.Styles.ShortKey = lipgloss.NewStyle().Foreground(style.S.Primary)
h.Styles.ShortDesc = lipgloss.NewStyle().Foreground(style.S.Muted)
h.Styles.ShortSeparator = lipgloss.NewStyle().Foreground(style.S.Subtle)
h.Styles.FullKey = lipgloss.NewStyle().Foreground(style.S.Primary)
h.Styles.FullDesc = lipgloss.NewStyle().Foreground(style.S.Muted)
h.Styles.FullSeparator = lipgloss.NewStyle().Foreground(style.S.Subtle)
h.Styles.Ellipsis = lipgloss.NewStyle().Foreground(style.S.Subtle)
return h
}
-47
View File
@@ -1,47 +0,0 @@
package bubbles
import (
"charm.land/bubbles/v2/list"
"github.com/anotherhadi/ilovetui/style"
)
func NewList(items []list.Item, width, height int) list.Model {
m := list.New(items, NewDefaultDelegate(), width, height)
m.Styles = themedListStyles()
return m
}
func themedListStyles() list.Styles {
s := list.DefaultStyles(true)
s.Title = s.Title.Background(style.S.Primary).Foreground(style.S.Background)
s.Spinner = s.Spinner.Foreground(style.S.Primary)
s.Filter = themedTextInputStyles()
s.DefaultFilterCharacterMatch = s.DefaultFilterCharacterMatch.Foreground(style.S.Primary)
s.StatusBar = s.StatusBar.Foreground(style.S.Muted)
s.StatusEmpty = s.StatusEmpty.Foreground(style.S.Subtle)
s.StatusBarActiveFilter = s.StatusBarActiveFilter.Foreground(style.S.Text)
s.StatusBarFilterCount = s.StatusBarFilterCount.Foreground(style.S.Subtle)
s.NoItems = s.NoItems.Foreground(style.S.Subtle)
s.ArabicPagination = s.ArabicPagination.Foreground(style.S.Subtle)
s.ActivePaginationDot = s.ActivePaginationDot.Foreground(style.S.Primary)
s.InactivePaginationDot = s.InactivePaginationDot.Foreground(style.S.Subtle)
s.DividerDot = s.DividerDot.Foreground(style.S.Subtle)
return s
}
func NewDefaultDelegate() list.DefaultDelegate {
d := list.NewDefaultDelegate()
d.Styles.NormalTitle = d.Styles.NormalTitle.Foreground(style.S.Text)
d.Styles.NormalDesc = d.Styles.NormalDesc.Foreground(style.S.Muted)
d.Styles.SelectedTitle = d.Styles.SelectedTitle.
BorderForeground(style.S.Primary).
Foreground(style.S.Primary)
d.Styles.SelectedDesc = d.Styles.SelectedDesc.
BorderForeground(style.S.Primary).
Foreground(style.S.Primary)
d.Styles.DimmedTitle = d.Styles.DimmedTitle.Foreground(style.S.Subtle)
d.Styles.DimmedDesc = d.Styles.DimmedDesc.Foreground(style.S.SubtleBg)
d.Styles.FilterMatch = d.Styles.FilterMatch.Foreground(style.S.Primary)
return d
}
-16
View File
@@ -1,16 +0,0 @@
package bubbles
import (
"charm.land/bubbles/v2/paginator"
"charm.land/lipgloss/v2"
"github.com/anotherhadi/ilovetui/style"
)
func NewPaginator() paginator.Model {
p := paginator.New()
p.Type = paginator.Dots
p.ActiveDot = lipgloss.NewStyle().Foreground(style.S.Primary).Render("•")
p.InactiveDot = lipgloss.NewStyle().Foreground(style.S.Subtle).Render("•")
return p
}
-16
View File
@@ -1,16 +0,0 @@
package bubbles
import (
"charm.land/bubbles/v2/progress"
"github.com/anotherhadi/ilovetui/style"
)
func NewProgress(opts ...progress.Option) progress.Model {
allOpts := append([]progress.Option{
progress.WithColors(style.S.Subtle, style.S.Primary),
}, opts...)
p := progress.New(allOpts...)
p.EmptyColor = style.S.SubtleBg
return p
}
-14
View File
@@ -1,14 +0,0 @@
package bubbles
import (
"charm.land/bubbles/v2/spinner"
"charm.land/lipgloss/v2"
"github.com/anotherhadi/ilovetui/style"
)
func NewSpinner(opts ...spinner.Option) spinner.Model {
s := spinner.New(opts...)
s.Style = lipgloss.NewStyle().Foreground(style.S.Primary)
return s
}
-17
View File
@@ -1,17 +0,0 @@
package bubbles
import (
"charm.land/bubbles/v2/table"
"github.com/anotherhadi/ilovetui/style"
)
func NewTable(opts ...table.Option) table.Model {
t := table.New(opts...)
s := table.DefaultStyles()
s.Header = s.Header.Foreground(style.S.Primary)
s.Cell = s.Cell.Foreground(style.S.Text)
s.Selected = s.Selected.Foreground(style.S.Primary)
t.SetStyles(s)
return t
}
-31
View File
@@ -1,31 +0,0 @@
package bubbles
import (
"charm.land/bubbles/v2/textarea"
"charm.land/lipgloss/v2"
"github.com/anotherhadi/ilovetui/style"
)
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(style.S.Text)
ts.Focused.CursorLine = lipgloss.NewStyle().Background(style.S.Selection).Foreground(style.S.Text)
ts.Focused.CursorLineNumber = lipgloss.NewStyle().Background(style.S.Selection).Foreground(style.S.Primary).Bold(true)
ts.Focused.LineNumber = lipgloss.NewStyle().Foreground(style.S.Subtle)
ts.Focused.Placeholder = lipgloss.NewStyle().Foreground(style.S.Subtle)
ts.Focused.EndOfBuffer = lipgloss.NewStyle().Foreground(style.S.SubtleBg)
ts.Blurred.Text = lipgloss.NewStyle().Foreground(style.S.Muted)
ts.Blurred.LineNumber = lipgloss.NewStyle().Foreground(style.S.SubtleBg)
ts.Blurred.Placeholder = lipgloss.NewStyle().Foreground(style.S.Subtle)
ts.Blurred.EndOfBuffer = lipgloss.NewStyle().Foreground(style.S.SubtleBg)
ta.SetStyles(ts)
return ta
}
-27
View File
@@ -1,27 +0,0 @@
package bubbles
import (
"charm.land/bubbles/v2/textinput"
"github.com/anotherhadi/ilovetui/style"
)
func NewTextInput() textinput.Model {
t := textinput.New()
t.SetStyles(themedTextInputStyles())
return t
}
func themedTextInputStyles() textinput.Styles {
s := textinput.DefaultStyles(true)
s.Focused.Text = s.Focused.Text.Foreground(style.S.Text)
s.Focused.Placeholder = s.Focused.Placeholder.Foreground(style.S.Subtle)
s.Focused.Suggestion = s.Focused.Suggestion.Foreground(style.S.Subtle)
s.Focused.Prompt = s.Focused.Prompt.Foreground(style.S.Primary)
s.Blurred.Text = s.Blurred.Text.Foreground(style.S.Muted)
s.Blurred.Placeholder = s.Blurred.Placeholder.Foreground(style.S.Subtle)
s.Blurred.Suggestion = s.Blurred.Suggestion.Foreground(style.S.Subtle)
s.Blurred.Prompt = s.Blurred.Prompt.Foreground(style.S.Subtle)
s.Cursor.Color = style.S.Primary
return s
}
-47
View File
@@ -1,47 +0,0 @@
package bubbles
import (
"charm.land/bubbles/v2/viewport"
"charm.land/lipgloss/v2"
"github.com/anotherhadi/ilovetui/style"
)
const ViewportGutterWidth = 2
func NewViewport() viewport.Model {
vp := viewport.New()
vp.MouseWheelEnabled = false
return vp
}
func ViewportView(vp *viewport.Model) string {
height := vp.Height()
total := vp.TotalLineCount()
blank := lipgloss.NewStyle().Width(ViewportGutterWidth).Render("")
if height <= 0 || total <= height {
vp.LeftGutterFunc = func(viewport.GutterContext) string { return blank }
return vp.View()
}
yOffset := vp.YOffset()
thumbSize := max(1, height*height/total)
thumbStart := 0
if maxOffset := total - height; maxOffset > 0 {
thumbStart = yOffset * (height - thumbSize) / maxOffset
}
trackStyle := lipgloss.NewStyle().Foreground(style.S.Subtle)
thumbStyle := lipgloss.NewStyle().Foreground(style.S.Primary)
vp.LeftGutterFunc = func(ctx viewport.GutterContext) string {
pos := ctx.Index - yOffset
if pos >= thumbStart && pos < thumbStart+thumbSize {
return thumbStyle.Render("█") + " "
}
return trackStyle.Render("│") + " "
}
return vp.View()
}
+273
View File
@@ -0,0 +1,273 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "ilovetui-opentui",
"dependencies": {
"yaml": "^2.8.1",
"zod": "^4.5.4",
},
"devDependencies": {
"@opentui/core": "^0.5.9",
"@opentui/keymap": "^0.5.9",
"@opentui/solid": "^0.5.9",
"@types/bun": "^1.3.14",
"opentui-spinner": "^0.0.7",
"solid-js": "1.9.12",
"typescript": "^5.9.3",
},
"peerDependencies": {
"@opentui/core": ">=0.5.0",
"@opentui/keymap": ">=0.5.0",
"@opentui/solid": ">=0.5.0",
"opentui-spinner": ">=0.0.7",
"solid-js": ">=1.9.0",
},
"optionalPeers": [
"@opentui/core",
"@opentui/keymap",
"@opentui/solid",
"opentui-spinner",
"solid-js",
],
},
},
"packages": {
"@ampproject/remapping": ["@ampproject/[email protected]", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
"@babel/code-frame": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
"@babel/compat-data": ["@babel/[email protected]", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="],
"@babel/core": ["@babel/[email protected]", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
"@babel/generator": ["@babel/[email protected]", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="],
"@babel/helper-annotate-as-pure": ["@babel/[email protected]", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw=="],
"@babel/helper-compilation-targets": ["@babel/[email protected]", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="],
"@babel/helper-create-class-features-plugin": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/traverse": "^7.29.7", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg=="],
"@babel/helper-globals": ["@babel/[email protected]", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="],
"@babel/helper-member-expression-to-functions": ["@babel/[email protected]", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg=="],
"@babel/helper-module-imports": ["@babel/[email protected]", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="],
"@babel/helper-module-transforms": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="],
"@babel/helper-optimise-call-expression": ["@babel/[email protected]", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong=="],
"@babel/helper-plugin-utils": ["@babel/[email protected]", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="],
"@babel/helper-replace-supers": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ=="],
"@babel/helper-skip-transparent-expression-wrappers": ["@babel/[email protected]", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ=="],
"@babel/helper-string-parser": ["@babel/[email protected]", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
"@babel/helper-validator-identifier": ["@babel/[email protected]", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
"@babel/helper-validator-option": ["@babel/[email protected]", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="],
"@babel/helpers": ["@babel/[email protected]", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="],
"@babel/parser": ["@babel/[email protected]", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="],
"@babel/plugin-syntax-jsx": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A=="],
"@babel/plugin-syntax-typescript": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA=="],
"@babel/plugin-transform-modules-commonjs": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-module-transforms": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ=="],
"@babel/plugin-transform-typescript": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/plugin-syntax-typescript": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw=="],
"@babel/preset-typescript": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ=="],
"@babel/template": ["@babel/[email protected]", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="],
"@babel/traverse": ["@babel/[email protected]", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg=="],
"@babel/types": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
"@jridgewell/gen-mapping": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/resolve-uri": ["@jridgewell/[email protected]", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/[email protected]", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="],
"@jridgewell/trace-mapping": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@opentui/core": ["@opentui/[email protected]", "", { "dependencies": { "bun-ffi-structs": "0.3.1", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.5.9", "@opentui/core-darwin-x64": "0.5.9", "@opentui/core-linux-arm64": "0.5.9", "@opentui/core-linux-arm64-musl": "0.5.9", "@opentui/core-linux-x64": "0.5.9", "@opentui/core-linux-x64-musl": "0.5.9", "@opentui/core-win32-arm64": "0.5.9", "@opentui/core-win32-x64": "0.5.9" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-d0EWYyp6djitu1N1R0o75NrLl4TxY3oJEmRNrX9vSKKC5/jriGQdSV6lJmwiB77O0cxtBnPzWztAV79vA1J2fA=="],
"@opentui/core-darwin-arm64": ["@opentui/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-YcpWGD8GwoO3UYYw0kLDI4qof3ElSwWW5M3fNG+Kw87BSjCa5frZqak5xebdZ/XU5Xsk+BjgbBsxbzo9yrIjZQ=="],
"@opentui/core-darwin-x64": ["@opentui/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-xgdZwgcwlDCqi0WPCS1d0GVSd+oKDFX+UVE1ZPei8u8W3pirXGSExxVHNku7QbEYCRvpCeMtoKIa6xN9cVTsoQ=="],
"@opentui/core-linux-arm64": ["@opentui/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-vHBiJp69wMaL+fRMDpnXCCIpPyQRPZEWCWBatnblUUGIWUseqeQlaPow6ljHSbgAd/GjygAf4tFCyTqNT2ezZA=="],
"@opentui/core-linux-arm64-musl": ["@opentui/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-A7GW4NCTInj2vy3X2gVO2Sm8jllgdLxwPdOklPqyl+xqivQtyG9qwS5YXoCk9fBJt10ORhYKETEjYEnIE5xvMQ=="],
"@opentui/core-linux-x64": ["@opentui/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-rdw7QVopVHwj6fMrJc7hPGVaYg2V5NBlW3MUfkrQBlUlo/87OC4aYDAh5oRIL1CIFD9LjDdh7hwmUNbn/K9Pcw=="],
"@opentui/core-linux-x64-musl": ["@opentui/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-J4wQs1OMPZ4hR93Op1C/BLFmIta2mUJm4M7djevlgcWcal4RSNO4V8QvUXfogWGci+ADZqEmX2osQQDY5NVJpw=="],
"@opentui/core-win32-arm64": ["@opentui/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-OnmEsGLWwsHrm6sTsWSSesu4LGjSV1JNWqCsEItbuitWaqH6rXdSahSc5jMFsS57bZxUyaRz01yAqiZzUSXnVQ=="],
"@opentui/core-win32-x64": ["@opentui/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-/CnAIfKL7+ZeGLyZoXV5zS71Nd8Zn97RUir2DAIY05MJozmfg5s7XOAdUNYuK1cg5mbwlAuqeAsfbuWUAEe15g=="],
"@opentui/keymap": ["@opentui/[email protected]", "", { "dependencies": { "@opentui/core": "0.5.9" }, "peerDependencies": { "@opentui/react": "0.5.9", "@opentui/solid": "0.5.9", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-ZcRNeuCDv+LJ89BS5xBw90e0S1Etn6j41jflg6LZlWj4bnqjCU55dq0+zERUdkkC4rqD4SikM8q7V3ZfkwzyyQ=="],
"@opentui/solid": ["@opentui/[email protected]", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.5.9", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-zGSP/ia9ww+TTMvQMZjJW2+h7fg9YkVRIM723EgvP+v7ZBZ/0vMzPNh7FJ0jET3ysw0h5hsbvcYISs4sUcf+UA=="],
"@types/bun": ["@types/[email protected]", "", { "dependencies": { "bun-types": "1.4.0" } }, "sha512-K+lZULY23vRgK/CfTjFIV+tyifaNdSMlPh9j+6mQ/cLfpOznLyAuzgV/JQysyECpkBQLVMSyvjlr2fBUSA9wFQ=="],
"@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ=="],
"ansi-regex": ["[email protected]", "", {}, "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ=="],
"babel-plugin-jsx-dom-expressions": ["[email protected]", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-lxve6Y02YiZTldB7efKpnbf1BH00XCFZNYYW235jSGsYaJNFtHrYlKV6/O+miHbjqpIr9FTe5+0no4hofAMbfA=="],
"babel-plugin-module-resolver": ["[email protected]", "", { "dependencies": { "find-babel-config": "^2.1.1", "glob": "^9.3.3", "pkg-up": "^3.1.0", "reselect": "^4.1.7", "resolve": "^1.22.8" } }, "sha512-9KtaCazHee2xc0ibfqsDeamwDps6FZNo5S0Q81dUqEuFzVwPhcT4J5jOqIVvgCA3Q/wO9hKYxN/Ds3tIsp5ygg=="],
"babel-preset-solid": ["[email protected]", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.6" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.12" }, "optionalPeers": ["solid-js"] }, "sha512-LLqnuKVDlKpyBlMPcH6qEvs/wmS9a+NczppxJ3ryS/c0O5IiSFOIBQi9GzyiGDSbcJpx4Gr87jyFTos1MyEuWg=="],
"balanced-match": ["[email protected]", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"baseline-browser-mapping": ["[email protected]", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw=="],
"brace-expansion": ["[email protected]", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="],
"browserslist": ["[email protected]", "", { "dependencies": { "baseline-browser-mapping": "^2.11.12", "caniuse-lite": "^1.0.30001809", "electron-to-chromium": "^1.5.402", "node-releases": "^2.0.53", "update-browserslist-db": "^1.3.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA=="],
"bun-ffi-structs": ["[email protected]", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-3gM7PpVWLyrwxWjcilSiGuhWanhZivvo6l0u573NziPH6f/gwk6McbaYgn7oJWov6pKGRTDbrg94W5DcJsKTtQ=="],
"bun-types": ["[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-iIKw23BspnQQYd3prITOBxeUsxBHnwzX6YJfGMuNOZzeNcMmVqzIIVGRm1l69ogaPQmb4wB6BN8mA5bE9YuC5Q=="],
"caniuse-lite": ["[email protected]", "", {}, "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg=="],
"cli-spinners": ["[email protected]", "", {}, "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw=="],
"convert-source-map": ["[email protected]", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
"csstype": ["[email protected]", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"debug": ["[email protected]", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"diff": ["[email protected]", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="],
"electron-to-chromium": ["[email protected]", "", {}, "sha512-4T+DTDWuMPM4aHlHwWdAVCVWwp7LDilnhzkj+c/Lbj91XSQrLuOmZSLtS9Q4iIqjlPUbPOnC624zDVVHCHaolQ=="],
"emoji-regex": ["[email protected]", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
"entities": ["[email protected]", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
"es-errors": ["[email protected]", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"escalade": ["[email protected]", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"find-babel-config": ["[email protected]", "", { "dependencies": { "json5": "^2.2.3" } }, "sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg=="],
"find-up": ["[email protected]", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="],
"fs.realpath": ["[email protected]", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
"function-bind": ["[email protected]", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"gensync": ["[email protected]", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
"get-east-asian-width": ["[email protected]", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="],
"glob": ["[email protected]", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="],
"hasown": ["[email protected]", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="],
"html-entities": ["[email protected]", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="],
"is-core-module": ["[email protected]", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="],
"js-tokens": ["[email protected]", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"jsesc": ["[email protected]", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"json5": ["[email protected]", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"locate-path": ["[email protected]", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="],
"lru-cache": ["[email protected]", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"marked": ["[email protected]", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="],
"minimatch": ["[email protected]", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg=="],
"minipass": ["[email protected]", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="],
"ms": ["[email protected]", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"node-releases": ["[email protected]", "", {}, "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ=="],
"opentui-spinner": ["[email protected]", "", { "dependencies": { "cli-spinners": "^3.3.0" }, "peerDependencies": { "@opentui/core": "^0.3.4", "@opentui/react": "^0.3.4", "@opentui/solid": "^0.3.4", "typescript": "^5" }, "optionalPeers": ["@opentui/react", "@opentui/solid"] }, "sha512-nPzwAvJG+y9rVEwwHLHqbsMzLnIk2zw+F9LqwA7aYJvpM5gsrKC2rrGi36A+tZpA+1RnWxXeWEgVZMchnaH18Q=="],
"p-limit": ["[email protected]", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
"p-locate": ["[email protected]", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="],
"p-try": ["[email protected]", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="],
"parse5": ["[email protected]", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
"path-exists": ["[email protected]", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="],
"path-parse": ["[email protected]", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
"path-scurry": ["[email protected]", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
"picocolors": ["[email protected]", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"pkg-up": ["[email protected]", "", { "dependencies": { "find-up": "^3.0.0" } }, "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA=="],
"reselect": ["[email protected]", "", {}, "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ=="],
"resolve": ["[email protected]", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="],
"s-js": ["[email protected]", "", {}, "sha512-RtpOm+cM6O0sHg6IA70wH+UC3FZcND+rccBZpBAHzlUgNO2Bm5BN+FnM8+OBxzXdwpKWFwX11JGF0MFRkhSoIQ=="],
"semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"seroval": ["[email protected]", "", {}, "sha512-rVQVWjjSvlINzaQPZH5JFqsqEsIWdTxY3iJZCnTL/5gQbXIRooVZKI60tVCkOVfzcRPejboxO2t0P89dg5mQaA=="],
"seroval-plugins": ["[email protected]", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-HXuLAX2pu/UByPpaeo/TaMfvMIi+1QqIoPJYCcAtU8QkVNwgR6MPlGuCQTErV1JwraaMbYaWVIBX7mppzGLATQ=="],
"solid-js": ["[email protected]", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-QzKaSJq2/iDrWR1As6MHZQ8fQkdOBf8GReYb7L5iKwMGceg7HxDcaOHk0at66tNgn9U2U7dXo8ZZpLIAmGMzgw=="],
"string-width": ["[email protected]", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
"strip-ansi": ["[email protected]", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
"supports-preserve-symlinks-flag": ["[email protected]", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
"typescript": ["[email protected]", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["[email protected]", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
"update-browserslist-db": ["[email protected]", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw=="],
"web-tree-sitter": ["[email protected]", "", { "peerDependencies": { "@types/emscripten": "^1.40.0" }, "optionalPeers": ["@types/emscripten"] }, "sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA=="],
"yallist": ["[email protected]", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"yaml": ["[email protected]", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
"zod": ["[email protected]", "", {}, "sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA=="],
"babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/[email protected]", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="],
"parse5/entities": ["[email protected]", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
"path-scurry/lru-cache": ["[email protected]", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"path-scurry/minipass": ["[email protected]", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
}
}
+25
View File
@@ -0,0 +1,25 @@
// @bun
// src/context/focus.ts
import { CliRenderEvents } from "@opentui/core";
import { useRenderer } from "@opentui/solid";
import { createEffect, createSignal, onCleanup } from "solid-js";
function isInside2(node, root) {
if (!root)
return false;
for (let current = node;current; current = current.parent) {
if (current === root)
return true;
}
return false;
}
function useFocusWithin2(target) {
const renderer = useRenderer();
const [focused, setFocused] = createSignal(false);
const sync = () => setFocused(isInside2(renderer.currentFocusedRenderable, target() ?? null));
renderer.on(CliRenderEvents.FOCUSED_RENDERABLE, sync);
onCleanup(() => renderer.off(CliRenderEvents.FOCUSED_RENDERABLE, sync));
createEffect(sync);
return focused;
}
export { isInside2, useFocusWithin2 };
+21
View File
@@ -0,0 +1,21 @@
// @bun
// src/context/notifications.ts
import { createSignal } from "solid-js";
var DEFAULT_DURATION = 3000;
var [notifications2, setNotifications] = createSignal([]);
var nextId = 0;
function notify2(message, options = {}) {
const id = options.id ?? `toast-${nextId++}`;
const toast = { id, title: options.title, message, kind: options.kind ?? "info" };
setNotifications((prev) => [...prev.filter((n) => n.id !== id), toast]);
const duration = options.duration ?? DEFAULT_DURATION;
if (duration > 0) {
setTimeout(() => dismiss2(id), duration);
}
return id;
}
function dismiss2(id) {
setNotifications((prev) => prev.filter((n) => n.id !== id));
}
export { notifications2, notify2, dismiss2 };
+23
View File
@@ -0,0 +1,23 @@
// @bun
import {
theme2
} from "./chunk-p6jvscj3.js";
// src/components/Box.tsx
import { spread as _$spread } from "@opentui/solid";
import { mergeProps as _$mergeProps } from "@opentui/solid";
import { memo as _$memo } from "@opentui/solid";
import { createElement as _$createElement } from "@opentui/solid";
function Box2(props) {
return (() => {
var _el$ = _$createElement("box");
_$spread(_el$, _$mergeProps(props, {
get borderStyle() {
return props.borderStyle ?? theme2.borderStyle;
}
}), false);
return _el$;
})();
}
export { Box2 };
+79
View File
@@ -0,0 +1,79 @@
// @bun
import {
notifications2,
dismiss2
} from "./chunk-6ggk5fg7.js";
import {
theme2
} from "./chunk-p6jvscj3.js";
// src/components/NotificationHost.tsx
import { effect as _$effect } from "@opentui/solid";
import { insertNode as _$insertNode } from "@opentui/solid";
import { memo as _$memo } from "@opentui/solid";
import { insert as _$insert } from "@opentui/solid";
import { createComponent as _$createComponent } from "@opentui/solid";
import { setProp as _$setProp } from "@opentui/solid";
import { createElement as _$createElement } from "@opentui/solid";
import { Portal } from "@opentui/solid";
import { For } from "solid-js";
var KIND_LABEL = {
info: "Info",
success: "Success",
warning: "Warning",
error: "Error"
};
function NotificationHost2(props = {}) {
const defaultColor = () => ({
info: theme2.primary,
success: theme2.success,
warning: theme2.warning,
error: theme2.error
});
const colorFor = (kind) => props.colors?.[kind] ?? defaultColor()[kind];
return _$createComponent(Portal, {
get children() {
var _el$ = _$createElement("box");
_$setProp(_el$, "position", "absolute");
_$setProp(_el$, "top", 1);
_$setProp(_el$, "right", 1);
_$setProp(_el$, "flexDirection", "column");
_$setProp(_el$, "zIndex", 1000);
_$insert(_el$, _$createComponent(For, {
get each() {
return notifications2();
},
children: (toast) => (() => {
var _el$2 = _$createElement("box"), _el$3 = _$createElement("text"), _el$4 = _$createElement("text");
_$insertNode(_el$2, _el$3);
_$insertNode(_el$2, _el$4);
_$setProp(_el$2, "border", true);
_$setProp(_el$2, "marginBottom", 1);
_$insert(_el$3, () => toast.title ?? KIND_LABEL[toast.kind]);
_$insert(_el$4, () => toast.message);
_$effect((_p$) => {
var _v$ = theme2.borderStyle, _v$2 = colorFor(toast.kind), _v$3 = props.backgroundColor ?? theme2.background, _v$4 = props.width ?? 32, _v$5 = theme2.mouse ? () => dismiss2(toast.id) : undefined, _v$6 = colorFor(toast.kind);
_v$ !== _p$.e && (_p$.e = _$setProp(_el$2, "borderStyle", _v$, _p$.e));
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$2, "borderColor", _v$2, _p$.t));
_v$3 !== _p$.a && (_p$.a = _$setProp(_el$2, "backgroundColor", _v$3, _p$.a));
_v$4 !== _p$.o && (_p$.o = _$setProp(_el$2, "width", _v$4, _p$.o));
_v$5 !== _p$.i && (_p$.i = _$setProp(_el$2, "onMouseDown", _v$5, _p$.i));
_v$6 !== _p$.n && (_p$.n = _$setProp(_el$3, "fg", _v$6, _p$.n));
return _p$;
}, {
e: undefined,
t: undefined,
a: undefined,
o: undefined,
i: undefined,
n: undefined
});
return _el$2;
})()
}));
return _el$;
}
});
}
export { NotificationHost2 };
+121
View File
@@ -0,0 +1,121 @@
// @bun
import {
theme2
} from "./chunk-p6jvscj3.js";
// src/components/HelpBar.tsx
import { createTextNode as _$createTextNode } from "@opentui/solid";
import { insertNode as _$insertNode } from "@opentui/solid";
import { effect as _$effect } from "@opentui/solid";
import { memo as _$memo } from "@opentui/solid";
import { insert as _$insert } from "@opentui/solid";
import { createComponent as _$createComponent } from "@opentui/solid";
import { use as _$use } from "@opentui/solid";
import { setProp as _$setProp } from "@opentui/solid";
import { createElement as _$createElement } from "@opentui/solid";
import { CliRenderEvents, TextAttributes } from "@opentui/core";
import { onResize, useRenderer } from "@opentui/solid";
import { formatCommandBindings } from "@opentui/keymap/extras";
import { useKeymapSelector } from "@opentui/keymap/solid";
import { createMemo, createSignal, For } from "solid-js";
var KEY_DISPLAY = {
up: "\u2191",
down: "\u2193",
left: "\u2190",
right: "\u2192",
enter: "Enter",
escape: "Esc"
};
var SEPARATOR = " ";
var ELLIPSIS = "\u2026";
function HelpBar2(props = {}) {
const accent = () => props.accentColor ?? theme2.primary;
const muted = () => props.mutedColor ?? theme2.muted;
const renderer = useRenderer();
const [width, setWidth] = createSignal(0);
let container;
const measure = () => {
if (!container)
return;
setWidth(container.width);
};
const remeasureNextFrame = () => renderer.once(CliRenderEvents.FRAME, measure);
onResize(remeasureNextFrame);
const entries = useKeymapSelector((keymap) => keymap.getCommandEntries({
visibility: "active"
}).filter((entry) => entry.command.shortHelp === true).map((entry) => ({
command: entry.command.name,
keys: formatCommandBindings(entry.bindings, {
keyNameAliases: KEY_DISPLAY
}) ?? "",
label: typeof entry.command.desc === "string" ? entry.command.desc : entry.command.name
})));
const visible = createMemo(() => {
const available = width() - 2;
const shown = [];
let used = 0;
for (const entry of entries()) {
const sepWidth = shown.length > 0 ? SEPARATOR.length : 0;
const entryWidth = entry.keys.length + 1 + entry.label.length;
if (used + sepWidth + entryWidth <= available) {
used += sepWidth + entryWidth;
shown.push(entry);
continue;
}
const remaining = available - used - sepWidth;
const prefix = `${entry.keys} `;
if (remaining > prefix.length + ELLIPSIS.length) {
const labelBudget = remaining - prefix.length - ELLIPSIS.length;
shown.push({
...entry,
label: `${entry.label.slice(0, labelBudget)}${ELLIPSIS}`
});
}
break;
}
return shown;
});
return (() => {
var _el$ = _$createElement("box");
_$use((el) => {
container = el;
remeasureNextFrame();
}, _el$);
_$setProp(_el$, "flexDirection", "row");
_$setProp(_el$, "paddingLeft", 1);
_$setProp(_el$, "paddingRight", 1);
_$setProp(_el$, "overflow", "hidden");
_$insert(_el$, _$createComponent(For, {
get each() {
return visible();
},
children: (entry, i) => [_$memo(() => _$memo(() => i() > 0)() && (() => {
var _el$4 = _$createElement("text");
_$insertNode(_el$4, _$createTextNode(` `));
_$effect((_$p) => _$setProp(_el$4, "fg", muted(), _$p));
return _el$4;
})()), (() => {
var _el$2 = _$createElement("text");
_$insert(_el$2, () => entry.keys);
_$effect((_p$) => {
var _v$ = accent(), _v$2 = TextAttributes.BOLD;
_v$ !== _p$.e && (_p$.e = _$setProp(_el$2, "fg", _v$, _p$.e));
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$2, "attributes", _v$2, _p$.t));
return _p$;
}, {
e: undefined,
t: undefined
});
return _el$2;
})(), (() => {
var _el$3 = _$createElement("text");
_$insert(_el$3, () => ` ${entry.label}`);
_$effect((_$p) => _$setProp(_el$3, "fg", muted(), _$p));
return _el$3;
})()]
}));
return _el$;
})();
}
export { HelpBar2 };
+44
View File
@@ -0,0 +1,44 @@
// @bun
import {
theme2
} from "./chunk-p6jvscj3.js";
// src/components/KeyValue.tsx
import { memo as _$memo } from "@opentui/solid";
import { effect as _$effect } from "@opentui/solid";
import { insertNode as _$insertNode } from "@opentui/solid";
import { createComponent as _$createComponent } from "@opentui/solid";
import { insert as _$insert } from "@opentui/solid";
import { setProp as _$setProp } from "@opentui/solid";
import { createElement as _$createElement } from "@opentui/solid";
import { Show } from "solid-js";
function KeyValue2(props) {
return (() => {
var _el$ = _$createElement("box"), _el$2 = _$createElement("text");
_$insertNode(_el$, _el$2);
_$setProp(_el$, "flexShrink", 0);
_$setProp(_el$, "flexDirection", "row");
_$setProp(_el$, "justifyContent", "space-between");
_$insert(_el$2, () => props.label);
_$insert(_el$, _$createComponent(Show, {
get when() {
return props.children;
},
get fallback() {
return (() => {
var _el$3 = _$createElement("text");
_$insert(_el$3, () => props.value ?? "");
_$effect((_$p) => _$setProp(_el$3, "fg", props.valueColor, _$p));
return _el$3;
})();
},
get children() {
return props.children;
}
}), null);
_$effect((_$p) => _$setProp(_el$2, "fg", theme2.muted, _$p));
return _el$;
})();
}
export { KeyValue2 };
+9
View File
@@ -0,0 +1,9 @@
// @bun
// src/context/help.ts
import { createSignal } from "solid-js";
var [helpOpen2, setHelpOpen] = createSignal(false);
function toggleHelp2() {
setHelpOpen((open) => !open);
}
export { helpOpen2, toggleHelp2 };
+87
View File
@@ -0,0 +1,87 @@
// @bun
import {
helpOpen2,
toggleHelp2
} from "./chunk-ee2qaswx.js";
import {
Modal2
} from "./chunk-t2cszvff.js";
// src/components/HelpModal.tsx
import { createComponent as _$createComponent } from "@opentui/solid";
import { insert as _$insert } from "@opentui/solid";
import { setProp as _$setProp } from "@opentui/solid";
import { effect as _$effect } from "@opentui/solid";
import { createTextNode as _$createTextNode } from "@opentui/solid";
import { insertNode as _$insertNode } from "@opentui/solid";
import { createElement as _$createElement } from "@opentui/solid";
import { TextAttributes } from "@opentui/core";
import { formatCommandBindings } from "@opentui/keymap/extras";
import { useKeymapSelector } from "@opentui/keymap/solid";
import { createSignal } from "solid-js";
var KEY_DISPLAY = {
up: "\u2191",
down: "\u2193",
left: "\u2190",
right: "\u2192",
enter: "Enter",
escape: "Esc"
};
function HelpModal2(props = {}) {
const [scope, setScope] = createSignal(null);
const entries = useKeymapSelector((keymap) => keymap.getCommandEntries({
visibility: "active",
focused: scope()
}).map((entry) => ({
command: entry.command.name,
keys: formatCommandBindings(entry.bindings, {
keyNameAliases: KEY_DISPLAY
}) ?? "",
label: typeof entry.command.desc === "string" ? entry.command.desc : entry.command.name
})));
return _$createComponent(Modal2, {
get open() {
return helpOpen2();
},
onDismiss: toggleHelp2,
onOpen: (previous) => setScope(() => previous),
get dismissKey() {
return props.dismissKey;
},
get accentColor() {
return props.accentColor;
},
get backgroundColor() {
return props.backgroundColor;
},
get backdropColor() {
return props.backdropColor;
},
get width() {
return props.width;
},
get maxHeight() {
return entries().length + 6;
},
scrollable: true,
get children() {
return [(() => {
var _el$ = _$createElement("text");
_$insertNode(_el$, _$createTextNode(`Keybindings`));
_$effect((_$p) => _$setProp(_el$, "attributes", TextAttributes.BOLD, _$p));
return _el$;
})(), (() => {
var _el$3 = _$createElement("text");
_$insertNode(_el$3, _$createTextNode(` `));
return _el$3;
})(), (() => {
var _el$5 = _$createElement("text");
_$insert(_el$5, () => entries().map((entry) => `${entry.keys.padEnd(10)}${entry.label}`).join(`
`));
return _el$5;
})()];
}
});
}
export { HelpModal2 };
+26
View File
@@ -0,0 +1,26 @@
// @bun
// src/context/helper.tsx
import { createComponent as _$createComponent } from "@opentui/solid";
import { createContext, useContext } from "solid-js";
function createSimpleContext2(input) {
const ctx = createContext();
return {
provider: (props) => {
const value = input.init(props);
return _$createComponent(ctx.Provider, {
value,
get children() {
return props.children;
}
});
},
use() {
const value = useContext(ctx);
if (!value)
throw new Error(`${input.name} context must be used within its provider`);
return value;
}
};
}
export { createSimpleContext2 };
+35
View File
@@ -0,0 +1,35 @@
// @bun
import {
theme2
} from "./chunk-p6jvscj3.js";
// src/components/Spinner.tsx
import { setProp as _$setProp } from "@opentui/solid";
import { effect as _$effect } from "@opentui/solid";
import { createElement as _$createElement } from "@opentui/solid";
import"opentui-spinner/solid";
function Spinner2(props) {
return (() => {
var _el$ = _$createElement("spinner");
_$effect((_p$) => {
var { name: _v$, frames: _v$2, interval: _v$3 } = props, _v$4 = props.autoplay ?? !theme2.reducedMotion, _v$5 = props.color ?? theme2.primary, _v$6 = props.backgroundColor;
_v$ !== _p$.e && (_p$.e = _$setProp(_el$, "name", _v$, _p$.e));
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$, "frames", _v$2, _p$.t));
_v$3 !== _p$.a && (_p$.a = _$setProp(_el$, "interval", _v$3, _p$.a));
_v$4 !== _p$.o && (_p$.o = _$setProp(_el$, "autoplay", _v$4, _p$.o));
_v$5 !== _p$.i && (_p$.i = _$setProp(_el$, "color", _v$5, _p$.i));
_v$6 !== _p$.n && (_p$.n = _$setProp(_el$, "backgroundColor", _v$6, _p$.n));
return _p$;
}, {
e: undefined,
t: undefined,
a: undefined,
o: undefined,
i: undefined,
n: undefined
});
return _el$;
})();
}
export { Spinner2 };
+65
View File
@@ -0,0 +1,65 @@
// @bun
// src/config.ts
import { z } from "zod";
var COLOR_KEYS2 = [
"base00",
"base01",
"base02",
"base03",
"base04",
"base05",
"base06",
"base07",
"base08",
"base09",
"base0a",
"base0b",
"base0c",
"base0d",
"base0e",
"base0f"
];
var OPENTUI_BORDER_STYLES2 = ["single", "double", "rounded", "heavy"];
var hexColor = z.string().regex(/^#?[0-9a-fA-F]{6}$/, 'expected a hex color like "#1e1e2e"');
var colorsShape = {};
for (const key of COLOR_KEYS2)
colorsShape[key] = hexColor.optional();
var ThemeConfigSchema2 = z.object({
nerd_fonts: z.boolean().optional(),
emoji_fonts: z.boolean().optional(),
reduced_motion: z.boolean().optional(),
mouse: z.boolean().optional(),
border: z.enum(OPENTUI_BORDER_STYLES2).optional(),
colors: z.object(colorsShape).optional()
});
function layerConfigs2(layers) {
const out = {};
for (const layer of layers) {
const { colors, ...rest } = layer;
Object.assign(out, rest);
if (colors)
out.colors = { ...out.colors, ...colors };
}
return out;
}
function resolveConfig2(config) {
const colors = {};
for (const key of COLOR_KEYS2)
colors[key] = normalizeColor2(config.colors?.[key] ?? "");
return {
nerdFonts: config.nerd_fonts ?? false,
emojiFonts: config.emoji_fonts ?? false,
reducedMotion: config.reduced_motion ?? false,
mouse: config.mouse ?? true,
border: config.border ?? "rounded",
colors
};
}
function normalizeColor2(hex) {
const trimmed = hex.trim();
if (trimmed && !trimmed.startsWith("#"))
return `#${trimmed}`;
return trimmed;
}
export { COLOR_KEYS2, OPENTUI_BORDER_STYLES2, ThemeConfigSchema2, layerConfigs2, resolveConfig2, normalizeColor2 };
+79
View File
@@ -0,0 +1,79 @@
// @bun
import {
ThemeConfigSchema2,
layerConfigs2,
resolveConfig2
} from "./chunk-njvj322f.js";
import {
notify2
} from "./chunk-6ggk5fg7.js";
import {
buildPresets2
} from "./chunk-yyg967q8.js";
import {
configPath2
} from "./chunk-xykm10y9.js";
// src/index.ts
import { existsSync, readFileSync } from "fs";
import { join } from "path";
import { parse as parseYAML } from "yaml";
import { z } from "zod";
function reportThemeError(path, message) {
const text = `${path}: ${message}`;
console.error(`[ilovetui theme] ${text}`);
notify2(text, { kind: "warning", duration: 0 });
}
function readThemeFile(path) {
if (!existsSync(path))
return {};
let raw;
try {
raw = parseYAML(readFileSync(path, "utf8"));
} catch (error) {
reportThemeError(path, error instanceof Error ? error.message : String(error));
return {};
}
const result = ThemeConfigSchema2.safeParse(raw ?? {});
if (!result.success) {
reportThemeError(path, z.prettifyError(result.error));
return {};
}
return result.data;
}
var fileLayers = [readThemeFile(join(import.meta.dir, "default.yaml")), readThemeFile(configPath2())];
function buildTheme(overrides) {
const resolved = resolveConfig2(layerConfigs2([...fileLayers, overrides]));
const colors = resolved.colors;
return {
...colors,
background: colors.base00,
subtleBg: colors.base01,
selection: colors.base02,
subtle: colors.base03,
muted: colors.base04,
text: colors.base05,
primary: colors.base0d,
success: colors.base0b,
warning: colors.base09,
error: colors.base08,
nerdFonts: resolved.nerdFonts,
emojiFonts: resolved.emojiFonts,
reducedMotion: resolved.reducedMotion,
mouse: resolved.mouse,
borderStyle: resolved.border
};
}
var theme2 = buildTheme({});
var presets2 = buildPresets2(theme2);
function configureTheme2(overrides = {}) {
Object.assign(theme2, buildTheme(overrides));
const next = buildPresets2(theme2);
for (const key of Object.keys(next))
Object.assign(presets2[key], next[key]);
}
function icon2(codepoint) {
return theme2.nerdFonts ? `${codepoint} ` : "";
}
export { theme2, presets2, configureTheme2, icon2 };
+54
View File
@@ -0,0 +1,54 @@
// @bun
import {
theme2
} from "./chunk-p6jvscj3.js";
// src/components/MinSizeGuard.tsx
import { createComponent as _$createComponent } from "@opentui/solid";
import { effect as _$effect } from "@opentui/solid";
import { insertNode as _$insertNode } from "@opentui/solid";
import { insert as _$insert } from "@opentui/solid";
import { setProp as _$setProp } from "@opentui/solid";
import { createElement as _$createElement } from "@opentui/solid";
import { memo as _$memo } from "@opentui/solid";
import { useTerminalDimensions } from "@opentui/solid";
import { Show } from "solid-js";
function MinSizeGuard2(props) {
const dimensions = useTerminalDimensions();
const minWidth = () => props.minWidth ?? 80;
const minHeight = () => props.minHeight ?? 24;
const tooSmall = () => dimensions().width < minWidth() || dimensions().height < minHeight();
return [_$memo(() => props.children), _$createComponent(Show, {
get when() {
return tooSmall();
},
get children() {
var _el$ = _$createElement("box"), _el$2 = _$createElement("text");
_$insertNode(_el$, _el$2);
_$setProp(_el$, "position", "absolute");
_$setProp(_el$, "top", 0);
_$setProp(_el$, "left", 0);
_$setProp(_el$, "alignItems", "center");
_$setProp(_el$, "justifyContent", "center");
_$setProp(_el$, "zIndex", 9999);
_$insert(_el$2, () => `Terminal too small
Minimum size: ${minWidth()}x${minHeight()} \u2014 current: ${dimensions().width}x${dimensions().height}`);
_$effect((_p$) => {
var _v$ = dimensions().width, _v$2 = dimensions().height, _v$3 = props.backgroundColor ?? theme2.background, _v$4 = props.textColor ?? theme2.muted;
_v$ !== _p$.e && (_p$.e = _$setProp(_el$, "width", _v$, _p$.e));
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$, "height", _v$2, _p$.t));
_v$3 !== _p$.a && (_p$.a = _$setProp(_el$, "backgroundColor", _v$3, _p$.a));
_v$4 !== _p$.o && (_p$.o = _$setProp(_el$2, "fg", _v$4, _p$.o));
return _p$;
}, {
e: undefined,
t: undefined,
a: undefined,
o: undefined
});
return _el$;
}
})];
}
export { MinSizeGuard2 };
+61
View File
@@ -0,0 +1,61 @@
// @bun
import {
theme2
} from "./chunk-p6jvscj3.js";
import {
Badge2
} from "./chunk-v9cetqzr.js";
// src/components/Toggle.tsx
import { setProp as _$setProp } from "@opentui/solid";
import { effect as _$effect } from "@opentui/solid";
import { insert as _$insert } from "@opentui/solid";
import { createComponent as _$createComponent } from "@opentui/solid";
import { memo as _$memo } from "@opentui/solid";
import { createElement as _$createElement } from "@opentui/solid";
import { TextAttributes } from "@opentui/core";
import { Show } from "solid-js";
function Toggle2(props) {
const onColor = () => props.onColor ?? theme2.success;
const offColor = () => props.offColor ?? theme2.muted;
const nerdFonts = () => props.withNerdfont ?? theme2.nerdFonts;
return (() => {
var _el$ = _$createElement("box");
_$insert(_el$, _$createComponent(Show, {
get when() {
return nerdFonts();
},
get fallback() {
return (() => {
var _el$2 = _$createElement("text");
_$insert(_el$2, () => props.on ? "on" : "off");
_$effect((_p$) => {
var _v$ = props.on ? onColor() : offColor(), _v$2 = TextAttributes.BOLD;
_v$ !== _p$.e && (_p$.e = _$setProp(_el$2, "fg", _v$, _p$.e));
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$2, "attributes", _v$2, _p$.t));
return _p$;
}, {
e: undefined,
t: undefined
});
return _el$2;
})();
},
get children() {
return _$createComponent(Badge2, {
get label() {
return props.on ? " \u25CF" : "\u25CF ";
},
get color() {
return _$memo(() => !!props.on)() ? onColor() : offColor();
},
withNerdfont: true
});
}
}));
_$effect((_$p) => _$setProp(_el$, "onMouseDown", theme2.mouse ? props.onToggle : undefined, _$p));
return _el$;
})();
}
export { Toggle2 };
+155
View File
@@ -0,0 +1,155 @@
// @bun
import {
theme2,
presets2
} from "./chunk-p6jvscj3.js";
// src/components/Sidebar.tsx
import { use as _$use } from "@opentui/solid";
import { createComponent as _$createComponent } from "@opentui/solid";
import { createTextNode as _$createTextNode } from "@opentui/solid";
import { insertNode as _$insertNode } from "@opentui/solid";
import { effect as _$effect } from "@opentui/solid";
import { insert as _$insert } from "@opentui/solid";
import { setProp as _$setProp } from "@opentui/solid";
import { createElement as _$createElement } from "@opentui/solid";
import { TextAttributes } from "@opentui/core";
import { createEffect, Show } from "solid-js";
function truncateWithEllipsis(text, maxWidth) {
if (maxWidth <= 0)
return "";
if (text.length <= maxWidth)
return text;
return text.substring(0, Math.max(0, maxWidth - 1)) + "\u2026";
}
function itemIndexAtScreenY(el, screenY) {
const internals = el;
if (!internals.linesPerItem)
return null;
const localY = screenY - el.screenY;
if (localY < 0)
return null;
const index = internals.scrollOffset + Math.floor(localY / internals.linesPerItem);
return index >= 0 && index < el.options.length ? index : null;
}
var BORDER_WIDTH = 2;
var ITEM_LEFT_PADDING = 1;
var ITEM_INDICATOR_WIDTH = 2;
function Sidebar2(props) {
let select;
const contentWidth = () => Math.max(0, (props.width ?? 24) - BORDER_WIDTH);
const itemTextWidth = () => Math.max(0, contentWidth() - ITEM_LEFT_PADDING - ITEM_INDICATOR_WIDTH);
const displayTitle = () => props.title ? truncateWithEllipsis(props.title, contentWidth()) : props.title;
const displayItems = () => props.items.map((item) => ({
...item,
name: truncateWithEllipsis(item.name, itemTextWidth()),
description: truncateWithEllipsis(item.description, itemTextWidth())
}));
const handleMouseDown = (event) => {
if (!select)
return;
const index = itemIndexAtScreenY(select, event.y);
if (index !== null) {
select.focus();
select.setSelectedIndex(index);
select.selectCurrent();
event.preventDefault();
}
};
const handleMouseScroll = (event) => {
if (!select || !event.scroll)
return;
const steps = Math.max(1, event.scroll.delta);
if (event.scroll.direction === "down")
select.moveDown(steps);
else if (event.scroll.direction === "up")
select.moveUp(steps);
};
createEffect(() => {
const index = props.items.findIndex((item) => item.value === props.value);
if (select && index !== -1 && select.getSelectedIndex() !== index)
select.setSelectedIndex(index);
});
return (() => {
var _el$ = _$createElement("box"), _el$5 = _$createElement("select");
_$insertNode(_el$, _el$5);
_$setProp(_el$, "border", true);
_$setProp(_el$, "flexDirection", "column");
_$insert(_el$, _$createComponent(Show, {
get when() {
return props.title;
},
get children() {
return [(() => {
var _el$2 = _$createElement("text");
_$insert(_el$2, displayTitle);
_$effect((_$p) => _$setProp(_el$2, "attributes", TextAttributes.BOLD, _$p));
return _el$2;
})(), (() => {
var _el$3 = _$createElement("text");
_$insertNode(_el$3, _$createTextNode(` `));
return _el$3;
})()];
}
}), _el$5);
_$use((el) => {
select = el ?? undefined;
props.ref?.(el && {
moveUp: () => el.moveUp(),
moveDown: () => el.moveDown(),
selectCurrent: () => el.selectCurrent(),
focus: () => el.focus()
});
}, _el$5);
_$setProp(_el$5, "flexGrow", 1);
_$setProp(_el$5, "showScrollIndicator", true);
_$setProp(_el$5, "onChange", (_index, option) => {
if (!option)
return;
props.onSelect(option);
});
_$setProp(_el$5, "onSelect", (_index, option) => {
if (!option)
return;
props.onConfirm?.(option);
});
_$effect((_p$) => {
var _v$ = props.width ?? 24, _v$2 = theme2.borderStyle, _v$3 = props.focused ? props.accentColor ?? theme2.primary : props.mutedColor ?? theme2.muted, _v$4 = props.focused, _v$5 = displayItems(), _v$6 = theme2.mouse ? handleMouseDown : undefined, _v$7 = theme2.mouse ? handleMouseScroll : undefined, _v$8 = props.backgroundColor ?? "transparent", _v$9 = props.textColor ?? presets2.select.textColor, _v$0 = props.focusedBackgroundColor ?? "transparent", _v$1 = props.focusedTextColor ?? presets2.select.focusedTextColor, _v$10 = props.selectedBackgroundColor ?? presets2.select.selectedBackgroundColor, _v$11 = props.selectedTextColor ?? presets2.select.selectedTextColor, _v$12 = props.descriptionColor ?? presets2.select.descriptionColor, _v$13 = props.selectedDescriptionColor ?? presets2.select.selectedDescriptionColor;
_v$ !== _p$.e && (_p$.e = _$setProp(_el$, "width", _v$, _p$.e));
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$, "borderStyle", _v$2, _p$.t));
_v$3 !== _p$.a && (_p$.a = _$setProp(_el$, "borderColor", _v$3, _p$.a));
_v$4 !== _p$.o && (_p$.o = _$setProp(_el$5, "focused", _v$4, _p$.o));
_v$5 !== _p$.i && (_p$.i = _$setProp(_el$5, "options", _v$5, _p$.i));
_v$6 !== _p$.n && (_p$.n = _$setProp(_el$5, "onMouseDown", _v$6, _p$.n));
_v$7 !== _p$.s && (_p$.s = _$setProp(_el$5, "onMouseScroll", _v$7, _p$.s));
_v$8 !== _p$.h && (_p$.h = _$setProp(_el$5, "backgroundColor", _v$8, _p$.h));
_v$9 !== _p$.r && (_p$.r = _$setProp(_el$5, "textColor", _v$9, _p$.r));
_v$0 !== _p$.d && (_p$.d = _$setProp(_el$5, "focusedBackgroundColor", _v$0, _p$.d));
_v$1 !== _p$.l && (_p$.l = _$setProp(_el$5, "focusedTextColor", _v$1, _p$.l));
_v$10 !== _p$.u && (_p$.u = _$setProp(_el$5, "selectedBackgroundColor", _v$10, _p$.u));
_v$11 !== _p$.c && (_p$.c = _$setProp(_el$5, "selectedTextColor", _v$11, _p$.c));
_v$12 !== _p$.w && (_p$.w = _$setProp(_el$5, "descriptionColor", _v$12, _p$.w));
_v$13 !== _p$.m && (_p$.m = _$setProp(_el$5, "selectedDescriptionColor", _v$13, _p$.m));
return _p$;
}, {
e: undefined,
t: undefined,
a: undefined,
o: undefined,
i: undefined,
n: undefined,
s: undefined,
h: undefined,
r: undefined,
d: undefined,
l: undefined,
u: undefined,
c: undefined,
w: undefined,
m: undefined
});
return _el$;
})();
}
export { Sidebar2 };
+71
View File
@@ -0,0 +1,71 @@
// @bun
import {
theme2,
presets2
} from "./chunk-p6jvscj3.js";
// src/components/ListRow.tsx
import { effect as _$effect } from "@opentui/solid";
import { createComponent as _$createComponent } from "@opentui/solid";
import { insertNode as _$insertNode } from "@opentui/solid";
import { insert as _$insert } from "@opentui/solid";
import { setProp as _$setProp } from "@opentui/solid";
import { createElement as _$createElement } from "@opentui/solid";
import { TextAttributes } from "@opentui/core";
import { Show } from "solid-js";
function ListRow2(props) {
const tone = (fallback) => props.selected ? presets2.select.selectedTextColor : fallback;
return (() => {
var _el$ = _$createElement("box"), _el$2 = _$createElement("box"), _el$3 = _$createElement("box"), _el$4 = _$createElement("text");
_$insertNode(_el$, _el$2);
_$setProp(_el$, "flexShrink", 0);
_$setProp(_el$, "flexDirection", "column");
_$setProp(_el$, "paddingLeft", 1);
_$setProp(_el$, "paddingRight", 1);
_$insertNode(_el$2, _el$3);
_$setProp(_el$2, "flexDirection", "row");
_$setProp(_el$2, "justifyContent", "space-between");
_$setProp(_el$2, "alignItems", "center");
_$setProp(_el$2, "height", 1);
_$insertNode(_el$3, _el$4);
_$setProp(_el$3, "flexGrow", 1);
_$setProp(_el$3, "flexShrink", 1);
_$setProp(_el$3, "minWidth", 0);
_$setProp(_el$3, "marginRight", 1);
_$setProp(_el$3, "overflow", "hidden");
_$setProp(_el$4, "wrapMode", "none");
_$insert(_el$4, () => props.label);
_$insert(_el$2, _$createComponent(Show, {
get when() {
return props.right;
},
get children() {
var _el$5 = _$createElement("box");
_$setProp(_el$5, "flexShrink", 0);
_$setProp(_el$5, "flexDirection", "row");
_$setProp(_el$5, "alignItems", "center");
_$insert(_el$5, () => props.right?.(tone));
return _el$5;
}
}), null);
_$insert(_el$, () => props.below?.(tone), null);
_$effect((_p$) => {
var _v$ = props.id, _v$2 = props.selected ? presets2.select.selectedBackgroundColor : "transparent", _v$3 = theme2.mouse && props.onClick ? () => props.onClick?.() : undefined, _v$4 = tone(), _v$5 = props.bold ? TextAttributes.BOLD : undefined;
_v$ !== _p$.e && (_p$.e = _$setProp(_el$, "id", _v$, _p$.e));
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$, "backgroundColor", _v$2, _p$.t));
_v$3 !== _p$.a && (_p$.a = _$setProp(_el$, "onMouseDown", _v$3, _p$.a));
_v$4 !== _p$.o && (_p$.o = _$setProp(_el$4, "fg", _v$4, _p$.o));
_v$5 !== _p$.i && (_p$.i = _$setProp(_el$4, "attributes", _v$5, _p$.i));
return _p$;
}, {
e: undefined,
t: undefined,
a: undefined,
o: undefined,
i: undefined
});
return _el$;
})();
}
export { ListRow2 };
+140
View File
@@ -0,0 +1,140 @@
// @bun
import {
theme2
} from "./chunk-p6jvscj3.js";
// src/components/Modal.tsx
import { insertNode as _$insertNode } from "@opentui/solid";
import { effect as _$effect } from "@opentui/solid";
import { insert as _$insert } from "@opentui/solid";
import { use as _$use } from "@opentui/solid";
import { setProp as _$setProp } from "@opentui/solid";
import { createElement as _$createElement } from "@opentui/solid";
import { createComponent as _$createComponent } from "@opentui/solid";
import { RGBA } from "@opentui/core";
import { useBindings } from "@opentui/keymap/solid";
import { Portal, useRenderer, useTerminalDimensions } from "@opentui/solid";
import { createSignal, onCleanup, onMount, Show } from "solid-js";
function Modal2(props) {
return _$createComponent(Portal, {
get children() {
return _$createComponent(Show, {
get when() {
return props.open;
},
get children() {
return _$createComponent(ModalPanel, props);
}
});
}
});
}
function ModalPanel(props) {
const renderer = useRenderer();
const dimensions = useTerminalDimensions();
const [panel, setPanel] = createSignal();
const screenCap = () => Math.max(5, dimensions().height - 4);
const maxPanelHeight = () => Math.min(props.maxHeight ?? screenCap(), screenCap());
const maxContentHeight = () => Math.max(1, maxPanelHeight() - 4);
const maxPanelWidth = () => Math.max(10, dimensions().width - 4);
const panelWidth = () => Math.min(props.width ?? 50, maxPanelWidth());
let previousFocus = null;
onMount(() => {
previousFocus = renderer.currentFocusedRenderable;
props.onOpen?.(previousFocus);
panel()?.focus();
});
onCleanup(() => {
if (previousFocus && !previousFocus.isDestroyed)
previousFocus.focus();
});
const dismissKey = props.dismissKey ?? "escape";
if (dismissKey !== false || props.commands || props.bindings) {
useBindings(() => ({
target: panel,
commands: [...dismissKey !== false ? [{
name: "modal_dismiss",
desc: "close",
shortHelp: true,
run: () => props.onDismiss?.()
}] : [], ...props.commands ?? []],
bindings: [...dismissKey !== false ? [{
key: dismissKey,
cmd: "modal_dismiss"
}] : [], ...props.bindings ?? []]
}));
}
return (() => {
var _el$ = _$createElement("box"), _el$2 = _$createElement("box");
_$insertNode(_el$, _el$2);
_$setProp(_el$, "position", "absolute");
_$setProp(_el$, "top", 0);
_$setProp(_el$, "left", 0);
_$setProp(_el$, "alignItems", "center");
_$setProp(_el$, "justifyContent", "center");
_$setProp(_el$, "zIndex", 2000);
_$use(setPanel, _el$2);
_$setProp(_el$2, "focusable", true);
_$setProp(_el$2, "border", true);
_$setProp(_el$2, "flexDirection", "column");
_$setProp(_el$2, "paddingLeft", 2);
_$setProp(_el$2, "paddingRight", 2);
_$setProp(_el$2, "paddingTop", 1);
_$setProp(_el$2, "paddingBottom", 1);
_$insert(_el$2, _$createComponent(Show, {
get when() {
return props.scrollable;
},
get fallback() {
return (() => {
var _el$4 = _$createElement("box");
_$setProp(_el$4, "flexDirection", "column");
_$setProp(_el$4, "overflow", "hidden");
_$insert(_el$4, () => props.children);
_$effect((_$p) => _$setProp(_el$4, "maxHeight", maxContentHeight(), _$p));
return _el$4;
})();
},
get children() {
var _el$3 = _$createElement("scrollbox");
_$setProp(_el$3, "flexShrink", 1);
_$setProp(_el$3, "scrollY", true);
_$setProp(_el$3, "scrollX", false);
_$setProp(_el$3, "focusable", false);
_$insert(_el$3, () => props.children);
_$effect((_$p) => _$setProp(_el$3, "maxHeight", maxContentHeight(), _$p));
return _el$3;
}
}));
_$effect((_p$) => {
var _v$ = dimensions().width, _v$2 = dimensions().height, _v$3 = props.backdropColor ?? RGBA.fromInts(0, 0, 0, 150), _v$4 = theme2.mouse ? props.onDismiss : undefined, _v$5 = panelWidth(), _v$6 = maxPanelHeight(), _v$7 = theme2.borderStyle, _v$8 = props.accentColor ?? theme2.primary, _v$9 = props.accentColor ?? theme2.primary, _v$0 = props.backgroundColor ?? theme2.background, _v$1 = theme2.mouse ? (event) => event.stopPropagation() : undefined;
_v$ !== _p$.e && (_p$.e = _$setProp(_el$, "width", _v$, _p$.e));
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$, "height", _v$2, _p$.t));
_v$3 !== _p$.a && (_p$.a = _$setProp(_el$, "backgroundColor", _v$3, _p$.a));
_v$4 !== _p$.o && (_p$.o = _$setProp(_el$, "onMouseDown", _v$4, _p$.o));
_v$5 !== _p$.i && (_p$.i = _$setProp(_el$2, "width", _v$5, _p$.i));
_v$6 !== _p$.n && (_p$.n = _$setProp(_el$2, "maxHeight", _v$6, _p$.n));
_v$7 !== _p$.s && (_p$.s = _$setProp(_el$2, "borderStyle", _v$7, _p$.s));
_v$8 !== _p$.h && (_p$.h = _$setProp(_el$2, "borderColor", _v$8, _p$.h));
_v$9 !== _p$.r && (_p$.r = _$setProp(_el$2, "focusedBorderColor", _v$9, _p$.r));
_v$0 !== _p$.d && (_p$.d = _$setProp(_el$2, "backgroundColor", _v$0, _p$.d));
_v$1 !== _p$.l && (_p$.l = _$setProp(_el$2, "onMouseDown", _v$1, _p$.l));
return _p$;
}, {
e: undefined,
t: undefined,
a: undefined,
o: undefined,
i: undefined,
n: undefined,
s: undefined,
h: undefined,
r: undefined,
d: undefined,
l: undefined
});
return _el$;
})();
}
export { Modal2 };
+32
View File
@@ -0,0 +1,32 @@
// @bun
import {
theme2
} from "./chunk-p6jvscj3.js";
// src/components/SignalBars.tsx
import { effect as _$effect } from "@opentui/solid";
import { insert as _$insert } from "@opentui/solid";
import { createComponent as _$createComponent } from "@opentui/solid";
import { setProp as _$setProp } from "@opentui/solid";
import { createElement as _$createElement } from "@opentui/solid";
import { For } from "solid-js";
var BARS = ["\u2581", "\u2583", "\u2585", "\u2587"];
function SignalBars2(props) {
const level = () => Math.floor(props.signal * BARS.length / 100);
return (() => {
var _el$ = _$createElement("box");
_$setProp(_el$, "flexDirection", "row");
_$insert(_el$, _$createComponent(For, {
each: BARS,
children: (bar, i) => (() => {
var _el$2 = _$createElement("text");
_$insert(_el$2, bar);
_$effect((_$p) => _$setProp(_el$2, "fg", i() < level() ? props.filledColor : props.emptyColor ?? theme2.muted, _$p));
return _el$2;
})()
}));
return _el$;
})();
}
export { SignalBars2 };
+83
View File
@@ -0,0 +1,83 @@
// @bun
import {
theme2
} from "./chunk-p6jvscj3.js";
// src/components/Badge.tsx
import { insert as _$insert } from "@opentui/solid";
import { memo as _$memo } from "@opentui/solid";
import { createComponent as _$createComponent } from "@opentui/solid";
import { effect as _$effect } from "@opentui/solid";
import { createTextNode as _$createTextNode } from "@opentui/solid";
import { insertNode as _$insertNode } from "@opentui/solid";
import { setProp as _$setProp } from "@opentui/solid";
import { createElement as _$createElement } from "@opentui/solid";
import { TextAttributes } from "@opentui/core";
import { Show } from "solid-js";
function Badge2(props) {
const textColor = () => props.textColor ?? theme2.background;
const backgroundColor = () => props.backgroundColor ?? theme2.background;
const attributes = () => props.bold === false ? undefined : TextAttributes.BOLD;
const nerdFonts = () => props.withNerdfont ?? theme2.nerdFonts;
return (() => {
var _el$ = _$createElement("box"), _el$4 = _$createElement("text");
_$insertNode(_el$, _el$4);
_$setProp(_el$, "flexDirection", "row");
_$insert(_el$, _$createComponent(Show, {
get when() {
return nerdFonts();
},
get children() {
var _el$2 = _$createElement("text");
_$insertNode(_el$2, _$createTextNode(`\uE0B6`));
_$effect((_p$) => {
var _v$ = props.color, _v$2 = backgroundColor();
_v$ !== _p$.e && (_p$.e = _$setProp(_el$2, "fg", _v$, _p$.e));
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$2, "bg", _v$2, _p$.t));
return _p$;
}, {
e: undefined,
t: undefined
});
return _el$2;
}
}), _el$4);
_$insert(_el$4, (() => {
var _c$ = _$memo(() => !!nerdFonts());
return () => _c$() ? props.label : ` ${props.label} `;
})());
_$insert(_el$, _$createComponent(Show, {
get when() {
return nerdFonts();
},
get children() {
var _el$5 = _$createElement("text");
_$insertNode(_el$5, _$createTextNode(`\uE0B4`));
_$effect((_p$) => {
var _v$3 = props.color, _v$4 = backgroundColor();
_v$3 !== _p$.e && (_p$.e = _$setProp(_el$5, "fg", _v$3, _p$.e));
_v$4 !== _p$.t && (_p$.t = _$setProp(_el$5, "bg", _v$4, _p$.t));
return _p$;
}, {
e: undefined,
t: undefined
});
return _el$5;
}
}), null);
_$effect((_p$) => {
var _v$5 = textColor(), _v$6 = props.color, _v$7 = attributes();
_v$5 !== _p$.e && (_p$.e = _$setProp(_el$4, "fg", _v$5, _p$.e));
_v$6 !== _p$.t && (_p$.t = _$setProp(_el$4, "bg", _v$6, _p$.t));
_v$7 !== _p$.a && (_p$.a = _$setProp(_el$4, "attributes", _v$7, _p$.a));
return _p$;
}, {
e: undefined,
t: undefined,
a: undefined
});
return _el$;
})();
}
export { Badge2 };
+42
View File
@@ -0,0 +1,42 @@
// @bun
import {
useFocusWithin2
} from "./chunk-59dzyjgd.js";
// src/components/Pane.tsx
import { createComponent as _$createComponent } from "@opentui/solid";
import { insert as _$insert } from "@opentui/solid";
import { use as _$use } from "@opentui/solid";
import { spread as _$spread } from "@opentui/solid";
import { createElement as _$createElement } from "@opentui/solid";
import { createContext, createSignal, splitProps, useContext } from "solid-js";
var PaneContext = createContext();
function Pane2(props) {
const [local, rest] = splitProps(props, ["handle", "children"]);
const [el, setEl] = createSignal();
const focused = useFocusWithin2(el);
const handle = {
target: el,
focused,
focus: () => el()?.focus()
};
local.handle?.(handle);
return _$createComponent(PaneContext.Provider, {
value: handle,
get children() {
var _el$ = _$createElement("box");
_$use(setEl, _el$);
_$spread(_el$, rest, true);
_$insert(_el$, () => local.children);
return _el$;
}
});
}
function usePane2() {
const pane = useContext(PaneContext);
if (!pane)
throw new Error("usePane() must be used inside a <Pane>");
return pane;
}
export { Pane2, usePane2 };
+168
View File
@@ -0,0 +1,168 @@
// @bun
import {
theme2
} from "./chunk-p6jvscj3.js";
// src/components/Tabs.tsx
import { effect as _$effect } from "@opentui/solid";
import { insertNode as _$insertNode } from "@opentui/solid";
import { createComponent as _$createComponent } from "@opentui/solid";
import { insert as _$insert } from "@opentui/solid";
import { use as _$use } from "@opentui/solid";
import { setProp as _$setProp } from "@opentui/solid";
import { createElement as _$createElement } from "@opentui/solid";
import { BorderChars, CliRenderEvents, TextAttributes } from "@opentui/core";
import { onResize, useRenderer } from "@opentui/solid";
import { createMemo, createSignal, Index } from "solid-js";
function stepTabValue2(items, value, delta) {
const index = items.findIndex((t) => t.value === value);
if (index === -1)
return value;
return items[(index + delta + items.length) % items.length].value;
}
function innerWidth(label) {
return [...label].length + 2;
}
function segmentWidth(label) {
return innerWidth(label) + 2;
}
function topEdge(chars, label) {
return `${chars.topLeft}${chars.horizontal.repeat(innerWidth(label))}${chars.topRight}`;
}
function bottomEdge(chars, label, isFirst, isLast, isActive) {
const left = isFirst ? isActive ? chars.vertical : chars.leftT : isActive ? chars.bottomRight : chars.bottomT;
const right = isLast ? isActive ? chars.vertical : chars.rightT : isActive ? chars.bottomLeft : chars.bottomT;
const fill = isActive ? " " : chars.horizontal;
return left + fill.repeat(innerWidth(label)) + right;
}
function buildBottomRow(chars, segments, available) {
const tabsWidth = segments.reduce((sum, s) => sum + segmentWidth(s.label), 0);
const remainder = available - tabsWidth;
const extending = remainder > 0;
const tabs = segments.map((s, i) => bottomEdge(chars, s.label, i === 0, i === segments.length - 1 && !extending, s.isActive)).join("");
return extending ? tabs + chars.horizontal.repeat(remainder - 1) + chars.topRight : tabs;
}
function fitCount(items, start, available) {
const hiddenBefore = start;
let used = 0;
let count = 0;
for (let i = start;i < items.length; i++) {
const width = segmentWidth(items[i].label);
const hiddenAfter = items.length - start - count - 1;
const totalHidden = hiddenBefore + hiddenAfter;
const reserve = totalHidden > 0 ? segmentWidth(`+${totalHidden}`) : 0;
if (used + width + reserve > available)
break;
used += width;
count++;
}
return Math.max(count, 1);
}
function Tabs2(props) {
const renderer = useRenderer();
const [width, setWidth] = createSignal(0);
let container;
const measure = () => {
if (!container)
return;
setWidth(container.width);
};
const remeasureNextFrame = () => renderer.once(CliRenderEvents.FRAME, measure);
onResize(remeasureNextFrame);
const segments = createMemo(() => {
const items = props.items;
const activeIndex = Math.max(0, items.findIndex((t) => t.value === props.value));
let start = 0;
let count = fitCount(items, start, width());
if (activeIndex >= start + count) {
start = activeIndex;
count = fitCount(items, start, width());
}
const shownItems = items.slice(start, start + count);
const shown = shownItems.map((item) => ({
value: item.value,
label: item.label,
isActive: item.value === props.value
}));
const hidden = items.length - count;
if (hidden > 0) {
const used = shownItems.reduce((sum, item) => sum + segmentWidth(item.label), 0);
const tag = `+${hidden}`;
if (shown.length === 0 || used + segmentWidth(tag) <= width()) {
shown.push({
value: undefined,
label: tag,
isActive: false
});
}
}
return shown;
});
const accent = () => props.accentColor ?? theme2.primary;
const muted = () => props.mutedColor ?? theme2.muted;
const borderColor = () => props.focused ? accent() : muted();
const chars = BorderChars[theme2.borderStyle];
return (() => {
var _el$ = _$createElement("box"), _el$2 = _$createElement("text"), _el$3 = _$createElement("box"), _el$4 = _$createElement("text");
_$insertNode(_el$, _el$2);
_$insertNode(_el$, _el$3);
_$insertNode(_el$, _el$4);
_$use((el) => {
container = el;
remeasureNextFrame();
}, _el$);
_$setProp(_el$, "flexDirection", "column");
_$setProp(_el$, "height", 3);
_$setProp(_el$2, "selectable", false);
_$insert(_el$2, () => segments().map((s) => topEdge(chars, s.label)).join(""));
_$setProp(_el$3, "flexDirection", "row");
_$insert(_el$3, _$createComponent(Index, {
get each() {
return segments();
},
children: (s) => [(() => {
var _el$5 = _$createElement("text");
_$setProp(_el$5, "selectable", false);
_$insert(_el$5, () => chars.vertical);
_$effect((_$p) => _$setProp(_el$5, "fg", borderColor(), _$p));
return _el$5;
})(), (() => {
var _el$6 = _$createElement("text");
_$setProp(_el$6, "selectable", false);
_$insert(_el$6, () => ` ${s().label} `);
_$effect((_p$) => {
var _v$3 = s().isActive ? accent() : muted(), _v$4 = s().isActive ? TextAttributes.BOLD : undefined, _v$5 = theme2.mouse && s().value !== undefined ? () => props.onChange(s().value) : undefined;
_v$3 !== _p$.e && (_p$.e = _$setProp(_el$6, "fg", _v$3, _p$.e));
_v$4 !== _p$.t && (_p$.t = _$setProp(_el$6, "attributes", _v$4, _p$.t));
_v$5 !== _p$.a && (_p$.a = _$setProp(_el$6, "onMouseDown", _v$5, _p$.a));
return _p$;
}, {
e: undefined,
t: undefined,
a: undefined
});
return _el$6;
})(), (() => {
var _el$7 = _$createElement("text");
_$setProp(_el$7, "selectable", false);
_$insert(_el$7, () => chars.vertical);
_$effect((_$p) => _$setProp(_el$7, "fg", borderColor(), _$p));
return _el$7;
})()]
}));
_$setProp(_el$4, "selectable", false);
_$insert(_el$4, () => buildBottomRow(chars, segments(), width()));
_$effect((_p$) => {
var _v$ = borderColor(), _v$2 = borderColor();
_v$ !== _p$.e && (_p$.e = _$setProp(_el$2, "fg", _v$, _p$.e));
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$4, "fg", _v$2, _p$.t));
return _p$;
}, {
e: undefined,
t: undefined
});
return _el$;
})();
}
export { stepTabValue2, Tabs2 };
+52
View File
@@ -0,0 +1,52 @@
// @bun
import {
theme2
} from "./chunk-p6jvscj3.js";
import {
Box2
} from "./chunk-8r4rajaz.js";
import {
Tabs2
} from "./chunk-vf2ybhtx.js";
// src/components/TabbedPanel.tsx
import { insert as _$insert } from "@opentui/solid";
import { memo as _$memo } from "@opentui/solid";
import { createComponent as _$createComponent } from "@opentui/solid";
import { setProp as _$setProp } from "@opentui/solid";
import { createElement as _$createElement } from "@opentui/solid";
function TabbedPanel2(props) {
return (() => {
var _el$ = _$createElement("box");
_$setProp(_el$, "flexDirection", "column");
_$setProp(_el$, "flexGrow", 1);
_$insert(_el$, _$createComponent(Tabs2, {
get items() {
return props.items;
},
get value() {
return props.value;
},
get onChange() {
return props.onChange;
},
get focused() {
return props.focused;
}
}), null);
_$insert(_el$, _$createComponent(Box2, {
flexGrow: 1,
border: ["left", "right", "bottom"],
get borderColor() {
return _$memo(() => !!props.focused)() ? theme2.primary : theme2.muted;
},
flexDirection: "column",
get children() {
return props.children;
}
}), null);
return _el$;
})();
}
export { TabbedPanel2 };
+10
View File
@@ -0,0 +1,10 @@
// @bun
// src/yaml.ts
import { homedir } from "os";
import { join } from "path";
function configPath2() {
const configDir = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
return join(configDir, "ilovetui", "config.yaml");
}
export { configPath2 };
+44
View File
@@ -0,0 +1,44 @@
// @bun
// src/presets.ts
function buildPresets2(theme) {
const select = {
backgroundColor: theme.background,
textColor: theme.text,
focusedBackgroundColor: theme.subtleBg,
focusedTextColor: theme.text,
selectedBackgroundColor: theme.selection,
selectedTextColor: theme.primary,
descriptionColor: theme.muted,
selectedDescriptionColor: theme.text
};
const tabSelect = {
backgroundColor: theme.background,
textColor: theme.text,
focusedBackgroundColor: theme.subtleBg,
focusedTextColor: theme.text,
selectedBackgroundColor: theme.selection,
selectedTextColor: theme.primary,
selectedDescriptionColor: theme.text
};
const input = {
backgroundColor: theme.background,
textColor: theme.text,
focusedBackgroundColor: theme.subtleBg,
focusedTextColor: theme.text,
placeholderColor: theme.muted
};
const box = {
border: true,
borderStyle: theme.borderStyle,
borderColor: theme.subtle,
focusedBorderColor: theme.primary,
backgroundColor: theme.background
};
const slider = {
backgroundColor: theme.subtleBg,
foregroundColor: theme.primary
};
return { select, tabSelect, input, textarea: input, box, slider };
}
export { buildPresets2 };
+58
View File
@@ -0,0 +1,58 @@
// @bun
import {
theme2
} from "./chunk-p6jvscj3.js";
// src/components/ProgressBar.tsx
import { effect as _$effect } from "@opentui/solid";
import { memo as _$memo } from "@opentui/solid";
import { insert as _$insert } from "@opentui/solid";
import { createComponent as _$createComponent } from "@opentui/solid";
import { setProp as _$setProp } from "@opentui/solid";
import { createElement as _$createElement } from "@opentui/solid";
import { parseColor, RGBA } from "@opentui/core";
import { createMemo, For } from "solid-js";
function sampleGradient(stops, t) {
if (stops.length === 1)
return stops[0];
const clamped = Math.min(Math.max(t, 0), 1);
const segment = clamped * (stops.length - 1);
const i = Math.min(Math.floor(segment), stops.length - 2);
const localT = segment - i;
const [ar, ag, ab, aa] = parseColor(stops[i]).toInts();
const [br, bg, bb, ba] = parseColor(stops[i + 1]).toInts();
return RGBA.fromInts(Math.round(ar + (br - ar) * localT), Math.round(ag + (bg - ag) * localT), Math.round(ab + (bb - ab) * localT), Math.round(aa + (ba - aa) * localT));
}
function ProgressBar2(props) {
const width = () => props.width ?? 20;
const stops = createMemo(() => Array.isArray(props.color) ? props.color : [props.color ?? theme2.primary]);
const filled = createMemo(() => Math.round(width() * Math.min(Math.max(props.value, 0), 100) / 100));
const cells = createMemo(() => Array.from({
length: width()
}, (_, i) => i));
return (() => {
var _el$ = _$createElement("box");
_$setProp(_el$, "flexDirection", "row");
_$insert(_el$, _$createComponent(For, {
get each() {
return cells();
},
children: (i) => {
const isFilled = () => i < filled();
const t = width() > 1 ? i / (width() - 1) : 0;
return (() => {
var _el$2 = _$createElement("text");
_$insert(_el$2, (() => {
var _c$ = _$memo(() => !!isFilled());
return () => _c$() ? props.fillChar ?? "\u2588" : props.trackChar ?? "\u2591";
})());
_$effect((_$p) => _$setProp(_el$2, "fg", isFilled() ? sampleGradient(stops(), t) : props.trackColor ?? theme2.muted, _$p));
return _el$2;
})();
}
}));
return _el$;
})();
}
export { ProgressBar2 };
+12
View File
@@ -0,0 +1,12 @@
// @bun
import {
Badge2
} from "../chunk-v9cetqzr.js";
import"../chunk-p6jvscj3.js";
import"../chunk-yyg967q8.js";
import"../chunk-njvj322f.js";
import"../chunk-6ggk5fg7.js";
import"../chunk-xykm10y9.js";
export {
Badge2 as Badge
};
+12
View File
@@ -0,0 +1,12 @@
// @bun
import {
Box2
} from "../chunk-8r4rajaz.js";
import"../chunk-p6jvscj3.js";
import"../chunk-yyg967q8.js";
import"../chunk-njvj322f.js";
import"../chunk-6ggk5fg7.js";
import"../chunk-xykm10y9.js";
export {
Box2 as Box
};
+12
View File
@@ -0,0 +1,12 @@
// @bun
import {
HelpBar2
} from "../chunk-c1dx8cn3.js";
import"../chunk-p6jvscj3.js";
import"../chunk-yyg967q8.js";
import"../chunk-njvj322f.js";
import"../chunk-6ggk5fg7.js";
import"../chunk-xykm10y9.js";
export {
HelpBar2 as HelpBar
};
+14
View File
@@ -0,0 +1,14 @@
// @bun
import {
HelpModal2
} from "../chunk-ersd5rrs.js";
import"../chunk-t2cszvff.js";
import"../chunk-p6jvscj3.js";
import"../chunk-yyg967q8.js";
import"../chunk-njvj322f.js";
import"../chunk-6ggk5fg7.js";
import"../chunk-xykm10y9.js";
import"../chunk-ee2qaswx.js";
export {
HelpModal2 as HelpModal
};
+12
View File
@@ -0,0 +1,12 @@
// @bun
import {
KeyValue2
} from "../chunk-c4gp0x16.js";
import"../chunk-p6jvscj3.js";
import"../chunk-yyg967q8.js";
import"../chunk-njvj322f.js";
import"../chunk-6ggk5fg7.js";
import"../chunk-xykm10y9.js";
export {
KeyValue2 as KeyValue
};
+12
View File
@@ -0,0 +1,12 @@
// @bun
import {
ListRow2
} from "../chunk-sh9n4zha.js";
import"../chunk-p6jvscj3.js";
import"../chunk-yyg967q8.js";
import"../chunk-njvj322f.js";
import"../chunk-6ggk5fg7.js";
import"../chunk-xykm10y9.js";
export {
ListRow2 as ListRow
};
+12
View File
@@ -0,0 +1,12 @@
// @bun
import {
MinSizeGuard2
} from "../chunk-qdyd8q9r.js";
import"../chunk-p6jvscj3.js";
import"../chunk-yyg967q8.js";
import"../chunk-njvj322f.js";
import"../chunk-6ggk5fg7.js";
import"../chunk-xykm10y9.js";
export {
MinSizeGuard2 as MinSizeGuard
};
+12
View File
@@ -0,0 +1,12 @@
// @bun
import {
Modal2
} from "../chunk-t2cszvff.js";
import"../chunk-p6jvscj3.js";
import"../chunk-yyg967q8.js";
import"../chunk-njvj322f.js";
import"../chunk-6ggk5fg7.js";
import"../chunk-xykm10y9.js";
export {
Modal2 as Modal
};
+12
View File
@@ -0,0 +1,12 @@
// @bun
import {
NotificationHost2
} from "../chunk-ajb193xj.js";
import"../chunk-p6jvscj3.js";
import"../chunk-yyg967q8.js";
import"../chunk-njvj322f.js";
import"../chunk-6ggk5fg7.js";
import"../chunk-xykm10y9.js";
export {
NotificationHost2 as NotificationHost
};
+10
View File
@@ -0,0 +1,10 @@
// @bun
import {
Pane2,
usePane2
} from "../chunk-vazj633p.js";
import"../chunk-59dzyjgd.js";
export {
Pane2 as Pane,
usePane2 as usePane
};
+12
View File
@@ -0,0 +1,12 @@
// @bun
import {
ProgressBar2
} from "../chunk-z734n0wr.js";
import"../chunk-p6jvscj3.js";
import"../chunk-yyg967q8.js";
import"../chunk-njvj322f.js";
import"../chunk-6ggk5fg7.js";
import"../chunk-xykm10y9.js";
export {
ProgressBar2 as ProgressBar
};
+81
View File
@@ -0,0 +1,81 @@
// @bun
import"../chunk-njvj322f.js";
import"../chunk-6ggk5fg7.js";
import"../chunk-yyg967q8.js";
import"../chunk-xykm10y9.js";
import {
theme2
} from "../chunk-p6jvscj3.js";
import {
Spinner2
} from "../chunk-kmqqzz9e.js";
// src/components/SectionHeader.tsx
import { createTextNode as _$createTextNode } from "@opentui/solid";
import { createComponent as _$createComponent } from "@opentui/solid";
import { effect as _$effect } from "@opentui/solid";
import { insertNode as _$insertNode } from "@opentui/solid";
import { insert as _$insert } from "@opentui/solid";
import { memo as _$memo } from "@opentui/solid";
import { setProp as _$setProp } from "@opentui/solid";
import { createElement as _$createElement } from "@opentui/solid";
import { TextAttributes } from "@opentui/core";
import { Show } from "solid-js";
function SectionHeader(props) {
return (() => {
var _el$ = _$createElement("box"), _el$2 = _$createElement("box"), _el$3 = _$createElement("text"), _el$4 = _$createElement("box");
_$insertNode(_el$, _el$2);
_$insertNode(_el$, _el$4);
_$setProp(_el$, "flexShrink", 0);
_$setProp(_el$, "flexDirection", "row");
_$setProp(_el$, "justifyContent", "space-between");
_$setProp(_el$, "alignItems", "center");
_$setProp(_el$, "height", 1);
_$insertNode(_el$2, _el$3);
_$setProp(_el$2, "flexGrow", 1);
_$setProp(_el$2, "flexShrink", 1);
_$setProp(_el$2, "minWidth", 0);
_$setProp(_el$2, "marginRight", 1);
_$setProp(_el$2, "overflow", "hidden");
_$setProp(_el$3, "wrapMode", "none");
_$insert(_el$3, () => props.title ?? "");
_$setProp(_el$4, "flexShrink", 0);
_$insert(_el$4, _$createComponent(Show, {
get when() {
return !props.loading;
},
get fallback() {
return (() => {
var _el$6 = _$createElement("box"), _el$7 = _$createElement("text"), _el$8 = _$createTextNode(` `);
_$insertNode(_el$6, _el$7);
_$setProp(_el$6, "flexDirection", "row");
_$setProp(_el$6, "alignItems", "center");
_$insert(_el$6, _$createComponent(Spinner2, {}), _el$7);
_$insertNode(_el$7, _el$8);
_$insert(_el$7, () => props.loadingLabel ?? "Loading\u2026", null);
_$effect((_$p) => _$setProp(_el$7, "fg", theme2.muted, _$p));
return _el$6;
})();
},
get children() {
var _el$5 = _$createElement("text");
_$insert(_el$5, () => props.hint ?? "");
_$effect((_$p) => _$setProp(_el$5, "fg", theme2.muted, _$p));
return _el$5;
}
}));
_$effect((_p$) => {
var _v$ = theme2.muted, _v$2 = TextAttributes.BOLD;
_v$ !== _p$.e && (_p$.e = _$setProp(_el$3, "fg", _v$, _p$.e));
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$3, "attributes", _v$2, _p$.t));
return _p$;
}, {
e: undefined,
t: undefined
});
return _el$;
})();
}
export {
SectionHeader
};
+12
View File
@@ -0,0 +1,12 @@
// @bun
import {
Sidebar2
} from "../chunk-rcywynbw.js";
import"../chunk-p6jvscj3.js";
import"../chunk-yyg967q8.js";
import"../chunk-njvj322f.js";
import"../chunk-6ggk5fg7.js";
import"../chunk-xykm10y9.js";
export {
Sidebar2 as Sidebar
};
+12
View File
@@ -0,0 +1,12 @@
// @bun
import {
SignalBars2
} from "../chunk-tz3vvffs.js";
import"../chunk-p6jvscj3.js";
import"../chunk-yyg967q8.js";
import"../chunk-njvj322f.js";
import"../chunk-6ggk5fg7.js";
import"../chunk-xykm10y9.js";
export {
SignalBars2 as SignalBars
};
+12
View File
@@ -0,0 +1,12 @@
// @bun
import {
Spinner2
} from "../chunk-kmqqzz9e.js";
import"../chunk-p6jvscj3.js";
import"../chunk-yyg967q8.js";
import"../chunk-njvj322f.js";
import"../chunk-6ggk5fg7.js";
import"../chunk-xykm10y9.js";
export {
Spinner2 as Spinner
};
+14
View File
@@ -0,0 +1,14 @@
// @bun
import {
TabbedPanel2
} from "../chunk-wtbg0e39.js";
import"../chunk-vf2ybhtx.js";
import"../chunk-8r4rajaz.js";
import"../chunk-p6jvscj3.js";
import"../chunk-yyg967q8.js";
import"../chunk-njvj322f.js";
import"../chunk-6ggk5fg7.js";
import"../chunk-xykm10y9.js";
export {
TabbedPanel2 as TabbedPanel
};
+14
View File
@@ -0,0 +1,14 @@
// @bun
import {
stepTabValue2,
Tabs2
} from "../chunk-vf2ybhtx.js";
import"../chunk-p6jvscj3.js";
import"../chunk-yyg967q8.js";
import"../chunk-njvj322f.js";
import"../chunk-6ggk5fg7.js";
import"../chunk-xykm10y9.js";
export {
Tabs2 as Tabs,
stepTabValue2 as stepTabValue
};
+13
View File
@@ -0,0 +1,13 @@
// @bun
import {
Toggle2
} from "../chunk-qhp6k176.js";
import"../chunk-v9cetqzr.js";
import"../chunk-p6jvscj3.js";
import"../chunk-yyg967q8.js";
import"../chunk-njvj322f.js";
import"../chunk-6ggk5fg7.js";
import"../chunk-xykm10y9.js";
export {
Toggle2 as Toggle
};
+18
View File
@@ -0,0 +1,18 @@
// @bun
import"../chunk-njvj322f.js";
import"../chunk-6ggk5fg7.js";
import"../chunk-yyg967q8.js";
import"../chunk-xykm10y9.js";
import"../chunk-p6jvscj3.js";
import {
HelpBar2
} from "../chunk-c1dx8cn3.js";
import"../chunk-ee2qaswx.js";
import"../chunk-t2cszvff.js";
import {
HelpModal2
} from "../chunk-ersd5rrs.js";
export {
HelpBar2 as HelpBar,
HelpModal2 as HelpModal
};
+69
View File
@@ -0,0 +1,69 @@
// @bun
import"../chunk-njvj322f.js";
import"../chunk-6ggk5fg7.js";
import"../chunk-yyg967q8.js";
import"../chunk-xykm10y9.js";
import"../chunk-p6jvscj3.js";
import {
Badge2
} from "../chunk-v9cetqzr.js";
import {
Box2
} from "../chunk-8r4rajaz.js";
import {
KeyValue2
} from "../chunk-c4gp0x16.js";
import {
ListRow2
} from "../chunk-sh9n4zha.js";
import {
MinSizeGuard2
} from "../chunk-qdyd8q9r.js";
import {
Modal2
} from "../chunk-t2cszvff.js";
import {
NotificationHost2
} from "../chunk-ajb193xj.js";
import"../chunk-59dzyjgd.js";
import {
Pane2,
usePane2
} from "../chunk-vazj633p.js";
import {
ProgressBar2
} from "../chunk-z734n0wr.js";
import {
Sidebar2
} from "../chunk-rcywynbw.js";
import {
SignalBars2
} from "../chunk-tz3vvffs.js";
import {
stepTabValue2,
Tabs2
} from "../chunk-vf2ybhtx.js";
import {
TabbedPanel2
} from "../chunk-wtbg0e39.js";
import {
Toggle2
} from "../chunk-qhp6k176.js";
export {
Badge2 as Badge,
Box2 as Box,
KeyValue2 as KeyValue,
ListRow2 as ListRow,
MinSizeGuard2 as MinSizeGuard,
Modal2 as Modal,
NotificationHost2 as NotificationHost,
Pane2 as Pane,
ProgressBar2 as ProgressBar,
Sidebar2 as Sidebar,
SignalBars2 as SignalBars,
TabbedPanel2 as TabbedPanel,
Tabs2 as Tabs,
Toggle2 as Toggle,
stepTabValue2 as stepTabValue,
usePane2 as usePane
};
+17
View File
@@ -0,0 +1,17 @@
// @bun
import {
COLOR_KEYS2,
OPENTUI_BORDER_STYLES2,
ThemeConfigSchema2,
layerConfigs2,
resolveConfig2,
normalizeColor2
} from "./chunk-njvj322f.js";
export {
COLOR_KEYS2 as COLOR_KEYS,
OPENTUI_BORDER_STYLES2 as OPENTUI_BORDER_STYLES,
ThemeConfigSchema2 as ThemeConfigSchema,
layerConfigs2 as layerConfigs,
normalizeColor2 as normalizeColor,
resolveConfig2 as resolveConfig
};
+9
View File
@@ -0,0 +1,9 @@
// @bun
import {
isInside2,
useFocusWithin2
} from "../chunk-59dzyjgd.js";
export {
isInside2 as isInside,
useFocusWithin2 as useFocusWithin
};
+9
View File
@@ -0,0 +1,9 @@
// @bun
import {
helpOpen2,
toggleHelp2
} from "../chunk-ee2qaswx.js";
export {
helpOpen2 as helpOpen,
toggleHelp2 as toggleHelp
};
+7
View File
@@ -0,0 +1,7 @@
// @bun
import {
createSimpleContext2
} from "../chunk-jza406qg.js";
export {
createSimpleContext2 as createSimpleContext
};
+27
View File
@@ -0,0 +1,27 @@
// @bun
import {
createSimpleContext2
} from "../chunk-jza406qg.js";
import {
notifications2,
notify2,
dismiss2
} from "../chunk-6ggk5fg7.js";
import {
helpOpen2,
toggleHelp2
} from "../chunk-ee2qaswx.js";
import {
isInside2,
useFocusWithin2
} from "../chunk-59dzyjgd.js";
export {
createSimpleContext2 as createSimpleContext,
dismiss2 as dismiss,
helpOpen2 as helpOpen,
isInside2 as isInside,
notifications2 as notifications,
notify2 as notify,
toggleHelp2 as toggleHelp,
useFocusWithin2 as useFocusWithin
};
+11
View File
@@ -0,0 +1,11 @@
// @bun
import {
notifications2,
notify2,
dismiss2
} from "../chunk-6ggk5fg7.js";
export {
dismiss2 as dismiss,
notifications2 as notifications,
notify2 as notify
};
+10 -3
View File
@@ -2,11 +2,18 @@
# Copy to ~/.config/ilovetui/config.yaml and edit to customize. # Copy to ~/.config/ilovetui/config.yaml and edit to customize.
# Whether components may use Nerd Font glyphs (requires a patched font). # Whether components may use Nerd Font glyphs (requires a patched font).
# Leave false for maximum terminal/font compatibility. nerd_fonts: true
nerd_fonts: false
# 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 style used by panels across all ilovetui-based TUIs.
# One of: rounded, normal, thick, double, hidden, ascii.
border: rounded border: rounded
colors: colors:
+23
View File
@@ -0,0 +1,23 @@
// @bun
import {
theme2,
presets2,
configureTheme2,
icon2
} from "./chunk-p6jvscj3.js";
import"./chunk-yyg967q8.js";
import {
ThemeConfigSchema2
} from "./chunk-njvj322f.js";
import"./chunk-6ggk5fg7.js";
import {
configPath2
} from "./chunk-xykm10y9.js";
export {
ThemeConfigSchema2 as ThemeConfigSchema,
configPath2 as configPath,
configureTheme2 as configureTheme,
icon2 as icon,
presets2 as presets,
theme2 as theme
};
+27
View File
@@ -0,0 +1,27 @@
// @bun
// src/keymap/provider.tsx
import { createComponent as _$createComponent } from "@opentui/solid";
import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui";
import { registerCommaBindings } from "@opentui/keymap/addons";
import { KeymapProvider as BaseKeymapProvider } from "@opentui/keymap/solid";
import { useRenderer } from "@opentui/solid";
import { onCleanup } from "solid-js";
import { useKeymap, useBindings, useKeymapSelector, reactiveMatcherFromSignal } from "@opentui/keymap/solid";
function KeymapProvider(props) {
const renderer = useRenderer();
const keymap = createDefaultOpenTuiKeymap(renderer);
onCleanup(registerCommaBindings(keymap));
return _$createComponent(BaseKeymapProvider, {
keymap,
get children() {
return props.children;
}
});
}
export {
KeymapProvider,
reactiveMatcherFromSignal,
useBindings,
useKeymap,
useKeymapSelector
};
+7
View File
@@ -0,0 +1,7 @@
// @bun
import {
buildPresets2
} from "./chunk-yyg967q8.js";
export {
buildPresets2 as buildPresets
};
+117
View File
@@ -0,0 +1,117 @@
// @bun
import {
notify2
} from "./chunk-6ggk5fg7.js";
// src/project-config.ts
import { existsSync, readFileSync } from "fs";
import { homedir } from "os";
import { join } from "path";
import { parse as parseYAML } from "yaml";
import { z } from "zod";
function userConfigPath(projectName) {
const configHome = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
return join(configHome, projectName, "config.yaml");
}
function readYaml(path) {
return parseYAML(readFileSync(path, "utf-8"));
}
function warn(projectName, path, message) {
const text = `${path}: ${message}`;
console.error(`[${projectName} config] ${text}`);
notify2(text, { kind: "warning", duration: 0 });
}
function loadProjectConfig(options) {
const { projectName, defaultConfigPath, defaultSchema, userSchema } = options;
let rawDefaults;
try {
rawDefaults = readYaml(defaultConfigPath);
} catch (error) {
throw new Error(`Failed to read ${defaultConfigPath}: ${error instanceof Error ? error.message : error}`);
}
const defaultsResult = defaultSchema.safeParse(rawDefaults);
if (!defaultsResult.success) {
throw new Error(`${defaultConfigPath} is invalid:
${z.prettifyError(defaultsResult.error)}`);
}
const path = userConfigPath(projectName);
const user = (() => {
if (!existsSync(path))
return {};
let raw;
try {
raw = readYaml(path);
} catch (error) {
warn(projectName, path, error instanceof Error ? error.message : String(error));
return {};
}
const result = userSchema.safeParse(raw ?? {});
if (!result.success) {
warn(projectName, path, z.prettifyError(result.error));
return {};
}
return result.data;
})();
return { defaults: defaultsResult.data, user };
}
function isPlainObject(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function deepMerge(base, over) {
if (!isPlainObject(base) || !isPlainObject(over))
return over === undefined ? base : over;
const out = { ...base };
for (const [key, value] of Object.entries(over))
out[key] = deepMerge(base[key], value);
return out;
}
function loadMergedProjectConfig(options) {
const { projectName, defaultConfigPath, schema } = options;
let rawDefaults;
try {
rawDefaults = readYaml(defaultConfigPath);
} catch (error) {
throw new Error(`Failed to read ${defaultConfigPath}: ${error instanceof Error ? error.message : error}`);
}
const defaults = schema.safeParse(rawDefaults);
if (!defaults.success) {
throw new Error(`${defaultConfigPath} is invalid:
${z.prettifyError(defaults.error)}`);
}
const path = userConfigPath(projectName);
if (!existsSync(path))
return defaults.data;
let rawUser;
try {
rawUser = readYaml(path);
} catch (error) {
warn(projectName, path, error instanceof Error ? error.message : String(error));
return defaults.data;
}
const merged = schema.safeParse(deepMerge(rawDefaults, rawUser ?? {}));
if (!merged.success) {
warn(projectName, path, z.prettifyError(merged.error));
return defaults.data;
}
return merged.data;
}
function keybindsSchema(names, required) {
const shape = {};
for (const name of names) {
shape[name] = required ? z.string().min(1) : z.string().min(1).optional();
}
return z.object(shape).strict();
}
function mergeKeybinds(names, defaults, user) {
const merged = {};
for (const name of names) {
merged[name] = user?.[name] ?? defaults[name];
}
return merged;
}
export {
keybindsSchema,
loadMergedProjectConfig,
loadProjectConfig,
mergeKeybinds
};
+48
View File
@@ -0,0 +1,48 @@
// @bun
import"./chunk-njvj322f.js";
import"./chunk-6ggk5fg7.js";
import"./chunk-yyg967q8.js";
import"./chunk-xykm10y9.js";
import {
theme2,
presets2
} from "./chunk-p6jvscj3.js";
// src/solid.ts
import { extend } from "@opentui/solid";
import {
ASCIIFontRenderable,
BoxRenderable,
InputRenderable,
SelectRenderable,
SliderRenderable,
TabSelectRenderable,
TextareaRenderable,
TextRenderable
} from "@opentui/core";
function withThemeDefaults(Ctor, defaults, after) {
class Themed extends Ctor {
constructor(...args) {
super(...args);
Object.assign(this, defaults());
after?.(this);
}
}
return Themed;
}
var boxDefaults = () => {
const { borderStyle, borderColor, focusedBorderColor } = presets2.box;
return { borderStyle, borderColor, focusedBorderColor };
};
extend({
box: withThemeDefaults(BoxRenderable, boxDefaults, (instance) => {
instance.border = false;
}),
select: withThemeDefaults(SelectRenderable, () => presets2.select),
tab_select: withThemeDefaults(TabSelectRenderable, () => presets2.tabSelect),
input: withThemeDefaults(InputRenderable, () => presets2.input),
textarea: withThemeDefaults(TextareaRenderable, () => presets2.textarea),
slider: withThemeDefaults(SliderRenderable, () => presets2.slider),
text: withThemeDefaults(TextRenderable, () => ({ fg: theme2.text })),
ascii_font: withThemeDefaults(ASCIIFontRenderable, () => ({ color: theme2.primary }))
});
+7
View File
@@ -0,0 +1,7 @@
// @bun
import {
configPath2
} from "./chunk-xykm10y9.js";
export {
configPath2 as configPath
};
-13
View File
@@ -1,13 +0,0 @@
# drawer
A full-height panel flush against the left or right edge of an already-rendered background, on top
of it dimmed. The sidebar/drawer equivalent of [`modal`](../modal/README.md), which it otherwise
mirrors closely: same `tea.Msg`-triggered stack (`ShowMsg`/`Show`), same composite-over-a-string
`Render`, same rule that content is a `tea.Model`.
- `WithSide(Left|Right)` and `WithWidth` are per-drawer `Show` options; width otherwise shrinks to
fit content, capped by the `Model`'s `WithMaxWidth`.
- The stack is a plain LIFO. Only the topmost drawer is updated; `Close()` closes it.
- `View(width, height)` renders on a blank background of that size, for use as a standalone pane.
See `examples/drawer`.
-61
View File
@@ -1,61 +0,0 @@
package drawer
import tea "charm.land/bubbletea/v2"
type Side int
const (
Left Side = iota
Right
)
type Drawer struct {
Title string
Content tea.Model
Side Side
Width int
Style *Styles
}
type DrawerOption func(*Drawer)
func WithSide(s Side) DrawerOption {
return func(d *Drawer) { d.Side = s }
}
func WithWidth(w int) DrawerOption {
return func(d *Drawer) { d.Width = w }
}
func WithDrawerStyle(s Styles) DrawerOption {
return func(d *Drawer) { d.Style = &s }
}
func newDrawer(title string, content tea.Model, opts ...DrawerOption) Drawer {
d := Drawer{Title: title, Content: content}
for _, opt := range opts {
opt(&d)
}
return d
}
type ShowMsg struct{ Drawer Drawer }
func Show(title string, content tea.Model, opts ...DrawerOption) tea.Cmd {
d := newDrawer(title, content, opts...)
return func() tea.Msg { return ShowMsg{Drawer: d} }
}
type DismissMsg struct{}
func Close() tea.Cmd {
return func() tea.Msg { return DismissMsg{} }
}
type text string
func (t text) Init() tea.Cmd { return nil }
func (t text) Update(tea.Msg) (tea.Model, tea.Cmd) { return t, nil }
func (t text) View() tea.View { return tea.NewView(string(t)) }
func Text(s string) tea.Model { return text(s) }
-75
View File
@@ -1,75 +0,0 @@
package drawer
import tea "charm.land/bubbletea/v2"
type Model struct {
drawers []Drawer
nextID int
maxWidth int
styles Styles
}
type Option func(*Model)
func WithMaxWidth(w int) Option {
return func(m *Model) { m.maxWidth = w }
}
func WithStyles(s Styles) Option {
return func(m *Model) { m.styles = s }
}
func New(opts ...Option) Model {
m := Model{
maxWidth: 30,
styles: DefaultStyles(),
}
for _, opt := range opts {
opt(&m)
}
return m
}
func (m Model) Init() tea.Cmd { return nil }
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
switch msg := msg.(type) {
case ShowMsg:
return m.show(msg.Drawer)
case DismissMsg:
return m.pop(), nil
}
return m.updateTop(msg)
}
func (m Model) updateTop(msg tea.Msg) (Model, tea.Cmd) {
i := len(m.drawers) - 1
if i < 0 || m.drawers[i].Content == nil {
return m, nil
}
var cmd tea.Cmd
m.drawers[i].Content, cmd = m.drawers[i].Content.Update(msg)
return m, cmd
}
func (m Model) Open() bool { return len(m.drawers) > 0 }
func (m Model) show(d Drawer) (Model, tea.Cmd) {
m.drawers = append(m.drawers, d)
return m, initContent(d)
}
func initContent(d Drawer) tea.Cmd {
if d.Content == nil {
return nil
}
return d.Content.Init()
}
func (m Model) pop() Model {
if len(m.drawers) == 0 {
return m
}
m.drawers = m.drawers[:len(m.drawers)-1]
return m
}
-119
View File
@@ -1,119 +0,0 @@
package drawer
import (
"image/color"
"strings"
"charm.land/lipgloss/v2"
"github.com/charmbracelet/x/ansi"
"github.com/anotherhadi/ilovetui/style"
)
func (m Model) Render(background string) string {
if len(m.drawers) == 0 {
return background
}
result := background
for _, d := range m.drawers {
w, h := lipgloss.Width(result), lipgloss.Height(result)
if w <= 0 || h <= 0 {
return result
}
result = m.renderOne(d, result, w, h)
}
return result
}
func (m Model) View(width, height int) string {
return m.Render(blank(width, height))
}
func (m Model) renderOne(d Drawer, background string, w, h int) string {
s := m.styles
if d.Style != nil {
s = *d.Style
}
box := m.renderBox(d, s, w, h)
bw := lipgloss.Width(box)
x := 0
if d.Side == Right {
x = max(w-bw, 0)
}
compositor := lipgloss.NewCompositor(
lipgloss.NewLayer(dim(background, s.DimColor)),
lipgloss.NewLayer(box).X(x).Y(0).Z(1),
)
return compositor.Render()
}
func dim(s string, c color.Color) string {
return lipgloss.NewStyle().Foreground(c).Render(ansi.Strip(s))
}
func (m Model) renderBox(d Drawer, s Styles, bgW, bgH int) string {
widthCap := m.maxWidth
if d.Width > 0 {
widthCap = d.Width
}
maxW := effectiveMax(widthCap, bgW)
body := contentView(d)
inner := contentWidth(d, body, maxW)
content := s.Content.Width(inner).Render(body)
boxWidth := inner + 4
return style.RenderWithTitle(s.Border, s.Title.Render(d.Title), content, boxWidth, bgH)
}
func contentView(d Drawer) string {
if d.Content == nil {
return ""
}
return d.Content.View().Content
}
func contentWidth(d Drawer, body string, maxWidth int) int {
capped := max(maxWidth-4, 1)
if d.Width > 0 {
return capped
}
natural := max(naturalWidth(body), lipgloss.Width(d.Title), 1)
return min(natural, capped)
}
func naturalWidth(content string) int {
w := 0
for _, line := range strings.Split(content, "\n") {
if lw := lipgloss.Width(line); lw > w {
w = lw
}
}
return w
}
func effectiveMax(configured, fits int) int {
if fits < 1 {
fits = 1
}
if configured > 0 && configured < fits {
return configured
}
return fits
}
func blank(width, height int) string {
if width <= 0 || height <= 0 {
return ""
}
line := strings.Repeat(" ", width)
lines := make([]string, height)
for i := range lines {
lines[i] = line
}
return strings.Join(lines, "\n")
}
-68
View File
@@ -1,68 +0,0 @@
package drawer
import (
"strings"
"testing"
"charm.land/lipgloss/v2"
)
func background(w, h int) string {
return blank(w, h)
}
func TestRenderLeftFlushToLeftEdge(t *testing.T) {
m := New(WithMaxWidth(10))
m, _ = m.Update(ShowMsg{Drawer: newDrawer("Nav", Text("hi"), WithSide(Left))})
out := m.Render(background(40, 10))
lines := strings.Split(out, "\n")
if len(lines) != 10 {
t.Fatalf("expected 10 lines, got %d", len(lines))
}
if lipgloss.Width(out) == 0 {
t.Fatalf("expected non-empty render")
}
if len([]rune(lines[0])) == 0 {
t.Fatalf("expected a rendered top border line")
}
}
func TestRenderRightFlushToRightEdge(t *testing.T) {
m := New(WithMaxWidth(10))
m, _ = m.Update(ShowMsg{Drawer: newDrawer("Inspector", Text("hi"), WithSide(Right))})
out := m.Render(background(40, 10))
if lipgloss.Width(out) != 40 {
t.Fatalf("expected full background width 40, got %d", lipgloss.Width(out))
}
}
func TestSpansFullBackgroundHeight(t *testing.T) {
m := New()
m, _ = m.Update(ShowMsg{Drawer: newDrawer("Nav", Text("hi"))})
out := m.Render(background(40, 12))
if lipgloss.Height(out) != 12 {
t.Fatalf("expected full height 12, got %d", lipgloss.Height(out))
}
}
func TestFixedWidthHonored(t *testing.T) {
m := New(WithMaxWidth(50))
m, _ = m.Update(ShowMsg{Drawer: newDrawer("Nav", Text("x"), WithWidth(20))})
box := m.renderBox(m.drawers[0], m.styles, 80, 10)
if got := lipgloss.Width(box); got != 20 {
t.Fatalf("expected fixed box width 20, got %d", got)
}
}
func TestNoDrawersReturnsBackgroundUnchanged(t *testing.T) {
m := New()
bg := background(10, 5)
if got := m.Render(bg); got != bg {
t.Fatalf("expected background unchanged when no drawer is open")
}
}
-25
View File
@@ -1,25 +0,0 @@
package drawer
import (
"image/color"
"charm.land/lipgloss/v2"
"github.com/anotherhadi/ilovetui/style"
)
type Styles struct {
Border lipgloss.Style
Title lipgloss.Style
Content lipgloss.Style
DimColor color.Color
}
func DefaultStyles() Styles {
return Styles{
Border: style.S.PanelFocused.Padding(0, 1),
Title: lipgloss.NewStyle().Bold(true).Foreground(style.S.Primary),
Content: lipgloss.NewStyle().Foreground(style.S.Text),
DimColor: style.S.Subtle,
}
}
-48
View File
@@ -1,48 +0,0 @@
package main
import (
"fmt"
"os"
"charm.land/bubbles/v2/spinner"
"charm.land/bubbles/v2/textinput"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
"github.com/anotherhadi/ilovetui/bubbles"
)
type model struct {
input textinput.Model
spinner spinner.Model
}
func newModel() model {
ti := bubbles.NewTextInput()
ti.Placeholder = "type something"
ti.Focus()
return model{input: ti, spinner: bubbles.NewSpinner()}
}
func (m model) Init() tea.Cmd { return m.spinner.Tick }
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if k, ok := msg.(tea.KeyPressMsg); ok && k.String() == "ctrl+c" {
return m, tea.Quit
}
var inputCmd, spinnerCmd tea.Cmd
m.input, inputCmd = m.input.Update(msg)
m.spinner, spinnerCmd = m.spinner.Update(msg)
return m, tea.Batch(inputCmd, spinnerCmd)
}
func (m model) View() tea.View {
return tea.NewView(lipgloss.JoinHorizontal(lipgloss.Center, m.spinner.View(), " ", m.input.View()))
}
func main() {
if _, err := tea.NewProgram(newModel()).Run(); err != nil {
fmt.Println("Error running program:", err)
os.Exit(1)
}
}
-55
View File
@@ -1,55 +0,0 @@
package main
import (
"fmt"
"os"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
"github.com/anotherhadi/ilovetui/drawer"
)
type model struct {
d drawer.Model
width, height int
}
func newModel() model { return model{d: drawer.New()} }
func (m model) Init() tea.Cmd { return m.d.Init() }
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width, m.height = msg.Width, msg.Height
case tea.KeyPressMsg:
switch msg.String() {
case "q":
return m, tea.Quit
case "l":
return m, drawer.Show("Nav", drawer.Text("Home\nSettings"), drawer.WithSide(drawer.Left))
case "esc":
if m.d.Open() {
return m, drawer.Close()
}
}
}
var cmd tea.Cmd
m.d, cmd = m.d.Update(msg)
return m, cmd
}
func (m model) View() tea.View {
background := lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, "l: open q: quit")
view := tea.NewView(m.d.Render(background))
view.AltScreen = true
return view
}
func main() {
if _, err := tea.NewProgram(newModel()).Run(); err != nil {
fmt.Println("Error running program:", err)
os.Exit(1)
}
}
-73
View File
@@ -1,73 +0,0 @@
package main
import (
"fmt"
"os"
"charm.land/bubbles/v2/key"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
"github.com/anotherhadi/ilovetui/helpbar"
)
type keyMap struct {
Inc, Help, Quit key.Binding
}
func defaultKeyMap() keyMap {
return keyMap{
Inc: key.NewBinding(key.WithKeys("+"), key.WithHelp("+", "increment")),
Help: key.NewBinding(key.WithKeys("?"), key.WithHelp("?", "help")),
Quit: key.NewBinding(key.WithKeys("q"), key.WithHelp("q", "quit")),
}
}
type model struct {
help helpbar.Model
keys keyMap
n, w int
}
func newModel() model {
keys := defaultKeyMap()
return model{
keys: keys,
help: helpbar.New(helpbar.WithToggle(keys.Help), helpbar.WithGlobal(keys.Quit)),
}
}
func (m model) Init() tea.Cmd { return nil }
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.w = msg.Width
m.help.SetWidth(m.w)
case tea.KeyPressMsg:
switch {
case key.Matches(msg, m.keys.Quit):
return m, tea.Quit
case key.Matches(msg, m.keys.Inc):
m.n++
default:
m.help, _ = m.help.Update(msg)
}
}
return m, nil
}
func (m model) View() tea.View {
bar := m.help.View(m.keys.Inc)
body := lipgloss.Place(m.w, 1, lipgloss.Center, lipgloss.Top, fmt.Sprintf("count: %d", m.n))
view := tea.NewView(lipgloss.JoinVertical(lipgloss.Left, body, bar))
view.AltScreen = true
return view
}
func main() {
if _, err := tea.NewProgram(newModel()).Run(); err != nil {
fmt.Println("Error running program:", err)
os.Exit(1)
}
}
-55
View File
@@ -1,55 +0,0 @@
package main
import (
"fmt"
"os"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
"github.com/anotherhadi/ilovetui/modal"
)
type model struct {
m modal.Model
width, height int
}
func newModel() model { return model{m: modal.New()} }
func (m model) Init() tea.Cmd { return m.m.Init() }
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width, m.height = msg.Width, msg.Height
case tea.KeyPressMsg:
switch msg.String() {
case "q":
return m, tea.Quit
case "o":
return m, modal.Show("Hello", modal.Text("This is a modal.\n\nesc: close"))
case "esc":
if m.m.Open() {
return m, modal.Close()
}
}
}
var cmd tea.Cmd
m.m, cmd = m.m.Update(msg)
return m, cmd
}
func (m model) View() tea.View {
background := lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, "o: open q: quit")
view := tea.NewView(m.m.Render(background))
view.AltScreen = true
return view
}
func main() {
if _, err := tea.NewProgram(newModel()).Run(); err != nil {
fmt.Println("Error running program:", err)
os.Exit(1)
}
}
-51
View File
@@ -1,51 +0,0 @@
package main
import (
"fmt"
"os"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
"github.com/anotherhadi/ilovetui/notification"
)
type model struct {
notif notification.Model
width, height int
}
func newModel() model { return model{notif: notification.New()} }
func (m model) Init() tea.Cmd { return m.notif.Init() }
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width, m.height = msg.Width, msg.Height
case tea.KeyPressMsg:
switch msg.String() {
case "q":
return m, tea.Quit
case "s":
return m, notification.Show("Saved", "Config written to disk.", notification.Success)
}
}
var cmd tea.Cmd
m.notif, cmd = m.notif.Update(msg)
return m, cmd
}
func (m model) View() tea.View {
background := lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, "s: show q: quit")
view := tea.NewView(m.notif.Render(background))
view.AltScreen = true
return view
}
func main() {
if _, err := tea.NewProgram(newModel()).Run(); err != nil {
fmt.Println("Error running program:", err)
os.Exit(1)
}
}
-42
View File
@@ -1,42 +0,0 @@
package main
import (
"fmt"
"os"
tea "charm.land/bubbletea/v2"
"github.com/anotherhadi/ilovetui/style"
)
type model struct{ focused bool }
func (m model) Init() tea.Cmd { return nil }
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyPressMsg:
switch msg.String() {
case "q":
return m, tea.Quit
case "tab":
m.focused = !m.focused
}
}
return m, nil
}
func (m model) View() tea.View {
box := style.S.Panel
if m.focused {
box = style.S.PanelFocused
}
return tea.NewView(style.RenderWithTitle(box, "Panel", "tab: toggle focus q: quit", 30, 5))
}
func main() {
if _, err := tea.NewProgram(model{}).Run(); err != nil {
fmt.Println("Error running program:", err)
os.Exit(1)
}
}
-55
View File
@@ -1,55 +0,0 @@
package main
import (
"fmt"
"os"
tea "charm.land/bubbletea/v2"
"github.com/anotherhadi/ilovetui/tabs"
)
type pane string
func (p pane) Init() tea.Cmd { return nil }
func (p pane) Update(tea.Msg) (tabs.Tab, tea.Cmd) { return p, nil }
func (p pane) View() string { return string(p) }
type model struct{ tabs tabs.Model }
func newModel() model {
items := []tabs.Item{
{Title: "First", Model: pane("first content")},
{Title: "Second", Model: pane("second content")},
{Title: "Third", Model: pane("third content")},
}
return model{tabs: tabs.New(items)}
}
func (m model) Init() tea.Cmd { return m.tabs.Init() }
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if k, ok := msg.(tea.KeyPressMsg); ok && k.String() == "q" {
return m, tea.Quit
}
if s, ok := msg.(tea.WindowSizeMsg); ok {
m.tabs.SetSize(s.Width, s.Height)
msg = tea.WindowSizeMsg{Width: m.tabs.ContentWidth(), Height: m.tabs.ContentHeight()}
}
var cmd tea.Cmd
m.tabs, cmd = m.tabs.Update(msg)
return m, cmd
}
func (m model) View() tea.View {
view := tea.NewView(m.tabs.View())
view.AltScreen = true
return view
}
func main() {
if _, err := tea.NewProgram(newModel()).Run(); err != nil {
fmt.Println("Error running program:", err)
os.Exit(1)
}
}
Generated
-87
View File
@@ -1,87 +0,0 @@
{
"nodes": {
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1767039857,
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
"owner": "NixOS",
"repo": "flake-compat",
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "flake-compat",
"type": "github"
}
},
"git-hooks": {
"inputs": {
"flake-compat": "flake-compat",
"gitignore": "gitignore",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1778507602,
"narHash": "sha256-kTwur1wV+01SdqskVMSo6JMEpg71ps3HpbFY2GsflKs=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "61ab0e80d9c7ab14c256b5b453d8b3fb0189ba0a",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "git-hooks.nix",
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
"git-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1709087332,
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1779560665,
"narHash": "sha256-tpyBcxPpcQb8ukyNF7DoCwfSY3VPsxHoYwj00Cayv5o=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "64c08a7ca051951c8eae34e3e3cb1e202fe36786",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"git-hooks": "git-hooks",
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}
-30
View File
@@ -1,30 +0,0 @@
{
description = "A shared Base16 theme, a themed wrapper around every official component, and a small set of custom Bubble Tea components, in one Go module, so every TUI built with it shares one config file and looks consistent.";
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};
};
});
};
}
-43
View File
@@ -1,43 +0,0 @@
module github.com/anotherhadi/ilovetui
go 1.25.8
require (
charm.land/bubbles/v2 v2.1.0
charm.land/bubbletea/v2 v2.0.2
charm.land/glamour/v2 v2.0.0
charm.land/lipgloss/v2 v2.0.3
github.com/charmbracelet/x/ansi v0.11.7
gopkg.in/yaml.v3 v3.0.1
)
require (
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/harmonica v0.2.0 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // 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/dustin/go-humanize v1.0.1 // 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/sahilm/fuzzy v0.1.1 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yuin/goldmark v1.7.8 // indirect
github.com/yuin/goldmark-emoji v1.0.5 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/text v0.24.0 // indirect
)
-87
View File
@@ -1,87 +0,0 @@
charm.land/bubbles/v2 v2.1.0 h1:YSnNh5cPYlYjPxRrzs5VEn3vwhtEn3jVGRBT3M7/I0g=
charm.land/bubbles/v2 v2.1.0/go.mod h1:l97h4hym2hvWBVfmJDtrEHHCtkIKeTEb3TTJ4ZOB3wY=
charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0=
charm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=
charm.land/glamour/v2 v2.0.0 h1:IDBoqLEy7Hdpb9VOXN+khLP/XSxtJy1VsHuW/yF87+U=
charm.land/glamour/v2 v2.0.0/go.mod h1:kjq9WB0s8vuUYZNYey2jp4Lgd9f4cKdzAw88FZtpj/w=
charm.land/lipgloss/v2 v2.0.3 h1:yM2zJ4Cf5Y51b7RHIwioil4ApI/aypFXXVHSwlM6RzU=
charm.land/lipgloss/v2 v2.0.3/go.mod h1:7myLU9iG/3xluAWzpY/fSxYYHCgoKTie7laxk6ATwXA=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
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/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-13
View File
@@ -1,13 +0,0 @@
# helpbar
A responsive help bar: one line of key bindings that expands, on toggle, into a multi-column view
reflowed to use as many columns as the available width allows.
- Bindings render in order: the `WithToggle` binding first, then `WithGlobal` bindings (set once),
then contextual bindings passed to `View` at render time.
- Disabled bindings (`key.Binding.SetEnabled(false)`) are dropped before layout, so the reflow never
budgets width for something that won't be drawn.
- `Height(contextual...)` always matches `lipgloss.Height` of `View` for the same arguments, so a
host can reserve exactly the right amount of space. An empty bar renders `""` and takes 0 rows.
See `examples/helpbar`.
-132
View File
@@ -1,132 +0,0 @@
package helpbar
import (
"strings"
"charm.land/bubbles/v2/help"
"charm.land/bubbles/v2/key"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
"github.com/charmbracelet/x/ansi"
"github.com/anotherhadi/ilovetui/bubbles"
)
type Model struct {
ShowAll bool
help help.Model
global []key.Binding
toggle key.Binding
width int
}
type Option func(*Model)
func WithGlobal(bindings ...key.Binding) Option {
return func(m *Model) { m.global = bindings }
}
func WithToggle(b key.Binding) Option {
return func(m *Model) { m.toggle = b }
}
func WithStyles(s help.Styles) Option {
return func(m *Model) { m.help.Styles = s }
}
func New(opts ...Option) Model {
m := Model{help: bubbles.NewHelp()}
for _, opt := range opts {
opt(&m)
}
return m
}
func (m *Model) SetWidth(w int) {
m.width = w
m.help.SetWidth(w)
}
func (m Model) Width() int { return m.width }
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
if keyMsg, ok := msg.(tea.KeyPressMsg); ok && key.Matches(keyMsg, m.toggle) {
m.ShowAll = !m.ShowAll
}
return m, nil
}
func (m Model) View(contextual ...key.Binding) string {
bindings := m.bindings(contextual)
if len(bindings) == 0 || m.width <= 0 {
return ""
}
var view string
if !m.ShowAll {
view = m.help.ShortHelpView(bindings)
} else {
view = m.help.FullHelpView(m.columns(bindings))
}
return m.clampWidth(view)
}
func (m Model) clampWidth(view string) string {
lines := strings.Split(view, "\n")
for i, line := range lines {
lines[i] = ansi.Truncate(line, m.width, "")
}
return strings.Join(lines, "\n")
}
func (m Model) Height(contextual ...key.Binding) int {
view := m.View(contextual...)
if view == "" {
return 0
}
return lipgloss.Height(view)
}
func (m Model) bindings(contextual []key.Binding) []key.Binding {
all := make([]key.Binding, 0, 1+len(m.global)+len(contextual))
if m.toggle.Enabled() {
all = append(all, m.toggle)
}
for _, b := range append(append([]key.Binding{}, m.global...), contextual...) {
if b.Enabled() {
all = append(all, b)
}
}
return all
}
func (m Model) columns(bindings []key.Binding) [][]key.Binding {
if m.width <= 0 {
return [][]key.Binding{bindings}
}
for rows := 1; rows < len(bindings); rows++ {
groups := chunkColumns(bindings, rows)
if m.renderedWidth(groups) <= m.width {
return groups
}
}
return chunkColumns(bindings, len(bindings))
}
func (m Model) renderedWidth(groups [][]key.Binding) int {
unbounded := m.help
unbounded.SetWidth(0)
return lipgloss.Width(unbounded.FullHelpView(groups))
}
func chunkColumns(bindings []key.Binding, rows int) [][]key.Binding {
if rows < 1 {
rows = 1
}
groups := make([][]key.Binding, 0, (len(bindings)+rows-1)/rows)
for i := 0; i < len(bindings); i += rows {
groups = append(groups, bindings[i:min(i+rows, len(bindings))])
}
return groups
}
-1
View File
@@ -1 +0,0 @@
package ilovetui

Some files were not shown because too many files have changed in this diff Show More