mirror of
https://github.com/anotherhadi/ilovetui.git
synced 2026-10-05 10:58:26 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa6496901f | ||
|
|
5f99fa7521 | ||
|
|
f3a1877832 |
@@ -0,0 +1,24 @@
|
||||
version: 2
|
||||
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
|
||||
builds:
|
||||
- skip: true
|
||||
|
||||
archives:
|
||||
- formats:
|
||||
- tar.gz
|
||||
name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}"
|
||||
|
||||
checksum:
|
||||
name_template: checksums.txt
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- "^docs:"
|
||||
- "^test:"
|
||||
- "^ci:"
|
||||
@@ -0,0 +1,10 @@
|
||||
# Contributing
|
||||
|
||||
Everybody is invited and welcome to contribute. There is a lot to do... Check the issues!
|
||||
|
||||
The process is straight-forward.
|
||||
|
||||
- Read [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#best-practices-for-faster-reviews) by Kubernetes (but skip step 0 and 1)
|
||||
- Fork this git repository
|
||||
- Write your changes (bug fixes, new features, ...).
|
||||
- Create a Pull Request against the main branch.
|
||||
@@ -0,0 +1 @@
|
||||
ko_fi: anotherhadi
|
||||
@@ -0,0 +1,28 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
- uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
version: "~> v2"
|
||||
args: release --clean --config .github/.goreleaser.yaml
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
+4
-1
@@ -1 +1,4 @@
|
||||
node_modules/
|
||||
.claude/
|
||||
CLAUDE.md
|
||||
result/
|
||||
.pre-commit-config.yaml
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Hadi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,29 +1,139 @@
|
||||
# I Love TUI
|
||||
# Ilovetui
|
||||
|
||||
Shared OpenTUI/Solid theme, components, and config helpers for my TUIs.
|
||||
A minimal Go library that provides a shared [Base16](https://github.com/tinted-theming/home) color theme for terminal UIs built with [bubbletea](https://github.com/charmbracelet/bubbletea) and [lipgloss](https://github.com/charmbracelet/lipgloss), plus a small collection of Bubble Tea v2 components on top of it.
|
||||
|
||||
## Install
|
||||
The idea is simple: instead of every TUI app managing its own colors, they all share one theme file so the user customizes once and every app looks consistent.
|
||||
|
||||
```bash
|
||||
bun add github:anotherhadi/ilovetui
|
||||
## Packages
|
||||
|
||||
- `github.com/anotherhadi/ilovetui/style` — the theme itself: colors, pre-built panel styles, config loading.
|
||||
- `github.com/anotherhadi/ilovetui/bubbles` — themed constructors for official `bubbles/v2` components (`help`, `textarea`, `textinput`, `list`, `table`, `filepicker`, `spinner`, `progress`, `paginator`, `viewport`).
|
||||
- `github.com/anotherhadi/ilovetui/tabs`, `.../helpbar`, `.../modal`, `.../drawer`, `.../notification` — custom components not found in the official `bubbles` library, styled from the same theme.
|
||||
|
||||
## How it works
|
||||
|
||||
On import, `style` automatically loads the user's theme from `~/.config/ilovetui/config.yaml` (respecting `$XDG_CONFIG_HOME`).
|
||||
If no config exists, it falls back to the embedded default. The active theme is exposed as the package-level variable `S`.
|
||||
|
||||
```go
|
||||
import "github.com/anotherhadi/ilovetui/style"
|
||||
|
||||
// Use colors directly
|
||||
s := lipgloss.NewStyle().Foreground(style.S.Primary)
|
||||
|
||||
// Use pre-built panel styles
|
||||
box := style.RenderWithTitle(style.S.PanelFocused, "Title", content, w, h)
|
||||
```
|
||||
|
||||
Peer dependencies, install whichever you actually use: `@opentui/core`, `@opentui/solid`, `@opentui/keymap`, `solid-js`, `opentui-spinner`.
|
||||
No setup required — just import and use.
|
||||
|
||||
## Usage
|
||||
## Installation
|
||||
|
||||
```ts
|
||||
import { theme, presets } from "ilovetui";
|
||||
import "ilovetui/solid";
|
||||
```sh
|
||||
go get github.com/anotherhadi/ilovetui
|
||||
```
|
||||
|
||||
Entry points: `.`, `./solid`, `./context`, `./project-config`, `./keymap`, `./components`, `./components/help`, `./components/spinner`, `./components/section-header`. See the source for what each exports.
|
||||
## Theme
|
||||
|
||||
The theme is base16 and user-overridable via `~/.config/ilovetui/config.yaml`. Copy [`src/default.yaml`](https://github.com/anotherhadi/ilovetui/blob/main/src/default.yaml) there as a starting point.
|
||||
The theme follows the [Base16](https://github.com/tinted-theming/home) standard (16 colors). The library exposes both the raw palette and semantic aliases:
|
||||
|
||||
An app can layer its own theme overrides on top of that file (e.g. a `theme:` section of its own config, validated with `ThemeConfigSchema`) by calling `configureTheme(overrides)` once at startup, before the first render. Invalid theme files are reported as warning toasts and skipped; unknown keys are ignored, so other TUIs can keep their own keys in the shared file.
|
||||
| Alias | Base16 | Meaning |
|
||||
| ------------ | ------ | --------------------------------------- |
|
||||
| `Background` | Base00 | Background |
|
||||
| `SubtleBg` | Base01 | Lighter Background / Status Bars |
|
||||
| `Selection` | Base02 | Selection Background |
|
||||
| `Subtle` | Base03 | Comments / Invisibles |
|
||||
| `Muted` | Base04 | Dark Foreground / Status Bars |
|
||||
| `Text` | Base05 | Default Foreground |
|
||||
| `Primary` | Base0D | Functions / Methods / Headings / Accent |
|
||||
| `Success` | Base0B | Strings / Success / Diff Inserted |
|
||||
| `Warning` | Base09 | Integers / Constants / Booleans |
|
||||
| `Error` | Base08 | Variables / Errors / Diff Deleted |
|
||||
|
||||
## Focus and keymap scoping
|
||||
The default theme is `style/default.yaml`. Copy it and edit to customize:
|
||||
|
||||
Wrap each focus region in a `<Pane>` (`focusable` when the pane itself is the focus target). Inside it, `usePane()` gives `target` — pass it to `useBindings` so the layer is only active while focus is within the pane — and `focused()` for styling. `<Modal>` takes focus while open, restores it on close, and binds its `dismissKey` (default `escape`) plus any `commands`/`bindings` you pass, scoped to itself. Note that keymap layers are ordered by `priority` then registration order, not by tree depth: keep overlapping layers on disjoint targets, or give them explicit priorities.
|
||||
```sh
|
||||
mkdir -p ~/.config/ilovetui
|
||||
cp $(go env GOPATH)/pkg/mod/github.com/anotherhadi/ilovetui*/style/default.yaml ~/.config/ilovetui/config.yaml
|
||||
```
|
||||
|
||||
Or let your app write it on first run:
|
||||
|
||||
```go
|
||||
style.WriteDefaultConfig(style.DefaultConfigPath())
|
||||
```
|
||||
|
||||
## Pre-built styles
|
||||
|
||||
`S` ships with a few ready-to-use lipgloss styles:
|
||||
|
||||
| Field | Description |
|
||||
| ---------------- | ---------------------------------------- |
|
||||
| `S.Bold` | Bold text |
|
||||
| `S.Faint` | Muted / dimmed text |
|
||||
| `S.Panel` | Rounded border, unfocused (Subtle color) |
|
||||
| `S.PanelFocused` | Rounded border, focused (Primary color) |
|
||||
|
||||
## Helpers
|
||||
|
||||
```go
|
||||
// Inner usable height of a bordered panel with outer height h
|
||||
inner := style.ContentHeight(h)
|
||||
|
||||
// Render a box with a title embedded in the top border
|
||||
box := style.RenderWithTitle(style.S.PanelFocused, "Header", content, w, h)
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
```go
|
||||
style.Init() // Reload from default config path
|
||||
style.InitFrom(path string) // Reload from a custom path
|
||||
style.InitFromBytes(data []byte) // Parse raw YAML
|
||||
style.DefaultConfigPath() string // ~/.config/ilovetui/config.yaml
|
||||
style.WriteDefaultConfig(path) // Write default config if missing
|
||||
```
|
||||
|
||||
## Themed official components
|
||||
|
||||
```go
|
||||
import "github.com/anotherhadi/ilovetui/bubbles"
|
||||
|
||||
h := bubbles.NewHelp()
|
||||
ta := bubbles.NewTextarea(false)
|
||||
ti := bubbles.NewTextInput()
|
||||
l := bubbles.NewList(items, width, height)
|
||||
t := bubbles.NewTable()
|
||||
fp := bubbles.NewFilePicker()
|
||||
sp := bubbles.NewSpinner()
|
||||
pr := bubbles.NewProgress()
|
||||
pg := bubbles.NewPaginator()
|
||||
vp := bubbles.NewViewport()
|
||||
```
|
||||
|
||||
Each constructor mirrors the official component's own `New`, then applies `style.S` on top. Where the
|
||||
official `New` takes options (`spinner`, `table`, `progress`), they're forwarded before the theme is
|
||||
applied, so you can still customize behavior; anything you pass that also sets colors will be overridden
|
||||
by the theme afterward.
|
||||
|
||||
## Custom components
|
||||
|
||||
```go
|
||||
import "github.com/anotherhadi/ilovetui/tabs"
|
||||
|
||||
t := tabs.New([]tabs.Item{{Title: "First", Model: firstPane}, {Title: "Second", Model: secondPane}})
|
||||
```
|
||||
|
||||
`tabs` renders a horizontal tab bar styled from `style.S`. The host application renders the content
|
||||
below it; see [`tabs/README.md`](tabs/README.md) and `examples/tabs` for a full example.
|
||||
|
||||
For the other custom components, see their own README: [`helpbar`](helpbar/README.md) (responsive
|
||||
help bar that reflows into as many columns as fit), [`modal`](modal/README.md) (centered popup
|
||||
dialogs), [`drawer`](drawer/README.md) (left/right sidebar panels, mirroring `modal`) and
|
||||
[`notification`](notification/README.md) (toast notifications).
|
||||
|
||||
## Projects using ilovetui
|
||||
|
||||
- [anotherhadi/spilltea](https://github.com/anotherhadi/spilltea): A minimal, terminal-based HTTP(S) proxy for pentesters and CTF players. Think Burp Suite or Caido, but entirely in your terminal.
|
||||
- [anotherhadi/usbguard-tui](https://github.com/anotherhadi/usbguard-tui): A terminal UI for managing USB devices via usbguard. TUI built with golang & bubbletea.
|
||||
- [anotherhadi/jwt-tui](https://github.com/anotherhadi/jwt-tui): A terminal UI for inspecting, editing, and signing JSON Web Tokens (JWTs).
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
// Package bubbles provides themed constructors for official bubbles/v2
|
||||
// components (help, textarea, textinput, list, table, filepicker, spinner,
|
||||
// progress, paginator, viewport). Each constructor mirrors the official
|
||||
// component's own New function, then applies the shared ilovetui/style
|
||||
// theme on top.
|
||||
package bubbles
|
||||
@@ -0,0 +1,26 @@
|
||||
package bubbles
|
||||
|
||||
import (
|
||||
"charm.land/bubbles/v2/filepicker"
|
||||
|
||||
"github.com/anotherhadi/ilovetui/style"
|
||||
)
|
||||
|
||||
// NewFilePicker returns a filepicker.Model styled with the active theme.
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package bubbles
|
||||
|
||||
import (
|
||||
"charm.land/bubbles/v2/help"
|
||||
"charm.land/lipgloss/v2"
|
||||
|
||||
"github.com/anotherhadi/ilovetui/style"
|
||||
)
|
||||
|
||||
// NewHelp returns a help.Model styled with the active theme.
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package bubbles
|
||||
|
||||
import "strings"
|
||||
|
||||
// SplitH splits totalHeight into top and bottom sections, accounting for the
|
||||
// height of statusBar (measured by newline count).
|
||||
func SplitH(totalHeight int, statusBar string, ratio float64) (top, bottom int) {
|
||||
statusH := strings.Count(statusBar, "\n") + 1
|
||||
available := totalHeight - statusH
|
||||
top = int(float64(available) * ratio)
|
||||
bottom = available - top
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package bubbles
|
||||
|
||||
import (
|
||||
"charm.land/bubbles/v2/list"
|
||||
|
||||
"github.com/anotherhadi/ilovetui/style"
|
||||
)
|
||||
|
||||
// NewList returns a list.Model styled with the active theme, using
|
||||
// NewDefaultDelegate for item rendering.
|
||||
func NewList(items []list.Item, width, height int) list.Model {
|
||||
m := list.New(items, NewDefaultDelegate(), width, height)
|
||||
m.Styles = themedListStyles()
|
||||
return m
|
||||
}
|
||||
|
||||
// themedListStyles builds list.Styles from the active theme.
|
||||
func themedListStyles() list.Styles {
|
||||
// isDark only affects a couple of fallback colors below, all of which
|
||||
// are overridden regardless.
|
||||
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
|
||||
}
|
||||
|
||||
// NewDefaultDelegate returns a list.DefaultDelegate styled with the active
|
||||
// theme, for use with NewList or a custom list.Model.
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package bubbles
|
||||
|
||||
import (
|
||||
"charm.land/bubbles/v2/paginator"
|
||||
"charm.land/lipgloss/v2"
|
||||
|
||||
"github.com/anotherhadi/ilovetui/style"
|
||||
)
|
||||
|
||||
// NewPaginator returns a dot-style paginator.Model styled with the active theme.
|
||||
func NewPaginator() paginator.Model {
|
||||
p := paginator.New()
|
||||
p.Type = paginator.Dots
|
||||
p.ActiveDot = lipgloss.NewStyle().Foreground(style.S.Primary).Render("•")
|
||||
p.InactiveDot = lipgloss.NewStyle().Foreground(style.S.Subtle).Render("•")
|
||||
return p
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package bubbles
|
||||
|
||||
import (
|
||||
"charm.land/bubbles/v2/progress"
|
||||
|
||||
"github.com/anotherhadi/ilovetui/style"
|
||||
)
|
||||
|
||||
// NewProgress returns a progress.Model with a themed fill, blending from
|
||||
// style.S.Subtle to style.S.Primary. Any opts are forwarded to progress.New;
|
||||
// pass progress.WithColors to override the default blend.
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package bubbles
|
||||
|
||||
import (
|
||||
"charm.land/bubbles/v2/spinner"
|
||||
"charm.land/lipgloss/v2"
|
||||
|
||||
"github.com/anotherhadi/ilovetui/style"
|
||||
)
|
||||
|
||||
// NewSpinner returns a spinner.Model styled with the active theme. Any opts
|
||||
// are forwarded to spinner.New before the theme is applied.
|
||||
func NewSpinner(opts ...spinner.Option) spinner.Model {
|
||||
s := spinner.New(opts...)
|
||||
s.Style = lipgloss.NewStyle().Foreground(style.S.Primary)
|
||||
return s
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package bubbles
|
||||
|
||||
import (
|
||||
"charm.land/bubbles/v2/table"
|
||||
|
||||
"github.com/anotherhadi/ilovetui/style"
|
||||
)
|
||||
|
||||
// NewTable returns a table.Model styled with the active theme. Any opts are
|
||||
// forwarded to table.New before the theme is applied.
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package bubbles
|
||||
|
||||
import (
|
||||
"charm.land/bubbles/v2/textarea"
|
||||
"charm.land/lipgloss/v2"
|
||||
|
||||
"github.com/anotherhadi/ilovetui/style"
|
||||
)
|
||||
|
||||
// NewTextarea returns a textarea.Model styled with the active theme.
|
||||
// Set showLineNumbers to true to display line numbers in the gutter.
|
||||
func NewTextarea(showLineNumbers bool) textarea.Model {
|
||||
ta := textarea.New()
|
||||
ta.Prompt = ""
|
||||
ta.ShowLineNumbers = showLineNumbers
|
||||
ta.CharLimit = 0
|
||||
ta.EndOfBufferCharacter = '~'
|
||||
ts := ta.Styles()
|
||||
ts.Focused.Base = lipgloss.NewStyle()
|
||||
ts.Blurred.Base = lipgloss.NewStyle()
|
||||
ts.Focused.Text = lipgloss.NewStyle().Foreground(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
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package bubbles
|
||||
|
||||
import (
|
||||
"charm.land/bubbles/v2/textinput"
|
||||
|
||||
"github.com/anotherhadi/ilovetui/style"
|
||||
)
|
||||
|
||||
// NewTextInput returns a textinput.Model styled with the active theme.
|
||||
func NewTextInput() textinput.Model {
|
||||
t := textinput.New()
|
||||
t.SetStyles(themedTextInputStyles())
|
||||
return t
|
||||
}
|
||||
|
||||
// themedTextInputStyles builds textinput.Styles from the active theme.
|
||||
// Shared with NewList, which themes its filter input the same way.
|
||||
func themedTextInputStyles() textinput.Styles {
|
||||
// isDark only affects textinput.DefaultStyles' Blurred.Text color, which
|
||||
// we override below regardless.
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package bubbles
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"charm.land/bubbles/v2/viewport"
|
||||
"charm.land/lipgloss/v2"
|
||||
|
||||
"github.com/anotherhadi/ilovetui/style"
|
||||
)
|
||||
|
||||
// NewViewport returns a viewport.Model with mouse wheel disabled.
|
||||
func NewViewport() viewport.Model {
|
||||
vp := viewport.New()
|
||||
vp.MouseWheelEnabled = false
|
||||
return vp
|
||||
}
|
||||
|
||||
// ViewportView renders the viewport and appends a subtle scroll indicator
|
||||
// on the last visible line when the user has not reached the bottom.
|
||||
func ViewportView(vp *viewport.Model) string {
|
||||
v := vp.View()
|
||||
if vp.AtBottom() {
|
||||
return v
|
||||
}
|
||||
lines := strings.Split(v, "\n")
|
||||
if len(lines) == 0 {
|
||||
return v
|
||||
}
|
||||
arrow := lipgloss.NewStyle().Foreground(style.S.Subtle).Render("↓")
|
||||
arrowW := lipgloss.Width(arrow)
|
||||
inner := vp.Width() - 2*arrowW
|
||||
if inner < 0 {
|
||||
inner = 0
|
||||
}
|
||||
lines[len(lines)-1] = arrow + strings.Repeat(" ", inner) + arrow
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
@@ -1,273 +0,0 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "ilovetui-opentui",
|
||||
"dependencies": {
|
||||
"yaml": "^2.8.1",
|
||||
"zod": "^4.5.4",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@opentui/core": "^0.5.9",
|
||||
"@opentui/keymap": "^0.5.9",
|
||||
"@opentui/solid": "^0.5.9",
|
||||
"@types/bun": "^1.3.14",
|
||||
"opentui-spinner": "^0.0.7",
|
||||
"solid-js": "1.9.12",
|
||||
"typescript": "^5.9.3",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentui/core": ">=0.5.0",
|
||||
"@opentui/keymap": ">=0.5.0",
|
||||
"@opentui/solid": ">=0.5.0",
|
||||
"opentui-spinner": ">=0.0.7",
|
||||
"solid-js": ">=1.9.0",
|
||||
},
|
||||
"optionalPeers": [
|
||||
"@opentui/core",
|
||||
"@opentui/keymap",
|
||||
"@opentui/solid",
|
||||
"opentui-spinner",
|
||||
"solid-js",
|
||||
],
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@ampproject/remapping": ["@ampproject/[email protected]", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
|
||||
|
||||
"@babel/code-frame": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
|
||||
|
||||
"@babel/compat-data": ["@babel/[email protected]", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="],
|
||||
|
||||
"@babel/core": ["@babel/[email protected]", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
|
||||
|
||||
"@babel/generator": ["@babel/[email protected]", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="],
|
||||
|
||||
"@babel/helper-annotate-as-pure": ["@babel/[email protected]", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw=="],
|
||||
|
||||
"@babel/helper-compilation-targets": ["@babel/[email protected]", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="],
|
||||
|
||||
"@babel/helper-create-class-features-plugin": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/traverse": "^7.29.7", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg=="],
|
||||
|
||||
"@babel/helper-globals": ["@babel/[email protected]", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="],
|
||||
|
||||
"@babel/helper-member-expression-to-functions": ["@babel/[email protected]", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg=="],
|
||||
|
||||
"@babel/helper-module-imports": ["@babel/[email protected]", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="],
|
||||
|
||||
"@babel/helper-module-transforms": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="],
|
||||
|
||||
"@babel/helper-optimise-call-expression": ["@babel/[email protected]", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong=="],
|
||||
|
||||
"@babel/helper-plugin-utils": ["@babel/[email protected]", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="],
|
||||
|
||||
"@babel/helper-replace-supers": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ=="],
|
||||
|
||||
"@babel/helper-skip-transparent-expression-wrappers": ["@babel/[email protected]", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ=="],
|
||||
|
||||
"@babel/helper-string-parser": ["@babel/[email protected]", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
|
||||
|
||||
"@babel/helper-validator-identifier": ["@babel/[email protected]", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
||||
|
||||
"@babel/helper-validator-option": ["@babel/[email protected]", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="],
|
||||
|
||||
"@babel/helpers": ["@babel/[email protected]", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="],
|
||||
|
||||
"@babel/parser": ["@babel/[email protected]", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="],
|
||||
|
||||
"@babel/plugin-syntax-jsx": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A=="],
|
||||
|
||||
"@babel/plugin-syntax-typescript": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA=="],
|
||||
|
||||
"@babel/plugin-transform-modules-commonjs": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-module-transforms": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ=="],
|
||||
|
||||
"@babel/plugin-transform-typescript": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/plugin-syntax-typescript": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw=="],
|
||||
|
||||
"@babel/preset-typescript": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ=="],
|
||||
|
||||
"@babel/template": ["@babel/[email protected]", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="],
|
||||
|
||||
"@babel/traverse": ["@babel/[email protected]", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg=="],
|
||||
|
||||
"@babel/types": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||
|
||||
"@jridgewell/resolve-uri": ["@jridgewell/[email protected]", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/[email protected]", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="],
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@opentui/core": ["@opentui/[email protected]", "", { "dependencies": { "bun-ffi-structs": "0.3.1", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.5.9", "@opentui/core-darwin-x64": "0.5.9", "@opentui/core-linux-arm64": "0.5.9", "@opentui/core-linux-arm64-musl": "0.5.9", "@opentui/core-linux-x64": "0.5.9", "@opentui/core-linux-x64-musl": "0.5.9", "@opentui/core-win32-arm64": "0.5.9", "@opentui/core-win32-x64": "0.5.9" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-d0EWYyp6djitu1N1R0o75NrLl4TxY3oJEmRNrX9vSKKC5/jriGQdSV6lJmwiB77O0cxtBnPzWztAV79vA1J2fA=="],
|
||||
|
||||
"@opentui/core-darwin-arm64": ["@opentui/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-YcpWGD8GwoO3UYYw0kLDI4qof3ElSwWW5M3fNG+Kw87BSjCa5frZqak5xebdZ/XU5Xsk+BjgbBsxbzo9yrIjZQ=="],
|
||||
|
||||
"@opentui/core-darwin-x64": ["@opentui/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-xgdZwgcwlDCqi0WPCS1d0GVSd+oKDFX+UVE1ZPei8u8W3pirXGSExxVHNku7QbEYCRvpCeMtoKIa6xN9cVTsoQ=="],
|
||||
|
||||
"@opentui/core-linux-arm64": ["@opentui/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-vHBiJp69wMaL+fRMDpnXCCIpPyQRPZEWCWBatnblUUGIWUseqeQlaPow6ljHSbgAd/GjygAf4tFCyTqNT2ezZA=="],
|
||||
|
||||
"@opentui/core-linux-arm64-musl": ["@opentui/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-A7GW4NCTInj2vy3X2gVO2Sm8jllgdLxwPdOklPqyl+xqivQtyG9qwS5YXoCk9fBJt10ORhYKETEjYEnIE5xvMQ=="],
|
||||
|
||||
"@opentui/core-linux-x64": ["@opentui/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-rdw7QVopVHwj6fMrJc7hPGVaYg2V5NBlW3MUfkrQBlUlo/87OC4aYDAh5oRIL1CIFD9LjDdh7hwmUNbn/K9Pcw=="],
|
||||
|
||||
"@opentui/core-linux-x64-musl": ["@opentui/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-J4wQs1OMPZ4hR93Op1C/BLFmIta2mUJm4M7djevlgcWcal4RSNO4V8QvUXfogWGci+ADZqEmX2osQQDY5NVJpw=="],
|
||||
|
||||
"@opentui/core-win32-arm64": ["@opentui/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-OnmEsGLWwsHrm6sTsWSSesu4LGjSV1JNWqCsEItbuitWaqH6rXdSahSc5jMFsS57bZxUyaRz01yAqiZzUSXnVQ=="],
|
||||
|
||||
"@opentui/core-win32-x64": ["@opentui/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-/CnAIfKL7+ZeGLyZoXV5zS71Nd8Zn97RUir2DAIY05MJozmfg5s7XOAdUNYuK1cg5mbwlAuqeAsfbuWUAEe15g=="],
|
||||
|
||||
"@opentui/keymap": ["@opentui/[email protected]", "", { "dependencies": { "@opentui/core": "0.5.9" }, "peerDependencies": { "@opentui/react": "0.5.9", "@opentui/solid": "0.5.9", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-ZcRNeuCDv+LJ89BS5xBw90e0S1Etn6j41jflg6LZlWj4bnqjCU55dq0+zERUdkkC4rqD4SikM8q7V3ZfkwzyyQ=="],
|
||||
|
||||
"@opentui/solid": ["@opentui/[email protected]", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.5.9", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-zGSP/ia9ww+TTMvQMZjJW2+h7fg9YkVRIM723EgvP+v7ZBZ/0vMzPNh7FJ0jET3ysw0h5hsbvcYISs4sUcf+UA=="],
|
||||
|
||||
"@types/bun": ["@types/[email protected]", "", { "dependencies": { "bun-types": "1.4.0" } }, "sha512-K+lZULY23vRgK/CfTjFIV+tyifaNdSMlPh9j+6mQ/cLfpOznLyAuzgV/JQysyECpkBQLVMSyvjlr2fBUSA9wFQ=="],
|
||||
|
||||
"@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ=="],
|
||||
|
||||
"ansi-regex": ["[email protected]", "", {}, "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ=="],
|
||||
|
||||
"babel-plugin-jsx-dom-expressions": ["[email protected]", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-lxve6Y02YiZTldB7efKpnbf1BH00XCFZNYYW235jSGsYaJNFtHrYlKV6/O+miHbjqpIr9FTe5+0no4hofAMbfA=="],
|
||||
|
||||
"babel-plugin-module-resolver": ["[email protected]", "", { "dependencies": { "find-babel-config": "^2.1.1", "glob": "^9.3.3", "pkg-up": "^3.1.0", "reselect": "^4.1.7", "resolve": "^1.22.8" } }, "sha512-9KtaCazHee2xc0ibfqsDeamwDps6FZNo5S0Q81dUqEuFzVwPhcT4J5jOqIVvgCA3Q/wO9hKYxN/Ds3tIsp5ygg=="],
|
||||
|
||||
"babel-preset-solid": ["[email protected]", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.6" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.12" }, "optionalPeers": ["solid-js"] }, "sha512-LLqnuKVDlKpyBlMPcH6qEvs/wmS9a+NczppxJ3ryS/c0O5IiSFOIBQi9GzyiGDSbcJpx4Gr87jyFTos1MyEuWg=="],
|
||||
|
||||
"balanced-match": ["[email protected]", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"baseline-browser-mapping": ["[email protected]", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw=="],
|
||||
|
||||
"brace-expansion": ["[email protected]", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="],
|
||||
|
||||
"browserslist": ["[email protected]", "", { "dependencies": { "baseline-browser-mapping": "^2.11.12", "caniuse-lite": "^1.0.30001809", "electron-to-chromium": "^1.5.402", "node-releases": "^2.0.53", "update-browserslist-db": "^1.3.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA=="],
|
||||
|
||||
"bun-ffi-structs": ["[email protected]", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-3gM7PpVWLyrwxWjcilSiGuhWanhZivvo6l0u573NziPH6f/gwk6McbaYgn7oJWov6pKGRTDbrg94W5DcJsKTtQ=="],
|
||||
|
||||
"bun-types": ["[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-iIKw23BspnQQYd3prITOBxeUsxBHnwzX6YJfGMuNOZzeNcMmVqzIIVGRm1l69ogaPQmb4wB6BN8mA5bE9YuC5Q=="],
|
||||
|
||||
"caniuse-lite": ["[email protected]", "", {}, "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg=="],
|
||||
|
||||
"cli-spinners": ["[email protected]", "", {}, "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw=="],
|
||||
|
||||
"convert-source-map": ["[email protected]", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||
|
||||
"csstype": ["[email protected]", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||
|
||||
"debug": ["[email protected]", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"diff": ["[email protected]", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="],
|
||||
|
||||
"electron-to-chromium": ["[email protected]", "", {}, "sha512-4T+DTDWuMPM4aHlHwWdAVCVWwp7LDilnhzkj+c/Lbj91XSQrLuOmZSLtS9Q4iIqjlPUbPOnC624zDVVHCHaolQ=="],
|
||||
|
||||
"emoji-regex": ["[email protected]", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
|
||||
|
||||
"entities": ["[email protected]", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
||||
|
||||
"es-errors": ["[email protected]", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"escalade": ["[email protected]", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
||||
"find-babel-config": ["[email protected]", "", { "dependencies": { "json5": "^2.2.3" } }, "sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg=="],
|
||||
|
||||
"find-up": ["[email protected]", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="],
|
||||
|
||||
"fs.realpath": ["[email protected]", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
|
||||
|
||||
"function-bind": ["[email protected]", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"gensync": ["[email protected]", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
||||
|
||||
"get-east-asian-width": ["[email protected]", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="],
|
||||
|
||||
"glob": ["[email protected]", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="],
|
||||
|
||||
"hasown": ["[email protected]", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="],
|
||||
|
||||
"html-entities": ["[email protected]", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="],
|
||||
|
||||
"is-core-module": ["[email protected]", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="],
|
||||
|
||||
"js-tokens": ["[email protected]", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"jsesc": ["[email protected]", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||
|
||||
"json5": ["[email protected]", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||
|
||||
"locate-path": ["[email protected]", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="],
|
||||
|
||||
"lru-cache": ["[email protected]", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
|
||||
"marked": ["[email protected]", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="],
|
||||
|
||||
"minimatch": ["[email protected]", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg=="],
|
||||
|
||||
"minipass": ["[email protected]", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="],
|
||||
|
||||
"ms": ["[email protected]", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"node-releases": ["[email protected]", "", {}, "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ=="],
|
||||
|
||||
"opentui-spinner": ["[email protected]", "", { "dependencies": { "cli-spinners": "^3.3.0" }, "peerDependencies": { "@opentui/core": "^0.3.4", "@opentui/react": "^0.3.4", "@opentui/solid": "^0.3.4", "typescript": "^5" }, "optionalPeers": ["@opentui/react", "@opentui/solid"] }, "sha512-nPzwAvJG+y9rVEwwHLHqbsMzLnIk2zw+F9LqwA7aYJvpM5gsrKC2rrGi36A+tZpA+1RnWxXeWEgVZMchnaH18Q=="],
|
||||
|
||||
"p-limit": ["[email protected]", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
|
||||
|
||||
"p-locate": ["[email protected]", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="],
|
||||
|
||||
"p-try": ["[email protected]", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="],
|
||||
|
||||
"parse5": ["[email protected]", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
|
||||
|
||||
"path-exists": ["[email protected]", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="],
|
||||
|
||||
"path-parse": ["[email protected]", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
|
||||
|
||||
"path-scurry": ["[email protected]", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
|
||||
|
||||
"picocolors": ["[email protected]", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"pkg-up": ["[email protected]", "", { "dependencies": { "find-up": "^3.0.0" } }, "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA=="],
|
||||
|
||||
"reselect": ["[email protected]", "", {}, "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ=="],
|
||||
|
||||
"resolve": ["[email protected]", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="],
|
||||
|
||||
"s-js": ["[email protected]", "", {}, "sha512-RtpOm+cM6O0sHg6IA70wH+UC3FZcND+rccBZpBAHzlUgNO2Bm5BN+FnM8+OBxzXdwpKWFwX11JGF0MFRkhSoIQ=="],
|
||||
|
||||
"semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"seroval": ["[email protected]", "", {}, "sha512-rVQVWjjSvlINzaQPZH5JFqsqEsIWdTxY3iJZCnTL/5gQbXIRooVZKI60tVCkOVfzcRPejboxO2t0P89dg5mQaA=="],
|
||||
|
||||
"seroval-plugins": ["[email protected]", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-HXuLAX2pu/UByPpaeo/TaMfvMIi+1QqIoPJYCcAtU8QkVNwgR6MPlGuCQTErV1JwraaMbYaWVIBX7mppzGLATQ=="],
|
||||
|
||||
"solid-js": ["[email protected]", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-QzKaSJq2/iDrWR1As6MHZQ8fQkdOBf8GReYb7L5iKwMGceg7HxDcaOHk0at66tNgn9U2U7dXo8ZZpLIAmGMzgw=="],
|
||||
|
||||
"string-width": ["[email protected]", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||
|
||||
"strip-ansi": ["[email protected]", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
|
||||
|
||||
"supports-preserve-symlinks-flag": ["[email protected]", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
||||
|
||||
"typescript": ["[email protected]", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"undici-types": ["[email protected]", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
|
||||
|
||||
"update-browserslist-db": ["[email protected]", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw=="],
|
||||
|
||||
"web-tree-sitter": ["[email protected]", "", { "peerDependencies": { "@types/emscripten": "^1.40.0" }, "optionalPeers": ["@types/emscripten"] }, "sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA=="],
|
||||
|
||||
"yallist": ["[email protected]", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||
|
||||
"yaml": ["[email protected]", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
|
||||
|
||||
"zod": ["[email protected]", "", {}, "sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA=="],
|
||||
|
||||
"babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/[email protected]", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="],
|
||||
|
||||
"parse5/entities": ["[email protected]", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
|
||||
|
||||
"path-scurry/lru-cache": ["[email protected]", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||
|
||||
"path-scurry/minipass": ["[email protected]", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
|
||||
}
|
||||
}
|
||||
Vendored
-25
@@ -1,25 +0,0 @@
|
||||
// @bun
|
||||
// src/context/focus.ts
|
||||
import { CliRenderEvents } from "@opentui/core";
|
||||
import { useRenderer } from "@opentui/solid";
|
||||
import { createEffect, createSignal, onCleanup } from "solid-js";
|
||||
function isInside2(node, root) {
|
||||
if (!root)
|
||||
return false;
|
||||
for (let current = node;current; current = current.parent) {
|
||||
if (current === root)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function useFocusWithin2(target) {
|
||||
const renderer = useRenderer();
|
||||
const [focused, setFocused] = createSignal(false);
|
||||
const sync = () => setFocused(isInside2(renderer.currentFocusedRenderable, target() ?? null));
|
||||
renderer.on(CliRenderEvents.FOCUSED_RENDERABLE, sync);
|
||||
onCleanup(() => renderer.off(CliRenderEvents.FOCUSED_RENDERABLE, sync));
|
||||
createEffect(sync);
|
||||
return focused;
|
||||
}
|
||||
|
||||
export { isInside2, useFocusWithin2 };
|
||||
Vendored
-21
@@ -1,21 +0,0 @@
|
||||
// @bun
|
||||
// src/context/notifications.ts
|
||||
import { createSignal } from "solid-js";
|
||||
var DEFAULT_DURATION = 3000;
|
||||
var [notifications2, setNotifications] = createSignal([]);
|
||||
var nextId = 0;
|
||||
function notify2(message, options = {}) {
|
||||
const id = options.id ?? `toast-${nextId++}`;
|
||||
const toast = { id, title: options.title, message, kind: options.kind ?? "info" };
|
||||
setNotifications((prev) => [...prev.filter((n) => n.id !== id), toast]);
|
||||
const duration = options.duration ?? DEFAULT_DURATION;
|
||||
if (duration > 0) {
|
||||
setTimeout(() => dismiss2(id), duration);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
function dismiss2(id) {
|
||||
setNotifications((prev) => prev.filter((n) => n.id !== id));
|
||||
}
|
||||
|
||||
export { notifications2, notify2, dismiss2 };
|
||||
Vendored
-23
@@ -1,23 +0,0 @@
|
||||
// @bun
|
||||
import {
|
||||
theme2
|
||||
} from "./chunk-p6jvscj3.js";
|
||||
|
||||
// src/components/Box.tsx
|
||||
import { spread as _$spread } from "@opentui/solid";
|
||||
import { mergeProps as _$mergeProps } from "@opentui/solid";
|
||||
import { memo as _$memo } from "@opentui/solid";
|
||||
import { createElement as _$createElement } from "@opentui/solid";
|
||||
function Box2(props) {
|
||||
return (() => {
|
||||
var _el$ = _$createElement("box");
|
||||
_$spread(_el$, _$mergeProps(props, {
|
||||
get borderStyle() {
|
||||
return props.borderStyle ?? theme2.borderStyle;
|
||||
}
|
||||
}), false);
|
||||
return _el$;
|
||||
})();
|
||||
}
|
||||
|
||||
export { Box2 };
|
||||
Vendored
-79
@@ -1,79 +0,0 @@
|
||||
// @bun
|
||||
import {
|
||||
notifications2,
|
||||
dismiss2
|
||||
} from "./chunk-6ggk5fg7.js";
|
||||
import {
|
||||
theme2
|
||||
} from "./chunk-p6jvscj3.js";
|
||||
|
||||
// src/components/NotificationHost.tsx
|
||||
import { effect as _$effect } from "@opentui/solid";
|
||||
import { insertNode as _$insertNode } from "@opentui/solid";
|
||||
import { memo as _$memo } from "@opentui/solid";
|
||||
import { insert as _$insert } from "@opentui/solid";
|
||||
import { createComponent as _$createComponent } from "@opentui/solid";
|
||||
import { setProp as _$setProp } from "@opentui/solid";
|
||||
import { createElement as _$createElement } from "@opentui/solid";
|
||||
import { Portal } from "@opentui/solid";
|
||||
import { For } from "solid-js";
|
||||
var KIND_LABEL = {
|
||||
info: "Info",
|
||||
success: "Success",
|
||||
warning: "Warning",
|
||||
error: "Error"
|
||||
};
|
||||
function NotificationHost2(props = {}) {
|
||||
const defaultColor = () => ({
|
||||
info: theme2.primary,
|
||||
success: theme2.success,
|
||||
warning: theme2.warning,
|
||||
error: theme2.error
|
||||
});
|
||||
const colorFor = (kind) => props.colors?.[kind] ?? defaultColor()[kind];
|
||||
return _$createComponent(Portal, {
|
||||
get children() {
|
||||
var _el$ = _$createElement("box");
|
||||
_$setProp(_el$, "position", "absolute");
|
||||
_$setProp(_el$, "top", 1);
|
||||
_$setProp(_el$, "right", 1);
|
||||
_$setProp(_el$, "flexDirection", "column");
|
||||
_$setProp(_el$, "zIndex", 1000);
|
||||
_$insert(_el$, _$createComponent(For, {
|
||||
get each() {
|
||||
return notifications2();
|
||||
},
|
||||
children: (toast) => (() => {
|
||||
var _el$2 = _$createElement("box"), _el$3 = _$createElement("text"), _el$4 = _$createElement("text");
|
||||
_$insertNode(_el$2, _el$3);
|
||||
_$insertNode(_el$2, _el$4);
|
||||
_$setProp(_el$2, "border", true);
|
||||
_$setProp(_el$2, "marginBottom", 1);
|
||||
_$insert(_el$3, () => toast.title ?? KIND_LABEL[toast.kind]);
|
||||
_$insert(_el$4, () => toast.message);
|
||||
_$effect((_p$) => {
|
||||
var _v$ = theme2.borderStyle, _v$2 = colorFor(toast.kind), _v$3 = props.backgroundColor ?? theme2.background, _v$4 = props.width ?? 32, _v$5 = theme2.mouse ? () => dismiss2(toast.id) : undefined, _v$6 = colorFor(toast.kind);
|
||||
_v$ !== _p$.e && (_p$.e = _$setProp(_el$2, "borderStyle", _v$, _p$.e));
|
||||
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$2, "borderColor", _v$2, _p$.t));
|
||||
_v$3 !== _p$.a && (_p$.a = _$setProp(_el$2, "backgroundColor", _v$3, _p$.a));
|
||||
_v$4 !== _p$.o && (_p$.o = _$setProp(_el$2, "width", _v$4, _p$.o));
|
||||
_v$5 !== _p$.i && (_p$.i = _$setProp(_el$2, "onMouseDown", _v$5, _p$.i));
|
||||
_v$6 !== _p$.n && (_p$.n = _$setProp(_el$3, "fg", _v$6, _p$.n));
|
||||
return _p$;
|
||||
}, {
|
||||
e: undefined,
|
||||
t: undefined,
|
||||
a: undefined,
|
||||
o: undefined,
|
||||
i: undefined,
|
||||
n: undefined
|
||||
});
|
||||
return _el$2;
|
||||
})()
|
||||
}));
|
||||
return _el$;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export { NotificationHost2 };
|
||||
Vendored
-121
@@ -1,121 +0,0 @@
|
||||
// @bun
|
||||
import {
|
||||
theme2
|
||||
} from "./chunk-p6jvscj3.js";
|
||||
|
||||
// src/components/HelpBar.tsx
|
||||
import { createTextNode as _$createTextNode } from "@opentui/solid";
|
||||
import { insertNode as _$insertNode } from "@opentui/solid";
|
||||
import { effect as _$effect } from "@opentui/solid";
|
||||
import { memo as _$memo } from "@opentui/solid";
|
||||
import { insert as _$insert } from "@opentui/solid";
|
||||
import { createComponent as _$createComponent } from "@opentui/solid";
|
||||
import { use as _$use } from "@opentui/solid";
|
||||
import { setProp as _$setProp } from "@opentui/solid";
|
||||
import { createElement as _$createElement } from "@opentui/solid";
|
||||
import { CliRenderEvents, TextAttributes } from "@opentui/core";
|
||||
import { onResize, useRenderer } from "@opentui/solid";
|
||||
import { formatCommandBindings } from "@opentui/keymap/extras";
|
||||
import { useKeymapSelector } from "@opentui/keymap/solid";
|
||||
import { createMemo, createSignal, For } from "solid-js";
|
||||
var KEY_DISPLAY = {
|
||||
up: "\u2191",
|
||||
down: "\u2193",
|
||||
left: "\u2190",
|
||||
right: "\u2192",
|
||||
enter: "Enter",
|
||||
escape: "Esc"
|
||||
};
|
||||
var SEPARATOR = " ";
|
||||
var ELLIPSIS = "\u2026";
|
||||
function HelpBar2(props = {}) {
|
||||
const accent = () => props.accentColor ?? theme2.primary;
|
||||
const muted = () => props.mutedColor ?? theme2.muted;
|
||||
const renderer = useRenderer();
|
||||
const [width, setWidth] = createSignal(0);
|
||||
let container;
|
||||
const measure = () => {
|
||||
if (!container)
|
||||
return;
|
||||
setWidth(container.width);
|
||||
};
|
||||
const remeasureNextFrame = () => renderer.once(CliRenderEvents.FRAME, measure);
|
||||
onResize(remeasureNextFrame);
|
||||
const entries = useKeymapSelector((keymap) => keymap.getCommandEntries({
|
||||
visibility: "active"
|
||||
}).filter((entry) => entry.command.shortHelp === true).map((entry) => ({
|
||||
command: entry.command.name,
|
||||
keys: formatCommandBindings(entry.bindings, {
|
||||
keyNameAliases: KEY_DISPLAY
|
||||
}) ?? "",
|
||||
label: typeof entry.command.desc === "string" ? entry.command.desc : entry.command.name
|
||||
})));
|
||||
const visible = createMemo(() => {
|
||||
const available = width() - 2;
|
||||
const shown = [];
|
||||
let used = 0;
|
||||
for (const entry of entries()) {
|
||||
const sepWidth = shown.length > 0 ? SEPARATOR.length : 0;
|
||||
const entryWidth = entry.keys.length + 1 + entry.label.length;
|
||||
if (used + sepWidth + entryWidth <= available) {
|
||||
used += sepWidth + entryWidth;
|
||||
shown.push(entry);
|
||||
continue;
|
||||
}
|
||||
const remaining = available - used - sepWidth;
|
||||
const prefix = `${entry.keys} `;
|
||||
if (remaining > prefix.length + ELLIPSIS.length) {
|
||||
const labelBudget = remaining - prefix.length - ELLIPSIS.length;
|
||||
shown.push({
|
||||
...entry,
|
||||
label: `${entry.label.slice(0, labelBudget)}${ELLIPSIS}`
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
return shown;
|
||||
});
|
||||
return (() => {
|
||||
var _el$ = _$createElement("box");
|
||||
_$use((el) => {
|
||||
container = el;
|
||||
remeasureNextFrame();
|
||||
}, _el$);
|
||||
_$setProp(_el$, "flexDirection", "row");
|
||||
_$setProp(_el$, "paddingLeft", 1);
|
||||
_$setProp(_el$, "paddingRight", 1);
|
||||
_$setProp(_el$, "overflow", "hidden");
|
||||
_$insert(_el$, _$createComponent(For, {
|
||||
get each() {
|
||||
return visible();
|
||||
},
|
||||
children: (entry, i) => [_$memo(() => _$memo(() => i() > 0)() && (() => {
|
||||
var _el$4 = _$createElement("text");
|
||||
_$insertNode(_el$4, _$createTextNode(` `));
|
||||
_$effect((_$p) => _$setProp(_el$4, "fg", muted(), _$p));
|
||||
return _el$4;
|
||||
})()), (() => {
|
||||
var _el$2 = _$createElement("text");
|
||||
_$insert(_el$2, () => entry.keys);
|
||||
_$effect((_p$) => {
|
||||
var _v$ = accent(), _v$2 = TextAttributes.BOLD;
|
||||
_v$ !== _p$.e && (_p$.e = _$setProp(_el$2, "fg", _v$, _p$.e));
|
||||
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$2, "attributes", _v$2, _p$.t));
|
||||
return _p$;
|
||||
}, {
|
||||
e: undefined,
|
||||
t: undefined
|
||||
});
|
||||
return _el$2;
|
||||
})(), (() => {
|
||||
var _el$3 = _$createElement("text");
|
||||
_$insert(_el$3, () => ` ${entry.label}`);
|
||||
_$effect((_$p) => _$setProp(_el$3, "fg", muted(), _$p));
|
||||
return _el$3;
|
||||
})()]
|
||||
}));
|
||||
return _el$;
|
||||
})();
|
||||
}
|
||||
|
||||
export { HelpBar2 };
|
||||
Vendored
-44
@@ -1,44 +0,0 @@
|
||||
// @bun
|
||||
import {
|
||||
theme2
|
||||
} from "./chunk-p6jvscj3.js";
|
||||
|
||||
// src/components/KeyValue.tsx
|
||||
import { memo as _$memo } from "@opentui/solid";
|
||||
import { effect as _$effect } from "@opentui/solid";
|
||||
import { insertNode as _$insertNode } from "@opentui/solid";
|
||||
import { createComponent as _$createComponent } from "@opentui/solid";
|
||||
import { insert as _$insert } from "@opentui/solid";
|
||||
import { setProp as _$setProp } from "@opentui/solid";
|
||||
import { createElement as _$createElement } from "@opentui/solid";
|
||||
import { Show } from "solid-js";
|
||||
function KeyValue2(props) {
|
||||
return (() => {
|
||||
var _el$ = _$createElement("box"), _el$2 = _$createElement("text");
|
||||
_$insertNode(_el$, _el$2);
|
||||
_$setProp(_el$, "flexShrink", 0);
|
||||
_$setProp(_el$, "flexDirection", "row");
|
||||
_$setProp(_el$, "justifyContent", "space-between");
|
||||
_$insert(_el$2, () => props.label);
|
||||
_$insert(_el$, _$createComponent(Show, {
|
||||
get when() {
|
||||
return props.children;
|
||||
},
|
||||
get fallback() {
|
||||
return (() => {
|
||||
var _el$3 = _$createElement("text");
|
||||
_$insert(_el$3, () => props.value ?? "");
|
||||
_$effect((_$p) => _$setProp(_el$3, "fg", props.valueColor, _$p));
|
||||
return _el$3;
|
||||
})();
|
||||
},
|
||||
get children() {
|
||||
return props.children;
|
||||
}
|
||||
}), null);
|
||||
_$effect((_$p) => _$setProp(_el$2, "fg", theme2.muted, _$p));
|
||||
return _el$;
|
||||
})();
|
||||
}
|
||||
|
||||
export { KeyValue2 };
|
||||
Vendored
-9
@@ -1,9 +0,0 @@
|
||||
// @bun
|
||||
// src/context/help.ts
|
||||
import { createSignal } from "solid-js";
|
||||
var [helpOpen2, setHelpOpen] = createSignal(false);
|
||||
function toggleHelp2() {
|
||||
setHelpOpen((open) => !open);
|
||||
}
|
||||
|
||||
export { helpOpen2, toggleHelp2 };
|
||||
Vendored
-87
@@ -1,87 +0,0 @@
|
||||
// @bun
|
||||
import {
|
||||
helpOpen2,
|
||||
toggleHelp2
|
||||
} from "./chunk-ee2qaswx.js";
|
||||
import {
|
||||
Modal2
|
||||
} from "./chunk-t2cszvff.js";
|
||||
|
||||
// src/components/HelpModal.tsx
|
||||
import { createComponent as _$createComponent } from "@opentui/solid";
|
||||
import { insert as _$insert } from "@opentui/solid";
|
||||
import { setProp as _$setProp } from "@opentui/solid";
|
||||
import { effect as _$effect } from "@opentui/solid";
|
||||
import { createTextNode as _$createTextNode } from "@opentui/solid";
|
||||
import { insertNode as _$insertNode } from "@opentui/solid";
|
||||
import { createElement as _$createElement } from "@opentui/solid";
|
||||
import { TextAttributes } from "@opentui/core";
|
||||
import { formatCommandBindings } from "@opentui/keymap/extras";
|
||||
import { useKeymapSelector } from "@opentui/keymap/solid";
|
||||
import { createSignal } from "solid-js";
|
||||
var KEY_DISPLAY = {
|
||||
up: "\u2191",
|
||||
down: "\u2193",
|
||||
left: "\u2190",
|
||||
right: "\u2192",
|
||||
enter: "Enter",
|
||||
escape: "Esc"
|
||||
};
|
||||
function HelpModal2(props = {}) {
|
||||
const [scope, setScope] = createSignal(null);
|
||||
const entries = useKeymapSelector((keymap) => keymap.getCommandEntries({
|
||||
visibility: "active",
|
||||
focused: scope()
|
||||
}).map((entry) => ({
|
||||
command: entry.command.name,
|
||||
keys: formatCommandBindings(entry.bindings, {
|
||||
keyNameAliases: KEY_DISPLAY
|
||||
}) ?? "",
|
||||
label: typeof entry.command.desc === "string" ? entry.command.desc : entry.command.name
|
||||
})));
|
||||
return _$createComponent(Modal2, {
|
||||
get open() {
|
||||
return helpOpen2();
|
||||
},
|
||||
onDismiss: toggleHelp2,
|
||||
onOpen: (previous) => setScope(() => previous),
|
||||
get dismissKey() {
|
||||
return props.dismissKey;
|
||||
},
|
||||
get accentColor() {
|
||||
return props.accentColor;
|
||||
},
|
||||
get backgroundColor() {
|
||||
return props.backgroundColor;
|
||||
},
|
||||
get backdropColor() {
|
||||
return props.backdropColor;
|
||||
},
|
||||
get width() {
|
||||
return props.width;
|
||||
},
|
||||
get maxHeight() {
|
||||
return entries().length + 6;
|
||||
},
|
||||
scrollable: true,
|
||||
get children() {
|
||||
return [(() => {
|
||||
var _el$ = _$createElement("text");
|
||||
_$insertNode(_el$, _$createTextNode(`Keybindings`));
|
||||
_$effect((_$p) => _$setProp(_el$, "attributes", TextAttributes.BOLD, _$p));
|
||||
return _el$;
|
||||
})(), (() => {
|
||||
var _el$3 = _$createElement("text");
|
||||
_$insertNode(_el$3, _$createTextNode(` `));
|
||||
return _el$3;
|
||||
})(), (() => {
|
||||
var _el$5 = _$createElement("text");
|
||||
_$insert(_el$5, () => entries().map((entry) => `${entry.keys.padEnd(10)}${entry.label}`).join(`
|
||||
`));
|
||||
return _el$5;
|
||||
})()];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export { HelpModal2 };
|
||||
Vendored
-26
@@ -1,26 +0,0 @@
|
||||
// @bun
|
||||
// src/context/helper.tsx
|
||||
import { createComponent as _$createComponent } from "@opentui/solid";
|
||||
import { createContext, useContext } from "solid-js";
|
||||
function createSimpleContext2(input) {
|
||||
const ctx = createContext();
|
||||
return {
|
||||
provider: (props) => {
|
||||
const value = input.init(props);
|
||||
return _$createComponent(ctx.Provider, {
|
||||
value,
|
||||
get children() {
|
||||
return props.children;
|
||||
}
|
||||
});
|
||||
},
|
||||
use() {
|
||||
const value = useContext(ctx);
|
||||
if (!value)
|
||||
throw new Error(`${input.name} context must be used within its provider`);
|
||||
return value;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export { createSimpleContext2 };
|
||||
Vendored
-35
@@ -1,35 +0,0 @@
|
||||
// @bun
|
||||
import {
|
||||
theme2
|
||||
} from "./chunk-p6jvscj3.js";
|
||||
|
||||
// src/components/Spinner.tsx
|
||||
import { setProp as _$setProp } from "@opentui/solid";
|
||||
import { effect as _$effect } from "@opentui/solid";
|
||||
import { createElement as _$createElement } from "@opentui/solid";
|
||||
import"opentui-spinner/solid";
|
||||
function Spinner2(props) {
|
||||
return (() => {
|
||||
var _el$ = _$createElement("spinner");
|
||||
_$effect((_p$) => {
|
||||
var { name: _v$, frames: _v$2, interval: _v$3 } = props, _v$4 = props.autoplay ?? !theme2.reducedMotion, _v$5 = props.color ?? theme2.primary, _v$6 = props.backgroundColor;
|
||||
_v$ !== _p$.e && (_p$.e = _$setProp(_el$, "name", _v$, _p$.e));
|
||||
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$, "frames", _v$2, _p$.t));
|
||||
_v$3 !== _p$.a && (_p$.a = _$setProp(_el$, "interval", _v$3, _p$.a));
|
||||
_v$4 !== _p$.o && (_p$.o = _$setProp(_el$, "autoplay", _v$4, _p$.o));
|
||||
_v$5 !== _p$.i && (_p$.i = _$setProp(_el$, "color", _v$5, _p$.i));
|
||||
_v$6 !== _p$.n && (_p$.n = _$setProp(_el$, "backgroundColor", _v$6, _p$.n));
|
||||
return _p$;
|
||||
}, {
|
||||
e: undefined,
|
||||
t: undefined,
|
||||
a: undefined,
|
||||
o: undefined,
|
||||
i: undefined,
|
||||
n: undefined
|
||||
});
|
||||
return _el$;
|
||||
})();
|
||||
}
|
||||
|
||||
export { Spinner2 };
|
||||
Vendored
-65
@@ -1,65 +0,0 @@
|
||||
// @bun
|
||||
// src/config.ts
|
||||
import { z } from "zod";
|
||||
var COLOR_KEYS2 = [
|
||||
"base00",
|
||||
"base01",
|
||||
"base02",
|
||||
"base03",
|
||||
"base04",
|
||||
"base05",
|
||||
"base06",
|
||||
"base07",
|
||||
"base08",
|
||||
"base09",
|
||||
"base0a",
|
||||
"base0b",
|
||||
"base0c",
|
||||
"base0d",
|
||||
"base0e",
|
||||
"base0f"
|
||||
];
|
||||
var OPENTUI_BORDER_STYLES2 = ["single", "double", "rounded", "heavy"];
|
||||
var hexColor = z.string().regex(/^#?[0-9a-fA-F]{6}$/, 'expected a hex color like "#1e1e2e"');
|
||||
var colorsShape = {};
|
||||
for (const key of COLOR_KEYS2)
|
||||
colorsShape[key] = hexColor.optional();
|
||||
var ThemeConfigSchema2 = z.object({
|
||||
nerd_fonts: z.boolean().optional(),
|
||||
emoji_fonts: z.boolean().optional(),
|
||||
reduced_motion: z.boolean().optional(),
|
||||
mouse: z.boolean().optional(),
|
||||
border: z.enum(OPENTUI_BORDER_STYLES2).optional(),
|
||||
colors: z.object(colorsShape).optional()
|
||||
});
|
||||
function layerConfigs2(layers) {
|
||||
const out = {};
|
||||
for (const layer of layers) {
|
||||
const { colors, ...rest } = layer;
|
||||
Object.assign(out, rest);
|
||||
if (colors)
|
||||
out.colors = { ...out.colors, ...colors };
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function resolveConfig2(config) {
|
||||
const colors = {};
|
||||
for (const key of COLOR_KEYS2)
|
||||
colors[key] = normalizeColor2(config.colors?.[key] ?? "");
|
||||
return {
|
||||
nerdFonts: config.nerd_fonts ?? false,
|
||||
emojiFonts: config.emoji_fonts ?? false,
|
||||
reducedMotion: config.reduced_motion ?? false,
|
||||
mouse: config.mouse ?? true,
|
||||
border: config.border ?? "rounded",
|
||||
colors
|
||||
};
|
||||
}
|
||||
function normalizeColor2(hex) {
|
||||
const trimmed = hex.trim();
|
||||
if (trimmed && !trimmed.startsWith("#"))
|
||||
return `#${trimmed}`;
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
export { COLOR_KEYS2, OPENTUI_BORDER_STYLES2, ThemeConfigSchema2, layerConfigs2, resolveConfig2, normalizeColor2 };
|
||||
Vendored
-79
@@ -1,79 +0,0 @@
|
||||
// @bun
|
||||
import {
|
||||
ThemeConfigSchema2,
|
||||
layerConfigs2,
|
||||
resolveConfig2
|
||||
} from "./chunk-njvj322f.js";
|
||||
import {
|
||||
notify2
|
||||
} from "./chunk-6ggk5fg7.js";
|
||||
import {
|
||||
buildPresets2
|
||||
} from "./chunk-yyg967q8.js";
|
||||
import {
|
||||
configPath2
|
||||
} from "./chunk-xykm10y9.js";
|
||||
|
||||
// src/index.ts
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { parse as parseYAML } from "yaml";
|
||||
import { z } from "zod";
|
||||
function reportThemeError(path, message) {
|
||||
const text = `${path}: ${message}`;
|
||||
console.error(`[ilovetui theme] ${text}`);
|
||||
notify2(text, { kind: "warning", duration: 0 });
|
||||
}
|
||||
function readThemeFile(path) {
|
||||
if (!existsSync(path))
|
||||
return {};
|
||||
let raw;
|
||||
try {
|
||||
raw = parseYAML(readFileSync(path, "utf8"));
|
||||
} catch (error) {
|
||||
reportThemeError(path, error instanceof Error ? error.message : String(error));
|
||||
return {};
|
||||
}
|
||||
const result = ThemeConfigSchema2.safeParse(raw ?? {});
|
||||
if (!result.success) {
|
||||
reportThemeError(path, z.prettifyError(result.error));
|
||||
return {};
|
||||
}
|
||||
return result.data;
|
||||
}
|
||||
var fileLayers = [readThemeFile(join(import.meta.dir, "default.yaml")), readThemeFile(configPath2())];
|
||||
function buildTheme(overrides) {
|
||||
const resolved = resolveConfig2(layerConfigs2([...fileLayers, overrides]));
|
||||
const colors = resolved.colors;
|
||||
return {
|
||||
...colors,
|
||||
background: colors.base00,
|
||||
subtleBg: colors.base01,
|
||||
selection: colors.base02,
|
||||
subtle: colors.base03,
|
||||
muted: colors.base04,
|
||||
text: colors.base05,
|
||||
primary: colors.base0d,
|
||||
success: colors.base0b,
|
||||
warning: colors.base09,
|
||||
error: colors.base08,
|
||||
nerdFonts: resolved.nerdFonts,
|
||||
emojiFonts: resolved.emojiFonts,
|
||||
reducedMotion: resolved.reducedMotion,
|
||||
mouse: resolved.mouse,
|
||||
borderStyle: resolved.border
|
||||
};
|
||||
}
|
||||
var theme2 = buildTheme({});
|
||||
var presets2 = buildPresets2(theme2);
|
||||
function configureTheme2(overrides = {}) {
|
||||
Object.assign(theme2, buildTheme(overrides));
|
||||
const next = buildPresets2(theme2);
|
||||
for (const key of Object.keys(next))
|
||||
Object.assign(presets2[key], next[key]);
|
||||
}
|
||||
function icon2(codepoint) {
|
||||
return theme2.nerdFonts ? `${codepoint} ` : "";
|
||||
}
|
||||
|
||||
export { theme2, presets2, configureTheme2, icon2 };
|
||||
Vendored
-54
@@ -1,54 +0,0 @@
|
||||
// @bun
|
||||
import {
|
||||
theme2
|
||||
} from "./chunk-p6jvscj3.js";
|
||||
|
||||
// src/components/MinSizeGuard.tsx
|
||||
import { createComponent as _$createComponent } from "@opentui/solid";
|
||||
import { effect as _$effect } from "@opentui/solid";
|
||||
import { insertNode as _$insertNode } from "@opentui/solid";
|
||||
import { insert as _$insert } from "@opentui/solid";
|
||||
import { setProp as _$setProp } from "@opentui/solid";
|
||||
import { createElement as _$createElement } from "@opentui/solid";
|
||||
import { memo as _$memo } from "@opentui/solid";
|
||||
import { useTerminalDimensions } from "@opentui/solid";
|
||||
import { Show } from "solid-js";
|
||||
function MinSizeGuard2(props) {
|
||||
const dimensions = useTerminalDimensions();
|
||||
const minWidth = () => props.minWidth ?? 80;
|
||||
const minHeight = () => props.minHeight ?? 24;
|
||||
const tooSmall = () => dimensions().width < minWidth() || dimensions().height < minHeight();
|
||||
return [_$memo(() => props.children), _$createComponent(Show, {
|
||||
get when() {
|
||||
return tooSmall();
|
||||
},
|
||||
get children() {
|
||||
var _el$ = _$createElement("box"), _el$2 = _$createElement("text");
|
||||
_$insertNode(_el$, _el$2);
|
||||
_$setProp(_el$, "position", "absolute");
|
||||
_$setProp(_el$, "top", 0);
|
||||
_$setProp(_el$, "left", 0);
|
||||
_$setProp(_el$, "alignItems", "center");
|
||||
_$setProp(_el$, "justifyContent", "center");
|
||||
_$setProp(_el$, "zIndex", 9999);
|
||||
_$insert(_el$2, () => `Terminal too small
|
||||
Minimum size: ${minWidth()}x${minHeight()} \u2014 current: ${dimensions().width}x${dimensions().height}`);
|
||||
_$effect((_p$) => {
|
||||
var _v$ = dimensions().width, _v$2 = dimensions().height, _v$3 = props.backgroundColor ?? theme2.background, _v$4 = props.textColor ?? theme2.muted;
|
||||
_v$ !== _p$.e && (_p$.e = _$setProp(_el$, "width", _v$, _p$.e));
|
||||
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$, "height", _v$2, _p$.t));
|
||||
_v$3 !== _p$.a && (_p$.a = _$setProp(_el$, "backgroundColor", _v$3, _p$.a));
|
||||
_v$4 !== _p$.o && (_p$.o = _$setProp(_el$2, "fg", _v$4, _p$.o));
|
||||
return _p$;
|
||||
}, {
|
||||
e: undefined,
|
||||
t: undefined,
|
||||
a: undefined,
|
||||
o: undefined
|
||||
});
|
||||
return _el$;
|
||||
}
|
||||
})];
|
||||
}
|
||||
|
||||
export { MinSizeGuard2 };
|
||||
Vendored
-61
@@ -1,61 +0,0 @@
|
||||
// @bun
|
||||
import {
|
||||
theme2
|
||||
} from "./chunk-p6jvscj3.js";
|
||||
import {
|
||||
Badge2
|
||||
} from "./chunk-v9cetqzr.js";
|
||||
|
||||
// src/components/Toggle.tsx
|
||||
import { setProp as _$setProp } from "@opentui/solid";
|
||||
import { effect as _$effect } from "@opentui/solid";
|
||||
import { insert as _$insert } from "@opentui/solid";
|
||||
import { createComponent as _$createComponent } from "@opentui/solid";
|
||||
import { memo as _$memo } from "@opentui/solid";
|
||||
import { createElement as _$createElement } from "@opentui/solid";
|
||||
import { TextAttributes } from "@opentui/core";
|
||||
import { Show } from "solid-js";
|
||||
function Toggle2(props) {
|
||||
const onColor = () => props.onColor ?? theme2.success;
|
||||
const offColor = () => props.offColor ?? theme2.muted;
|
||||
const nerdFonts = () => props.withNerdfont ?? theme2.nerdFonts;
|
||||
return (() => {
|
||||
var _el$ = _$createElement("box");
|
||||
_$insert(_el$, _$createComponent(Show, {
|
||||
get when() {
|
||||
return nerdFonts();
|
||||
},
|
||||
get fallback() {
|
||||
return (() => {
|
||||
var _el$2 = _$createElement("text");
|
||||
_$insert(_el$2, () => props.on ? "on" : "off");
|
||||
_$effect((_p$) => {
|
||||
var _v$ = props.on ? onColor() : offColor(), _v$2 = TextAttributes.BOLD;
|
||||
_v$ !== _p$.e && (_p$.e = _$setProp(_el$2, "fg", _v$, _p$.e));
|
||||
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$2, "attributes", _v$2, _p$.t));
|
||||
return _p$;
|
||||
}, {
|
||||
e: undefined,
|
||||
t: undefined
|
||||
});
|
||||
return _el$2;
|
||||
})();
|
||||
},
|
||||
get children() {
|
||||
return _$createComponent(Badge2, {
|
||||
get label() {
|
||||
return props.on ? " \u25CF" : "\u25CF ";
|
||||
},
|
||||
get color() {
|
||||
return _$memo(() => !!props.on)() ? onColor() : offColor();
|
||||
},
|
||||
withNerdfont: true
|
||||
});
|
||||
}
|
||||
}));
|
||||
_$effect((_$p) => _$setProp(_el$, "onMouseDown", theme2.mouse ? props.onToggle : undefined, _$p));
|
||||
return _el$;
|
||||
})();
|
||||
}
|
||||
|
||||
export { Toggle2 };
|
||||
Vendored
-155
@@ -1,155 +0,0 @@
|
||||
// @bun
|
||||
import {
|
||||
theme2,
|
||||
presets2
|
||||
} from "./chunk-p6jvscj3.js";
|
||||
|
||||
// src/components/Sidebar.tsx
|
||||
import { use as _$use } from "@opentui/solid";
|
||||
import { createComponent as _$createComponent } from "@opentui/solid";
|
||||
import { createTextNode as _$createTextNode } from "@opentui/solid";
|
||||
import { insertNode as _$insertNode } from "@opentui/solid";
|
||||
import { effect as _$effect } from "@opentui/solid";
|
||||
import { insert as _$insert } from "@opentui/solid";
|
||||
import { setProp as _$setProp } from "@opentui/solid";
|
||||
import { createElement as _$createElement } from "@opentui/solid";
|
||||
import { TextAttributes } from "@opentui/core";
|
||||
import { createEffect, Show } from "solid-js";
|
||||
function truncateWithEllipsis(text, maxWidth) {
|
||||
if (maxWidth <= 0)
|
||||
return "";
|
||||
if (text.length <= maxWidth)
|
||||
return text;
|
||||
return text.substring(0, Math.max(0, maxWidth - 1)) + "\u2026";
|
||||
}
|
||||
function itemIndexAtScreenY(el, screenY) {
|
||||
const internals = el;
|
||||
if (!internals.linesPerItem)
|
||||
return null;
|
||||
const localY = screenY - el.screenY;
|
||||
if (localY < 0)
|
||||
return null;
|
||||
const index = internals.scrollOffset + Math.floor(localY / internals.linesPerItem);
|
||||
return index >= 0 && index < el.options.length ? index : null;
|
||||
}
|
||||
var BORDER_WIDTH = 2;
|
||||
var ITEM_LEFT_PADDING = 1;
|
||||
var ITEM_INDICATOR_WIDTH = 2;
|
||||
function Sidebar2(props) {
|
||||
let select;
|
||||
const contentWidth = () => Math.max(0, (props.width ?? 24) - BORDER_WIDTH);
|
||||
const itemTextWidth = () => Math.max(0, contentWidth() - ITEM_LEFT_PADDING - ITEM_INDICATOR_WIDTH);
|
||||
const displayTitle = () => props.title ? truncateWithEllipsis(props.title, contentWidth()) : props.title;
|
||||
const displayItems = () => props.items.map((item) => ({
|
||||
...item,
|
||||
name: truncateWithEllipsis(item.name, itemTextWidth()),
|
||||
description: truncateWithEllipsis(item.description, itemTextWidth())
|
||||
}));
|
||||
const handleMouseDown = (event) => {
|
||||
if (!select)
|
||||
return;
|
||||
const index = itemIndexAtScreenY(select, event.y);
|
||||
if (index !== null) {
|
||||
select.focus();
|
||||
select.setSelectedIndex(index);
|
||||
select.selectCurrent();
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
const handleMouseScroll = (event) => {
|
||||
if (!select || !event.scroll)
|
||||
return;
|
||||
const steps = Math.max(1, event.scroll.delta);
|
||||
if (event.scroll.direction === "down")
|
||||
select.moveDown(steps);
|
||||
else if (event.scroll.direction === "up")
|
||||
select.moveUp(steps);
|
||||
};
|
||||
createEffect(() => {
|
||||
const index = props.items.findIndex((item) => item.value === props.value);
|
||||
if (select && index !== -1 && select.getSelectedIndex() !== index)
|
||||
select.setSelectedIndex(index);
|
||||
});
|
||||
return (() => {
|
||||
var _el$ = _$createElement("box"), _el$5 = _$createElement("select");
|
||||
_$insertNode(_el$, _el$5);
|
||||
_$setProp(_el$, "border", true);
|
||||
_$setProp(_el$, "flexDirection", "column");
|
||||
_$insert(_el$, _$createComponent(Show, {
|
||||
get when() {
|
||||
return props.title;
|
||||
},
|
||||
get children() {
|
||||
return [(() => {
|
||||
var _el$2 = _$createElement("text");
|
||||
_$insert(_el$2, displayTitle);
|
||||
_$effect((_$p) => _$setProp(_el$2, "attributes", TextAttributes.BOLD, _$p));
|
||||
return _el$2;
|
||||
})(), (() => {
|
||||
var _el$3 = _$createElement("text");
|
||||
_$insertNode(_el$3, _$createTextNode(` `));
|
||||
return _el$3;
|
||||
})()];
|
||||
}
|
||||
}), _el$5);
|
||||
_$use((el) => {
|
||||
select = el ?? undefined;
|
||||
props.ref?.(el && {
|
||||
moveUp: () => el.moveUp(),
|
||||
moveDown: () => el.moveDown(),
|
||||
selectCurrent: () => el.selectCurrent(),
|
||||
focus: () => el.focus()
|
||||
});
|
||||
}, _el$5);
|
||||
_$setProp(_el$5, "flexGrow", 1);
|
||||
_$setProp(_el$5, "showScrollIndicator", true);
|
||||
_$setProp(_el$5, "onChange", (_index, option) => {
|
||||
if (!option)
|
||||
return;
|
||||
props.onSelect(option);
|
||||
});
|
||||
_$setProp(_el$5, "onSelect", (_index, option) => {
|
||||
if (!option)
|
||||
return;
|
||||
props.onConfirm?.(option);
|
||||
});
|
||||
_$effect((_p$) => {
|
||||
var _v$ = props.width ?? 24, _v$2 = theme2.borderStyle, _v$3 = props.focused ? props.accentColor ?? theme2.primary : props.mutedColor ?? theme2.muted, _v$4 = props.focused, _v$5 = displayItems(), _v$6 = theme2.mouse ? handleMouseDown : undefined, _v$7 = theme2.mouse ? handleMouseScroll : undefined, _v$8 = props.backgroundColor ?? "transparent", _v$9 = props.textColor ?? presets2.select.textColor, _v$0 = props.focusedBackgroundColor ?? "transparent", _v$1 = props.focusedTextColor ?? presets2.select.focusedTextColor, _v$10 = props.selectedBackgroundColor ?? presets2.select.selectedBackgroundColor, _v$11 = props.selectedTextColor ?? presets2.select.selectedTextColor, _v$12 = props.descriptionColor ?? presets2.select.descriptionColor, _v$13 = props.selectedDescriptionColor ?? presets2.select.selectedDescriptionColor;
|
||||
_v$ !== _p$.e && (_p$.e = _$setProp(_el$, "width", _v$, _p$.e));
|
||||
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$, "borderStyle", _v$2, _p$.t));
|
||||
_v$3 !== _p$.a && (_p$.a = _$setProp(_el$, "borderColor", _v$3, _p$.a));
|
||||
_v$4 !== _p$.o && (_p$.o = _$setProp(_el$5, "focused", _v$4, _p$.o));
|
||||
_v$5 !== _p$.i && (_p$.i = _$setProp(_el$5, "options", _v$5, _p$.i));
|
||||
_v$6 !== _p$.n && (_p$.n = _$setProp(_el$5, "onMouseDown", _v$6, _p$.n));
|
||||
_v$7 !== _p$.s && (_p$.s = _$setProp(_el$5, "onMouseScroll", _v$7, _p$.s));
|
||||
_v$8 !== _p$.h && (_p$.h = _$setProp(_el$5, "backgroundColor", _v$8, _p$.h));
|
||||
_v$9 !== _p$.r && (_p$.r = _$setProp(_el$5, "textColor", _v$9, _p$.r));
|
||||
_v$0 !== _p$.d && (_p$.d = _$setProp(_el$5, "focusedBackgroundColor", _v$0, _p$.d));
|
||||
_v$1 !== _p$.l && (_p$.l = _$setProp(_el$5, "focusedTextColor", _v$1, _p$.l));
|
||||
_v$10 !== _p$.u && (_p$.u = _$setProp(_el$5, "selectedBackgroundColor", _v$10, _p$.u));
|
||||
_v$11 !== _p$.c && (_p$.c = _$setProp(_el$5, "selectedTextColor", _v$11, _p$.c));
|
||||
_v$12 !== _p$.w && (_p$.w = _$setProp(_el$5, "descriptionColor", _v$12, _p$.w));
|
||||
_v$13 !== _p$.m && (_p$.m = _$setProp(_el$5, "selectedDescriptionColor", _v$13, _p$.m));
|
||||
return _p$;
|
||||
}, {
|
||||
e: undefined,
|
||||
t: undefined,
|
||||
a: undefined,
|
||||
o: undefined,
|
||||
i: undefined,
|
||||
n: undefined,
|
||||
s: undefined,
|
||||
h: undefined,
|
||||
r: undefined,
|
||||
d: undefined,
|
||||
l: undefined,
|
||||
u: undefined,
|
||||
c: undefined,
|
||||
w: undefined,
|
||||
m: undefined
|
||||
});
|
||||
return _el$;
|
||||
})();
|
||||
}
|
||||
|
||||
export { Sidebar2 };
|
||||
Vendored
-71
@@ -1,71 +0,0 @@
|
||||
// @bun
|
||||
import {
|
||||
theme2,
|
||||
presets2
|
||||
} from "./chunk-p6jvscj3.js";
|
||||
|
||||
// src/components/ListRow.tsx
|
||||
import { effect as _$effect } from "@opentui/solid";
|
||||
import { createComponent as _$createComponent } from "@opentui/solid";
|
||||
import { insertNode as _$insertNode } from "@opentui/solid";
|
||||
import { insert as _$insert } from "@opentui/solid";
|
||||
import { setProp as _$setProp } from "@opentui/solid";
|
||||
import { createElement as _$createElement } from "@opentui/solid";
|
||||
import { TextAttributes } from "@opentui/core";
|
||||
import { Show } from "solid-js";
|
||||
function ListRow2(props) {
|
||||
const tone = (fallback) => props.selected ? presets2.select.selectedTextColor : fallback;
|
||||
return (() => {
|
||||
var _el$ = _$createElement("box"), _el$2 = _$createElement("box"), _el$3 = _$createElement("box"), _el$4 = _$createElement("text");
|
||||
_$insertNode(_el$, _el$2);
|
||||
_$setProp(_el$, "flexShrink", 0);
|
||||
_$setProp(_el$, "flexDirection", "column");
|
||||
_$setProp(_el$, "paddingLeft", 1);
|
||||
_$setProp(_el$, "paddingRight", 1);
|
||||
_$insertNode(_el$2, _el$3);
|
||||
_$setProp(_el$2, "flexDirection", "row");
|
||||
_$setProp(_el$2, "justifyContent", "space-between");
|
||||
_$setProp(_el$2, "alignItems", "center");
|
||||
_$setProp(_el$2, "height", 1);
|
||||
_$insertNode(_el$3, _el$4);
|
||||
_$setProp(_el$3, "flexGrow", 1);
|
||||
_$setProp(_el$3, "flexShrink", 1);
|
||||
_$setProp(_el$3, "minWidth", 0);
|
||||
_$setProp(_el$3, "marginRight", 1);
|
||||
_$setProp(_el$3, "overflow", "hidden");
|
||||
_$setProp(_el$4, "wrapMode", "none");
|
||||
_$insert(_el$4, () => props.label);
|
||||
_$insert(_el$2, _$createComponent(Show, {
|
||||
get when() {
|
||||
return props.right;
|
||||
},
|
||||
get children() {
|
||||
var _el$5 = _$createElement("box");
|
||||
_$setProp(_el$5, "flexShrink", 0);
|
||||
_$setProp(_el$5, "flexDirection", "row");
|
||||
_$setProp(_el$5, "alignItems", "center");
|
||||
_$insert(_el$5, () => props.right?.(tone));
|
||||
return _el$5;
|
||||
}
|
||||
}), null);
|
||||
_$insert(_el$, () => props.below?.(tone), null);
|
||||
_$effect((_p$) => {
|
||||
var _v$ = props.id, _v$2 = props.selected ? presets2.select.selectedBackgroundColor : "transparent", _v$3 = theme2.mouse && props.onClick ? () => props.onClick?.() : undefined, _v$4 = tone(), _v$5 = props.bold ? TextAttributes.BOLD : undefined;
|
||||
_v$ !== _p$.e && (_p$.e = _$setProp(_el$, "id", _v$, _p$.e));
|
||||
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$, "backgroundColor", _v$2, _p$.t));
|
||||
_v$3 !== _p$.a && (_p$.a = _$setProp(_el$, "onMouseDown", _v$3, _p$.a));
|
||||
_v$4 !== _p$.o && (_p$.o = _$setProp(_el$4, "fg", _v$4, _p$.o));
|
||||
_v$5 !== _p$.i && (_p$.i = _$setProp(_el$4, "attributes", _v$5, _p$.i));
|
||||
return _p$;
|
||||
}, {
|
||||
e: undefined,
|
||||
t: undefined,
|
||||
a: undefined,
|
||||
o: undefined,
|
||||
i: undefined
|
||||
});
|
||||
return _el$;
|
||||
})();
|
||||
}
|
||||
|
||||
export { ListRow2 };
|
||||
Vendored
-140
@@ -1,140 +0,0 @@
|
||||
// @bun
|
||||
import {
|
||||
theme2
|
||||
} from "./chunk-p6jvscj3.js";
|
||||
|
||||
// src/components/Modal.tsx
|
||||
import { insertNode as _$insertNode } from "@opentui/solid";
|
||||
import { effect as _$effect } from "@opentui/solid";
|
||||
import { insert as _$insert } from "@opentui/solid";
|
||||
import { use as _$use } from "@opentui/solid";
|
||||
import { setProp as _$setProp } from "@opentui/solid";
|
||||
import { createElement as _$createElement } from "@opentui/solid";
|
||||
import { createComponent as _$createComponent } from "@opentui/solid";
|
||||
import { RGBA } from "@opentui/core";
|
||||
import { useBindings } from "@opentui/keymap/solid";
|
||||
import { Portal, useRenderer, useTerminalDimensions } from "@opentui/solid";
|
||||
import { createSignal, onCleanup, onMount, Show } from "solid-js";
|
||||
function Modal2(props) {
|
||||
return _$createComponent(Portal, {
|
||||
get children() {
|
||||
return _$createComponent(Show, {
|
||||
get when() {
|
||||
return props.open;
|
||||
},
|
||||
get children() {
|
||||
return _$createComponent(ModalPanel, props);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
function ModalPanel(props) {
|
||||
const renderer = useRenderer();
|
||||
const dimensions = useTerminalDimensions();
|
||||
const [panel, setPanel] = createSignal();
|
||||
const screenCap = () => Math.max(5, dimensions().height - 4);
|
||||
const maxPanelHeight = () => Math.min(props.maxHeight ?? screenCap(), screenCap());
|
||||
const maxContentHeight = () => Math.max(1, maxPanelHeight() - 4);
|
||||
const maxPanelWidth = () => Math.max(10, dimensions().width - 4);
|
||||
const panelWidth = () => Math.min(props.width ?? 50, maxPanelWidth());
|
||||
let previousFocus = null;
|
||||
onMount(() => {
|
||||
previousFocus = renderer.currentFocusedRenderable;
|
||||
props.onOpen?.(previousFocus);
|
||||
panel()?.focus();
|
||||
});
|
||||
onCleanup(() => {
|
||||
if (previousFocus && !previousFocus.isDestroyed)
|
||||
previousFocus.focus();
|
||||
});
|
||||
const dismissKey = props.dismissKey ?? "escape";
|
||||
if (dismissKey !== false || props.commands || props.bindings) {
|
||||
useBindings(() => ({
|
||||
target: panel,
|
||||
commands: [...dismissKey !== false ? [{
|
||||
name: "modal_dismiss",
|
||||
desc: "close",
|
||||
shortHelp: true,
|
||||
run: () => props.onDismiss?.()
|
||||
}] : [], ...props.commands ?? []],
|
||||
bindings: [...dismissKey !== false ? [{
|
||||
key: dismissKey,
|
||||
cmd: "modal_dismiss"
|
||||
}] : [], ...props.bindings ?? []]
|
||||
}));
|
||||
}
|
||||
return (() => {
|
||||
var _el$ = _$createElement("box"), _el$2 = _$createElement("box");
|
||||
_$insertNode(_el$, _el$2);
|
||||
_$setProp(_el$, "position", "absolute");
|
||||
_$setProp(_el$, "top", 0);
|
||||
_$setProp(_el$, "left", 0);
|
||||
_$setProp(_el$, "alignItems", "center");
|
||||
_$setProp(_el$, "justifyContent", "center");
|
||||
_$setProp(_el$, "zIndex", 2000);
|
||||
_$use(setPanel, _el$2);
|
||||
_$setProp(_el$2, "focusable", true);
|
||||
_$setProp(_el$2, "border", true);
|
||||
_$setProp(_el$2, "flexDirection", "column");
|
||||
_$setProp(_el$2, "paddingLeft", 2);
|
||||
_$setProp(_el$2, "paddingRight", 2);
|
||||
_$setProp(_el$2, "paddingTop", 1);
|
||||
_$setProp(_el$2, "paddingBottom", 1);
|
||||
_$insert(_el$2, _$createComponent(Show, {
|
||||
get when() {
|
||||
return props.scrollable;
|
||||
},
|
||||
get fallback() {
|
||||
return (() => {
|
||||
var _el$4 = _$createElement("box");
|
||||
_$setProp(_el$4, "flexDirection", "column");
|
||||
_$setProp(_el$4, "overflow", "hidden");
|
||||
_$insert(_el$4, () => props.children);
|
||||
_$effect((_$p) => _$setProp(_el$4, "maxHeight", maxContentHeight(), _$p));
|
||||
return _el$4;
|
||||
})();
|
||||
},
|
||||
get children() {
|
||||
var _el$3 = _$createElement("scrollbox");
|
||||
_$setProp(_el$3, "flexShrink", 1);
|
||||
_$setProp(_el$3, "scrollY", true);
|
||||
_$setProp(_el$3, "scrollX", false);
|
||||
_$setProp(_el$3, "focusable", false);
|
||||
_$insert(_el$3, () => props.children);
|
||||
_$effect((_$p) => _$setProp(_el$3, "maxHeight", maxContentHeight(), _$p));
|
||||
return _el$3;
|
||||
}
|
||||
}));
|
||||
_$effect((_p$) => {
|
||||
var _v$ = dimensions().width, _v$2 = dimensions().height, _v$3 = props.backdropColor ?? RGBA.fromInts(0, 0, 0, 150), _v$4 = theme2.mouse ? props.onDismiss : undefined, _v$5 = panelWidth(), _v$6 = maxPanelHeight(), _v$7 = theme2.borderStyle, _v$8 = props.accentColor ?? theme2.primary, _v$9 = props.accentColor ?? theme2.primary, _v$0 = props.backgroundColor ?? theme2.background, _v$1 = theme2.mouse ? (event) => event.stopPropagation() : undefined;
|
||||
_v$ !== _p$.e && (_p$.e = _$setProp(_el$, "width", _v$, _p$.e));
|
||||
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$, "height", _v$2, _p$.t));
|
||||
_v$3 !== _p$.a && (_p$.a = _$setProp(_el$, "backgroundColor", _v$3, _p$.a));
|
||||
_v$4 !== _p$.o && (_p$.o = _$setProp(_el$, "onMouseDown", _v$4, _p$.o));
|
||||
_v$5 !== _p$.i && (_p$.i = _$setProp(_el$2, "width", _v$5, _p$.i));
|
||||
_v$6 !== _p$.n && (_p$.n = _$setProp(_el$2, "maxHeight", _v$6, _p$.n));
|
||||
_v$7 !== _p$.s && (_p$.s = _$setProp(_el$2, "borderStyle", _v$7, _p$.s));
|
||||
_v$8 !== _p$.h && (_p$.h = _$setProp(_el$2, "borderColor", _v$8, _p$.h));
|
||||
_v$9 !== _p$.r && (_p$.r = _$setProp(_el$2, "focusedBorderColor", _v$9, _p$.r));
|
||||
_v$0 !== _p$.d && (_p$.d = _$setProp(_el$2, "backgroundColor", _v$0, _p$.d));
|
||||
_v$1 !== _p$.l && (_p$.l = _$setProp(_el$2, "onMouseDown", _v$1, _p$.l));
|
||||
return _p$;
|
||||
}, {
|
||||
e: undefined,
|
||||
t: undefined,
|
||||
a: undefined,
|
||||
o: undefined,
|
||||
i: undefined,
|
||||
n: undefined,
|
||||
s: undefined,
|
||||
h: undefined,
|
||||
r: undefined,
|
||||
d: undefined,
|
||||
l: undefined
|
||||
});
|
||||
return _el$;
|
||||
})();
|
||||
}
|
||||
|
||||
export { Modal2 };
|
||||
Vendored
-32
@@ -1,32 +0,0 @@
|
||||
// @bun
|
||||
import {
|
||||
theme2
|
||||
} from "./chunk-p6jvscj3.js";
|
||||
|
||||
// src/components/SignalBars.tsx
|
||||
import { effect as _$effect } from "@opentui/solid";
|
||||
import { insert as _$insert } from "@opentui/solid";
|
||||
import { createComponent as _$createComponent } from "@opentui/solid";
|
||||
import { setProp as _$setProp } from "@opentui/solid";
|
||||
import { createElement as _$createElement } from "@opentui/solid";
|
||||
import { For } from "solid-js";
|
||||
var BARS = ["\u2581", "\u2583", "\u2585", "\u2587"];
|
||||
function SignalBars2(props) {
|
||||
const level = () => Math.floor(props.signal * BARS.length / 100);
|
||||
return (() => {
|
||||
var _el$ = _$createElement("box");
|
||||
_$setProp(_el$, "flexDirection", "row");
|
||||
_$insert(_el$, _$createComponent(For, {
|
||||
each: BARS,
|
||||
children: (bar, i) => (() => {
|
||||
var _el$2 = _$createElement("text");
|
||||
_$insert(_el$2, bar);
|
||||
_$effect((_$p) => _$setProp(_el$2, "fg", i() < level() ? props.filledColor : props.emptyColor ?? theme2.muted, _$p));
|
||||
return _el$2;
|
||||
})()
|
||||
}));
|
||||
return _el$;
|
||||
})();
|
||||
}
|
||||
|
||||
export { SignalBars2 };
|
||||
Vendored
-83
@@ -1,83 +0,0 @@
|
||||
// @bun
|
||||
import {
|
||||
theme2
|
||||
} from "./chunk-p6jvscj3.js";
|
||||
|
||||
// src/components/Badge.tsx
|
||||
import { insert as _$insert } from "@opentui/solid";
|
||||
import { memo as _$memo } from "@opentui/solid";
|
||||
import { createComponent as _$createComponent } from "@opentui/solid";
|
||||
import { effect as _$effect } from "@opentui/solid";
|
||||
import { createTextNode as _$createTextNode } from "@opentui/solid";
|
||||
import { insertNode as _$insertNode } from "@opentui/solid";
|
||||
import { setProp as _$setProp } from "@opentui/solid";
|
||||
import { createElement as _$createElement } from "@opentui/solid";
|
||||
import { TextAttributes } from "@opentui/core";
|
||||
import { Show } from "solid-js";
|
||||
function Badge2(props) {
|
||||
const textColor = () => props.textColor ?? theme2.background;
|
||||
const backgroundColor = () => props.backgroundColor ?? theme2.background;
|
||||
const attributes = () => props.bold === false ? undefined : TextAttributes.BOLD;
|
||||
const nerdFonts = () => props.withNerdfont ?? theme2.nerdFonts;
|
||||
return (() => {
|
||||
var _el$ = _$createElement("box"), _el$4 = _$createElement("text");
|
||||
_$insertNode(_el$, _el$4);
|
||||
_$setProp(_el$, "flexDirection", "row");
|
||||
_$insert(_el$, _$createComponent(Show, {
|
||||
get when() {
|
||||
return nerdFonts();
|
||||
},
|
||||
get children() {
|
||||
var _el$2 = _$createElement("text");
|
||||
_$insertNode(_el$2, _$createTextNode(`\uE0B6`));
|
||||
_$effect((_p$) => {
|
||||
var _v$ = props.color, _v$2 = backgroundColor();
|
||||
_v$ !== _p$.e && (_p$.e = _$setProp(_el$2, "fg", _v$, _p$.e));
|
||||
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$2, "bg", _v$2, _p$.t));
|
||||
return _p$;
|
||||
}, {
|
||||
e: undefined,
|
||||
t: undefined
|
||||
});
|
||||
return _el$2;
|
||||
}
|
||||
}), _el$4);
|
||||
_$insert(_el$4, (() => {
|
||||
var _c$ = _$memo(() => !!nerdFonts());
|
||||
return () => _c$() ? props.label : ` ${props.label} `;
|
||||
})());
|
||||
_$insert(_el$, _$createComponent(Show, {
|
||||
get when() {
|
||||
return nerdFonts();
|
||||
},
|
||||
get children() {
|
||||
var _el$5 = _$createElement("text");
|
||||
_$insertNode(_el$5, _$createTextNode(`\uE0B4`));
|
||||
_$effect((_p$) => {
|
||||
var _v$3 = props.color, _v$4 = backgroundColor();
|
||||
_v$3 !== _p$.e && (_p$.e = _$setProp(_el$5, "fg", _v$3, _p$.e));
|
||||
_v$4 !== _p$.t && (_p$.t = _$setProp(_el$5, "bg", _v$4, _p$.t));
|
||||
return _p$;
|
||||
}, {
|
||||
e: undefined,
|
||||
t: undefined
|
||||
});
|
||||
return _el$5;
|
||||
}
|
||||
}), null);
|
||||
_$effect((_p$) => {
|
||||
var _v$5 = textColor(), _v$6 = props.color, _v$7 = attributes();
|
||||
_v$5 !== _p$.e && (_p$.e = _$setProp(_el$4, "fg", _v$5, _p$.e));
|
||||
_v$6 !== _p$.t && (_p$.t = _$setProp(_el$4, "bg", _v$6, _p$.t));
|
||||
_v$7 !== _p$.a && (_p$.a = _$setProp(_el$4, "attributes", _v$7, _p$.a));
|
||||
return _p$;
|
||||
}, {
|
||||
e: undefined,
|
||||
t: undefined,
|
||||
a: undefined
|
||||
});
|
||||
return _el$;
|
||||
})();
|
||||
}
|
||||
|
||||
export { Badge2 };
|
||||
Vendored
-42
@@ -1,42 +0,0 @@
|
||||
// @bun
|
||||
import {
|
||||
useFocusWithin2
|
||||
} from "./chunk-59dzyjgd.js";
|
||||
|
||||
// src/components/Pane.tsx
|
||||
import { createComponent as _$createComponent } from "@opentui/solid";
|
||||
import { insert as _$insert } from "@opentui/solid";
|
||||
import { use as _$use } from "@opentui/solid";
|
||||
import { spread as _$spread } from "@opentui/solid";
|
||||
import { createElement as _$createElement } from "@opentui/solid";
|
||||
import { createContext, createSignal, splitProps, useContext } from "solid-js";
|
||||
var PaneContext = createContext();
|
||||
function Pane2(props) {
|
||||
const [local, rest] = splitProps(props, ["handle", "children"]);
|
||||
const [el, setEl] = createSignal();
|
||||
const focused = useFocusWithin2(el);
|
||||
const handle = {
|
||||
target: el,
|
||||
focused,
|
||||
focus: () => el()?.focus()
|
||||
};
|
||||
local.handle?.(handle);
|
||||
return _$createComponent(PaneContext.Provider, {
|
||||
value: handle,
|
||||
get children() {
|
||||
var _el$ = _$createElement("box");
|
||||
_$use(setEl, _el$);
|
||||
_$spread(_el$, rest, true);
|
||||
_$insert(_el$, () => local.children);
|
||||
return _el$;
|
||||
}
|
||||
});
|
||||
}
|
||||
function usePane2() {
|
||||
const pane = useContext(PaneContext);
|
||||
if (!pane)
|
||||
throw new Error("usePane() must be used inside a <Pane>");
|
||||
return pane;
|
||||
}
|
||||
|
||||
export { Pane2, usePane2 };
|
||||
Vendored
-168
@@ -1,168 +0,0 @@
|
||||
// @bun
|
||||
import {
|
||||
theme2
|
||||
} from "./chunk-p6jvscj3.js";
|
||||
|
||||
// src/components/Tabs.tsx
|
||||
import { effect as _$effect } from "@opentui/solid";
|
||||
import { insertNode as _$insertNode } from "@opentui/solid";
|
||||
import { createComponent as _$createComponent } from "@opentui/solid";
|
||||
import { insert as _$insert } from "@opentui/solid";
|
||||
import { use as _$use } from "@opentui/solid";
|
||||
import { setProp as _$setProp } from "@opentui/solid";
|
||||
import { createElement as _$createElement } from "@opentui/solid";
|
||||
import { BorderChars, CliRenderEvents, TextAttributes } from "@opentui/core";
|
||||
import { onResize, useRenderer } from "@opentui/solid";
|
||||
import { createMemo, createSignal, Index } from "solid-js";
|
||||
function stepTabValue2(items, value, delta) {
|
||||
const index = items.findIndex((t) => t.value === value);
|
||||
if (index === -1)
|
||||
return value;
|
||||
return items[(index + delta + items.length) % items.length].value;
|
||||
}
|
||||
function innerWidth(label) {
|
||||
return [...label].length + 2;
|
||||
}
|
||||
function segmentWidth(label) {
|
||||
return innerWidth(label) + 2;
|
||||
}
|
||||
function topEdge(chars, label) {
|
||||
return `${chars.topLeft}${chars.horizontal.repeat(innerWidth(label))}${chars.topRight}`;
|
||||
}
|
||||
function bottomEdge(chars, label, isFirst, isLast, isActive) {
|
||||
const left = isFirst ? isActive ? chars.vertical : chars.leftT : isActive ? chars.bottomRight : chars.bottomT;
|
||||
const right = isLast ? isActive ? chars.vertical : chars.rightT : isActive ? chars.bottomLeft : chars.bottomT;
|
||||
const fill = isActive ? " " : chars.horizontal;
|
||||
return left + fill.repeat(innerWidth(label)) + right;
|
||||
}
|
||||
function buildBottomRow(chars, segments, available) {
|
||||
const tabsWidth = segments.reduce((sum, s) => sum + segmentWidth(s.label), 0);
|
||||
const remainder = available - tabsWidth;
|
||||
const extending = remainder > 0;
|
||||
const tabs = segments.map((s, i) => bottomEdge(chars, s.label, i === 0, i === segments.length - 1 && !extending, s.isActive)).join("");
|
||||
return extending ? tabs + chars.horizontal.repeat(remainder - 1) + chars.topRight : tabs;
|
||||
}
|
||||
function fitCount(items, start, available) {
|
||||
const hiddenBefore = start;
|
||||
let used = 0;
|
||||
let count = 0;
|
||||
for (let i = start;i < items.length; i++) {
|
||||
const width = segmentWidth(items[i].label);
|
||||
const hiddenAfter = items.length - start - count - 1;
|
||||
const totalHidden = hiddenBefore + hiddenAfter;
|
||||
const reserve = totalHidden > 0 ? segmentWidth(`+${totalHidden}`) : 0;
|
||||
if (used + width + reserve > available)
|
||||
break;
|
||||
used += width;
|
||||
count++;
|
||||
}
|
||||
return Math.max(count, 1);
|
||||
}
|
||||
function Tabs2(props) {
|
||||
const renderer = useRenderer();
|
||||
const [width, setWidth] = createSignal(0);
|
||||
let container;
|
||||
const measure = () => {
|
||||
if (!container)
|
||||
return;
|
||||
setWidth(container.width);
|
||||
};
|
||||
const remeasureNextFrame = () => renderer.once(CliRenderEvents.FRAME, measure);
|
||||
onResize(remeasureNextFrame);
|
||||
const segments = createMemo(() => {
|
||||
const items = props.items;
|
||||
const activeIndex = Math.max(0, items.findIndex((t) => t.value === props.value));
|
||||
let start = 0;
|
||||
let count = fitCount(items, start, width());
|
||||
if (activeIndex >= start + count) {
|
||||
start = activeIndex;
|
||||
count = fitCount(items, start, width());
|
||||
}
|
||||
const shownItems = items.slice(start, start + count);
|
||||
const shown = shownItems.map((item) => ({
|
||||
value: item.value,
|
||||
label: item.label,
|
||||
isActive: item.value === props.value
|
||||
}));
|
||||
const hidden = items.length - count;
|
||||
if (hidden > 0) {
|
||||
const used = shownItems.reduce((sum, item) => sum + segmentWidth(item.label), 0);
|
||||
const tag = `+${hidden}`;
|
||||
if (shown.length === 0 || used + segmentWidth(tag) <= width()) {
|
||||
shown.push({
|
||||
value: undefined,
|
||||
label: tag,
|
||||
isActive: false
|
||||
});
|
||||
}
|
||||
}
|
||||
return shown;
|
||||
});
|
||||
const accent = () => props.accentColor ?? theme2.primary;
|
||||
const muted = () => props.mutedColor ?? theme2.muted;
|
||||
const borderColor = () => props.focused ? accent() : muted();
|
||||
const chars = BorderChars[theme2.borderStyle];
|
||||
return (() => {
|
||||
var _el$ = _$createElement("box"), _el$2 = _$createElement("text"), _el$3 = _$createElement("box"), _el$4 = _$createElement("text");
|
||||
_$insertNode(_el$, _el$2);
|
||||
_$insertNode(_el$, _el$3);
|
||||
_$insertNode(_el$, _el$4);
|
||||
_$use((el) => {
|
||||
container = el;
|
||||
remeasureNextFrame();
|
||||
}, _el$);
|
||||
_$setProp(_el$, "flexDirection", "column");
|
||||
_$setProp(_el$, "height", 3);
|
||||
_$setProp(_el$2, "selectable", false);
|
||||
_$insert(_el$2, () => segments().map((s) => topEdge(chars, s.label)).join(""));
|
||||
_$setProp(_el$3, "flexDirection", "row");
|
||||
_$insert(_el$3, _$createComponent(Index, {
|
||||
get each() {
|
||||
return segments();
|
||||
},
|
||||
children: (s) => [(() => {
|
||||
var _el$5 = _$createElement("text");
|
||||
_$setProp(_el$5, "selectable", false);
|
||||
_$insert(_el$5, () => chars.vertical);
|
||||
_$effect((_$p) => _$setProp(_el$5, "fg", borderColor(), _$p));
|
||||
return _el$5;
|
||||
})(), (() => {
|
||||
var _el$6 = _$createElement("text");
|
||||
_$setProp(_el$6, "selectable", false);
|
||||
_$insert(_el$6, () => ` ${s().label} `);
|
||||
_$effect((_p$) => {
|
||||
var _v$3 = s().isActive ? accent() : muted(), _v$4 = s().isActive ? TextAttributes.BOLD : undefined, _v$5 = theme2.mouse && s().value !== undefined ? () => props.onChange(s().value) : undefined;
|
||||
_v$3 !== _p$.e && (_p$.e = _$setProp(_el$6, "fg", _v$3, _p$.e));
|
||||
_v$4 !== _p$.t && (_p$.t = _$setProp(_el$6, "attributes", _v$4, _p$.t));
|
||||
_v$5 !== _p$.a && (_p$.a = _$setProp(_el$6, "onMouseDown", _v$5, _p$.a));
|
||||
return _p$;
|
||||
}, {
|
||||
e: undefined,
|
||||
t: undefined,
|
||||
a: undefined
|
||||
});
|
||||
return _el$6;
|
||||
})(), (() => {
|
||||
var _el$7 = _$createElement("text");
|
||||
_$setProp(_el$7, "selectable", false);
|
||||
_$insert(_el$7, () => chars.vertical);
|
||||
_$effect((_$p) => _$setProp(_el$7, "fg", borderColor(), _$p));
|
||||
return _el$7;
|
||||
})()]
|
||||
}));
|
||||
_$setProp(_el$4, "selectable", false);
|
||||
_$insert(_el$4, () => buildBottomRow(chars, segments(), width()));
|
||||
_$effect((_p$) => {
|
||||
var _v$ = borderColor(), _v$2 = borderColor();
|
||||
_v$ !== _p$.e && (_p$.e = _$setProp(_el$2, "fg", _v$, _p$.e));
|
||||
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$4, "fg", _v$2, _p$.t));
|
||||
return _p$;
|
||||
}, {
|
||||
e: undefined,
|
||||
t: undefined
|
||||
});
|
||||
return _el$;
|
||||
})();
|
||||
}
|
||||
|
||||
export { stepTabValue2, Tabs2 };
|
||||
Vendored
-52
@@ -1,52 +0,0 @@
|
||||
// @bun
|
||||
import {
|
||||
theme2
|
||||
} from "./chunk-p6jvscj3.js";
|
||||
import {
|
||||
Box2
|
||||
} from "./chunk-8r4rajaz.js";
|
||||
import {
|
||||
Tabs2
|
||||
} from "./chunk-vf2ybhtx.js";
|
||||
|
||||
// src/components/TabbedPanel.tsx
|
||||
import { insert as _$insert } from "@opentui/solid";
|
||||
import { memo as _$memo } from "@opentui/solid";
|
||||
import { createComponent as _$createComponent } from "@opentui/solid";
|
||||
import { setProp as _$setProp } from "@opentui/solid";
|
||||
import { createElement as _$createElement } from "@opentui/solid";
|
||||
function TabbedPanel2(props) {
|
||||
return (() => {
|
||||
var _el$ = _$createElement("box");
|
||||
_$setProp(_el$, "flexDirection", "column");
|
||||
_$setProp(_el$, "flexGrow", 1);
|
||||
_$insert(_el$, _$createComponent(Tabs2, {
|
||||
get items() {
|
||||
return props.items;
|
||||
},
|
||||
get value() {
|
||||
return props.value;
|
||||
},
|
||||
get onChange() {
|
||||
return props.onChange;
|
||||
},
|
||||
get focused() {
|
||||
return props.focused;
|
||||
}
|
||||
}), null);
|
||||
_$insert(_el$, _$createComponent(Box2, {
|
||||
flexGrow: 1,
|
||||
border: ["left", "right", "bottom"],
|
||||
get borderColor() {
|
||||
return _$memo(() => !!props.focused)() ? theme2.primary : theme2.muted;
|
||||
},
|
||||
flexDirection: "column",
|
||||
get children() {
|
||||
return props.children;
|
||||
}
|
||||
}), null);
|
||||
return _el$;
|
||||
})();
|
||||
}
|
||||
|
||||
export { TabbedPanel2 };
|
||||
Vendored
-10
@@ -1,10 +0,0 @@
|
||||
// @bun
|
||||
// src/yaml.ts
|
||||
import { homedir } from "os";
|
||||
import { join } from "path";
|
||||
function configPath2() {
|
||||
const configDir = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
||||
return join(configDir, "ilovetui", "config.yaml");
|
||||
}
|
||||
|
||||
export { configPath2 };
|
||||
Vendored
-44
@@ -1,44 +0,0 @@
|
||||
// @bun
|
||||
// src/presets.ts
|
||||
function buildPresets2(theme) {
|
||||
const select = {
|
||||
backgroundColor: theme.background,
|
||||
textColor: theme.text,
|
||||
focusedBackgroundColor: theme.subtleBg,
|
||||
focusedTextColor: theme.text,
|
||||
selectedBackgroundColor: theme.selection,
|
||||
selectedTextColor: theme.primary,
|
||||
descriptionColor: theme.muted,
|
||||
selectedDescriptionColor: theme.text
|
||||
};
|
||||
const tabSelect = {
|
||||
backgroundColor: theme.background,
|
||||
textColor: theme.text,
|
||||
focusedBackgroundColor: theme.subtleBg,
|
||||
focusedTextColor: theme.text,
|
||||
selectedBackgroundColor: theme.selection,
|
||||
selectedTextColor: theme.primary,
|
||||
selectedDescriptionColor: theme.text
|
||||
};
|
||||
const input = {
|
||||
backgroundColor: theme.background,
|
||||
textColor: theme.text,
|
||||
focusedBackgroundColor: theme.subtleBg,
|
||||
focusedTextColor: theme.text,
|
||||
placeholderColor: theme.muted
|
||||
};
|
||||
const box = {
|
||||
border: true,
|
||||
borderStyle: theme.borderStyle,
|
||||
borderColor: theme.subtle,
|
||||
focusedBorderColor: theme.primary,
|
||||
backgroundColor: theme.background
|
||||
};
|
||||
const slider = {
|
||||
backgroundColor: theme.subtleBg,
|
||||
foregroundColor: theme.primary
|
||||
};
|
||||
return { select, tabSelect, input, textarea: input, box, slider };
|
||||
}
|
||||
|
||||
export { buildPresets2 };
|
||||
Vendored
-58
@@ -1,58 +0,0 @@
|
||||
// @bun
|
||||
import {
|
||||
theme2
|
||||
} from "./chunk-p6jvscj3.js";
|
||||
|
||||
// src/components/ProgressBar.tsx
|
||||
import { effect as _$effect } from "@opentui/solid";
|
||||
import { memo as _$memo } from "@opentui/solid";
|
||||
import { insert as _$insert } from "@opentui/solid";
|
||||
import { createComponent as _$createComponent } from "@opentui/solid";
|
||||
import { setProp as _$setProp } from "@opentui/solid";
|
||||
import { createElement as _$createElement } from "@opentui/solid";
|
||||
import { parseColor, RGBA } from "@opentui/core";
|
||||
import { createMemo, For } from "solid-js";
|
||||
function sampleGradient(stops, t) {
|
||||
if (stops.length === 1)
|
||||
return stops[0];
|
||||
const clamped = Math.min(Math.max(t, 0), 1);
|
||||
const segment = clamped * (stops.length - 1);
|
||||
const i = Math.min(Math.floor(segment), stops.length - 2);
|
||||
const localT = segment - i;
|
||||
const [ar, ag, ab, aa] = parseColor(stops[i]).toInts();
|
||||
const [br, bg, bb, ba] = parseColor(stops[i + 1]).toInts();
|
||||
return RGBA.fromInts(Math.round(ar + (br - ar) * localT), Math.round(ag + (bg - ag) * localT), Math.round(ab + (bb - ab) * localT), Math.round(aa + (ba - aa) * localT));
|
||||
}
|
||||
function ProgressBar2(props) {
|
||||
const width = () => props.width ?? 20;
|
||||
const stops = createMemo(() => Array.isArray(props.color) ? props.color : [props.color ?? theme2.primary]);
|
||||
const filled = createMemo(() => Math.round(width() * Math.min(Math.max(props.value, 0), 100) / 100));
|
||||
const cells = createMemo(() => Array.from({
|
||||
length: width()
|
||||
}, (_, i) => i));
|
||||
return (() => {
|
||||
var _el$ = _$createElement("box");
|
||||
_$setProp(_el$, "flexDirection", "row");
|
||||
_$insert(_el$, _$createComponent(For, {
|
||||
get each() {
|
||||
return cells();
|
||||
},
|
||||
children: (i) => {
|
||||
const isFilled = () => i < filled();
|
||||
const t = width() > 1 ? i / (width() - 1) : 0;
|
||||
return (() => {
|
||||
var _el$2 = _$createElement("text");
|
||||
_$insert(_el$2, (() => {
|
||||
var _c$ = _$memo(() => !!isFilled());
|
||||
return () => _c$() ? props.fillChar ?? "\u2588" : props.trackChar ?? "\u2591";
|
||||
})());
|
||||
_$effect((_$p) => _$setProp(_el$2, "fg", isFilled() ? sampleGradient(stops(), t) : props.trackColor ?? theme2.muted, _$p));
|
||||
return _el$2;
|
||||
})();
|
||||
}
|
||||
}));
|
||||
return _el$;
|
||||
})();
|
||||
}
|
||||
|
||||
export { ProgressBar2 };
|
||||
Vendored
-12
@@ -1,12 +0,0 @@
|
||||
// @bun
|
||||
import {
|
||||
Badge2
|
||||
} from "../chunk-v9cetqzr.js";
|
||||
import"../chunk-p6jvscj3.js";
|
||||
import"../chunk-yyg967q8.js";
|
||||
import"../chunk-njvj322f.js";
|
||||
import"../chunk-6ggk5fg7.js";
|
||||
import"../chunk-xykm10y9.js";
|
||||
export {
|
||||
Badge2 as Badge
|
||||
};
|
||||
Vendored
-12
@@ -1,12 +0,0 @@
|
||||
// @bun
|
||||
import {
|
||||
Box2
|
||||
} from "../chunk-8r4rajaz.js";
|
||||
import"../chunk-p6jvscj3.js";
|
||||
import"../chunk-yyg967q8.js";
|
||||
import"../chunk-njvj322f.js";
|
||||
import"../chunk-6ggk5fg7.js";
|
||||
import"../chunk-xykm10y9.js";
|
||||
export {
|
||||
Box2 as Box
|
||||
};
|
||||
Vendored
-12
@@ -1,12 +0,0 @@
|
||||
// @bun
|
||||
import {
|
||||
HelpBar2
|
||||
} from "../chunk-c1dx8cn3.js";
|
||||
import"../chunk-p6jvscj3.js";
|
||||
import"../chunk-yyg967q8.js";
|
||||
import"../chunk-njvj322f.js";
|
||||
import"../chunk-6ggk5fg7.js";
|
||||
import"../chunk-xykm10y9.js";
|
||||
export {
|
||||
HelpBar2 as HelpBar
|
||||
};
|
||||
Vendored
-14
@@ -1,14 +0,0 @@
|
||||
// @bun
|
||||
import {
|
||||
HelpModal2
|
||||
} from "../chunk-ersd5rrs.js";
|
||||
import"../chunk-t2cszvff.js";
|
||||
import"../chunk-p6jvscj3.js";
|
||||
import"../chunk-yyg967q8.js";
|
||||
import"../chunk-njvj322f.js";
|
||||
import"../chunk-6ggk5fg7.js";
|
||||
import"../chunk-xykm10y9.js";
|
||||
import"../chunk-ee2qaswx.js";
|
||||
export {
|
||||
HelpModal2 as HelpModal
|
||||
};
|
||||
Vendored
-12
@@ -1,12 +0,0 @@
|
||||
// @bun
|
||||
import {
|
||||
KeyValue2
|
||||
} from "../chunk-c4gp0x16.js";
|
||||
import"../chunk-p6jvscj3.js";
|
||||
import"../chunk-yyg967q8.js";
|
||||
import"../chunk-njvj322f.js";
|
||||
import"../chunk-6ggk5fg7.js";
|
||||
import"../chunk-xykm10y9.js";
|
||||
export {
|
||||
KeyValue2 as KeyValue
|
||||
};
|
||||
Vendored
-12
@@ -1,12 +0,0 @@
|
||||
// @bun
|
||||
import {
|
||||
ListRow2
|
||||
} from "../chunk-sh9n4zha.js";
|
||||
import"../chunk-p6jvscj3.js";
|
||||
import"../chunk-yyg967q8.js";
|
||||
import"../chunk-njvj322f.js";
|
||||
import"../chunk-6ggk5fg7.js";
|
||||
import"../chunk-xykm10y9.js";
|
||||
export {
|
||||
ListRow2 as ListRow
|
||||
};
|
||||
Vendored
-12
@@ -1,12 +0,0 @@
|
||||
// @bun
|
||||
import {
|
||||
MinSizeGuard2
|
||||
} from "../chunk-qdyd8q9r.js";
|
||||
import"../chunk-p6jvscj3.js";
|
||||
import"../chunk-yyg967q8.js";
|
||||
import"../chunk-njvj322f.js";
|
||||
import"../chunk-6ggk5fg7.js";
|
||||
import"../chunk-xykm10y9.js";
|
||||
export {
|
||||
MinSizeGuard2 as MinSizeGuard
|
||||
};
|
||||
Vendored
-12
@@ -1,12 +0,0 @@
|
||||
// @bun
|
||||
import {
|
||||
Modal2
|
||||
} from "../chunk-t2cszvff.js";
|
||||
import"../chunk-p6jvscj3.js";
|
||||
import"../chunk-yyg967q8.js";
|
||||
import"../chunk-njvj322f.js";
|
||||
import"../chunk-6ggk5fg7.js";
|
||||
import"../chunk-xykm10y9.js";
|
||||
export {
|
||||
Modal2 as Modal
|
||||
};
|
||||
Vendored
-12
@@ -1,12 +0,0 @@
|
||||
// @bun
|
||||
import {
|
||||
NotificationHost2
|
||||
} from "../chunk-ajb193xj.js";
|
||||
import"../chunk-p6jvscj3.js";
|
||||
import"../chunk-yyg967q8.js";
|
||||
import"../chunk-njvj322f.js";
|
||||
import"../chunk-6ggk5fg7.js";
|
||||
import"../chunk-xykm10y9.js";
|
||||
export {
|
||||
NotificationHost2 as NotificationHost
|
||||
};
|
||||
Vendored
-10
@@ -1,10 +0,0 @@
|
||||
// @bun
|
||||
import {
|
||||
Pane2,
|
||||
usePane2
|
||||
} from "../chunk-vazj633p.js";
|
||||
import"../chunk-59dzyjgd.js";
|
||||
export {
|
||||
Pane2 as Pane,
|
||||
usePane2 as usePane
|
||||
};
|
||||
Vendored
-12
@@ -1,12 +0,0 @@
|
||||
// @bun
|
||||
import {
|
||||
ProgressBar2
|
||||
} from "../chunk-z734n0wr.js";
|
||||
import"../chunk-p6jvscj3.js";
|
||||
import"../chunk-yyg967q8.js";
|
||||
import"../chunk-njvj322f.js";
|
||||
import"../chunk-6ggk5fg7.js";
|
||||
import"../chunk-xykm10y9.js";
|
||||
export {
|
||||
ProgressBar2 as ProgressBar
|
||||
};
|
||||
Vendored
-81
@@ -1,81 +0,0 @@
|
||||
// @bun
|
||||
import"../chunk-njvj322f.js";
|
||||
import"../chunk-6ggk5fg7.js";
|
||||
import"../chunk-yyg967q8.js";
|
||||
import"../chunk-xykm10y9.js";
|
||||
import {
|
||||
theme2
|
||||
} from "../chunk-p6jvscj3.js";
|
||||
import {
|
||||
Spinner2
|
||||
} from "../chunk-kmqqzz9e.js";
|
||||
|
||||
// src/components/SectionHeader.tsx
|
||||
import { createTextNode as _$createTextNode } from "@opentui/solid";
|
||||
import { createComponent as _$createComponent } from "@opentui/solid";
|
||||
import { effect as _$effect } from "@opentui/solid";
|
||||
import { insertNode as _$insertNode } from "@opentui/solid";
|
||||
import { insert as _$insert } from "@opentui/solid";
|
||||
import { memo as _$memo } from "@opentui/solid";
|
||||
import { setProp as _$setProp } from "@opentui/solid";
|
||||
import { createElement as _$createElement } from "@opentui/solid";
|
||||
import { TextAttributes } from "@opentui/core";
|
||||
import { Show } from "solid-js";
|
||||
function SectionHeader(props) {
|
||||
return (() => {
|
||||
var _el$ = _$createElement("box"), _el$2 = _$createElement("box"), _el$3 = _$createElement("text"), _el$4 = _$createElement("box");
|
||||
_$insertNode(_el$, _el$2);
|
||||
_$insertNode(_el$, _el$4);
|
||||
_$setProp(_el$, "flexShrink", 0);
|
||||
_$setProp(_el$, "flexDirection", "row");
|
||||
_$setProp(_el$, "justifyContent", "space-between");
|
||||
_$setProp(_el$, "alignItems", "center");
|
||||
_$setProp(_el$, "height", 1);
|
||||
_$insertNode(_el$2, _el$3);
|
||||
_$setProp(_el$2, "flexGrow", 1);
|
||||
_$setProp(_el$2, "flexShrink", 1);
|
||||
_$setProp(_el$2, "minWidth", 0);
|
||||
_$setProp(_el$2, "marginRight", 1);
|
||||
_$setProp(_el$2, "overflow", "hidden");
|
||||
_$setProp(_el$3, "wrapMode", "none");
|
||||
_$insert(_el$3, () => props.title ?? "");
|
||||
_$setProp(_el$4, "flexShrink", 0);
|
||||
_$insert(_el$4, _$createComponent(Show, {
|
||||
get when() {
|
||||
return !props.loading;
|
||||
},
|
||||
get fallback() {
|
||||
return (() => {
|
||||
var _el$6 = _$createElement("box"), _el$7 = _$createElement("text"), _el$8 = _$createTextNode(` `);
|
||||
_$insertNode(_el$6, _el$7);
|
||||
_$setProp(_el$6, "flexDirection", "row");
|
||||
_$setProp(_el$6, "alignItems", "center");
|
||||
_$insert(_el$6, _$createComponent(Spinner2, {}), _el$7);
|
||||
_$insertNode(_el$7, _el$8);
|
||||
_$insert(_el$7, () => props.loadingLabel ?? "Loading\u2026", null);
|
||||
_$effect((_$p) => _$setProp(_el$7, "fg", theme2.muted, _$p));
|
||||
return _el$6;
|
||||
})();
|
||||
},
|
||||
get children() {
|
||||
var _el$5 = _$createElement("text");
|
||||
_$insert(_el$5, () => props.hint ?? "");
|
||||
_$effect((_$p) => _$setProp(_el$5, "fg", theme2.muted, _$p));
|
||||
return _el$5;
|
||||
}
|
||||
}));
|
||||
_$effect((_p$) => {
|
||||
var _v$ = theme2.muted, _v$2 = TextAttributes.BOLD;
|
||||
_v$ !== _p$.e && (_p$.e = _$setProp(_el$3, "fg", _v$, _p$.e));
|
||||
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$3, "attributes", _v$2, _p$.t));
|
||||
return _p$;
|
||||
}, {
|
||||
e: undefined,
|
||||
t: undefined
|
||||
});
|
||||
return _el$;
|
||||
})();
|
||||
}
|
||||
export {
|
||||
SectionHeader
|
||||
};
|
||||
Vendored
-12
@@ -1,12 +0,0 @@
|
||||
// @bun
|
||||
import {
|
||||
Sidebar2
|
||||
} from "../chunk-rcywynbw.js";
|
||||
import"../chunk-p6jvscj3.js";
|
||||
import"../chunk-yyg967q8.js";
|
||||
import"../chunk-njvj322f.js";
|
||||
import"../chunk-6ggk5fg7.js";
|
||||
import"../chunk-xykm10y9.js";
|
||||
export {
|
||||
Sidebar2 as Sidebar
|
||||
};
|
||||
Vendored
-12
@@ -1,12 +0,0 @@
|
||||
// @bun
|
||||
import {
|
||||
SignalBars2
|
||||
} from "../chunk-tz3vvffs.js";
|
||||
import"../chunk-p6jvscj3.js";
|
||||
import"../chunk-yyg967q8.js";
|
||||
import"../chunk-njvj322f.js";
|
||||
import"../chunk-6ggk5fg7.js";
|
||||
import"../chunk-xykm10y9.js";
|
||||
export {
|
||||
SignalBars2 as SignalBars
|
||||
};
|
||||
Vendored
-12
@@ -1,12 +0,0 @@
|
||||
// @bun
|
||||
import {
|
||||
Spinner2
|
||||
} from "../chunk-kmqqzz9e.js";
|
||||
import"../chunk-p6jvscj3.js";
|
||||
import"../chunk-yyg967q8.js";
|
||||
import"../chunk-njvj322f.js";
|
||||
import"../chunk-6ggk5fg7.js";
|
||||
import"../chunk-xykm10y9.js";
|
||||
export {
|
||||
Spinner2 as Spinner
|
||||
};
|
||||
Vendored
-14
@@ -1,14 +0,0 @@
|
||||
// @bun
|
||||
import {
|
||||
TabbedPanel2
|
||||
} from "../chunk-wtbg0e39.js";
|
||||
import"../chunk-vf2ybhtx.js";
|
||||
import"../chunk-8r4rajaz.js";
|
||||
import"../chunk-p6jvscj3.js";
|
||||
import"../chunk-yyg967q8.js";
|
||||
import"../chunk-njvj322f.js";
|
||||
import"../chunk-6ggk5fg7.js";
|
||||
import"../chunk-xykm10y9.js";
|
||||
export {
|
||||
TabbedPanel2 as TabbedPanel
|
||||
};
|
||||
Vendored
-14
@@ -1,14 +0,0 @@
|
||||
// @bun
|
||||
import {
|
||||
stepTabValue2,
|
||||
Tabs2
|
||||
} from "../chunk-vf2ybhtx.js";
|
||||
import"../chunk-p6jvscj3.js";
|
||||
import"../chunk-yyg967q8.js";
|
||||
import"../chunk-njvj322f.js";
|
||||
import"../chunk-6ggk5fg7.js";
|
||||
import"../chunk-xykm10y9.js";
|
||||
export {
|
||||
Tabs2 as Tabs,
|
||||
stepTabValue2 as stepTabValue
|
||||
};
|
||||
Vendored
-13
@@ -1,13 +0,0 @@
|
||||
// @bun
|
||||
import {
|
||||
Toggle2
|
||||
} from "../chunk-qhp6k176.js";
|
||||
import"../chunk-v9cetqzr.js";
|
||||
import"../chunk-p6jvscj3.js";
|
||||
import"../chunk-yyg967q8.js";
|
||||
import"../chunk-njvj322f.js";
|
||||
import"../chunk-6ggk5fg7.js";
|
||||
import"../chunk-xykm10y9.js";
|
||||
export {
|
||||
Toggle2 as Toggle
|
||||
};
|
||||
Vendored
-18
@@ -1,18 +0,0 @@
|
||||
// @bun
|
||||
import"../chunk-njvj322f.js";
|
||||
import"../chunk-6ggk5fg7.js";
|
||||
import"../chunk-yyg967q8.js";
|
||||
import"../chunk-xykm10y9.js";
|
||||
import"../chunk-p6jvscj3.js";
|
||||
import {
|
||||
HelpBar2
|
||||
} from "../chunk-c1dx8cn3.js";
|
||||
import"../chunk-ee2qaswx.js";
|
||||
import"../chunk-t2cszvff.js";
|
||||
import {
|
||||
HelpModal2
|
||||
} from "../chunk-ersd5rrs.js";
|
||||
export {
|
||||
HelpBar2 as HelpBar,
|
||||
HelpModal2 as HelpModal
|
||||
};
|
||||
Vendored
-69
@@ -1,69 +0,0 @@
|
||||
// @bun
|
||||
import"../chunk-njvj322f.js";
|
||||
import"../chunk-6ggk5fg7.js";
|
||||
import"../chunk-yyg967q8.js";
|
||||
import"../chunk-xykm10y9.js";
|
||||
import"../chunk-p6jvscj3.js";
|
||||
import {
|
||||
Badge2
|
||||
} from "../chunk-v9cetqzr.js";
|
||||
import {
|
||||
Box2
|
||||
} from "../chunk-8r4rajaz.js";
|
||||
import {
|
||||
KeyValue2
|
||||
} from "../chunk-c4gp0x16.js";
|
||||
import {
|
||||
ListRow2
|
||||
} from "../chunk-sh9n4zha.js";
|
||||
import {
|
||||
MinSizeGuard2
|
||||
} from "../chunk-qdyd8q9r.js";
|
||||
import {
|
||||
Modal2
|
||||
} from "../chunk-t2cszvff.js";
|
||||
import {
|
||||
NotificationHost2
|
||||
} from "../chunk-ajb193xj.js";
|
||||
import"../chunk-59dzyjgd.js";
|
||||
import {
|
||||
Pane2,
|
||||
usePane2
|
||||
} from "../chunk-vazj633p.js";
|
||||
import {
|
||||
ProgressBar2
|
||||
} from "../chunk-z734n0wr.js";
|
||||
import {
|
||||
Sidebar2
|
||||
} from "../chunk-rcywynbw.js";
|
||||
import {
|
||||
SignalBars2
|
||||
} from "../chunk-tz3vvffs.js";
|
||||
import {
|
||||
stepTabValue2,
|
||||
Tabs2
|
||||
} from "../chunk-vf2ybhtx.js";
|
||||
import {
|
||||
TabbedPanel2
|
||||
} from "../chunk-wtbg0e39.js";
|
||||
import {
|
||||
Toggle2
|
||||
} from "../chunk-qhp6k176.js";
|
||||
export {
|
||||
Badge2 as Badge,
|
||||
Box2 as Box,
|
||||
KeyValue2 as KeyValue,
|
||||
ListRow2 as ListRow,
|
||||
MinSizeGuard2 as MinSizeGuard,
|
||||
Modal2 as Modal,
|
||||
NotificationHost2 as NotificationHost,
|
||||
Pane2 as Pane,
|
||||
ProgressBar2 as ProgressBar,
|
||||
Sidebar2 as Sidebar,
|
||||
SignalBars2 as SignalBars,
|
||||
TabbedPanel2 as TabbedPanel,
|
||||
Tabs2 as Tabs,
|
||||
Toggle2 as Toggle,
|
||||
stepTabValue2 as stepTabValue,
|
||||
usePane2 as usePane
|
||||
};
|
||||
Vendored
-17
@@ -1,17 +0,0 @@
|
||||
// @bun
|
||||
import {
|
||||
COLOR_KEYS2,
|
||||
OPENTUI_BORDER_STYLES2,
|
||||
ThemeConfigSchema2,
|
||||
layerConfigs2,
|
||||
resolveConfig2,
|
||||
normalizeColor2
|
||||
} from "./chunk-njvj322f.js";
|
||||
export {
|
||||
COLOR_KEYS2 as COLOR_KEYS,
|
||||
OPENTUI_BORDER_STYLES2 as OPENTUI_BORDER_STYLES,
|
||||
ThemeConfigSchema2 as ThemeConfigSchema,
|
||||
layerConfigs2 as layerConfigs,
|
||||
normalizeColor2 as normalizeColor,
|
||||
resolveConfig2 as resolveConfig
|
||||
};
|
||||
Vendored
-9
@@ -1,9 +0,0 @@
|
||||
// @bun
|
||||
import {
|
||||
isInside2,
|
||||
useFocusWithin2
|
||||
} from "../chunk-59dzyjgd.js";
|
||||
export {
|
||||
isInside2 as isInside,
|
||||
useFocusWithin2 as useFocusWithin
|
||||
};
|
||||
Vendored
-9
@@ -1,9 +0,0 @@
|
||||
// @bun
|
||||
import {
|
||||
helpOpen2,
|
||||
toggleHelp2
|
||||
} from "../chunk-ee2qaswx.js";
|
||||
export {
|
||||
helpOpen2 as helpOpen,
|
||||
toggleHelp2 as toggleHelp
|
||||
};
|
||||
Vendored
-7
@@ -1,7 +0,0 @@
|
||||
// @bun
|
||||
import {
|
||||
createSimpleContext2
|
||||
} from "../chunk-jza406qg.js";
|
||||
export {
|
||||
createSimpleContext2 as createSimpleContext
|
||||
};
|
||||
Vendored
-27
@@ -1,27 +0,0 @@
|
||||
// @bun
|
||||
import {
|
||||
createSimpleContext2
|
||||
} from "../chunk-jza406qg.js";
|
||||
import {
|
||||
notifications2,
|
||||
notify2,
|
||||
dismiss2
|
||||
} from "../chunk-6ggk5fg7.js";
|
||||
import {
|
||||
helpOpen2,
|
||||
toggleHelp2
|
||||
} from "../chunk-ee2qaswx.js";
|
||||
import {
|
||||
isInside2,
|
||||
useFocusWithin2
|
||||
} from "../chunk-59dzyjgd.js";
|
||||
export {
|
||||
createSimpleContext2 as createSimpleContext,
|
||||
dismiss2 as dismiss,
|
||||
helpOpen2 as helpOpen,
|
||||
isInside2 as isInside,
|
||||
notifications2 as notifications,
|
||||
notify2 as notify,
|
||||
toggleHelp2 as toggleHelp,
|
||||
useFocusWithin2 as useFocusWithin
|
||||
};
|
||||
Vendored
-11
@@ -1,11 +0,0 @@
|
||||
// @bun
|
||||
import {
|
||||
notifications2,
|
||||
notify2,
|
||||
dismiss2
|
||||
} from "../chunk-6ggk5fg7.js";
|
||||
export {
|
||||
dismiss2 as dismiss,
|
||||
notifications2 as notifications,
|
||||
notify2 as notify
|
||||
};
|
||||
Vendored
-35
@@ -1,35 +0,0 @@
|
||||
# ilovetui default config
|
||||
# Copy to ~/.config/ilovetui/config.yaml and edit to customize.
|
||||
|
||||
# Whether components may use Nerd Font glyphs (requires a patched font).
|
||||
nerd_fonts: true
|
||||
|
||||
# Whether the terminal font can render emoji glyphs.
|
||||
emoji_fonts: true
|
||||
|
||||
# Disable spinner animation and other motion across all ilovetui-based TUIs.
|
||||
reduced_motion: false
|
||||
|
||||
# Whether components may register mouse handlers (e.g. click-to-dismiss).
|
||||
mouse: true
|
||||
|
||||
# Border style used by panels across all ilovetui-based TUIs.
|
||||
border: rounded
|
||||
|
||||
colors:
|
||||
base00: "#110F12" # Background
|
||||
base01: "#1C1920" # Lighter Background / Status Bars
|
||||
base02: "#1D1A26" # Selection Background
|
||||
base03: "#514D63" # Comments / Invisibles
|
||||
base04: "#8E8AA0" # Dark Foreground / Status Bars
|
||||
base05: "#C2BED6" # Default Foreground
|
||||
base06: "#D8D5EA" # Light Foreground
|
||||
base07: "#EAE7F7" # Light Background
|
||||
base08: "#E07080" # Variables / Errors / Diff Deleted
|
||||
base09: "#D49070" # Integers / Constants / Booleans
|
||||
base0a: "#C4B060" # Classes / Warnings / Search Background
|
||||
base0b: "#80B880" # Strings / Success / Diff Inserted
|
||||
base0c: "#70B8C0" # Support / Regex / Escape Characters
|
||||
base0d: "#9E97F8" # Functions / Methods / Headings / Accent
|
||||
base0e: "#C090E8" # Keywords / Storage / Diff Changed
|
||||
base0f: "#D080A0" # Embedded / Misc
|
||||
Vendored
-23
@@ -1,23 +0,0 @@
|
||||
// @bun
|
||||
import {
|
||||
theme2,
|
||||
presets2,
|
||||
configureTheme2,
|
||||
icon2
|
||||
} from "./chunk-p6jvscj3.js";
|
||||
import"./chunk-yyg967q8.js";
|
||||
import {
|
||||
ThemeConfigSchema2
|
||||
} from "./chunk-njvj322f.js";
|
||||
import"./chunk-6ggk5fg7.js";
|
||||
import {
|
||||
configPath2
|
||||
} from "./chunk-xykm10y9.js";
|
||||
export {
|
||||
ThemeConfigSchema2 as ThemeConfigSchema,
|
||||
configPath2 as configPath,
|
||||
configureTheme2 as configureTheme,
|
||||
icon2 as icon,
|
||||
presets2 as presets,
|
||||
theme2 as theme
|
||||
};
|
||||
Vendored
-27
@@ -1,27 +0,0 @@
|
||||
// @bun
|
||||
// src/keymap/provider.tsx
|
||||
import { createComponent as _$createComponent } from "@opentui/solid";
|
||||
import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui";
|
||||
import { registerCommaBindings } from "@opentui/keymap/addons";
|
||||
import { KeymapProvider as BaseKeymapProvider } from "@opentui/keymap/solid";
|
||||
import { useRenderer } from "@opentui/solid";
|
||||
import { onCleanup } from "solid-js";
|
||||
import { useKeymap, useBindings, useKeymapSelector, reactiveMatcherFromSignal } from "@opentui/keymap/solid";
|
||||
function KeymapProvider(props) {
|
||||
const renderer = useRenderer();
|
||||
const keymap = createDefaultOpenTuiKeymap(renderer);
|
||||
onCleanup(registerCommaBindings(keymap));
|
||||
return _$createComponent(BaseKeymapProvider, {
|
||||
keymap,
|
||||
get children() {
|
||||
return props.children;
|
||||
}
|
||||
});
|
||||
}
|
||||
export {
|
||||
KeymapProvider,
|
||||
reactiveMatcherFromSignal,
|
||||
useBindings,
|
||||
useKeymap,
|
||||
useKeymapSelector
|
||||
};
|
||||
Vendored
-7
@@ -1,7 +0,0 @@
|
||||
// @bun
|
||||
import {
|
||||
buildPresets2
|
||||
} from "./chunk-yyg967q8.js";
|
||||
export {
|
||||
buildPresets2 as buildPresets
|
||||
};
|
||||
Vendored
-117
@@ -1,117 +0,0 @@
|
||||
// @bun
|
||||
import {
|
||||
notify2
|
||||
} from "./chunk-6ggk5fg7.js";
|
||||
|
||||
// src/project-config.ts
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
import { homedir } from "os";
|
||||
import { join } from "path";
|
||||
import { parse as parseYAML } from "yaml";
|
||||
import { z } from "zod";
|
||||
function userConfigPath(projectName) {
|
||||
const configHome = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
||||
return join(configHome, projectName, "config.yaml");
|
||||
}
|
||||
function readYaml(path) {
|
||||
return parseYAML(readFileSync(path, "utf-8"));
|
||||
}
|
||||
function warn(projectName, path, message) {
|
||||
const text = `${path}: ${message}`;
|
||||
console.error(`[${projectName} config] ${text}`);
|
||||
notify2(text, { kind: "warning", duration: 0 });
|
||||
}
|
||||
function loadProjectConfig(options) {
|
||||
const { projectName, defaultConfigPath, defaultSchema, userSchema } = options;
|
||||
let rawDefaults;
|
||||
try {
|
||||
rawDefaults = readYaml(defaultConfigPath);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to read ${defaultConfigPath}: ${error instanceof Error ? error.message : error}`);
|
||||
}
|
||||
const defaultsResult = defaultSchema.safeParse(rawDefaults);
|
||||
if (!defaultsResult.success) {
|
||||
throw new Error(`${defaultConfigPath} is invalid:
|
||||
${z.prettifyError(defaultsResult.error)}`);
|
||||
}
|
||||
const path = userConfigPath(projectName);
|
||||
const user = (() => {
|
||||
if (!existsSync(path))
|
||||
return {};
|
||||
let raw;
|
||||
try {
|
||||
raw = readYaml(path);
|
||||
} catch (error) {
|
||||
warn(projectName, path, error instanceof Error ? error.message : String(error));
|
||||
return {};
|
||||
}
|
||||
const result = userSchema.safeParse(raw ?? {});
|
||||
if (!result.success) {
|
||||
warn(projectName, path, z.prettifyError(result.error));
|
||||
return {};
|
||||
}
|
||||
return result.data;
|
||||
})();
|
||||
return { defaults: defaultsResult.data, user };
|
||||
}
|
||||
function isPlainObject(value) {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
function deepMerge(base, over) {
|
||||
if (!isPlainObject(base) || !isPlainObject(over))
|
||||
return over === undefined ? base : over;
|
||||
const out = { ...base };
|
||||
for (const [key, value] of Object.entries(over))
|
||||
out[key] = deepMerge(base[key], value);
|
||||
return out;
|
||||
}
|
||||
function loadMergedProjectConfig(options) {
|
||||
const { projectName, defaultConfigPath, schema } = options;
|
||||
let rawDefaults;
|
||||
try {
|
||||
rawDefaults = readYaml(defaultConfigPath);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to read ${defaultConfigPath}: ${error instanceof Error ? error.message : error}`);
|
||||
}
|
||||
const defaults = schema.safeParse(rawDefaults);
|
||||
if (!defaults.success) {
|
||||
throw new Error(`${defaultConfigPath} is invalid:
|
||||
${z.prettifyError(defaults.error)}`);
|
||||
}
|
||||
const path = userConfigPath(projectName);
|
||||
if (!existsSync(path))
|
||||
return defaults.data;
|
||||
let rawUser;
|
||||
try {
|
||||
rawUser = readYaml(path);
|
||||
} catch (error) {
|
||||
warn(projectName, path, error instanceof Error ? error.message : String(error));
|
||||
return defaults.data;
|
||||
}
|
||||
const merged = schema.safeParse(deepMerge(rawDefaults, rawUser ?? {}));
|
||||
if (!merged.success) {
|
||||
warn(projectName, path, z.prettifyError(merged.error));
|
||||
return defaults.data;
|
||||
}
|
||||
return merged.data;
|
||||
}
|
||||
function keybindsSchema(names, required) {
|
||||
const shape = {};
|
||||
for (const name of names) {
|
||||
shape[name] = required ? z.string().min(1) : z.string().min(1).optional();
|
||||
}
|
||||
return z.object(shape).strict();
|
||||
}
|
||||
function mergeKeybinds(names, defaults, user) {
|
||||
const merged = {};
|
||||
for (const name of names) {
|
||||
merged[name] = user?.[name] ?? defaults[name];
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
export {
|
||||
keybindsSchema,
|
||||
loadMergedProjectConfig,
|
||||
loadProjectConfig,
|
||||
mergeKeybinds
|
||||
};
|
||||
Vendored
-48
@@ -1,48 +0,0 @@
|
||||
// @bun
|
||||
import"./chunk-njvj322f.js";
|
||||
import"./chunk-6ggk5fg7.js";
|
||||
import"./chunk-yyg967q8.js";
|
||||
import"./chunk-xykm10y9.js";
|
||||
import {
|
||||
theme2,
|
||||
presets2
|
||||
} from "./chunk-p6jvscj3.js";
|
||||
|
||||
// src/solid.ts
|
||||
import { extend } from "@opentui/solid";
|
||||
import {
|
||||
ASCIIFontRenderable,
|
||||
BoxRenderable,
|
||||
InputRenderable,
|
||||
SelectRenderable,
|
||||
SliderRenderable,
|
||||
TabSelectRenderable,
|
||||
TextareaRenderable,
|
||||
TextRenderable
|
||||
} from "@opentui/core";
|
||||
function withThemeDefaults(Ctor, defaults, after) {
|
||||
class Themed extends Ctor {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
Object.assign(this, defaults());
|
||||
after?.(this);
|
||||
}
|
||||
}
|
||||
return Themed;
|
||||
}
|
||||
var boxDefaults = () => {
|
||||
const { borderStyle, borderColor, focusedBorderColor } = presets2.box;
|
||||
return { borderStyle, borderColor, focusedBorderColor };
|
||||
};
|
||||
extend({
|
||||
box: withThemeDefaults(BoxRenderable, boxDefaults, (instance) => {
|
||||
instance.border = false;
|
||||
}),
|
||||
select: withThemeDefaults(SelectRenderable, () => presets2.select),
|
||||
tab_select: withThemeDefaults(TabSelectRenderable, () => presets2.tabSelect),
|
||||
input: withThemeDefaults(InputRenderable, () => presets2.input),
|
||||
textarea: withThemeDefaults(TextareaRenderable, () => presets2.textarea),
|
||||
slider: withThemeDefaults(SliderRenderable, () => presets2.slider),
|
||||
text: withThemeDefaults(TextRenderable, () => ({ fg: theme2.text })),
|
||||
ascii_font: withThemeDefaults(ASCIIFontRenderable, () => ({ color: theme2.primary }))
|
||||
});
|
||||
Vendored
-7
@@ -1,7 +0,0 @@
|
||||
// @bun
|
||||
import {
|
||||
configPath2
|
||||
} from "./chunk-xykm10y9.js";
|
||||
export {
|
||||
configPath2 as configPath
|
||||
};
|
||||
@@ -0,0 +1,140 @@
|
||||
# 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 this package
|
||||
otherwise mirrors closely: same stack of panels triggered from anywhere via an exported `tea.Msg`
|
||||
(`ShowMsg`/`Show`) rather than a direct reference to the `Model` that ends up rendering it, same
|
||||
composite-over-an-already-rendered-string `Render`, no assumption about how the host builds that
|
||||
string.
|
||||
|
||||
## Quick start
|
||||
|
||||
```go
|
||||
import (
|
||||
"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.KeyPressMsg:
|
||||
if msg.String() == "l" {
|
||||
return m, drawer.Show("Nav", drawer.Text("Home\nProjects\nSettings"))
|
||||
}
|
||||
if msg.String() == "esc" && 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 := renderYourUI(m.width, m.height)
|
||||
view := tea.NewView(m.d.Render(background))
|
||||
view.AltScreen = true
|
||||
return view
|
||||
}
|
||||
```
|
||||
|
||||
Any component in the same bubbletea program can trigger a drawer via `drawer.Show`, without
|
||||
holding a reference to the `drawer.Model` that will actually render it - that `Model` just needs
|
||||
to see every `tea.Msg` the program produces (i.e. get its `Update` called from the top-level
|
||||
`Update`), same as any other child model.
|
||||
|
||||
## Content is a model
|
||||
|
||||
A drawer's body is a `tea.Model`, not a string. While a drawer is on top of the stack it gets
|
||||
every message the `drawer.Model` receives, its `Init` runs when it opens, and its commands come
|
||||
back out - so it can hold a file list, a filter form, or a picker that reports its choice with a
|
||||
`tea.Msg` of its own, which the component that opened it listens for:
|
||||
|
||||
```go
|
||||
type pickedMsg struct{ file string }
|
||||
|
||||
// somewhere else
|
||||
return m, drawer.Show("Files", newFileList(dir), drawer.WithSide(drawer.Right))
|
||||
```
|
||||
|
||||
Only the topmost drawer is updated: everything beneath it is dimmed and frozen until the drawers
|
||||
above it close.
|
||||
|
||||
For a drawer with nothing to interact with, `drawer.Text` wraps a plain string:
|
||||
|
||||
```go
|
||||
return m, drawer.Show("Nav", drawer.Text("Home\nProjects\nSettings"))
|
||||
```
|
||||
|
||||
The box shrinks to fit whatever the content draws (unless `WithWidth` fixes it), so a content
|
||||
that wants a specific size sets it on itself - the drawer only ever sees the rendered result.
|
||||
|
||||
## Side and width
|
||||
|
||||
```go
|
||||
return m, drawer.Show("Nav", drawer.Text("Home\nProjects\nSettings"),
|
||||
drawer.WithSide(drawer.Right), drawer.WithWidth(24))
|
||||
```
|
||||
|
||||
`WithSide` anchors the drawer to `drawer.Left` (the default) or `drawer.Right`. `WithWidth` fixes
|
||||
the drawer's total width instead of shrinking to fit its content, still capped by whatever
|
||||
actually fits the background - same unit as the `Model`-level `WithMaxWidth`. Either way, the
|
||||
drawer always spans the background's full height, flush top to bottom.
|
||||
|
||||
## Showing and closing
|
||||
|
||||
```go
|
||||
return m, drawer.Show("Files", newFileList(dir), drawer.WithSide(drawer.Right))
|
||||
|
||||
return m, drawer.Close() // close the topmost drawer
|
||||
```
|
||||
|
||||
The stack is a plain LIFO, with no identity: a drawer is closed by being on top, never by being
|
||||
named. There is nothing to tag a drawer with, and nothing that can target one in the middle of the
|
||||
stack - the topmost is both the only one that receives messages and the only one `Close` can
|
||||
reach, so the two rules never disagree.
|
||||
|
||||
- `drawer.Open()` reports whether at least one drawer is currently shown - handy for a host that
|
||||
wants to route key presses to the drawer instead of its normal UI while one is open. Note that a
|
||||
host doing this also makes it impossible for a key to open a second drawer, which is what keeps
|
||||
the stack shallow without any bookkeeping.
|
||||
- A content model closes its own drawer by returning `drawer.Close()`, since it only ever runs
|
||||
while it is the topmost one.
|
||||
|
||||
## Stacking
|
||||
|
||||
Drawers stack: showing a second one while the first is still open pushes it on top, dimming both
|
||||
the background and the first drawer to the same flat color, same as `modal`. Opening a left drawer
|
||||
and a right drawer at once still stacks - if you want both visible at full color simultaneously,
|
||||
render them as two ordinary panels via `layout` instead; this package is for transient
|
||||
sidebars, not permanent chrome.
|
||||
|
||||
## Styling
|
||||
|
||||
```go
|
||||
d := drawer.New(drawer.WithMaxWidth(30), drawer.WithStyles(myStyles))
|
||||
|
||||
return m, drawer.Show("Nav", drawer.Text("content"), drawer.WithDrawerStyle(oneOffStyles))
|
||||
```
|
||||
|
||||
`WithMaxWidth` caps how wide a drawer can grow (border and padding included) before wrapping; a
|
||||
drawer narrower than the cap shrinks to fit its content instead of padding out to it, unless shown
|
||||
with `WithWidth`. A drawer can also never overflow past the edge of whatever background it's
|
||||
rendered on. `WithStyles` sets the default look for every drawer shown by this `Model`;
|
||||
`WithDrawerStyle` (a `Show` option) overrides it for one drawer alone. `DefaultStyles()` builds
|
||||
from `style.S`, same palette roles as `modal.DefaultStyles()`.
|
||||
|
||||
## Examples
|
||||
|
||||
- `examples/drawer` - a left nav drawer and a right inspector drawer, either one at a time.
|
||||
@@ -0,0 +1,109 @@
|
||||
package drawer
|
||||
|
||||
import tea "charm.land/bubbletea/v2"
|
||||
|
||||
// Side is which edge of the background a Drawer is anchored to.
|
||||
type Side int
|
||||
|
||||
const (
|
||||
// Left anchors the drawer to the left edge (the zero value, so a Drawer
|
||||
// built without WithSide opens on the left).
|
||||
Left Side = iota
|
||||
// Right anchors the drawer to the right edge.
|
||||
Right
|
||||
)
|
||||
|
||||
// Drawer is one sidebar panel on the stack.
|
||||
type Drawer struct {
|
||||
Title string
|
||||
// Content is the drawer's body: a full model, updated and rendered by
|
||||
// the drawer.Model while it's on top of the stack. Wrap a plain string
|
||||
// with Text for a drawer with nothing to interact with.
|
||||
Content tea.Model
|
||||
Side Side
|
||||
// Width, if non-zero, fixes the drawer's total width (border and
|
||||
// padding included, same unit as the Model's WithMaxWidth) instead of
|
||||
// shrinking to fit what Content draws, and Title. Still capped by whatever actually
|
||||
// fits the background.
|
||||
Width int
|
||||
// Style, if non-nil, overrides the Model's default Styles for this
|
||||
// drawer alone.
|
||||
Style *Styles
|
||||
}
|
||||
|
||||
// DrawerOption configures a Drawer built by Show.
|
||||
type DrawerOption func(*Drawer)
|
||||
|
||||
// WithSide anchors the drawer to the given Side (default Left).
|
||||
func WithSide(s Side) DrawerOption {
|
||||
return func(d *Drawer) { d.Side = s }
|
||||
}
|
||||
|
||||
// WithWidth fixes the drawer's total width instead of shrinking to fit its
|
||||
// content, still capped by whatever actually fits the background.
|
||||
func WithWidth(w int) DrawerOption {
|
||||
return func(d *Drawer) { d.Width = w }
|
||||
}
|
||||
|
||||
// WithDrawerStyle overrides the Model's default Styles for this drawer
|
||||
// alone, for a one-off custom look instead of the shared theme.
|
||||
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
|
||||
}
|
||||
|
||||
// ShowMsg tells a drawer.Model to display Drawer, pushing it on top of the
|
||||
// stack. Any component in the same bubbletea program can trigger one via
|
||||
// Show, without holding a reference to the drawer.Model that will actually
|
||||
// render it - that Model just needs to see every tea.Msg the program
|
||||
// produces, same as any other child model.
|
||||
type ShowMsg struct{ Drawer Drawer }
|
||||
|
||||
// Show returns a tea.Cmd that opens a new drawer on top of the stack,
|
||||
// anchored to the left edge by default. content is a model, so a drawer can
|
||||
// hold anything a pane can - a file list, a filter form, a picker that
|
||||
// reports its choice with its own tea.Msg:
|
||||
//
|
||||
// return m, drawer.Show("Files", newFileList(dir), drawer.WithSide(drawer.Right))
|
||||
// return m, drawer.Show("Nav", drawer.Text("Home\nProjects"))
|
||||
//
|
||||
// The content's Init runs when the drawer opens, and it receives every
|
||||
// message while it's the topmost drawer (see Model.Update).
|
||||
func Show(title string, content tea.Model, opts ...DrawerOption) tea.Cmd {
|
||||
d := newDrawer(title, content, opts...)
|
||||
return func() tea.Msg { return ShowMsg{Drawer: d} }
|
||||
}
|
||||
|
||||
// DismissMsg closes the topmost drawer. The stack is a plain LIFO: a drawer
|
||||
// is closed by being on top, never by being named.
|
||||
type DismissMsg struct{}
|
||||
|
||||
// Close returns a tea.Cmd that closes the topmost drawer - which is also the
|
||||
// only one that can act (see Model.Update), so a content model closes itself
|
||||
// by returning it:
|
||||
//
|
||||
// return c, drawer.Close()
|
||||
func Close() tea.Cmd {
|
||||
return func() tea.Msg { return DismissMsg{} }
|
||||
}
|
||||
|
||||
// text is a model wrapping a fixed string: a drawer body with nothing to
|
||||
// update.
|
||||
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)) }
|
||||
|
||||
// Text wraps a plain string as drawer content, for the common drawer that has
|
||||
// nothing to interact with:
|
||||
//
|
||||
// drawer.Show("Nav", drawer.Text("Home\nProjects\nSettings"))
|
||||
func Text(s string) tea.Model { return text(s) }
|
||||
@@ -0,0 +1,114 @@
|
||||
// Package drawer renders 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 github.com/anotherhadi/ilovetui/modal,
|
||||
// which this package otherwise mirrors closely: same stack of panels
|
||||
// triggered from anywhere via an exported tea.Msg (see ShowMsg/Show)
|
||||
// rather than a direct reference to the Model that ends up rendering it,
|
||||
// same composite-over-an-already-rendered-string Render, same absence of
|
||||
// any assumption about how the host builds that string.
|
||||
//
|
||||
// A drawer's content is a model, not a string: it is updated while it's on
|
||||
// top of the stack, so it can hold anything a pane can - a file list, a
|
||||
// filter form, a picker reporting its choice back with its own tea.Msg. See
|
||||
// Show and Text.
|
||||
package drawer
|
||||
|
||||
import tea "charm.land/bubbletea/v2"
|
||||
|
||||
// Model holds the currently open drawers (a stack: the most recently shown
|
||||
// is drawn on top, everything beneath it - the background and any earlier
|
||||
// drawer - dimmed, see Render) and the rendering config (max width, styles)
|
||||
// they share. Build one with New.
|
||||
type Model struct {
|
||||
drawers []Drawer
|
||||
nextID int
|
||||
maxWidth int
|
||||
styles Styles
|
||||
}
|
||||
|
||||
// Option configures a Model at construction. See WithMaxWidth, WithStyles.
|
||||
type Option func(*Model)
|
||||
|
||||
// WithMaxWidth caps a drawer's total width (border and padding included). A
|
||||
// drawer shown without WithWidth shrinks to fit its content instead of
|
||||
// padding out to the cap; a drawer shown with WithWidth uses that width
|
||||
// instead, still capped by this. 0 means only the background's own width
|
||||
// caps it.
|
||||
func WithMaxWidth(w int) Option {
|
||||
return func(m *Model) { m.maxWidth = w }
|
||||
}
|
||||
|
||||
// WithStyles overrides the default styles (see DefaultStyles).
|
||||
func WithStyles(s Styles) Option {
|
||||
return func(m *Model) { m.styles = s }
|
||||
}
|
||||
|
||||
// New builds a Model. Defaults: a 30-column max width, DefaultStyles.
|
||||
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)
|
||||
}
|
||||
|
||||
// updateTop forwards msg to the topmost drawer's content - the only one the
|
||||
// user can interact with, everything beneath it being dimmed (see Render). A
|
||||
// drawer deeper in the stack is frozen until the ones above it close.
|
||||
//
|
||||
// This is what lets drawer content be a real model: it gets the key presses,
|
||||
// the ticks and the results of its own commands, and can report back to the
|
||||
// rest of the program with a tea.Msg of its own.
|
||||
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
|
||||
}
|
||||
|
||||
// Open reports whether at least one drawer is currently shown - handy for a
|
||||
// host that wants to route key presses to the drawer (e.g. esc to dismiss)
|
||||
// instead of its normal UI while one is open.
|
||||
func (m Model) Open() bool { return len(m.drawers) > 0 }
|
||||
|
||||
// show pushes a drawer on top of the stack and returns its content's Init - a
|
||||
// drawer's body starts the same way any other model does.
|
||||
func (m Model) show(d Drawer) (Model, tea.Cmd) {
|
||||
m.drawers = append(m.drawers, d)
|
||||
return m, initContent(d)
|
||||
}
|
||||
|
||||
// initContent is d's content's Init, or nil for a drawer without content.
|
||||
func initContent(d Drawer) tea.Cmd {
|
||||
if d.Content == nil {
|
||||
return nil
|
||||
}
|
||||
return d.Content.Init()
|
||||
}
|
||||
|
||||
// pop closes the topmost drawer (see Close).
|
||||
func (m Model) pop() Model {
|
||||
if len(m.drawers) == 0 {
|
||||
return m
|
||||
}
|
||||
m.drawers = m.drawers[:len(m.drawers)-1]
|
||||
return m
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package drawer
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"strings"
|
||||
|
||||
"charm.land/lipgloss/v2"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
|
||||
"github.com/anotherhadi/ilovetui/style"
|
||||
)
|
||||
|
||||
// Render draws every open drawer (see Model.Update/Show) on top of
|
||||
// background (already rendered, e.g. layout.Model.View() or any other
|
||||
// component's View()) and returns the result. Each drawer in the stack
|
||||
// first flattens whatever came before it - background plus any earlier
|
||||
// drawer - to a single flat DimColor (see dim), then draws its own
|
||||
// full-height box flush against its Side on top, so nesting a second
|
||||
// drawer on top of a first dims the first one too. background is returned
|
||||
// unchanged whenever there's nothing to draw.
|
||||
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
|
||||
}
|
||||
|
||||
// View is a convenience for a pane whose sole purpose is showing drawers
|
||||
// (e.g. a dedicated layout.Leaf): it draws the stack over a blank
|
||||
// width x height area instead of an existing background.
|
||||
func (m Model) View(width, height int) string {
|
||||
return m.Render(blank(width, height))
|
||||
}
|
||||
|
||||
// renderOne dims background flat and draws d's box, spanning its full
|
||||
// height, flush against d.Side.
|
||||
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()
|
||||
}
|
||||
|
||||
// dim flattens s to a single flat color: every existing style (colors,
|
||||
// bold, underline...) is stripped, then every character - including
|
||||
// whitespace, so highlighted/selected backgrounds vanish too - is
|
||||
// repainted in c. Applying a Foreground style to a multi-line string styles
|
||||
// each line independently (see lipgloss.Style.Render), so this keeps s's
|
||||
// line structure intact.
|
||||
func dim(s string, c color.Color) string {
|
||||
return lipgloss.NewStyle().Foreground(c).Render(ansi.Strip(s))
|
||||
}
|
||||
|
||||
// renderBox draws d as a bordered, title-embedded box (style.RenderWithTitle)
|
||||
// spanning the background's full height, capped by the Model's configured
|
||||
// max width and by whatever actually fits inside a bgW-wide background.
|
||||
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 // border (2) + Padding(0, 1) (2)
|
||||
|
||||
return style.RenderWithTitle(s.Border, s.Title.Render(d.Title), content, boxWidth, bgH)
|
||||
}
|
||||
|
||||
// contentView is the drawer body's rendered string, or "" for a drawer
|
||||
// without content. The box shrinks to fit whatever the content model draws,
|
||||
// so a content that wants a specific size sets it on itself - the drawer only
|
||||
// ever sees the result.
|
||||
func contentView(d Drawer) string {
|
||||
if d.Content == nil {
|
||||
return ""
|
||||
}
|
||||
return d.Content.View().Content
|
||||
}
|
||||
|
||||
// contentWidth is the drawer's inner (border/padding excluded) width, given
|
||||
// the resolved total-width cap maxWidth (see renderBox): maxWidth-4 if
|
||||
// d.Width is set (a fixed total width), otherwise its natural size (long
|
||||
// enough for the widest line of title/content), capped at maxWidth-4.
|
||||
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)
|
||||
}
|
||||
|
||||
// naturalWidth is the width of content's widest line.
|
||||
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
|
||||
}
|
||||
|
||||
// effectiveMax resolves the cap actually used along the width axis:
|
||||
// configured (0 = unlimited) narrowed down to fits, whatever actually fits
|
||||
// the background - a drawer can never overflow past the edge of the
|
||||
// background, or the terminal, when the background is a full-screen View(),
|
||||
// regardless of how WithMaxWidth/WithWidth was set.
|
||||
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")
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
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")
|
||||
}
|
||||
// The border's top-left corner glyph should be the very first rune.
|
||||
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 { // WithWidth is the total box width
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package drawer
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
|
||||
"charm.land/lipgloss/v2"
|
||||
|
||||
"github.com/anotherhadi/ilovetui/style"
|
||||
)
|
||||
|
||||
// Styles is the set of lipgloss styles, plus the dim color, used to render a
|
||||
// drawer and the background behind it. Border carries the box's border
|
||||
// (shape + color, no size), Title and Content color the two pieces of text
|
||||
// drawn inside it, DimColor is the single flat color every character of the
|
||||
// background gets overwritten with while the drawer is open.
|
||||
type Styles struct {
|
||||
Border lipgloss.Style
|
||||
Title lipgloss.Style
|
||||
Content lipgloss.Style
|
||||
DimColor color.Color
|
||||
}
|
||||
|
||||
// DefaultStyles builds a Styles from style.S: the box borrows
|
||||
// PanelFocused's border (the drawer is what has focus while it's open).
|
||||
// DimColor reuses Subtle - the base16 "comments/invisibles" role, already
|
||||
// used across this repo for de-emphasized text (borders, placeholders,
|
||||
// separators, see bubbles/*.go and modal.DefaultStyles) - darker than
|
||||
// Muted, which reads too bright once it's covering an entire screen instead
|
||||
// of a single blurred field.
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
|
||||
"github.com/anotherhadi/ilovetui/drawer"
|
||||
"github.com/anotherhadi/ilovetui/style"
|
||||
)
|
||||
|
||||
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
|
||||
return m, nil
|
||||
|
||||
case tea.KeyPressMsg:
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
|
||||
case "l":
|
||||
return m, drawer.Show("Nav", drawer.Text("Home\nProjects\nSettings"),
|
||||
drawer.WithSide(drawer.Left), drawer.WithWidth(20))
|
||||
|
||||
case "r":
|
||||
return m, drawer.Show("Inspector", drawer.Text("id: 42\nstatus: ok"),
|
||||
drawer.WithSide(drawer.Right), drawer.WithWidth(20))
|
||||
|
||||
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 {
|
||||
title := lipgloss.NewStyle().Bold(true).Foreground(style.S.Primary).Render("My App")
|
||||
body := lipgloss.NewStyle().Foreground(style.S.Text).Render(
|
||||
"Some regular content, styled with theme colors,\nso you can see it turn flat gray behind the drawer.")
|
||||
help := lipgloss.NewStyle().Foreground(style.S.Subtle).Render(
|
||||
"l: open left drawer r: open right drawer esc: close the top one q: quit")
|
||||
|
||||
background := lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center,
|
||||
lipgloss.JoinVertical(lipgloss.Center, title, "", body, "", help))
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"charm.land/bubbles/v2/key"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
|
||||
"github.com/anotherhadi/ilovetui/drawer"
|
||||
"github.com/anotherhadi/ilovetui/examples/fullapp/metrics"
|
||||
"github.com/anotherhadi/ilovetui/examples/fullapp/overview"
|
||||
"github.com/anotherhadi/ilovetui/examples/fullapp/settings"
|
||||
"github.com/anotherhadi/ilovetui/examples/fullapp/sidebar"
|
||||
"github.com/anotherhadi/ilovetui/helpbar"
|
||||
"github.com/anotherhadi/ilovetui/modal"
|
||||
"github.com/anotherhadi/ilovetui/notification"
|
||||
"github.com/anotherhadi/ilovetui/style"
|
||||
)
|
||||
|
||||
var pages = []struct {
|
||||
item sidebar.NavItem
|
||||
new func() tea.Model
|
||||
}{
|
||||
{sidebar.NavItem{Icon: "", Name: "Overview"}, func() tea.Model { return overview.New() }},
|
||||
{sidebar.NavItem{Icon: "", Name: "Metrics"}, func() tea.Model { return metrics.New() }},
|
||||
{sidebar.NavItem{Icon: "", Name: "Settings"}, func() tea.Model { return settings.New() }},
|
||||
}
|
||||
|
||||
type HelpProvider interface {
|
||||
HelpBindings() []key.Binding
|
||||
}
|
||||
|
||||
func navItems() []sidebar.NavItem {
|
||||
items := make([]sidebar.NavItem, len(pages))
|
||||
for i, p := range pages {
|
||||
items[i] = p.item
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
type keyMap struct {
|
||||
FocusSidebar key.Binding
|
||||
Close key.Binding
|
||||
Help key.Binding
|
||||
Quit key.Binding
|
||||
}
|
||||
|
||||
func defaultKeyMap() keyMap {
|
||||
return keyMap{
|
||||
FocusSidebar: key.NewBinding(key.WithKeys("ctrl+b"), key.WithHelp("ctrl+b", "focus sidebar")),
|
||||
Close: key.NewBinding(key.WithKeys("esc"), key.WithHelp("esc", "close")),
|
||||
Help: key.NewBinding(key.WithKeys("?"), key.WithHelp("?", "help")),
|
||||
Quit: key.NewBinding(key.WithKeys("ctrl+c", "q"), key.WithHelp("q / ctrl+c", "quit")),
|
||||
}
|
||||
}
|
||||
|
||||
type Model struct {
|
||||
sidebar sidebar.Model
|
||||
page tea.Model
|
||||
help helpbar.Model
|
||||
notif notification.Model
|
||||
modal modal.Model
|
||||
drawer drawer.Model
|
||||
keys keyMap
|
||||
|
||||
sidebarWidth int
|
||||
hideNav bool
|
||||
contentFocused bool
|
||||
width, height int
|
||||
}
|
||||
|
||||
func NewModel() Model {
|
||||
keys := defaultKeyMap()
|
||||
return Model{
|
||||
sidebar: sidebar.New(navItems()...),
|
||||
sidebarWidth: 24,
|
||||
page: pages[0].new(),
|
||||
help: helpbar.New(helpbar.WithToggle(keys.Help), helpbar.WithGlobal(keys.FocusSidebar, keys.Quit)),
|
||||
notif: notification.New(),
|
||||
modal: modal.New(),
|
||||
drawer: drawer.New(),
|
||||
keys: keys,
|
||||
hideNav: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) Init() tea.Cmd {
|
||||
return m.page.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
|
||||
return m.resize()
|
||||
|
||||
case sidebar.SelectMsg:
|
||||
m.page = pages[msg.Index].new()
|
||||
var cmd tea.Cmd
|
||||
m, cmd = m.resize()
|
||||
return m, tea.Batch(m.page.Init(), cmd)
|
||||
|
||||
case sidebar.BlurMsg:
|
||||
m.contentFocused = true
|
||||
return m.resize()
|
||||
|
||||
case tea.KeyPressMsg:
|
||||
if m.modal.Open() {
|
||||
if key.Matches(msg, m.keys.Close) {
|
||||
return m, modal.Close()
|
||||
}
|
||||
var cmd tea.Cmd
|
||||
m.modal, cmd = m.modal.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
if m.drawer.Open() {
|
||||
if key.Matches(msg, m.keys.Close) {
|
||||
return m, drawer.Close()
|
||||
}
|
||||
var cmd tea.Cmd
|
||||
m.drawer, cmd = m.drawer.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
switch {
|
||||
case key.Matches(msg, m.keys.Quit):
|
||||
return m, tea.Quit
|
||||
case key.Matches(msg, m.keys.Help):
|
||||
m.help, _ = m.help.Update(msg)
|
||||
return m.resize()
|
||||
case key.Matches(msg, m.keys.FocusSidebar):
|
||||
m.contentFocused = !m.contentFocused
|
||||
return m.resize()
|
||||
}
|
||||
|
||||
if m.contentFocused {
|
||||
var cmd tea.Cmd
|
||||
m.page, cmd = m.page.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
var cmd tea.Cmd
|
||||
m.sidebar, cmd = m.sidebar.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
var notifCmd, modalCmd, drawerCmd, sidebarCmd, pageCmd tea.Cmd
|
||||
m.notif, notifCmd = m.notif.Update(msg)
|
||||
m.modal, modalCmd = m.modal.Update(msg)
|
||||
m.drawer, drawerCmd = m.drawer.Update(msg)
|
||||
m.sidebar, sidebarCmd = m.sidebar.Update(msg)
|
||||
m.page, pageCmd = m.page.Update(msg)
|
||||
return m, tea.Batch(notifCmd, modalCmd, drawerCmd, sidebarCmd, pageCmd)
|
||||
}
|
||||
|
||||
func (m Model) navWidth() int {
|
||||
if m.hideNav && m.contentFocused {
|
||||
return 0
|
||||
}
|
||||
return m.sidebarWidth
|
||||
}
|
||||
|
||||
func (m Model) resize() (Model, tea.Cmd) {
|
||||
if m.width <= 0 || m.height <= 0 {
|
||||
return m, nil
|
||||
}
|
||||
m.help.SetWidth(m.width)
|
||||
inner := style.ContentHeight(m.height - m.help.Height(m.focusedHelp()...))
|
||||
|
||||
navW := m.navWidth()
|
||||
m.sidebar.SetSize(max(navW-2, 0), inner)
|
||||
|
||||
var cmd tea.Cmd
|
||||
m.page, cmd = m.page.Update(tea.WindowSizeMsg{
|
||||
Width: max(m.width-navW-2, 0),
|
||||
Height: inner,
|
||||
})
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m Model) focusedHelp() []key.Binding {
|
||||
if !m.contentFocused {
|
||||
return m.sidebar.HelpBindings()
|
||||
}
|
||||
if hp, ok := m.page.(HelpProvider); ok {
|
||||
return hp.HelpBindings()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Model) View() tea.View {
|
||||
if m.width <= 0 || m.height <= 0 {
|
||||
return tea.NewView("")
|
||||
}
|
||||
helpBar := m.help.View(m.focusedHelp()...)
|
||||
bodyH := max(m.height-lipgloss.Height(helpBar), 0)
|
||||
|
||||
navW := m.navWidth()
|
||||
body := style.RenderWithTitle(
|
||||
panel(m.contentFocused), m.sidebar.Selected().Name, m.page.View().Content, m.width-navW, bodyH)
|
||||
|
||||
if navW > 0 {
|
||||
left := style.RenderWithTitle(
|
||||
panel(!m.contentFocused), "Menu", m.sidebar.View(), navW, bodyH)
|
||||
body = lipgloss.JoinHorizontal(lipgloss.Top, left, body)
|
||||
}
|
||||
|
||||
appView := lipgloss.JoinVertical(lipgloss.Left,
|
||||
body,
|
||||
helpBar,
|
||||
)
|
||||
|
||||
view := tea.NewView(m.notif.Render(m.modal.Render(m.drawer.Render(appView))))
|
||||
view.AltScreen = true
|
||||
return view
|
||||
}
|
||||
|
||||
func panel(focused bool) lipgloss.Style {
|
||||
if focused {
|
||||
return style.S.PanelFocused
|
||||
}
|
||||
return style.S.Panel
|
||||
}
|
||||
|
||||
func main() {
|
||||
if _, err := tea.NewProgram(NewModel()).Run(); err != nil {
|
||||
fmt.Println("Error running program:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// Package metrics is the fullapp example's second page: a spinner and a few
|
||||
// gauges. It has no keys either, but unlike overview it runs a command, so
|
||||
// it's what proves the shell keeps feeding ticks to a pane that doesn't have
|
||||
// focus.
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"charm.land/bubbles/v2/progress"
|
||||
"charm.land/bubbles/v2/spinner"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
|
||||
"github.com/anotherhadi/ilovetui/bubbles"
|
||||
"github.com/anotherhadi/ilovetui/style"
|
||||
)
|
||||
|
||||
// gaugeWidth is how wide a bar renders, before the label in front of it.
|
||||
const gaugeWidth = 30
|
||||
|
||||
type gauge struct {
|
||||
name string
|
||||
percent float64
|
||||
}
|
||||
|
||||
// Model is the page. Values are hardcoded - this is a layout test, not a
|
||||
// monitoring tool.
|
||||
type Model struct {
|
||||
spinner spinner.Model
|
||||
bar progress.Model
|
||||
gauges []gauge
|
||||
|
||||
width, height int
|
||||
}
|
||||
|
||||
func New() Model {
|
||||
bar := bubbles.NewProgress()
|
||||
bar.SetWidth(gaugeWidth)
|
||||
return Model{
|
||||
spinner: bubbles.NewSpinner(spinner.WithSpinner(spinner.MiniDot)),
|
||||
bar: bar,
|
||||
gauges: []gauge{
|
||||
{"cpu", 0.42},
|
||||
{"memory", 0.71},
|
||||
{"disk", 0.13},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Init starts the spinner ticking. The shell returns it from its own Init,
|
||||
// or the spinner never starts.
|
||||
func (m Model) Init() tea.Cmd { return m.spinner.Tick }
|
||||
|
||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if size, ok := msg.(tea.WindowSizeMsg); ok {
|
||||
m.width, m.height = size.Width, size.Height
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Every other message goes to the spinner, whose own tick keeps it
|
||||
// turning - including while another pane has focus.
|
||||
var cmd tea.Cmd
|
||||
m.spinner, cmd = m.spinner.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m Model) View() tea.View {
|
||||
rows := []string{style.S.Bold.Render(m.spinner.View() + " collecting")}
|
||||
for _, g := range m.gauges {
|
||||
// ViewAs renders a given percentage without animating toward it,
|
||||
// which is all a fixed value needs.
|
||||
rows = append(rows, fmt.Sprintf("%-8s %s", g.name, m.bar.ViewAs(g.percent)))
|
||||
}
|
||||
|
||||
return tea.NewView(lipgloss.NewStyle().
|
||||
Width(m.width).Height(m.height).
|
||||
AlignHorizontal(lipgloss.Center).
|
||||
AlignVertical(lipgloss.Center).
|
||||
Render(lipgloss.JoinVertical(lipgloss.Left, rows...)))
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Package overview is the fullapp example's first page: the simplest pane
|
||||
// there is - no keys, no commands, just text sized to whatever room the
|
||||
// shell gives it.
|
||||
package overview
|
||||
|
||||
import (
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
|
||||
"github.com/anotherhadi/ilovetui/style"
|
||||
)
|
||||
|
||||
// Model is the page. It's an ordinary tea.Model: nothing about it knows it
|
||||
// lives in a shell, and it never implements HelpBindings because it has no
|
||||
// keys of its own to advertise.
|
||||
type Model struct {
|
||||
width, height int
|
||||
}
|
||||
|
||||
func New() Model { return Model{} }
|
||||
|
||||
func (m Model) Init() tea.Cmd { return nil }
|
||||
|
||||
// Update only tracks the size the shell hands down as a tea.WindowSizeMsg,
|
||||
// same as if the page were the whole program.
|
||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if size, ok := msg.(tea.WindowSizeMsg); ok {
|
||||
m.width, m.height = size.Width, size.Height
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) View() tea.View {
|
||||
body := lipgloss.JoinVertical(lipgloss.Center,
|
||||
style.S.Bold.Render("Overview"),
|
||||
"",
|
||||
style.S.Faint.Render("tab focuses this pane, but there's"),
|
||||
style.S.Faint.Render("nothing here to focus on."),
|
||||
)
|
||||
return tea.NewView(lipgloss.NewStyle().
|
||||
Width(m.width).Height(m.height).
|
||||
AlignHorizontal(lipgloss.Center).
|
||||
AlignVertical(lipgloss.Center).
|
||||
Render(body))
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// Package settings is the fullapp example's third page: a short list of
|
||||
// toggles. It's the one page with keys of its own, so it's what proves the
|
||||
// shell routes presses to the focused pane and lists that pane's bindings in
|
||||
// the help bar.
|
||||
package settings
|
||||
|
||||
import (
|
||||
"charm.land/bubbles/v2/key"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
|
||||
"github.com/anotherhadi/ilovetui/style"
|
||||
)
|
||||
|
||||
type toggle struct {
|
||||
name string
|
||||
on bool
|
||||
}
|
||||
|
||||
type keyMap struct {
|
||||
Up key.Binding
|
||||
Down key.Binding
|
||||
Toggle key.Binding
|
||||
}
|
||||
|
||||
// Model is the page. The cursor is a plain int: three lines don't need a
|
||||
// list.Model behind them.
|
||||
type Model struct {
|
||||
keys keyMap
|
||||
toggles []toggle
|
||||
cursor int
|
||||
|
||||
width, height int
|
||||
}
|
||||
|
||||
func New() Model {
|
||||
return Model{
|
||||
keys: keyMap{
|
||||
Up: key.NewBinding(key.WithKeys("up", "k"), key.WithHelp("↑/k", "up")),
|
||||
Down: key.NewBinding(key.WithKeys("down", "j"), key.WithHelp("↓/j", "down")),
|
||||
Toggle: key.NewBinding(key.WithKeys("space"), key.WithHelp("space", "toggle")),
|
||||
},
|
||||
toggles: []toggle{
|
||||
{name: "Nerd fonts", on: style.S.NerdFonts},
|
||||
{name: "Notifications", on: true},
|
||||
{name: "Telemetry", on: false},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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.width, m.height = msg.Width, msg.Height
|
||||
|
||||
case tea.KeyPressMsg:
|
||||
// The shell only sends these while this pane has focus, so there's
|
||||
// no focused check to make here.
|
||||
switch {
|
||||
case key.Matches(msg, m.keys.Up):
|
||||
m.cursor = max(m.cursor-1, 0)
|
||||
case key.Matches(msg, m.keys.Down):
|
||||
m.cursor = min(m.cursor+1, len(m.toggles)-1)
|
||||
case key.Matches(msg, m.keys.Toggle):
|
||||
m.toggles[m.cursor].on = !m.toggles[m.cursor].on
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) View() tea.View {
|
||||
rows := make([]string, len(m.toggles))
|
||||
for i, t := range m.toggles {
|
||||
cursor, box := " ", "[ ]"
|
||||
if i == m.cursor {
|
||||
cursor = "> "
|
||||
}
|
||||
if t.on {
|
||||
box = "[x]"
|
||||
}
|
||||
line := cursor + box + " " + t.name
|
||||
if i == m.cursor {
|
||||
line = lipgloss.NewStyle().Foreground(style.S.Primary).Render(line)
|
||||
}
|
||||
rows[i] = line
|
||||
}
|
||||
|
||||
return tea.NewView(lipgloss.NewStyle().
|
||||
Width(m.width).Height(m.height).
|
||||
AlignHorizontal(lipgloss.Center).
|
||||
AlignVertical(lipgloss.Center).
|
||||
Render(lipgloss.JoinVertical(lipgloss.Left, rows...)))
|
||||
}
|
||||
|
||||
// HelpBindings makes the page's keys show up in the shell's help bar while
|
||||
// it has focus. It's the optional half of the contract: overview and metrics
|
||||
// have no keys and don't implement it.
|
||||
func (m Model) HelpBindings() []key.Binding {
|
||||
return []key.Binding{m.keys.Up, m.keys.Down, m.keys.Toggle}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package sidebar
|
||||
|
||||
import (
|
||||
"charm.land/bubbles/v2/key"
|
||||
"charm.land/bubbles/v2/list"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
|
||||
"github.com/anotherhadi/ilovetui/bubbles"
|
||||
)
|
||||
|
||||
type NavItem struct {
|
||||
Icon string
|
||||
Name string
|
||||
}
|
||||
|
||||
func (n NavItem) Title() string {
|
||||
if n.Icon == "" {
|
||||
return n.Name
|
||||
}
|
||||
return n.Icon + " " + n.Name
|
||||
}
|
||||
|
||||
func (n NavItem) Description() string { return "" }
|
||||
func (n NavItem) FilterValue() string { return n.Name }
|
||||
|
||||
type SelectMsg struct {
|
||||
Index int
|
||||
Item NavItem
|
||||
}
|
||||
|
||||
// BlurMsg is the sidebar asking to be given up: it goes out with the
|
||||
// SelectMsg, on the grounds that picking an entry means you're done with the
|
||||
// menu. Where focus lands instead is the host's call - the sidebar has no
|
||||
// idea what else is on screen.
|
||||
type BlurMsg struct{}
|
||||
|
||||
// blur is BlurMsg's command form.
|
||||
func blur() tea.Msg { return BlurMsg{} }
|
||||
|
||||
type KeyMap struct {
|
||||
Select key.Binding
|
||||
}
|
||||
|
||||
func DefaultKeyMap() KeyMap {
|
||||
return KeyMap{
|
||||
Select: key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "select")),
|
||||
}
|
||||
}
|
||||
|
||||
type Model struct {
|
||||
KeyMap KeyMap
|
||||
|
||||
list list.Model
|
||||
items []NavItem
|
||||
selected int
|
||||
}
|
||||
|
||||
func New(items ...NavItem) Model {
|
||||
d := bubbles.NewDefaultDelegate()
|
||||
d.ShowDescription = false
|
||||
d.SetSpacing(0)
|
||||
|
||||
l := bubbles.NewList(listItems(items), 0, 0)
|
||||
l.SetDelegate(d)
|
||||
l.SetShowTitle(false)
|
||||
l.SetShowStatusBar(false)
|
||||
l.SetShowHelp(false)
|
||||
l.SetShowPagination(false)
|
||||
l.SetFilteringEnabled(false)
|
||||
l.DisableQuitKeybindings()
|
||||
|
||||
return Model{
|
||||
KeyMap: DefaultKeyMap(),
|
||||
list: l,
|
||||
items: items,
|
||||
}
|
||||
}
|
||||
|
||||
func listItems(items []NavItem) []list.Item {
|
||||
out := make([]list.Item, len(items))
|
||||
for i, item := range items {
|
||||
out[i] = item
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (m Model) Init() tea.Cmd { return nil }
|
||||
|
||||
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
||||
if press, ok := msg.(tea.KeyPressMsg); ok && key.Matches(press, m.KeyMap.Select) {
|
||||
// Only the interactive path asks for focus to move on. Select on its
|
||||
// own doesn't, because the host also calls it to set the starting
|
||||
// entry - which must not steal focus from anything.
|
||||
return m, tea.Batch(m.Select(m.list.Index()), blur)
|
||||
}
|
||||
|
||||
var cmd tea.Cmd
|
||||
m.list, cmd = m.list.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m *Model) Select(index int) tea.Cmd {
|
||||
if index < 0 || index >= len(m.items) {
|
||||
return nil
|
||||
}
|
||||
m.selected = index
|
||||
m.list.Select(index)
|
||||
|
||||
item := m.items[index]
|
||||
return func() tea.Msg { return SelectMsg{Index: index, Item: item} }
|
||||
}
|
||||
|
||||
func (m *Model) SetSize(width, height int) { m.list.SetSize(width, height) }
|
||||
|
||||
func (m Model) Selected() NavItem {
|
||||
if m.selected < 0 || m.selected >= len(m.items) {
|
||||
return NavItem{}
|
||||
}
|
||||
return m.items[m.selected]
|
||||
}
|
||||
|
||||
func (m Model) SelectedIndex() int { return m.selected }
|
||||
|
||||
func (m Model) Cursor() int { return m.list.Index() }
|
||||
|
||||
func (m Model) View() string { return m.list.View() }
|
||||
|
||||
func (m Model) HelpBindings() []key.Binding {
|
||||
return []key.Binding{
|
||||
m.list.KeyMap.CursorUp,
|
||||
m.list.KeyMap.CursorDown,
|
||||
m.KeyMap.Select,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
|
||||
"github.com/anotherhadi/ilovetui/modal"
|
||||
"github.com/anotherhadi/ilovetui/style"
|
||||
)
|
||||
|
||||
// confirmedMsg is what the confirmation modal reports back with. The app
|
||||
// listens for it like any other message - it never holds a reference to the
|
||||
// modal, and the modal never knows what confirming means.
|
||||
type confirmedMsg struct{}
|
||||
|
||||
// confirm is the modal's content: a model, so it owns its own keys. The host
|
||||
// no longer has to ask which modal is on top to know where "y" should go.
|
||||
type confirm struct{}
|
||||
|
||||
func (c confirm) Init() tea.Cmd { return nil }
|
||||
|
||||
func (c confirm) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if k, ok := msg.(tea.KeyPressMsg); ok && k.String() == "y" {
|
||||
// Report back and close itself: Close() is a package-level command,
|
||||
// so the content needs no reference to the modal.Model either.
|
||||
return c, tea.Batch(
|
||||
func() tea.Msg { return confirmedMsg{} },
|
||||
modal.Close(),
|
||||
)
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c confirm) View() tea.View {
|
||||
return tea.NewView("This can't be undone.\n\ny: confirm esc: cancel")
|
||||
}
|
||||
|
||||
type model struct {
|
||||
m modal.Model
|
||||
deleted bool
|
||||
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
|
||||
return m, nil
|
||||
|
||||
case confirmedMsg:
|
||||
m.deleted = true
|
||||
return m, nil
|
||||
|
||||
case tea.KeyPressMsg:
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
|
||||
case "m":
|
||||
return m, modal.Show("Delete file?", confirm{})
|
||||
|
||||
case "n":
|
||||
if m.m.Open() {
|
||||
return m, modal.Show("Really sure?", modal.Text("There's no undo for this one either."))
|
||||
}
|
||||
|
||||
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 {
|
||||
title := lipgloss.NewStyle().Bold(true).Foreground(style.S.Primary).Render("My App")
|
||||
text := "Some regular content, styled with theme colors,\nso you can see it turn flat gray behind the modal."
|
||||
if m.deleted {
|
||||
text = "File deleted - the modal's content reported back\nwith its own message, and closed itself."
|
||||
}
|
||||
body := lipgloss.NewStyle().Foreground(style.S.Text).Render(text)
|
||||
help := lipgloss.NewStyle().Foreground(style.S.Subtle).Render(
|
||||
"m: open modal n: open nested modal y: confirm esc: cancel q: quit")
|
||||
|
||||
background := lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center,
|
||||
lipgloss.JoinVertical(lipgloss.Center, title, "", body, "", help))
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
|
||||
"github.com/anotherhadi/ilovetui/notification"
|
||||
"github.com/anotherhadi/ilovetui/style"
|
||||
)
|
||||
|
||||
var positions = []struct {
|
||||
name string
|
||||
pos notification.Position
|
||||
}{
|
||||
{"top", notification.Top},
|
||||
{"top-left", notification.TopLeft},
|
||||
{"top-right", notification.TopRight},
|
||||
{"bottom", notification.Bottom},
|
||||
{"bottom-left", notification.BottomLeft},
|
||||
{"bottom-right", notification.BottomRight},
|
||||
}
|
||||
|
||||
const stickyID = "sticky-demo"
|
||||
|
||||
type model struct {
|
||||
notif notification.Model
|
||||
posIdx int
|
||||
width, height int
|
||||
}
|
||||
|
||||
func newModel() model {
|
||||
return model{notif: notification.New(notification.WithPosition(positions[0].pos))}
|
||||
}
|
||||
|
||||
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
|
||||
return m, nil
|
||||
|
||||
case tea.KeyPressMsg:
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
|
||||
case "1":
|
||||
return m, notification.Show("Info", "Just so you know.", notification.Info)
|
||||
case "2":
|
||||
return m, notification.Show("Success", "Config written to disk.", notification.Success)
|
||||
case "3":
|
||||
return m, notification.Show("Warning", "Disk space getting low on /dev/sda1.", notification.Warning)
|
||||
case "4":
|
||||
return m, notification.Show("Error", "Failed to reach the remote host.", notification.Error)
|
||||
|
||||
case "s":
|
||||
return m, notification.Show("Sticky", "Stays until you press d.",
|
||||
notification.Info, notification.WithID(stickyID), notification.WithDuration(0))
|
||||
case "d":
|
||||
return m, notification.Dismiss(stickyID)
|
||||
|
||||
case "p":
|
||||
m.posIdx = (m.posIdx + 1) % len(positions)
|
||||
m.notif = notification.New(notification.WithPosition(positions[m.posIdx].pos))
|
||||
return m, m.notif.Init()
|
||||
}
|
||||
}
|
||||
|
||||
var cmd tea.Cmd
|
||||
m.notif, cmd = m.notif.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m model) View() tea.View {
|
||||
help := lipgloss.NewStyle().Foreground(style.S.Subtle).Render(
|
||||
"1-4: info/success/warning/error s: sticky d: dismiss sticky p: position (" +
|
||||
positions[m.posIdx].name + ") q: quit")
|
||||
|
||||
background := lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, help)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
// Command sidebar is the whole app-shell pattern in one file: a sidebar on
|
||||
// the left, an ordinary tea.Model on the right, a global help bar at the
|
||||
// bottom, tab to move focus between the two. No layout package involved -
|
||||
// lipgloss.JoinHorizontal/JoinVertical and style.RenderWithTitle already do
|
||||
// all of it, and the panes stay plain tea.Models.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"charm.land/bubbles/v2/key"
|
||||
"charm.land/bubbles/v2/list"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
|
||||
"github.com/anotherhadi/ilovetui/bubbles"
|
||||
"github.com/anotherhadi/ilovetui/helpbar"
|
||||
"github.com/anotherhadi/ilovetui/style"
|
||||
)
|
||||
|
||||
// sidebarWidth is the sidebar's total width, border included.
|
||||
const sidebarWidth = 24
|
||||
|
||||
// HelpProvider is the only contract in this pattern, and it's optional: a
|
||||
// pane that implements it gets its own bindings listed in the global help
|
||||
// bar while it's focused. A pane that doesn't just contributes nothing.
|
||||
type HelpProvider interface {
|
||||
HelpBindings() []key.Binding
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- shell keys
|
||||
|
||||
type keyMap struct {
|
||||
Focus key.Binding
|
||||
Help key.Binding
|
||||
Quit key.Binding
|
||||
}
|
||||
|
||||
func defaultKeyMap() keyMap {
|
||||
return keyMap{
|
||||
Focus: key.NewBinding(key.WithKeys("tab"), key.WithHelp("tab", "switch pane")),
|
||||
Help: key.NewBinding(key.WithKeys("?"), key.WithHelp("?", "help")),
|
||||
Quit: key.NewBinding(key.WithKeys("ctrl+c", "q"), key.WithHelp("q", "quit")),
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- the shell
|
||||
|
||||
type model struct {
|
||||
sidebar list.Model
|
||||
content tea.Model
|
||||
help helpbar.Model
|
||||
keys keyMap
|
||||
|
||||
contentFocused bool
|
||||
w, h int
|
||||
}
|
||||
|
||||
func newModel() model {
|
||||
items := []list.Item{page("Overview"), page("Metrics"), page("Settings")}
|
||||
sidebar := bubbles.NewList(items, sidebarWidth-2, 0)
|
||||
sidebar.SetShowTitle(false)
|
||||
sidebar.SetShowStatusBar(false)
|
||||
sidebar.SetShowHelp(false)
|
||||
|
||||
keys := defaultKeyMap()
|
||||
return model{
|
||||
sidebar: sidebar,
|
||||
content: newCounter(),
|
||||
help: helpbar.New(helpbar.WithToggle(keys.Help), helpbar.WithGlobal(keys.Focus, keys.Quit)),
|
||||
keys: keys,
|
||||
}
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd { return m.content.Init() }
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
m.w, m.h = msg.Width, msg.Height
|
||||
return m.resize()
|
||||
|
||||
case tea.KeyPressMsg:
|
||||
switch {
|
||||
case key.Matches(msg, m.keys.Quit):
|
||||
return m, tea.Quit
|
||||
case key.Matches(msg, m.keys.Help):
|
||||
// Expanding the bar takes rows away from the panes.
|
||||
m.help, _ = m.help.Update(msg)
|
||||
return m.resize()
|
||||
case key.Matches(msg, m.keys.Focus):
|
||||
m.contentFocused = !m.contentFocused
|
||||
return m, nil
|
||||
}
|
||||
// Only the focused pane sees key presses.
|
||||
if m.contentFocused {
|
||||
var cmd tea.Cmd
|
||||
m.content, cmd = m.content.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
var cmd tea.Cmd
|
||||
m.sidebar, cmd = m.sidebar.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
// Everything else (ticks, HTTP responses...) goes to both, so a blurred
|
||||
// pane keeps working.
|
||||
var sidebarCmd, contentCmd tea.Cmd
|
||||
m.sidebar, sidebarCmd = m.sidebar.Update(msg)
|
||||
m.content, contentCmd = m.content.Update(msg)
|
||||
return m, tea.Batch(sidebarCmd, contentCmd)
|
||||
}
|
||||
|
||||
// bodyHeight is the height left for the two panes once the help bar has
|
||||
// taken its share. resize and View both go through it so they can't disagree.
|
||||
func (m model) bodyHeight() int {
|
||||
return max(m.h-m.help.Height(m.focusedHelp()...), 0)
|
||||
}
|
||||
|
||||
func (m model) resize() (model, tea.Cmd) {
|
||||
if m.w <= 0 || m.h <= 0 {
|
||||
return m, nil
|
||||
}
|
||||
m.help.SetWidth(m.w)
|
||||
inner := style.ContentHeight(m.bodyHeight())
|
||||
|
||||
m.sidebar.SetSize(sidebarWidth-2, inner)
|
||||
|
||||
var cmd tea.Cmd
|
||||
m.content, cmd = m.content.Update(tea.WindowSizeMsg{
|
||||
Width: max(m.w-sidebarWidth-2, 0), Height: inner,
|
||||
})
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
// focusedHelp is the focused pane's own bindings, if it offers any.
|
||||
func (m model) focusedHelp() []key.Binding {
|
||||
if m.contentFocused {
|
||||
if hp, ok := m.content.(HelpProvider); ok {
|
||||
return hp.HelpBindings()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return []key.Binding{m.sidebar.KeyMap.CursorUp, m.sidebar.KeyMap.CursorDown}
|
||||
}
|
||||
|
||||
func (m model) View() tea.View {
|
||||
if m.w <= 0 || m.h <= 0 {
|
||||
return tea.NewView("")
|
||||
}
|
||||
helpBar := m.help.View(m.focusedHelp()...)
|
||||
bodyH := m.bodyHeight()
|
||||
|
||||
left := style.RenderWithTitle(
|
||||
panel(!m.contentFocused), "Menu", m.sidebar.View(), sidebarWidth, bodyH)
|
||||
right := style.RenderWithTitle(
|
||||
panel(m.contentFocused), "Content", m.content.View().Content, m.w-sidebarWidth, bodyH)
|
||||
|
||||
view := tea.NewView(lipgloss.JoinVertical(lipgloss.Left,
|
||||
lipgloss.JoinHorizontal(lipgloss.Top, left, right),
|
||||
helpBar,
|
||||
))
|
||||
view.AltScreen = true
|
||||
return view
|
||||
}
|
||||
|
||||
// panel picks the bordered panel style matching a pane's focus state.
|
||||
func panel(focused bool) lipgloss.Style {
|
||||
if focused {
|
||||
return style.S.PanelFocused
|
||||
}
|
||||
return style.S.Panel
|
||||
}
|
||||
|
||||
// ------------------------------------------------------- the right-hand pane
|
||||
|
||||
// counter is an ordinary tea.Model - nothing about it knows it's living in a
|
||||
// shell. It implements HelpProvider purely to appear in the help bar.
|
||||
type counter struct {
|
||||
n int
|
||||
w, h int
|
||||
keys struct{ Inc, Dec key.Binding }
|
||||
}
|
||||
|
||||
func newCounter() *counter {
|
||||
c := &counter{}
|
||||
c.keys.Inc = key.NewBinding(key.WithKeys("+", "k"), key.WithHelp("+/k", "increment"))
|
||||
c.keys.Dec = key.NewBinding(key.WithKeys("-", "j"), key.WithHelp("-/j", "decrement"))
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *counter) Init() tea.Cmd { return nil }
|
||||
|
||||
func (c *counter) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
c.w, c.h = msg.Width, msg.Height
|
||||
case tea.KeyPressMsg:
|
||||
switch {
|
||||
case key.Matches(msg, c.keys.Inc):
|
||||
c.n++
|
||||
case key.Matches(msg, c.keys.Dec):
|
||||
c.n--
|
||||
}
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *counter) View() tea.View {
|
||||
return tea.NewView(lipgloss.NewStyle().
|
||||
Width(c.w).Height(c.h).
|
||||
AlignHorizontal(lipgloss.Center).
|
||||
AlignVertical(lipgloss.Center).
|
||||
Render(fmt.Sprintf("count: %d", c.n)))
|
||||
}
|
||||
|
||||
func (c *counter) HelpBindings() []key.Binding {
|
||||
return []key.Binding{c.keys.Inc, c.keys.Dec}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- sidebar items
|
||||
|
||||
type page string
|
||||
|
||||
func (p page) Title() string { return string(p) }
|
||||
func (p page) Description() string { return "" }
|
||||
func (p page) FilterValue() string { return string(p) }
|
||||
|
||||
func main() {
|
||||
if _, err := tea.NewProgram(newModel()).Run(); err != nil {
|
||||
fmt.Println("Error running program:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
|
||||
"github.com/anotherhadi/ilovetui/tabs"
|
||||
)
|
||||
|
||||
// pane is a minimal tabs.Tab implementation. It keeps its own counter to
|
||||
// show that each tab's model has independent state that persists across
|
||||
// switches, and its own width/height to show how a host propagates size
|
||||
// down to a wrapped Tab (see model.Update's tea.WindowSizeMsg case).
|
||||
type pane struct {
|
||||
name string
|
||||
count int
|
||||
width, height int
|
||||
}
|
||||
|
||||
func newPane(name string) pane {
|
||||
return pane{name: name}
|
||||
}
|
||||
|
||||
func (p pane) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p pane) Update(msg tea.Msg) (tabs.Tab, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
p.width, p.height = msg.Width, msg.Height
|
||||
case tea.KeyPressMsg:
|
||||
if msg.String() == "+" {
|
||||
p.count++
|
||||
}
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (p pane) View() string {
|
||||
return fmt.Sprintf("%s\n\npress + to increment: %d\n(content area: %dx%d)", p.name, p.count, p.width, p.height)
|
||||
}
|
||||
|
||||
var docStyle = lipgloss.NewStyle().Padding(1, 2, 1, 2)
|
||||
|
||||
type model struct {
|
||||
tabs tabs.Model
|
||||
}
|
||||
|
||||
func newModel() model {
|
||||
items := []tabs.Item{
|
||||
{Title: "Lip Gloss", Model: newPane("Lip Gloss")},
|
||||
{Title: "Blush", Model: newPane("Blush")},
|
||||
{Title: "Eye Shadow", Model: newPane("Eye Shadow")},
|
||||
{Title: "Mascara", Model: newPane("Mascara")},
|
||||
{Title: "Foundation", Model: newPane("Foundation")},
|
||||
}
|
||||
|
||||
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) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyPressMsg:
|
||||
if msg.String() == "ctrl+c" || msg.String() == "q" {
|
||||
return m, tea.Quit
|
||||
}
|
||||
case tea.WindowSizeMsg:
|
||||
// Size the tabs component to fill the terminal, net of docStyle's own
|
||||
// frame. The tab bar itself keeps its natural width; Content stretches.
|
||||
m.tabs.SetSize(
|
||||
msg.Width-docStyle.GetHorizontalFrameSize(),
|
||||
msg.Height-docStyle.GetVerticalFrameSize(),
|
||||
)
|
||||
|
||||
// tabs has no generic way to size an arbitrary Tab itself, so forward
|
||||
// the actual usable content area as a WindowSizeMsg: tabs.Update
|
||||
// already routes non-key messages to the active item's Update, so
|
||||
// this reaches pane.Update's own tea.WindowSizeMsg case above.
|
||||
var cmd tea.Cmd
|
||||
m.tabs, cmd = m.tabs.Update(tea.WindowSizeMsg{
|
||||
Width: m.tabs.ContentWidth(),
|
||||
Height: m.tabs.ContentHeight(),
|
||||
})
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
var cmd tea.Cmd
|
||||
m.tabs, cmd = m.tabs.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m model) View() tea.View {
|
||||
view := tea.NewView(docStyle.Render(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
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1767039857,
|
||||
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
|
||||
"owner": "NixOS",
|
||||
"repo": "flake-compat",
|
||||
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"git-hooks": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
"gitignore": "gitignore",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1778507602,
|
||||
"narHash": "sha256-kTwur1wV+01SdqskVMSo6JMEpg71ps3HpbFY2GsflKs=",
|
||||
"owner": "cachix",
|
||||
"repo": "git-hooks.nix",
|
||||
"rev": "61ab0e80d9c7ab14c256b5b453d8b3fb0189ba0a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"repo": "git-hooks.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"gitignore": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"git-hooks",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1709087332,
|
||||
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1779560665,
|
||||
"narHash": "sha256-tpyBcxPpcQb8ukyNF7DoCwfSY3VPsxHoYwj00Cayv5o=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "64c08a7ca051951c8eae34e3e3cb1e202fe36786",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"git-hooks": "git-hooks",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
description = "A minimal Go library that provides a shared Base16color theme for terminal UIs built with bubbletea and lipgloss.";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
git-hooks = {
|
||||
url = "github:cachix/git-hooks.nix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
|
||||
outputs = {
|
||||
self,
|
||||
nixpkgs,
|
||||
git-hooks,
|
||||
}: let
|
||||
supportedSystems = ["x86_64-linux" "aarch64-linux"];
|
||||
|
||||
forAllSystems = f:
|
||||
nixpkgs.lib.genAttrs supportedSystems
|
||||
(system: f system (import nixpkgs {inherit system;}));
|
||||
in {
|
||||
devShells = forAllSystems (system: pkgs: {
|
||||
default = import ./nix/shell.nix {
|
||||
inherit pkgs;
|
||||
gitHooksLib = git-hooks.lib.${system};
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
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
|
||||
)
|
||||
@@ -0,0 +1,87 @@
|
||||
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=
|
||||
@@ -0,0 +1,91 @@
|
||||
# Helpbar
|
||||
|
||||
A responsive help bar: one line of key bindings that expands, on demand, into a multi-column view reflowed to use as many columns as the available width allows.
|
||||
|
||||
## Quick start
|
||||
|
||||
```go
|
||||
import "github.com/anotherhadi/ilovetui/helpbar"
|
||||
|
||||
type model struct {
|
||||
help helpbar.Model
|
||||
keys keyMap
|
||||
h int
|
||||
}
|
||||
|
||||
func newModel() model {
|
||||
keys := defaultKeyMap()
|
||||
return model{
|
||||
help: helpbar.New(
|
||||
helpbar.WithToggle(keys.Help), // '?' expands/collapses
|
||||
helpbar.WithGlobal(keys.Focus, keys.Quit), // always shown
|
||||
),
|
||||
keys: keys,
|
||||
}
|
||||
}
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
m.h = msg.Height
|
||||
m.help.SetWidth(msg.Width)
|
||||
case tea.KeyPressMsg:
|
||||
m.help, _ = m.help.Update(msg) // flips ShowAll on the toggle binding
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m model) View() tea.View {
|
||||
bar := m.help.View(m.focused().HelpBindings()...)
|
||||
body := renderBody(m.h - lipgloss.Height(bar))
|
||||
return tea.NewView(lipgloss.JoinVertical(lipgloss.Left, body, bar))
|
||||
}
|
||||
```
|
||||
|
||||
## Global vs. contextual bindings
|
||||
|
||||
Bindings come from two places, and they render in this order:
|
||||
|
||||
1. The `WithToggle` binding, so the way to expand the bar always leads.
|
||||
2. `WithGlobal` bindings - what your app reserves for itself (quit, switch pane...), set once.
|
||||
3. Contextual bindings, passed to `View` at render time.
|
||||
|
||||
Contextual bindings are meant to change from render to render, so the bar can track whatever component currently has focus. Nothing in the package knows what "focus" means for your app - you just hand it a different slice:
|
||||
|
||||
```go
|
||||
func (m model) focusedHelp() []key.Binding {
|
||||
if m.contentFocused {
|
||||
return m.content.HelpBindings()
|
||||
}
|
||||
return []key.Binding{m.sidebar.KeyMap.CursorUp, m.sidebar.KeyMap.CursorDown}
|
||||
}
|
||||
```
|
||||
|
||||
Disabled bindings (`key.Binding.SetEnabled(false)`) are dropped before layout, so the reflow never
|
||||
budgets width for something that won't be drawn.
|
||||
|
||||
## Reserving room for the bar
|
||||
|
||||
The bar's height depends on its content and on whether it's expanded, so ask it rather than assuming
|
||||
a fixed number of rows:
|
||||
|
||||
```go
|
||||
body := m.height - m.help.Height(contextual...)
|
||||
```
|
||||
|
||||
`Height` is exactly `lipgloss.Height` of what `View` returns for the same arguments, so the two can
|
||||
never disagree about where the bar begins. An empty bar (no bindings, or no width set) renders `""`
|
||||
and takes zero rows.
|
||||
|
||||
## Styling
|
||||
|
||||
Defaults come from the shared `style` theme. Override with `WithStyles`:
|
||||
|
||||
```go
|
||||
helpbar.New(helpbar.WithStyles(myStyles)) // help.Styles from charm.land/bubbles/v2/help
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
- `examples/sidebar` uses it as an app-wide bar tracking the focused pane.
|
||||
- `examples/app` full app with a single help bar
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user