mirror of
https://github.com/anotherhadi/ilovetui.git
synced 2026-06-26 00:42:33 +02:00
init
Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
version: 2
|
||||
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
|
||||
archives:
|
||||
- formats:
|
||||
- tar.gz
|
||||
name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}"
|
||||
|
||||
checksum:
|
||||
name_template: checksums.txt
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- "^docs:"
|
||||
- "^test:"
|
||||
- "^ci:"
|
||||
@@ -0,0 +1,10 @@
|
||||
# Contributing
|
||||
|
||||
Everybody is invited and welcome to contribute. There is a lot to do... Check the issues!
|
||||
|
||||
The process is straight-forward.
|
||||
|
||||
- Read [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#best-practices-for-faster-reviews) by Kubernetes (but skip step 0 and 1)
|
||||
- Fork this git repository
|
||||
- Write your changes (bug fixes, new features, ...).
|
||||
- Create a Pull Request against the main branch.
|
||||
@@ -0,0 +1 @@
|
||||
ko_fi: anotherhadi
|
||||
@@ -0,0 +1,28 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
- uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
version: "~> v2"
|
||||
args: release --clean --config .github/.goreleaser.yaml
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,4 @@
|
||||
.claude/
|
||||
CLAUDE.md
|
||||
result/
|
||||
.pre-commit-config.yaml
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Hadi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,95 @@
|
||||
# Ilovetui
|
||||
|
||||
A minimal Go library that provides a shared [Base16](https://github.com/tinted-theming/home) color theme for terminal UIs built with [bubbletea](https://github.com/charmbracelet/bubbletea) and [lipgloss](https://github.com/charmbracelet/lipgloss).
|
||||
|
||||
The idea is simple: instead of every TUI app managing its own colors, they all share one theme file so the user customizes once and every app looks consistent.
|
||||
|
||||
## How it works
|
||||
|
||||
On import, `ilovetui` automatically loads the user's theme from `~/.config/ilovetui/config.yaml` (respecting `$XDG_CONFIG_HOME`).
|
||||
If no config exists, it falls back to the embedded default. The active theme is exposed as the package-level variable `S`.
|
||||
|
||||
```go
|
||||
import "github.com/anotherhadi/ilovetui"
|
||||
|
||||
// Use colors directly
|
||||
style := lipgloss.NewStyle().Foreground(ilovetui.S.Primary)
|
||||
|
||||
// Use pre-built panel styles
|
||||
box := ilovetui.RenderWithTitle(ilovetui.S.PanelFocused, "Title", content, w, h)
|
||||
```
|
||||
|
||||
No setup required — just import and use.
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
go get github.com/anotherhadi/ilovetui
|
||||
```
|
||||
|
||||
## Theme
|
||||
|
||||
The theme follows the [Base16](https://github.com/tinted-theming/home) standard (16 colors). The library exposes both the raw palette and semantic aliases:
|
||||
|
||||
| Alias | Base16 | Meaning |
|
||||
| ------------ | ------ | --------------------------------------- |
|
||||
| `Background` | Base00 | Background |
|
||||
| `SubtleBg` | Base01 | Lighter Background / Status Bars |
|
||||
| `Selection` | Base02 | Selection Background |
|
||||
| `Subtle` | Base03 | Comments / Invisibles |
|
||||
| `Muted` | Base04 | Dark Foreground / Status Bars |
|
||||
| `Text` | Base05 | Default Foreground |
|
||||
| `Primary` | Base0D | Functions / Methods / Headings / Accent |
|
||||
| `Success` | Base0B | Strings / Success / Diff Inserted |
|
||||
| `Warning` | Base09 | Integers / Constants / Booleans |
|
||||
| `Error` | Base08 | Variables / Errors / Diff Deleted |
|
||||
|
||||
The default theme is `./default.yaml`. Copy it and edit to customize:
|
||||
|
||||
```sh
|
||||
mkdir -p ~/.config/ilovetui
|
||||
cp $(go env GOPATH)/pkg/mod/github.com/anotherhadi/ilovetui*/default.yaml ~/.config/ilovetui/config.yaml
|
||||
```
|
||||
|
||||
Or let your app write it on first run:
|
||||
|
||||
```go
|
||||
ilovetui.WriteDefaultConfig(ilovetui.DefaultConfigPath())
|
||||
```
|
||||
|
||||
## Pre-built styles
|
||||
|
||||
`S` ships with a few ready-to-use lipgloss styles:
|
||||
|
||||
| Field | Description |
|
||||
| ---------------- | ---------------------------------------- |
|
||||
| `S.Bold` | Bold text |
|
||||
| `S.Faint` | Muted / dimmed text |
|
||||
| `S.Panel` | Rounded border, unfocused (Subtle color) |
|
||||
| `S.PanelFocused` | Rounded border, focused (Primary color) |
|
||||
|
||||
## Helpers
|
||||
|
||||
```go
|
||||
// Inner usable height of a bordered panel with outer height h
|
||||
inner := ilovetui.ContentHeight(h)
|
||||
|
||||
// Render a box with a title embedded in the top border
|
||||
box := ilovetui.RenderWithTitle(ilovetui.S.PanelFocused, "Header", content, w, h)
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
```go
|
||||
ilovetui.Init() // Reload from default config path
|
||||
ilovetui.InitFrom(path string) // Reload from a custom path
|
||||
ilovetui.InitFromBytes(data []byte) // Parse raw YAML
|
||||
ilovetui.DefaultConfigPath() string // ~/.config/ilovetui/config.yaml
|
||||
ilovetui.WriteDefaultConfig(path) // Write default config if missing
|
||||
```
|
||||
|
||||
## Projects using ilovetui
|
||||
|
||||
- [anotherhadi/spilltea](https://github.com/anotherhadi/spilltea): A minimal, terminal-based HTTP(S) proxy for pentesters and CTF players. Think Burp Suite or Caido, but entirely in your terminal.
|
||||
- [anotherhadi/usbguard-tui](https://github.com/anotherhadi/usbguard-tui): A terminal UI for managing USB devices via usbguard. TUI built with golang & bubbletea.
|
||||
- [anotherhadi/jwt-tui](https://github.com/anotherhadi/jwt-tui): A terminal UI for inspecting, editing, and signing JSON Web Tokens (JWTs).
|
||||
@@ -0,0 +1,44 @@
|
||||
package ilovetui
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"charm.land/lipgloss/v2"
|
||||
)
|
||||
|
||||
// ContentHeight returns the usable inner height for a bordered panel of totalH rows.
|
||||
func ContentHeight(totalH int) int {
|
||||
h := totalH - 2
|
||||
if h < 0 {
|
||||
return 0
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// RenderWithTitle renders a bordered box with a title embedded in the top border.
|
||||
// title may contain ANSI color codes. width and height are the total outer dimensions.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// box := ilovetui.RenderWithTitle(theme.Styles.PanelFocused, "Header", content, w, h)
|
||||
func RenderWithTitle(border lipgloss.Style, title, content string, width, height int) string {
|
||||
boxH := height - 1
|
||||
if contentH := boxH - 1; contentH > 0 {
|
||||
lines := strings.Split(content, "\n")
|
||||
if len(lines) > contentH {
|
||||
content = strings.Join(lines[:contentH], "\n")
|
||||
}
|
||||
}
|
||||
box := border.BorderTop(false).Width(width).Height(boxH).Render(content)
|
||||
|
||||
boxWidth := lipgloss.Width(strings.SplitN(box, "\n", 2)[0])
|
||||
titleW := lipgloss.Width(title)
|
||||
fillW := boxWidth - titleW - 4 // 4 = "╭ " + " " + "╮"
|
||||
if fillW < 0 {
|
||||
fillW = 0
|
||||
}
|
||||
bc := lipgloss.NewStyle().Foreground(border.GetBorderTopForeground())
|
||||
topLine := bc.Render("╭ ") + title + bc.Render(" "+strings.Repeat("─", fillW)+"╮")
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Left, topLine, box)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package ilovetui
|
||||
|
||||
type colorsYAML struct {
|
||||
Base00 string `yaml:"base00"`
|
||||
Base01 string `yaml:"base01"`
|
||||
Base02 string `yaml:"base02"`
|
||||
Base03 string `yaml:"base03"`
|
||||
Base04 string `yaml:"base04"`
|
||||
Base05 string `yaml:"base05"`
|
||||
Base06 string `yaml:"base06"`
|
||||
Base07 string `yaml:"base07"`
|
||||
Base08 string `yaml:"base08"`
|
||||
Base09 string `yaml:"base09"`
|
||||
Base0A string `yaml:"base0a"`
|
||||
Base0B string `yaml:"base0b"`
|
||||
Base0C string `yaml:"base0c"`
|
||||
Base0D string `yaml:"base0d"`
|
||||
Base0E string `yaml:"base0e"`
|
||||
Base0F string `yaml:"base0f"`
|
||||
}
|
||||
|
||||
type configYAML struct {
|
||||
Colors colorsYAML `yaml:"colors"`
|
||||
}
|
||||
|
||||
func mergeColors(base, user colorsYAML) colorsYAML {
|
||||
pick := func(b, u string) string {
|
||||
if u != "" {
|
||||
return u
|
||||
}
|
||||
return b
|
||||
}
|
||||
return colorsYAML{
|
||||
Base00: pick(base.Base00, user.Base00),
|
||||
Base01: pick(base.Base01, user.Base01),
|
||||
Base02: pick(base.Base02, user.Base02),
|
||||
Base03: pick(base.Base03, user.Base03),
|
||||
Base04: pick(base.Base04, user.Base04),
|
||||
Base05: pick(base.Base05, user.Base05),
|
||||
Base06: pick(base.Base06, user.Base06),
|
||||
Base07: pick(base.Base07, user.Base07),
|
||||
Base08: pick(base.Base08, user.Base08),
|
||||
Base09: pick(base.Base09, user.Base09),
|
||||
Base0A: pick(base.Base0A, user.Base0A),
|
||||
Base0B: pick(base.Base0B, user.Base0B),
|
||||
Base0C: pick(base.Base0C, user.Base0C),
|
||||
Base0D: pick(base.Base0D, user.Base0D),
|
||||
Base0E: pick(base.Base0E, user.Base0E),
|
||||
Base0F: pick(base.Base0F, user.Base0F),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
# ilovetui default theme
|
||||
# Copy to ~/.config/ilovetui/config.yaml and edit to customize.
|
||||
colors:
|
||||
base00: "#1e1e2e" # Background
|
||||
base01: "#181825" # Lighter Background / Status Bars
|
||||
base02: "#313244" # Selection Background
|
||||
base03: "#45475a" # Comments / Invisibles
|
||||
base04: "#585b70" # Dark Foreground / Status Bars
|
||||
base05: "#cdd6f4" # Default Foreground
|
||||
base06: "#f5f5f5" # Light Foreground
|
||||
base07: "#b4befe" # Light Background
|
||||
base08: "#f38ba8" # Variables / Errors / Diff Deleted
|
||||
base09: "#fab387" # Integers / Constants / Booleans
|
||||
base0a: "#f9e2af" # Classes / Warnings / Search Background
|
||||
base0b: "#a6e3a1" # Strings / Success / Diff Inserted
|
||||
base0c: "#94e2d5" # Support / Regex / Escape Characters
|
||||
base0d: "#89b4fa" # Functions / Methods / Headings / Accent
|
||||
base0e: "#cba6f7" # Keywords / Storage / Diff Changed
|
||||
base0f: "#f2cdcd" # Embedded / Misc
|
||||
Generated
+87
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1767039857,
|
||||
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
|
||||
"owner": "NixOS",
|
||||
"repo": "flake-compat",
|
||||
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"git-hooks": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
"gitignore": "gitignore",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1778507602,
|
||||
"narHash": "sha256-kTwur1wV+01SdqskVMSo6JMEpg71ps3HpbFY2GsflKs=",
|
||||
"owner": "cachix",
|
||||
"repo": "git-hooks.nix",
|
||||
"rev": "61ab0e80d9c7ab14c256b5b453d8b3fb0189ba0a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"repo": "git-hooks.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"gitignore": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"git-hooks",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1709087332,
|
||||
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1779560665,
|
||||
"narHash": "sha256-tpyBcxPpcQb8ukyNF7DoCwfSY3VPsxHoYwj00Cayv5o=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "64c08a7ca051951c8eae34e3e3cb1e202fe36786",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"git-hooks": "git-hooks",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
description = "";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
git-hooks = {
|
||||
url = "github:cachix/git-hooks.nix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
|
||||
outputs = {
|
||||
self,
|
||||
nixpkgs,
|
||||
git-hooks,
|
||||
}: let
|
||||
supportedSystems = ["x86_64-linux" "aarch64-linux"];
|
||||
|
||||
forAllSystems = f:
|
||||
nixpkgs.lib.genAttrs supportedSystems
|
||||
(system: f system (import nixpkgs {inherit system;}));
|
||||
in {
|
||||
devShells = forAllSystems (system: pkgs: {
|
||||
default = import ./nix/shell.nix {
|
||||
inherit pkgs;
|
||||
gitHooksLib = git-hooks.lib.${system};
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
module github.com/anotherhadi/ilovetui
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
charm.land/lipgloss/v2 v2.0.3
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/charmbracelet/colorprofile v0.4.3 // indirect
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.11.7 // 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/lucasb-eyer/go-colorful v1.4.0 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.23 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
golang.org/x/sync v0.18.0 // indirect
|
||||
golang.org/x/sys v0.43.0 // indirect
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
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/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
|
||||
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 h1:OqDqxQZliC7C8adA7KjelW3OjtAxREfeHkNcd66wpeI=
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318/go.mod h1:Y6kE2GzHfkyQQVCSL9r2hwokSrIlHGzZG+71+wDYSZI=
|
||||
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/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/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/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
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/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.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=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
// Package ilovetui provides a shared Base16 color theme for bubbletea/lipgloss
|
||||
// applications. The theme is loaded automatically on import from
|
||||
// ~/.config/ilovetui/config.yaml (falling back to the embedded
|
||||
// default config). Access colors and styles via the package-level variable S.
|
||||
//
|
||||
// import "github.com/anotherhadi/ilovetui"
|
||||
//
|
||||
// style := lipgloss.NewStyle().Foreground(ilovetui.S.Primary)
|
||||
// box := ilovetui.RenderWithTitle(ilovetui.S.PanelFocused, "Title", content, w, h)
|
||||
package ilovetui
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
//go:embed default.yaml
|
||||
var DefaultConfig []byte
|
||||
|
||||
// S is the active theme. It is populated automatically at import time and can
|
||||
// be reloaded at any point by calling Init, InitFrom, or InitFromBytes.
|
||||
var S Styles
|
||||
|
||||
func init() {
|
||||
path := DefaultConfigPath()
|
||||
if data, err := os.ReadFile(path); err == nil {
|
||||
if s, err := stylesFromBytes(data); err == nil {
|
||||
S = s
|
||||
return
|
||||
}
|
||||
}
|
||||
// Silent fallback: embedded default always works.
|
||||
s, _ := stylesFromBytes(DefaultConfig)
|
||||
S = s
|
||||
}
|
||||
|
||||
// Init reloads S from the user config file, falling back to the embedded
|
||||
// default if the file is missing. Returns an error only on parse failures.
|
||||
func Init() error {
|
||||
path := DefaultConfigPath()
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
s, e := stylesFromBytes(DefaultConfig)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
S = s
|
||||
return nil
|
||||
}
|
||||
return InitFromBytes(data)
|
||||
}
|
||||
|
||||
// InitFrom reloads S from an explicit file path.
|
||||
func InitFrom(path string) error {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ilovetui: read config: %w", err)
|
||||
}
|
||||
return InitFromBytes(data)
|
||||
}
|
||||
|
||||
// InitFromBytes reloads S from raw YAML. Accepts hex strings with or without
|
||||
// the leading '#'.
|
||||
func InitFromBytes(data []byte) error {
|
||||
s, err := stylesFromBytes(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
S = s
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteDefaultConfig writes the embedded default config to path, creating
|
||||
// parent directories as needed. No-op if the file already exists.
|
||||
func WriteDefaultConfig(path string) error {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return nil
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return fmt.Errorf("ilovetui: create config dir: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(path, DefaultConfig, 0o600); err != nil {
|
||||
return fmt.Errorf("ilovetui: write config: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DefaultConfigPath returns the canonical user config path,
|
||||
// respecting $XDG_CONFIG_HOME.
|
||||
func DefaultConfigPath() string {
|
||||
return filepath.Join(configDir(), "ilovetui", "config.yaml")
|
||||
}
|
||||
|
||||
func stylesFromBytes(data []byte) (Styles, error) {
|
||||
var base configYAML
|
||||
if err := yaml.Unmarshal(DefaultConfig, &base); err != nil {
|
||||
return Styles{}, fmt.Errorf("ilovetui: parse default config: %w", err)
|
||||
}
|
||||
var user configYAML
|
||||
if err := yaml.Unmarshal(data, &user); err != nil {
|
||||
return Styles{}, fmt.Errorf("ilovetui: parse config: %w", err)
|
||||
}
|
||||
return newStyles(mergeColors(base.Colors, user.Colors)), nil
|
||||
}
|
||||
|
||||
func configDir() string {
|
||||
if dir := os.Getenv("XDG_CONFIG_HOME"); dir != "" {
|
||||
return dir
|
||||
}
|
||||
home, _ := os.UserHomeDir()
|
||||
return filepath.Join(home, ".config")
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
pkgs,
|
||||
gitHooksLib,
|
||||
}: let
|
||||
hooks = gitHooksLib.run {
|
||||
src = ../.;
|
||||
hooks = {
|
||||
gofmt.enable = true;
|
||||
govet.enable = true;
|
||||
};
|
||||
};
|
||||
in
|
||||
pkgs.mkShell {
|
||||
packages = with pkgs;
|
||||
[
|
||||
go
|
||||
doctoc
|
||||
]
|
||||
++ hooks.enabledPackages;
|
||||
|
||||
shellHook = hooks.shellHook;
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package ilovetui
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"strings"
|
||||
|
||||
"charm.land/lipgloss/v2"
|
||||
)
|
||||
|
||||
// Styles holds both the raw Base16 palette and ready-to-use semantic colors
|
||||
// and lipgloss styles. Access via the package-level variable S.
|
||||
type Styles struct {
|
||||
// Raw Base16 palette
|
||||
Base00 color.Color
|
||||
Base01 color.Color
|
||||
Base02 color.Color
|
||||
Base03 color.Color
|
||||
Base04 color.Color
|
||||
Base05 color.Color
|
||||
Base06 color.Color
|
||||
Base07 color.Color
|
||||
Base08 color.Color
|
||||
Base09 color.Color
|
||||
Base0A color.Color
|
||||
Base0B color.Color
|
||||
Base0C color.Color
|
||||
Base0D color.Color
|
||||
Base0E color.Color
|
||||
Base0F color.Color
|
||||
|
||||
// Semantic color aliases
|
||||
Background color.Color
|
||||
SubtleBg color.Color
|
||||
Selection color.Color
|
||||
Subtle color.Color
|
||||
Muted color.Color
|
||||
Text color.Color
|
||||
Primary color.Color
|
||||
Success color.Color
|
||||
Warning color.Color
|
||||
Error color.Color
|
||||
|
||||
// Pre-built text styles
|
||||
Bold lipgloss.Style
|
||||
Faint lipgloss.Style
|
||||
|
||||
// Pre-built panel styles (rounded border)
|
||||
Panel lipgloss.Style
|
||||
PanelFocused lipgloss.Style
|
||||
}
|
||||
|
||||
func newStyles(c colorsYAML) Styles {
|
||||
lc := func(s string) color.Color {
|
||||
s = strings.TrimSpace(s)
|
||||
if s != "" && s[0] != '#' {
|
||||
s = "#" + s
|
||||
}
|
||||
return lipgloss.Color(s)
|
||||
}
|
||||
|
||||
b00 := lc(c.Base00)
|
||||
b01 := lc(c.Base01)
|
||||
b02 := lc(c.Base02)
|
||||
b03 := lc(c.Base03)
|
||||
b04 := lc(c.Base04)
|
||||
b05 := lc(c.Base05)
|
||||
b06 := lc(c.Base06)
|
||||
b07 := lc(c.Base07)
|
||||
b08 := lc(c.Base08)
|
||||
b09 := lc(c.Base09)
|
||||
b0A := lc(c.Base0A)
|
||||
b0B := lc(c.Base0B)
|
||||
b0C := lc(c.Base0C)
|
||||
b0D := lc(c.Base0D)
|
||||
b0E := lc(c.Base0E)
|
||||
b0F := lc(c.Base0F)
|
||||
|
||||
return Styles{
|
||||
Base00: b00, Base01: b01, Base02: b02, Base03: b03,
|
||||
Base04: b04, Base05: b05, Base06: b06, Base07: b07,
|
||||
Base08: b08, Base09: b09, Base0A: b0A, Base0B: b0B,
|
||||
Base0C: b0C, Base0D: b0D, Base0E: b0E, Base0F: b0F,
|
||||
|
||||
Background: b00,
|
||||
SubtleBg: b01,
|
||||
Selection: b02,
|
||||
Subtle: b03,
|
||||
Muted: b04,
|
||||
Text: b05,
|
||||
Primary: b0D,
|
||||
Success: b0B,
|
||||
Warning: b09,
|
||||
Error: b08,
|
||||
|
||||
Bold: lipgloss.NewStyle().Bold(true),
|
||||
Faint: lipgloss.NewStyle().Foreground(b03).Faint(true),
|
||||
|
||||
Panel: lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(b03),
|
||||
|
||||
PanelFocused: lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(b0D),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user