mirror of
https://github.com/BishopFox/sliver.git
synced 2026-11-07 17:08:13 -10:00
Compare commits
36
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82d979436b | ||
|
|
d2632e75bd | ||
|
|
a55831fb44 | ||
|
|
39d4edec43 | ||
|
|
bbdb08e7a4 | ||
|
|
39b488bd25 | ||
|
|
e7b8a24534 | ||
|
|
17b9d611f8 | ||
|
|
ed9075d065 | ||
|
|
1f7221933d | ||
|
|
6333e0691a | ||
|
|
3bbaf80510 | ||
|
|
25726d8105 | ||
|
|
67e9a7f37f | ||
|
|
a5b6935da6 | ||
|
|
2e36f45bdf | ||
|
|
3e06a9d5ee | ||
|
|
952b2df49d | ||
|
|
9769f1bb57 | ||
|
|
f5f2c80ad3 | ||
|
|
bb2edf63cf | ||
|
|
cd711afe8a | ||
|
|
f9bbbb34bb | ||
|
|
c3582fc203 | ||
|
|
07c95b203f | ||
|
|
958e44deef | ||
|
|
836395a2d6 | ||
|
|
1a7a1c9b2d | ||
|
|
4ba285796f | ||
|
|
a05dfad100 | ||
|
|
0eb29ad9b8 | ||
|
|
ad8b329f85 | ||
|
|
c6206257b6 | ||
|
|
4d4eab998f | ||
|
|
c081f17380 | ||
|
|
fafbac3fa6 |
No files matched your search
@@ -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.
|
||||
@@ -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,6 +1,7 @@
|
||||
package alias
|
||||
|
||||
import (
|
||||
"github.com/bishopfox/sliver/client/command/completers"
|
||||
"github.com/bishopfox/sliver/client/command/flags"
|
||||
"github.com/bishopfox/sliver/client/command/help"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
@@ -32,6 +33,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
}
|
||||
aliasCmd.AddCommand(aliasLoadCmd)
|
||||
flags.NewCompletions(aliasLoadCmd).PositionalCompletion(carapace.ActionDirectories().Tag("alias directory").Usage("path to the alias directory"))
|
||||
completers.RegisterLocalFilePathPositionalCompletion(aliasLoadCmd, 0)
|
||||
|
||||
aliasInstallCmd := &cobra.Command{
|
||||
Use: consts.InstallStr + " [ALIAS]",
|
||||
@@ -44,6 +46,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
}
|
||||
aliasCmd.AddCommand(aliasInstallCmd)
|
||||
flags.NewCompletions(aliasInstallCmd).PositionalCompletion(carapace.ActionFiles().Tag("alias file"))
|
||||
completers.RegisterLocalFilePathPositionalCompletion(aliasInstallCmd, 0)
|
||||
|
||||
aliasRemove := &cobra.Command{
|
||||
Use: consts.RmStr + " [ALIAS]",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package c2profiles
|
||||
|
||||
import (
|
||||
"github.com/bishopfox/sliver/client/command/completers"
|
||||
"github.com/bishopfox/sliver/client/command/flags"
|
||||
"github.com/bishopfox/sliver/client/command/generate"
|
||||
"github.com/bishopfox/sliver/client/command/help"
|
||||
@@ -27,6 +28,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
f.StringP("file", "f", "", "Path to C2 configuration file to import")
|
||||
f.BoolP("overwrite", "o", false, "Overwrite profile if it exists")
|
||||
})
|
||||
completers.RegisterLocalFilePathFlagCompletion(importC2ProfileCmd, "file")
|
||||
|
||||
exportC2ProfileCmd := &cobra.Command{
|
||||
Use: consts.ExportC2ProfileStr,
|
||||
@@ -40,6 +42,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
f.StringP("file", "f", "", "Path to file to export C2 configuration to")
|
||||
f.StringP("name", "n", consts.DefaultC2Profile, "HTTP C2 Profile name")
|
||||
})
|
||||
completers.RegisterLocalFilePathFlagCompletion(exportC2ProfileCmd, "file")
|
||||
flags.BindFlagCompletions(exportC2ProfileCmd, func(comp *carapace.ActionMap) {
|
||||
(*comp)["name"] = generate.HTTPC2Completer(con)
|
||||
})
|
||||
@@ -76,6 +79,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
f.StringP("name", "n", "", "HTTP C2 Profile name to save C2Profile as")
|
||||
f.StringP("template", "t", consts.DefaultC2Profile, "HTTP C2 Profile to use as a template for the new profile")
|
||||
})
|
||||
completers.RegisterLocalFilePathFlagCompletion(generateC2ProfileCmd, "file")
|
||||
|
||||
flags.BindFlagCompletions(generateC2ProfileCmd, func(comp *carapace.ActionMap) {
|
||||
(*comp)["template"] = generate.HTTPC2Completer(con)
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"net"
|
||||
|
||||
"github.com/rsteube/carapace"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// ClientInterfacesCompleter completes interface addresses on the client host.
|
||||
@@ -84,3 +85,60 @@ func LocalProxyCompleter() carapace.Action {
|
||||
}).Invoke(c).Prefix(prefix).ToA()
|
||||
})
|
||||
}
|
||||
|
||||
// LocalFilePathCompleter completes local file and directory paths.
|
||||
func LocalFilePathCompleter() carapace.Action {
|
||||
return carapace.ActionFiles().Tag("local file path")
|
||||
}
|
||||
|
||||
// RegisterLocalFilePathFlagCompletion registers local file-path completion for a flag.
|
||||
func RegisterLocalFilePathFlagCompletion(cmd *cobra.Command, name string) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
if _, ok := cmd.GetFlagCompletionFunc(name); ok {
|
||||
return
|
||||
}
|
||||
if cmd.Flags().Lookup(name) == nil && cmd.PersistentFlags().Lookup(name) == nil {
|
||||
return
|
||||
}
|
||||
_ = cmd.RegisterFlagCompletionFunc(name, func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
||||
return nil, cobra.ShellCompDirectiveDefault
|
||||
})
|
||||
}
|
||||
|
||||
// RegisterLocalFilePathFlagCompletions registers local file-path completion for a list of flags.
|
||||
func RegisterLocalFilePathFlagCompletions(cmd *cobra.Command, names ...string) {
|
||||
for _, name := range names {
|
||||
RegisterLocalFilePathFlagCompletion(cmd, name)
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterLocalFilePathPositionalCompletion registers local file-path completion for a positional argument index.
|
||||
func RegisterLocalFilePathPositionalCompletion(cmd *cobra.Command, index int) {
|
||||
if cmd == nil || index < 0 {
|
||||
return
|
||||
}
|
||||
if cmd.ValidArgsFunction != nil {
|
||||
return
|
||||
}
|
||||
cmd.ValidArgsFunction = func(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(args) == index {
|
||||
return nil, cobra.ShellCompDirectiveDefault
|
||||
}
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterLocalFilePathPositionalAnyCompletion registers local file-path completion for all positional arguments.
|
||||
func RegisterLocalFilePathPositionalAnyCompletion(cmd *cobra.Command) {
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
if cmd.ValidArgsFunction != nil {
|
||||
return
|
||||
}
|
||||
cmd.ValidArgsFunction = func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
||||
return nil, cobra.ShellCompDirectiveDefault
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package crack
|
||||
|
||||
import (
|
||||
"github.com/bishopfox/sliver/client/command/completers"
|
||||
"github.com/rsteube/carapace"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
@@ -61,6 +62,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
f.StringP("name", "n", "", "wordlist name (blank = filename)")
|
||||
})
|
||||
carapace.Gen(wordlistsAddCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to local wordlist file"))
|
||||
completers.RegisterLocalFilePathPositionalCompletion(wordlistsAddCmd, 0)
|
||||
wordlistsCmd.AddCommand(wordlistsAddCmd)
|
||||
|
||||
wordlistsRmCmd := &cobra.Command{
|
||||
@@ -95,6 +97,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
f.StringP("name", "n", "", "rules name (blank = filename)")
|
||||
})
|
||||
carapace.Gen(rulesAddCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to local rules file"))
|
||||
completers.RegisterLocalFilePathPositionalCompletion(rulesAddCmd, 0)
|
||||
rulesCmd.AddCommand(rulesAddCmd)
|
||||
|
||||
rulesRmCmd := &cobra.Command{
|
||||
@@ -130,6 +133,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
f.StringP("name", "n", "", "hcstat2 name (blank = filename)")
|
||||
})
|
||||
carapace.Gen(hcstat2AddCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to local hcstat2 file"))
|
||||
completers.RegisterLocalFilePathPositionalCompletion(hcstat2AddCmd, 0)
|
||||
hcstat2Cmd.AddCommand(hcstat2AddCmd)
|
||||
|
||||
hcstat2RmCmd := &cobra.Command{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package creds
|
||||
|
||||
import (
|
||||
"github.com/bishopfox/sliver/client/command/completers"
|
||||
"github.com/bishopfox/sliver/client/command/flags"
|
||||
"github.com/bishopfox/sliver/client/command/help"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
@@ -65,6 +66,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
(*comp)["hash-type"] = CredsHashTypeCompleter(con)
|
||||
})
|
||||
carapace.Gen(credsAddFileCmd).PositionalCompletion(carapace.ActionFiles().Tag("credential file"))
|
||||
completers.RegisterLocalFilePathPositionalCompletion(credsAddFileCmd, 0)
|
||||
credsAddCmd.AddCommand(credsAddFileCmd)
|
||||
|
||||
credsRmCmd := &cobra.Command{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package cursed
|
||||
|
||||
import (
|
||||
"github.com/bishopfox/sliver/client/command/completers"
|
||||
"github.com/bishopfox/sliver/client/command/flags"
|
||||
"github.com/bishopfox/sliver/client/command/help"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
@@ -75,6 +76,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
flags.BindFlagCompletions(cursedChromeCmd, func(comp *carapace.ActionMap) {
|
||||
(*comp)["payload"] = carapace.ActionFiles("js").Tag("javascript files")
|
||||
})
|
||||
completers.RegisterLocalFilePathFlagCompletion(cursedChromeCmd, "payload")
|
||||
cursedChromeCmd.Flags().ParseErrorsWhitelist.UnknownFlags = true
|
||||
carapace.Gen(cursedChromeCmd).PositionalAnyCompletion(carapace.ActionValues().Usage("additional Chrome CLI arguments"))
|
||||
|
||||
@@ -99,6 +101,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
flags.BindFlagCompletions(cursedEdgeCmd, func(comp *carapace.ActionMap) {
|
||||
(*comp)["payload"] = carapace.ActionFiles("js").Tag("javascript files")
|
||||
})
|
||||
completers.RegisterLocalFilePathFlagCompletion(cursedEdgeCmd, "payload")
|
||||
cursedEdgeCmd.Flags().ParseErrorsWhitelist.UnknownFlags = true
|
||||
carapace.Gen(cursedEdgeCmd).PositionalAnyCompletion(carapace.ActionValues().Usage("additional Edge CLI arguments"))
|
||||
|
||||
@@ -130,6 +133,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
flags.Bind("", false, CursedCookiesCmd, func(f *pflag.FlagSet) {
|
||||
f.StringP("save", "s", "", "save to file")
|
||||
})
|
||||
completers.RegisterLocalFilePathFlagCompletion(CursedCookiesCmd, "save")
|
||||
|
||||
cursedScreenshotCmd := &cobra.Command{
|
||||
Use: consts.ScreenshotStr,
|
||||
@@ -144,6 +148,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
f.Int64P("quality", "q", 100, "screenshot quality (1 - 100)")
|
||||
f.StringP("save", "s", "", "save to file")
|
||||
})
|
||||
completers.RegisterLocalFilePathFlagCompletion(cursedScreenshotCmd, "save")
|
||||
|
||||
return []*cobra.Command{cursedCmd}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package dllhijack
|
||||
|
||||
import (
|
||||
"github.com/bishopfox/sliver/client/command/completers"
|
||||
"github.com/rsteube/carapace"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
@@ -37,6 +38,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
(*comp)["file"] = carapace.ActionFiles()
|
||||
(*comp)["profile"] = generate.ProfileNameCompleter(con)
|
||||
})
|
||||
completers.RegisterLocalFilePathFlagCompletions(dllhijackCmd, "reference-file", "file")
|
||||
carapace.Gen(dllhijackCmd).PositionalCompletion(carapace.ActionValues().Usage("Path to upload the DLL to on the remote system"))
|
||||
|
||||
return []*cobra.Command{dllhijackCmd}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package exec
|
||||
|
||||
import (
|
||||
"github.com/bishopfox/sliver/client/command/completers"
|
||||
"github.com/bishopfox/sliver/client/command/flags"
|
||||
"github.com/bishopfox/sliver/client/command/generate"
|
||||
"github.com/bishopfox/sliver/client/command/help"
|
||||
@@ -93,6 +94,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
|
||||
carapace.Gen(executeAssemblyCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to assembly file (required)"))
|
||||
carapace.Gen(executeAssemblyCmd).PositionalAnyCompletion(carapace.ActionValues().Usage("arguments to pass to the assembly entrypoint (optional)"))
|
||||
completers.RegisterLocalFilePathPositionalCompletion(executeAssemblyCmd, 0)
|
||||
|
||||
executeShellcodeCmd := &cobra.Command{
|
||||
Use: consts.ExecuteShellcodeStr,
|
||||
@@ -112,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")
|
||||
})
|
||||
@@ -119,6 +129,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
(*comp)["shikata-ga-nai"] = carapace.ActionValues("386", "amd64").Tag("shikata-ga-nai architectures")
|
||||
})
|
||||
carapace.Gen(executeShellcodeCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to shellcode file (required)"))
|
||||
completers.RegisterLocalFilePathPositionalCompletion(executeShellcodeCmd, 0)
|
||||
|
||||
sideloadCmd := &cobra.Command{
|
||||
Use: consts.SideloadStr,
|
||||
@@ -147,6 +158,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
|
||||
carapace.Gen(sideloadCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to shared library file (required)"))
|
||||
carapace.Gen(sideloadCmd).PositionalAnyCompletion(carapace.ActionValues().Usage("arguments to pass to the binary (optional)"))
|
||||
completers.RegisterLocalFilePathPositionalCompletion(sideloadCmd, 0)
|
||||
|
||||
spawnDllCmd := &cobra.Command{
|
||||
Use: consts.SpawnDllStr,
|
||||
@@ -175,6 +187,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
|
||||
carapace.Gen(spawnDllCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to DLL file (required)"))
|
||||
carapace.Gen(spawnDllCmd).PositionalAnyCompletion(carapace.ActionValues().Usage("arguments to pass to the DLL entrypoint (optional)"))
|
||||
completers.RegisterLocalFilePathPositionalCompletion(spawnDllCmd, 0)
|
||||
|
||||
migrateCmd := &cobra.Command{
|
||||
Use: consts.MigrateStr,
|
||||
@@ -260,6 +273,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
(*comp)["custom-exe"] = carapace.ActionFiles()
|
||||
(*comp)["profile"] = generate.ProfileNameCompleter(con)
|
||||
})
|
||||
completers.RegisterLocalFilePathFlagCompletion(psExecCmd, "custom-exe")
|
||||
carapace.Gen(psExecCmd).PositionalCompletion(carapace.ActionValues().Usage("hostname (required)"))
|
||||
|
||||
sshCmd := &cobra.Command{
|
||||
@@ -290,6 +304,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
(*comp)["private-key"] = carapace.ActionFiles()
|
||||
(*comp)["kerberos-keytab"] = carapace.ActionFiles()
|
||||
})
|
||||
completers.RegisterLocalFilePathFlagCompletions(sshCmd, "private-key", "kerberos-keytab")
|
||||
|
||||
carapace.Gen(sshCmd).PositionalCompletion(carapace.ActionValues().Usage("remote host to SSH to (required)"))
|
||||
carapace.Gen(sshCmd).PositionalAnyCompletion(carapace.ActionValues().Usage("command line with arguments"))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package extensions
|
||||
|
||||
import (
|
||||
"github.com/bishopfox/sliver/client/command/completers"
|
||||
"github.com/bishopfox/sliver/client/command/flags"
|
||||
"github.com/bishopfox/sliver/client/command/help"
|
||||
"github.com/bishopfox/sliver/client/command/use"
|
||||
@@ -33,6 +34,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
}
|
||||
extensionCmd.AddCommand(extensionLoadCmd)
|
||||
carapace.Gen(extensionLoadCmd).PositionalCompletion(carapace.ActionDirectories().Usage("path to the extension directory"))
|
||||
completers.RegisterLocalFilePathPositionalCompletion(extensionLoadCmd, 0)
|
||||
|
||||
extensionInstallCmd := &cobra.Command{
|
||||
Use: consts.InstallStr,
|
||||
@@ -45,6 +47,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
}
|
||||
extensionCmd.AddCommand(extensionInstallCmd)
|
||||
carapace.Gen(extensionInstallCmd).PositionalCompletion(carapace.ActionFiles().Usage("path to the extension .tar.gz or directory"))
|
||||
completers.RegisterLocalFilePathPositionalCompletion(extensionInstallCmd, 0)
|
||||
|
||||
extensionRmCmd := &cobra.Command{
|
||||
Use: consts.RmStr,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package filesystem
|
||||
|
||||
import (
|
||||
"github.com/bishopfox/sliver/client/command/completers"
|
||||
"github.com/bishopfox/sliver/client/command/flags"
|
||||
"github.com/bishopfox/sliver/client/command/help"
|
||||
"github.com/bishopfox/sliver/client/command/loot"
|
||||
@@ -174,6 +175,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
carapace.ActionValues().Usage("path to the file or directory to download"),
|
||||
carapace.ActionFiles().Usage("local path where the downloaded file will be saved (optional)"),
|
||||
)
|
||||
completers.RegisterLocalFilePathPositionalCompletion(downloadCmd, 1)
|
||||
|
||||
uploadCmd := &cobra.Command{
|
||||
Use: consts.UploadStr,
|
||||
@@ -194,6 +196,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
carapace.ActionFiles().Usage("local path to the file to upload"),
|
||||
carapace.ActionValues().Usage("path to the file or directory to upload to (optional)"),
|
||||
)
|
||||
completers.RegisterLocalFilePathPositionalCompletion(uploadCmd, 0)
|
||||
|
||||
memfilesCmd := &cobra.Command{
|
||||
Use: consts.MemfilesStr,
|
||||
|
||||
@@ -3,6 +3,7 @@ package generate
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/completers"
|
||||
"github.com/bishopfox/sliver/client/command/flags"
|
||||
"github.com/bishopfox/sliver/client/command/help"
|
||||
shellcodeencoders "github.com/bishopfox/sliver/client/command/shellcode-encoders"
|
||||
@@ -94,6 +95,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
f.BoolP("skip-tests", "s", false, "skip testing the traffic encoder (not recommended)")
|
||||
})
|
||||
carapace.Gen(trafficEncodersAddCmd).PositionalCompletion(carapace.ActionFiles("wasm").Tag("wasm files").Usage("local file path (expects .wasm)"))
|
||||
completers.RegisterLocalFilePathPositionalCompletion(trafficEncodersAddCmd, 0)
|
||||
trafficEncodersCmd.AddCommand(trafficEncodersAddCmd)
|
||||
|
||||
trafficEncodersRmCmd := &cobra.Command{
|
||||
@@ -126,6 +128,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
flags.BindFlagCompletions(regenerateCmd, func(comp *carapace.ActionMap) {
|
||||
(*comp)["save"] = carapace.ActionFiles().Tag("directory/file to save implant")
|
||||
})
|
||||
completers.RegisterLocalFilePathFlagCompletion(regenerateCmd, "save")
|
||||
carapace.Gen(regenerateCmd).PositionalCompletion(ImplantBuildNameCompleter(con))
|
||||
|
||||
// [ Profiles ] --------------------------------------------------------------
|
||||
@@ -162,6 +165,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
(*comp)["save"] = carapace.ActionFiles().Tag("directory/file to save implant")
|
||||
(*comp)["shellcode-encoder"] = shellcodeencoders.ShellcodeEncoderNameCompleter(con)
|
||||
})
|
||||
completers.RegisterLocalFilePathFlagCompletion(profilesGenerateCmd, "save")
|
||||
carapace.Gen(profilesGenerateCmd).PositionalCompletion(ProfileNameCompleter(con))
|
||||
profilesCmd.AddCommand(profilesGenerateCmd)
|
||||
|
||||
@@ -193,6 +197,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
f.BoolP("prepend-size", "p", false, "Prepend stage size")
|
||||
f.StringP("compress", "c", "", "Compress stage (zlib, gzip, deflate9 or deflate)")
|
||||
})
|
||||
completers.RegisterLocalFilePathFlagCompletion(profilesStageCmd, "save")
|
||||
|
||||
carapace.Gen(profilesStageCmd).PositionalCompletion(ProfileNameCompleter(con))
|
||||
profilesCmd.AddCommand(profilesStageCmd)
|
||||
@@ -432,12 +437,13 @@ func coreImplantFlagCompletions(cmd *cobra.Command, con *console.SliverClient) {
|
||||
(*comp)["arch"] = ArchCompleter(con)
|
||||
(*comp)["strategy"] = carapace.ActionValuesDescribed([]string{"r", "random", "rd", "random domain", "s", "sequential"}...).Tag("C2 strategy")
|
||||
(*comp)["format"] = FormatCompleter()
|
||||
(*comp)["save"] = carapace.ActionFiles().Tag("directory/file to save implant")
|
||||
(*comp)["save"] = completers.LocalFilePathCompleter().Tag("directory/file to save implant")
|
||||
(*comp)[spoofMetadataFlagName] = carapace.ActionFiles().Tag("optional donor metadata file")
|
||||
(*comp)["shellcode-encoder"] = shellcodeencoders.ShellcodeEncoderNameCompleter(con)
|
||||
(*comp)["traffic-encoders"] = TrafficEncodersCompleter(con).UniqueList(",")
|
||||
(*comp)["c2profile"] = HTTPC2Completer(con)
|
||||
})
|
||||
completers.RegisterLocalFilePathFlagCompletions(cmd, "save", "debug-file", spoofMetadataFlagName)
|
||||
registerImplantTargetFlagCompletions(cmd, con)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package generate
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func TestGenerateSaveFlagCompletionRegistered(t *testing.T) {
|
||||
cmds := Commands(&console.SliverClient{})
|
||||
|
||||
generateCmd := commandByUse(cmds, consts.GenerateStr)
|
||||
if generateCmd == nil {
|
||||
t.Fatalf("missing %q command", consts.GenerateStr)
|
||||
}
|
||||
assertFlagHasDefaultFileCompletion(t, generateCmd, "save")
|
||||
|
||||
beaconCmd := commandByUse(generateCmd.Commands(), consts.BeaconStr)
|
||||
if beaconCmd == nil {
|
||||
t.Fatalf("missing %q subcommand", consts.BeaconStr)
|
||||
}
|
||||
assertFlagHasDefaultFileCompletion(t, beaconCmd, "save")
|
||||
}
|
||||
|
||||
func assertFlagHasDefaultFileCompletion(t *testing.T, cmd *cobra.Command, flag string) {
|
||||
t.Helper()
|
||||
|
||||
completionFn, ok := cmd.GetFlagCompletionFunc(flag)
|
||||
if !ok {
|
||||
t.Fatalf("%q missing %q flag completion func", cmd.Use, flag)
|
||||
}
|
||||
|
||||
values, directive := completionFn(cmd, nil, "")
|
||||
if len(values) != 0 {
|
||||
t.Fatalf("%q %q completion should return no explicit values, got %d", cmd.Use, flag, len(values))
|
||||
}
|
||||
if directive != cobra.ShellCompDirectiveDefault {
|
||||
t.Fatalf("%q %q completion directive mismatch: got=%v want=%v", cmd.Use, flag, directive, cobra.ShellCompDirectiveDefault)
|
||||
}
|
||||
}
|
||||
|
||||
func commandByUse(cmds []*cobra.Command, use string) *cobra.Command {
|
||||
for _, cmd := range cmds {
|
||||
if cmd.Use == use {
|
||||
return cmd
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -19,6 +19,7 @@ package jobs
|
||||
*/
|
||||
|
||||
import (
|
||||
"github.com/bishopfox/sliver/client/command/completers"
|
||||
"github.com/bishopfox/sliver/client/command/flags"
|
||||
"github.com/bishopfox/sliver/client/command/generate"
|
||||
"github.com/bishopfox/sliver/client/command/help"
|
||||
@@ -155,6 +156,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
(*comp)["cert"] = carapace.ActionFiles().Tag("certificate file")
|
||||
(*comp)["key"] = carapace.ActionFiles().Tag("key file")
|
||||
})
|
||||
completers.RegisterLocalFilePathFlagCompletions(httpsCmd, "cert", "key")
|
||||
registerWebsiteFlagCompletion(httpsCmd, "website", con)
|
||||
|
||||
// Staging listeners
|
||||
@@ -185,6 +187,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
(*comp)["key"] = carapace.ActionFiles().Tag("key file")
|
||||
(*comp)["compress"] = carapace.ActionValues([]string{"zlib", "gzip", "deflate9", "none"}...).Tag("compression formats")
|
||||
})
|
||||
completers.RegisterLocalFilePathFlagCompletions(stageCmd, "cert", "key")
|
||||
|
||||
return []*cobra.Command{jobsCmd, mtlsCmd, wgCmd, dnsCmd, httpCmd, httpsCmd, stageCmd}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package loot
|
||||
|
||||
import (
|
||||
"github.com/bishopfox/sliver/client/command/completers"
|
||||
"github.com/rsteube/carapace"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
@@ -50,6 +51,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
})
|
||||
carapace.Gen(lootAddCmd).PositionalCompletion(
|
||||
carapace.ActionFiles().Tag("local loot file").Usage("The local file path to the loot"))
|
||||
completers.RegisterLocalFilePathPositionalCompletion(lootAddCmd, 0)
|
||||
|
||||
lootRemoteCmd := &cobra.Command{
|
||||
Use: consts.LootRemoteStr,
|
||||
@@ -99,6 +101,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
(*comp)["save"] = carapace.ActionFiles().Tag("directory/file to save loot")
|
||||
(*comp)["filter"] = FileTypeCompleter(con)
|
||||
})
|
||||
completers.RegisterLocalFilePathFlagCompletion(lootFetchCmd, "save")
|
||||
|
||||
lootRmCmd := &cobra.Command{
|
||||
Use: consts.RmStr,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package processes
|
||||
|
||||
import (
|
||||
"github.com/bishopfox/sliver/client/command/completers"
|
||||
"github.com/rsteube/carapace"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
@@ -56,6 +57,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
flags.BindFlagCompletions(procdumpCmd, func(comp *carapace.ActionMap) {
|
||||
(*comp)["save"] = carapace.ActionFiles()
|
||||
})
|
||||
completers.RegisterLocalFilePathFlagCompletion(procdumpCmd, "save")
|
||||
|
||||
terminateCmd := &cobra.Command{
|
||||
Use: consts.TerminateStr,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
"github.com/bishopfox/sliver/client/command/completers"
|
||||
"github.com/rsteube/carapace"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
@@ -56,6 +57,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
f.StringP("name", "n", "", "name to assign loot (optional)")
|
||||
f.StringP("type", "T", "", "force a specific loot type (file/cred) if looting (optional)")
|
||||
})
|
||||
completers.RegisterLocalFilePathFlagCompletion(registryReadHiveCmd, "save")
|
||||
registryReadCmd.AddCommand(registryReadHiveCmd)
|
||||
|
||||
registryWriteCmd := &cobra.Command{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package screenshot
|
||||
|
||||
import (
|
||||
"github.com/bishopfox/sliver/client/command/completers"
|
||||
"github.com/bishopfox/sliver/client/command/flags"
|
||||
"github.com/bishopfox/sliver/client/command/help"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
@@ -31,6 +32,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
flags.BindFlagCompletions(screenshotCmd, func(comp *carapace.ActionMap) {
|
||||
(*comp)["save"] = carapace.ActionFiles()
|
||||
})
|
||||
completers.RegisterLocalFilePathFlagCompletion(screenshotCmd, "save")
|
||||
|
||||
return []*cobra.Command{screenshotCmd}
|
||||
}
|
||||
@@ -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,6 +1,7 @@
|
||||
package shellcodeencoders
|
||||
|
||||
import (
|
||||
"github.com/bishopfox/sliver/client/command/completers"
|
||||
"github.com/bishopfox/sliver/client/command/flags"
|
||||
"github.com/bishopfox/sliver/client/command/help"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
@@ -48,7 +49,9 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
(*comp)["arch"] = ShellcodeEncoderArchCompleter(con)
|
||||
(*comp)["output"] = carapace.ActionFiles().Tag("output file/directory")
|
||||
})
|
||||
completers.RegisterLocalFilePathFlagCompletion(shellcodeEncodersEncodeCmd, "output")
|
||||
carapace.Gen(shellcodeEncodersEncodeCmd).PositionalCompletion(carapace.ActionFiles().Tag("shellcode file"))
|
||||
completers.RegisterLocalFilePathPositionalAnyCompletion(shellcodeEncodersEncodeCmd)
|
||||
shellcodeEncodersCmd.AddCommand(shellcodeEncodersEncodeCmd)
|
||||
|
||||
return []*cobra.Command{shellcodeEncodersCmd}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package sgn
|
||||
|
||||
import (
|
||||
"github.com/bishopfox/sliver/client/command/completers"
|
||||
"github.com/rsteube/carapace"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
@@ -34,7 +35,9 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
(*comp)["arch"] = carapace.ActionValues("386", "amd64").Tag("shikata-ga-nai architectures")
|
||||
(*comp)["save"] = carapace.ActionFiles().Tag("directory/file to save shellcode")
|
||||
})
|
||||
completers.RegisterLocalFilePathFlagCompletion(shikataGaNaiCmd, "save")
|
||||
carapace.Gen(shikataGaNaiCmd).PositionalCompletion(carapace.ActionFiles().Tag("shellcode file"))
|
||||
completers.RegisterLocalFilePathPositionalCompletion(shikataGaNaiCmd, 0)
|
||||
|
||||
return []*cobra.Command{shikataGaNaiCmd}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
flags.BindFlagCompletions(updateCmd, func(comp *carapace.ActionMap) {
|
||||
(*comp)["proxy"] = completers.LocalProxyCompleter()
|
||||
})
|
||||
completers.RegisterLocalFilePathFlagCompletion(updateCmd, "save")
|
||||
|
||||
versionCmd := &cobra.Command{
|
||||
Use: consts.VersionStr,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package wasm
|
||||
|
||||
import (
|
||||
"github.com/bishopfox/sliver/client/command/completers"
|
||||
"github.com/bishopfox/sliver/client/command/flags"
|
||||
"github.com/bishopfox/sliver/client/command/help"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
@@ -35,9 +36,11 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
(*comp)["file"] = carapace.ActionFiles()
|
||||
(*comp)["dir"] = carapace.ActionDirectories()
|
||||
})
|
||||
completers.RegisterLocalFilePathFlagCompletion(wasmCmd, "file")
|
||||
wasmComp := carapace.Gen(wasmCmd)
|
||||
wasmComp.PositionalCompletion(carapace.ActionFiles().Usage("wasm/wasi module file (.wasm)"))
|
||||
wasmComp.PositionalAnyCompletion(carapace.ActionValues().Usage("arguments to pass to the wasm module (optional)"))
|
||||
completers.RegisterLocalFilePathPositionalCompletion(wasmCmd, 0)
|
||||
|
||||
wasmLsCmd := &cobra.Command{
|
||||
Use: consts.LsStr,
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/completers"
|
||||
"github.com/bishopfox/sliver/client/command/flags"
|
||||
"github.com/bishopfox/sliver/client/command/help"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
@@ -127,6 +128,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
(*comp)["content"] = carapace.ActionFiles().Tag("content directory/files")
|
||||
(*comp)["website"] = WebsiteNameCompleter(con)
|
||||
})
|
||||
completers.RegisterLocalFilePathFlagCompletion(websitesContentCmd, "content")
|
||||
websitesCmd.AddCommand(websitesContentCmd)
|
||||
|
||||
websitesContentTypeCmd := &cobra.Command{
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package wireguard
|
||||
|
||||
import (
|
||||
"github.com/bishopfox/sliver/client/command/completers"
|
||||
"github.com/bishopfox/sliver/client/command/flags"
|
||||
"github.com/bishopfox/sliver/client/command/help"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
@@ -31,6 +32,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
flags.BindFlagCompletions(wgConfigCmd, func(comp *carapace.ActionMap) {
|
||||
(*comp)["save"] = carapace.ActionFiles().Tag("directory/file to save config")
|
||||
})
|
||||
completers.RegisterLocalFilePathFlagCompletion(wgConfigCmd, "save")
|
||||
|
||||
return []*cobra.Command{wgConfigCmd}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
-214
@@ -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,222 +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 / 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
|
||||
@@ -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
|
||||
@@ -47,9 +49,9 @@ require (
|
||||
github.com/reeflective/readline v1.1.4
|
||||
github.com/rsteube/carapace v0.50.2
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/sliverarmory/beignet v0.0.2
|
||||
github.com/sliverarmory/malasada v0.0.2
|
||||
github.com/sliverarmory/wasm-donut v0.0.5
|
||||
github.com/sliverarmory/beignet v0.0.3
|
||||
github.com/sliverarmory/malasada v0.0.3
|
||||
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
|
||||
|
||||
@@ -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,14 +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/malasada v0.0.2 h1:X2+7dNGPWfm4ijsr/rMvZlIMalskdCVpBZUYs50RHIA=
|
||||
github.com/sliverarmory/malasada v0.0.2/go.mod h1:F96IgTOoX72FU995F+Jip0ni8Z5sXiYOwfmlRGobJkQ=
|
||||
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.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=
|
||||
@@ -926,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=
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{}}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
<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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
+1415
File diff suppressed because it is too large.
Load diff
+48
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Loaded 100 of 309 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user