mirror of
https://github.com/anotherhadi/spilltea.git
synced 2026-07-06 20:42:33 +02:00
Compare commits
16 Commits
v0.0.6
...
6128dcc15e
| Author | SHA1 | Date | |
|---|---|---|---|
| 6128dcc15e | |||
| 41d5366952 | |||
| 8683d17242 | |||
| 2e61ffa125 | |||
| 9ac6040629 | |||
| c7523e69e7 | |||
| aa458c142f | |||
| 0b395e018a | |||
| 5841ed0a95 | |||
| 9cabe81771 | |||
| 021090f52c | |||
| 0b9e1a1cf0 | |||
| f60cdf2033 | |||
| 6af1388652 | |||
| a708830309 | |||
| 44b3c67a37 |
@@ -1,27 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
CURRENT_HASH=$(grep -oP '(?<=vendorHash = ")[^"]+' flake.nix)
|
||||
|
||||
go mod vendor
|
||||
|
||||
COMPUTED_HASH=$(nix hash path vendor/)
|
||||
|
||||
rm -rf vendor/
|
||||
|
||||
if [ "$CURRENT_HASH" = "$COMPUTED_HASH" ]; then
|
||||
echo "vendorHash is up to date"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Updating vendorHash in flake.nix..."
|
||||
python3 -c "
|
||||
import sys
|
||||
with open('flake.nix', 'r') as f:
|
||||
content = f.read()
|
||||
content = content.replace('$CURRENT_HASH', '$COMPUTED_HASH')
|
||||
with open('flake.nix', 'w') as f:
|
||||
f.write(content)
|
||||
"
|
||||
echo " Old: $CURRENT_HASH"
|
||||
echo " New: $COMPUTED_HASH"
|
||||
@@ -9,10 +9,10 @@
|
||||
> A minimal, terminal-based HTTP(S) proxy for pentesters and CTF players.
|
||||
> Think Burp Suite or Caido, but entirely in your terminal.
|
||||
|
||||
[](go.mod)
|
||||
[](https://github.com/anotherhadi/spilltea/releases)
|
||||
[](LICENSE)
|
||||
[](https://goreportcard.com/report/github.com/anotherhadi/spilltea)
|
||||

|
||||

|
||||

|
||||
[](https://ko-fi.com/anotherhadi)
|
||||
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
@@ -25,6 +25,7 @@
|
||||
- [Configuration](#configuration)
|
||||
- [CLI Flags](#cli-flags)
|
||||
- [Plugin System](#plugin-system)
|
||||
- [Vim / Neovim Integration](#vim--neovim-integration)
|
||||
- [Deployment](#deployment)
|
||||
- [Tech Stack](#tech-stack)
|
||||
|
||||
@@ -39,6 +40,7 @@ It is intentionally minimal. No Electron, no browser, no bloat. Just a fast, key
|
||||
<img alt="demo" src="./.github/assets/demo.gif" width="700" />
|
||||
|
||||
<!-- exec: cat ./docs/legal-disclaimer.md -->
|
||||
|
||||
## Legal Disclaimer
|
||||
|
||||
**This tool is provided for educational purposes and authorized security testing only.**
|
||||
@@ -46,6 +48,7 @@ It is intentionally minimal. No Electron, no browser, no bloat. Just a fast, key
|
||||
Use Spilltea only on systems and networks you own or have explicit written permission to test. Intercepting network traffic without authorization may violate local laws (such as the Computer Fraud and Abuse Act, GDPR, or equivalent legislation in your jurisdiction).
|
||||
|
||||
The author(s) and contributors are not responsible for any misuse, damage, or legal consequences resulting from the use of this software. By using Spilltea, you agree that you are solely responsible for ensuring your usage is lawful and authorized.
|
||||
|
||||
<!-- endexec -->
|
||||
|
||||
## Features
|
||||
@@ -100,6 +103,7 @@ environment.systemPackages = [ inputs.spilltea.packages.${pkgs.system}.default ]
|
||||
</details>
|
||||
|
||||
<!-- exec: cat ./docs/basics.md -->
|
||||
|
||||
## Project Management
|
||||
|
||||
Spilltea organizes work into **projects**. Each project maps to a SQLite database file that stores all intercepted traffic for that session & a log files.
|
||||
@@ -115,6 +119,8 @@ On startup, you choose:
|
||||
Spilltea is fully configured via a YAML file at `~/.config/spilltea/config.yaml`.
|
||||
Check the default configuration with all the options [here](./internal/config/default_config.yaml)
|
||||
|
||||
Colors and styles can be customized using [ilovetui](https://github.com/anotherhadi/ilovetui), which applies theme changes across all compatible TUI applications at once.
|
||||
|
||||
## CLI Flags
|
||||
|
||||
```
|
||||
@@ -130,6 +136,7 @@ Usage: spilltea [flags]
|
||||
--upstream-proxy string upstream proxy URL, e.g. http://user:pass@host:8888 (overrides config)
|
||||
-v, --version print version
|
||||
```
|
||||
|
||||
<!-- endexec -->
|
||||
|
||||
## Plugin System
|
||||
@@ -137,6 +144,15 @@ Usage: spilltea [flags]
|
||||
Spilltea supports plugins written in **Lua**. Plugins are loaded from `~/.config/spilltea/plugins/` by default and do not require recompilation or access to the source code.
|
||||
For a full reference and examples, see the [plugin documentation](./docs/plugins.md) or [plugin examples](./plugins/).
|
||||
|
||||
## Vim / Neovim Integration
|
||||
|
||||
Spilltea pairs naturally with Neovim. If you live in your editor, these plugins let you inspect and transform captured traffic without switching context:
|
||||
|
||||
- **[sttr.nvim](https://github.com/anotherhadi/sttr.nvim)**: Run string transformations (base64, URL encode/decode, hex, hashing, and more) on any selected text directly from Neovim. Handy for decoding values spotted in the proxy history.
|
||||
- **[jwt-tui.nvim](https://github.com/anotherhadi/jwt-tui.nvim)**: Decode and inspect JWT tokens in a floating TUI window. Great for quickly reading tokens intercepted through the proxy.
|
||||
|
||||
Both plugins are built to feel native in a terminal workflow, just like Spilltea.
|
||||
|
||||
## Deployment
|
||||
|
||||
spilltea runs **locally** on the machine used for pentesting or CTF. There is no separate server component.
|
||||
|
||||
+19
-4
@@ -44,7 +44,8 @@ func main() {
|
||||
)
|
||||
flag.CommandLine.SetOutput(os.Stdout)
|
||||
flag.Usage = func() {
|
||||
fmt.Println("Usage: spilltea [flags]\n")
|
||||
fmt.Println("Usage: spilltea [flags]")
|
||||
fmt.Println("")
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
flag.Parse()
|
||||
@@ -121,7 +122,7 @@ func main() {
|
||||
config.Global.App.UpstreamProxy = *flagUpstreamProxy
|
||||
}
|
||||
|
||||
style.Init(config.Global)
|
||||
style.Init()
|
||||
icons.Init(config.Global)
|
||||
keys.Init(config.Global)
|
||||
|
||||
@@ -136,17 +137,31 @@ func main() {
|
||||
}
|
||||
broker := intercept.NewBroker()
|
||||
m := appUI.New(broker, project.Name, project.Path)
|
||||
if _, err := tea.NewProgram(m).Run(); err != nil {
|
||||
final, err := tea.NewProgram(m).Run()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "tui: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if app, ok := final.(appUI.Model); ok {
|
||||
if ferr := app.FatalErr(); ferr != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", ferr)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Run home + app in a single program to avoid a blank flash on transition.
|
||||
root := rootModel{home: homeUI.New(projectDir)}
|
||||
if _, err := tea.NewProgram(root).Run(); err != nil {
|
||||
final, err := tea.NewProgram(root).Run()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "tui: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if r, ok := final.(rootModel); ok {
|
||||
if ferr := r.FatalErr(); ferr != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", ferr)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,3 +58,12 @@ func (m rootModel) View() tea.View {
|
||||
}
|
||||
return m.home.View()
|
||||
}
|
||||
|
||||
func (m rootModel) FatalErr() error {
|
||||
if m.state == rootStateApp {
|
||||
if app, ok := m.app.(appUI.Model); ok {
|
||||
return app.FatalErr()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ On startup, you choose:
|
||||
Spilltea is fully configured via a YAML file at `~/.config/spilltea/config.yaml`.
|
||||
Check the default configuration with all the options [here](./internal/config/default_config.yaml)
|
||||
|
||||
Colors and styles can be customized using [ilovetui](https://github.com/anotherhadi/ilovetui), which applies theme changes across all compatible TUI applications at once.
|
||||
|
||||
## CLI Flags
|
||||
|
||||
<!-- exec: echo '```' && go run ./cmd/spilltea -h && echo '```' -->
|
||||
|
||||
@@ -123,6 +123,26 @@ else
|
||||
log("output: " .. out)
|
||||
end
|
||||
|
||||
-- Send an HTTP request directly (bypasses the proxy pipeline and other plugins).
|
||||
-- method: HTTP method ("GET", "POST", etc.)
|
||||
-- url: full URL to request
|
||||
-- headers: table of request headers (optional, pass {} if unused)
|
||||
-- body: request body string (optional, pass "" if unused)
|
||||
-- options: optional table of options:
|
||||
-- insecure = true skip TLS certificate verification
|
||||
-- Returns: response table, error string (nil on success).
|
||||
local res, err = send_request("POST", "https://example.com/api", {
|
||||
["Authorization"] = "Bearer " .. token,
|
||||
["Content-Type"] = "application/json",
|
||||
}, '{"key":"value"}', { insecure = true })
|
||||
if err then
|
||||
log("request failed: " .. err)
|
||||
else
|
||||
log(tostring(res.status_code))
|
||||
log(res.body)
|
||||
log(res.headers["Content-Type"] or "")
|
||||
end
|
||||
|
||||
-- Return the plugin's config section as a Lua table (parsed from YAML).
|
||||
-- Returns an empty table if no config is set.
|
||||
local cfg = get_config()
|
||||
@@ -154,6 +174,9 @@ plugins:
|
||||
The config block is edited from the **Plugins** page in the TUI.
|
||||
Inside a plugin, call `get_config()` to retrieve the config as a Lua table.
|
||||
|
||||
Global defaults for any plugin can be set in `~/.config/spilltea/config.yaml` under a `plugins` key with the same structure as `plugins.yaml`.
|
||||
These defaults are applied the first time a plugin is loaded in a project; once the plugin has an entry in the project's `plugins.yaml`, the project config takes full precedence and the global defaults are ignored.
|
||||
|
||||
`on_config()` is called once at startup (before `on_start`) and again every time the user saves the config in the TUI.
|
||||
It is the right place to read `get_config()` and populate local variables.
|
||||
|
||||
|
||||
@@ -7,7 +7,10 @@ require (
|
||||
charm.land/bubbletea/v2 v2.0.6
|
||||
charm.land/glamour/v2 v2.0.0
|
||||
charm.land/lipgloss/v2 v2.0.3
|
||||
github.com/andybalholm/brotli v1.0.4
|
||||
github.com/anotherhadi/ilovetui v0.1.6
|
||||
github.com/charmbracelet/x/ansi v0.11.7
|
||||
github.com/klauspost/compress v1.17.8
|
||||
github.com/lqqyt2423/go-mitmproxy v1.8.11
|
||||
github.com/sirupsen/logrus v1.9.4
|
||||
github.com/spf13/pflag v1.0.10
|
||||
@@ -20,7 +23,6 @@ require (
|
||||
|
||||
require (
|
||||
github.com/alecthomas/chroma/v2 v2.24.1 // indirect
|
||||
github.com/andybalholm/brotli v1.0.4 // indirect
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.4.3 // indirect
|
||||
@@ -39,7 +41,6 @@ require (
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/klauspost/compress v1.17.8 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.22 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.23 // indirect
|
||||
|
||||
@@ -16,6 +16,8 @@ github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs
|
||||
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/anotherhadi/ilovetui v0.1.6 h1:NKg+T1DpV08Q4r+iowFrXF+0bTd6Y2f4OFpFwhsfsyY=
|
||||
github.com/anotherhadi/ilovetui v0.1.6/go.mod h1:HVai6u5NGKSMOpmioYpwrN0lSxQjc7HtISUc5hTwvOw=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package config
|
||||
|
||||
type Colors struct {
|
||||
Base00 string `mapstructure:"base00"`
|
||||
Base01 string `mapstructure:"base01"`
|
||||
Base02 string `mapstructure:"base02"`
|
||||
Base03 string `mapstructure:"base03"`
|
||||
Base04 string `mapstructure:"base04"`
|
||||
Base05 string `mapstructure:"base05"`
|
||||
Base06 string `mapstructure:"base06"`
|
||||
Base07 string `mapstructure:"base07"`
|
||||
Base08 string `mapstructure:"base08"`
|
||||
Base09 string `mapstructure:"base09"`
|
||||
Base0A string `mapstructure:"base0a"`
|
||||
Base0B string `mapstructure:"base0b"`
|
||||
Base0C string `mapstructure:"base0c"`
|
||||
Base0D string `mapstructure:"base0d"`
|
||||
Base0E string `mapstructure:"base0e"`
|
||||
Base0F string `mapstructure:"base0f"`
|
||||
}
|
||||
@@ -31,7 +31,6 @@ type Config struct {
|
||||
} `mapstructure:"app"`
|
||||
|
||||
TUI struct {
|
||||
Colors Colors `mapstructure:"colors"`
|
||||
UseNerdfontIcons bool `mapstructure:"use_nerdfont_icons"`
|
||||
DefaultSidebarState string `mapstructure:"default_sidebar_state"`
|
||||
PrettyPrintBody bool `mapstructure:"pretty_print_body"`
|
||||
@@ -54,6 +53,13 @@ type Config struct {
|
||||
} `mapstructure:"history"`
|
||||
|
||||
Keybindings Keybindings `mapstructure:"keybindings"`
|
||||
|
||||
Plugins map[string]GlobalPlugin `mapstructure:"plugins"`
|
||||
}
|
||||
|
||||
type GlobalPlugin struct {
|
||||
Enable *bool `mapstructure:"enable"`
|
||||
Config interface{} `mapstructure:"config"`
|
||||
}
|
||||
|
||||
var Global *Config
|
||||
|
||||
@@ -27,23 +27,6 @@ tui:
|
||||
use_nerdfont_icons: false
|
||||
default_sidebar_state: "expanded" # hidden, collapsed, expanded
|
||||
pretty_print_body: true # auto-indent JSON and HTML response bodies
|
||||
colors:
|
||||
base00: "110F12" # Default Background
|
||||
base01: "1C1920" # Lighter Background (status bars, line numbers)
|
||||
base02: "1D1A26" # Selection Background
|
||||
base03: "514D63" # Comments, Invisibles, faint text
|
||||
base04: "8E8AA0" # Dark Foreground (status bars)
|
||||
base05: "C2BED6" # Default Foreground, Caret, Delimiters
|
||||
base06: "D8D5EA" # Light Foreground (rarely used)
|
||||
base07: "EAE7F7" # Light Background (rarely used)
|
||||
base08: "E07080" # Red: errors, diff deleted
|
||||
base09: "D49070" # Orange: integers, constants, warnings
|
||||
base0a: "C4B060" # Yellow: classes, search highlight
|
||||
base0b: "80B880" # Green: strings, diff inserted, success
|
||||
base0c: "70B8C0" # Cyan: support, regex, escape chars
|
||||
base0d: "9E97F8" # Blue/Accent: functions, headings, primary
|
||||
base0e: "C090E8" # Purple: keywords, storage
|
||||
base0f: "D080A0" # Pink: deprecated, embedded language tags
|
||||
|
||||
keybindings:
|
||||
global:
|
||||
|
||||
@@ -144,3 +144,18 @@ func (d *DB) DeleteAllEntries() error {
|
||||
_, err := d.conn.Exec(`DELETE FROM entries`)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteAllExceptFlagged deletes all unflagged entries. If there are no
|
||||
// unflagged entries (only flagged ones remain), it deletes everything.
|
||||
func (d *DB) DeleteAllExceptFlagged() error {
|
||||
var count int
|
||||
if err := d.conn.QueryRow(`SELECT COUNT(*) FROM entries WHERE flagged = 0`).Scan(&count); err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
_, err := d.conn.Exec(`DELETE FROM entries WHERE flagged = 0`)
|
||||
return err
|
||||
}
|
||||
_, err := d.conn.Exec(`DELETE FROM entries`)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -3,11 +3,16 @@ package plugins
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/anotherhadi/spilltea/internal/config"
|
||||
"github.com/anotherhadi/spilltea/internal/db"
|
||||
goproxy "github.com/lqqyt2423/go-mitmproxy/proxy"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
@@ -197,6 +202,78 @@ func registerUtilities(L *lua.LState, mgr *Manager, p *Plugin) {
|
||||
return 1
|
||||
}))
|
||||
|
||||
L.SetGlobal("send_request", L.NewFunction(func(L *lua.LState) int {
|
||||
method := L.CheckString(1)
|
||||
rawURL := L.CheckString(2)
|
||||
hdrs := L.OptTable(3, L.NewTable())
|
||||
body := L.OptString(4, "")
|
||||
opts := L.OptTable(5, L.NewTable())
|
||||
|
||||
insecure := false
|
||||
if v, ok := L.GetField(opts, "insecure").(lua.LBool); ok {
|
||||
insecure = bool(v)
|
||||
}
|
||||
|
||||
transport := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: insecure},
|
||||
}
|
||||
if up := config.Global.App.UpstreamProxy; up != "" {
|
||||
if proxyURL, err := url.Parse(up); err == nil {
|
||||
transport.Proxy = http.ProxyURL(proxyURL)
|
||||
}
|
||||
}
|
||||
client := &http.Client{
|
||||
Transport: transport,
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
var bodyReader io.Reader
|
||||
if body != "" {
|
||||
bodyReader = strings.NewReader(body)
|
||||
}
|
||||
req, err := http.NewRequest(method, rawURL, bodyReader)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
hdrs.ForEach(func(k, v lua.LValue) {
|
||||
ks, kok := k.(lua.LString)
|
||||
vs, vok := v.(lua.LString)
|
||||
if kok && vok {
|
||||
req.Header.Set(string(ks), string(vs))
|
||||
}
|
||||
})
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
|
||||
result := L.NewTable()
|
||||
L.SetField(result, "status_code", lua.LNumber(resp.StatusCode))
|
||||
L.SetField(result, "body", lua.LString(string(respBody)))
|
||||
respHeaders := L.NewTable()
|
||||
for k, vals := range resp.Header {
|
||||
L.SetField(respHeaders, k, lua.LString(strings.Join(vals, ", ")))
|
||||
}
|
||||
L.SetField(result, "headers", respHeaders)
|
||||
|
||||
L.Push(result)
|
||||
L.Push(lua.LNil)
|
||||
return 2
|
||||
}))
|
||||
|
||||
L.SetGlobal("shell_pipe", L.NewFunction(func(L *lua.LState) int {
|
||||
cmd := L.CheckString(1)
|
||||
input := L.OptString(2, "")
|
||||
@@ -292,6 +369,21 @@ func pushRequest(L *lua.LState, f *goproxy.Flow) *lua.LTable {
|
||||
return 0
|
||||
}))
|
||||
|
||||
L.SetField(t, "set_path", L.NewFunction(func(L *lua.LState) int {
|
||||
r.URL.Path = L.CheckString(2)
|
||||
return 0
|
||||
}))
|
||||
|
||||
L.SetField(t, "set_url", L.NewFunction(func(L *lua.LState) int {
|
||||
parsed, err := url.Parse(L.CheckString(2))
|
||||
if err != nil {
|
||||
log.Printf("[plugin] set_url: %v", err)
|
||||
return 0
|
||||
}
|
||||
r.URL = parsed
|
||||
return 0
|
||||
}))
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
|
||||
@@ -13,8 +13,15 @@ import (
|
||||
"github.com/anotherhadi/spilltea/internal/intercept"
|
||||
goproxy "github.com/lqqyt2423/go-mitmproxy/proxy"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// GlobalDefault holds global defaults for a single plugin, read from config.yaml.
|
||||
type GlobalDefault struct {
|
||||
Enable *bool
|
||||
Config interface{}
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
mu sync.RWMutex
|
||||
plugins []*Plugin
|
||||
@@ -22,6 +29,7 @@ type Manager struct {
|
||||
db *db.DB
|
||||
pluginsFile *PluginsFile
|
||||
broker *intercept.Broker
|
||||
globalDefaults map[string]GlobalDefault
|
||||
|
||||
Notifs chan PluginNotifMsg
|
||||
Quit chan string
|
||||
@@ -48,6 +56,10 @@ func (m *Manager) SetPluginsFile(pf *PluginsFile) {
|
||||
m.pluginsFile = pf
|
||||
}
|
||||
|
||||
func (m *Manager) SetGlobalDefaults(defaults map[string]GlobalDefault) {
|
||||
m.globalDefaults = defaults
|
||||
}
|
||||
|
||||
func (m *Manager) LoadFromDir(dir string) error {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if os.IsNotExist(err) {
|
||||
@@ -72,7 +84,17 @@ func (m *Manager) LoadFromDir(dir string) error {
|
||||
p.Enabled = enabled
|
||||
p.ConfigText = configText
|
||||
} else {
|
||||
m.pluginsFile.register(p.ID, p.Enabled)
|
||||
if def, ok := m.globalDefaults[p.ID]; ok {
|
||||
if def.Enable != nil {
|
||||
p.Enabled = *def.Enable
|
||||
}
|
||||
if def.Config != nil {
|
||||
if raw, err := yaml.Marshal(def.Config); err == nil {
|
||||
p.ConfigText = string(raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
m.pluginsFile.register(p.ID, p.Enabled, p.ConfigText)
|
||||
}
|
||||
}
|
||||
m.mu.Lock()
|
||||
|
||||
@@ -66,9 +66,16 @@ func (pf *PluginsFile) get(id string) (enabled bool, config string, found bool)
|
||||
return e.Enable, string(raw), true
|
||||
}
|
||||
|
||||
func (pf *PluginsFile) register(id string, defaultEnabled bool) {
|
||||
func (pf *PluginsFile) register(id string, defaultEnabled bool, configText string) {
|
||||
if _, ok := pf.data.Plugins[id]; !ok {
|
||||
pf.data.Plugins[id] = pluginFileEntry{Enable: defaultEnabled}
|
||||
entry := pluginFileEntry{Enable: defaultEnabled}
|
||||
if configText != "" {
|
||||
var parsed interface{}
|
||||
if err := yaml.Unmarshal([]byte(configText), &parsed); err == nil {
|
||||
entry.Config = parsed
|
||||
}
|
||||
}
|
||||
pf.data.Plugins[id] = entry
|
||||
_ = pf.save()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
package style
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"charm.land/lipgloss/v2"
|
||||
)
|
||||
|
||||
// PanelContentH returns the usable inner content height for a panel rendered by
|
||||
// RenderWithTitle. It subtracts the two border lines (top + bottom) from the
|
||||
// total panel height.
|
||||
func PanelContentH(totalH int) int {
|
||||
h := totalH - 2
|
||||
if h < 0 {
|
||||
return 0
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// RenderWithTitle renders a lipgloss bordered box with a title embedded in the
|
||||
// top border, matching the border's own foreground color. height is the total
|
||||
// desired output height (including both border lines).
|
||||
func RenderWithTitle(border lipgloss.Style, title, content string, width, height int) string {
|
||||
boxH := height - 1
|
||||
if contentH := boxH - 1; contentH > 0 {
|
||||
lines := strings.Split(content, "\n")
|
||||
if len(lines) > contentH {
|
||||
content = strings.Join(lines[:contentH], "\n")
|
||||
}
|
||||
}
|
||||
box := border.BorderTop(false).Width(width).Height(boxH).Render(content)
|
||||
|
||||
boxWidth := lipgloss.Width(strings.SplitN(box, "\n", 2)[0])
|
||||
label := " " + title + " "
|
||||
fillW := boxWidth - lipgloss.Width(label) - 2
|
||||
if fillW < 0 {
|
||||
fillW = 0
|
||||
}
|
||||
bc := lipgloss.NewStyle().Foreground(border.GetBorderTopForeground())
|
||||
topLine := bc.Render("╭ ") + bc.Render(title) + bc.Render(" "+strings.Repeat("─", fillW)+"╮")
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Left, topLine, box)
|
||||
}
|
||||
@@ -1,83 +1,23 @@
|
||||
package style
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"charm.land/bubbles/v2/paginator"
|
||||
"charm.land/bubbles/v2/textarea"
|
||||
"charm.land/bubbles/v2/viewport"
|
||||
"charm.land/lipgloss/v2"
|
||||
ilovetui "github.com/anotherhadi/ilovetui"
|
||||
)
|
||||
|
||||
func NewViewport() viewport.Model {
|
||||
vp := viewport.New()
|
||||
vp.MouseWheelEnabled = false
|
||||
return vp
|
||||
}
|
||||
|
||||
func ViewportView(vp *viewport.Model) string {
|
||||
v := vp.View()
|
||||
if vp.AtBottom() {
|
||||
return v
|
||||
}
|
||||
lines := strings.Split(v, "\n")
|
||||
if len(lines) == 0 {
|
||||
return v
|
||||
}
|
||||
arrow := lipgloss.NewStyle().Foreground(S.Subtle).Render("↓")
|
||||
arrowW := lipgloss.Width(arrow)
|
||||
inner := vp.Width() - 2*arrowW
|
||||
if inner < 0 {
|
||||
inner = 0
|
||||
}
|
||||
lines[len(lines)-1] = arrow + strings.Repeat(" ", inner) + arrow
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func NewPaginator() paginator.Model {
|
||||
p := paginator.New()
|
||||
p.Type = paginator.Dots
|
||||
p.ActiveDot = S.PagerDotActive
|
||||
p.InactiveDot = S.PagerDotInactive
|
||||
return p
|
||||
}
|
||||
|
||||
func NewTextarea(showLineNumbers bool) textarea.Model {
|
||||
ta := textarea.New()
|
||||
ta.Prompt = ""
|
||||
ta.ShowLineNumbers = showLineNumbers
|
||||
ta.CharLimit = 0
|
||||
ta.EndOfBufferCharacter = '~'
|
||||
ts := ta.Styles()
|
||||
ts.Focused.Base = lipgloss.NewStyle()
|
||||
ts.Blurred.Base = lipgloss.NewStyle()
|
||||
ts.Focused.Text = lipgloss.NewStyle().Foreground(S.Text)
|
||||
ts.Focused.CursorLine = lipgloss.NewStyle().Background(S.Selection).Foreground(S.Text)
|
||||
ts.Focused.CursorLineNumber = lipgloss.NewStyle().Background(S.Selection).Foreground(S.Primary).Bold(true)
|
||||
ts.Focused.LineNumber = lipgloss.NewStyle().Foreground(S.Subtle)
|
||||
ts.Focused.Placeholder = lipgloss.NewStyle().Foreground(S.Subtle)
|
||||
ts.Focused.EndOfBuffer = lipgloss.NewStyle().Foreground(S.SubtleBg)
|
||||
ts.Blurred.Text = lipgloss.NewStyle().Foreground(S.MutedFg)
|
||||
ts.Blurred.LineNumber = lipgloss.NewStyle().Foreground(S.SubtleBg)
|
||||
ts.Blurred.Placeholder = lipgloss.NewStyle().Foreground(S.Subtle)
|
||||
ts.Blurred.EndOfBuffer = lipgloss.NewStyle().Foreground(S.SubtleBg)
|
||||
ta.SetStyles(ts)
|
||||
return ta
|
||||
}
|
||||
|
||||
func SeverityStyle(sev string) lipgloss.Style {
|
||||
base := lipgloss.NewStyle().Bold(true)
|
||||
switch sev {
|
||||
case "critical":
|
||||
return base.Foreground(S.Error)
|
||||
return base.Foreground(ilovetui.S.Error)
|
||||
case "high":
|
||||
return base.Foreground(S.Warning)
|
||||
return base.Foreground(ilovetui.S.Warning)
|
||||
case "medium":
|
||||
return base.Foreground(S.Primary)
|
||||
return base.Foreground(ilovetui.S.Primary)
|
||||
case "low":
|
||||
return base.Foreground(S.Success)
|
||||
return base.Foreground(ilovetui.S.Success)
|
||||
default:
|
||||
return base.Foreground(S.Subtle)
|
||||
return base.Foreground(ilovetui.S.Subtle)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,22 +25,12 @@ func StatusStyle(code, width int) lipgloss.Style {
|
||||
base := lipgloss.NewStyle().Bold(true).Width(width)
|
||||
switch {
|
||||
case code >= 500:
|
||||
return base.Foreground(S.Error)
|
||||
return base.Foreground(ilovetui.S.Error)
|
||||
case code >= 400:
|
||||
return base.Foreground(S.Warning)
|
||||
return base.Foreground(ilovetui.S.Warning)
|
||||
case code >= 300:
|
||||
return base.Foreground(S.Primary)
|
||||
return base.Foreground(ilovetui.S.Primary)
|
||||
default:
|
||||
return base.Foreground(S.Success)
|
||||
return base.Foreground(ilovetui.S.Success)
|
||||
}
|
||||
}
|
||||
|
||||
// SplitH splits totalHeight into top and bottom sections, accounting for the
|
||||
// status bar height.
|
||||
func SplitH(totalHeight int, statusBar string, ratio float64) (top, bottom int) {
|
||||
statusH := strings.Count(statusBar, "\n") + 1
|
||||
available := totalHeight - statusH
|
||||
top = int(float64(available) * ratio)
|
||||
bottom = available - top
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,236 +0,0 @@
|
||||
package style
|
||||
|
||||
import (
|
||||
"github.com/anotherhadi/spilltea/internal/config"
|
||||
|
||||
"charm.land/glamour/v2/ansi"
|
||||
)
|
||||
|
||||
func GlamourStyleConfig(cfg *config.Config) ansi.StyleConfig {
|
||||
c := cfg.TUI.Colors
|
||||
|
||||
str := func(s string) *string { return &s }
|
||||
hex := func(base string) *string { return str("#" + base) }
|
||||
boolPtr := func(b bool) *bool { return &b }
|
||||
uintPtr := func(u uint) *uint { return &u }
|
||||
|
||||
return ansi.StyleConfig{
|
||||
Document: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
BlockPrefix: "\n",
|
||||
BlockSuffix: "\n",
|
||||
Color: hex(c.Base05),
|
||||
},
|
||||
Margin: uintPtr(2),
|
||||
},
|
||||
BlockQuote: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Color: hex(c.Base03),
|
||||
Italic: boolPtr(true),
|
||||
},
|
||||
Indent: uintPtr(1),
|
||||
IndentToken: str("│ "),
|
||||
},
|
||||
List: ansi.StyleList{
|
||||
LevelIndent: 2,
|
||||
},
|
||||
Heading: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
BlockSuffix: "\n",
|
||||
Color: hex(c.Base0D),
|
||||
Bold: boolPtr(true),
|
||||
},
|
||||
},
|
||||
H1: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: " ",
|
||||
Suffix: " ",
|
||||
Color: hex(c.Base07),
|
||||
BackgroundColor: hex(c.Base0D),
|
||||
Bold: boolPtr(true),
|
||||
},
|
||||
},
|
||||
H2: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: "## ",
|
||||
Color: hex(c.Base0D),
|
||||
Bold: boolPtr(true),
|
||||
},
|
||||
},
|
||||
H3: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: "### ",
|
||||
Color: hex(c.Base0C),
|
||||
},
|
||||
},
|
||||
H4: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: "#### ",
|
||||
Color: hex(c.Base0B),
|
||||
},
|
||||
},
|
||||
H5: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: "##### ",
|
||||
Color: hex(c.Base09),
|
||||
},
|
||||
},
|
||||
H6: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: "###### ",
|
||||
Color: hex(c.Base08),
|
||||
Bold: boolPtr(false),
|
||||
},
|
||||
},
|
||||
Strikethrough: ansi.StylePrimitive{
|
||||
CrossedOut: boolPtr(true),
|
||||
},
|
||||
Emph: ansi.StylePrimitive{
|
||||
Italic: boolPtr(true),
|
||||
},
|
||||
Strong: ansi.StylePrimitive{
|
||||
Bold: boolPtr(true),
|
||||
},
|
||||
HorizontalRule: ansi.StylePrimitive{
|
||||
Color: hex(c.Base03),
|
||||
Format: "\n--------\n",
|
||||
},
|
||||
Item: ansi.StylePrimitive{
|
||||
BlockPrefix: "• ",
|
||||
},
|
||||
Enumeration: ansi.StylePrimitive{
|
||||
BlockPrefix: ". ",
|
||||
},
|
||||
Task: ansi.StyleTask{
|
||||
Ticked: "[✓] ",
|
||||
Unticked: "[ ] ",
|
||||
},
|
||||
Link: ansi.StylePrimitive{
|
||||
Color: hex(c.Base0C),
|
||||
Underline: boolPtr(true),
|
||||
},
|
||||
LinkText: ansi.StylePrimitive{
|
||||
Color: hex(c.Base0D),
|
||||
Bold: boolPtr(true),
|
||||
},
|
||||
Image: ansi.StylePrimitive{
|
||||
Color: hex(c.Base0C),
|
||||
Underline: boolPtr(true),
|
||||
},
|
||||
ImageText: ansi.StylePrimitive{
|
||||
Color: hex(c.Base04),
|
||||
Format: "Image: {{.text}} ->",
|
||||
},
|
||||
Code: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: " ",
|
||||
Suffix: " ",
|
||||
Color: hex(c.Base0B),
|
||||
BackgroundColor: hex(c.Base01),
|
||||
},
|
||||
},
|
||||
CodeBlock: ansi.StyleCodeBlock{
|
||||
StyleBlock: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Color: hex(c.Base04),
|
||||
},
|
||||
Margin: uintPtr(2),
|
||||
},
|
||||
Chroma: &ansi.Chroma{
|
||||
Text: ansi.StylePrimitive{
|
||||
Color: hex(c.Base05),
|
||||
},
|
||||
Error: ansi.StylePrimitive{
|
||||
Color: hex(c.Base07),
|
||||
BackgroundColor: hex(c.Base08),
|
||||
},
|
||||
Comment: ansi.StylePrimitive{
|
||||
Color: hex(c.Base03),
|
||||
Italic: boolPtr(true),
|
||||
},
|
||||
CommentPreproc: ansi.StylePrimitive{
|
||||
Color: hex(c.Base09),
|
||||
},
|
||||
Keyword: ansi.StylePrimitive{
|
||||
Color: hex(c.Base0E),
|
||||
},
|
||||
KeywordReserved: ansi.StylePrimitive{
|
||||
Color: hex(c.Base0E),
|
||||
},
|
||||
KeywordNamespace: ansi.StylePrimitive{
|
||||
Color: hex(c.Base0D),
|
||||
},
|
||||
KeywordType: ansi.StylePrimitive{
|
||||
Color: hex(c.Base0A),
|
||||
},
|
||||
Operator: ansi.StylePrimitive{
|
||||
Color: hex(c.Base05),
|
||||
},
|
||||
Punctuation: ansi.StylePrimitive{
|
||||
Color: hex(c.Base05),
|
||||
},
|
||||
Name: ansi.StylePrimitive{
|
||||
Color: hex(c.Base05),
|
||||
},
|
||||
NameBuiltin: ansi.StylePrimitive{
|
||||
Color: hex(c.Base0D),
|
||||
},
|
||||
NameTag: ansi.StylePrimitive{
|
||||
Color: hex(c.Base08),
|
||||
},
|
||||
NameAttribute: ansi.StylePrimitive{
|
||||
Color: hex(c.Base0A),
|
||||
},
|
||||
NameClass: ansi.StylePrimitive{
|
||||
Color: hex(c.Base0A),
|
||||
Bold: boolPtr(true),
|
||||
Underline: boolPtr(true),
|
||||
},
|
||||
NameConstant: ansi.StylePrimitive{
|
||||
Color: hex(c.Base09),
|
||||
},
|
||||
NameDecorator: ansi.StylePrimitive{
|
||||
Color: hex(c.Base0C),
|
||||
},
|
||||
NameFunction: ansi.StylePrimitive{
|
||||
Color: hex(c.Base0D),
|
||||
},
|
||||
LiteralNumber: ansi.StylePrimitive{
|
||||
Color: hex(c.Base09),
|
||||
},
|
||||
LiteralString: ansi.StylePrimitive{
|
||||
Color: hex(c.Base0B),
|
||||
},
|
||||
LiteralStringEscape: ansi.StylePrimitive{
|
||||
Color: hex(c.Base0C),
|
||||
},
|
||||
GenericDeleted: ansi.StylePrimitive{
|
||||
Color: hex(c.Base08),
|
||||
},
|
||||
GenericEmph: ansi.StylePrimitive{
|
||||
Italic: boolPtr(true),
|
||||
},
|
||||
GenericInserted: ansi.StylePrimitive{
|
||||
Color: hex(c.Base0B),
|
||||
},
|
||||
GenericStrong: ansi.StylePrimitive{
|
||||
Bold: boolPtr(true),
|
||||
},
|
||||
GenericSubheading: ansi.StylePrimitive{
|
||||
Color: hex(c.Base04),
|
||||
},
|
||||
Background: ansi.StylePrimitive{
|
||||
BackgroundColor: hex(c.Base01),
|
||||
},
|
||||
},
|
||||
},
|
||||
Table: ansi.StyleTable{
|
||||
StyleBlock: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{},
|
||||
},
|
||||
},
|
||||
DefinitionDescription: ansi.StylePrimitive{
|
||||
BlockPrefix: "\n> ",
|
||||
},
|
||||
}
|
||||
}
|
||||
+26
-25
@@ -3,12 +3,13 @@ package style
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"image/color"
|
||||
"strings"
|
||||
|
||||
"charm.land/lipgloss/v2"
|
||||
ilovetui "github.com/anotherhadi/ilovetui"
|
||||
"github.com/anotherhadi/spilltea/internal/config"
|
||||
"golang.org/x/net/html"
|
||||
"image/color"
|
||||
)
|
||||
|
||||
func Paint(c color.Color, s string) string {
|
||||
@@ -75,8 +76,8 @@ func highlightHeaders(raw string) string {
|
||||
} else if trimmed == "" {
|
||||
out.WriteString(line)
|
||||
} else if idx := strings.Index(trimmed, ": "); idx != -1 {
|
||||
out.WriteString(Paint(S.Subtle, trimmed[:idx+2]))
|
||||
out.WriteString(Paint(S.Text, trimmed[idx+2:]))
|
||||
out.WriteString(Paint(ilovetui.S.Subtle, trimmed[:idx+2]))
|
||||
out.WriteString(Paint(ilovetui.S.Text, trimmed[idx+2:]))
|
||||
} else {
|
||||
out.WriteString(line)
|
||||
}
|
||||
@@ -95,16 +96,16 @@ func highlightStatusLine(line string) string {
|
||||
switch parts[0] {
|
||||
case "GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "CONNECT", "TRACE":
|
||||
result := S.Method(parts[0]).Width(0).Render(parts[0]) + " "
|
||||
result += Paint(S.Primary, parts[1])
|
||||
result += Paint(ilovetui.S.Primary, parts[1])
|
||||
if len(parts) == 3 {
|
||||
result += " " + Paint(S.Subtle, parts[2])
|
||||
result += " " + Paint(ilovetui.S.Subtle, parts[2])
|
||||
}
|
||||
return result
|
||||
}
|
||||
result := Paint(S.Subtle, parts[0]) + " "
|
||||
result += Paint(S.Warning, parts[1])
|
||||
result := Paint(ilovetui.S.Subtle, parts[0]) + " "
|
||||
result += Paint(ilovetui.S.Warning, parts[1])
|
||||
if len(parts) == 3 {
|
||||
result += " " + Paint(S.MutedFg, parts[2])
|
||||
result += " " + Paint(ilovetui.S.Muted, parts[2])
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -134,9 +135,9 @@ func highlightJSON(s string) string {
|
||||
k++
|
||||
}
|
||||
if k < n && s[k] == ':' {
|
||||
out.WriteString(Paint(S.Primary, str))
|
||||
out.WriteString(Paint(ilovetui.S.Primary, str))
|
||||
} else {
|
||||
out.WriteString(Paint(S.Success, str))
|
||||
out.WriteString(Paint(ilovetui.S.Success, str))
|
||||
}
|
||||
i = j
|
||||
case (ch >= '0' && ch <= '9') || (ch == '-' && i+1 < n && s[i+1] >= '0' && s[i+1] <= '9'):
|
||||
@@ -147,19 +148,19 @@ func highlightJSON(s string) string {
|
||||
for j < n && ((s[j] >= '0' && s[j] <= '9') || s[j] == '.' || s[j] == 'e' || s[j] == 'E' || s[j] == '+' || s[j] == '-') {
|
||||
j++
|
||||
}
|
||||
out.WriteString(Paint(S.Warning, s[i:j]))
|
||||
out.WriteString(Paint(ilovetui.S.Warning, s[i:j]))
|
||||
i = j
|
||||
case i+4 <= n && s[i:i+4] == "true":
|
||||
out.WriteString(Paint(S.Error, "true"))
|
||||
out.WriteString(Paint(ilovetui.S.Error, "true"))
|
||||
i += 4
|
||||
case i+5 <= n && s[i:i+5] == "false":
|
||||
out.WriteString(Paint(S.Error, "false"))
|
||||
out.WriteString(Paint(ilovetui.S.Error, "false"))
|
||||
i += 5
|
||||
case i+4 <= n && s[i:i+4] == "null":
|
||||
out.WriteString(Paint(S.Error, "null"))
|
||||
out.WriteString(Paint(ilovetui.S.Error, "null"))
|
||||
i += 4
|
||||
case ch == '{' || ch == '}' || ch == '[' || ch == ']' || ch == ':' || ch == ',':
|
||||
out.WriteString(Paint(S.Subtle, string(ch)))
|
||||
out.WriteString(Paint(ilovetui.S.Subtle, string(ch)))
|
||||
i++
|
||||
default:
|
||||
out.WriteByte(ch)
|
||||
@@ -265,11 +266,11 @@ func highlightHTML(s string) string {
|
||||
if i+4 <= n && s[i:i+4] == "<!--" {
|
||||
end := strings.Index(s[i:], "-->")
|
||||
if end == -1 {
|
||||
out.WriteString(Paint(S.Subtle, s[i:]))
|
||||
out.WriteString(Paint(ilovetui.S.Subtle, s[i:]))
|
||||
break
|
||||
}
|
||||
end = i + end + 3
|
||||
out.WriteString(Paint(S.Subtle, s[i:end]))
|
||||
out.WriteString(Paint(ilovetui.S.Subtle, s[i:end]))
|
||||
i = end
|
||||
continue
|
||||
}
|
||||
@@ -278,10 +279,10 @@ func highlightHTML(s string) string {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
out.WriteString(Paint(S.Subtle, "<"))
|
||||
out.WriteString(Paint(ilovetui.S.Subtle, "<"))
|
||||
i++
|
||||
if i < n && (s[i] == '/' || s[i] == '!') {
|
||||
out.WriteString(Paint(S.Subtle, string(s[i])))
|
||||
out.WriteString(Paint(ilovetui.S.Subtle, string(s[i])))
|
||||
i++
|
||||
}
|
||||
j := i
|
||||
@@ -289,7 +290,7 @@ func highlightHTML(s string) string {
|
||||
j++
|
||||
}
|
||||
if j > i {
|
||||
out.WriteString(Paint(S.Primary, s[i:j]))
|
||||
out.WriteString(Paint(ilovetui.S.Primary, s[i:j]))
|
||||
i = j
|
||||
}
|
||||
for i < n && s[i] != '>' {
|
||||
@@ -299,10 +300,10 @@ func highlightHTML(s string) string {
|
||||
out.WriteByte(ch)
|
||||
i++
|
||||
case ch == '/':
|
||||
out.WriteString(Paint(S.Subtle, "/"))
|
||||
out.WriteString(Paint(ilovetui.S.Subtle, "/"))
|
||||
i++
|
||||
case ch == '=':
|
||||
out.WriteString(Paint(S.Subtle, "="))
|
||||
out.WriteString(Paint(ilovetui.S.Subtle, "="))
|
||||
i++
|
||||
case ch == '"' || ch == '\'':
|
||||
q := ch
|
||||
@@ -313,19 +314,19 @@ func highlightHTML(s string) string {
|
||||
if j < n {
|
||||
j++
|
||||
}
|
||||
out.WriteString(Paint(S.Success, s[i:j]))
|
||||
out.WriteString(Paint(ilovetui.S.Success, s[i:j]))
|
||||
i = j
|
||||
default:
|
||||
j = i
|
||||
for j < n && s[j] != '=' && s[j] != ' ' && s[j] != '>' && s[j] != '/' && s[j] != '\t' && s[j] != '\n' {
|
||||
j++
|
||||
}
|
||||
out.WriteString(Paint(S.Warning, s[i:j]))
|
||||
out.WriteString(Paint(ilovetui.S.Warning, s[i:j]))
|
||||
i = j
|
||||
}
|
||||
}
|
||||
if i < n && s[i] == '>' {
|
||||
out.WriteString(Paint(S.Subtle, ">"))
|
||||
out.WriteString(Paint(ilovetui.S.Subtle, ">"))
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
+8
-78
@@ -1,34 +1,13 @@
|
||||
package style
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
|
||||
"charm.land/bubbles/v2/help"
|
||||
"charm.land/lipgloss/v2"
|
||||
"github.com/anotherhadi/spilltea/internal/config"
|
||||
ilovetui "github.com/anotherhadi/ilovetui"
|
||||
)
|
||||
|
||||
type Styles struct {
|
||||
Primary color.Color
|
||||
Success color.Color
|
||||
Error color.Color
|
||||
Warning color.Color
|
||||
SubtleBg color.Color
|
||||
Selection color.Color
|
||||
Text color.Color
|
||||
MutedFg color.Color
|
||||
Subtle color.Color
|
||||
|
||||
Bold lipgloss.Style
|
||||
Faint lipgloss.Style
|
||||
|
||||
Panel lipgloss.Style
|
||||
PanelFocused lipgloss.Style
|
||||
PanelEditing lipgloss.Style
|
||||
|
||||
PagerDotActive string
|
||||
PagerDotInactive string
|
||||
|
||||
methodGet lipgloss.Style
|
||||
methodPost lipgloss.Style
|
||||
methodPutPatch lipgloss.Style
|
||||
@@ -38,70 +17,21 @@ type Styles struct {
|
||||
|
||||
var S *Styles
|
||||
|
||||
func Init(cfg *config.Config) {
|
||||
c := cfg.TUI.Colors
|
||||
|
||||
subtleBg := lipgloss.Color("#" + c.Base01) // Lighter Background (status bars)
|
||||
selection := lipgloss.Color("#" + c.Base02) // Selection Background
|
||||
subtle := lipgloss.Color("#" + c.Base03) // Faint text, borders
|
||||
mutedFg := lipgloss.Color("#" + c.Base04) // Muted foreground
|
||||
text := lipgloss.Color("#" + c.Base05) // Default Foreground
|
||||
errCol := lipgloss.Color("#" + c.Base08) // Red: errors
|
||||
warning := lipgloss.Color("#" + c.Base09) // Orange: warnings
|
||||
success := lipgloss.Color("#" + c.Base0B) // Green: success
|
||||
primary := lipgloss.Color("#" + c.Base0D) // Accent: primary
|
||||
purple := lipgloss.Color("#" + c.Base0E) // Purple: editing
|
||||
|
||||
func Init() {
|
||||
methodBase := lipgloss.NewStyle().Bold(true).Width(7)
|
||||
S = &Styles{
|
||||
Primary: primary,
|
||||
Success: success,
|
||||
Error: errCol,
|
||||
Warning: warning,
|
||||
SubtleBg: subtleBg,
|
||||
Selection: selection,
|
||||
MutedFg: mutedFg,
|
||||
Text: text,
|
||||
Subtle: subtle,
|
||||
|
||||
Bold: lipgloss.NewStyle().Bold(true),
|
||||
Faint: lipgloss.NewStyle().Foreground(subtle).Faint(true),
|
||||
|
||||
Panel: lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(subtle),
|
||||
|
||||
PanelFocused: lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(primary),
|
||||
|
||||
PanelEditing: lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(purple),
|
||||
BorderForeground(ilovetui.S.Base0E),
|
||||
|
||||
PagerDotActive: lipgloss.NewStyle().Foreground(primary).SetString("•").String(),
|
||||
PagerDotInactive: lipgloss.NewStyle().Foreground(subtle).SetString("•").String(),
|
||||
|
||||
methodGet: methodBase.Foreground(success),
|
||||
methodPost: methodBase.Foreground(warning),
|
||||
methodPutPatch: methodBase.Foreground(primary),
|
||||
methodDelete: methodBase.Foreground(errCol),
|
||||
methodDefault: methodBase.Foreground(text),
|
||||
methodGet: methodBase.Foreground(ilovetui.S.Success),
|
||||
methodPost: methodBase.Foreground(ilovetui.S.Warning),
|
||||
methodPutPatch: methodBase.Foreground(ilovetui.S.Primary),
|
||||
methodDelete: methodBase.Foreground(ilovetui.S.Error),
|
||||
methodDefault: methodBase.Foreground(ilovetui.S.Text),
|
||||
}
|
||||
}
|
||||
|
||||
func NewHelp() help.Model {
|
||||
h := help.New()
|
||||
h.Styles.ShortKey = lipgloss.NewStyle().Foreground(S.Primary)
|
||||
h.Styles.ShortDesc = lipgloss.NewStyle().Foreground(S.MutedFg)
|
||||
h.Styles.ShortSeparator = lipgloss.NewStyle().Foreground(S.Subtle)
|
||||
h.Styles.FullKey = lipgloss.NewStyle().Foreground(S.Primary)
|
||||
h.Styles.FullDesc = lipgloss.NewStyle().Foreground(S.MutedFg)
|
||||
h.Styles.FullSeparator = lipgloss.NewStyle().Foreground(S.Subtle)
|
||||
h.Styles.Ellipsis = lipgloss.NewStyle().Foreground(S.Subtle)
|
||||
return h
|
||||
}
|
||||
|
||||
func (s *Styles) Method(method string) lipgloss.Style {
|
||||
switch method {
|
||||
case "GET":
|
||||
|
||||
@@ -55,6 +55,7 @@ type Model struct {
|
||||
database *db.DB
|
||||
logFile *os.File
|
||||
pluginManager *plugins.Manager
|
||||
fatalErr error
|
||||
|
||||
width int
|
||||
height int
|
||||
@@ -112,6 +113,14 @@ func New(broker *intercept.Broker, name, path string) Model {
|
||||
}
|
||||
mgr.SetPluginsFile(pf)
|
||||
|
||||
if len(cfg.Plugins) > 0 {
|
||||
defaults := make(map[string]plugins.GlobalDefault, len(cfg.Plugins))
|
||||
for id, gp := range cfg.Plugins {
|
||||
defaults[id] = plugins.GlobalDefault{Enable: gp.Enable, Config: gp.Config}
|
||||
}
|
||||
mgr.SetGlobalDefaults(defaults)
|
||||
}
|
||||
|
||||
pluginsDir := config.ExpandPath(cfg.App.PluginsDir)
|
||||
if err := mgr.LoadFromDir(pluginsDir); err != nil {
|
||||
log.Printf("plugins: %v", err)
|
||||
@@ -127,6 +136,8 @@ func New(broker *intercept.Broker, name, path string) Model {
|
||||
return m
|
||||
}
|
||||
|
||||
func (m Model) FatalErr() error { return m.fatalErr }
|
||||
|
||||
func (m Model) Init() tea.Cmd {
|
||||
mgr := m.pluginManager
|
||||
return tea.Batch(
|
||||
|
||||
+10
-11
@@ -5,7 +5,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"charm.land/lipgloss/v2"
|
||||
"github.com/anotherhadi/spilltea/internal/style"
|
||||
ilovetui "github.com/anotherhadi/ilovetui"
|
||||
)
|
||||
|
||||
type sidebarState string
|
||||
@@ -42,7 +42,6 @@ func (m *Model) renderSidebar() string {
|
||||
if m.sidebarState == sidebarHidden {
|
||||
return ""
|
||||
}
|
||||
s := style.S
|
||||
// content width inside bordered panel
|
||||
inner := m.getSidebarWidth() - 2
|
||||
|
||||
@@ -50,18 +49,18 @@ func (m *Model) renderSidebar() string {
|
||||
if m.sidebarState == sidebarCollapsed {
|
||||
titleText = "SPLT"
|
||||
}
|
||||
title := lipgloss.NewStyle().Width(inner).Bold(true).Foreground(s.Primary).Padding(0, 1).Render(titleText)
|
||||
title := lipgloss.NewStyle().Width(inner).Bold(true).Foreground(ilovetui.S.Primary).Padding(0, 1).Render(titleText)
|
||||
|
||||
divider := strings.Repeat("─", inner)
|
||||
|
||||
badgeSelected := lipgloss.NewStyle().Foreground(s.Primary).Bold(true)
|
||||
badgeNormal := lipgloss.NewStyle().Foreground(s.Subtle)
|
||||
textSelected := lipgloss.NewStyle().Foreground(s.Primary)
|
||||
textNormal := lipgloss.NewStyle().Foreground(s.Text)
|
||||
badgeSelected := lipgloss.NewStyle().Foreground(ilovetui.S.Primary).Bold(true)
|
||||
badgeNormal := lipgloss.NewStyle().Foreground(ilovetui.S.Subtle)
|
||||
textSelected := lipgloss.NewStyle().Foreground(ilovetui.S.Primary)
|
||||
textNormal := lipgloss.NewStyle().Foreground(ilovetui.S.Text)
|
||||
lineStyle := lipgloss.NewStyle().Width(inner).Padding(0, 1)
|
||||
|
||||
var items strings.Builder
|
||||
badgeUnread := lipgloss.NewStyle().Foreground(s.Warning).Bold(true)
|
||||
badgeUnread := lipgloss.NewStyle().Foreground(ilovetui.S.Warning).Bold(true)
|
||||
|
||||
for i, entry := range sidebarEntries {
|
||||
selected := entry.id == m.page
|
||||
@@ -95,13 +94,13 @@ func (m *Model) renderSidebar() string {
|
||||
}
|
||||
parts := []string{
|
||||
title,
|
||||
lipgloss.NewStyle().Width(inner).Foreground(s.Subtle).Padding(0, 1).Render(name),
|
||||
lipgloss.NewStyle().Width(inner).Foreground(ilovetui.S.Subtle).Padding(0, 1).Render(name),
|
||||
}
|
||||
parts = append(parts,
|
||||
lipgloss.NewStyle().Foreground(s.Subtle).Render(divider),
|
||||
lipgloss.NewStyle().Foreground(ilovetui.S.Subtle).Render(divider),
|
||||
items.String(),
|
||||
)
|
||||
body := lipgloss.JoinVertical(lipgloss.Left, parts...)
|
||||
|
||||
return s.Panel.Width(m.getSidebarWidth()).Height(m.height).Render(body)
|
||||
return ilovetui.S.Panel.Width(m.getSidebarWidth()).Height(m.height).Render(body)
|
||||
}
|
||||
|
||||
@@ -107,16 +107,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
case proxyPkg.ErrMsg:
|
||||
if msg.Err != nil {
|
||||
log.Printf("proxy error: %v", msg.Err)
|
||||
return m, tea.Batch(
|
||||
func() tea.Msg {
|
||||
return notificationsUI.NotificationMsg{
|
||||
Title: "Proxy Error",
|
||||
Body: msg.Err.Error(),
|
||||
Kind: notificationsUI.KindError,
|
||||
}
|
||||
},
|
||||
tea.Quit,
|
||||
)
|
||||
m.fatalErr = msg.Err
|
||||
return m, tea.Quit
|
||||
}
|
||||
return m, nil
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ package app
|
||||
import (
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
"github.com/anotherhadi/spilltea/internal/style"
|
||||
ilovetui "github.com/anotherhadi/ilovetui"
|
||||
)
|
||||
|
||||
func (m Model) View() tea.View {
|
||||
@@ -52,5 +52,5 @@ func (m *Model) renderActivePage() string {
|
||||
return e.render(m)
|
||||
}
|
||||
}
|
||||
return style.S.Faint.Render("Work in progress")
|
||||
return ilovetui.S.Faint.Render("Work in progress")
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"charm.land/bubbles/v2/list"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
"github.com/anotherhadi/spilltea/internal/style"
|
||||
ilovetui "github.com/anotherhadi/ilovetui"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -47,20 +47,18 @@ type Model struct {
|
||||
}
|
||||
|
||||
func New() Model {
|
||||
s := style.S
|
||||
|
||||
delegate := list.NewDefaultDelegate()
|
||||
delegate.SetSpacing(0)
|
||||
delegate.Styles.NormalTitle = lipgloss.NewStyle().Foreground(s.Text).PaddingLeft(2)
|
||||
delegate.Styles.NormalDesc = lipgloss.NewStyle().Foreground(s.Subtle).PaddingLeft(2)
|
||||
delegate.Styles.NormalTitle = lipgloss.NewStyle().Foreground(ilovetui.S.Text).PaddingLeft(2)
|
||||
delegate.Styles.NormalDesc = lipgloss.NewStyle().Foreground(ilovetui.S.Subtle).PaddingLeft(2)
|
||||
delegate.Styles.SelectedTitle = lipgloss.NewStyle().
|
||||
Border(lipgloss.NormalBorder(), false, false, false, true).
|
||||
BorderForeground(s.Primary).
|
||||
Foreground(s.Primary).Bold(true).PaddingLeft(1)
|
||||
BorderForeground(ilovetui.S.Primary).
|
||||
Foreground(ilovetui.S.Primary).Bold(true).PaddingLeft(1)
|
||||
delegate.Styles.SelectedDesc = lipgloss.NewStyle().
|
||||
Border(lipgloss.NormalBorder(), false, false, false, true).
|
||||
BorderForeground(s.Primary).
|
||||
Foreground(s.MutedFg).PaddingLeft(1)
|
||||
BorderForeground(ilovetui.S.Primary).
|
||||
Foreground(ilovetui.S.Muted).PaddingLeft(1)
|
||||
|
||||
l := list.New(allItems, delegate, popupW, 8)
|
||||
l.SetShowTitle(false)
|
||||
@@ -125,7 +123,7 @@ func (m Model) popupHeight() int {
|
||||
}
|
||||
|
||||
func (m Model) listHeight() int {
|
||||
return style.PanelContentH(m.popupHeight()) - 1
|
||||
return ilovetui.ContentHeight(m.popupHeight()) - 1
|
||||
}
|
||||
|
||||
func (m Model) extract(id string) string {
|
||||
|
||||
@@ -2,14 +2,12 @@ package copy
|
||||
|
||||
import (
|
||||
"charm.land/lipgloss/v2"
|
||||
"github.com/anotherhadi/spilltea/internal/style"
|
||||
ilovetui "github.com/anotherhadi/ilovetui"
|
||||
copyasUI "github.com/anotherhadi/spilltea/internal/ui/components/copyas"
|
||||
)
|
||||
|
||||
func (m *Model) View(background string) string {
|
||||
s := style.S
|
||||
|
||||
hint := lipgloss.NewStyle().Foreground(s.Subtle).
|
||||
hint := lipgloss.NewStyle().Foreground(ilovetui.S.Subtle).
|
||||
Render(" enter: copy • /: filter • esc: cancel")
|
||||
|
||||
inner := lipgloss.JoinVertical(lipgloss.Left,
|
||||
@@ -19,10 +17,10 @@ func (m *Model) View(background string) string {
|
||||
|
||||
border := lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(s.Primary)
|
||||
BorderForeground(ilovetui.S.Primary)
|
||||
|
||||
popupH := m.popupHeight()
|
||||
popup := style.RenderWithTitle(border, "Copy", inner, m.popupInnerWidth()+2, popupH)
|
||||
popup := ilovetui.RenderWithTitle(border, "Copy", inner, m.popupInnerWidth()+2, popupH)
|
||||
|
||||
return copyasUI.OverlayCenter(background, popup, m.width, m.height)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"charm.land/bubbles/v2/list"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
"github.com/anotherhadi/spilltea/internal/style"
|
||||
ilovetui "github.com/anotherhadi/ilovetui"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -48,20 +48,18 @@ type Model struct {
|
||||
}
|
||||
|
||||
func New() Model {
|
||||
s := style.S
|
||||
|
||||
delegate := list.NewDefaultDelegate()
|
||||
delegate.SetSpacing(0)
|
||||
delegate.Styles.NormalTitle = lipgloss.NewStyle().Foreground(s.Text).PaddingLeft(2)
|
||||
delegate.Styles.NormalDesc = lipgloss.NewStyle().Foreground(s.Subtle).PaddingLeft(2)
|
||||
delegate.Styles.NormalTitle = lipgloss.NewStyle().Foreground(ilovetui.S.Text).PaddingLeft(2)
|
||||
delegate.Styles.NormalDesc = lipgloss.NewStyle().Foreground(ilovetui.S.Subtle).PaddingLeft(2)
|
||||
delegate.Styles.SelectedTitle = lipgloss.NewStyle().
|
||||
Border(lipgloss.NormalBorder(), false, false, false, true).
|
||||
BorderForeground(s.Primary).
|
||||
Foreground(s.Primary).Bold(true).PaddingLeft(1)
|
||||
BorderForeground(ilovetui.S.Primary).
|
||||
Foreground(ilovetui.S.Primary).Bold(true).PaddingLeft(1)
|
||||
delegate.Styles.SelectedDesc = lipgloss.NewStyle().
|
||||
Border(lipgloss.NormalBorder(), false, false, false, true).
|
||||
BorderForeground(s.Primary).
|
||||
Foreground(s.MutedFg).PaddingLeft(1)
|
||||
BorderForeground(ilovetui.S.Primary).
|
||||
Foreground(ilovetui.S.Muted).PaddingLeft(1)
|
||||
|
||||
l := list.New(allFormats, delegate, popupW, 8)
|
||||
l.SetShowTitle(false)
|
||||
@@ -116,5 +114,5 @@ func (m Model) popupHeight() int {
|
||||
|
||||
// listHeight = panel content area - hint line (1)
|
||||
func (m Model) listHeight() int {
|
||||
return style.PanelContentH(m.popupHeight()) - 1
|
||||
return ilovetui.ContentHeight(m.popupHeight()) - 1
|
||||
}
|
||||
|
||||
@@ -4,14 +4,12 @@ import (
|
||||
"strings"
|
||||
|
||||
"charm.land/lipgloss/v2"
|
||||
"github.com/anotherhadi/spilltea/internal/style"
|
||||
ilovetui "github.com/anotherhadi/ilovetui"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
)
|
||||
|
||||
func (m *Model) View(background string) string {
|
||||
s := style.S
|
||||
|
||||
hint := lipgloss.NewStyle().Foreground(s.Subtle).
|
||||
hint := lipgloss.NewStyle().Foreground(ilovetui.S.Subtle).
|
||||
Render(" enter: copy • /: filter • esc: cancel")
|
||||
|
||||
inner := lipgloss.JoinVertical(lipgloss.Left,
|
||||
@@ -21,17 +19,15 @@ func (m *Model) View(background string) string {
|
||||
|
||||
border := lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(s.Primary)
|
||||
BorderForeground(ilovetui.S.Primary)
|
||||
|
||||
popupH := m.popupHeight()
|
||||
popup := style.RenderWithTitle(border, "Copy as", inner, m.popupInnerWidth()+2, popupH)
|
||||
popup := ilovetui.RenderWithTitle(border, "Copy as", inner, m.popupInnerWidth()+2, popupH)
|
||||
|
||||
return OverlayCenter(background, popup, m.width, m.height)
|
||||
}
|
||||
|
||||
func OverlayCenter(bg, popup string, w, h int) string {
|
||||
s := style.S
|
||||
|
||||
stripped := ansi.Strip(bg)
|
||||
rawLines := strings.Split(stripped, "\n")
|
||||
bgRunes := make([][]rune, h)
|
||||
@@ -67,7 +63,7 @@ func OverlayCenter(bg, popup string, w, h int) string {
|
||||
startX = 0
|
||||
}
|
||||
|
||||
dim := lipgloss.NewStyle().Foreground(s.Subtle).Faint(true)
|
||||
dim := lipgloss.NewStyle().Foreground(ilovetui.S.Subtle).Faint(true)
|
||||
|
||||
result := make([]string, h)
|
||||
for y := 0; y < h; y++ {
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
"github.com/anotherhadi/spilltea/internal/style"
|
||||
ilovetui "github.com/anotherhadi/ilovetui"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
)
|
||||
|
||||
@@ -76,7 +76,6 @@ func (m Model) View(background string) string {
|
||||
return background
|
||||
}
|
||||
|
||||
s := style.S
|
||||
const popupW = 34
|
||||
|
||||
var popups []string
|
||||
@@ -89,17 +88,17 @@ func (m Model) View(background string) string {
|
||||
var accent color.Color
|
||||
switch n.kind {
|
||||
case KindSuccess:
|
||||
accent = s.Success
|
||||
accent = ilovetui.S.Success
|
||||
case KindWarning:
|
||||
accent = s.Warning
|
||||
accent = ilovetui.S.Warning
|
||||
case KindError:
|
||||
accent = s.Error
|
||||
accent = ilovetui.S.Error
|
||||
default:
|
||||
accent = s.Primary
|
||||
accent = ilovetui.S.Primary
|
||||
}
|
||||
|
||||
titleStr := lipgloss.NewStyle().Foreground(accent).Bold(true).Render(n.title)
|
||||
bodyStr := lipgloss.NewStyle().Foreground(s.Text).Width(popupW).Render(n.body)
|
||||
bodyStr := lipgloss.NewStyle().Foreground(ilovetui.S.Text).Width(popupW).Render(n.body)
|
||||
|
||||
inner := lipgloss.JoinVertical(lipgloss.Left, titleStr, bodyStr)
|
||||
box := lipgloss.NewStyle().
|
||||
|
||||
+10
-12
@@ -8,6 +8,7 @@ import (
|
||||
"charm.land/bubbles/v2/viewport"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
ilovetui "github.com/anotherhadi/ilovetui"
|
||||
"github.com/anotherhadi/spilltea/internal/keys"
|
||||
"github.com/anotherhadi/spilltea/internal/style"
|
||||
"github.com/anotherhadi/spilltea/internal/util"
|
||||
@@ -85,10 +86,9 @@ func wordDiff(leftLine, rightLine string) (leftRendered, rightRendered string) {
|
||||
segs[lo], segs[hi] = segs[hi], segs[lo]
|
||||
}
|
||||
|
||||
s := style.S
|
||||
boldErr := lipgloss.NewStyle().Foreground(s.Error).Bold(true)
|
||||
boldOk := lipgloss.NewStyle().Foreground(s.Success).Bold(true)
|
||||
dim := lipgloss.NewStyle().Foreground(s.Subtle)
|
||||
boldErr := lipgloss.NewStyle().Foreground(ilovetui.S.Error).Bold(true)
|
||||
boldOk := lipgloss.NewStyle().Foreground(ilovetui.S.Success).Bold(true)
|
||||
dim := lipgloss.NewStyle().Foreground(ilovetui.S.Subtle)
|
||||
|
||||
var lb, rb strings.Builder
|
||||
for _, seg := range segs {
|
||||
@@ -212,9 +212,9 @@ type Model struct {
|
||||
|
||||
func New() Model {
|
||||
return Model{
|
||||
leftViewport: style.NewViewport(),
|
||||
rightViewport: style.NewViewport(),
|
||||
help: style.NewHelp(),
|
||||
leftViewport: ilovetui.NewViewport(),
|
||||
rightViewport: ilovetui.NewViewport(),
|
||||
help: ilovetui.NewHelp(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,7 +255,7 @@ func (m *Model) recalcSizes() {
|
||||
rightInner = 0
|
||||
}
|
||||
|
||||
viewportH := style.PanelContentH(panelH)
|
||||
viewportH := ilovetui.ContentHeight(panelH)
|
||||
|
||||
m.leftViewport.SetWidth(leftInner)
|
||||
m.leftViewport.SetHeight(viewportH)
|
||||
@@ -296,13 +296,11 @@ func hlLines(raw string) []string {
|
||||
}
|
||||
|
||||
func (m *Model) refreshViewports() {
|
||||
s := style.S
|
||||
|
||||
if m.left.raw == "" {
|
||||
placeholder := lipgloss.Place(
|
||||
m.leftViewport.Width(), m.leftViewport.Height(),
|
||||
lipgloss.Center, lipgloss.Center,
|
||||
s.Faint.Render(util.CenterLines("<(^_^)>", "send two entries here to compare")),
|
||||
ilovetui.S.Faint.Render(util.CenterLines("<(^_^)>", "send two entries here to compare")),
|
||||
)
|
||||
m.leftViewport.SetContent(placeholder)
|
||||
m.rightViewport.SetContent("")
|
||||
@@ -314,7 +312,7 @@ func (m *Model) refreshViewports() {
|
||||
placeholder := lipgloss.Place(
|
||||
m.rightViewport.Width(), m.rightViewport.Height(),
|
||||
lipgloss.Center, lipgloss.Center,
|
||||
s.Faint.Render(util.CenterLines("(・3・)", "waiting for second entry…")),
|
||||
ilovetui.S.Faint.Render(util.CenterLines("(・3・)", "waiting for second entry…")),
|
||||
)
|
||||
m.rightViewport.SetContent(placeholder)
|
||||
return
|
||||
|
||||
+11
-14
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
ilovetui "github.com/anotherhadi/ilovetui"
|
||||
"github.com/anotherhadi/spilltea/internal/icons"
|
||||
"github.com/anotherhadi/spilltea/internal/style"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
@@ -26,8 +27,6 @@ func (m Model) View() tea.View {
|
||||
}
|
||||
|
||||
func (m *Model) renderPanels(panelH int) string {
|
||||
s := style.S
|
||||
|
||||
leftW := m.width / 2
|
||||
rightW := m.width - leftW
|
||||
|
||||
@@ -46,20 +45,20 @@ func (m *Model) renderPanels(panelH int) string {
|
||||
rightTitle = ansi.Truncate(rightTitle, maxW, "…")
|
||||
}
|
||||
|
||||
leftBorder := s.Panel
|
||||
rightBorder := s.Panel
|
||||
leftBorder := ilovetui.S.Panel
|
||||
rightBorder := ilovetui.S.Panel
|
||||
switch m.focus {
|
||||
case bothSlots:
|
||||
leftBorder = s.PanelFocused
|
||||
rightBorder = s.PanelFocused
|
||||
leftBorder = ilovetui.S.PanelFocused
|
||||
rightBorder = ilovetui.S.PanelFocused
|
||||
case leftSlot:
|
||||
leftBorder = s.PanelFocused
|
||||
leftBorder = ilovetui.S.PanelFocused
|
||||
case rightSlot:
|
||||
rightBorder = s.PanelFocused
|
||||
rightBorder = ilovetui.S.PanelFocused
|
||||
}
|
||||
|
||||
left := style.RenderWithTitle(leftBorder, leftTitle, style.ViewportView(&m.leftViewport), leftW, panelH)
|
||||
right := style.RenderWithTitle(rightBorder, rightTitle, style.ViewportView(&m.rightViewport), rightW, panelH)
|
||||
left := ilovetui.RenderWithTitle(leftBorder, leftTitle, ilovetui.ViewportView(&m.leftViewport), leftW, panelH)
|
||||
right := ilovetui.RenderWithTitle(rightBorder, rightTitle, ilovetui.ViewportView(&m.rightViewport), rightW, panelH)
|
||||
|
||||
return lipgloss.JoinHorizontal(lipgloss.Top, left, right)
|
||||
}
|
||||
@@ -69,12 +68,11 @@ func (m *Model) renderStatusBar() string {
|
||||
}
|
||||
|
||||
func renderLeftLines(lines []diffLine) string {
|
||||
s := style.S
|
||||
var sb strings.Builder
|
||||
for _, l := range lines {
|
||||
switch l.kind {
|
||||
case lineRemoved:
|
||||
sb.WriteString(style.Paint(s.Error, "- ") + l.text + "\n")
|
||||
sb.WriteString(style.Paint(ilovetui.S.Error, "- ") + l.text + "\n")
|
||||
case lineAdded:
|
||||
sb.WriteString("\n")
|
||||
default:
|
||||
@@ -85,12 +83,11 @@ func renderLeftLines(lines []diffLine) string {
|
||||
}
|
||||
|
||||
func renderRightLines(lines []diffLine) string {
|
||||
s := style.S
|
||||
var sb strings.Builder
|
||||
for _, l := range lines {
|
||||
switch l.kind {
|
||||
case lineAdded:
|
||||
sb.WriteString(style.Paint(s.Success, "+ ") + l.text + "\n")
|
||||
sb.WriteString(style.Paint(ilovetui.S.Success, "+ ") + l.text + "\n")
|
||||
case lineRemoved:
|
||||
sb.WriteString("\n")
|
||||
default:
|
||||
|
||||
@@ -14,8 +14,8 @@ import (
|
||||
"charm.land/bubbles/v2/viewport"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
ilovetui "github.com/anotherhadi/ilovetui"
|
||||
"github.com/anotherhadi/spilltea/internal/keys"
|
||||
"github.com/anotherhadi/spilltea/internal/style"
|
||||
)
|
||||
|
||||
func readDoc(name string) string {
|
||||
@@ -58,12 +58,12 @@ func New() Model {
|
||||
ti := textinput.New()
|
||||
ti.Prompt = "/"
|
||||
s := ti.Styles()
|
||||
s.Focused.Prompt = lipgloss.NewStyle().Foreground(style.S.Primary)
|
||||
s.Focused.Prompt = lipgloss.NewStyle().Foreground(ilovetui.S.Primary)
|
||||
ti.SetStyles(s)
|
||||
|
||||
return Model{
|
||||
viewport: viewport.New(),
|
||||
help: style.NewHelp(),
|
||||
help: ilovetui.NewHelp(),
|
||||
searchInput: ti,
|
||||
}
|
||||
}
|
||||
@@ -182,8 +182,8 @@ func injectHighlightsInLine(ansiLine string, intervals [][]int, currentIdx int)
|
||||
return ansiLine
|
||||
}
|
||||
|
||||
normalOpen, normalClose := lipglossAnsiCodes(lipgloss.NewStyle().Background(style.S.SubtleBg))
|
||||
currentOpen, currentClose := lipglossAnsiCodes(lipgloss.NewStyle().Background(style.S.Primary).Foreground(style.S.Text))
|
||||
normalOpen, normalClose := lipglossAnsiCodes(lipgloss.NewStyle().Background(ilovetui.S.SubtleBg))
|
||||
currentOpen, currentClose := lipglossAnsiCodes(lipgloss.NewStyle().Background(ilovetui.S.Primary).Foreground(ilovetui.S.Text))
|
||||
|
||||
type injection struct {
|
||||
visPos int
|
||||
|
||||
@@ -9,15 +9,15 @@ import (
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/glamour/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
ilovetui "github.com/anotherhadi/ilovetui"
|
||||
"github.com/anotherhadi/spilltea/internal/config"
|
||||
"github.com/anotherhadi/spilltea/internal/style"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
)
|
||||
|
||||
func windowStyle() lipgloss.Style {
|
||||
return lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(style.S.Subtle).
|
||||
BorderForeground(ilovetui.S.Subtle).
|
||||
Padding(0, 0)
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ func (e Model) View() tea.View {
|
||||
countText = fmt.Sprintf("%d/%d", e.matchIndex+1, len(e.matches))
|
||||
}
|
||||
count := lipgloss.NewStyle().Padding(0, 1).
|
||||
Foreground(style.S.MutedFg).
|
||||
Foreground(ilovetui.S.Muted).
|
||||
Render(countText)
|
||||
statusBar = lipgloss.JoinHorizontal(lipgloss.Top, statusBar, count)
|
||||
}
|
||||
@@ -61,7 +61,7 @@ func (m *Model) renderMarkdown() {
|
||||
|
||||
width := m.viewport.Width() - 2
|
||||
renderer, _ := glamour.NewTermRenderer(
|
||||
glamour.WithStyles(style.GlamourStyleConfig(cfg)),
|
||||
glamour.WithStyles(ilovetui.GlamourStyleConfig()),
|
||||
glamour.WithWordWrap(width),
|
||||
)
|
||||
|
||||
|
||||
@@ -11,10 +11,9 @@ import (
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/glamour/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
"github.com/anotherhadi/spilltea/internal/config"
|
||||
ilovetui "github.com/anotherhadi/ilovetui"
|
||||
"github.com/anotherhadi/spilltea/internal/db"
|
||||
"github.com/anotherhadi/spilltea/internal/keys"
|
||||
"github.com/anotherhadi/spilltea/internal/style"
|
||||
"github.com/anotherhadi/spilltea/internal/util"
|
||||
)
|
||||
|
||||
@@ -39,10 +38,10 @@ type Model struct {
|
||||
|
||||
func New() Model {
|
||||
return Model{
|
||||
listViewport: style.NewViewport(),
|
||||
bodyViewport: style.NewViewport(),
|
||||
pager: style.NewPaginator(),
|
||||
help: style.NewHelp(),
|
||||
listViewport: ilovetui.NewViewport(),
|
||||
bodyViewport: ilovetui.NewViewport(),
|
||||
pager: ilovetui.NewPaginator(),
|
||||
help: ilovetui.NewHelp(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,9 +75,9 @@ func (m *Model) recalcSizes() {
|
||||
m.help.SetWidth(m.width - 2)
|
||||
inner := m.width - 2
|
||||
|
||||
listH, bodyH := style.SplitH(m.height, m.renderStatusBar(), 0.35)
|
||||
listH, bodyH := ilovetui.SplitH(m.height, m.renderStatusBar(), 0.35)
|
||||
|
||||
listVH := style.PanelContentH(listH) - 1 // -1 for the pager dots row
|
||||
listVH := ilovetui.ContentHeight(listH) - 1 // -1 for the pager dots row
|
||||
if listVH < 0 {
|
||||
listVH = 0
|
||||
}
|
||||
@@ -89,7 +88,7 @@ func (m *Model) recalcSizes() {
|
||||
m.pager.PerPage = 1
|
||||
}
|
||||
|
||||
bodyVH := style.PanelContentH(bodyH)
|
||||
bodyVH := ilovetui.ContentHeight(bodyH)
|
||||
m.bodyViewport.SetWidth(inner)
|
||||
m.bodyViewport.SetHeight(bodyVH)
|
||||
|
||||
@@ -148,7 +147,7 @@ func (m *Model) refreshBodyScroll(reset bool) {
|
||||
|
||||
func (m *Model) renderMarkdownCached(src string, width int) string {
|
||||
if src == "" {
|
||||
return style.S.Faint.Render(util.CenterLines("(ㆆ _ ㆆ)", "no description"))
|
||||
return ilovetui.S.Faint.Render(util.CenterLines("(ㆆ _ ㆆ)", "no description"))
|
||||
}
|
||||
tmpl, err := template.New("").Parse(src)
|
||||
if err != nil {
|
||||
@@ -164,7 +163,7 @@ func (m *Model) renderMarkdownCached(src string, width int) string {
|
||||
// Rebuild renderer if width changed or not yet built.
|
||||
if m.renderer == nil || m.rendererWidth != width {
|
||||
r, err := glamour.NewTermRenderer(
|
||||
glamour.WithStyles(style.GlamourStyleConfig(config.Global)),
|
||||
glamour.WithStyles(ilovetui.GlamourStyleConfig()),
|
||||
glamour.WithWordWrap(width),
|
||||
)
|
||||
if err == nil {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
ilovetui "github.com/anotherhadi/ilovetui"
|
||||
"github.com/anotherhadi/spilltea/internal/icons"
|
||||
"github.com/anotherhadi/spilltea/internal/style"
|
||||
"github.com/anotherhadi/spilltea/internal/util"
|
||||
@@ -16,7 +17,7 @@ func (m Model) View() tea.View {
|
||||
return tea.NewView("Loading...")
|
||||
}
|
||||
|
||||
listH, bodyH := style.SplitH(m.height, m.renderStatusBar(), 0.35)
|
||||
listH, bodyH := ilovetui.SplitH(m.height, m.renderStatusBar(), 0.35)
|
||||
|
||||
content := lipgloss.JoinVertical(lipgloss.Left,
|
||||
m.renderListPanel(m.width, listH),
|
||||
@@ -27,34 +28,31 @@ func (m Model) View() tea.View {
|
||||
}
|
||||
|
||||
func (m *Model) renderListPanel(w, h int) string {
|
||||
s := style.S
|
||||
var dots string
|
||||
if len(m.findings) > 0 {
|
||||
dots = s.Faint.Render(m.pager.View())
|
||||
dots = ilovetui.S.Faint.Render(m.pager.View())
|
||||
}
|
||||
inner := lipgloss.JoinVertical(lipgloss.Left,
|
||||
m.listViewport.View(),
|
||||
lipgloss.PlaceHorizontal(m.listViewport.Width(), lipgloss.Center, dots),
|
||||
)
|
||||
return style.RenderWithTitle(s.PanelFocused, icons.I.Findings+"Findings", inner, w, h)
|
||||
return ilovetui.RenderWithTitle(ilovetui.S.PanelFocused, icons.I.Findings+"Findings", inner, w, h)
|
||||
}
|
||||
|
||||
func (m *Model) renderBodyPanel(h int) string {
|
||||
s := style.S
|
||||
title := "Description"
|
||||
if len(m.findings) > 0 {
|
||||
title = m.findings[m.cursor].Title
|
||||
}
|
||||
return style.RenderWithTitle(s.Panel, title, style.ViewportView(&m.bodyViewport), m.width, h)
|
||||
return ilovetui.RenderWithTitle(ilovetui.S.Panel, title, ilovetui.ViewportView(&m.bodyViewport), m.width, h)
|
||||
}
|
||||
|
||||
func (m *Model) renderList() string {
|
||||
s := style.S
|
||||
if len(m.findings) == 0 {
|
||||
return lipgloss.Place(
|
||||
m.listViewport.Width(), m.listViewport.Height(),
|
||||
lipgloss.Center, lipgloss.Center,
|
||||
s.Faint.Render(util.CenterLines("(҂◡_◡) ᕤ", "no findings")),
|
||||
ilovetui.S.Faint.Render(util.CenterLines("(҂◡_◡) ᕤ", "no findings")),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -76,18 +74,18 @@ func (m *Model) renderList() string {
|
||||
titleW = 0
|
||||
}
|
||||
|
||||
pluginStr := s.Faint.Width(8).Render(util.Truncate(f.PluginName, 8))
|
||||
pluginStr := ilovetui.S.Faint.Width(8).Render(util.Truncate(f.PluginName, 8))
|
||||
|
||||
var line string
|
||||
if selected {
|
||||
bg := lipgloss.NewStyle().Background(s.Selection)
|
||||
bg := lipgloss.NewStyle().Background(ilovetui.S.Selection)
|
||||
line = lipgloss.JoinHorizontal(lipgloss.Top,
|
||||
bg.Bold(true).Foreground(s.Primary).Width(2).Render(">"),
|
||||
sevStyle.Background(s.Selection).Width(8).Render(f.Severity),
|
||||
bg.Bold(true).Foreground(ilovetui.S.Primary).Width(2).Render(">"),
|
||||
sevStyle.Background(ilovetui.S.Selection).Width(8).Render(f.Severity),
|
||||
bg.Width(1).Render(""),
|
||||
bg.Foreground(s.Subtle).Width(8).Render(util.Truncate(f.PluginName, 8)),
|
||||
bg.Foreground(ilovetui.S.Subtle).Width(8).Render(util.Truncate(f.PluginName, 8)),
|
||||
bg.Width(1).Render(""),
|
||||
bg.Foreground(s.Subtle).Width(10).Render(ts),
|
||||
bg.Foreground(ilovetui.S.Subtle).Width(10).Render(ts),
|
||||
bg.Width(1).Render(""),
|
||||
bg.Bold(true).Width(titleW).Render(f.Title),
|
||||
)
|
||||
@@ -98,9 +96,9 @@ func (m *Model) renderList() string {
|
||||
" ",
|
||||
pluginStr,
|
||||
" ",
|
||||
s.Faint.Width(10).Render(ts),
|
||||
ilovetui.S.Faint.Width(10).Render(ts),
|
||||
" ",
|
||||
s.Bold.Render(f.Title),
|
||||
ilovetui.S.Bold.Render(f.Title),
|
||||
)
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%s\n", line))
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"charm.land/bubbles/v2/textinput"
|
||||
"charm.land/bubbles/v2/viewport"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
ilovetui "github.com/anotherhadi/ilovetui"
|
||||
"github.com/anotherhadi/spilltea/internal/db"
|
||||
"github.com/anotherhadi/spilltea/internal/keys"
|
||||
"github.com/anotherhadi/spilltea/internal/style"
|
||||
"github.com/anotherhadi/spilltea/internal/util"
|
||||
)
|
||||
|
||||
@@ -44,10 +44,10 @@ func New() Model {
|
||||
ti := textinput.New()
|
||||
ti.Prompt = ""
|
||||
return Model{
|
||||
listViewport: style.NewViewport(),
|
||||
bodyViewport: style.NewViewport(),
|
||||
pager: style.NewPaginator(),
|
||||
help: style.NewHelp(),
|
||||
listViewport: ilovetui.NewViewport(),
|
||||
bodyViewport: ilovetui.NewViewport(),
|
||||
pager: ilovetui.NewPaginator(),
|
||||
help: ilovetui.NewHelp(),
|
||||
searchInput: ti,
|
||||
}
|
||||
}
|
||||
@@ -124,14 +124,14 @@ func (m *Model) recalcSizes() {
|
||||
// 2 (padding) + 2 (prefix char + space)
|
||||
m.searchInput.SetWidth(m.width - 4)
|
||||
|
||||
listH, bodyH := style.SplitH(m.height, m.renderStatusBar(), 0.35)
|
||||
listH, bodyH := ilovetui.SplitH(m.height, m.renderStatusBar(), 0.35)
|
||||
|
||||
inner := m.width - 2
|
||||
if inner < 0 {
|
||||
inner = 0
|
||||
}
|
||||
|
||||
listVH := style.PanelContentH(listH) - 1 // -1 for the pager dots row
|
||||
listVH := ilovetui.ContentHeight(listH) - 1 // -1 for the pager dots row
|
||||
if listVH < 0 {
|
||||
listVH = 0
|
||||
}
|
||||
@@ -142,7 +142,7 @@ func (m *Model) recalcSizes() {
|
||||
m.pager.PerPage = 1
|
||||
}
|
||||
|
||||
bodyVH := style.PanelContentH(bodyH)
|
||||
bodyVH := ilovetui.ContentHeight(bodyH)
|
||||
m.bodyViewport.SetWidth(inner)
|
||||
m.bodyViewport.SetHeight(bodyVH)
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"charm.land/bubbles/v2/key"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
ilovetui "github.com/anotherhadi/ilovetui"
|
||||
"github.com/anotherhadi/spilltea/internal/db"
|
||||
"github.com/anotherhadi/spilltea/internal/keys"
|
||||
"github.com/anotherhadi/spilltea/internal/style"
|
||||
@@ -249,11 +250,21 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
case key.Matches(msg, h.DeleteAll):
|
||||
if m.database != nil {
|
||||
if m.searchKind != searchKindOff {
|
||||
hasUnflagged := false
|
||||
for _, e := range m.entries {
|
||||
if !e.Flagged {
|
||||
hasUnflagged = true
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, e := range m.entries {
|
||||
if hasUnflagged && e.Flagged {
|
||||
continue
|
||||
}
|
||||
m.database.DeleteEntry(e.ID)
|
||||
}
|
||||
} else {
|
||||
m.database.DeleteAllEntries()
|
||||
m.database.DeleteAllExceptFlagged()
|
||||
}
|
||||
}
|
||||
return m, m.clearSearch()
|
||||
@@ -335,7 +346,7 @@ func (m *Model) refreshBody() {
|
||||
}
|
||||
if raw == "" {
|
||||
w, h := m.bodyViewport.Width(), m.bodyViewport.Height()
|
||||
m.bodyViewport.SetContent(lipgloss.Place(w, h, lipgloss.Center, lipgloss.Center, style.S.Faint.Render(util.CenterLines("(˘・_・˘)", "no response stored"))))
|
||||
m.bodyViewport.SetContent(lipgloss.Place(w, h, lipgloss.Center, lipgloss.Center, ilovetui.S.Faint.Render(util.CenterLines("(˘・_・˘)", "no response stored"))))
|
||||
return
|
||||
}
|
||||
m.bodyViewport.SetContent(style.HighlightHTTP(raw))
|
||||
|
||||
+23
-26
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
ilovetui "github.com/anotherhadi/ilovetui"
|
||||
"github.com/anotherhadi/spilltea/internal/icons"
|
||||
"github.com/anotherhadi/spilltea/internal/keys"
|
||||
"github.com/anotherhadi/spilltea/internal/style"
|
||||
@@ -17,7 +18,7 @@ func (m Model) View() tea.View {
|
||||
return tea.NewView("Loading...")
|
||||
}
|
||||
|
||||
listH, bodyH := style.SplitH(m.height, m.renderStatusBar(), 0.35)
|
||||
listH, bodyH := ilovetui.SplitH(m.height, m.renderStatusBar(), 0.35)
|
||||
|
||||
content := lipgloss.JoinVertical(lipgloss.Left,
|
||||
m.renderListPanel(m.width, listH),
|
||||
@@ -28,60 +29,56 @@ func (m Model) View() tea.View {
|
||||
}
|
||||
|
||||
func (m *Model) renderListPanel(w, h int) string {
|
||||
s := style.S
|
||||
var dots string
|
||||
if len(m.entries) > 0 {
|
||||
dots = s.Faint.Render(m.pager.View())
|
||||
dots = ilovetui.S.Faint.Render(m.pager.View())
|
||||
}
|
||||
inner := lipgloss.JoinVertical(lipgloss.Left,
|
||||
m.listViewport.View(),
|
||||
lipgloss.PlaceHorizontal(m.listViewport.Width(), lipgloss.Center, dots),
|
||||
)
|
||||
return style.RenderWithTitle(s.PanelFocused, icons.I.History+"History", inner, w, h)
|
||||
return ilovetui.RenderWithTitle(ilovetui.S.PanelFocused, icons.I.History+"History", inner, w, h)
|
||||
}
|
||||
|
||||
func (m *Model) renderBodyPanel(h int) string {
|
||||
s := style.S
|
||||
title := icons.I.Request + "Request"
|
||||
if m.focusedPanel == panelResponse {
|
||||
title = icons.I.Response + "Response"
|
||||
}
|
||||
return style.RenderWithTitle(s.Panel, title, style.ViewportView(&m.bodyViewport), m.width, h)
|
||||
return ilovetui.RenderWithTitle(ilovetui.S.Panel, title, ilovetui.ViewportView(&m.bodyViewport), m.width, h)
|
||||
}
|
||||
|
||||
func (m *Model) renderStatusBar() string {
|
||||
s := style.S
|
||||
pad := lipgloss.NewStyle().Padding(0, 1)
|
||||
escKey := keys.Keys.Global.Escape.Help().Key
|
||||
switch m.searchKind {
|
||||
case searchKindFulltext:
|
||||
filterKey := keys.Keys.History.Filter.Help().Key
|
||||
if m.searchAccepted {
|
||||
accent := lipgloss.NewStyle().Foreground(s.Primary)
|
||||
filterLine := pad.Render(accent.Render(filterKey) + " " + s.Bold.Render(m.searchInput.Value()) + s.Faint.Render(" "+escKey+" to clear"))
|
||||
accent := lipgloss.NewStyle().Foreground(ilovetui.S.Primary)
|
||||
filterLine := pad.Render(accent.Render(filterKey) + " " + ilovetui.S.Bold.Render(m.searchInput.Value()) + ilovetui.S.Faint.Render(" "+escKey+" to clear"))
|
||||
return lipgloss.JoinVertical(lipgloss.Left, filterLine, pad.Render(m.help.View(historyKeyMap{width: m.width})))
|
||||
}
|
||||
return pad.Render(s.Faint.Render(filterKey) + " " + m.searchInput.View())
|
||||
return pad.Render(ilovetui.S.Faint.Render(filterKey) + " " + m.searchInput.View())
|
||||
case searchKindSQL:
|
||||
sqlKey := keys.Keys.History.SqlQuery.Help().Key
|
||||
if m.searchAccepted {
|
||||
accent := lipgloss.NewStyle().Foreground(s.Primary)
|
||||
filterLine := pad.Render(accent.Render(sqlKey) + " " + s.Bold.Render(m.searchInput.Value()) + s.Faint.Render(" "+escKey+" to clear"))
|
||||
accent := lipgloss.NewStyle().Foreground(ilovetui.S.Primary)
|
||||
filterLine := pad.Render(accent.Render(sqlKey) + " " + ilovetui.S.Bold.Render(m.searchInput.Value()) + ilovetui.S.Faint.Render(" "+escKey+" to clear"))
|
||||
return lipgloss.JoinVertical(lipgloss.Left, filterLine, pad.Render(m.help.View(historyKeyMap{width: m.width})))
|
||||
}
|
||||
return pad.Render(s.Faint.Render(sqlKey) + " " + m.searchInput.View())
|
||||
return pad.Render(ilovetui.S.Faint.Render(sqlKey) + " " + m.searchInput.View())
|
||||
default:
|
||||
return pad.Render(m.help.View(historyKeyMap{width: m.width}))
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) renderList() string {
|
||||
s := style.S
|
||||
if m.searchErr != "" {
|
||||
return lipgloss.Place(
|
||||
m.listViewport.Width(), m.listViewport.Height(),
|
||||
lipgloss.Center, lipgloss.Center,
|
||||
lipgloss.NewStyle().Foreground(s.Error).Render(m.searchErr),
|
||||
lipgloss.NewStyle().Foreground(ilovetui.S.Error).Render(m.searchErr),
|
||||
)
|
||||
}
|
||||
if len(m.entries) == 0 {
|
||||
@@ -92,7 +89,7 @@ func (m *Model) renderList() string {
|
||||
return lipgloss.Place(
|
||||
m.listViewport.Width(), m.listViewport.Height(),
|
||||
lipgloss.Center, lipgloss.Center,
|
||||
s.Faint.Render(msg),
|
||||
ilovetui.S.Faint.Render(msg),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -103,7 +100,7 @@ func (m *Model) renderList() string {
|
||||
globalIdx := start + i
|
||||
selected := globalIdx == m.cursor
|
||||
|
||||
selBg := s.Selection
|
||||
selBg := ilovetui.S.Selection
|
||||
w := m.listViewport.Width()
|
||||
|
||||
statusStr := fmt.Sprintf("%3d", e.StatusCode)
|
||||
@@ -115,7 +112,7 @@ func (m *Model) renderList() string {
|
||||
|
||||
ts := e.Timestamp.Format("15:04:05")
|
||||
statusSt := style.StatusStyle(e.StatusCode, 3)
|
||||
flagSt := lipgloss.NewStyle().Foreground(s.Primary)
|
||||
flagSt := lipgloss.NewStyle().Foreground(ilovetui.S.Primary)
|
||||
|
||||
var line string
|
||||
if selected {
|
||||
@@ -128,13 +125,13 @@ func (m *Model) renderList() string {
|
||||
}
|
||||
}
|
||||
line = lipgloss.JoinHorizontal(lipgloss.Top,
|
||||
bg.Bold(true).Foreground(s.Primary).Width(2).Render(">"),
|
||||
bg.Foreground(s.Primary).Width(2).Render(flagStr),
|
||||
s.Method(e.Method).Background(selBg).Render(e.Method),
|
||||
bg.Bold(true).Foreground(ilovetui.S.Primary).Width(2).Render(">"),
|
||||
bg.Foreground(ilovetui.S.Primary).Width(2).Render(flagStr),
|
||||
style.S.Method(e.Method).Background(selBg).Render(e.Method),
|
||||
bg.Width(1).Render(""),
|
||||
statusSt.Background(selBg).Render(statusStr),
|
||||
bg.Width(1).Render(""),
|
||||
bg.Foreground(s.Subtle).Width(10).Render(ts),
|
||||
bg.Foreground(ilovetui.S.Subtle).Width(10).Render(ts),
|
||||
bg.Width(1).Render(""),
|
||||
bg.Bold(true).Width(hostPathW).Render(e.Host+e.Path),
|
||||
)
|
||||
@@ -149,14 +146,14 @@ func (m *Model) renderList() string {
|
||||
line = lipgloss.JoinHorizontal(lipgloss.Top,
|
||||
" ",
|
||||
flagSt.Width(2).Render(flagStr),
|
||||
s.Method(e.Method).Render(e.Method),
|
||||
style.S.Method(e.Method).Render(e.Method),
|
||||
" ",
|
||||
statusSt.Render(statusStr),
|
||||
" ",
|
||||
s.Faint.Width(10).Render(ts),
|
||||
ilovetui.S.Faint.Width(10).Render(ts),
|
||||
" ",
|
||||
s.Bold.Render(e.Host),
|
||||
s.Faint.Render(e.Path),
|
||||
ilovetui.S.Bold.Render(e.Host),
|
||||
ilovetui.S.Faint.Render(e.Path),
|
||||
)
|
||||
}
|
||||
sb.WriteString(line + "\n")
|
||||
|
||||
+10
-12
@@ -14,10 +14,10 @@ import (
|
||||
"charm.land/bubbles/v2/textinput"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
ilovetui "github.com/anotherhadi/ilovetui"
|
||||
"github.com/anotherhadi/spilltea/internal/db"
|
||||
"github.com/anotherhadi/spilltea/internal/icons"
|
||||
"github.com/anotherhadi/spilltea/internal/keys"
|
||||
"github.com/anotherhadi/spilltea/internal/style"
|
||||
"github.com/anotherhadi/spilltea/internal/ui/components/teapot"
|
||||
)
|
||||
|
||||
@@ -88,19 +88,18 @@ type homeDelegate struct {
|
||||
}
|
||||
|
||||
func newHomeDelegate() homeDelegate {
|
||||
s := style.S
|
||||
leftBorder := lipgloss.Border{Left: "│"}
|
||||
return homeDelegate{
|
||||
normalTitle: lipgloss.NewStyle().Foreground(s.Text).PaddingLeft(4),
|
||||
normalDesc: lipgloss.NewStyle().Foreground(s.Subtle).Faint(true).PaddingLeft(4),
|
||||
normalTitle: lipgloss.NewStyle().Foreground(ilovetui.S.Text).PaddingLeft(4),
|
||||
normalDesc: lipgloss.NewStyle().Foreground(ilovetui.S.Subtle).Faint(true).PaddingLeft(4),
|
||||
selectedTitle: lipgloss.NewStyle().
|
||||
Border(leftBorder, false, false, false, true).
|
||||
BorderForeground(s.Primary).
|
||||
Foreground(s.Primary).Bold(true).PaddingLeft(3),
|
||||
BorderForeground(ilovetui.S.Primary).
|
||||
Foreground(ilovetui.S.Primary).Bold(true).PaddingLeft(3),
|
||||
selectedDesc: lipgloss.NewStyle().
|
||||
Border(leftBorder, false, false, false, true).
|
||||
BorderForeground(s.Primary).
|
||||
Foreground(s.MutedFg).PaddingLeft(3),
|
||||
BorderForeground(ilovetui.S.Primary).
|
||||
Foreground(ilovetui.S.Muted).PaddingLeft(3),
|
||||
filterMatch: lipgloss.NewStyle().Underline(true),
|
||||
}
|
||||
}
|
||||
@@ -304,14 +303,13 @@ func buildItems(projects []Project) []list.Item {
|
||||
}
|
||||
|
||||
func (m Model) renderHelpLine() string {
|
||||
s := style.S
|
||||
k := keys.Keys.Home
|
||||
fs := m.list.FilterState()
|
||||
|
||||
kStyle := lipgloss.NewStyle().Foreground(s.MutedFg).Inline(true)
|
||||
dStyle := s.Faint.Inline(true)
|
||||
kStyle := lipgloss.NewStyle().Foreground(ilovetui.S.Muted).Inline(true)
|
||||
dStyle := ilovetui.S.Faint.Inline(true)
|
||||
|
||||
sep := s.Faint.Inline(true).Render(" • ")
|
||||
sep := ilovetui.S.Faint.Inline(true).Render(" • ")
|
||||
item := func(keyStr, desc string) string {
|
||||
return kStyle.Render(keyStr) + " " + dStyle.Render(desc)
|
||||
}
|
||||
|
||||
@@ -5,29 +5,28 @@ import (
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
"github.com/anotherhadi/spilltea/internal/style"
|
||||
ilovetui "github.com/anotherhadi/ilovetui"
|
||||
"github.com/anotherhadi/spilltea/internal/ui/components/teapot"
|
||||
)
|
||||
|
||||
const inputPanelMaxW = 44
|
||||
|
||||
func (m Model) View() tea.View {
|
||||
s := style.S
|
||||
iw := m.innerW()
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString("\n")
|
||||
if m.height > teapotMinH {
|
||||
frames := teapot.TeapotFrames()
|
||||
frame := lipgloss.NewStyle().Foreground(s.Primary).Render(frames[m.teapotFrame])
|
||||
frame := lipgloss.NewStyle().Foreground(ilovetui.S.Primary).Render(frames[m.teapotFrame])
|
||||
sb.WriteString(center(iw, frame))
|
||||
sb.WriteString("\n\n")
|
||||
} else {
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
sb.WriteString(center(iw, lipgloss.NewStyle().Bold(true).Foreground(s.Primary).Render("SPILLTEA")))
|
||||
sb.WriteString(center(iw, lipgloss.NewStyle().Bold(true).Foreground(ilovetui.S.Primary).Render("SPILLTEA")))
|
||||
sb.WriteString("\n")
|
||||
sb.WriteString(center(iw, s.Faint.Render("choose a project to get started")))
|
||||
sb.WriteString(center(iw, ilovetui.S.Faint.Render("choose a project to get started")))
|
||||
sb.WriteString("\n\n")
|
||||
|
||||
if m.mode == modeNaming {
|
||||
@@ -50,7 +49,6 @@ func (m Model) View() tea.View {
|
||||
}
|
||||
|
||||
func (m Model) renderNamingPanel() string {
|
||||
s := style.S
|
||||
iw := m.innerW()
|
||||
|
||||
panelW := inputPanelMaxW
|
||||
@@ -63,15 +61,15 @@ func (m Model) renderNamingPanel() string {
|
||||
innerW := inputPanelInnerW(iw)
|
||||
inputLine := lipgloss.NewStyle().Width(innerW).Render(m.nameInput.View())
|
||||
|
||||
label := lipgloss.NewStyle().Foreground(s.MutedFg).Render("Project name")
|
||||
label := lipgloss.NewStyle().Foreground(ilovetui.S.Muted).Render("Project name")
|
||||
panel := lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(s.Primary).
|
||||
BorderForeground(ilovetui.S.Primary).
|
||||
Padding(1, 2).
|
||||
Width(panelW).
|
||||
Render(label + "\n" + inputLine)
|
||||
|
||||
hint := s.Faint.Render("[enter] confirm [esc] cancel")
|
||||
hint := ilovetui.S.Faint.Render("[enter] confirm [esc] cancel")
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString(center(iw, panel))
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
ilovetui "github.com/anotherhadi/ilovetui"
|
||||
"github.com/anotherhadi/spilltea/internal/intercept"
|
||||
"github.com/anotherhadi/spilltea/internal/style"
|
||||
"github.com/anotherhadi/spilltea/internal/util"
|
||||
@@ -157,20 +158,20 @@ func (m *Model) listHalfWidths() (leftW, rightW int) {
|
||||
func (m *Model) recalcSizes() {
|
||||
m.help.SetWidth(m.width - 2)
|
||||
|
||||
listH, bodyH := style.SplitH(m.height, m.renderStatusBar(), 0.35)
|
||||
listH, bodyH := ilovetui.SplitH(m.height, m.renderStatusBar(), 0.35)
|
||||
|
||||
bodyInner := m.width - 2
|
||||
if bodyInner < 0 {
|
||||
bodyInner = 0
|
||||
}
|
||||
bodyVH := style.PanelContentH(bodyH)
|
||||
bodyVH := ilovetui.ContentHeight(bodyH)
|
||||
|
||||
m.textarea.SetWidth(bodyInner)
|
||||
m.textarea.SetHeight(bodyVH)
|
||||
m.bodyViewport.SetWidth(bodyInner)
|
||||
m.bodyViewport.SetHeight(bodyVH)
|
||||
|
||||
listVH := style.PanelContentH(listH) - 1 // -1 for the pager dots row
|
||||
listVH := ilovetui.ContentHeight(listH) - 1 // -1 for the pager dots row
|
||||
if listVH < 0 {
|
||||
listVH = 0
|
||||
}
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
package intercept
|
||||
|
||||
import (
|
||||
"charm.land/bubbles/v2/help"
|
||||
"charm.land/bubbles/v2/key"
|
||||
"github.com/anotherhadi/spilltea/internal/icons"
|
||||
"github.com/anotherhadi/spilltea/internal/keys"
|
||||
"github.com/anotherhadi/spilltea/internal/style"
|
||||
)
|
||||
|
||||
func newHelp() help.Model { return style.NewHelp() }
|
||||
|
||||
type interceptKeyMap struct{ width int }
|
||||
|
||||
func iconBinding(b key.Binding, icon string) key.Binding {
|
||||
|
||||
@@ -6,9 +6,9 @@ import (
|
||||
"charm.land/bubbles/v2/textarea"
|
||||
"charm.land/bubbles/v2/viewport"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
ilovetui "github.com/anotherhadi/ilovetui"
|
||||
"github.com/anotherhadi/spilltea/internal/config"
|
||||
"github.com/anotherhadi/spilltea/internal/intercept"
|
||||
"github.com/anotherhadi/spilltea/internal/style"
|
||||
)
|
||||
|
||||
type panel int
|
||||
@@ -48,14 +48,14 @@ type Model struct {
|
||||
|
||||
func New(broker *intercept.Broker) Model {
|
||||
cfg := config.Global
|
||||
ta := style.NewTextarea(false)
|
||||
ta := ilovetui.NewTextarea(false)
|
||||
ta.Blur()
|
||||
|
||||
lv := style.NewViewport()
|
||||
rv := style.NewViewport()
|
||||
bv := style.NewViewport()
|
||||
p := style.NewPaginator()
|
||||
rp := style.NewPaginator()
|
||||
lv := ilovetui.NewViewport()
|
||||
rv := ilovetui.NewViewport()
|
||||
bv := ilovetui.NewViewport()
|
||||
p := ilovetui.NewPaginator()
|
||||
rp := ilovetui.NewPaginator()
|
||||
|
||||
broker.SetCaptureResponse(cfg.Intercept.DefaultCaptureResponse)
|
||||
|
||||
@@ -69,7 +69,7 @@ func New(broker *intercept.Broker) Model {
|
||||
textarea: ta,
|
||||
pager: p,
|
||||
responsePager: rp,
|
||||
help: newHelp(),
|
||||
help: ilovetui.NewHelp(),
|
||||
pendingEdits: make(map[*intercept.PendingRequest]string),
|
||||
pendingResponseEdits: make(map[*intercept.PendingResponse]string),
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
ilovetui "github.com/anotherhadi/ilovetui"
|
||||
"github.com/anotherhadi/spilltea/internal/icons"
|
||||
"github.com/anotherhadi/spilltea/internal/style"
|
||||
"github.com/anotherhadi/spilltea/internal/util"
|
||||
@@ -16,7 +17,7 @@ func (m Model) View() tea.View {
|
||||
return tea.NewView("Loading...")
|
||||
}
|
||||
|
||||
listH, bodyH := style.SplitH(m.height, m.renderStatusBar(), 0.35)
|
||||
listH, bodyH := ilovetui.SplitH(m.height, m.renderStatusBar(), 0.35)
|
||||
|
||||
var listRow string
|
||||
if m.captureResponse {
|
||||
@@ -38,17 +39,15 @@ func (m Model) View() tea.View {
|
||||
}
|
||||
|
||||
func (m *Model) renderListPanel(w, h int) string {
|
||||
s := style.S
|
||||
|
||||
focused := !m.editing && (!m.captureResponse || m.focusedPanel == panelRequests)
|
||||
border := s.Panel
|
||||
border := ilovetui.S.Panel
|
||||
if focused {
|
||||
border = s.PanelFocused
|
||||
border = ilovetui.S.PanelFocused
|
||||
}
|
||||
|
||||
var dots string
|
||||
if len(m.queue) > 0 {
|
||||
dots = s.Faint.Render(m.pager.View())
|
||||
dots = ilovetui.S.Faint.Render(m.pager.View())
|
||||
}
|
||||
inner := lipgloss.JoinVertical(lipgloss.Left,
|
||||
m.listViewport.View(),
|
||||
@@ -57,46 +56,42 @@ func (m *Model) renderListPanel(w, h int) string {
|
||||
|
||||
title := icons.I.Request + "Requests"
|
||||
if !m.interceptEnabled {
|
||||
title += " " + lipgloss.NewStyle().Foreground(style.S.Error).Render("[intercept off]")
|
||||
title += " " + lipgloss.NewStyle().Foreground(ilovetui.S.Error).Render("[intercept off]")
|
||||
}
|
||||
return style.RenderWithTitle(border, title, inner, w, h)
|
||||
return ilovetui.RenderWithTitle(border, title, inner, w, h)
|
||||
}
|
||||
|
||||
func (m *Model) renderResponseListPanel(w, h int) string {
|
||||
s := style.S
|
||||
|
||||
focused := !m.editing && m.focusedPanel == panelResponses
|
||||
border := s.Panel
|
||||
border := ilovetui.S.Panel
|
||||
if focused {
|
||||
border = s.PanelFocused
|
||||
border = ilovetui.S.PanelFocused
|
||||
}
|
||||
|
||||
dots := s.Faint.Render(m.responsePager.View())
|
||||
dots := ilovetui.S.Faint.Render(m.responsePager.View())
|
||||
inner := lipgloss.JoinVertical(lipgloss.Left,
|
||||
m.responseViewport.View(),
|
||||
lipgloss.PlaceHorizontal(m.responseViewport.Width(), lipgloss.Center, dots),
|
||||
)
|
||||
|
||||
return style.RenderWithTitle(border, icons.I.Response+"Responses", inner, w, h)
|
||||
return ilovetui.RenderWithTitle(border, icons.I.Response+"Responses", inner, w, h)
|
||||
}
|
||||
|
||||
func (m *Model) renderBodyPanel(h int) string {
|
||||
s := style.S
|
||||
|
||||
var body string
|
||||
if m.editing {
|
||||
body = m.textarea.View()
|
||||
} else {
|
||||
body = style.ViewportView(&m.bodyViewport)
|
||||
body = ilovetui.ViewportView(&m.bodyViewport)
|
||||
}
|
||||
|
||||
border := s.Panel
|
||||
border := ilovetui.S.Panel
|
||||
if m.editing {
|
||||
border = s.PanelFocused
|
||||
border = ilovetui.S.PanelFocused
|
||||
}
|
||||
|
||||
title := icons.I.Detail + "Details"
|
||||
return style.RenderWithTitle(border, title, body, m.width, h)
|
||||
return ilovetui.RenderWithTitle(border, title, body, m.width, h)
|
||||
}
|
||||
|
||||
func (m *Model) renderStatusBar() string {
|
||||
@@ -105,10 +100,9 @@ func (m *Model) renderStatusBar() string {
|
||||
|
||||
func (m *Model) renderList() string {
|
||||
if len(m.queue) == 0 {
|
||||
return lipgloss.Place(m.listViewport.Width(), m.listViewport.Height(), lipgloss.Center, lipgloss.Center, style.S.Faint.Render(util.CenterLines("(。◕‿‿◕。)", "waiting for a request")))
|
||||
return lipgloss.Place(m.listViewport.Width(), m.listViewport.Height(), lipgloss.Center, lipgloss.Center, ilovetui.S.Faint.Render(util.CenterLines("(。◕‿‿◕。)", "waiting for a request")))
|
||||
}
|
||||
|
||||
s := style.S
|
||||
start, end := util.PageBounds(m.pager, len(m.queue))
|
||||
|
||||
var sb strings.Builder
|
||||
@@ -121,7 +115,7 @@ func (m *Model) renderList() string {
|
||||
}
|
||||
|
||||
selected := globalIdx == m.cursor
|
||||
selBg := s.Selection
|
||||
selBg := ilovetui.S.Selection
|
||||
|
||||
w := m.listViewport.Width()
|
||||
const fixedW = 2 + 7 + 2
|
||||
@@ -134,18 +128,18 @@ func (m *Model) renderList() string {
|
||||
if selected {
|
||||
bg := lipgloss.NewStyle().Background(selBg)
|
||||
line = lipgloss.JoinHorizontal(lipgloss.Top,
|
||||
bg.Bold(true).Foreground(s.Primary).Width(2).Render(">"),
|
||||
s.Method(r.Method).Background(selBg).Render(r.Method),
|
||||
bg.Bold(true).Foreground(ilovetui.S.Primary).Width(2).Render(">"),
|
||||
style.S.Method(r.Method).Background(selBg).Render(r.Method),
|
||||
bg.Width(2).Render(""),
|
||||
bg.Bold(true).Width(hostPathW).Render(r.URL.Host+path),
|
||||
)
|
||||
} else {
|
||||
line = lipgloss.JoinHorizontal(lipgloss.Top,
|
||||
" ",
|
||||
s.Method(r.Method).Render(r.Method),
|
||||
s.Faint.Render(" "),
|
||||
s.Bold.Render(r.URL.Host),
|
||||
s.Faint.Render(path),
|
||||
style.S.Method(r.Method).Render(r.Method),
|
||||
ilovetui.S.Faint.Render(" "),
|
||||
ilovetui.S.Bold.Render(r.URL.Host),
|
||||
ilovetui.S.Faint.Render(path),
|
||||
)
|
||||
}
|
||||
sb.WriteString(line + "\n")
|
||||
@@ -155,10 +149,9 @@ func (m *Model) renderList() string {
|
||||
|
||||
func (m *Model) renderResponseList() string {
|
||||
if len(m.responseQueue) == 0 {
|
||||
return lipgloss.Place(m.responseViewport.Width(), m.responseViewport.Height(), lipgloss.Center, lipgloss.Center, style.S.Faint.Render(util.CenterLines("(҂◡_◡)", "no response yet")))
|
||||
return lipgloss.Place(m.responseViewport.Width(), m.responseViewport.Height(), lipgloss.Center, lipgloss.Center, ilovetui.S.Faint.Render(util.CenterLines("(҂◡_◡)", "no response yet")))
|
||||
}
|
||||
|
||||
s := style.S
|
||||
start, end := util.PageBounds(m.responsePager, len(m.responseQueue))
|
||||
|
||||
var sb strings.Builder
|
||||
@@ -177,7 +170,7 @@ func (m *Model) renderResponseList() string {
|
||||
statusStr := fmt.Sprintf("%d", code)
|
||||
|
||||
selected := globalIdx == m.responseCursor
|
||||
selBg := s.Selection
|
||||
selBg := ilovetui.S.Selection
|
||||
|
||||
statusSt := style.StatusStyle(code, 7)
|
||||
|
||||
@@ -192,7 +185,7 @@ func (m *Model) renderResponseList() string {
|
||||
if selected {
|
||||
bg := lipgloss.NewStyle().Background(selBg)
|
||||
line = lipgloss.JoinHorizontal(lipgloss.Top,
|
||||
bg.Bold(true).Foreground(s.Primary).Width(2).Render(">"),
|
||||
bg.Bold(true).Foreground(ilovetui.S.Primary).Width(2).Render(">"),
|
||||
statusSt.Background(selBg).Render(statusStr),
|
||||
bg.Width(2).Render(""),
|
||||
bg.Bold(true).Width(hostPathW).Render(f.Request.URL.Host+path),
|
||||
@@ -201,9 +194,9 @@ func (m *Model) renderResponseList() string {
|
||||
line = lipgloss.JoinHorizontal(lipgloss.Top,
|
||||
" ",
|
||||
statusSt.Render(statusStr),
|
||||
s.Faint.Render(" "),
|
||||
s.Bold.Render(f.Request.URL.Host),
|
||||
s.Faint.Render(path),
|
||||
ilovetui.S.Faint.Render(" "),
|
||||
ilovetui.S.Bold.Render(f.Request.URL.Host),
|
||||
ilovetui.S.Faint.Render(path),
|
||||
)
|
||||
}
|
||||
sb.WriteString(line + "\n")
|
||||
|
||||
@@ -10,9 +10,9 @@ import (
|
||||
"charm.land/bubbles/v2/textinput"
|
||||
"charm.land/bubbles/v2/viewport"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
ilovetui "github.com/anotherhadi/ilovetui"
|
||||
"github.com/anotherhadi/spilltea/internal/keys"
|
||||
"github.com/anotherhadi/spilltea/internal/plugins"
|
||||
"github.com/anotherhadi/spilltea/internal/style"
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
@@ -36,7 +36,7 @@ type Model struct {
|
||||
}
|
||||
|
||||
func New(mgr *plugins.Manager) Model {
|
||||
ta := style.NewTextarea(true)
|
||||
ta := ilovetui.NewTextarea(true)
|
||||
ta.Placeholder = "plugin configuration..."
|
||||
ta.Blur()
|
||||
|
||||
@@ -45,12 +45,12 @@ func New(mgr *plugins.Manager) Model {
|
||||
|
||||
return Model{
|
||||
manager: mgr,
|
||||
listViewport: style.NewViewport(),
|
||||
detailViewport: style.NewViewport(),
|
||||
listViewport: ilovetui.NewViewport(),
|
||||
detailViewport: ilovetui.NewViewport(),
|
||||
textarea: ta,
|
||||
filterInput: fi,
|
||||
pager: style.NewPaginator(),
|
||||
help: style.NewHelp(),
|
||||
pager: ilovetui.NewPaginator(),
|
||||
help: ilovetui.NewHelp(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,14 +70,14 @@ func (m *Model) recalcSizes() {
|
||||
}
|
||||
m.help.SetWidth(m.width - 2)
|
||||
|
||||
listH, detailH := style.SplitH(m.height, m.renderStatusBar(), 0.4)
|
||||
listH, detailH := ilovetui.SplitH(m.height, m.renderStatusBar(), 0.4)
|
||||
|
||||
inner := m.width - 2
|
||||
if inner < 0 {
|
||||
inner = 0
|
||||
}
|
||||
|
||||
listVH := style.PanelContentH(listH) - 1 // -1 for the pager dots row
|
||||
listVH := ilovetui.ContentHeight(listH) - 1 // -1 for the pager dots row
|
||||
if listVH < 0 {
|
||||
listVH = 0
|
||||
}
|
||||
@@ -90,7 +90,7 @@ func (m *Model) recalcSizes() {
|
||||
|
||||
m.filterInput.SetWidth(inner - 2)
|
||||
|
||||
detailContentH := style.PanelContentH(detailH)
|
||||
detailContentH := ilovetui.ContentHeight(detailH)
|
||||
const headerH = 2
|
||||
const configFixedH = 2 // blank line + label line
|
||||
textareaH := max(3, detailContentH/3)
|
||||
|
||||
+29
-34
@@ -7,19 +7,18 @@ import (
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/glamour/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
"github.com/anotherhadi/spilltea/internal/config"
|
||||
ilovetui "github.com/anotherhadi/ilovetui"
|
||||
"github.com/anotherhadi/spilltea/internal/icons"
|
||||
"github.com/anotherhadi/spilltea/internal/keys"
|
||||
"github.com/anotherhadi/spilltea/internal/style"
|
||||
"github.com/anotherhadi/spilltea/internal/util"
|
||||
)
|
||||
|
||||
func (m Model) View() tea.View {
|
||||
if m.width == 0 || m.manager == nil {
|
||||
return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, style.S.Faint.Render(util.CenterLines("(._.)~*.'", "no plugins loaded"))))
|
||||
return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, ilovetui.S.Faint.Render(util.CenterLines("(._.)~*.'", "no plugins loaded"))))
|
||||
}
|
||||
|
||||
listH, detailH := style.SplitH(m.height, m.renderStatusBar(), 0.4)
|
||||
listH, detailH := ilovetui.SplitH(m.height, m.renderStatusBar(), 0.4)
|
||||
|
||||
content := lipgloss.JoinVertical(lipgloss.Left,
|
||||
m.renderListPanel(m.width, listH),
|
||||
@@ -30,36 +29,34 @@ func (m Model) View() tea.View {
|
||||
}
|
||||
|
||||
func (m *Model) renderListPanel(w, h int) string {
|
||||
s := style.S
|
||||
panelStyle := s.PanelFocused
|
||||
panelStyle := ilovetui.S.PanelFocused
|
||||
if m.editing {
|
||||
panelStyle = s.Panel
|
||||
panelStyle = ilovetui.S.Panel
|
||||
}
|
||||
var dots string
|
||||
if len(m.filtered) > 0 {
|
||||
dots = s.Faint.Render(m.pager.View())
|
||||
dots = ilovetui.S.Faint.Render(m.pager.View())
|
||||
}
|
||||
inner := lipgloss.JoinVertical(lipgloss.Left,
|
||||
m.listViewport.View(),
|
||||
lipgloss.PlaceHorizontal(m.listViewport.Width(), lipgloss.Center, dots),
|
||||
)
|
||||
return style.RenderWithTitle(panelStyle, icons.I.Plugin+"Plugins", inner, w, h)
|
||||
return ilovetui.RenderWithTitle(panelStyle, icons.I.Plugin+"Plugins", inner, w, h)
|
||||
}
|
||||
|
||||
func (m *Model) renderDetailPanel(h int) string {
|
||||
s := style.S
|
||||
panelStyle := s.Panel
|
||||
panelStyle := ilovetui.S.Panel
|
||||
if m.editing {
|
||||
panelStyle = s.PanelFocused
|
||||
panelStyle = ilovetui.S.PanelFocused
|
||||
}
|
||||
info, ok := m.selected()
|
||||
if !ok {
|
||||
return style.RenderWithTitle(panelStyle, icons.I.Detail+"Detail", "", m.width, h)
|
||||
return ilovetui.RenderWithTitle(panelStyle, icons.I.Detail+"Detail", "", m.width, h)
|
||||
}
|
||||
|
||||
statusSt := lipgloss.NewStyle().Foreground(s.Error)
|
||||
statusSt := lipgloss.NewStyle().Foreground(ilovetui.S.Error)
|
||||
if info.Enabled {
|
||||
statusSt = lipgloss.NewStyle().Foreground(s.Success)
|
||||
statusSt = lipgloss.NewStyle().Foreground(ilovetui.S.Success)
|
||||
}
|
||||
status := "disabled"
|
||||
if info.Enabled {
|
||||
@@ -69,26 +66,26 @@ func (m *Model) renderDetailPanel(h int) string {
|
||||
pad := lipgloss.NewStyle().Padding(0, 1)
|
||||
|
||||
header := pad.Render(
|
||||
s.Bold.Render(info.Name) + " " + statusSt.Render(status) + "\n" +
|
||||
s.Faint.Render(filepath.Base(info.FilePath)),
|
||||
ilovetui.S.Bold.Render(info.Name) + " " + statusSt.Render(status) + "\n" +
|
||||
ilovetui.S.Faint.Render(filepath.Base(info.FilePath)),
|
||||
)
|
||||
|
||||
parts := []string{header, style.ViewportView(&m.detailViewport)}
|
||||
parts := []string{header, ilovetui.ViewportView(&m.detailViewport)}
|
||||
|
||||
if m.hasConfig() {
|
||||
var configLabel string
|
||||
if m.editing {
|
||||
escKey := keys.Keys.Global.Escape.Help().Key
|
||||
configLabel = pad.Render(s.Faint.Render("editing config (" + escKey + " to save):"))
|
||||
configLabel = pad.Render(ilovetui.S.Faint.Render("editing config (" + escKey + " to save):"))
|
||||
} else {
|
||||
editKey := keys.Keys.Plugins.EditConfig.Help().Key
|
||||
configLabel = pad.Render(s.Faint.Render("config (" + editKey + " to edit):"))
|
||||
configLabel = pad.Render(ilovetui.S.Faint.Render("config (" + editKey + " to edit):"))
|
||||
}
|
||||
parts = append(parts, "", configLabel, pad.Render(m.textarea.View()))
|
||||
}
|
||||
|
||||
inner := lipgloss.JoinVertical(lipgloss.Left, parts...)
|
||||
return style.RenderWithTitle(panelStyle, icons.I.Detail+"Detail", inner, m.width, h)
|
||||
return ilovetui.RenderWithTitle(panelStyle, icons.I.Detail+"Detail", inner, m.width, h)
|
||||
}
|
||||
|
||||
func renderPluginDescription(desc string, width int) string {
|
||||
@@ -100,7 +97,7 @@ func renderPluginDescription(desc string, width int) string {
|
||||
desc = strings.Join(lines, "\n")
|
||||
|
||||
r, err := glamour.NewTermRenderer(
|
||||
glamour.WithStyles(style.GlamourStyleConfig(config.Global)),
|
||||
glamour.WithStyles(ilovetui.GlamourStyleConfig()),
|
||||
glamour.WithWordWrap(width),
|
||||
)
|
||||
if err != nil {
|
||||
@@ -114,23 +111,21 @@ func renderPluginDescription(desc string, width int) string {
|
||||
}
|
||||
|
||||
func (m *Model) renderStatusBar() string {
|
||||
s := style.S
|
||||
pad := lipgloss.NewStyle().Padding(0, 1)
|
||||
filterKey := keys.Keys.Plugins.Filter.Help().Key
|
||||
if m.filtering {
|
||||
return pad.Render(s.Faint.Render(filterKey) + " " + m.filterInput.View())
|
||||
return pad.Render(ilovetui.S.Faint.Render(filterKey) + " " + m.filterInput.View())
|
||||
}
|
||||
if m.filter != "" {
|
||||
escKey := keys.Keys.Global.Escape.Help().Key
|
||||
accent := lipgloss.NewStyle().Foreground(s.Primary)
|
||||
filterLine := pad.Render(accent.Render(filterKey) + " " + s.Bold.Render(m.filter) + s.Faint.Render(" "+escKey+" to clear"))
|
||||
accent := lipgloss.NewStyle().Foreground(ilovetui.S.Primary)
|
||||
filterLine := pad.Render(accent.Render(filterKey) + " " + ilovetui.S.Bold.Render(m.filter) + ilovetui.S.Faint.Render(" "+escKey+" to clear"))
|
||||
return lipgloss.JoinVertical(lipgloss.Left, filterLine, pad.Render(m.help.View(pluginsKeyMap{editing: m.editing, hasConfig: m.hasConfig(), width: m.width})))
|
||||
}
|
||||
return pad.Render(m.help.View(pluginsKeyMap{editing: m.editing, hasConfig: m.hasConfig(), width: m.width}))
|
||||
}
|
||||
|
||||
func (m *Model) renderList() string {
|
||||
s := style.S
|
||||
if len(m.filtered) == 0 {
|
||||
msg := util.CenterLines("(ง •̀_•́)ง", "no plugins", "", "spilltea --add-default-plugins")
|
||||
if m.filter != "" {
|
||||
@@ -139,7 +134,7 @@ func (m *Model) renderList() string {
|
||||
return lipgloss.Place(
|
||||
m.listViewport.Width(), m.listViewport.Height(),
|
||||
lipgloss.Center, lipgloss.Center,
|
||||
s.Faint.Render(msg),
|
||||
ilovetui.S.Faint.Render(msg),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -150,10 +145,10 @@ func (m *Model) renderList() string {
|
||||
globalIdx := start + i
|
||||
selected := globalIdx == m.cursor
|
||||
|
||||
enabledSt := lipgloss.NewStyle().Foreground(s.Error)
|
||||
enabledSt := lipgloss.NewStyle().Foreground(ilovetui.S.Error)
|
||||
enabledStr := "off"
|
||||
if p.Enabled {
|
||||
enabledSt = lipgloss.NewStyle().Foreground(s.Success)
|
||||
enabledSt = lipgloss.NewStyle().Foreground(ilovetui.S.Success)
|
||||
enabledStr = "on "
|
||||
}
|
||||
|
||||
@@ -166,10 +161,10 @@ func (m *Model) renderList() string {
|
||||
|
||||
var line string
|
||||
if selected {
|
||||
bg := lipgloss.NewStyle().Background(s.Selection)
|
||||
bg := lipgloss.NewStyle().Background(ilovetui.S.Selection)
|
||||
line = lipgloss.JoinHorizontal(lipgloss.Top,
|
||||
bg.Bold(true).Foreground(s.Primary).Width(2).Render(">"),
|
||||
enabledSt.Background(s.Selection).Width(3).Render(enabledStr),
|
||||
bg.Bold(true).Foreground(ilovetui.S.Primary).Width(2).Render(">"),
|
||||
enabledSt.Background(ilovetui.S.Selection).Width(3).Render(enabledStr),
|
||||
bg.Width(1).Render(""),
|
||||
bg.Bold(true).Width(nameW).Render(p.Name),
|
||||
)
|
||||
@@ -178,7 +173,7 @@ func (m *Model) renderList() string {
|
||||
" ",
|
||||
enabledSt.Width(3).Render(enabledStr),
|
||||
" ",
|
||||
s.Bold.Render(p.Name),
|
||||
ilovetui.S.Bold.Render(p.Name),
|
||||
)
|
||||
}
|
||||
sb.WriteString(line + "\n")
|
||||
|
||||
+10
-10
@@ -9,9 +9,9 @@ import (
|
||||
"charm.land/bubbles/v2/textarea"
|
||||
"charm.land/bubbles/v2/viewport"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
ilovetui "github.com/anotherhadi/ilovetui"
|
||||
"github.com/anotherhadi/spilltea/internal/db"
|
||||
"github.com/anotherhadi/spilltea/internal/keys"
|
||||
"github.com/anotherhadi/spilltea/internal/style"
|
||||
)
|
||||
|
||||
type SendToReplayMsg struct {
|
||||
@@ -61,15 +61,15 @@ type Model struct {
|
||||
}
|
||||
|
||||
func New() Model {
|
||||
ta := style.NewTextarea(false)
|
||||
ta := ilovetui.NewTextarea(false)
|
||||
ta.Blur()
|
||||
return Model{
|
||||
listViewport: style.NewViewport(),
|
||||
requestViewport: style.NewViewport(),
|
||||
responseViewport: style.NewViewport(),
|
||||
listViewport: ilovetui.NewViewport(),
|
||||
requestViewport: ilovetui.NewViewport(),
|
||||
responseViewport: ilovetui.NewViewport(),
|
||||
textarea: ta,
|
||||
pager: style.NewPaginator(),
|
||||
help: style.NewHelp(),
|
||||
pager: ilovetui.NewPaginator(),
|
||||
help: ilovetui.NewHelp(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,13 +149,13 @@ func (m *Model) SetSize(w, h int) {
|
||||
func (m *Model) recalcSizes() {
|
||||
m.help.SetWidth(m.width - 2)
|
||||
|
||||
listH, bodyH := style.SplitH(m.height, m.renderStatusBar(), 0.35)
|
||||
listH, bodyH := ilovetui.SplitH(m.height, m.renderStatusBar(), 0.35)
|
||||
|
||||
listInner := m.width - 2
|
||||
if listInner < 0 {
|
||||
listInner = 0
|
||||
}
|
||||
listVH := style.PanelContentH(listH) - 1 // -1 for the pager dots row
|
||||
listVH := ilovetui.ContentHeight(listH) - 1 // -1 for the pager dots row
|
||||
if listVH < 0 {
|
||||
listVH = 0
|
||||
}
|
||||
@@ -175,7 +175,7 @@ func (m *Model) recalcSizes() {
|
||||
if rightInner < 0 {
|
||||
rightInner = 0
|
||||
}
|
||||
bodyVH := style.PanelContentH(bodyH)
|
||||
bodyVH := ilovetui.ContentHeight(bodyH)
|
||||
|
||||
m.requestViewport.SetWidth(leftInner)
|
||||
m.requestViewport.SetHeight(bodyVH)
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
"github.com/andybalholm/brotli"
|
||||
ilovetui "github.com/anotherhadi/ilovetui"
|
||||
"github.com/anotherhadi/spilltea/internal/config"
|
||||
"github.com/anotherhadi/spilltea/internal/db"
|
||||
"github.com/anotherhadi/spilltea/internal/keys"
|
||||
@@ -181,6 +182,9 @@ func (m Model) updateNormalMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
|
||||
|
||||
case key.Matches(msg, r.EditExt):
|
||||
if len(m.entries) > 0 {
|
||||
if m.focusedPanel == panelResponse {
|
||||
return m, util.OpenExternalEditorView(m.entries[m.cursor].ResponseRaw)
|
||||
}
|
||||
return m, util.OpenExternalEditor(m.entries[m.cursor].RequestRaw)
|
||||
}
|
||||
|
||||
@@ -347,11 +351,11 @@ func (m *Model) refreshBody() {
|
||||
m.requestViewport.SetXOffset(0)
|
||||
|
||||
if e.Sending {
|
||||
m.responseViewport.SetContent(lipgloss.Place(m.responseViewport.Width(), m.responseViewport.Height(), lipgloss.Center, lipgloss.Center, style.S.Faint.Render(util.CenterLines("(ノ◕ヮ◕)ノ*:・゚", "sending..."))))
|
||||
m.responseViewport.SetContent(lipgloss.Place(m.responseViewport.Width(), m.responseViewport.Height(), lipgloss.Center, lipgloss.Center, ilovetui.S.Faint.Render(util.CenterLines("(ノ◕ヮ◕)ノ*:・゚", "sending..."))))
|
||||
} else if e.ResponseRaw != "" {
|
||||
m.responseViewport.SetContent(style.HighlightHTTP(e.ResponseRaw))
|
||||
} else {
|
||||
m.responseViewport.SetContent(lipgloss.Place(m.responseViewport.Width(), m.responseViewport.Height(), lipgloss.Center, lipgloss.Center, style.S.Faint.Render(util.CenterLines("( •_•)>⌐■", "press send to fire"))))
|
||||
m.responseViewport.SetContent(lipgloss.Place(m.responseViewport.Width(), m.responseViewport.Height(), lipgloss.Center, lipgloss.Center, ilovetui.S.Faint.Render(util.CenterLines("( •_•)>⌐■", "press send to fire"))))
|
||||
}
|
||||
m.responseViewport.SetYOffset(0)
|
||||
m.responseViewport.SetXOffset(0)
|
||||
|
||||
+24
-27
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
ilovetui "github.com/anotherhadi/ilovetui"
|
||||
"github.com/anotherhadi/spilltea/internal/icons"
|
||||
"github.com/anotherhadi/spilltea/internal/style"
|
||||
"github.com/anotherhadi/spilltea/internal/util"
|
||||
@@ -16,7 +17,7 @@ func (m Model) View() tea.View {
|
||||
return tea.NewView("Loading...")
|
||||
}
|
||||
|
||||
listH, bodyH := style.SplitH(m.height, m.renderStatusBar(), 0.35)
|
||||
listH, bodyH := ilovetui.SplitH(m.height, m.renderStatusBar(), 0.35)
|
||||
leftW, rightW := m.bodyHalfWidths()
|
||||
|
||||
bodyRow := lipgloss.JoinHorizontal(lipgloss.Top,
|
||||
@@ -33,45 +34,42 @@ func (m Model) View() tea.View {
|
||||
}
|
||||
|
||||
func (m *Model) renderListPanel(w, h int) string {
|
||||
s := style.S
|
||||
panelStyle := s.Panel
|
||||
panelStyle := ilovetui.S.Panel
|
||||
if !m.editing && m.focusedPanel == panelList {
|
||||
panelStyle = s.PanelFocused
|
||||
panelStyle = ilovetui.S.PanelFocused
|
||||
}
|
||||
var dots string
|
||||
if len(m.entries) > 0 {
|
||||
dots = s.Faint.Render(m.pager.View())
|
||||
dots = ilovetui.S.Faint.Render(m.pager.View())
|
||||
}
|
||||
inner := lipgloss.JoinVertical(lipgloss.Left,
|
||||
m.listViewport.View(),
|
||||
lipgloss.PlaceHorizontal(m.listViewport.Width(), lipgloss.Center, dots),
|
||||
)
|
||||
return style.RenderWithTitle(panelStyle, icons.I.Replay+"Replay", inner, w, h)
|
||||
return ilovetui.RenderWithTitle(panelStyle, icons.I.Replay+"Replay", inner, w, h)
|
||||
}
|
||||
|
||||
func (m *Model) renderRequestPanel(w, h int) string {
|
||||
s := style.S
|
||||
var body string
|
||||
border := s.Panel
|
||||
border := ilovetui.S.Panel
|
||||
if m.editing {
|
||||
body = m.textarea.View()
|
||||
border = s.PanelFocused
|
||||
border = ilovetui.S.PanelFocused
|
||||
} else {
|
||||
body = style.ViewportView(&m.requestViewport)
|
||||
body = ilovetui.ViewportView(&m.requestViewport)
|
||||
if m.focusedPanel == panelRequest {
|
||||
border = s.PanelFocused
|
||||
border = ilovetui.S.PanelFocused
|
||||
}
|
||||
}
|
||||
return style.RenderWithTitle(border, icons.I.Request+"Request", body, w, h)
|
||||
return ilovetui.RenderWithTitle(border, icons.I.Request+"Request", body, w, h)
|
||||
}
|
||||
|
||||
func (m *Model) renderResponsePanel(w, h int) string {
|
||||
s := style.S
|
||||
border := s.Panel
|
||||
border := ilovetui.S.Panel
|
||||
if !m.editing && m.focusedPanel == panelResponse {
|
||||
border = s.PanelFocused
|
||||
border = ilovetui.S.PanelFocused
|
||||
}
|
||||
return style.RenderWithTitle(border, icons.I.Response+"Response", style.ViewportView(&m.responseViewport), w, h)
|
||||
return ilovetui.RenderWithTitle(border, icons.I.Response+"Response", ilovetui.ViewportView(&m.responseViewport), w, h)
|
||||
}
|
||||
|
||||
func (m *Model) renderStatusBar() string {
|
||||
@@ -83,18 +81,17 @@ func (m *Model) renderList() string {
|
||||
return lipgloss.Place(
|
||||
m.listViewport.Width(), m.listViewport.Height(),
|
||||
lipgloss.Center, lipgloss.Center,
|
||||
style.S.Faint.Render(util.CenterLines("(╥﹏╥)", "send a request from History or Intercept")),
|
||||
ilovetui.S.Faint.Render(util.CenterLines("(╥﹏╥)", "send a request from History or Intercept")),
|
||||
)
|
||||
}
|
||||
|
||||
s := style.S
|
||||
start, end := util.PageBounds(m.pager, len(m.entries))
|
||||
|
||||
var sb strings.Builder
|
||||
for i, e := range m.entries[start:end] {
|
||||
globalIdx := start + i
|
||||
selected := globalIdx == m.cursor
|
||||
selBg := s.Selection
|
||||
selBg := ilovetui.S.Selection
|
||||
|
||||
w := m.listViewport.Width()
|
||||
const fixedW = 2 + 7 + 1 + 3 + 1
|
||||
@@ -109,8 +106,8 @@ func (m *Model) renderList() string {
|
||||
if selected {
|
||||
bg := lipgloss.NewStyle().Background(selBg)
|
||||
line = lipgloss.JoinHorizontal(lipgloss.Top,
|
||||
bg.Bold(true).Foreground(s.Primary).Width(2).Render(">"),
|
||||
s.Method(e.Method).Background(selBg).Render(e.Method),
|
||||
bg.Bold(true).Foreground(ilovetui.S.Primary).Width(2).Render(">"),
|
||||
style.S.Method(e.Method).Background(selBg).Render(e.Method),
|
||||
bg.Width(1).Render(""),
|
||||
statusSt.Background(selBg).Render(statusStr),
|
||||
bg.Width(1).Render(""),
|
||||
@@ -119,12 +116,12 @@ func (m *Model) renderList() string {
|
||||
} else {
|
||||
line = lipgloss.JoinHorizontal(lipgloss.Top,
|
||||
" ",
|
||||
s.Method(e.Method).Render(e.Method),
|
||||
style.S.Method(e.Method).Render(e.Method),
|
||||
" ",
|
||||
statusSt.Render(statusStr),
|
||||
" ",
|
||||
s.Bold.Render(e.Host),
|
||||
s.Faint.Render(e.Path),
|
||||
ilovetui.S.Bold.Render(e.Host),
|
||||
ilovetui.S.Faint.Render(e.Path),
|
||||
)
|
||||
}
|
||||
sb.WriteString(line + "\n")
|
||||
@@ -136,11 +133,11 @@ func entryStatus(e Entry) (string, lipgloss.Style) {
|
||||
base := lipgloss.NewStyle().Bold(true).Width(3)
|
||||
switch {
|
||||
case e.Sending:
|
||||
return "···", base.Foreground(style.S.Subtle)
|
||||
return "···", base.Foreground(ilovetui.S.Subtle)
|
||||
case e.Err != nil:
|
||||
return "ERR", base.Foreground(style.S.Error)
|
||||
return "ERR", base.Foreground(ilovetui.S.Error)
|
||||
case e.StatusCode == 0:
|
||||
return "---", base.Foreground(style.S.Subtle)
|
||||
return "---", base.Foreground(ilovetui.S.Subtle)
|
||||
}
|
||||
return fmt.Sprintf("%3d", e.StatusCode), style.StatusStyle(e.StatusCode, 3)
|
||||
}
|
||||
|
||||
+18
-2
@@ -15,7 +15,7 @@ type EditorFinishedMsg struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
func OpenExternalEditor(content string) tea.Cmd {
|
||||
func resolveEditor() string {
|
||||
editor := config.Global.App.ExternalEditor
|
||||
if editor == "" {
|
||||
editor = os.Getenv("EDITOR")
|
||||
@@ -23,6 +23,10 @@ func OpenExternalEditor(content string) tea.Cmd {
|
||||
if editor == "" {
|
||||
editor = "vi"
|
||||
}
|
||||
return editor
|
||||
}
|
||||
|
||||
func openWithEditor(content string, callback func(string, error) tea.Msg) tea.Cmd {
|
||||
f, err := os.CreateTemp("", "spilltea-*.http")
|
||||
if err != nil {
|
||||
return nil
|
||||
@@ -32,8 +36,14 @@ func OpenExternalEditor(content string) tea.Cmd {
|
||||
log.Printf("editor: writing temp file: %v", err)
|
||||
}
|
||||
f.Close()
|
||||
return tea.ExecProcess(exec.Command(editor, tmpPath), func(err error) tea.Msg {
|
||||
return tea.ExecProcess(exec.Command(resolveEditor(), tmpPath), func(err error) tea.Msg {
|
||||
defer os.Remove(tmpPath)
|
||||
return callback(tmpPath, err)
|
||||
})
|
||||
}
|
||||
|
||||
func OpenExternalEditor(content string) tea.Cmd {
|
||||
return openWithEditor(content, func(tmpPath string, err error) tea.Msg {
|
||||
if err != nil {
|
||||
return EditorFinishedMsg{Err: err}
|
||||
}
|
||||
@@ -44,3 +54,9 @@ func OpenExternalEditor(content string) tea.Cmd {
|
||||
return EditorFinishedMsg{Content: string(data)}
|
||||
})
|
||||
}
|
||||
|
||||
func OpenExternalEditorView(content string) tea.Cmd {
|
||||
return openWithEditor(content, func(_ string, _ error) tea.Msg {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -25,6 +25,10 @@ schema = 3
|
||||
version = 'v1.0.4'
|
||||
hash = 'sha256-gAnPRdGP4yna4hiRIEDyBtDOVJqd7RU27wlPu96Rdf8='
|
||||
|
||||
[mod.'github.com/anotherhadi/ilovetui']
|
||||
version = 'v0.1.6'
|
||||
hash = 'sha256-7E+7UFks5vM3XWCvX2joFmHGcW7qqnoox6ZPFglaLO4='
|
||||
|
||||
[mod.'github.com/atotto/clipboard']
|
||||
version = 'v0.1.4'
|
||||
hash = 'sha256-ZZ7U5X0gWOu8zcjZcWbcpzGOGdycwq0TjTFh/eZHjXk='
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
hooks = {
|
||||
gofmt.enable = true;
|
||||
govet.enable = true;
|
||||
stylua.enable = true;
|
||||
|
||||
gomod2nix = {
|
||||
enable = true;
|
||||
@@ -53,6 +54,7 @@ in
|
||||
go
|
||||
python3
|
||||
doctoc
|
||||
stylua
|
||||
trufflehog
|
||||
gomod2nixPkgs.gomod2nix
|
||||
]
|
||||
|
||||
@@ -3,7 +3,7 @@ Plugin = {
|
||||
description = [[
|
||||
Inject custom headers into every intercepted request.
|
||||
|
||||
**Config** (YAML):
|
||||
**Config**:
|
||||
```yaml
|
||||
headers:
|
||||
- "X-My-Header: myvalue"
|
||||
|
||||
@@ -3,12 +3,13 @@ Plugin = {
|
||||
description = [[
|
||||
Checks that the proxy's outbound IP is in an allowed list on startup.
|
||||
|
||||
**Config** (YAML):
|
||||
**Config**:
|
||||
```yaml
|
||||
ips:
|
||||
- "1.2.3.4" # whitelist entry
|
||||
- "!5.6.7.8" # blacklist entry (blocked)
|
||||
```
|
||||
|
||||
- If no IPs are configured, the check is skipped.
|
||||
]],
|
||||
on_start = { sync = false },
|
||||
@@ -27,7 +28,9 @@ function on_config()
|
||||
if trimmed ~= "" then
|
||||
if trimmed:sub(1, 1) == "!" then
|
||||
local ip = trimmed:sub(2):match("^%s*(.-)%s*$")
|
||||
if ip ~= "" then table.insert(blacklist, ip) end
|
||||
if ip ~= "" then
|
||||
table.insert(blacklist, ip)
|
||||
end
|
||||
else
|
||||
table.insert(whitelist, trimmed)
|
||||
end
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
Plugin = {
|
||||
name = "Match & Replace",
|
||||
description = [[
|
||||
Automatically find and replace content in requests and responses.
|
||||
|
||||
**Config**:
|
||||
```yaml
|
||||
rules:
|
||||
- on: "request" # "request", "response", or "both" (default: "both")
|
||||
url: "example%.com" # optional: Lua pattern to filter by URL
|
||||
target: "body" # "body", "path", "url", or "header:Name"
|
||||
find: "old_string" # Lua pattern to search
|
||||
replace: "new_string" # replacement string (supports %1, %2 for captures)
|
||||
```
|
||||
|
||||
Example (inject a debug flag in JSON bodies):
|
||||
```yaml
|
||||
rules:
|
||||
- on: "both"
|
||||
url: "api%.example%.com"
|
||||
target: "body"
|
||||
find: '"debug":false'
|
||||
replace: '"debug":true'
|
||||
```
|
||||
|
||||
Example (replace an Authorization header):
|
||||
```yaml
|
||||
rules:
|
||||
- on: "request"
|
||||
target: "header:Authorization"
|
||||
find: "Bearer .*"
|
||||
replace: "Bearer MY_NEW_TOKEN"
|
||||
```
|
||||
|
||||
Example (rewrite a path prefix):
|
||||
```yaml
|
||||
rules:
|
||||
- on: "request"
|
||||
url: "staging%.example%.com"
|
||||
target: "path"
|
||||
find: "^/v1/"
|
||||
replace: "/v2/"
|
||||
```
|
||||
]],
|
||||
priority = 50,
|
||||
on_request = { sync = true },
|
||||
on_response = { sync = true },
|
||||
}
|
||||
|
||||
local rules = {}
|
||||
|
||||
function on_config()
|
||||
rules = {}
|
||||
local cfg = get_config()
|
||||
if not cfg or not cfg.rules then
|
||||
return
|
||||
end
|
||||
for _, r in ipairs(cfg.rules) do
|
||||
if r.find and r.find ~= "" then
|
||||
table.insert(rules, {
|
||||
on = r.on or "both",
|
||||
url = r.url,
|
||||
target = r.target or "body",
|
||||
find = r.find,
|
||||
replace = r.replace or "",
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function should_apply(rule, req, context)
|
||||
if rule.on ~= "both" and rule.on ~= context then
|
||||
return false
|
||||
end
|
||||
if rule.url and not req.url:match(rule.url) then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local function apply(rule, req, res, context)
|
||||
local target = rule.target
|
||||
if target == "body" then
|
||||
local obj = (context == "response") and res or req
|
||||
local body = obj:get_body()
|
||||
local new_body = body:gsub(rule.find, rule.replace)
|
||||
if new_body ~= body then
|
||||
obj:set_body(new_body)
|
||||
end
|
||||
elseif target == "path" then
|
||||
local new_path = req.path:gsub(rule.find, rule.replace)
|
||||
if new_path ~= req.path then
|
||||
req:set_path(new_path)
|
||||
end
|
||||
elseif target == "url" then
|
||||
local new_url = req.url:gsub(rule.find, rule.replace)
|
||||
if new_url ~= req.url then
|
||||
req:set_url(new_url)
|
||||
end
|
||||
elseif target:sub(1, 7) == "header:" then
|
||||
local name = target:sub(8)
|
||||
local obj = (context == "response") and res or req
|
||||
local val = obj.headers[name] or ""
|
||||
local new_val = val:gsub(rule.find, rule.replace)
|
||||
if new_val ~= val then
|
||||
obj:set_header(name, new_val)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function on_request(req)
|
||||
for _, rule in ipairs(rules) do
|
||||
if should_apply(rule, req, "request") then
|
||||
apply(rule, req, nil, "request")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function on_response(req, res)
|
||||
for _, rule in ipairs(rules) do
|
||||
if should_apply(rule, req, "response") then
|
||||
apply(rule, req, res, "response")
|
||||
end
|
||||
end
|
||||
end
|
||||
+28
-10
@@ -3,7 +3,7 @@ Plugin = {
|
||||
description = [[
|
||||
Auto-forward requests and exclude them from history based on patterns.
|
||||
|
||||
**Config** (YAML):
|
||||
**Config**:
|
||||
```yaml
|
||||
patterns:
|
||||
- "pattern" # whitelist: only intercept matching requests/responses and history
|
||||
@@ -52,7 +52,9 @@ function on_config()
|
||||
blacklist_req, whitelist_req = {}, {}
|
||||
blacklist_hist, whitelist_hist = {}, {}
|
||||
local cfg = get_config()
|
||||
if not cfg or not cfg.patterns then return end
|
||||
if not cfg or not cfg.patterns then
|
||||
return
|
||||
end
|
||||
for _, line in ipairs(cfg.patterns) do
|
||||
local trimmed = line:match("^%s*(.-)%s*$")
|
||||
if trimmed ~= "" and trimmed:sub(1, 1) ~= "#" then
|
||||
@@ -73,17 +75,27 @@ end
|
||||
|
||||
local function check_skip(url, bl_extra, wl_extra)
|
||||
for _, p in ipairs(blacklist) do
|
||||
if url:match(p) then return true end
|
||||
if url:match(p) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
for _, p in ipairs(bl_extra) do
|
||||
if url:match(p) then return true end
|
||||
if url:match(p) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
local wl = {}
|
||||
for _, p in ipairs(whitelist) do wl[#wl + 1] = p end
|
||||
for _, p in ipairs(wl_extra) do wl[#wl + 1] = p end
|
||||
for _, p in ipairs(whitelist) do
|
||||
wl[#wl + 1] = p
|
||||
end
|
||||
for _, p in ipairs(wl_extra) do
|
||||
wl[#wl + 1] = p
|
||||
end
|
||||
if #wl > 0 then
|
||||
for _, p in ipairs(wl) do
|
||||
if url:match(p) then return false end
|
||||
if url:match(p) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
@@ -91,13 +103,19 @@ local function check_skip(url, bl_extra, wl_extra)
|
||||
end
|
||||
|
||||
function on_request(req)
|
||||
if check_skip(req.url, blacklist_req, whitelist_req) then return "forward" end
|
||||
if check_skip(req.url, blacklist_req, whitelist_req) then
|
||||
return "forward"
|
||||
end
|
||||
end
|
||||
|
||||
function on_response(req)
|
||||
if check_skip(req.url, blacklist_req, whitelist_req) then return "forward" end
|
||||
if check_skip(req.url, blacklist_req, whitelist_req) then
|
||||
return "forward"
|
||||
end
|
||||
end
|
||||
|
||||
function on_history_entry(entry)
|
||||
if check_skip(entry.host .. entry.path, blacklist_hist, whitelist_hist) then return "skip" end
|
||||
if check_skip(entry.host .. entry.path, blacklist_hist, whitelist_hist) then
|
||||
return "skip"
|
||||
end
|
||||
end
|
||||
|
||||
+76
-28
@@ -21,40 +21,61 @@ local CONTENT_TYPES = {
|
||||
-- Key name alternation (case-insensitive via grep -i)
|
||||
-- Suffixes are required (no bare generic keyword alone).
|
||||
local KEYS = {
|
||||
"access(_key|_token)", "accessid_secret", "account(_key|_sid)",
|
||||
"admin_pass(word)?", "admin_user",
|
||||
"access(_key|_token)",
|
||||
"accessid_secret",
|
||||
"account(_key|_sid)",
|
||||
"admin_pass(word)?",
|
||||
"admin_user",
|
||||
"(algolia|aws|gcp|azure|heroku|firebase|github|gitlab|slack|datadog|stripe|twilio|vercel|supabase|sendgrid|cloudinary|cloudflare|bitbucket|npm|netlify|auth0|okta|sentry)(_?(api|secret|access)(_?(key|token|id|sid|secret))?|_?(key|token|id|sid|secret))",
|
||||
"ansible_vault_password", "aos_key",
|
||||
"ansible_vault_password",
|
||||
"aos_key",
|
||||
"api(_key|_secret|_token)",
|
||||
"app_(id|key|secret)", "application(_key|_id|_secret)",
|
||||
"auth(_token|_secret|orization)", "authkey", "authsecret",
|
||||
"app_(id|key|secret)",
|
||||
"application(_key|_id|_secret)",
|
||||
"auth(_token|_secret|orization)",
|
||||
"authkey",
|
||||
"authsecret",
|
||||
"bearer_?token",
|
||||
"bucket(_password|_key)",
|
||||
"cert_?pass(word)?", "certificate_password",
|
||||
"cert_?pass(word)?",
|
||||
"certificate_password",
|
||||
"client(_id|_secret)",
|
||||
"codecov_token", "consumer_(key|secret)",
|
||||
"connection_?string", "credentials?", "crypt(_key|_secret)",
|
||||
"codecov_token",
|
||||
"consumer_(key|secret)",
|
||||
"connection_?string",
|
||||
"credentials?",
|
||||
"crypt(_key|_secret)",
|
||||
"db_(password|passwd|user(name)?)",
|
||||
"deploy(_key|_password|_token)",
|
||||
"docker_?pass(word)?", "dockerhub_?password",
|
||||
"docker_?pass(word)?",
|
||||
"dockerhub_?password",
|
||||
"encryption_(key|password)",
|
||||
"jwt_secret", "json_web_token",
|
||||
"keycloak_secret", "kubernetes_token",
|
||||
"ldap_(password|bindpw)", "login(_password|_token)",
|
||||
"mail_?password", "mail_smtp_pass",
|
||||
"mysql_password", "mongo_password",
|
||||
"netlify_token", "npm(_token|_auth_token)",
|
||||
"jwt_secret",
|
||||
"json_web_token",
|
||||
"keycloak_secret",
|
||||
"kubernetes_token",
|
||||
"ldap_(password|bindpw)",
|
||||
"login(_password|_token)",
|
||||
"mail_?password",
|
||||
"mail_smtp_pass",
|
||||
"mysql_password",
|
||||
"mongo_password",
|
||||
"netlify_token",
|
||||
"npm(_token|_auth_token)",
|
||||
"oauth(_token|_secret)",
|
||||
"openai_(api_key|secret)",
|
||||
"pass(word)?", "passwd",
|
||||
"pass(word)?",
|
||||
"passwd",
|
||||
"private(_key|_token)",
|
||||
"rds_password",
|
||||
"s3(_key|_secret|_access_key_id)",
|
||||
"secret(_key|_token|_id)", "security_token",
|
||||
"secret(_key|_token|_id)",
|
||||
"security_token",
|
||||
"sendgrid_api_key",
|
||||
"ses_(smtp|access|secret)",
|
||||
"service(_account|_key|_token)",
|
||||
"smtp_pass(word)?", "smtp_secret",
|
||||
"smtp_pass(word)?",
|
||||
"smtp_secret",
|
||||
"sonar_token",
|
||||
"ssh(_key|_private_key|_rsa)",
|
||||
"supabase(_anon|_service)?_key",
|
||||
@@ -79,10 +100,14 @@ local FULL_PAT = KEY_PAT .. '[a-z0-9._-]{0,20}[^=:a-zA-Z0-9_]{0,3}[[:space:]]*[:
|
||||
local GREP_CMD = "grep -Eoni '" .. FULL_PAT .. "'"
|
||||
|
||||
local function is_relevant(ct)
|
||||
if not ct or ct == "" then return false end
|
||||
if not ct or ct == "" then
|
||||
return false
|
||||
end
|
||||
ct = ct:lower()
|
||||
for _, t in ipairs(CONTENT_TYPES) do
|
||||
if ct:find(t, 1, true) then return true end
|
||||
if ct:find(t, 1, true) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
@@ -94,17 +119,23 @@ local function build_context(lines, linenum)
|
||||
local before, after = {}, {}
|
||||
for i = lo, linenum - 1 do
|
||||
local l = lines[i] or ""
|
||||
if #l > 120 then l = l:sub(1, 120) .. "..." end
|
||||
if #l > 120 then
|
||||
l = l:sub(1, 120) .. "..."
|
||||
end
|
||||
table.insert(before, l)
|
||||
end
|
||||
for i = linenum + 1, hi do
|
||||
local l = lines[i] or ""
|
||||
if #l > 120 then l = l:sub(1, 120) .. "..." end
|
||||
if #l > 120 then
|
||||
l = l:sub(1, 120) .. "..."
|
||||
end
|
||||
table.insert(after, l)
|
||||
end
|
||||
|
||||
local matched_line = lines[linenum] or ""
|
||||
if #matched_line > 200 then matched_line = matched_line:sub(1, 200) .. "..." end
|
||||
if #matched_line > 200 then
|
||||
matched_line = matched_line:sub(1, 200) .. "..."
|
||||
end
|
||||
|
||||
local parts = {}
|
||||
if #before > 0 then
|
||||
@@ -118,15 +149,21 @@ local function build_context(lines, linenum)
|
||||
end
|
||||
|
||||
local function scan(label, ct, body, host, path)
|
||||
if not is_relevant(ct) then return end
|
||||
if not body or body == "" then return end
|
||||
if not is_relevant(ct) then
|
||||
return
|
||||
end
|
||||
if not body or body == "" then
|
||||
return
|
||||
end
|
||||
|
||||
local out, err = shell_pipe(GREP_CMD, body)
|
||||
if err and err ~= "" then
|
||||
log("grep error on " .. label .. " for " .. host .. path .. ": " .. err)
|
||||
return
|
||||
end
|
||||
if not out or out == "" then return end
|
||||
if not out or out == "" then
|
||||
return
|
||||
end
|
||||
|
||||
local lines = {}
|
||||
for line in (body .. "\n"):gmatch("([^\n]*)\n") do
|
||||
@@ -140,11 +177,22 @@ local function scan(label, ct, body, host, path)
|
||||
matched = matched:match("^%s*(.-)%s*$")
|
||||
if matched ~= "" then
|
||||
local display = matched
|
||||
if #display > 200 then display = display:sub(1, 200) .. "..." end
|
||||
if #display > 200 then
|
||||
display = display:sub(1, 200) .. "..."
|
||||
end
|
||||
local ctx = build_context(lines, linenum)
|
||||
create_finding({
|
||||
title = "Potential secret in " .. label .. " (" .. host .. ")",
|
||||
description = "**Host:** `" .. host .. "` \n**Path:** `" .. path .. "`\n\n**Match:** `" .. display .. "`\n\n" .. ctx,
|
||||
description = "**Host:** `"
|
||||
.. host
|
||||
.. "`\n"
|
||||
.. "\n**Path:** `"
|
||||
.. path
|
||||
.. "`\n"
|
||||
.. "\n**Match:** `"
|
||||
.. display
|
||||
.. "`\n\n"
|
||||
.. ctx,
|
||||
key = host .. "|" .. path .. "|" .. label .. "|" .. matched,
|
||||
severity = "high",
|
||||
})
|
||||
|
||||
+17
-9
@@ -4,9 +4,6 @@ Plugin = {
|
||||
Scans request and response bodies for secrets using [TruffleHog](https://github.com/trufflesecurity/trufflehog).
|
||||
|
||||
Requires `trufflehog` v3+ to be installed and available in PATH.
|
||||
|
||||
Each finding is stored on the **Findings** page with the matched detector output.
|
||||
Findings are deduplicated per host+path+body content so repeated requests do not create duplicates.
|
||||
]],
|
||||
on_start = { sync = false },
|
||||
on_request = { sync = false },
|
||||
@@ -24,28 +21,39 @@ function on_start()
|
||||
end
|
||||
|
||||
local function scan(label, content, host, path)
|
||||
if not content or content == "" then return end
|
||||
local out, err = shell_pipe("f=$(mktemp) && cat > \"$f\" && trufflehog filesystem --no-color \"$f\"; rc=$?; rm -f \"$f\"; exit $rc", content)
|
||||
if not content or content == "" then
|
||||
return
|
||||
end
|
||||
local out, err = shell_pipe(
|
||||
'f=$(mktemp) && cat > "$f" && trufflehog filesystem --no-color "$f"; rc=$?; rm -f "$f"; exit $rc',
|
||||
content
|
||||
)
|
||||
if err and err ~= "" then
|
||||
log("trufflehog error on " .. label .. ": " .. err)
|
||||
return
|
||||
end
|
||||
if not out or out == "" then return end
|
||||
if not out or out == "" then
|
||||
return
|
||||
end
|
||||
local blocks = {}
|
||||
local current = nil
|
||||
for line in out:gmatch("[^\n]+") do
|
||||
if line:match("^Found ") then
|
||||
if current then table.insert(blocks, current) end
|
||||
if current then
|
||||
table.insert(blocks, current)
|
||||
end
|
||||
current = line
|
||||
elseif current then
|
||||
current = current .. "\n" .. line
|
||||
end
|
||||
end
|
||||
if current then table.insert(blocks, current) end
|
||||
if current then
|
||||
table.insert(blocks, current)
|
||||
end
|
||||
for _, block in ipairs(blocks) do
|
||||
create_finding({
|
||||
title = "Secret detected in " .. label .. " (" .. host .. ")",
|
||||
description = "**Host:** `" .. host .. "` \n**Path:** `" .. path .. "`\n\n```\n" .. block .. "\n```",
|
||||
description = "**Host:** `" .. host .. "`\n\n**Path:** `" .. path .. "`\n\n```\n" .. block .. "\n```",
|
||||
key = host .. "|" .. path .. "|" .. label .. "|" .. block,
|
||||
severity = "high",
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user