diff --git a/.envrc b/.envrc deleted file mode 100644 index 3550a30..0000000 --- a/.envrc +++ /dev/null @@ -1 +0,0 @@ -use flake diff --git a/.github/.goreleaser.yaml b/.github/.goreleaser.yaml deleted file mode 100644 index b914ca2..0000000 --- a/.github/.goreleaser.yaml +++ /dev/null @@ -1,24 +0,0 @@ -version: 2 - -before: - hooks: - - go mod tidy - -builds: - - skip: true - -archives: - - formats: - - tar.gz - name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}" - -checksum: - name_template: checksums.txt - -changelog: - sort: asc - filters: - exclude: - - "^docs:" - - "^test:" - - "^ci:" diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md deleted file mode 100644 index 7b8ae86..0000000 --- a/.github/CONTRIBUTING.md +++ /dev/null @@ -1,10 +0,0 @@ -# Contributing - -Everybody is invited and welcome to contribute. There is a lot to do... Check the issues! - -The process is straight-forward. - -- Read [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#best-practices-for-faster-reviews) by Kubernetes (but skip step 0 and 1) -- Fork this git repository -- Write your changes (bug fixes, new features, ...). -- Create a Pull Request against the main branch. diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 5e917f0..0000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1 +0,0 @@ -custom: ["https://hadi.icu/donate"] diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index bf265c5..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Release - -on: - push: - tags: - - "v*" - -permissions: - contents: write - -jobs: - release: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - uses: actions/setup-go@v5 - with: - go-version-file: go.mod - - - uses: goreleaser/goreleaser-action@v6 - with: - version: "~> v2" - args: release --clean --config .github/.goreleaser.yaml - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 03ab393..b947077 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,2 @@ -.claude/ -CLAUDE.md -result/ -.pre-commit-config.yaml +node_modules/ +dist/ diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 7ce7478..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2026 Hadi - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/README.md b/README.md index 0dc36c7..6dd79cd 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,22 @@ # I Love TUI -A shared [Base16](https://github.com/tinted-theming/home) theme, a themed wrapper around every official component, and a small set of custom Bubble Tea components, in one Go module, so every TUI built with it shares one config file and looks consistent. +Shared OpenTUI/Solid theme, components, and config helpers for my TUIs. ## Install -```sh -go get github.com/anotherhadi/ilovetui +```bash +bun add github:anotherhadi/ilovetui ``` -## Packages +Peer dependencies, install whichever you actually use: `@opentui/core`, `@opentui/solid`, `@opentui/keymap`, `solid-js`, `opentui-spinner`. -- [`style`](style/README.md): the theme itself. Colors, pre-built panel styles, config loading. -- [`bubbles`](bubbles/README.md): themed constructors for official `bubbles/v2` components (`help`, `textarea`, `textinput`, `viewport`, ...). -- [`tabs`](tabs/README.md), [`modal`](modal/README.md), [`drawer`](drawer/README.md), [`notification`](notification/README.md), [`helpbar`](helpbar/README.md): custom components not found in the official `bubbles` library, styled from the same theme. +## Usage -Each package has its own README and a runnable example under `examples/`. - -## Quick start - -On import, `style` automatically loads the user's theme from `~/.config/ilovetui/config.yaml` (embedded default as fallback), exposed as the package-level `S`: - -```go -import "github.com/anotherhadi/ilovetui/style" - -s := lipgloss.NewStyle().Foreground(style.S.Primary) -box := style.RenderWithTitle(style.S.PanelFocused, "Title", content, w, h) +```ts +import { theme, presets } from "ilovetui"; +import "ilovetui/solid"; ``` -No setup required. See [`style/README.md`](style/README.md) for config details. +Entry points: `.`, `./solid`, `./context`, `./project-config`, `./keymap`, `./components`, `./components/help`, `./components/spinner`. See the source for what each exports. -## 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). +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. diff --git a/app/components.go b/app/components.go deleted file mode 100644 index f175ddd..0000000 --- a/app/components.go +++ /dev/null @@ -1,55 +0,0 @@ -package app - -import ( - "image/color" - "strings" - - "charm.land/lipgloss/v2" - "github.com/anotherhadi/ilovetui/style" -) - -func RenderToggle(on bool) string { - if !style.S.NerdFonts { - if on { - return lipgloss.NewStyle(). - Foreground(style.S.Success). - Bold(true). - Render("on") - } - return lipgloss.NewStyle(). - Foreground(style.S.Subtle). - Bold(true). - Render("off") - } - - if on { - return RenderBadge(" ●", style.S.Success) - } - return RenderBadge("● ", style.S.Subtle) -} - -func RenderBadge(label string, fill color.Color) string { - body := lipgloss.NewStyle(). - Background(fill). - Foreground(style.S.Background). - Bold(true). - Render(label) - - cap := lipgloss.NewStyle().Foreground(fill).Background(style.S.Background) - return cap.Render("") + body + cap.Render("") -} - -func SignalBars(signal int) string { - bars := []rune("▁▃▅▇") - level := signal * len(bars) / 100 - - var b strings.Builder - for i, ch := range bars { - s := lipgloss.NewStyle().Foreground(style.S.Subtle) - if i < level { - s = lipgloss.NewStyle().Foreground(style.S.Text) - } - b.WriteString(s.Render(string(ch))) - } - return b.String() -} diff --git a/app/msg.go b/app/msg.go deleted file mode 100644 index 9c9adde..0000000 --- a/app/msg.go +++ /dev/null @@ -1,33 +0,0 @@ -package app - -import tea "charm.land/bubbletea/v2" - -type QuitMsg struct{} - -func Quit() tea.Cmd { - return func() tea.Msg { return QuitMsg{} } -} - -type FocusMsg struct{} - -func Focus() tea.Cmd { - return func() tea.Msg { return FocusMsg{} } -} - -type BlurMsg struct{} - -func Blur() tea.Cmd { - return func() tea.Msg { return BlurMsg{} } -} - -type RequestFocusMsg struct{} - -func RequestFocus() tea.Cmd { - return func() tea.Msg { return RequestFocusMsg{} } -} - -type SetTitleMsg struct{ Title string } - -func SetTitle(title string) tea.Cmd { - return func() tea.Msg { return SetTitleMsg{Title: title} } -} diff --git a/bubbles/README.md b/bubbles/README.md deleted file mode 100644 index 29487e2..0000000 --- a/bubbles/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# bubbles - -Themed constructors for every official `bubbles/v2` component: `help`, `textarea`, `textinput`, -`list`, `table`, `filepicker`, `spinner`, `progress`, `paginator`, `viewport`. Each wraps the -official `New`, then applies `style.S` colors on top. - -- `spinner`, `table`, `progress` forward any `opts` to the upstream `New` before the theme is - applied, so the theme's colors always win over anything conflicting in `opts`. -- `ViewportView` renders a `viewport.Model` with a themed scrollbar thumb in the left gutter, - shown only when content overflows. -- `NewList`/`NewDefaultDelegate` theme both the list chrome and the selection/filter colors of its - default delegate. - -Custom components in this repo (`tabs`, `modal`, `drawer`, `notification`, `helpbar`) build any -official component they need through here, never through `charm.land/bubbles/v2` directly. - -See `examples/bubbles`. diff --git a/bubbles/bubbles.go b/bubbles/bubbles.go deleted file mode 100644 index 9b9e8d4..0000000 --- a/bubbles/bubbles.go +++ /dev/null @@ -1 +0,0 @@ -package bubbles diff --git a/bubbles/filepicker.go b/bubbles/filepicker.go deleted file mode 100644 index 63af9ce..0000000 --- a/bubbles/filepicker.go +++ /dev/null @@ -1,25 +0,0 @@ -package bubbles - -import ( - "charm.land/bubbles/v2/filepicker" - - "github.com/anotherhadi/ilovetui/style" -) - -func NewFilePicker() filepicker.Model { - f := filepicker.New() - s := filepicker.DefaultStyles() - s.Cursor = s.Cursor.Foreground(style.S.Primary) - s.DisabledCursor = s.DisabledCursor.Foreground(style.S.Subtle) - s.Symlink = s.Symlink.Foreground(style.S.Warning) - s.Directory = s.Directory.Foreground(style.S.Primary) - s.File = s.File.Foreground(style.S.Text) - s.DisabledFile = s.DisabledFile.Foreground(style.S.Subtle) - s.Permission = s.Permission.Foreground(style.S.Muted) - s.Selected = s.Selected.Foreground(style.S.Primary) - s.DisabledSelected = s.DisabledSelected.Foreground(style.S.Subtle) - s.FileSize = s.FileSize.Foreground(style.S.Muted) - s.EmptyDirectory = s.EmptyDirectory.Foreground(style.S.Subtle) - f.Styles = s - return f -} diff --git a/bubbles/help.go b/bubbles/help.go deleted file mode 100644 index 7c1bac1..0000000 --- a/bubbles/help.go +++ /dev/null @@ -1,20 +0,0 @@ -package bubbles - -import ( - "charm.land/bubbles/v2/help" - "charm.land/lipgloss/v2" - - "github.com/anotherhadi/ilovetui/style" -) - -func NewHelp() help.Model { - h := help.New() - h.Styles.ShortKey = lipgloss.NewStyle().Foreground(style.S.Primary) - h.Styles.ShortDesc = lipgloss.NewStyle().Foreground(style.S.Muted) - h.Styles.ShortSeparator = lipgloss.NewStyle().Foreground(style.S.Subtle) - h.Styles.FullKey = lipgloss.NewStyle().Foreground(style.S.Primary) - h.Styles.FullDesc = lipgloss.NewStyle().Foreground(style.S.Muted) - h.Styles.FullSeparator = lipgloss.NewStyle().Foreground(style.S.Subtle) - h.Styles.Ellipsis = lipgloss.NewStyle().Foreground(style.S.Subtle) - return h -} diff --git a/bubbles/list.go b/bubbles/list.go deleted file mode 100644 index 1cb71a0..0000000 --- a/bubbles/list.go +++ /dev/null @@ -1,47 +0,0 @@ -package bubbles - -import ( - "charm.land/bubbles/v2/list" - - "github.com/anotherhadi/ilovetui/style" -) - -func NewList(items []list.Item, width, height int) list.Model { - m := list.New(items, NewDefaultDelegate(), width, height) - m.Styles = themedListStyles() - return m -} - -func themedListStyles() list.Styles { - s := list.DefaultStyles(true) - s.Title = s.Title.Background(style.S.Primary).Foreground(style.S.Background) - s.Spinner = s.Spinner.Foreground(style.S.Primary) - s.Filter = themedTextInputStyles() - s.DefaultFilterCharacterMatch = s.DefaultFilterCharacterMatch.Foreground(style.S.Primary) - s.StatusBar = s.StatusBar.Foreground(style.S.Muted) - s.StatusEmpty = s.StatusEmpty.Foreground(style.S.Subtle) - s.StatusBarActiveFilter = s.StatusBarActiveFilter.Foreground(style.S.Text) - s.StatusBarFilterCount = s.StatusBarFilterCount.Foreground(style.S.Subtle) - s.NoItems = s.NoItems.Foreground(style.S.Subtle) - s.ArabicPagination = s.ArabicPagination.Foreground(style.S.Subtle) - s.ActivePaginationDot = s.ActivePaginationDot.Foreground(style.S.Primary) - s.InactivePaginationDot = s.InactivePaginationDot.Foreground(style.S.Subtle) - s.DividerDot = s.DividerDot.Foreground(style.S.Subtle) - return s -} - -func NewDefaultDelegate() list.DefaultDelegate { - d := list.NewDefaultDelegate() - d.Styles.NormalTitle = d.Styles.NormalTitle.Foreground(style.S.Text) - d.Styles.NormalDesc = d.Styles.NormalDesc.Foreground(style.S.Muted) - d.Styles.SelectedTitle = d.Styles.SelectedTitle. - BorderForeground(style.S.Primary). - Foreground(style.S.Primary) - d.Styles.SelectedDesc = d.Styles.SelectedDesc. - BorderForeground(style.S.Primary). - Foreground(style.S.Primary) - d.Styles.DimmedTitle = d.Styles.DimmedTitle.Foreground(style.S.Subtle) - d.Styles.DimmedDesc = d.Styles.DimmedDesc.Foreground(style.S.SubtleBg) - d.Styles.FilterMatch = d.Styles.FilterMatch.Foreground(style.S.Primary) - return d -} diff --git a/bubbles/paginator.go b/bubbles/paginator.go deleted file mode 100644 index 71e276a..0000000 --- a/bubbles/paginator.go +++ /dev/null @@ -1,16 +0,0 @@ -package bubbles - -import ( - "charm.land/bubbles/v2/paginator" - "charm.land/lipgloss/v2" - - "github.com/anotherhadi/ilovetui/style" -) - -func NewPaginator() paginator.Model { - p := paginator.New() - p.Type = paginator.Dots - p.ActiveDot = lipgloss.NewStyle().Foreground(style.S.Primary).Render("•") - p.InactiveDot = lipgloss.NewStyle().Foreground(style.S.Subtle).Render("•") - return p -} diff --git a/bubbles/progress.go b/bubbles/progress.go deleted file mode 100644 index 52f7946..0000000 --- a/bubbles/progress.go +++ /dev/null @@ -1,16 +0,0 @@ -package bubbles - -import ( - "charm.land/bubbles/v2/progress" - - "github.com/anotherhadi/ilovetui/style" -) - -func NewProgress(opts ...progress.Option) progress.Model { - allOpts := append([]progress.Option{ - progress.WithColors(style.S.Subtle, style.S.Primary), - }, opts...) - p := progress.New(allOpts...) - p.EmptyColor = style.S.SubtleBg - return p -} diff --git a/bubbles/spinner.go b/bubbles/spinner.go deleted file mode 100644 index ec8349d..0000000 --- a/bubbles/spinner.go +++ /dev/null @@ -1,14 +0,0 @@ -package bubbles - -import ( - "charm.land/bubbles/v2/spinner" - "charm.land/lipgloss/v2" - - "github.com/anotherhadi/ilovetui/style" -) - -func NewSpinner(opts ...spinner.Option) spinner.Model { - s := spinner.New(opts...) - s.Style = lipgloss.NewStyle().Foreground(style.S.Primary) - return s -} diff --git a/bubbles/table.go b/bubbles/table.go deleted file mode 100644 index 5ba0122..0000000 --- a/bubbles/table.go +++ /dev/null @@ -1,17 +0,0 @@ -package bubbles - -import ( - "charm.land/bubbles/v2/table" - - "github.com/anotherhadi/ilovetui/style" -) - -func NewTable(opts ...table.Option) table.Model { - t := table.New(opts...) - s := table.DefaultStyles() - s.Header = s.Header.Foreground(style.S.Primary) - s.Cell = s.Cell.Foreground(style.S.Text) - s.Selected = s.Selected.Foreground(style.S.Primary) - t.SetStyles(s) - return t -} diff --git a/bubbles/textarea.go b/bubbles/textarea.go deleted file mode 100644 index b81a202..0000000 --- a/bubbles/textarea.go +++ /dev/null @@ -1,31 +0,0 @@ -package bubbles - -import ( - "charm.land/bubbles/v2/textarea" - "charm.land/lipgloss/v2" - - "github.com/anotherhadi/ilovetui/style" -) - -func NewTextarea(showLineNumbers bool) textarea.Model { - ta := textarea.New() - ta.Prompt = "" - ta.ShowLineNumbers = showLineNumbers - ta.CharLimit = 0 - ta.EndOfBufferCharacter = '~' - ts := ta.Styles() - ts.Focused.Base = lipgloss.NewStyle() - ts.Blurred.Base = lipgloss.NewStyle() - ts.Focused.Text = lipgloss.NewStyle().Foreground(style.S.Text) - ts.Focused.CursorLine = lipgloss.NewStyle().Background(style.S.Selection).Foreground(style.S.Text) - ts.Focused.CursorLineNumber = lipgloss.NewStyle().Background(style.S.Selection).Foreground(style.S.Primary).Bold(true) - ts.Focused.LineNumber = lipgloss.NewStyle().Foreground(style.S.Subtle) - ts.Focused.Placeholder = lipgloss.NewStyle().Foreground(style.S.Subtle) - ts.Focused.EndOfBuffer = lipgloss.NewStyle().Foreground(style.S.SubtleBg) - ts.Blurred.Text = lipgloss.NewStyle().Foreground(style.S.Muted) - ts.Blurred.LineNumber = lipgloss.NewStyle().Foreground(style.S.SubtleBg) - ts.Blurred.Placeholder = lipgloss.NewStyle().Foreground(style.S.Subtle) - ts.Blurred.EndOfBuffer = lipgloss.NewStyle().Foreground(style.S.SubtleBg) - ta.SetStyles(ts) - return ta -} diff --git a/bubbles/textinput.go b/bubbles/textinput.go deleted file mode 100644 index 0bae5f4..0000000 --- a/bubbles/textinput.go +++ /dev/null @@ -1,27 +0,0 @@ -package bubbles - -import ( - "charm.land/bubbles/v2/textinput" - - "github.com/anotherhadi/ilovetui/style" -) - -func NewTextInput() textinput.Model { - t := textinput.New() - t.SetStyles(themedTextInputStyles()) - return t -} - -func themedTextInputStyles() textinput.Styles { - s := textinput.DefaultStyles(true) - s.Focused.Text = s.Focused.Text.Foreground(style.S.Text) - s.Focused.Placeholder = s.Focused.Placeholder.Foreground(style.S.Subtle) - s.Focused.Suggestion = s.Focused.Suggestion.Foreground(style.S.Subtle) - s.Focused.Prompt = s.Focused.Prompt.Foreground(style.S.Primary) - s.Blurred.Text = s.Blurred.Text.Foreground(style.S.Muted) - s.Blurred.Placeholder = s.Blurred.Placeholder.Foreground(style.S.Subtle) - s.Blurred.Suggestion = s.Blurred.Suggestion.Foreground(style.S.Subtle) - s.Blurred.Prompt = s.Blurred.Prompt.Foreground(style.S.Subtle) - s.Cursor.Color = style.S.Primary - return s -} diff --git a/bubbles/viewport.go b/bubbles/viewport.go deleted file mode 100644 index 8fa8892..0000000 --- a/bubbles/viewport.go +++ /dev/null @@ -1,47 +0,0 @@ -package bubbles - -import ( - "charm.land/bubbles/v2/viewport" - "charm.land/lipgloss/v2" - - "github.com/anotherhadi/ilovetui/style" -) - -const ViewportGutterWidth = 2 - -func NewViewport() viewport.Model { - vp := viewport.New() - vp.MouseWheelEnabled = false - return vp -} - -func ViewportView(vp *viewport.Model) string { - height := vp.Height() - total := vp.TotalLineCount() - blank := lipgloss.NewStyle().Width(ViewportGutterWidth).Render("") - - if height <= 0 || total <= height { - vp.LeftGutterFunc = func(viewport.GutterContext) string { return blank } - return vp.View() - } - - yOffset := vp.YOffset() - thumbSize := max(1, height*height/total) - thumbStart := 0 - if maxOffset := total - height; maxOffset > 0 { - thumbStart = yOffset * (height - thumbSize) / maxOffset - } - - trackStyle := lipgloss.NewStyle().Foreground(style.S.Subtle) - thumbStyle := lipgloss.NewStyle().Foreground(style.S.Primary) - - vp.LeftGutterFunc = func(ctx viewport.GutterContext) string { - pos := ctx.Index - yOffset - if pos >= thumbStart && pos < thumbStart+thumbSize { - return thumbStyle.Render("█") + " " - } - return trackStyle.Render("│") + " " - } - - return vp.View() -} diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..7812986 --- /dev/null +++ b/bun.lock @@ -0,0 +1,271 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "ilovetui-opentui", + "dependencies": { + "yaml": "^2.8.1", + "zod": "^4.5.4", + }, + "devDependencies": { + "@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/keymap", + "@opentui/solid", + "opentui-spinner", + "solid-js", + ], + }, + }, + "packages": { + "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], + + "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "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/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="], + + "@babel/core": ["@babel/core@7.28.0", "", { "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/generator@7.29.8", "", { "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/helper-annotate-as-pure@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "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/helper-create-class-features-plugin@7.29.7", "", { "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/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], + + "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "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/helper-optimise-call-expression@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], + + "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.29.7", "", { "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/helper-skip-transparent-expression-wrappers@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="], + + "@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="], + + "@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="], + + "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.29.7", "", { "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/plugin-syntax-typescript@7.29.7", "", { "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/plugin-transform-modules-commonjs@7.29.7", "", { "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/plugin-transform-typescript@7.29.7", "", { "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/preset-typescript@7.27.1", "", { "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/template@7.29.7", "", { "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/traverse@7.29.8", "", { "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/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@opentui/core": ["@opentui/core@0.5.9", "", { "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/core-darwin-arm64@0.5.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-YcpWGD8GwoO3UYYw0kLDI4qof3ElSwWW5M3fNG+Kw87BSjCa5frZqak5xebdZ/XU5Xsk+BjgbBsxbzo9yrIjZQ=="], + + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.5.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-xgdZwgcwlDCqi0WPCS1d0GVSd+oKDFX+UVE1ZPei8u8W3pirXGSExxVHNku7QbEYCRvpCeMtoKIa6xN9cVTsoQ=="], + + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.5.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-vHBiJp69wMaL+fRMDpnXCCIpPyQRPZEWCWBatnblUUGIWUseqeQlaPow6ljHSbgAd/GjygAf4tFCyTqNT2ezZA=="], + + "@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.5.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-A7GW4NCTInj2vy3X2gVO2Sm8jllgdLxwPdOklPqyl+xqivQtyG9qwS5YXoCk9fBJt10ORhYKETEjYEnIE5xvMQ=="], + + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.5.9", "", { "os": "linux", "cpu": "x64" }, "sha512-rdw7QVopVHwj6fMrJc7hPGVaYg2V5NBlW3MUfkrQBlUlo/87OC4aYDAh5oRIL1CIFD9LjDdh7hwmUNbn/K9Pcw=="], + + "@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.5.9", "", { "os": "linux", "cpu": "x64" }, "sha512-J4wQs1OMPZ4hR93Op1C/BLFmIta2mUJm4M7djevlgcWcal4RSNO4V8QvUXfogWGci+ADZqEmX2osQQDY5NVJpw=="], + + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.5.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-OnmEsGLWwsHrm6sTsWSSesu4LGjSV1JNWqCsEItbuitWaqH6rXdSahSc5jMFsS57bZxUyaRz01yAqiZzUSXnVQ=="], + + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.5.9", "", { "os": "win32", "cpu": "x64" }, "sha512-/CnAIfKL7+ZeGLyZoXV5zS71Nd8Zn97RUir2DAIY05MJozmfg5s7XOAdUNYuK1cg5mbwlAuqeAsfbuWUAEe15g=="], + + "@opentui/keymap": ["@opentui/keymap@0.5.9", "", { "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/solid@0.5.9", "", { "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/bun@1.4.0", "", { "dependencies": { "bun-types": "1.4.0" } }, "sha512-K+lZULY23vRgK/CfTjFIV+tyifaNdSMlPh9j+6mQ/cLfpOznLyAuzgV/JQysyECpkBQLVMSyvjlr2fBUSA9wFQ=="], + + "@types/node": ["@types/node@26.4.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ=="], + + "ansi-regex": ["ansi-regex@6.3.0", "", {}, "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ=="], + + "babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.10", "", { "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": ["babel-plugin-module-resolver@5.0.2", "", { "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": ["babel-preset-solid@1.9.12", "", { "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": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.11.20", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw=="], + + "brace-expansion": ["brace-expansion@2.1.4", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="], + + "browserslist": ["browserslist@4.28.8", "", { "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": ["bun-ffi-structs@0.3.1", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-3gM7PpVWLyrwxWjcilSiGuhWanhZivvo6l0u573NziPH6f/gwk6McbaYgn7oJWov6pKGRTDbrg94W5DcJsKTtQ=="], + + "bun-types": ["bun-types@1.4.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-iIKw23BspnQQYd3prITOBxeUsxBHnwzX6YJfGMuNOZzeNcMmVqzIIVGRm1l69ogaPQmb4wB6BN8mA5bE9YuC5Q=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001810", "", {}, "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg=="], + + "cli-spinners": ["cli-spinners@3.4.0", "", {}, "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.417", "", {}, "sha512-4T+DTDWuMPM4aHlHwWdAVCVWwp7LDilnhzkj+c/Lbj91XSQrLuOmZSLtS9Q4iIqjlPUbPOnC624zDVVHCHaolQ=="], + + "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + + "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "find-babel-config": ["find-babel-config@2.1.2", "", { "dependencies": { "json5": "^2.2.3" } }, "sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg=="], + + "find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="], + + "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="], + + "glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="], + + "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], + + "html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="], + + "is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="], + + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="], + + "minimatch": ["minimatch@8.0.7", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg=="], + + "minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "node-releases": ["node-releases@2.0.54", "", {}, "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ=="], + + "opentui-spinner": ["opentui-spinner@0.0.7", "", { "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": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + + "p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="], + + "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], + + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + + "path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="], + + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "pkg-up": ["pkg-up@3.1.0", "", { "dependencies": { "find-up": "^3.0.0" } }, "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA=="], + + "reselect": ["reselect@4.1.8", "", {}, "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ=="], + + "resolve": ["resolve@1.22.12", "", { "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": ["s-js@0.4.9", "", {}, "sha512-RtpOm+cM6O0sHg6IA70wH+UC3FZcND+rccBZpBAHzlUgNO2Bm5BN+FnM8+OBxzXdwpKWFwX11JGF0MFRkhSoIQ=="], + + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "seroval": ["seroval@1.5.6", "", {}, "sha512-rVQVWjjSvlINzaQPZH5JFqsqEsIWdTxY3iJZCnTL/5gQbXIRooVZKI60tVCkOVfzcRPejboxO2t0P89dg5mQaA=="], + + "seroval-plugins": ["seroval-plugins@1.5.6", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-HXuLAX2pu/UByPpaeo/TaMfvMIi+1QqIoPJYCcAtU8QkVNwgR6MPlGuCQTErV1JwraaMbYaWVIBX7mppzGLATQ=="], + + "solid-js": ["solid-js@1.9.12", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-QzKaSJq2/iDrWR1As6MHZQ8fQkdOBf8GReYb7L5iKwMGceg7HxDcaOHk0at66tNgn9U2U7dXo8ZZpLIAmGMzgw=="], + + "string-width": ["string-width@7.2.0", "", { "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": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], + + "update-browserslist-db": ["update-browserslist-db@1.3.2", "", { "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": ["web-tree-sitter@0.25.10", "", { "peerDependencies": { "@types/emscripten": "^1.40.0" }, "optionalPeers": ["@types/emscripten"] }, "sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA=="], + + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], + + "zod": ["zod@4.5.4", "", {}, "sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA=="], + + "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], + + "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + + "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "path-scurry/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + } +} diff --git a/drawer/README.md b/drawer/README.md deleted file mode 100644 index 58a3652..0000000 --- a/drawer/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# drawer - -A full-height panel flush against the left or right edge of an already-rendered background, on top -of it dimmed. The sidebar/drawer equivalent of [`modal`](../modal/README.md), which it otherwise -mirrors closely: same `tea.Msg`-triggered stack (`ShowMsg`/`Show`), same composite-over-a-string -`Render`, same rule that content is a `tea.Model`. - -- `WithSide(Left|Right)` and `WithWidth` are per-drawer `Show` options; width otherwise shrinks to - fit content, capped by the `Model`'s `WithMaxWidth`. -- The stack is a plain LIFO. Only the topmost drawer is updated; `Close()` closes it. -- `View(width, height)` renders on a blank background of that size, for use as a standalone pane. - -See `examples/drawer`. diff --git a/drawer/content.go b/drawer/content.go deleted file mode 100644 index 739cf38..0000000 --- a/drawer/content.go +++ /dev/null @@ -1,61 +0,0 @@ -package drawer - -import tea "charm.land/bubbletea/v2" - -type Side int - -const ( - Left Side = iota - Right -) - -type Drawer struct { - Title string - Content tea.Model - Side Side - Width int - Style *Styles -} - -type DrawerOption func(*Drawer) - -func WithSide(s Side) DrawerOption { - return func(d *Drawer) { d.Side = s } -} - -func WithWidth(w int) DrawerOption { - return func(d *Drawer) { d.Width = w } -} - -func WithDrawerStyle(s Styles) DrawerOption { - return func(d *Drawer) { d.Style = &s } -} - -func newDrawer(title string, content tea.Model, opts ...DrawerOption) Drawer { - d := Drawer{Title: title, Content: content} - for _, opt := range opts { - opt(&d) - } - return d -} - -type ShowMsg struct{ Drawer Drawer } - -func Show(title string, content tea.Model, opts ...DrawerOption) tea.Cmd { - d := newDrawer(title, content, opts...) - return func() tea.Msg { return ShowMsg{Drawer: d} } -} - -type DismissMsg struct{} - -func Close() tea.Cmd { - return func() tea.Msg { return DismissMsg{} } -} - -type text string - -func (t text) Init() tea.Cmd { return nil } -func (t text) Update(tea.Msg) (tea.Model, tea.Cmd) { return t, nil } -func (t text) View() tea.View { return tea.NewView(string(t)) } - -func Text(s string) tea.Model { return text(s) } diff --git a/drawer/drawer.go b/drawer/drawer.go deleted file mode 100644 index f442712..0000000 --- a/drawer/drawer.go +++ /dev/null @@ -1,75 +0,0 @@ -package drawer - -import tea "charm.land/bubbletea/v2" - -type Model struct { - drawers []Drawer - nextID int - maxWidth int - styles Styles -} - -type Option func(*Model) - -func WithMaxWidth(w int) Option { - return func(m *Model) { m.maxWidth = w } -} - -func WithStyles(s Styles) Option { - return func(m *Model) { m.styles = s } -} - -func New(opts ...Option) Model { - m := Model{ - maxWidth: 30, - styles: DefaultStyles(), - } - for _, opt := range opts { - opt(&m) - } - return m -} - -func (m Model) Init() tea.Cmd { return nil } - -func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { - switch msg := msg.(type) { - case ShowMsg: - return m.show(msg.Drawer) - case DismissMsg: - return m.pop(), nil - } - return m.updateTop(msg) -} - -func (m Model) updateTop(msg tea.Msg) (Model, tea.Cmd) { - i := len(m.drawers) - 1 - if i < 0 || m.drawers[i].Content == nil { - return m, nil - } - var cmd tea.Cmd - m.drawers[i].Content, cmd = m.drawers[i].Content.Update(msg) - return m, cmd -} - -func (m Model) Open() bool { return len(m.drawers) > 0 } - -func (m Model) show(d Drawer) (Model, tea.Cmd) { - m.drawers = append(m.drawers, d) - return m, initContent(d) -} - -func initContent(d Drawer) tea.Cmd { - if d.Content == nil { - return nil - } - return d.Content.Init() -} - -func (m Model) pop() Model { - if len(m.drawers) == 0 { - return m - } - m.drawers = m.drawers[:len(m.drawers)-1] - return m -} diff --git a/drawer/render.go b/drawer/render.go deleted file mode 100644 index 3a9f880..0000000 --- a/drawer/render.go +++ /dev/null @@ -1,115 +0,0 @@ -package drawer - -import ( - "image/color" - "strings" - - "charm.land/lipgloss/v2" - "github.com/charmbracelet/x/ansi" - - "github.com/anotherhadi/ilovetui/style" -) - -func (m Model) Render(background string) string { - if len(m.drawers) == 0 { - return background - } - - result := background - for _, d := range m.drawers { - w, h := lipgloss.Width(result), lipgloss.Height(result) - if w <= 0 || h <= 0 { - return result - } - result = m.renderOne(d, result, w, h) - } - return result -} - -func (m Model) View(width, height int) string { - return m.Render(blank(width, height)) -} - -func (m Model) renderOne(d Drawer, background string, w, h int) string { - s := m.styles - if d.Style != nil { - s = *d.Style - } - - box := m.renderBox(d, s, w, h) - bw := lipgloss.Width(box) - x := 0 - if d.Side == Right { - x = max(w-bw, 0) - } - - return style.Overlay(dim(background, s.DimColor), box, x, 0) -} - -func dim(s string, c color.Color) string { - return lipgloss.NewStyle().Foreground(c).Render(ansi.Strip(s)) -} - -func (m Model) renderBox(d Drawer, s Styles, bgW, bgH int) string { - widthCap := m.maxWidth - if d.Width > 0 { - widthCap = d.Width - } - maxW := effectiveMax(widthCap, bgW) - - body := contentView(d) - inner := contentWidth(d, body, maxW) - content := s.Content.Width(inner).Render(body) - - boxWidth := inner + 4 - - return style.RenderWithTitle(s.Border, s.Title.Render(d.Title), content, boxWidth, bgH) -} - -func contentView(d Drawer) string { - if d.Content == nil { - return "" - } - return d.Content.View().Content -} - -func contentWidth(d Drawer, body string, maxWidth int) int { - capped := max(maxWidth-4, 1) - if d.Width > 0 { - return capped - } - natural := max(naturalWidth(body), lipgloss.Width(d.Title), 1) - return min(natural, capped) -} - -func naturalWidth(content string) int { - w := 0 - for _, line := range strings.Split(content, "\n") { - if lw := lipgloss.Width(line); lw > w { - w = lw - } - } - return w -} - -func effectiveMax(configured, fits int) int { - if fits < 1 { - fits = 1 - } - if configured > 0 && configured < fits { - return configured - } - return fits -} - -func blank(width, height int) string { - if width <= 0 || height <= 0 { - return "" - } - line := strings.Repeat(" ", width) - lines := make([]string, height) - for i := range lines { - lines[i] = line - } - return strings.Join(lines, "\n") -} diff --git a/drawer/render_test.go b/drawer/render_test.go deleted file mode 100644 index 101dd90..0000000 --- a/drawer/render_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package drawer - -import ( - "strings" - "testing" - - "charm.land/lipgloss/v2" -) - -func background(w, h int) string { - return blank(w, h) -} - -func TestRenderLeftFlushToLeftEdge(t *testing.T) { - m := New(WithMaxWidth(10)) - m, _ = m.Update(ShowMsg{Drawer: newDrawer("Nav", Text("hi"), WithSide(Left))}) - - out := m.Render(background(40, 10)) - lines := strings.Split(out, "\n") - if len(lines) != 10 { - t.Fatalf("expected 10 lines, got %d", len(lines)) - } - if lipgloss.Width(out) == 0 { - t.Fatalf("expected non-empty render") - } - - if len([]rune(lines[0])) == 0 { - t.Fatalf("expected a rendered top border line") - } -} - -func TestRenderRightFlushToRightEdge(t *testing.T) { - m := New(WithMaxWidth(10)) - m, _ = m.Update(ShowMsg{Drawer: newDrawer("Inspector", Text("hi"), WithSide(Right))}) - - out := m.Render(background(40, 10)) - if lipgloss.Width(out) != 40 { - t.Fatalf("expected full background width 40, got %d", lipgloss.Width(out)) - } -} - -func TestSpansFullBackgroundHeight(t *testing.T) { - m := New() - m, _ = m.Update(ShowMsg{Drawer: newDrawer("Nav", Text("hi"))}) - - out := m.Render(background(40, 12)) - if lipgloss.Height(out) != 12 { - t.Fatalf("expected full height 12, got %d", lipgloss.Height(out)) - } -} - -func TestFixedWidthHonored(t *testing.T) { - m := New(WithMaxWidth(50)) - m, _ = m.Update(ShowMsg{Drawer: newDrawer("Nav", Text("x"), WithWidth(20))}) - - box := m.renderBox(m.drawers[0], m.styles, 80, 10) - if got := lipgloss.Width(box); got != 20 { - t.Fatalf("expected fixed box width 20, got %d", got) - } -} - -func TestNoDrawersReturnsBackgroundUnchanged(t *testing.T) { - m := New() - bg := background(10, 5) - if got := m.Render(bg); got != bg { - t.Fatalf("expected background unchanged when no drawer is open") - } -} diff --git a/drawer/styles.go b/drawer/styles.go deleted file mode 100644 index 74eb2f1..0000000 --- a/drawer/styles.go +++ /dev/null @@ -1,25 +0,0 @@ -package drawer - -import ( - "image/color" - - "charm.land/lipgloss/v2" - - "github.com/anotherhadi/ilovetui/style" -) - -type Styles struct { - Border lipgloss.Style - Title lipgloss.Style - Content lipgloss.Style - DimColor color.Color -} - -func DefaultStyles() Styles { - return Styles{ - Border: style.S.PanelFocused.Padding(0, 1), - Title: lipgloss.NewStyle().Bold(true).Foreground(style.S.Primary), - Content: lipgloss.NewStyle().Foreground(style.S.Text), - DimColor: style.S.Subtle, - } -} diff --git a/examples/bubbles/main.go b/examples/bubbles/main.go deleted file mode 100644 index 274bc2a..0000000 --- a/examples/bubbles/main.go +++ /dev/null @@ -1,48 +0,0 @@ -package main - -import ( - "fmt" - "os" - - "charm.land/bubbles/v2/spinner" - "charm.land/bubbles/v2/textinput" - tea "charm.land/bubbletea/v2" - "charm.land/lipgloss/v2" - - "github.com/anotherhadi/ilovetui/bubbles" -) - -type model struct { - input textinput.Model - spinner spinner.Model -} - -func newModel() model { - ti := bubbles.NewTextInput() - ti.Placeholder = "type something" - ti.Focus() - return model{input: ti, spinner: bubbles.NewSpinner()} -} - -func (m model) Init() tea.Cmd { return m.spinner.Tick } - -func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - if k, ok := msg.(tea.KeyPressMsg); ok && k.String() == "ctrl+c" { - return m, tea.Quit - } - var inputCmd, spinnerCmd tea.Cmd - m.input, inputCmd = m.input.Update(msg) - m.spinner, spinnerCmd = m.spinner.Update(msg) - return m, tea.Batch(inputCmd, spinnerCmd) -} - -func (m model) View() tea.View { - return tea.NewView(lipgloss.JoinHorizontal(lipgloss.Center, m.spinner.View(), " ", m.input.View())) -} - -func main() { - if _, err := tea.NewProgram(newModel()).Run(); err != nil { - fmt.Println("Error running program:", err) - os.Exit(1) - } -} diff --git a/examples/drawer/main.go b/examples/drawer/main.go deleted file mode 100644 index 1acbe14..0000000 --- a/examples/drawer/main.go +++ /dev/null @@ -1,55 +0,0 @@ -package main - -import ( - "fmt" - "os" - - tea "charm.land/bubbletea/v2" - "charm.land/lipgloss/v2" - - "github.com/anotherhadi/ilovetui/drawer" -) - -type model struct { - d drawer.Model - width, height int -} - -func newModel() model { return model{d: drawer.New()} } - -func (m model) Init() tea.Cmd { return m.d.Init() } - -func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case tea.WindowSizeMsg: - m.width, m.height = msg.Width, msg.Height - case tea.KeyPressMsg: - switch msg.String() { - case "q": - return m, tea.Quit - case "l": - return m, drawer.Show("Nav", drawer.Text("Home\nSettings"), drawer.WithSide(drawer.Left)) - case "esc": - if m.d.Open() { - return m, drawer.Close() - } - } - } - var cmd tea.Cmd - m.d, cmd = m.d.Update(msg) - return m, cmd -} - -func (m model) View() tea.View { - background := lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, "l: open q: quit") - view := tea.NewView(m.d.Render(background)) - view.AltScreen = true - return view -} - -func main() { - if _, err := tea.NewProgram(newModel()).Run(); err != nil { - fmt.Println("Error running program:", err) - os.Exit(1) - } -} diff --git a/examples/helpbar/main.go b/examples/helpbar/main.go deleted file mode 100644 index 682e0d8..0000000 --- a/examples/helpbar/main.go +++ /dev/null @@ -1,73 +0,0 @@ -package main - -import ( - "fmt" - "os" - - "charm.land/bubbles/v2/key" - tea "charm.land/bubbletea/v2" - "charm.land/lipgloss/v2" - - "github.com/anotherhadi/ilovetui/helpbar" -) - -type keyMap struct { - Inc, Help, Quit key.Binding -} - -func defaultKeyMap() keyMap { - return keyMap{ - Inc: key.NewBinding(key.WithKeys("+"), key.WithHelp("+", "increment")), - Help: key.NewBinding(key.WithKeys("?"), key.WithHelp("?", "help")), - Quit: key.NewBinding(key.WithKeys("q"), key.WithHelp("q", "quit")), - } -} - -type model struct { - help helpbar.Model - keys keyMap - n, w int -} - -func newModel() model { - keys := defaultKeyMap() - return model{ - keys: keys, - help: helpbar.New(helpbar.WithToggle(keys.Help), helpbar.WithGlobal(keys.Quit)), - } -} - -func (m model) Init() tea.Cmd { return nil } - -func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case tea.WindowSizeMsg: - m.w = msg.Width - m.help.SetWidth(m.w) - case tea.KeyPressMsg: - switch { - case key.Matches(msg, m.keys.Quit): - return m, tea.Quit - case key.Matches(msg, m.keys.Inc): - m.n++ - default: - m.help, _ = m.help.Update(msg) - } - } - return m, nil -} - -func (m model) View() tea.View { - bar := m.help.View(m.keys.Inc) - body := lipgloss.Place(m.w, 1, lipgloss.Center, lipgloss.Top, fmt.Sprintf("count: %d", m.n)) - view := tea.NewView(lipgloss.JoinVertical(lipgloss.Left, body, bar)) - view.AltScreen = true - return view -} - -func main() { - if _, err := tea.NewProgram(newModel()).Run(); err != nil { - fmt.Println("Error running program:", err) - os.Exit(1) - } -} diff --git a/examples/modal/main.go b/examples/modal/main.go deleted file mode 100644 index 8b3174a..0000000 --- a/examples/modal/main.go +++ /dev/null @@ -1,55 +0,0 @@ -package main - -import ( - "fmt" - "os" - - tea "charm.land/bubbletea/v2" - "charm.land/lipgloss/v2" - - "github.com/anotherhadi/ilovetui/modal" -) - -type model struct { - m modal.Model - width, height int -} - -func newModel() model { return model{m: modal.New()} } - -func (m model) Init() tea.Cmd { return m.m.Init() } - -func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case tea.WindowSizeMsg: - m.width, m.height = msg.Width, msg.Height - case tea.KeyPressMsg: - switch msg.String() { - case "q": - return m, tea.Quit - case "o": - return m, modal.Show("Hello", modal.Text("This is a modal.\n\nesc: close")) - case "esc": - if m.m.Open() { - return m, modal.Close() - } - } - } - var cmd tea.Cmd - m.m, cmd = m.m.Update(msg) - return m, cmd -} - -func (m model) View() tea.View { - background := lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, "o: open q: quit") - view := tea.NewView(m.m.Render(background)) - view.AltScreen = true - return view -} - -func main() { - if _, err := tea.NewProgram(newModel()).Run(); err != nil { - fmt.Println("Error running program:", err) - os.Exit(1) - } -} diff --git a/examples/notification/main.go b/examples/notification/main.go deleted file mode 100644 index c490d82..0000000 --- a/examples/notification/main.go +++ /dev/null @@ -1,51 +0,0 @@ -package main - -import ( - "fmt" - "os" - - tea "charm.land/bubbletea/v2" - "charm.land/lipgloss/v2" - - "github.com/anotherhadi/ilovetui/notification" -) - -type model struct { - notif notification.Model - width, height int -} - -func newModel() model { return model{notif: notification.New()} } - -func (m model) Init() tea.Cmd { return m.notif.Init() } - -func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case tea.WindowSizeMsg: - m.width, m.height = msg.Width, msg.Height - case tea.KeyPressMsg: - switch msg.String() { - case "q": - return m, tea.Quit - case "s": - return m, notification.Show("Saved", "Config written to disk.", notification.Success) - } - } - var cmd tea.Cmd - m.notif, cmd = m.notif.Update(msg) - return m, cmd -} - -func (m model) View() tea.View { - background := lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, "s: show q: quit") - view := tea.NewView(m.notif.Render(background)) - view.AltScreen = true - return view -} - -func main() { - if _, err := tea.NewProgram(newModel()).Run(); err != nil { - fmt.Println("Error running program:", err) - os.Exit(1) - } -} diff --git a/examples/style/main.go b/examples/style/main.go deleted file mode 100644 index a4d5241..0000000 --- a/examples/style/main.go +++ /dev/null @@ -1,42 +0,0 @@ -package main - -import ( - "fmt" - "os" - - tea "charm.land/bubbletea/v2" - - "github.com/anotherhadi/ilovetui/style" -) - -type model struct{ focused bool } - -func (m model) Init() tea.Cmd { return nil } - -func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case tea.KeyPressMsg: - switch msg.String() { - case "q": - return m, tea.Quit - case "tab": - m.focused = !m.focused - } - } - return m, nil -} - -func (m model) View() tea.View { - box := style.S.Panel - if m.focused { - box = style.S.PanelFocused - } - return tea.NewView(style.RenderWithTitle(box, "Panel", "tab: toggle focus q: quit", 30, 5)) -} - -func main() { - if _, err := tea.NewProgram(model{}).Run(); err != nil { - fmt.Println("Error running program:", err) - os.Exit(1) - } -} diff --git a/examples/tabs/main.go b/examples/tabs/main.go deleted file mode 100644 index 1bd20b8..0000000 --- a/examples/tabs/main.go +++ /dev/null @@ -1,55 +0,0 @@ -package main - -import ( - "fmt" - "os" - - tea "charm.land/bubbletea/v2" - - "github.com/anotherhadi/ilovetui/tabs" -) - -type pane string - -func (p pane) Init() tea.Cmd { return nil } -func (p pane) Update(tea.Msg) (tabs.Tab, tea.Cmd) { return p, nil } -func (p pane) View() string { return string(p) } - -type model struct{ tabs tabs.Model } - -func newModel() model { - items := []tabs.Item{ - {Title: "First", Model: pane("first content")}, - {Title: "Second", Model: pane("second content")}, - {Title: "Third", Model: pane("third content")}, - } - return model{tabs: tabs.New(items)} -} - -func (m model) Init() tea.Cmd { return m.tabs.Init() } - -func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - if k, ok := msg.(tea.KeyPressMsg); ok && k.String() == "q" { - return m, tea.Quit - } - if s, ok := msg.(tea.WindowSizeMsg); ok { - m.tabs.SetSize(s.Width, s.Height) - msg = tea.WindowSizeMsg{Width: m.tabs.ContentWidth(), Height: m.tabs.ContentHeight()} - } - var cmd tea.Cmd - m.tabs, cmd = m.tabs.Update(msg) - return m, cmd -} - -func (m model) View() tea.View { - view := tea.NewView(m.tabs.View()) - view.AltScreen = true - return view -} - -func main() { - if _, err := tea.NewProgram(newModel()).Run(); err != nil { - fmt.Println("Error running program:", err) - os.Exit(1) - } -} diff --git a/flake.lock b/flake.lock deleted file mode 100644 index a96ad5c..0000000 --- a/flake.lock +++ /dev/null @@ -1,87 +0,0 @@ -{ - "nodes": { - "flake-compat": { - "flake": false, - "locked": { - "lastModified": 1767039857, - "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=", - "owner": "NixOS", - "repo": "flake-compat", - "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", - "type": "github" - }, - "original": { - "owner": "NixOS", - "repo": "flake-compat", - "type": "github" - } - }, - "git-hooks": { - "inputs": { - "flake-compat": "flake-compat", - "gitignore": "gitignore", - "nixpkgs": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1778507602, - "narHash": "sha256-kTwur1wV+01SdqskVMSo6JMEpg71ps3HpbFY2GsflKs=", - "owner": "cachix", - "repo": "git-hooks.nix", - "rev": "61ab0e80d9c7ab14c256b5b453d8b3fb0189ba0a", - "type": "github" - }, - "original": { - "owner": "cachix", - "repo": "git-hooks.nix", - "type": "github" - } - }, - "gitignore": { - "inputs": { - "nixpkgs": [ - "git-hooks", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1709087332, - "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", - "owner": "hercules-ci", - "repo": "gitignore.nix", - "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", - "type": "github" - }, - "original": { - "owner": "hercules-ci", - "repo": "gitignore.nix", - "type": "github" - } - }, - "nixpkgs": { - "locked": { - "lastModified": 1779560665, - "narHash": "sha256-tpyBcxPpcQb8ukyNF7DoCwfSY3VPsxHoYwj00Cayv5o=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "64c08a7ca051951c8eae34e3e3cb1e202fe36786", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "root": { - "inputs": { - "git-hooks": "git-hooks", - "nixpkgs": "nixpkgs" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/flake.nix b/flake.nix deleted file mode 100644 index c49b9ee..0000000 --- a/flake.nix +++ /dev/null @@ -1,30 +0,0 @@ -{ - description = "A shared Base16 theme, a themed wrapper around every official component, and a small set of custom Bubble Tea components, in one Go module, so every TUI built with it shares one config file and looks consistent."; - - inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - git-hooks = { - url = "github:cachix/git-hooks.nix"; - inputs.nixpkgs.follows = "nixpkgs"; - }; - }; - - outputs = { - self, - nixpkgs, - git-hooks, - }: let - supportedSystems = ["x86_64-linux" "aarch64-linux"]; - - forAllSystems = f: - nixpkgs.lib.genAttrs supportedSystems - (system: f system (import nixpkgs {inherit system;})); - in { - devShells = forAllSystems (system: pkgs: { - default = import ./nix/shell.nix { - inherit pkgs; - gitHooksLib = git-hooks.lib.${system}; - }; - }); - }; -} diff --git a/go.mod b/go.mod deleted file mode 100644 index 0e5b916..0000000 --- a/go.mod +++ /dev/null @@ -1,43 +0,0 @@ -module github.com/anotherhadi/ilovetui - -go 1.25.8 - -require ( - charm.land/bubbles/v2 v2.1.0 - charm.land/bubbletea/v2 v2.0.2 - charm.land/glamour/v2 v2.0.0 - charm.land/lipgloss/v2 v2.0.3 - github.com/charmbracelet/x/ansi v0.11.7 - gopkg.in/yaml.v3 v3.0.1 -) - -require ( - github.com/alecthomas/chroma/v2 v2.14.0 // indirect - github.com/atotto/clipboard v0.1.4 // indirect - github.com/aymerick/douceur v0.2.0 // indirect - github.com/charmbracelet/colorprofile v0.4.3 // indirect - github.com/charmbracelet/harmonica v0.2.0 // indirect - github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect - github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect - github.com/charmbracelet/x/term v0.2.2 // indirect - github.com/charmbracelet/x/termios v0.1.1 // indirect - github.com/charmbracelet/x/windows v0.2.2 // indirect - github.com/clipperhouse/displaywidth v0.11.0 // indirect - github.com/clipperhouse/uax29/v2 v2.7.0 // indirect - github.com/dlclark/regexp2 v1.11.0 // indirect - github.com/dustin/go-humanize v1.0.1 // indirect - github.com/gorilla/css v1.0.1 // indirect - github.com/lucasb-eyer/go-colorful v1.4.0 // indirect - github.com/mattn/go-runewidth v0.0.23 // indirect - github.com/microcosm-cc/bluemonday v1.0.27 // indirect - github.com/muesli/cancelreader v0.2.2 // indirect - github.com/rivo/uniseg v0.4.7 // indirect - github.com/sahilm/fuzzy v0.1.1 // indirect - github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - github.com/yuin/goldmark v1.7.8 // indirect - github.com/yuin/goldmark-emoji v1.0.5 // indirect - golang.org/x/net v0.39.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.43.0 // indirect - golang.org/x/text v0.24.0 // indirect -) diff --git a/go.sum b/go.sum deleted file mode 100644 index 5b5d7fc..0000000 --- a/go.sum +++ /dev/null @@ -1,87 +0,0 @@ -charm.land/bubbles/v2 v2.1.0 h1:YSnNh5cPYlYjPxRrzs5VEn3vwhtEn3jVGRBT3M7/I0g= -charm.land/bubbles/v2 v2.1.0/go.mod h1:l97h4hym2hvWBVfmJDtrEHHCtkIKeTEb3TTJ4ZOB3wY= -charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0= -charm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ= -charm.land/glamour/v2 v2.0.0 h1:IDBoqLEy7Hdpb9VOXN+khLP/XSxtJy1VsHuW/yF87+U= -charm.land/glamour/v2 v2.0.0/go.mod h1:kjq9WB0s8vuUYZNYey2jp4Lgd9f4cKdzAw88FZtpj/w= -charm.land/lipgloss/v2 v2.0.3 h1:yM2zJ4Cf5Y51b7RHIwioil4ApI/aypFXXVHSwlM6RzU= -charm.land/lipgloss/v2 v2.0.3/go.mod h1:7myLU9iG/3xluAWzpY/fSxYYHCgoKTie7laxk6ATwXA= -github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= -github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= -github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= -github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= -github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= -github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= -github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= -github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= -github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= -github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o= -github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w= -github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= -github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= -github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= -github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q= -github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= -github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= -github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA= -github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98= -github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI= -github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ= -github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA= -github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I= -github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI= -github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU= -github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= -github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= -github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= -github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= -github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM= -github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k= -github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= -github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= -github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= -github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= -github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= -github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= -github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= -github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= -github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= -github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4= -github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw= -github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= -github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= -github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= -github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= -github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= -github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= -github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= -github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= -github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= -github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= -github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk= -github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= -golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= -golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= -golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/helpbar/README.md b/helpbar/README.md deleted file mode 100644 index c96dda2..0000000 --- a/helpbar/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# helpbar - -A responsive help bar: one line of key bindings that expands, on toggle, into a multi-column view -reflowed to use as many columns as the available width allows. - -- Bindings render in order: the `WithToggle` binding first, then `WithGlobal` bindings (set once), - then contextual bindings passed to `View` at render time. -- Disabled bindings (`key.Binding.SetEnabled(false)`) are dropped before layout, so the reflow never - budgets width for something that won't be drawn. -- `Height(contextual...)` always matches `lipgloss.Height` of `View` for the same arguments, so a - host can reserve exactly the right amount of space. An empty bar renders `""` and takes 0 rows. - -See `examples/helpbar`. diff --git a/helpbar/helpbar.go b/helpbar/helpbar.go deleted file mode 100644 index 481882a..0000000 --- a/helpbar/helpbar.go +++ /dev/null @@ -1,132 +0,0 @@ -package helpbar - -import ( - "strings" - - "charm.land/bubbles/v2/help" - "charm.land/bubbles/v2/key" - tea "charm.land/bubbletea/v2" - "charm.land/lipgloss/v2" - "github.com/charmbracelet/x/ansi" - - "github.com/anotherhadi/ilovetui/bubbles" -) - -type Model struct { - ShowAll bool - - help help.Model - global []key.Binding - toggle key.Binding - width int -} - -type Option func(*Model) - -func WithGlobal(bindings ...key.Binding) Option { - return func(m *Model) { m.global = bindings } -} - -func WithToggle(b key.Binding) Option { - return func(m *Model) { m.toggle = b } -} - -func WithStyles(s help.Styles) Option { - return func(m *Model) { m.help.Styles = s } -} - -func New(opts ...Option) Model { - m := Model{help: bubbles.NewHelp()} - for _, opt := range opts { - opt(&m) - } - return m -} - -func (m *Model) SetWidth(w int) { - m.width = w - m.help.SetWidth(w) -} - -func (m Model) Width() int { return m.width } - -func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { - if keyMsg, ok := msg.(tea.KeyPressMsg); ok && key.Matches(keyMsg, m.toggle) { - m.ShowAll = !m.ShowAll - } - return m, nil -} - -func (m Model) View(contextual ...key.Binding) string { - bindings := m.bindings(contextual) - if len(bindings) == 0 || m.width <= 0 { - return "" - } - var view string - if !m.ShowAll { - view = m.help.ShortHelpView(bindings) - } else { - view = m.help.FullHelpView(m.columns(bindings)) - } - return m.clampWidth(view) -} - -func (m Model) clampWidth(view string) string { - lines := strings.Split(view, "\n") - for i, line := range lines { - lines[i] = ansi.Truncate(line, m.width, "") - } - return strings.Join(lines, "\n") -} - -func (m Model) Height(contextual ...key.Binding) int { - view := m.View(contextual...) - if view == "" { - return 0 - } - return lipgloss.Height(view) -} - -func (m Model) bindings(contextual []key.Binding) []key.Binding { - all := make([]key.Binding, 0, 1+len(m.global)+len(contextual)) - if m.toggle.Enabled() { - all = append(all, m.toggle) - } - for _, b := range append(append([]key.Binding{}, m.global...), contextual...) { - if b.Enabled() { - all = append(all, b) - } - } - return all -} - -func (m Model) columns(bindings []key.Binding) [][]key.Binding { - if m.width <= 0 { - return [][]key.Binding{bindings} - } - for rows := 1; rows < len(bindings); rows++ { - groups := chunkColumns(bindings, rows) - if m.renderedWidth(groups) <= m.width { - return groups - } - } - - return chunkColumns(bindings, len(bindings)) -} - -func (m Model) renderedWidth(groups [][]key.Binding) int { - unbounded := m.help - unbounded.SetWidth(0) - return lipgloss.Width(unbounded.FullHelpView(groups)) -} - -func chunkColumns(bindings []key.Binding, rows int) [][]key.Binding { - if rows < 1 { - rows = 1 - } - groups := make([][]key.Binding, 0, (len(bindings)+rows-1)/rows) - for i := 0; i < len(bindings); i += rows { - groups = append(groups, bindings[i:min(i+rows, len(bindings))]) - } - return groups -} diff --git a/ilovetui.go b/ilovetui.go deleted file mode 100644 index 4f3b6d8..0000000 --- a/ilovetui.go +++ /dev/null @@ -1 +0,0 @@ -package ilovetui diff --git a/minsize/minsize.go b/minsize/minsize.go deleted file mode 100644 index e505b0d..0000000 --- a/minsize/minsize.go +++ /dev/null @@ -1,60 +0,0 @@ -package minsize - -import ( - "fmt" - - "charm.land/lipgloss/v2" - - "github.com/anotherhadi/ilovetui/style" -) - -type Model struct { - MinWidth int - MinHeight int - Style lipgloss.Style -} - -type Option func(*Model) - -func WithStyle(s lipgloss.Style) Option { - return func(m *Model) { m.Style = s } -} - -func New(minWidth, minHeight int, opts ...Option) Model { - m := Model{ - MinWidth: minWidth, - MinHeight: minHeight, - Style: lipgloss.NewStyle().Foreground(style.S.Muted), - } - for _, opt := range opts { - opt(&m) - } - return m -} - -func (m Model) Fits(width, height int) bool { - return width >= m.MinWidth && height >= m.MinHeight -} - -func (m Model) View(width, height int) string { - msg := m.Style.Render(m.message(width, height)) - if width < 1 || height < 1 { - return msg - } - return lipgloss.Place(width, height, lipgloss.Center, lipgloss.Center, msg) -} - -func (m Model) message(width, height int) string { - tooNarrow := width < m.MinWidth - tooShort := height < m.MinHeight - switch { - case tooNarrow && tooShort: - return fmt.Sprintf("minimum is %dx%d", m.MinWidth, m.MinHeight) - case tooNarrow: - return fmt.Sprintf("minimum width: %d", m.MinWidth) - case tooShort: - return fmt.Sprintf("minimum height: %d", m.MinHeight) - default: - return "" - } -} diff --git a/modal/README.md b/modal/README.md deleted file mode 100644 index 6c7831e..0000000 --- a/modal/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# modal - -A centered popup box on top of a dimmed background, triggered from anywhere in a bubbletea program -via an exported `tea.Msg` (`ShowMsg`/`Show`), not a direct reference to the `Model` that renders it. -Composites over an already-rendered string, so it makes no assumption about how the host builds -that string. - -- A modal's content is a `tea.Model`, not a string: it gets `Init`/`Update` while it's on top, and - can report back with its own `tea.Msg` (see `modal.Text` for a plain, non-interactive content). -- The stack is a plain LIFO with no identity. Only the topmost modal is updated; `Close()` always - closes it. -- Showing a second modal while one is open pushes it on top, nesting confirmations naturally. -- `WithMaxWidth`/`WithMaxHeight` cap growth; a modal narrower than the cap shrinks to fit instead. - -See `examples/modal`. diff --git a/modal/content.go b/modal/content.go deleted file mode 100644 index 19e4029..0000000 --- a/modal/content.go +++ /dev/null @@ -1,46 +0,0 @@ -package modal - -import tea "charm.land/bubbletea/v2" - -type Modal struct { - Title string - - Content tea.Model - - Style *Styles -} - -type ModalOption func(*Modal) - -func WithModalStyle(s Styles) ModalOption { - return func(mo *Modal) { mo.Style = &s } -} - -func newModal(title string, content tea.Model, opts ...ModalOption) Modal { - mo := Modal{Title: title, Content: content} - for _, opt := range opts { - opt(&mo) - } - return mo -} - -type ShowMsg struct{ Modal Modal } - -func Show(title string, content tea.Model, opts ...ModalOption) tea.Cmd { - mo := newModal(title, content, opts...) - return func() tea.Msg { return ShowMsg{Modal: mo} } -} - -type DismissMsg struct{} - -func Close() tea.Cmd { - return func() tea.Msg { return DismissMsg{} } -} - -type text string - -func (t text) Init() tea.Cmd { return nil } -func (t text) Update(tea.Msg) (tea.Model, tea.Cmd) { return t, nil } -func (t text) View() tea.View { return tea.NewView(string(t)) } - -func Text(s string) tea.Model { return text(s) } diff --git a/modal/modal.go b/modal/modal.go deleted file mode 100644 index 572327e..0000000 --- a/modal/modal.go +++ /dev/null @@ -1,80 +0,0 @@ -package modal - -import tea "charm.land/bubbletea/v2" - -type Model struct { - modals []Modal - maxWidth int - maxHeight int - styles Styles -} - -type Option func(*Model) - -func WithMaxWidth(w int) Option { - return func(m *Model) { m.maxWidth = w } -} - -func WithMaxHeight(h int) Option { - return func(m *Model) { m.maxHeight = h } -} - -func WithStyles(s Styles) Option { - return func(m *Model) { m.styles = s } -} - -func New(opts ...Option) Model { - m := Model{ - maxWidth: 60, - maxHeight: 20, - 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.Modal) - case DismissMsg: - return m.pop(), nil - } - return m.updateTop(msg) -} - -func (m Model) updateTop(msg tea.Msg) (Model, tea.Cmd) { - i := len(m.modals) - 1 - if i < 0 || m.modals[i].Content == nil { - return m, nil - } - var cmd tea.Cmd - m.modals[i].Content, cmd = m.modals[i].Content.Update(msg) - return m, cmd -} - -func (m Model) Open() bool { return len(m.modals) > 0 } - -func (m Model) show(mo Modal) (Model, tea.Cmd) { - m.modals = append(m.modals, mo) - return m, initContent(mo) -} - -func initContent(mo Modal) tea.Cmd { - if mo.Content == nil { - return nil - } - return mo.Content.Init() -} - -func (m Model) pop() Model { - if len(m.modals) == 0 { - return m - } - m.modals = m.modals[:len(m.modals)-1] - return m -} diff --git a/modal/render.go b/modal/render.go deleted file mode 100644 index 1553aeb..0000000 --- a/modal/render.go +++ /dev/null @@ -1,109 +0,0 @@ -package modal - -import ( - "image/color" - "strings" - - "charm.land/lipgloss/v2" - "github.com/charmbracelet/x/ansi" - - "github.com/anotherhadi/ilovetui/style" -) - -const margin = 2 - -func (m Model) Render(background string) string { - if len(m.modals) == 0 { - return background - } - - result := background - for _, mo := range m.modals { - w, h := lipgloss.Width(result), lipgloss.Height(result) - if w <= 0 || h <= 0 { - return result - } - result = m.renderOne(mo, result, w, h) - } - return result -} - -func (m Model) View(width, height int) string { - return m.Render(blank(width, height)) -} - -func (m Model) renderOne(mo Modal, background string, w, h int) string { - s := m.styles - if mo.Style != nil { - s = *mo.Style - } - - box := m.renderBox(mo, s, w, h) - bw, bh := lipgloss.Width(box), lipgloss.Height(box) - x, y := max((w-bw)/2, 0), max((h-bh)/2, 0) - - return style.Overlay(dim(background, s.DimColor), box, x, y) -} - -func dim(s string, c color.Color) string { - return lipgloss.NewStyle().Foreground(c).Render(ansi.Strip(s)) -} - -func (m Model) renderBox(mo Modal, s Styles, bgW, bgH int) string { - maxW := effectiveMax(m.maxWidth, bgW-2*margin) - maxH := effectiveMax(m.maxHeight, bgH-2*margin) - - body := contentView(mo) - inner := contentWidth(body, mo.Title, maxW) - content := s.Content.Width(inner).Render(body) - - boxWidth := inner + 4 - boxHeight := min(lipgloss.Height(content)+2, maxH) - - return style.RenderWithTitle(s.Border, s.Title.Render(mo.Title), content, boxWidth, boxHeight) -} - -func contentView(mo Modal) string { - if mo.Content == nil { - return "" - } - return mo.Content.View().Content -} - -func contentWidth(body, title string, maxWidth int) int { - natural := max(naturalWidth(body), lipgloss.Width(title), 1) - capped := max(maxWidth-4, 1) - return min(natural, capped) -} - -func naturalWidth(content string) int { - w := 0 - for _, line := range strings.Split(content, "\n") { - if lw := lipgloss.Width(line); lw > w { - w = lw - } - } - return w -} - -func effectiveMax(configured, fits int) int { - if fits < 1 { - fits = 1 - } - if configured > 0 && configured < fits { - return configured - } - return fits -} - -func blank(width, height int) string { - if width <= 0 || height <= 0 { - return "" - } - line := strings.Repeat(" ", width) - lines := make([]string, height) - for i := range lines { - lines[i] = line - } - return strings.Join(lines, "\n") -} diff --git a/modal/styles.go b/modal/styles.go deleted file mode 100644 index 6530447..0000000 --- a/modal/styles.go +++ /dev/null @@ -1,25 +0,0 @@ -package modal - -import ( - "image/color" - - "charm.land/lipgloss/v2" - - "github.com/anotherhadi/ilovetui/style" -) - -type Styles struct { - Border lipgloss.Style - Title lipgloss.Style - Content lipgloss.Style - DimColor color.Color -} - -func DefaultStyles() Styles { - return Styles{ - Border: style.S.PanelFocused.Padding(0, 1), - Title: lipgloss.NewStyle().Bold(true).Foreground(style.S.Primary), - Content: lipgloss.NewStyle().Foreground(style.S.Text), - DimColor: style.S.Subtle, - } -} diff --git a/nix/shell.nix b/nix/shell.nix deleted file mode 100644 index c787b15..0000000 --- a/nix/shell.nix +++ /dev/null @@ -1,23 +0,0 @@ -{ - pkgs, - gitHooksLib, -}: let - hooks = gitHooksLib.run { - src = ../.; - hooks = { - gofmt.enable = true; - govet.enable = true; - }; - }; -in - pkgs.mkShell { - packages = with pkgs; - [ - go - doctoc - (python3.withPackages (ps: [ps.pyte])) - ] - ++ hooks.enabledPackages; - - shellHook = hooks.shellHook; - } diff --git a/nix/tui-snapshot.py b/nix/tui-snapshot.py deleted file mode 100644 index 69af579..0000000 --- a/nix/tui-snapshot.py +++ /dev/null @@ -1,192 +0,0 @@ -#!/usr/bin/env python3 -"""Run a TUI binary in a virtual terminal (via pyte) and print its rendered -screen as plain text, for visual verification of bubbletea/lipgloss output. - -Dev tooling only, provided by the nix-shell. Not part of the Go module. - -Usage: - tui-snapshot.py [--width W] [--height H] [--wait SECONDS] [--keys 'k:delay,...'] -- [args...] - -Example: - tui-snapshot.py --keys 'l:0.2,l:0.2,q:0' -- go run ./examples/tabs -""" - -import argparse -import fcntl -import os -import pty -import select -import struct -import sys -import termios -import time - -import pyte - -class RepeatAwareScreen(pyte.Screen): - """pyte has no support for the ECMA-48 REP sequence (``CSI Ps b``, - "repeat the preceding graphic character Ps times"), which bubbletea's - renderer uses to compress runs of identical styled cells (e.g. long - border/padding runs). Left unhandled, pyte silently drops it, corrupting - the frame. This adds the missing handler.""" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._last_drawn = " " - - def draw(self, data): - super().draw(data) - if data: - self._last_drawn = data[-1] - - def repeat_last_character(self, count=1): - self.draw(self._last_drawn * (count or 1)) - - -class RepeatAwareStream(pyte.Stream): - """Also works around a pyte bug: it maps ECMA-48 HPA (Character Position - Absolute, "move cursor to column Ps") to "'" (apostrophe, 0x27) instead - of the actual standard character "`" (backtick, 0x60) - see pyte's - escape.py. Real terminal apps send the correct backtick, which pyte then - silently no-ops on since it's not in its dispatch table, leaving the - cursor stuck instead of moving it. bubbletea's renderer uses HPA (mixed - with CUF) to jump the cursor to a border's column after erasing padding - with ECH, so without this the trailing border character gets drawn right - after the leading one instead of at the far edge.""" - - csi = { - **pyte.Stream.csi, - "b": "repeat_last_character", - "`": "cursor_to_column", - } - - -KEYMAP = { - "enter": "\r", - "esc": "\x1b", - "tab": "\t", - "up": "\x1b[A", - "down": "\x1b[B", - "right": "\x1b[C", - "left": "\x1b[D", - "space": " ", -} - - -def encode_key(key): - """Resolve a --keys token to the bytes to write to the pty. Falls back to - the literal string for a plain character (e.g. 'l'), but without this, - something like 'ctrl+l' would be sent as the 6 literal characters - 'c','t','r','l','+','l' instead of the single 0x0C control byte a real - terminal would send for Ctrl-L - silently testing the wrong thing.""" - if key in KEYMAP: - return KEYMAP[key] - if key.startswith("ctrl+") and len(key) == 6 and key[5].isalpha(): - return chr(ord(key[5].lower()) - ord("a") + 1) - return key - - -def set_size(fd, rows, cols): - fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack("HHHH", rows, cols, 0, 0)) - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument("--width", type=int, default=80) - parser.add_argument("--height", type=int, default=24) - parser.add_argument( - "--wait", type=float, default=1.0, help="seconds to let the app render before the snapshot" - ) - parser.add_argument( - "--keys", - default="", - help="comma-separated key:delay pairs sent before the final snapshot, e.g. 'l:0.2,l:0.2,q:0'", - ) - parser.add_argument( - "--colors", - action="store_true", - help="also print, per row, the foreground color of each non-blank cell (for border/color bugs " - "that don't show up in the plain-text dump)", - ) - parser.add_argument("cmd", nargs=argparse.REMAINDER) - args = parser.parse_args() - - cmd = args.cmd - if cmd and cmd[0] == "--": - cmd = cmd[1:] - if not cmd: - parser.error("missing command to run") - - screen = RepeatAwareScreen(args.width, args.height) - stream = RepeatAwareStream(screen) - - # Open the pty and set its size *before* forking, so the child never - # observes a stale/default size on its first render (pyte has no way to - # recover from a corrupted initial frame drawn at the wrong width). - master_fd, slave_fd = pty.openpty() - set_size(slave_fd, args.height, args.width) - - pid = os.fork() - if pid == 0: - os.close(master_fd) - os.setsid() - fcntl.ioctl(slave_fd, termios.TIOCSCTTY, 0) - os.dup2(slave_fd, 0) - os.dup2(slave_fd, 1) - os.dup2(slave_fd, 2) - os.close(slave_fd) - os.execvp(cmd[0], cmd) - os._exit(1) - - os.close(slave_fd) - fd = master_fd - - def pump(duration): - end = time.time() + duration - while True: - remaining = end - time.time() - if remaining <= 0: - break - r, _, _ = select.select([fd], [], [], remaining) - if fd not in r: - continue - try: - data = os.read(fd, 65536) - except OSError: - break - if not data: - break - stream.feed(data.decode(errors="ignore")) - - pump(args.wait) - - for pair in filter(None, args.keys.split(",")): - key, _, delay = pair.partition(":") - try: - os.write(fd, encode_key(key).encode()) - except OSError: - break - pump(float(delay) if delay else 0.3) - - try: - os.kill(pid, 15) - os.waitpid(pid, 0) - except (ProcessLookupError, ChildProcessError): - pass - - for y, line in enumerate(screen.display): - print(line.rstrip()) - if args.colors: - row = screen.buffer[y] - cells = [] - for x in sorted(row): - ch = row[x] - if ch.data.strip(): - fg = ch.fg if ch.fg != "default" else "-" - cells.append(f"{x}:{ch.data!r}:{fg}") - if cells: - print(" " + " ".join(cells)) - - -if __name__ == "__main__": - main() diff --git a/notification/README.md b/notification/README.md deleted file mode 100644 index 63b61a0..0000000 --- a/notification/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# notification - -Toast notifications, triggered from anywhere in a bubbletea program via an exported `tea.Msg` -(`ShowMsg`/`Show`), not a direct reference to the `Model` that renders them. Composites over an -already-rendered string, so it makes no assumption about how the host builds that string. - -- Four kinds: `Info`, `Success`, `Warning`, `Error`, each with its own `style.S` color preset. -- Auto-dismisses after `DefaultDuration` (3s) unless shown with `WithDuration(0)`, which makes it - sticky; a sticky toast needs `WithID` so `Dismiss(id)` can remove it later. -- Six anchors (`Top`, `TopLeft`, `TopRight`, `Bottom`, `BottomLeft`, `BottomRight`). Toasts stack - along the anchored edge, newest closest to it. -- `WithMaxWidth` caps growth; a toast narrower than the cap shrinks to fit instead. - -See `examples/notification`. diff --git a/notification/notification.go b/notification/notification.go deleted file mode 100644 index 99ba12a..0000000 --- a/notification/notification.go +++ /dev/null @@ -1,93 +0,0 @@ -package notification - -import ( - "fmt" - "time" - - tea "charm.land/bubbletea/v2" -) - -type Model struct { - toasts []Toast - nextID int - position Position - maxWidth int - styles Styles -} - -type Option func(*Model) - -func WithPosition(p Position) Option { - return func(m *Model) { m.position = p } -} - -func WithMaxWidth(w int) Option { - return func(m *Model) { m.maxWidth = w } -} - -func WithStyles(s Styles) Option { - return func(m *Model) { m.styles = s } -} - -func New(opts ...Option) Model { - m := Model{ - position: TopRight, - maxWidth: 40, - 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.Toast) - case DismissMsg: - return m.remove(msg.ID), nil - case expireMsg: - return m.remove(msg.id), nil - } - return m, nil -} - -func (m Model) show(t Toast) (Model, tea.Cmd) { - if t.ID == "" { - t.ID = fmt.Sprintf("toast-%d", m.nextID) - m.nextID++ - } - - replaced := false - for i, existing := range m.toasts { - if existing.ID == t.ID { - m.toasts[i] = t - replaced = true - break - } - } - if !replaced { - m.toasts = append(m.toasts, t) - } - - if t.Duration <= 0 { - return m, nil - } - id := t.ID - return m, tea.Tick(t.Duration, func(time.Time) tea.Msg { - return expireMsg{id: id} - }) -} - -func (m Model) remove(id string) Model { - for i, t := range m.toasts { - if t.ID == id { - m.toasts = append(m.toasts[:i], m.toasts[i+1:]...) - break - } - } - return m -} diff --git a/notification/position.go b/notification/position.go deleted file mode 100644 index b3bcc66..0000000 --- a/notification/position.go +++ /dev/null @@ -1,48 +0,0 @@ -package notification - -type Position int - -const ( - Top Position = iota - TopLeft - TopRight - Bottom - BottomLeft - BottomRight -) - -const margin = 1 - -func placement(pos Position, w, h, sw, sh int) (x, y int) { - switch pos { - case Top: - x = (w - sw) / 2 - y = margin - case TopLeft: - x = margin - y = margin - case TopRight: - x = w - sw - margin - y = margin - case Bottom: - x = (w - sw) / 2 - y = h - sh - margin - case BottomLeft: - x = margin - y = h - sh - margin - case BottomRight: - x = w - sw - margin - y = h - sh - margin - } - if x < 0 { - x = 0 - } - if y < 0 { - y = 0 - } - return x, y -} - -func (pos Position) anchoredTop() bool { - return pos == Top || pos == TopLeft || pos == TopRight -} diff --git a/notification/render.go b/notification/render.go deleted file mode 100644 index fa600f6..0000000 --- a/notification/render.go +++ /dev/null @@ -1,123 +0,0 @@ -package notification - -import ( - "strings" - - "charm.land/lipgloss/v2" - - "github.com/anotherhadi/ilovetui/style" -) - -func (m Model) Render(background string) string { - if len(m.toasts) == 0 { - return background - } - w, h := lipgloss.Width(background), lipgloss.Height(background) - if w <= 0 || h <= 0 { - return background - } - - stack := clipToHeight(m.renderStack(effectiveMaxWidth(m.maxWidth, w)), h-2*margin, m.position.anchoredTop()) - if stack == "" { - return background - } - sw, sh := lipgloss.Width(stack), lipgloss.Height(stack) - x, y := placement(m.position, w, h, sw, sh) - - return style.Overlay(background, stack, x, y) -} - -func (m Model) View(width, height int) string { - return m.Render(blank(width, height)) -} - -func (m Model) renderStack(maxWidth int) string { - ordered := m.orderedToasts() - parts := make([]string, 0, len(ordered)*2-1) - for i, t := range ordered { - if i > 0 { - parts = append(parts, "") - } - parts = append(parts, m.renderToast(t, maxWidth)) - } - return lipgloss.JoinVertical(stackAlign(m.position), parts...) -} - -func (m Model) orderedToasts() []Toast { - if !m.position.anchoredTop() { - return m.toasts - } - ordered := make([]Toast, len(m.toasts)) - for i, t := range m.toasts { - ordered[len(m.toasts)-1-i] = t - } - return ordered -} - -func stackAlign(pos Position) lipgloss.Position { - switch pos { - case TopLeft, BottomLeft: - return lipgloss.Left - case TopRight, BottomRight: - return lipgloss.Right - default: - return lipgloss.Center - } -} - -func (m Model) renderToast(t Toast, maxWidth int) string { - k := m.styles.forKind(t) - - inner := contentWidth(t, maxWidth) - message := k.Message.Width(inner).Render(t.Message) - - boxWidth := inner + 4 - boxHeight := lipgloss.Height(message) + 2 - - return style.RenderWithTitle(k.Border, k.Title.Render(t.Title), message, boxWidth, boxHeight) -} - -func contentWidth(t Toast, maxWidth int) int { - natural := max(lipgloss.Width(t.Title), lipgloss.Width(t.Message), 1) - if maxWidth <= 0 { - return natural - } - capped := max(maxWidth-4, 1) - return min(natural, capped) -} - -func effectiveMaxWidth(configured, bgWidth int) int { - fits := max(bgWidth-2*margin, 1) - if configured > 0 && configured < fits { - return configured - } - return fits -} - -func clipToHeight(stack string, maxHeight int, anchoredTop bool) string { - lines := strings.Split(stack, "\n") - if len(lines) <= maxHeight { - return stack - } - if maxHeight <= 0 { - return "" - } - if anchoredTop { - lines = lines[:maxHeight] - } else { - lines = lines[len(lines)-maxHeight:] - } - return strings.Join(lines, "\n") -} - -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") -} diff --git a/notification/styles.go b/notification/styles.go deleted file mode 100644 index 8ca6ea0..0000000 --- a/notification/styles.go +++ /dev/null @@ -1,58 +0,0 @@ -package notification - -import ( - "image/color" - - "charm.land/lipgloss/v2" - - "github.com/anotherhadi/ilovetui/style" -) - -type KindStyle struct { - Border lipgloss.Style - Title lipgloss.Style - Message lipgloss.Style -} - -type Styles struct { - Info KindStyle - Success KindStyle - Warning KindStyle - Error KindStyle -} - -func DefaultStyles() Styles { - return Styles{ - Info: kindStyle(style.S.Primary), - Success: kindStyle(style.S.Success), - Warning: kindStyle(style.S.Warning), - Error: kindStyle(style.S.Error), - } -} - -func kindStyle(c color.Color) KindStyle { - return KindStyle{ - Border: lipgloss.NewStyle(). - Border(style.S.BorderType). - BorderForeground(c). - Padding(0, 1), - Title: lipgloss.NewStyle().Bold(true).Foreground(c), - Message: lipgloss.NewStyle().Foreground(style.S.Text), - } -} - -func (s Styles) forKind(t Toast) KindStyle { - if t.Style != nil { - return *t.Style - } - switch t.Kind { - case Success: - return s.Success - case Warning: - return s.Warning - case Error: - return s.Error - default: - return s.Info - } -} diff --git a/notification/toast.go b/notification/toast.go deleted file mode 100644 index 41f5f12..0000000 --- a/notification/toast.go +++ /dev/null @@ -1,71 +0,0 @@ -package notification - -import ( - "time" - - tea "charm.land/bubbletea/v2" -) - -type Kind int - -const ( - Info Kind = iota - Success - Warning - Error -) - -const DefaultDuration = 3 * time.Second - -type Toast struct { - ID string - Title string - Message string - Kind Kind - - Duration time.Duration - - Style *KindStyle -} - -type ToastOption func(*Toast) - -func WithID(id string) ToastOption { - return func(t *Toast) { t.ID = id } -} - -func WithDuration(d time.Duration) ToastOption { - return func(t *Toast) { t.Duration = d } -} - -func WithToastStyle(s KindStyle) ToastOption { - return func(t *Toast) { t.Style = &s } -} - -func newToast(title, message string, kind Kind, opts ...ToastOption) Toast { - t := Toast{ - Title: title, - Message: message, - Kind: kind, - Duration: DefaultDuration, - } - for _, opt := range opts { - opt(&t) - } - return t -} - -type ShowMsg struct{ Toast Toast } - -func Show(title, message string, kind Kind, opts ...ToastOption) tea.Cmd { - t := newToast(title, message, kind, opts...) - return func() tea.Msg { return ShowMsg{Toast: t} } -} - -type DismissMsg struct{ ID string } - -func Dismiss(id string) tea.Cmd { - return func() tea.Msg { return DismissMsg{ID: id} } -} - -type expireMsg struct{ id string } diff --git a/package.json b/package.json new file mode 100644 index 0000000..c2c78a3 --- /dev/null +++ b/package.json @@ -0,0 +1,61 @@ +{ + "name": "ilovetui", + "version": "0.1.0", + "module": "dist/index.js", + "type": "module", + "types": "./src/index.ts", + "exports": { + ".": { "types": "./src/index.ts", "default": "./dist/index.js" }, + "./solid": { "types": "./src/solid.ts", "default": "./dist/solid.js" }, + "./context": { "types": "./src/context/index.ts", "default": "./dist/context/index.js" }, + "./project-config": { "types": "./src/project-config.ts", "default": "./dist/project-config.js" }, + "./keymap": { "types": "./src/keymap/provider.tsx", "default": "./dist/keymap/provider.js" }, + "./components": { "types": "./src/components/index.ts", "default": "./dist/components/index.js" }, + "./components/spinner": { "types": "./src/components/Spinner.tsx", "default": "./dist/components/Spinner.js" }, + "./components/help": { "types": "./src/components/help.ts", "default": "./dist/components/help.js" } + }, + "private": true, + "files": ["dist", "src"], + "scripts": { + "build": "bun run scripts/build.ts", + "prepare": "bun run scripts/build.ts", + "typecheck": "tsc --noEmit" + }, + "peerDependencies": { + "@opentui/core": ">=0.5.0", + "@opentui/solid": ">=0.5.0", + "@opentui/keymap": ">=0.5.0", + "solid-js": ">=1.9.0", + "opentui-spinner": ">=0.0.7" + }, + "peerDependenciesMeta": { + "@opentui/core": { + "optional": true + }, + "@opentui/solid": { + "optional": true + }, + "@opentui/keymap": { + "optional": true + }, + "solid-js": { + "optional": true + }, + "opentui-spinner": { + "optional": true + } + }, + "dependencies": { + "yaml": "^2.8.1", + "zod": "^4.5.4" + }, + "devDependencies": { + "@opentui/core": "^0.5.9", + "@opentui/keymap": "^0.5.9", + "@opentui/solid": "^0.5.9", + "@types/bun": "^1.3.14", + "opentui-spinner": "^0.0.7", + "solid-js": "1.9.12", + "typescript": "^5.9.3" + } +} diff --git a/scripts/build.ts b/scripts/build.ts new file mode 100644 index 0000000..f25acd3 --- /dev/null +++ b/scripts/build.ts @@ -0,0 +1,25 @@ +import { createSolidTransformPlugin } from "@opentui/solid/bun-plugin"; + +const entrypoints = await Array.fromAsync(new Bun.Glob("**/*.{ts,tsx}").scan({ cwd: "src" })); + +await Bun.$`rm -rf dist`; + +const result = await Bun.build({ + entrypoints: entrypoints.map((path) => `src/${path}`), + outdir: "dist", + root: "src", + target: "bun", + format: "esm", + splitting: true, + plugins: [createSolidTransformPlugin()], + external: ["@opentui/core", "@opentui/solid", "@opentui/keymap", "solid-js", "opentui-spinner", "yaml", "zod"], +}); + +if (!result.success) { + for (const message of result.logs) console.error(message); + process.exit(1); +} + +await Bun.write("dist/default.yaml", Bun.file("src/default.yaml")); + +console.log(`Built ${result.outputs.length} files to dist/`); diff --git a/src/components/Badge.tsx b/src/components/Badge.tsx new file mode 100644 index 0000000..414f432 --- /dev/null +++ b/src/components/Badge.tsx @@ -0,0 +1,40 @@ +import { type ColorInput, TextAttributes } from "@opentui/core"; +import { Show } from "solid-js"; +import { theme } from "../index.ts"; + +const CAP_LEFT = ""; +const CAP_RIGHT = ""; + +export interface BadgeProps { + label: string; + color: ColorInput; + textColor?: ColorInput; + backgroundColor?: ColorInput; + bold?: boolean; + withNerdfont?: boolean; +} + +export function Badge(props: BadgeProps) { + const textColor = () => props.textColor ?? theme.background; + const backgroundColor = () => props.backgroundColor ?? theme.background; + const attributes = () => (props.bold === false ? undefined : TextAttributes.BOLD); + const nerdFonts = () => props.withNerdfont ?? theme.nerdFonts; + + return ( + + + + {CAP_LEFT} + + + + {nerdFonts() ? props.label : ` ${props.label} `} + + + + {CAP_RIGHT} + + + + ); +} diff --git a/src/components/HelpBar.tsx b/src/components/HelpBar.tsx new file mode 100644 index 0000000..93aab40 --- /dev/null +++ b/src/components/HelpBar.tsx @@ -0,0 +1,94 @@ +import { type ColorInput, CliRenderEvents, TextAttributes, type BoxRenderable } from "@opentui/core"; +import { onResize, useRenderer } from "@opentui/solid"; +import { formatCommandBindings } from "@opentui/keymap/extras"; +import { useKeymapSelector } from "@opentui/keymap/solid"; +import { createMemo, createSignal, For } from "solid-js"; +import { theme } from "../index.ts"; + +const KEY_DISPLAY = { up: "↑", down: "↓", left: "←", right: "→", enter: "Enter", escape: "Esc" }; +const SEPARATOR = " "; +const ELLIPSIS = "…"; + +export interface HelpBarProps { + accentColor?: ColorInput; + mutedColor?: ColorInput; +} + +interface HelpEntry { + command: string; + keys: string; + label: string; +} + +export function HelpBar(props: HelpBarProps = {}) { + const accent = () => props.accentColor ?? theme.primary; + const muted = () => props.mutedColor ?? theme.muted; + const renderer = useRenderer(); + const [width, setWidth] = createSignal(0); + let container: BoxRenderable | undefined; + const measure = () => { + if (!container) return; + setWidth(container.width); + }; + const remeasureNextFrame = () => renderer.once(CliRenderEvents.FRAME, measure); + onResize(remeasureNextFrame); + + const entries = useKeymapSelector((keymap): HelpEntry[] => + keymap + .getCommandEntries({ visibility: "active" }) + .filter((entry) => entry.command.shortHelp === true) + .map((entry) => ({ + command: entry.command.name, + keys: formatCommandBindings(entry.bindings, { keyNameAliases: KEY_DISPLAY }) ?? "", + label: typeof entry.command.desc === "string" ? entry.command.desc : entry.command.name, + })), + ); + + const visible = createMemo((): HelpEntry[] => { + const available = width() - 2; + const shown: HelpEntry[] = []; + let used = 0; + for (const entry of entries()) { + const sepWidth = shown.length > 0 ? SEPARATOR.length : 0; + const entryWidth = entry.keys.length + 1 + entry.label.length; + if (used + sepWidth + entryWidth <= available) { + used += sepWidth + entryWidth; + shown.push(entry); + continue; + } + const remaining = available - used - sepWidth; + const prefix = `${entry.keys} `; + if (remaining > prefix.length + ELLIPSIS.length) { + const labelBudget = remaining - prefix.length - ELLIPSIS.length; + shown.push({ ...entry, label: `${entry.label.slice(0, labelBudget)}${ELLIPSIS}` }); + } + break; + } + return shown; + }); + + return ( + { + container = el; + remeasureNextFrame(); + }} + > + + {(entry, i) => ( + <> + {i() > 0 && {SEPARATOR}} + + {entry.keys} + + {` ${entry.label}`} + + )} + + + ); +} diff --git a/src/components/HelpModal.tsx b/src/components/HelpModal.tsx new file mode 100644 index 0000000..8bb1345 --- /dev/null +++ b/src/components/HelpModal.tsx @@ -0,0 +1,45 @@ +import { type ColorInput, TextAttributes } from "@opentui/core"; +import { formatCommandBindings } from "@opentui/keymap/extras"; +import { useKeymapSelector } from "@opentui/keymap/solid"; +import { helpOpen, toggleHelp } from "../context/help.ts"; +import { Modal } from "./Modal.tsx"; + +const KEY_DISPLAY = { up: "↑", down: "↓", left: "←", right: "→", enter: "Enter", escape: "Esc" }; + +export interface HelpModalProps { + accentColor?: ColorInput; + backgroundColor?: ColorInput; + backdropColor?: ColorInput; + width?: number; +} + +interface HelpEntry { + command: string; + keys: string; + label: string; +} + +export function HelpModal(props: HelpModalProps = {}) { + const entries = useKeymapSelector((keymap): HelpEntry[] => + keymap.getCommandEntries({ visibility: "active" }).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 ( + + Keybindings + + {entries().map((entry) => `${entry.keys.padEnd(10)}${entry.label}`).join("\n")} + + ); +} diff --git a/src/components/MinSizeGuard.tsx b/src/components/MinSizeGuard.tsx new file mode 100644 index 0000000..73d268f --- /dev/null +++ b/src/components/MinSizeGuard.tsx @@ -0,0 +1,37 @@ +import type { ColorInput } from "@opentui/core"; +import { useTerminalDimensions } from "@opentui/solid"; +import { Match, Switch, type ParentProps } from "solid-js"; +import { theme } from "../index.ts"; + +export interface MinSizeGuardProps extends ParentProps { + minWidth?: number; + minHeight?: number; + backgroundColor?: ColorInput; + textColor?: ColorInput; +} + +export function MinSizeGuard(props: MinSizeGuardProps) { + const dimensions = useTerminalDimensions(); + const minWidth = () => props.minWidth ?? 80; + const minHeight = () => props.minHeight ?? 24; + const tooSmall = () => dimensions().width < minWidth() || dimensions().height < minHeight(); + + return ( + + + + + {`Terminal too small\nMinimum size: ${minWidth()}x${minHeight()} — current: ${dimensions().width}x${dimensions().height}`} + + + + {props.children} + + ); +} diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx new file mode 100644 index 0000000..c1a4323 --- /dev/null +++ b/src/components/Modal.tsx @@ -0,0 +1,51 @@ +import { type ColorInput, RGBA } from "@opentui/core"; +import { Portal, useTerminalDimensions } from "@opentui/solid"; +import { Show, type ParentProps } from "solid-js"; +import { theme } from "../index.ts"; + +export interface ModalProps extends ParentProps { + open: boolean; + onDismiss?: () => void; + accentColor?: ColorInput; + backgroundColor?: ColorInput; + backdropColor?: ColorInput; + width?: number; +} + +export function Modal(props: ModalProps) { + const dimensions = useTerminalDimensions(); + + return ( + + + + + {props.children} + + + + + ); +} diff --git a/src/components/NotificationHost.tsx b/src/components/NotificationHost.tsx new file mode 100644 index 0000000..6b1feb0 --- /dev/null +++ b/src/components/NotificationHost.tsx @@ -0,0 +1,49 @@ +import type { ColorInput } from "@opentui/core"; +import { Portal } from "@opentui/solid"; +import { For } from "solid-js"; +import { dismiss, notifications, type NotificationKind } from "../context/notifications.ts"; +import { theme } from "../index.ts"; + +const KIND_LABEL: Record = { + info: "Info", + success: "Success", + warning: "Warning", + error: "Error", +}; + +export interface NotificationHostProps { + width?: number; + colors?: Partial>; +} + +export function NotificationHost(props: NotificationHostProps = {}) { + const defaultColor = (): Record => ({ + info: theme.primary, + success: theme.success, + warning: theme.warning, + error: theme.error, + }); + const colorFor = (kind: NotificationKind) => props.colors?.[kind] ?? defaultColor()[kind]; + + return ( + + + + {(toast) => ( + dismiss(toast.id) : undefined} + > + {toast.title ?? KIND_LABEL[toast.kind]} + {toast.message} + + )} + + + + ); +} diff --git a/src/components/ProgressBar.tsx b/src/components/ProgressBar.tsx new file mode 100644 index 0000000..d9b8149 --- /dev/null +++ b/src/components/ProgressBar.tsx @@ -0,0 +1,51 @@ +import { type ColorInput, parseColor, RGBA } from "@opentui/core"; +import { createMemo, For } from "solid-js"; +import { theme } from "../index.ts"; + +function sampleGradient(stops: ColorInput[], t: number): ColorInput { + if (stops.length === 1) return stops[0]!; + const clamped = Math.min(Math.max(t, 0), 1); + const segment = clamped * (stops.length - 1); + const i = Math.min(Math.floor(segment), stops.length - 2); + const localT = segment - i; + const [ar, ag, ab, aa] = parseColor(stops[i]!).toInts(); + const [br, bg, bb, ba] = parseColor(stops[i + 1]!).toInts(); + return RGBA.fromInts( + Math.round(ar + (br - ar) * localT), + Math.round(ag + (bg - ag) * localT), + Math.round(ab + (bb - ab) * localT), + Math.round(aa + (ba - aa) * localT), + ); +} + +export interface ProgressBarProps { + value: number; + width?: number; + color?: ColorInput | ColorInput[]; + trackColor?: ColorInput; + fillChar?: string; + trackChar?: string; +} + +export function ProgressBar(props: ProgressBarProps) { + const width = () => props.width ?? 20; + const stops = createMemo(() => (Array.isArray(props.color) ? props.color : [props.color ?? theme.primary])); + const filled = createMemo(() => Math.round((width() * Math.min(Math.max(props.value, 0), 100)) / 100)); + const cells = createMemo(() => Array.from({ length: width() }, (_, i) => i)); + + return ( + + + {(i) => { + const isFilled = () => i < filled(); + const t = width() > 1 ? i / (width() - 1) : 0; + return ( + + {isFilled() ? (props.fillChar ?? "█") : (props.trackChar ?? "░")} + + ); + }} + + + ); +} diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx new file mode 100644 index 0000000..de03a90 --- /dev/null +++ b/src/components/Sidebar.tsx @@ -0,0 +1,75 @@ +import { type ColorInput, type SelectOption, TextAttributes } from "@opentui/core"; +import { theme } from "../index.ts"; + +export interface SidebarHandle { + moveUp(): void; + moveDown(): void; + selectCurrent(): void; +} + +export interface SidebarProps { + title: string; + items: SelectOption[]; + width?: number; + focused?: boolean; + onSelect: (option: SelectOption) => void; + onConfirm?: (option: SelectOption) => void; + ref?: (handle: SidebarHandle | null) => void; + + accentColor?: ColorInput; + mutedColor?: ColorInput; + + backgroundColor?: ColorInput; + textColor?: ColorInput; + focusedBackgroundColor?: ColorInput; + focusedTextColor?: ColorInput; + selectedBackgroundColor?: ColorInput; + selectedTextColor?: ColorInput; + descriptionColor?: ColorInput; + selectedDescriptionColor?: ColorInput; +} + +export function Sidebar(props: SidebarProps) { + return ( + + {props.title} +