mirror of
https://github.com/anotherhadi/spilltea.git
synced 2026-05-20 09:42:34 +02:00
Compare commits
24 Commits
v0.0.3
..
ed59923b7d
| Author | SHA1 | Date | |
|---|---|---|---|
| ed59923b7d | |||
| aa7b639f82 | |||
| 27e0c418e9 | |||
| 08757a5d1d | |||
| ffee0978e6 | |||
| 4f45a7c061 | |||
| 0fafa52c65 | |||
| 366bb682d2 | |||
| 9fe0c74150 | |||
| 3b6b58ac2b | |||
| 789a513469 | |||
| 615093bd8b | |||
| 7e1b7d3b5a | |||
| e3e89582c1 | |||
| 2705c2882d | |||
| 6ea692754a | |||
| 85c2806604 | |||
| d451965fa0 | |||
| d82a220e91 | |||
| 1ac5eb26e8 | |||
| 969febb14c | |||
| 6aa377acd8 | |||
| 2f4765bf37 | |||
| fac335a16e |
Binary file not shown.
|
After Width: | Height: | Size: 906 KiB |
@@ -0,0 +1,41 @@
|
|||||||
|
Output ./.github/assets/demo.gif
|
||||||
|
Require spilltea
|
||||||
|
|
||||||
|
Set Shell "zsh"
|
||||||
|
Set FontSize 32
|
||||||
|
Set Width 1600
|
||||||
|
Set Height 1900
|
||||||
|
|
||||||
|
Type "spilltea"
|
||||||
|
Sleep 800ms
|
||||||
|
Enter
|
||||||
|
Sleep 3s
|
||||||
|
|
||||||
|
Down@800ms 2
|
||||||
|
Up@800ms 1
|
||||||
|
Enter@800ms 1
|
||||||
|
|
||||||
|
Wait+Screen /hadi.icu/
|
||||||
|
Sleep 3s
|
||||||
|
|
||||||
|
Ctrl+Y
|
||||||
|
Sleep 3s
|
||||||
|
|
||||||
|
Down@1.9 4
|
||||||
|
Escape@1.3 1
|
||||||
|
|
||||||
|
Ctrl+R
|
||||||
|
Sleep 3s
|
||||||
|
Type "e"
|
||||||
|
Sleep 1s
|
||||||
|
Escape
|
||||||
|
Sleep 1s
|
||||||
|
Type "s"
|
||||||
|
Sleep 6s
|
||||||
|
|
||||||
|
Type "1"
|
||||||
|
Sleep 2s
|
||||||
|
Type "f"
|
||||||
|
Sleep 2s
|
||||||
|
Type "2"
|
||||||
|
Sleep 2s
|
||||||
|
|||||||
Executable
+27
@@ -0,0 +1,27 @@
|
|||||||
|
#!/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"
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
PATTERN = re.compile(r"<!-- exec: (.+?) -->.*?<!-- endexec -->", re.DOTALL)
|
||||||
|
|
||||||
|
|
||||||
|
def replace(match):
|
||||||
|
cmd = match.group(1).strip()
|
||||||
|
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
|
||||||
|
output = result.stdout
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f"[inject-exec] command failed ({result.returncode}): {cmd}", file=sys.stderr)
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
output = re.sub(r"<!-- exec: .+? -->\n?|<!-- endexec -->\n?", "", output)
|
||||||
|
if output and not output.endswith("\n"):
|
||||||
|
output += "\n"
|
||||||
|
return f"<!-- exec: {cmd} -->\n{output}<!-- endexec -->"
|
||||||
|
|
||||||
|
|
||||||
|
def process(path):
|
||||||
|
content = Path(path).read_text()
|
||||||
|
new_content = PATTERN.sub(replace, content)
|
||||||
|
if new_content != content:
|
||||||
|
Path(path).write_text(new_content)
|
||||||
|
print(f"[inject-exec] updated {path}")
|
||||||
|
|
||||||
|
|
||||||
|
for p in sys.argv[1:]:
|
||||||
|
process(p)
|
||||||
@@ -14,23 +14,92 @@
|
|||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
[](https://goreportcard.com/report/github.com/anotherhadi/spilltea)
|
[](https://goreportcard.com/report/github.com/anotherhadi/spilltea)
|
||||||
|
|
||||||
|
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||||
|
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||||
|
|
||||||
|
- [What is Spilltea?](#what-is-spilltea)
|
||||||
|
- [Legal Disclaimer](#legal-disclaimer)
|
||||||
|
- [Features](#features)
|
||||||
|
- [Installation](#installation)
|
||||||
|
- [Project Management](#project-management)
|
||||||
|
- [Configuration](#configuration)
|
||||||
|
- [CLI Flags](#cli-flags)
|
||||||
|
- [Plugin System](#plugin-system)
|
||||||
|
- [Deployment](#deployment)
|
||||||
|
- [Tech Stack](#tech-stack)
|
||||||
|
|
||||||
|
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||||
|
|
||||||
## What is Spilltea?
|
## What is Spilltea?
|
||||||
|
|
||||||
Spilltea is a **terminal-native HTTP(S) interception proxy**. It sits between your browser and the internet, letting you inspect, modify, and replay traffic without ever leaving your terminal.
|
Spilltea is a **terminal-native HTTP(S) interception proxy**. It sits between your browser and the internet, letting you inspect, modify, and replay traffic without ever leaving your terminal.
|
||||||
|
|
||||||
It is intentionally minimal. No Electron, no browser, no bloat. Just a fast, keyboard-driven tool that gets out of your way.
|
It is intentionally minimal. No Electron, no browser, no bloat. Just a fast, keyboard-driven tool that gets out of your way.
|
||||||
|
|
||||||
|
<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.**
|
||||||
|
|
||||||
|
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
|
## Features
|
||||||
|
|
||||||
- **Intercept**: Pause requests and responses in-flight. Inspect and modify them (even with your favorite editor) before forwarding.
|
- **Intercept**: Pause requests and responses in-flight. Inspect and modify them (even with your favorite editor) before forwarding.
|
||||||
- **HTTP History**: Every request that passes through the proxy is stored. Browse, search and filter your full session history.
|
- **HTTP History**: Every request that passes through the proxy is stored. Browse, search and filter your full session history.
|
||||||
- **Replay**: Pick any request from the history, modify it if needed, and send it again. Useful for manual testing and quick iteration
|
- **Replay**: Pick any request from the history, modify it if needed, and send it again. Useful for manual testing and quick iteration
|
||||||
- **HTTPS Support** (using go-mitmproxy under the hood)
|
- **HTTPS Support** (using go-mitmproxy under the hood)
|
||||||
|
- **Vim-like Navigation**: The entire interface is keyboard-driven with Vim-inspired shortcuts. Use `h/j/k/l` to move, `gg`/`G` to jump to the top/bottom, `/` to search, `q` to close panels, and more. All keybindings are fully customizable via the config file.
|
||||||
- Built-in Integrations:
|
- Built-in Integrations:
|
||||||
- **FFuf Export**: Generate a ffuf command or configuration directly from a request to start fuzzing instantly.
|
- **FFuf Export**: Generate a ffuf command or configuration directly from a request to start fuzzing instantly.
|
||||||
- **cURL / HTTPie**: Copy any request as a curl or httpie command to your clipboard.
|
- **cURL / HTTPie**: Copy any request as a curl or httpie command to your clipboard.
|
||||||
- **Markdown Export**: Export any request and its response as a clean Markdown snippet, ready to drop into a report.
|
- **Markdown Export**: Export any request and its response as a clean Markdown snippet, ready to drop into a report.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Go install</summary>
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go install github.com/anotherhadi/spilltea/cmd/spilltea@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires Go 1.22+. The binary will be placed in `$GOPATH/bin` (or `~/go/bin`).
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Nix (temporary run, no install)</summary>
|
||||||
|
|
||||||
|
```sh
|
||||||
|
nix run github:anotherhadi/spilltea
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>NixOS (flake)</summary>
|
||||||
|
|
||||||
|
Add spilltea to your flake inputs:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
inputs.spilltea.url = "github:anotherhadi/spilltea";
|
||||||
|
```
|
||||||
|
|
||||||
|
Then add the package to your system or home-manager packages:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
environment.systemPackages = [ inputs.spilltea.packages.${pkgs.system}.default ];
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<!-- exec: cat ./docs/basics.md -->
|
||||||
## Project Management
|
## 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.
|
Spilltea organizes work into **projects**. Each project maps to a SQLite database file that stores all intercepted traffic for that session & a log files.
|
||||||
@@ -41,11 +110,6 @@ On startup, you choose:
|
|||||||
- **Existing project**: pick from a list of previous projects
|
- **Existing project**: pick from a list of previous projects
|
||||||
- **Temporary**: no name needed, stored in `/tmp/spilltea/projects/` and will be deleted on your next reboot!
|
- **Temporary**: no name needed, stored in `/tmp/spilltea/projects/` and will be deleted on your next reboot!
|
||||||
|
|
||||||
## Plugin System
|
|
||||||
|
|
||||||
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](./.github/docs/plugins.md) or [plugin examples](./plugins/).
|
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Spilltea is fully configured via a YAML file at `~/.config/spilltea/config.yaml`.
|
Spilltea is fully configured via a YAML file at `~/.config/spilltea/config.yaml`.
|
||||||
@@ -53,16 +117,25 @@ Check the default configuration with all the options [here](./internal/config/de
|
|||||||
|
|
||||||
## CLI Flags
|
## CLI Flags
|
||||||
|
|
||||||
| Flag | Short | Description |
|
```
|
||||||
| ----------------------- | ----- | ------------------------------------------------------------------------------ |
|
Usage: spilltea [flags]
|
||||||
| `--config` | `-c` | Path to config file (default: `~/.config/spilltea/config.yaml`) |
|
|
||||||
| `--plugin-dir` | | Path to plugins dir, overrides config (default: `~/.config/spilltea/plugins/`) |
|
--add-default-config copy the default config file to the config path and exit
|
||||||
| `--host` | | Proxy host, overrides config |
|
--add-default-plugins copy built-in example plugins into the plugins dir and exit
|
||||||
| `--port` | `-p` | Proxy port, overrides config |
|
-c, --config string path to config file
|
||||||
| `--project` | `-P` | Project name to open directly, or `tmp` for a temporary session |
|
--host string proxy host (overrides config)
|
||||||
| `--upstream-proxy` | | Upstream proxy URL, overrides config (e.g. `http://user:pass@host:8888`) |
|
--plugins-dir string path to plugins dir (overrides config)
|
||||||
| `--version` | `-v` | Print version and exit |
|
-p, --port int proxy port (overrides config)
|
||||||
| `--add-default-plugins` | | Add the default plugins to your plugins dir and exit |
|
-P, --project string project name to open directly, or "tmp" for a temporary session
|
||||||
|
--upstream-proxy string upstream proxy URL, e.g. http://user:pass@host:8888 (overrides config)
|
||||||
|
-v, --version print version
|
||||||
|
```
|
||||||
|
<!-- endexec -->
|
||||||
|
|
||||||
|
## Plugin System
|
||||||
|
|
||||||
|
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/).
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,13 @@ func main() {
|
|||||||
flagVersion = flag.BoolP("version", "v", false, "print version")
|
flagVersion = flag.BoolP("version", "v", false, "print version")
|
||||||
flagProject = flag.StringP("project", "P", "", `project name to open directly, or "tmp" for a temporary session`)
|
flagProject = flag.StringP("project", "P", "", `project name to open directly, or "tmp" for a temporary session`)
|
||||||
flagAddDefaultPlugins = flag.Bool("add-default-plugins", false, "copy built-in example plugins into the plugins dir and exit")
|
flagAddDefaultPlugins = flag.Bool("add-default-plugins", false, "copy built-in example plugins into the plugins dir and exit")
|
||||||
|
flagAddDefaultConfig = flag.Bool("add-default-config", false, "copy the default config file to the config path and exit")
|
||||||
)
|
)
|
||||||
|
flag.CommandLine.SetOutput(os.Stdout)
|
||||||
|
flag.Usage = func() {
|
||||||
|
fmt.Println("Usage: spilltea [flags]\n")
|
||||||
|
flag.PrintDefaults()
|
||||||
|
}
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if *flagVersion {
|
if *flagVersion {
|
||||||
@@ -61,6 +67,19 @@ func main() {
|
|||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if *flagAddDefaultConfig {
|
||||||
|
cfgPath := filepath.Join(os.Getenv("HOME"), ".config", "spilltea", "config.yaml")
|
||||||
|
if *flagConfig != "" {
|
||||||
|
cfgPath = *flagConfig
|
||||||
|
}
|
||||||
|
if err := config.WriteDefaultConfig(cfgPath); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "add-default-config: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Printf("default config written to %s\n", cfgPath)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
if *flagProject != "" && !homeUI.IsValidProjectName(*flagProject) {
|
if *flagProject != "" && !homeUI.IsValidProjectName(*flagProject) {
|
||||||
fmt.Fprintf(os.Stderr, "project: invalid name %q (only lowercase letters, digits, - and _ are allowed)\n", *flagProject)
|
fmt.Fprintf(os.Stderr, "project: invalid name %q (only lowercase letters, digits, - and _ are allowed)\n", *flagProject)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|||||||
@@ -2,5 +2,5 @@ package spilltea
|
|||||||
|
|
||||||
import "embed"
|
import "embed"
|
||||||
|
|
||||||
//go:embed .github/docs
|
//go:embed docs
|
||||||
var DocsFS embed.FS
|
var DocsFS embed.FS
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
## 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.
|
||||||
|
|
||||||
|
On startup, you choose:
|
||||||
|
|
||||||
|
- **New project**: enter a name, stored in `~/.local/share/spilltea/projects/` by default
|
||||||
|
- **Existing project**: pick from a list of previous projects
|
||||||
|
- **Temporary**: no name needed, stored in `/tmp/spilltea/projects/` and will be deleted on your next reboot!
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
## CLI Flags
|
||||||
|
|
||||||
|
<!-- exec: echo '```' && go run ./cmd/spilltea -h && echo '```' -->
|
||||||
|
```
|
||||||
|
Usage: spilltea [flags]
|
||||||
|
|
||||||
|
--add-default-config copy the default config file to the config path and exit
|
||||||
|
--add-default-plugins copy built-in example plugins into the plugins dir and exit
|
||||||
|
-c, --config string path to config file
|
||||||
|
--host string proxy host (overrides config)
|
||||||
|
--plugins-dir string path to plugins dir (overrides config)
|
||||||
|
-p, --port int proxy port (overrides config)
|
||||||
|
-P, --project string project name to open directly, or "tmp" for a temporary session
|
||||||
|
--upstream-proxy string upstream proxy URL, e.g. http://user:pass@host:8888 (overrides config)
|
||||||
|
-v, --version print version
|
||||||
|
```
|
||||||
|
<!-- endexec -->
|
||||||
@@ -12,3 +12,6 @@
|
|||||||
- Select the "Authorities" tab and click on "Import".
|
- Select the "Authorities" tab and click on "Import".
|
||||||
- Select the `mitmproxy-ca-cert.pem` file in `{{.Cfg.App.CertDir}}`.
|
- Select the `mitmproxy-ca-cert.pem` file in `{{.Cfg.App.CertDir}}`.
|
||||||
- When prompted, click the "Trust this CA to identify websites" checkbox, then click on "OK".
|
- When prompted, click the "Trust this CA to identify websites" checkbox, then click on "OK".
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> Never install this certificate in a permanent system trust store: it grants decryption of all HTTPS traffic. Remove it from your browser after use.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
## Legal Disclaimer
|
||||||
|
|
||||||
|
**This tool is provided for educational purposes and authorized security testing only.**
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
(system: f system (import nixpkgs {inherit system;}));
|
(system: f system (import nixpkgs {inherit system;}));
|
||||||
|
|
||||||
pname = "spilltea";
|
pname = "spilltea";
|
||||||
version = "0.0.3";
|
version = "0.0.4";
|
||||||
|
|
||||||
ldflags = ["-s" "-w" "-X main.version=${version}"];
|
ldflags = ["-s" "-w" "-X main.version=${version}"];
|
||||||
in {
|
in {
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
src = ./.;
|
src = ./.;
|
||||||
outputs = ["out"];
|
outputs = ["out"];
|
||||||
|
|
||||||
vendorHash = "sha256-v37RFS/T6KGZTO1tHmtUqBrRcCqNS3+ACBcsd7tl50c=";
|
vendorHash = "sha256-1iPwFsyzdonak9EWMRnudwcCQZfI+Uvre38+puG4s0s=";
|
||||||
|
|
||||||
meta = with pkgs.lib; {
|
meta = with pkgs.lib; {
|
||||||
description = "A minimal, terminal-based HTTP(S) proxy for pentesters and CTF players.";
|
description = "A minimal, terminal-based HTTP(S) proxy for pentesters and CTF players.";
|
||||||
@@ -37,5 +37,20 @@
|
|||||||
"${pname}" = pkg;
|
"${pname}" = pkg;
|
||||||
default = pkg;
|
default = pkg;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
devShells = forAllSystems (system: pkgs: {
|
||||||
|
default = pkgs.mkShell {
|
||||||
|
packages = with pkgs; [
|
||||||
|
go
|
||||||
|
python3
|
||||||
|
lefthook
|
||||||
|
doctoc
|
||||||
|
];
|
||||||
|
|
||||||
|
shellHook = ''
|
||||||
|
lefthook install
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ require (
|
|||||||
charm.land/lipgloss/v2 v2.0.3
|
charm.land/lipgloss/v2 v2.0.3
|
||||||
github.com/charmbracelet/x/ansi v0.11.7
|
github.com/charmbracelet/x/ansi v0.11.7
|
||||||
github.com/lqqyt2423/go-mitmproxy v1.8.11
|
github.com/lqqyt2423/go-mitmproxy v1.8.11
|
||||||
github.com/sirupsen/logrus v1.8.3
|
github.com/sirupsen/logrus v1.9.4
|
||||||
github.com/spf13/pflag v1.0.10
|
github.com/spf13/pflag v1.0.10
|
||||||
github.com/spf13/viper v1.21.0
|
github.com/spf13/viper v1.21.0
|
||||||
github.com/yuin/gopher-lua v1.1.2
|
github.com/yuin/gopher-lua v1.1.2
|
||||||
golang.org/x/net v0.39.0
|
golang.org/x/net v0.54.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
modernc.org/sqlite v1.50.0
|
modernc.org/sqlite v1.50.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -24,8 +24,8 @@ require (
|
|||||||
github.com/atotto/clipboard v0.1.4 // indirect
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
github.com/aymerick/douceur v0.2.0 // indirect
|
github.com/aymerick/douceur v0.2.0 // indirect
|
||||||
github.com/charmbracelet/colorprofile v0.4.3 // indirect
|
github.com/charmbracelet/colorprofile v0.4.3 // indirect
|
||||||
github.com/charmbracelet/ultraviolet v0.0.0-20260416155717-489999b90468 // indirect
|
github.com/charmbracelet/ultraviolet v0.0.0-20260511121909-c840852527f3 // indirect
|
||||||
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
|
github.com/charmbracelet/x/exp/slice v0.0.0-20260517005351-920740d613be // indirect
|
||||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||||
github.com/charmbracelet/x/termios v0.1.1 // indirect
|
github.com/charmbracelet/x/termios v0.1.1 // indirect
|
||||||
github.com/charmbracelet/x/windows v0.2.2 // indirect
|
github.com/charmbracelet/x/windows v0.2.2 // indirect
|
||||||
@@ -33,38 +33,37 @@ require (
|
|||||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||||
github.com/dlclark/regexp2 v1.12.0 // indirect
|
github.com/dlclark/regexp2 v1.12.0 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
github.com/fsnotify/fsnotify v1.10.1 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/gorilla/css v1.0.1 // indirect
|
github.com/gorilla/css v1.0.1 // indirect
|
||||||
github.com/gorilla/websocket v1.5.0 // indirect
|
github.com/gorilla/websocket v1.5.0 // indirect
|
||||||
github.com/klauspost/compress v1.17.8 // indirect
|
github.com/klauspost/compress v1.17.8 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.22 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.23 // indirect
|
github.com/mattn/go-runewidth v0.0.23 // indirect
|
||||||
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
|
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
|
||||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.3.1 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
||||||
github.com/sahilm/fuzzy v0.1.1 // indirect
|
github.com/sahilm/fuzzy v0.1.1 // indirect
|
||||||
github.com/satori/go.uuid v1.2.0 // indirect
|
github.com/satori/go.uuid v1.2.0 // indirect
|
||||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
|
||||||
github.com/spf13/afero v1.15.0 // indirect
|
github.com/spf13/afero v1.15.0 // indirect
|
||||||
github.com/spf13/cast v1.10.0 // indirect
|
github.com/spf13/cast v1.10.0 // indirect
|
||||||
github.com/subosito/gotenv v1.6.0 // indirect
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
github.com/yuin/goldmark v1.7.8 // indirect
|
github.com/yuin/goldmark v1.8.2 // indirect
|
||||||
github.com/yuin/goldmark-emoji v1.0.5 // indirect
|
github.com/yuin/goldmark-emoji v1.0.6 // indirect
|
||||||
go.uber.org/atomic v1.11.0 // indirect
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
golang.org/x/sync v0.20.0 // indirect
|
golang.org/x/sync v0.20.0 // indirect
|
||||||
golang.org/x/sys v0.43.0 // indirect
|
golang.org/x/sys v0.44.0 // indirect
|
||||||
golang.org/x/text v0.28.0 // indirect
|
golang.org/x/text v0.37.0 // indirect
|
||||||
modernc.org/libc v1.72.0 // indirect
|
modernc.org/libc v1.72.3 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -24,14 +24,14 @@ github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuP
|
|||||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||||
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
|
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
|
||||||
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
|
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
|
||||||
github.com/charmbracelet/ultraviolet v0.0.0-20260416155717-489999b90468 h1:Q9fO0y1Zo5KB/5Vu8JZoLGm1N3RzF9bNj3Ao3xoR+Ac=
|
github.com/charmbracelet/ultraviolet v0.0.0-20260511121909-c840852527f3 h1:pxGjlWZFcRQMWAdtjRelpL3Gbu8iYIyuO3Eqbd037Ow=
|
||||||
github.com/charmbracelet/ultraviolet v0.0.0-20260416155717-489999b90468/go.mod h1:bAAz7dh/FTYfC+oiHavL4mX1tOIBZ0ZwYjSi3qE6ivM=
|
github.com/charmbracelet/ultraviolet v0.0.0-20260511121909-c840852527f3/go.mod h1:SnKWaPaTnkTNXJgdgdquu66de12V8pW/b/qlTGaF9xg=
|
||||||
github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI=
|
github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI=
|
||||||
github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ=
|
github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ=
|
||||||
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
|
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
|
||||||
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
|
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
|
||||||
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI=
|
github.com/charmbracelet/x/exp/slice v0.0.0-20260517005351-920740d613be h1:O22D2Od8gEsRGTDPKDTRzx2BGrvVcIAJlwBf+1sTeN0=
|
||||||
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU=
|
github.com/charmbracelet/x/exp/slice v0.0.0-20260517005351-920740d613be/go.mod h1:vqEfX6xzqW1pKKZUUiFOKg0OQ7bCh54Q2vR/tserrRA=
|
||||||
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||||
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||||
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
|
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
|
||||||
@@ -42,7 +42,6 @@ github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSE
|
|||||||
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
||||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
|
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
|
||||||
@@ -51,10 +50,10 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
|
|||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
|
||||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
@@ -83,8 +82,8 @@ github.com/lqqyt2423/go-mitmproxy v1.8.11 h1:Au/qwhXSlKKCkDxPVa6aSfCJeoxoH6I+7zm
|
|||||||
github.com/lqqyt2423/go-mitmproxy v1.8.11/go.mod h1:dSGnI17tVZ8dtYu9vnaIz7kxVwJNFH0CoNQwEQlTpxE=
|
github.com/lqqyt2423/go-mitmproxy v1.8.11/go.mod h1:dSGnI17tVZ8dtYu9vnaIz7kxVwJNFH0CoNQwEQlTpxE=
|
||||||
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
|
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
|
||||||
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||||
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
|
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
|
||||||
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||||
@@ -93,8 +92,8 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU
|
|||||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
@@ -103,16 +102,14 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
|||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
|
||||||
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
|
||||||
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
|
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
|
||||||
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
||||||
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
|
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
|
||||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||||
github.com/sirupsen/logrus v1.8.3 h1:DBBfY8eMYazKEJHb3JKpSPfpgd2mBCoNFlQx6C5fftU=
|
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||||
github.com/sirupsen/logrus v1.8.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
|
||||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
|
||||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||||
@@ -121,19 +118,16 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
|||||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
|
||||||
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||||
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
|
||||||
github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
|
github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=
|
||||||
github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
|
|
||||||
github.com/yuin/gopher-lua v1.1.2 h1:yF/FjE3hD65tBbt0VXLE13HWS9h34fdzJmrWRXwobGA=
|
github.com/yuin/gopher-lua v1.1.2 h1:yF/FjE3hD65tBbt0VXLE13HWS9h34fdzJmrWRXwobGA=
|
||||||
github.com/yuin/gopher-lua v1.1.2/go.mod h1:7aRmXIWl37SqRf0koeyylBEzJ+aPt8A+mmkQ4f1ntR8=
|
github.com/yuin/gopher-lua v1.1.2/go.mod h1:7aRmXIWl37SqRf0koeyylBEzJ+aPt8A+mmkQ4f1ntR8=
|
||||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||||
@@ -142,30 +136,27 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
|||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
||||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
||||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
|
||||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
||||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
||||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
|
||||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U=
|
modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY=
|
||||||
modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8=
|
modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||||
modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU=
|
modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ=
|
||||||
modernc.org/ccgo/v4 v4.32.4/go.mod h1:lY7f+fiTDHfcv6YlRgSkxYfhs+UvOEEzj49jAn2TOx0=
|
modernc.org/ccgo/v4 v4.34.0/go.mod h1:AS5WYMyBakQ+fhsHhtP8mWB82KTGPkNNJDGfGQCe0/A=
|
||||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||||
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
@@ -174,18 +165,18 @@ modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
|
|||||||
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||||
modernc.org/libc v1.72.0 h1:IEu559v9a0XWjw0DPoVKtXpO2qt5NVLAnFaBbjq+n8c=
|
modernc.org/libc v1.72.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU=
|
||||||
modernc.org/libc v1.72.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ=
|
modernc.org/libc v1.72.3/go.mod h1:dn0dZNnnn1clLyvRxLxYExxiKRZIRENOfqQ8XEeg4Qs=
|
||||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
|
||||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
modernc.org/sqlite v1.50.0 h1:eMowQSWLK0MeiQTdmz3lqoF5dqclujdlIKeJA11+7oM=
|
modernc.org/sqlite v1.50.1 h1:l+cQvn0sd0zJJtfygGHuQJ5AjlrwXmWPw4KP3ZMwr9w=
|
||||||
modernc.org/sqlite v1.50.0/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew=
|
modernc.org/sqlite v1.50.1/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
|
||||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ type Config struct {
|
|||||||
|
|
||||||
History struct {
|
History struct {
|
||||||
SkipDuplicates bool `mapstructure:"skip_duplicates"`
|
SkipDuplicates bool `mapstructure:"skip_duplicates"`
|
||||||
|
KeepResponses bool `mapstructure:"keep_responses"`
|
||||||
} `mapstructure:"history"`
|
} `mapstructure:"history"`
|
||||||
|
|
||||||
Keybindings Keybindings `mapstructure:"keybindings"`
|
Keybindings Keybindings `mapstructure:"keybindings"`
|
||||||
@@ -73,6 +74,16 @@ func Load(path string) error {
|
|||||||
return viper.Unmarshal(Global)
|
return viper.Unmarshal(Global)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WriteDefaultConfig(path string) error {
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||||
|
return fmt.Errorf("create config dir: %w", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(path, defaultConfig, 0o644); err != nil {
|
||||||
|
return fmt.Errorf("write config: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func ExpandPath(p string) string {
|
func ExpandPath(p string) string {
|
||||||
if strings.HasPrefix(p, "~/") {
|
if strings.HasPrefix(p, "~/") {
|
||||||
home, err := os.UserHomeDir()
|
home, err := os.UserHomeDir()
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ replay:
|
|||||||
|
|
||||||
history:
|
history:
|
||||||
skip_duplicates: true # if true, skip saving entries with the same method, host, path and body
|
skip_duplicates: true # if true, skip saving entries with the same method, host, path and body
|
||||||
|
keep_responses: false # if true, response body and headers are stored in history
|
||||||
|
|
||||||
tui:
|
tui:
|
||||||
use_nerdfont_icons: false
|
use_nerdfont_icons: false
|
||||||
@@ -98,3 +99,9 @@ keybindings:
|
|||||||
toggle: "space"
|
toggle: "space"
|
||||||
edit_config: "e,enter"
|
edit_config: "e,enter"
|
||||||
filter: "/"
|
filter: "/"
|
||||||
|
|
||||||
|
docs:
|
||||||
|
search: "/"
|
||||||
|
search_reset: "r"
|
||||||
|
search_next: "n"
|
||||||
|
search_prev: "N"
|
||||||
|
|||||||
@@ -66,6 +66,13 @@ type PluginsKeys struct {
|
|||||||
Filter string `mapstructure:"filter"`
|
Filter string `mapstructure:"filter"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DocsKeys struct {
|
||||||
|
Search string `mapstructure:"search"`
|
||||||
|
SearchReset string `mapstructure:"search_reset"`
|
||||||
|
SearchNext string `mapstructure:"search_next"`
|
||||||
|
SearchPrev string `mapstructure:"search_prev"`
|
||||||
|
}
|
||||||
|
|
||||||
type Keybindings struct {
|
type Keybindings struct {
|
||||||
Global GlobalKeys `mapstructure:"global"`
|
Global GlobalKeys `mapstructure:"global"`
|
||||||
Intercept InterceptKeys `mapstructure:"intercept"`
|
Intercept InterceptKeys `mapstructure:"intercept"`
|
||||||
@@ -75,4 +82,5 @@ type Keybindings struct {
|
|||||||
Diff DiffKeys `mapstructure:"diff"`
|
Diff DiffKeys `mapstructure:"diff"`
|
||||||
Findings FindingsKeys `mapstructure:"findings"`
|
Findings FindingsKeys `mapstructure:"findings"`
|
||||||
Plugins PluginsKeys `mapstructure:"plugins"`
|
Plugins PluginsKeys `mapstructure:"plugins"`
|
||||||
|
Docs DocsKeys `mapstructure:"docs"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -177,7 +177,12 @@ func (b *Broker) SaveEntry(f *proxy.Flow) {
|
|||||||
Path: path,
|
Path: path,
|
||||||
StatusCode: status,
|
StatusCode: status,
|
||||||
RequestRaw: FormatRawRequest(f),
|
RequestRaw: FormatRawRequest(f),
|
||||||
ResponseRaw: FormatRawResponse(f),
|
ResponseRaw: func() string {
|
||||||
|
if config.Global.History.KeepResponses {
|
||||||
|
return FormatRawResponse(f)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}(),
|
||||||
}
|
}
|
||||||
if cb := b.onBeforeNewEntry; cb != nil {
|
if cb := b.onBeforeNewEntry; cb != nil {
|
||||||
if !cb(pending) {
|
if !cb(pending) {
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package keys
|
||||||
|
|
||||||
|
import (
|
||||||
|
"charm.land/bubbles/v2/key"
|
||||||
|
"github.com/anotherhadi/spilltea/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DocsKeyMap struct {
|
||||||
|
Search key.Binding
|
||||||
|
SearchReset key.Binding
|
||||||
|
SearchNext key.Binding
|
||||||
|
SearchPrev key.Binding
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDocsKeyMap(cfg config.DocsKeys) DocsKeyMap {
|
||||||
|
return DocsKeyMap{
|
||||||
|
Search: binding(cfg.Search, "search"),
|
||||||
|
SearchReset: binding(cfg.SearchReset, "reset search"),
|
||||||
|
SearchNext: binding(cfg.SearchNext, "next match"),
|
||||||
|
SearchPrev: binding(cfg.SearchPrev, "prev match"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d DocsKeyMap) Bindings() []key.Binding {
|
||||||
|
return []key.Binding{d.Search, d.SearchReset, d.SearchNext, d.SearchPrev}
|
||||||
|
}
|
||||||
@@ -54,3 +54,8 @@ func (g GlobalKeyMap) Bindings() []key.Binding {
|
|||||||
g.ScrollUp, g.ScrollDown,
|
g.ScrollUp, g.ScrollDown,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CommonBindings returns keys available on every page.
|
||||||
|
func (g GlobalKeyMap) CommonBindings() []key.Binding {
|
||||||
|
return []key.Binding{g.Quit, g.Help, g.OpenLogs, g.ToggleSidebar}
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ type KeyMap struct {
|
|||||||
Diff DiffKeyMap
|
Diff DiffKeyMap
|
||||||
Findings FindingsKeyMap
|
Findings FindingsKeyMap
|
||||||
Plugins PluginsKeyMap
|
Plugins PluginsKeyMap
|
||||||
|
Docs DocsKeyMap
|
||||||
}
|
}
|
||||||
|
|
||||||
var Keys *KeyMap
|
var Keys *KeyMap
|
||||||
@@ -31,6 +32,7 @@ func Init(cfg *config.Config) {
|
|||||||
Diff: newDiffKeyMap(kb.Diff),
|
Diff: newDiffKeyMap(kb.Diff),
|
||||||
Findings: newFindingsKeyMap(kb.Findings),
|
Findings: newFindingsKeyMap(kb.Findings),
|
||||||
Plugins: newPluginsKeyMap(kb.Plugins),
|
Plugins: newPluginsKeyMap(kb.Plugins),
|
||||||
|
Docs: newDocsKeyMap(kb.Docs),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ var pageRegistry = []pageEntry{
|
|||||||
m.docs = updated.(docsUI.Model)
|
m.docs = updated.(docsUI.Model)
|
||||||
return cmd
|
return cmd
|
||||||
},
|
},
|
||||||
resize: func(m *Model, w, h int) { m.docs.SetSize(w, h) },
|
isEditing: func(m *Model) bool { return m.docs.IsEditing() },
|
||||||
|
resize: func(m *Model, w, h int) { m.docs.SetSize(w, h) },
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ func (m *Model) renderSidebar() string {
|
|||||||
titleText = "SPLT"
|
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(s.Primary).Padding(0, 1).Render(titleText)
|
||||||
|
|
||||||
divider := strings.Repeat("─", inner)
|
divider := strings.Repeat("─", inner)
|
||||||
|
|
||||||
badgeSelected := lipgloss.NewStyle().Foreground(s.Primary).Bold(true)
|
badgeSelected := lipgloss.NewStyle().Foreground(s.Primary).Bold(true)
|
||||||
@@ -75,14 +76,28 @@ func (m *Model) renderSidebar() string {
|
|||||||
label += string(entry.id)
|
label += string(entry.id)
|
||||||
}
|
}
|
||||||
line := lineStyle.Render(badgeStyle.Render(strconv.Itoa(i+1)) + textStyle.Render(label))
|
line := lineStyle.Render(badgeStyle.Render(strconv.Itoa(i+1)) + textStyle.Render(label))
|
||||||
|
if m.sidebarState == sidebarCollapsed && icon == "" {
|
||||||
|
line = " " + line
|
||||||
|
}
|
||||||
items.WriteString(line + "\n")
|
items.WriteString(line + "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
body := lipgloss.JoinVertical(lipgloss.Left,
|
maxLen := inner - 2
|
||||||
|
name := m.projectName
|
||||||
|
if m.sidebarState == sidebarCollapsed && name == "temporary" {
|
||||||
|
name = "tmp"
|
||||||
|
} else if len(name) > maxLen {
|
||||||
|
name = name[:maxLen-1] + "…"
|
||||||
|
}
|
||||||
|
parts := []string{
|
||||||
title,
|
title,
|
||||||
|
lipgloss.NewStyle().Width(inner).Foreground(s.Subtle).Padding(0, 1).Render(name),
|
||||||
|
}
|
||||||
|
parts = append(parts,
|
||||||
lipgloss.NewStyle().Foreground(s.Subtle).Render(divider),
|
lipgloss.NewStyle().Foreground(s.Subtle).Render(divider),
|
||||||
items.String(),
|
items.String(),
|
||||||
)
|
)
|
||||||
|
body := lipgloss.JoinVertical(lipgloss.Left, parts...)
|
||||||
|
|
||||||
return s.Panel.Width(m.getSidebarWidth()).Height(m.height).Render(body)
|
return s.Panel.Width(m.getSidebarWidth()).Height(m.height).Render(body)
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-16
@@ -176,22 +176,24 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
if !m.activeIsEditing() {
|
if !m.activeIsEditing() {
|
||||||
switch {
|
switch {
|
||||||
case key.Matches(msg, keys.Keys.Global.CopyAs):
|
case key.Matches(msg, keys.Keys.Global.CopyAs):
|
||||||
if m.page == pageDiff {
|
var raw, scheme string
|
||||||
if raw := m.diff.CurrentRaw(); raw != "" {
|
switch m.page {
|
||||||
m.copyAs.SetSize(m.width, m.height)
|
case pageDiff:
|
||||||
m.copyAs.Open(copyasUI.OpenMsg{
|
raw = m.diff.CurrentRaw()
|
||||||
RawRequest: raw,
|
scheme = "https"
|
||||||
Scheme: "https",
|
case pageIntercept:
|
||||||
})
|
raw = m.intercept.CurrentRaw()
|
||||||
}
|
scheme = m.intercept.CurrentScheme()
|
||||||
} else if m.page == pageIntercept {
|
case pageHistory:
|
||||||
if raw := m.intercept.CurrentRaw(); raw != "" {
|
raw = m.history.CurrentRaw()
|
||||||
m.copyAs.SetSize(m.width, m.height)
|
scheme = m.history.CurrentScheme()
|
||||||
m.copyAs.Open(copyasUI.OpenMsg{
|
case pageReplay:
|
||||||
RawRequest: raw,
|
raw = m.replay.CurrentRaw()
|
||||||
Scheme: m.intercept.CurrentScheme(),
|
scheme = m.replay.CurrentScheme()
|
||||||
})
|
}
|
||||||
}
|
if raw != "" {
|
||||||
|
m.copyAs.SetSize(m.width, m.height)
|
||||||
|
m.copyAs.Open(copyasUI.OpenMsg{RawRequest: raw, Scheme: scheme})
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ import (
|
|||||||
"github.com/anotherhadi/spilltea/internal/style"
|
"github.com/anotherhadi/spilltea/internal/style"
|
||||||
)
|
)
|
||||||
|
|
||||||
const popupInnerW = 40
|
const (
|
||||||
|
popupW = 55
|
||||||
|
popupH = 20
|
||||||
|
)
|
||||||
|
|
||||||
func writeClipboard(text string) {
|
func writeClipboard(text string) {
|
||||||
encoded := base64.StdEncoding.EncodeToString([]byte(text))
|
encoded := base64.StdEncoding.EncodeToString([]byte(text))
|
||||||
@@ -66,7 +69,7 @@ func New() Model {
|
|||||||
BorderForeground(s.Primary).
|
BorderForeground(s.Primary).
|
||||||
Foreground(s.MutedFg).PaddingLeft(1)
|
Foreground(s.MutedFg).PaddingLeft(1)
|
||||||
|
|
||||||
l := list.New(allItems, delegate, popupInnerW, 8)
|
l := list.New(allItems, delegate, popupW, 8)
|
||||||
l.SetShowTitle(false)
|
l.SetShowTitle(false)
|
||||||
l.SetShowStatusBar(false)
|
l.SetShowStatusBar(false)
|
||||||
l.SetShowHelp(false)
|
l.SetShowHelp(false)
|
||||||
@@ -89,17 +92,25 @@ func (m *Model) Open(msg OpenMsg) {
|
|||||||
m.open = true
|
m.open = true
|
||||||
m.list.ResetFilter()
|
m.list.ResetFilter()
|
||||||
m.list.Select(0)
|
m.list.Select(0)
|
||||||
m.list.SetSize(popupInnerW, m.listHeight())
|
m.list.SetSize(m.popupInnerWidth(), m.listHeight())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) SetSize(w, h int) {
|
func (m *Model) SetSize(w, h int) {
|
||||||
m.width = w
|
m.width = w
|
||||||
m.height = h
|
m.height = h
|
||||||
m.list.SetSize(popupInnerW, m.listHeight())
|
m.list.SetSize(m.popupInnerWidth(), m.listHeight())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) popupInnerWidth() int {
|
||||||
|
w := popupW
|
||||||
|
if m.width > 0 && m.width-4 < w {
|
||||||
|
w = m.width - 4
|
||||||
|
}
|
||||||
|
return w
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) popupHeight() int {
|
func (m Model) popupHeight() int {
|
||||||
h := 12
|
h := popupH
|
||||||
if m.height > 0 && m.height-4 < h {
|
if m.height > 0 && m.height-4 < h {
|
||||||
h = m.height - 4
|
h = m.height - 4
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ func (m *Model) View(background string) string {
|
|||||||
BorderForeground(s.Primary)
|
BorderForeground(s.Primary)
|
||||||
|
|
||||||
popupH := m.popupHeight()
|
popupH := m.popupHeight()
|
||||||
popup := style.RenderWithTitle(border, "Copy", inner, popupInnerW+2, popupH)
|
popup := style.RenderWithTitle(border, "Copy", inner, m.popupInnerWidth()+2, popupH)
|
||||||
|
|
||||||
return copyasUI.OverlayCenter(background, popup, m.width, m.height)
|
return copyasUI.OverlayCenter(background, popup, m.width, m.height)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ import (
|
|||||||
"github.com/anotherhadi/spilltea/internal/style"
|
"github.com/anotherhadi/spilltea/internal/style"
|
||||||
)
|
)
|
||||||
|
|
||||||
const popupInnerW = 46
|
const (
|
||||||
|
popupW = 61
|
||||||
|
popupH = 20
|
||||||
|
)
|
||||||
|
|
||||||
// writeClipboard uses the OSC 52 terminal escape sequence to set the clipboard.
|
// writeClipboard uses the OSC 52 terminal escape sequence to set the clipboard.
|
||||||
// Supported by most modern terminals (foot, kitty, wezterm, alacritty, xterm…).
|
// Supported by most modern terminals (foot, kitty, wezterm, alacritty, xterm…).
|
||||||
@@ -69,7 +72,7 @@ func New() Model {
|
|||||||
BorderForeground(s.Primary).
|
BorderForeground(s.Primary).
|
||||||
Foreground(s.MutedFg).PaddingLeft(1)
|
Foreground(s.MutedFg).PaddingLeft(1)
|
||||||
|
|
||||||
l := list.New(allFormats, delegate, popupInnerW, 8)
|
l := list.New(allFormats, delegate, popupW, 8)
|
||||||
l.SetShowTitle(false)
|
l.SetShowTitle(false)
|
||||||
l.SetShowStatusBar(false)
|
l.SetShowStatusBar(false)
|
||||||
l.SetShowHelp(false)
|
l.SetShowHelp(false)
|
||||||
@@ -92,17 +95,25 @@ func (m *Model) Open(msg OpenMsg) {
|
|||||||
m.open = true
|
m.open = true
|
||||||
m.list.ResetFilter()
|
m.list.ResetFilter()
|
||||||
m.list.Select(0)
|
m.list.Select(0)
|
||||||
m.list.SetSize(popupInnerW, m.listHeight())
|
m.list.SetSize(m.popupInnerWidth(), m.listHeight())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) SetSize(w, h int) {
|
func (m *Model) SetSize(w, h int) {
|
||||||
m.width = w
|
m.width = w
|
||||||
m.height = h
|
m.height = h
|
||||||
m.list.SetSize(popupInnerW, m.listHeight())
|
m.list.SetSize(m.popupInnerWidth(), m.listHeight())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) popupInnerWidth() int {
|
||||||
|
w := popupW
|
||||||
|
if m.width > 0 && m.width-4 < w {
|
||||||
|
w = m.width - 4
|
||||||
|
}
|
||||||
|
return w
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) popupHeight() int {
|
func (m Model) popupHeight() int {
|
||||||
h := 14
|
h := popupH
|
||||||
if m.height > 0 && m.height-4 < h {
|
if m.height > 0 && m.height-4 < h {
|
||||||
h = m.height - 4
|
h = m.height - 4
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ func (m *Model) View(background string) string {
|
|||||||
BorderForeground(s.Primary)
|
BorderForeground(s.Primary)
|
||||||
|
|
||||||
popupH := m.popupHeight()
|
popupH := m.popupHeight()
|
||||||
popup := style.RenderWithTitle(border, "Copy as", inner, popupInnerW+2, popupH)
|
popup := style.RenderWithTitle(border, "Copy as", inner, m.popupInnerWidth()+2, popupH)
|
||||||
|
|
||||||
return OverlayCenter(background, popup, m.width, m.height)
|
return OverlayCenter(background, popup, m.width, m.height)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -243,14 +243,6 @@ func lcsAlignedDiff(a, b, aHL, bHL []string) (left, right []diffLine) {
|
|||||||
return left, right
|
return left, right
|
||||||
}
|
}
|
||||||
|
|
||||||
func diffBindings() []key.Binding {
|
|
||||||
g := keys.Keys.Global
|
|
||||||
return []key.Binding{
|
|
||||||
g.Up, g.Down, g.ScrollUp, g.ScrollDown,
|
|
||||||
g.CycleFocus, keys.Keys.Diff.Clear,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type diffKeyMap struct{ width int }
|
type diffKeyMap struct{ width int }
|
||||||
|
|
||||||
func (diffKeyMap) ShortHelp() []key.Binding {
|
func (diffKeyMap) ShortHelp() []key.Binding {
|
||||||
@@ -259,6 +251,9 @@ func (diffKeyMap) ShortHelp() []key.Binding {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m diffKeyMap) FullHelp() [][]key.Binding {
|
func (m diffKeyMap) FullHelp() [][]key.Binding {
|
||||||
all := append(diffBindings(), keys.Keys.Global.Bindings()...)
|
g := keys.Keys.Global
|
||||||
|
pageGlobals := []key.Binding{g.Up, g.Down, g.CycleFocus, g.ScrollUp, g.ScrollDown, g.Left, g.Right, g.Copy, g.CopyAs}
|
||||||
|
all := append(keys.Keys.Diff.Bindings(), pageGlobals...)
|
||||||
|
all = append(all, g.CommonBindings()...)
|
||||||
return keys.ChunkByWidth(all, m.width)
|
return keys.ChunkByWidth(all, m.width)
|
||||||
}
|
}
|
||||||
|
|||||||
+255
-3
@@ -1,36 +1,288 @@
|
|||||||
package docs
|
package docs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
spilltea "github.com/anotherhadi/spilltea"
|
spilltea "github.com/anotherhadi/spilltea"
|
||||||
|
|
||||||
|
"charm.land/bubbles/v2/help"
|
||||||
|
"charm.land/bubbles/v2/key"
|
||||||
|
"charm.land/bubbles/v2/textinput"
|
||||||
"charm.land/bubbles/v2/viewport"
|
"charm.land/bubbles/v2/viewport"
|
||||||
tea "charm.land/bubbletea/v2"
|
tea "charm.land/bubbletea/v2"
|
||||||
|
"charm.land/lipgloss/v2"
|
||||||
|
"github.com/anotherhadi/spilltea/internal/keys"
|
||||||
|
"github.com/anotherhadi/spilltea/internal/style"
|
||||||
)
|
)
|
||||||
|
|
||||||
func readDoc(name string) string {
|
func readDoc(name string) string {
|
||||||
b, _ := spilltea.DocsFS.ReadFile(".github/docs/" + name)
|
b, _ := spilltea.DocsFS.ReadFile("docs/" + name)
|
||||||
return string(b)
|
return string(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
var contentMarkdown = strings.Join([]string{
|
var contentMarkdown = strings.Join([]string{
|
||||||
readDoc("main.md"),
|
readDoc("main.md"),
|
||||||
|
readDoc("legal-disclaimer.md"),
|
||||||
|
readDoc("basics.md"),
|
||||||
readDoc("proxy.md"),
|
readDoc("proxy.md"),
|
||||||
readDoc("certificate.md"),
|
readDoc("certificate.md"),
|
||||||
readDoc("history.md"),
|
readDoc("history.md"),
|
||||||
}, "\n")
|
}, "\n")
|
||||||
|
|
||||||
|
type matchEntry struct {
|
||||||
|
line int
|
||||||
|
start int
|
||||||
|
end int
|
||||||
|
}
|
||||||
|
|
||||||
type Model struct {
|
type Model struct {
|
||||||
viewport viewport.Model
|
viewport viewport.Model
|
||||||
|
help help.Model
|
||||||
|
searchInput textinput.Model
|
||||||
|
searching bool
|
||||||
|
|
||||||
|
matches []matchEntry
|
||||||
|
matchIndex int
|
||||||
|
|
||||||
|
renderedLines []string
|
||||||
|
strippedLines []string
|
||||||
|
|
||||||
|
width int
|
||||||
|
height int
|
||||||
}
|
}
|
||||||
|
|
||||||
func New() Model {
|
func New() Model {
|
||||||
|
ti := textinput.New()
|
||||||
|
ti.Prompt = "/"
|
||||||
|
s := ti.Styles()
|
||||||
|
s.Focused.Prompt = lipgloss.NewStyle().Foreground(style.S.Primary)
|
||||||
|
ti.SetStyles(s)
|
||||||
|
|
||||||
return Model{
|
return Model{
|
||||||
viewport: viewport.New(),
|
viewport: viewport.New(),
|
||||||
|
help: style.NewHelp(),
|
||||||
|
searchInput: ti,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e Model) Init() tea.Cmd {
|
func (e Model) Init() tea.Cmd {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m Model) IsEditing() bool { return m.searching }
|
||||||
|
|
||||||
|
func (m *Model) SetSize(w, h int) {
|
||||||
|
m.width = w
|
||||||
|
m.height = h
|
||||||
|
m.help.SetWidth(w - 2)
|
||||||
|
m.searchInput.SetWidth(w - 4)
|
||||||
|
|
||||||
|
statusH := strings.Count(m.renderStatusBar(), "\n") + 1
|
||||||
|
frameW := windowStyle().GetHorizontalFrameSize()
|
||||||
|
frameH := windowStyle().GetVerticalFrameSize()
|
||||||
|
|
||||||
|
m.viewport.SetWidth(w - frameW)
|
||||||
|
m.viewport.SetHeight(h - frameH - statusH)
|
||||||
|
m.renderMarkdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) applySearch() {
|
||||||
|
query := m.searchInput.Value()
|
||||||
|
m.matches = nil
|
||||||
|
m.matchIndex = 0
|
||||||
|
|
||||||
|
if query != "" {
|
||||||
|
re, err := regexp.Compile("(?i)" + regexp.QuoteMeta(query))
|
||||||
|
if err == nil {
|
||||||
|
for lineIdx, stripped := range m.strippedLines {
|
||||||
|
for _, match := range re.FindAllStringIndex(stripped, -1) {
|
||||||
|
m.matches = append(m.matches, matchEntry{
|
||||||
|
line: lineIdx,
|
||||||
|
start: match[0],
|
||||||
|
end: match[1],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.rebuildViewportContent()
|
||||||
|
if len(m.matches) > 0 {
|
||||||
|
m.viewport.SetYOffset(m.matches[0].line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) searchNext() {
|
||||||
|
if len(m.matches) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.matchIndex = (m.matchIndex + 1) % len(m.matches)
|
||||||
|
m.rebuildViewportContent()
|
||||||
|
m.viewport.SetYOffset(m.matches[m.matchIndex].line)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) searchPrev() {
|
||||||
|
if len(m.matches) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.matchIndex = (m.matchIndex - 1 + len(m.matches)) % len(m.matches)
|
||||||
|
m.rebuildViewportContent()
|
||||||
|
m.viewport.SetYOffset(m.matches[m.matchIndex].line)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) rebuildViewportContent() {
|
||||||
|
if len(m.matches) == 0 || m.searchInput.Value() == "" {
|
||||||
|
m.viewport.SetContent(strings.Join(m.renderedLines, "\n"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type lineInfo struct {
|
||||||
|
intervals [][]int
|
||||||
|
currentIdx int
|
||||||
|
}
|
||||||
|
byLine := make(map[int]*lineInfo)
|
||||||
|
for i, match := range m.matches {
|
||||||
|
li := byLine[match.line]
|
||||||
|
if li == nil {
|
||||||
|
li = &lineInfo{currentIdx: -1}
|
||||||
|
byLine[match.line] = li
|
||||||
|
}
|
||||||
|
li.intervals = append(li.intervals, []int{match.start, match.end})
|
||||||
|
if i == m.matchIndex {
|
||||||
|
li.currentIdx = len(li.intervals) - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := make([]string, len(m.renderedLines))
|
||||||
|
for i, ansiLine := range m.renderedLines {
|
||||||
|
if li, ok := byLine[i]; ok {
|
||||||
|
lines[i] = injectHighlightsInLine(ansiLine, li.intervals, li.currentIdx)
|
||||||
|
} else {
|
||||||
|
lines[i] = ansiLine
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.viewport.SetContent(strings.Join(lines, "\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func lipglossAnsiCodes(s lipgloss.Style) (open, close string) {
|
||||||
|
const sentinel = "X"
|
||||||
|
rendered := s.Render(sentinel)
|
||||||
|
idx := strings.Index(rendered, sentinel)
|
||||||
|
if idx < 0 {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
return rendered[:idx], rendered[idx+len(sentinel):]
|
||||||
|
}
|
||||||
|
|
||||||
|
func injectHighlightsInLine(ansiLine string, intervals [][]int, currentIdx int) string {
|
||||||
|
if len(intervals) == 0 {
|
||||||
|
return ansiLine
|
||||||
|
}
|
||||||
|
|
||||||
|
normalOpen, normalClose := lipglossAnsiCodes(lipgloss.NewStyle().Background(style.S.SubtleBg))
|
||||||
|
currentOpen, currentClose := lipglossAnsiCodes(lipgloss.NewStyle().Background(style.S.Primary).Foreground(style.S.Text))
|
||||||
|
|
||||||
|
type injection struct {
|
||||||
|
visPos int
|
||||||
|
code string
|
||||||
|
priority int // 0 = close (emit before opens at same pos), 1 = open
|
||||||
|
}
|
||||||
|
var injections []injection
|
||||||
|
for i, iv := range intervals {
|
||||||
|
open, close := normalOpen, normalClose
|
||||||
|
if i == currentIdx {
|
||||||
|
open, close = currentOpen, currentClose
|
||||||
|
}
|
||||||
|
injections = append(injections, injection{visPos: iv[0], code: open, priority: 1})
|
||||||
|
injections = append(injections, injection{visPos: iv[1], code: close, priority: 0})
|
||||||
|
}
|
||||||
|
sort.SliceStable(injections, func(a, b int) bool {
|
||||||
|
if injections[a].visPos != injections[b].visPos {
|
||||||
|
return injections[a].visPos < injections[b].visPos
|
||||||
|
}
|
||||||
|
return injections[a].priority < injections[b].priority
|
||||||
|
})
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
visPos := 0
|
||||||
|
injIdx := 0
|
||||||
|
i := 0
|
||||||
|
for i < len(ansiLine) {
|
||||||
|
for injIdx < len(injections) && injections[injIdx].visPos == visPos {
|
||||||
|
sb.WriteString(injections[injIdx].code)
|
||||||
|
injIdx++
|
||||||
|
}
|
||||||
|
if ansiLine[i] == '\x1b' {
|
||||||
|
j := i + 1
|
||||||
|
if j < len(ansiLine) {
|
||||||
|
switch ansiLine[j] {
|
||||||
|
case '[':
|
||||||
|
j++
|
||||||
|
for j < len(ansiLine) && (ansiLine[j] < '@' || ansiLine[j] > '~') {
|
||||||
|
j++
|
||||||
|
}
|
||||||
|
if j < len(ansiLine) {
|
||||||
|
j++
|
||||||
|
}
|
||||||
|
case ']':
|
||||||
|
j++
|
||||||
|
for j < len(ansiLine) {
|
||||||
|
if ansiLine[j] == '\a' {
|
||||||
|
j++
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if ansiLine[j] == '\x1b' && j+1 < len(ansiLine) && ansiLine[j+1] == '\\' {
|
||||||
|
j += 2
|
||||||
|
break
|
||||||
|
}
|
||||||
|
j++
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
j++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.WriteString(ansiLine[i:j])
|
||||||
|
i = j
|
||||||
|
} else {
|
||||||
|
_, size := utf8.DecodeRuneInString(ansiLine[i:])
|
||||||
|
if size == 0 {
|
||||||
|
size = 1
|
||||||
|
}
|
||||||
|
sb.WriteString(ansiLine[i : i+size])
|
||||||
|
i += size
|
||||||
|
visPos += size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for injIdx < len(injections) {
|
||||||
|
sb.WriteString(injections[injIdx].code)
|
||||||
|
injIdx++
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) renderStatusBar() string {
|
||||||
|
if m.searching {
|
||||||
|
return lipgloss.NewStyle().Padding(0, 1).Render(m.searchInput.View())
|
||||||
|
}
|
||||||
|
return lipgloss.NewStyle().Padding(0, 1).Render(m.help.View(docsKeyMap{width: m.width}))
|
||||||
|
}
|
||||||
|
|
||||||
|
type docsKeyMap struct{ width int }
|
||||||
|
|
||||||
|
func (docsKeyMap) ShortHelp() []key.Binding {
|
||||||
|
g := keys.Keys.Global
|
||||||
|
d := keys.Keys.Docs
|
||||||
|
return []key.Binding{g.Up, g.Down, d.Search, g.Help}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m docsKeyMap) FullHelp() [][]key.Binding {
|
||||||
|
g := keys.Keys.Global
|
||||||
|
d := keys.Keys.Docs
|
||||||
|
pageGlobals := []key.Binding{g.Up, g.Down, g.ScrollUp, g.ScrollDown}
|
||||||
|
all := append(d.Bindings(), pageGlobals...)
|
||||||
|
all = append(all, g.CommonBindings()...)
|
||||||
|
return keys.ChunkByWidth(all, m.width)
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import (
|
|||||||
|
|
||||||
func (e Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (e Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
g := keys.Keys.Global
|
g := keys.Keys.Global
|
||||||
|
d := keys.Keys.Docs
|
||||||
|
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case tea.MouseWheelMsg:
|
case tea.MouseWheelMsg:
|
||||||
switch msg.Button {
|
switch msg.Button {
|
||||||
@@ -18,7 +20,42 @@ func (e Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case tea.KeyPressMsg:
|
case tea.KeyPressMsg:
|
||||||
|
if e.searching {
|
||||||
|
switch {
|
||||||
|
case key.Matches(msg, d.SearchReset):
|
||||||
|
e.searching = false
|
||||||
|
e.searchInput.Blur()
|
||||||
|
e.searchInput.SetValue("")
|
||||||
|
e.matches = nil
|
||||||
|
e.matchIndex = 0
|
||||||
|
e.SetSize(e.width, e.height)
|
||||||
|
case msg.String() == "enter":
|
||||||
|
e.searching = false
|
||||||
|
e.searchInput.Blur()
|
||||||
|
e.SetSize(e.width, e.height)
|
||||||
|
default:
|
||||||
|
var cmd tea.Cmd
|
||||||
|
e.searchInput, cmd = e.searchInput.Update(msg)
|
||||||
|
e.applySearch()
|
||||||
|
return e, cmd
|
||||||
|
}
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
|
case key.Matches(msg, d.Search):
|
||||||
|
e.searching = true
|
||||||
|
e.searchInput.SetValue("")
|
||||||
|
e.searchInput.Focus()
|
||||||
|
e.SetSize(e.width, e.height)
|
||||||
|
case key.Matches(msg, d.SearchReset):
|
||||||
|
e.matches = nil
|
||||||
|
e.matchIndex = 0
|
||||||
|
e.rebuildViewportContent()
|
||||||
|
case key.Matches(msg, d.SearchNext):
|
||||||
|
e.searchNext()
|
||||||
|
case key.Matches(msg, d.SearchPrev):
|
||||||
|
e.searchPrev()
|
||||||
case key.Matches(msg, g.Up):
|
case key.Matches(msg, g.Up):
|
||||||
e.viewport.SetYOffset(e.viewport.YOffset() - 1)
|
e.viewport.SetYOffset(e.viewport.YOffset() - 1)
|
||||||
case key.Matches(msg, g.Down):
|
case key.Matches(msg, g.Down):
|
||||||
@@ -35,16 +72,10 @@ func (e Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
step = 1
|
step = 1
|
||||||
}
|
}
|
||||||
e.viewport.SetYOffset(e.viewport.YOffset() + step)
|
e.viewport.SetYOffset(e.viewport.YOffset() + step)
|
||||||
|
case key.Matches(msg, g.Help):
|
||||||
|
e.help.ShowAll = !e.help.ShowAll
|
||||||
|
e.SetSize(e.width, e.height)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return e, nil
|
return e, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) SetSize(w, h int) {
|
|
||||||
frameW := windowStyle().GetHorizontalFrameSize()
|
|
||||||
frameH := windowStyle().GetVerticalFrameSize()
|
|
||||||
|
|
||||||
m.viewport.SetWidth(w - frameW)
|
|
||||||
m.viewport.SetHeight(h - frameH)
|
|
||||||
m.renderMarkdown()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ package docs
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
_ "embed"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
tea "charm.land/bubbletea/v2"
|
tea "charm.land/bubbletea/v2"
|
||||||
@@ -10,6 +11,7 @@ import (
|
|||||||
"charm.land/lipgloss/v2"
|
"charm.land/lipgloss/v2"
|
||||||
"github.com/anotherhadi/spilltea/internal/config"
|
"github.com/anotherhadi/spilltea/internal/config"
|
||||||
"github.com/anotherhadi/spilltea/internal/style"
|
"github.com/anotherhadi/spilltea/internal/style"
|
||||||
|
"github.com/charmbracelet/x/ansi"
|
||||||
)
|
)
|
||||||
|
|
||||||
func windowStyle() lipgloss.Style {
|
func windowStyle() lipgloss.Style {
|
||||||
@@ -20,7 +22,23 @@ func windowStyle() lipgloss.Style {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e Model) View() tea.View {
|
func (e Model) View() tea.View {
|
||||||
return tea.NewView(windowStyle().Render(e.viewport.View()))
|
statusBar := e.renderStatusBar()
|
||||||
|
if len(e.matches) > 0 {
|
||||||
|
var countText string
|
||||||
|
if e.searching {
|
||||||
|
countText = fmt.Sprintf("%d matches", len(e.matches))
|
||||||
|
} else {
|
||||||
|
countText = fmt.Sprintf("%d/%d", e.matchIndex+1, len(e.matches))
|
||||||
|
}
|
||||||
|
count := lipgloss.NewStyle().Padding(0, 1).
|
||||||
|
Foreground(style.S.MutedFg).
|
||||||
|
Render(countText)
|
||||||
|
statusBar = lipgloss.JoinHorizontal(lipgloss.Top, statusBar, count)
|
||||||
|
}
|
||||||
|
return tea.NewView(lipgloss.JoinVertical(lipgloss.Left,
|
||||||
|
windowStyle().Render(e.viewport.View()),
|
||||||
|
statusBar,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) renderMarkdown() {
|
func (m *Model) renderMarkdown() {
|
||||||
@@ -48,5 +66,10 @@ func (m *Model) renderMarkdown() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
str, _ := renderer.Render(processed.String())
|
str, _ := renderer.Render(processed.String())
|
||||||
m.viewport.SetContent(str)
|
m.renderedLines = strings.Split(str, "\n")
|
||||||
|
m.strippedLines = make([]string, len(m.renderedLines))
|
||||||
|
for i, l := range m.renderedLines {
|
||||||
|
m.strippedLines[i] = ansi.Strip(l)
|
||||||
|
}
|
||||||
|
m.applySearch()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ func (m *Model) recalcSizes() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) renderStatusBar() string {
|
func (m *Model) renderStatusBar() string {
|
||||||
return lipgloss.NewStyle().Padding(0, 1).Render(m.help.View(findingsKeyMap{}))
|
return lipgloss.NewStyle().Padding(0, 1).Render(m.help.View(findingsKeyMap{width: m.width}))
|
||||||
}
|
}
|
||||||
|
|
||||||
// RefreshCmd loads findings from the database.
|
// RefreshCmd loads findings from the database.
|
||||||
@@ -143,14 +143,18 @@ func renderMarkdown(src string, width int) string {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
type findingsKeyMap struct{}
|
type findingsKeyMap struct{ width int }
|
||||||
|
|
||||||
func (findingsKeyMap) ShortHelp() []key.Binding {
|
func (findingsKeyMap) ShortHelp() []key.Binding {
|
||||||
g := keys.Keys.Global
|
g := keys.Keys.Global
|
||||||
f := keys.Keys.Findings
|
f := keys.Keys.Findings
|
||||||
return []key.Binding{g.Up, g.Down, f.Dismiss}
|
return []key.Binding{g.Up, g.Down, f.Dismiss, g.Help}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (findingsKeyMap) FullHelp() [][]key.Binding {
|
func (m findingsKeyMap) FullHelp() [][]key.Binding {
|
||||||
return [][]key.Binding{findingsKeyMap{}.ShortHelp()}
|
g := keys.Keys.Global
|
||||||
|
pageGlobals := []key.Binding{g.Up, g.Down, g.ScrollUp, g.ScrollDown}
|
||||||
|
all := append(keys.Keys.Findings.Bindings(), pageGlobals...)
|
||||||
|
all = append(all, g.CommonBindings()...)
|
||||||
|
return keys.ChunkByWidth(all, m.width)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,12 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
if m.cursor >= len(m.findings) {
|
if m.cursor >= len(m.findings) {
|
||||||
m.cursor = max(0, len(m.findings)-1)
|
m.cursor = max(0, len(m.findings)-1)
|
||||||
}
|
}
|
||||||
m.pager.SetTotalPages(len(m.findings))
|
if len(m.findings) == 0 {
|
||||||
|
m.pager.Page = 0
|
||||||
|
m.pager.TotalPages = 0
|
||||||
|
} else {
|
||||||
|
m.pager.SetTotalPages(len(m.findings))
|
||||||
|
}
|
||||||
m.refreshListViewport()
|
m.refreshListViewport()
|
||||||
m.refreshBody()
|
m.refreshBody()
|
||||||
return m, nil
|
return m, nil
|
||||||
@@ -76,6 +81,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
step = 1
|
step = 1
|
||||||
}
|
}
|
||||||
m.bodyViewport.SetYOffset(m.bodyViewport.YOffset() + step)
|
m.bodyViewport.SetYOffset(m.bodyViewport.YOffset() + step)
|
||||||
|
case key.Matches(msg, g.Help):
|
||||||
|
m.help.ShowAll = !m.help.ShowAll
|
||||||
|
m.recalcSizes()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|||||||
@@ -28,7 +28,10 @@ func (m Model) View() tea.View {
|
|||||||
|
|
||||||
func (m *Model) renderListPanel(w, h int) string {
|
func (m *Model) renderListPanel(w, h int) string {
|
||||||
s := style.S
|
s := style.S
|
||||||
dots := s.Faint.Render(m.pager.View())
|
var dots string
|
||||||
|
if len(m.findings) > 0 {
|
||||||
|
dots = s.Faint.Render(m.pager.View())
|
||||||
|
}
|
||||||
inner := lipgloss.JoinVertical(lipgloss.Left,
|
inner := lipgloss.JoinVertical(lipgloss.Left,
|
||||||
m.listViewport.View(),
|
m.listViewport.View(),
|
||||||
lipgloss.PlaceHorizontal(m.listViewport.Width(), lipgloss.Center, dots),
|
lipgloss.PlaceHorizontal(m.listViewport.Width(), lipgloss.Center, dots),
|
||||||
|
|||||||
@@ -145,14 +145,16 @@ func (historyKeyMap) ShortHelp() []key.Binding {
|
|||||||
return []key.Binding{
|
return []key.Binding{
|
||||||
g.Up, g.Down, g.CycleFocus,
|
g.Up, g.Down, g.CycleFocus,
|
||||||
h.DeleteEntry, h.DeleteAll,
|
h.DeleteEntry, h.DeleteAll,
|
||||||
h.Filter, h.SqlQuery,
|
|
||||||
g.Help,
|
g.Help,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m historyKeyMap) FullHelp() [][]key.Binding {
|
func (m historyKeyMap) FullHelp() [][]key.Binding {
|
||||||
h := keys.Keys.History
|
h := keys.Keys.History
|
||||||
|
g := keys.Keys.Global
|
||||||
|
pageGlobals := []key.Binding{g.Up, g.Down, g.CycleFocus, g.ScrollUp, g.ScrollDown, g.Left, g.Right, g.Escape, g.SendToReplay, g.SendToDiff, g.Copy, g.CopyAs}
|
||||||
all := []key.Binding{h.DeleteEntry, h.DeleteAll, h.Filter, h.SqlQuery}
|
all := []key.Binding{h.DeleteEntry, h.DeleteAll, h.Filter, h.SqlQuery}
|
||||||
all = append(all, keys.Keys.Global.Bindings()...)
|
all = append(all, pageGlobals...)
|
||||||
|
all = append(all, g.CommonBindings()...)
|
||||||
return keys.ChunkByWidth(all, m.width)
|
return keys.ChunkByWidth(all, m.width)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
"charm.land/bubbles/v2/key"
|
"charm.land/bubbles/v2/key"
|
||||||
tea "charm.land/bubbletea/v2"
|
tea "charm.land/bubbletea/v2"
|
||||||
|
"charm.land/lipgloss/v2"
|
||||||
"github.com/anotherhadi/spilltea/internal/db"
|
"github.com/anotherhadi/spilltea/internal/db"
|
||||||
"github.com/anotherhadi/spilltea/internal/keys"
|
"github.com/anotherhadi/spilltea/internal/keys"
|
||||||
"github.com/anotherhadi/spilltea/internal/style"
|
"github.com/anotherhadi/spilltea/internal/style"
|
||||||
@@ -281,8 +282,13 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
|
|
||||||
func (m *Model) refreshListViewport() {
|
func (m *Model) refreshListViewport() {
|
||||||
if m.pager.PerPage > 0 {
|
if m.pager.PerPage > 0 {
|
||||||
m.pager.Page = m.cursor / m.pager.PerPage
|
if len(m.entries) == 0 {
|
||||||
m.pager.SetTotalPages(len(m.entries))
|
m.pager.Page = 0
|
||||||
|
m.pager.TotalPages = 0
|
||||||
|
} else {
|
||||||
|
m.pager.Page = m.cursor / m.pager.PerPage
|
||||||
|
m.pager.SetTotalPages(len(m.entries))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
m.listViewport.SetContent(m.renderList())
|
m.listViewport.SetContent(m.renderList())
|
||||||
}
|
}
|
||||||
@@ -299,5 +305,10 @@ func (m *Model) refreshBody() {
|
|||||||
} else {
|
} else {
|
||||||
raw = e.RequestRaw
|
raw = e.RequestRaw
|
||||||
}
|
}
|
||||||
|
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(" (˘・_・˘)\nno response stored")))
|
||||||
|
return
|
||||||
|
}
|
||||||
m.bodyViewport.SetContent(style.HighlightHTTP(raw))
|
m.bodyViewport.SetContent(style.HighlightHTTP(raw))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,10 @@ func (m Model) View() tea.View {
|
|||||||
|
|
||||||
func (m *Model) renderListPanel(w, h int) string {
|
func (m *Model) renderListPanel(w, h int) string {
|
||||||
s := style.S
|
s := style.S
|
||||||
dots := s.Faint.Render(m.pager.View())
|
var dots string
|
||||||
|
if len(m.entries) > 0 {
|
||||||
|
dots = s.Faint.Render(m.pager.View())
|
||||||
|
}
|
||||||
inner := lipgloss.JoinVertical(lipgloss.Left,
|
inner := lipgloss.JoinVertical(lipgloss.Left,
|
||||||
m.listViewport.View(),
|
m.listViewport.View(),
|
||||||
lipgloss.PlaceHorizontal(m.listViewport.Width(), lipgloss.Center, dots),
|
lipgloss.PlaceHorizontal(m.listViewport.Width(), lipgloss.Center, dots),
|
||||||
|
|||||||
@@ -295,16 +295,26 @@ func (m *Model) recalcSizes() {
|
|||||||
|
|
||||||
func (m *Model) refreshListViewport() {
|
func (m *Model) refreshListViewport() {
|
||||||
if m.pager.PerPage > 0 {
|
if m.pager.PerPage > 0 {
|
||||||
m.pager.Page = m.cursor / m.pager.PerPage
|
if len(m.queue) == 0 {
|
||||||
m.pager.SetTotalPages(len(m.queue))
|
m.pager.Page = 0
|
||||||
|
m.pager.TotalPages = 0
|
||||||
|
} else {
|
||||||
|
m.pager.Page = m.cursor / m.pager.PerPage
|
||||||
|
m.pager.SetTotalPages(len(m.queue))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
m.listViewport.SetContent(m.renderList())
|
m.listViewport.SetContent(m.renderList())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) refreshResponseListViewport() {
|
func (m *Model) refreshResponseListViewport() {
|
||||||
if m.responsePager.PerPage > 0 {
|
if m.responsePager.PerPage > 0 {
|
||||||
m.responsePager.Page = m.responseCursor / m.responsePager.PerPage
|
if len(m.responseQueue) == 0 {
|
||||||
m.responsePager.SetTotalPages(len(m.responseQueue))
|
m.responsePager.Page = 0
|
||||||
|
m.responsePager.TotalPages = 0
|
||||||
|
} else {
|
||||||
|
m.responsePager.Page = m.responseCursor / m.responsePager.PerPage
|
||||||
|
m.responsePager.SetTotalPages(len(m.responseQueue))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
m.responseViewport.SetContent(m.renderResponseList())
|
m.responseViewport.SetContent(m.renderResponseList())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ func (interceptKeyMap) ShortHelp() []key.Binding {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m interceptKeyMap) FullHelp() [][]key.Binding {
|
func (m interceptKeyMap) FullHelp() [][]key.Binding {
|
||||||
all := append(keys.Keys.Intercept.Bindings(), keys.Keys.Global.Bindings()...)
|
g := keys.Keys.Global
|
||||||
|
pageGlobals := []key.Binding{g.Up, g.Down, g.CycleFocus, g.ScrollUp, g.ScrollDown, g.Left, g.Right, g.Escape, g.SendToReplay, g.SendToDiff, g.Copy, g.CopyAs}
|
||||||
|
all := append(keys.Keys.Intercept.Bindings(), pageGlobals...)
|
||||||
|
all = append(all, g.CommonBindings()...)
|
||||||
return keys.ChunkByWidth(all, m.width)
|
return keys.ChunkByWidth(all, m.width)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,10 @@ func (m *Model) renderListPanel(w, h int) string {
|
|||||||
border = s.PanelFocused
|
border = s.PanelFocused
|
||||||
}
|
}
|
||||||
|
|
||||||
dots := s.Faint.Render(m.pager.View())
|
var dots string
|
||||||
|
if len(m.queue) > 0 {
|
||||||
|
dots = s.Faint.Render(m.pager.View())
|
||||||
|
}
|
||||||
inner := lipgloss.JoinVertical(lipgloss.Left,
|
inner := lipgloss.JoinVertical(lipgloss.Left,
|
||||||
m.listViewport.View(),
|
m.listViewport.View(),
|
||||||
lipgloss.PlaceHorizontal(m.listViewport.Width(), lipgloss.Center, dots),
|
lipgloss.PlaceHorizontal(m.listViewport.Width(), lipgloss.Center, dots),
|
||||||
|
|||||||
@@ -187,8 +187,13 @@ func (m *Model) syncDetailViewport() {
|
|||||||
|
|
||||||
func (m *Model) refreshListViewport() {
|
func (m *Model) refreshListViewport() {
|
||||||
if m.pager.PerPage > 0 {
|
if m.pager.PerPage > 0 {
|
||||||
m.pager.Page = m.cursor / m.pager.PerPage
|
if len(m.filtered) == 0 {
|
||||||
m.pager.SetTotalPages(len(m.filtered))
|
m.pager.Page = 0
|
||||||
|
m.pager.TotalPages = 0
|
||||||
|
} else {
|
||||||
|
m.pager.Page = m.cursor / m.pager.PerPage
|
||||||
|
m.pager.SetTotalPages(len(m.filtered))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
m.listViewport.SetContent(m.renderList())
|
m.listViewport.SetContent(m.renderList())
|
||||||
}
|
}
|
||||||
@@ -196,6 +201,7 @@ func (m *Model) refreshListViewport() {
|
|||||||
type pluginsKeyMap struct {
|
type pluginsKeyMap struct {
|
||||||
editing bool
|
editing bool
|
||||||
hasConfig bool
|
hasConfig bool
|
||||||
|
width int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k pluginsKeyMap) ShortHelp() []key.Binding {
|
func (k pluginsKeyMap) ShortHelp() []key.Binding {
|
||||||
@@ -210,11 +216,20 @@ func (k pluginsKeyMap) ShortHelp() []key.Binding {
|
|||||||
key.WithHelp(g.ScrollUp.Help().Key+"/"+g.ScrollDown.Help().Key, "scroll detail"),
|
key.WithHelp(g.ScrollUp.Help().Key+"/"+g.ScrollDown.Help().Key, "scroll detail"),
|
||||||
)
|
)
|
||||||
if k.hasConfig {
|
if k.hasConfig {
|
||||||
return []key.Binding{pk.Toggle, pk.EditConfig, pk.Filter, scrollHint}
|
return []key.Binding{pk.Toggle, pk.EditConfig, scrollHint, g.Help}
|
||||||
}
|
}
|
||||||
return []key.Binding{pk.Toggle, pk.Filter, scrollHint}
|
return []key.Binding{pk.Toggle, scrollHint, g.Help}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k pluginsKeyMap) FullHelp() [][]key.Binding {
|
func (k pluginsKeyMap) FullHelp() [][]key.Binding {
|
||||||
return [][]key.Binding{k.ShortHelp()}
|
g := keys.Keys.Global
|
||||||
|
if k.editing {
|
||||||
|
return [][]key.Binding{k.ShortHelp()}
|
||||||
|
}
|
||||||
|
pk := keys.Keys.Plugins
|
||||||
|
pageGlobals := []key.Binding{g.Up, g.Down, g.ScrollUp, g.ScrollDown, g.Escape}
|
||||||
|
all := []key.Binding{pk.Toggle, pk.EditConfig, pk.Filter}
|
||||||
|
all = append(all, pageGlobals...)
|
||||||
|
all = append(all, g.CommonBindings()...)
|
||||||
|
return keys.ChunkByWidth(all, k.width)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
|
|
||||||
func (m Model) View() tea.View {
|
func (m Model) View() tea.View {
|
||||||
if m.width == 0 || m.manager == nil {
|
if m.width == 0 || m.manager == nil {
|
||||||
return tea.NewView(style.S.Faint.Render("\nno plugins loaded"))
|
return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, style.S.Faint.Render(" (._.)~*.'\n no plugins loaded")))
|
||||||
}
|
}
|
||||||
|
|
||||||
listH, detailH := style.SplitH(m.height, m.renderStatusBar(), 0.4)
|
listH, detailH := style.SplitH(m.height, m.renderStatusBar(), 0.4)
|
||||||
@@ -34,7 +34,10 @@ func (m *Model) renderListPanel(w, h int) string {
|
|||||||
if m.editing {
|
if m.editing {
|
||||||
panelStyle = s.Panel
|
panelStyle = s.Panel
|
||||||
}
|
}
|
||||||
dots := s.Faint.Render(m.pager.View())
|
var dots string
|
||||||
|
if len(m.filtered) > 0 {
|
||||||
|
dots = s.Faint.Render(m.pager.View())
|
||||||
|
}
|
||||||
inner := lipgloss.JoinVertical(lipgloss.Left,
|
inner := lipgloss.JoinVertical(lipgloss.Left,
|
||||||
m.listViewport.View(),
|
m.listViewport.View(),
|
||||||
lipgloss.PlaceHorizontal(m.listViewport.Width(), lipgloss.Center, dots),
|
lipgloss.PlaceHorizontal(m.listViewport.Width(), lipgloss.Center, dots),
|
||||||
@@ -120,9 +123,9 @@ func (m *Model) renderStatusBar() string {
|
|||||||
escKey := keys.Keys.Global.Escape.Help().Key
|
escKey := keys.Keys.Global.Escape.Help().Key
|
||||||
accent := lipgloss.NewStyle().Foreground(s.Primary)
|
accent := lipgloss.NewStyle().Foreground(s.Primary)
|
||||||
filterLine := pad.Render(accent.Render(filterKey) + " " + s.Bold.Render(m.filter) + s.Faint.Render(" "+escKey+" to clear"))
|
filterLine := pad.Render(accent.Render(filterKey) + " " + s.Bold.Render(m.filter) + s.Faint.Render(" "+escKey+" to clear"))
|
||||||
return lipgloss.JoinVertical(lipgloss.Left, filterLine, pad.Render(m.help.View(pluginsKeyMap{editing: m.editing, hasConfig: m.hasConfig()})))
|
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()}))
|
return pad.Render(m.help.View(pluginsKeyMap{editing: m.editing, hasConfig: m.hasConfig(), width: m.width}))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) renderList() string {
|
func (m *Model) renderList() string {
|
||||||
|
|||||||
@@ -187,6 +187,9 @@ func (replayKeyMap) ShortHelp() []key.Binding {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m replayKeyMap) FullHelp() [][]key.Binding {
|
func (m replayKeyMap) FullHelp() [][]key.Binding {
|
||||||
all := append(keys.Keys.Replay.Bindings(), keys.Keys.Global.Bindings()...)
|
g := keys.Keys.Global
|
||||||
|
pageGlobals := []key.Binding{g.Up, g.Down, g.ScrollUp, g.ScrollDown, g.Left, g.Right, g.Escape, g.Copy, g.CopyAs}
|
||||||
|
all := append(keys.Keys.Replay.Bindings(), pageGlobals...)
|
||||||
|
all = append(all, g.CommonBindings()...)
|
||||||
return keys.ChunkByWidth(all, m.width)
|
return keys.ChunkByWidth(all, m.width)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
|
|
||||||
"charm.land/bubbles/v2/key"
|
"charm.land/bubbles/v2/key"
|
||||||
tea "charm.land/bubbletea/v2"
|
tea "charm.land/bubbletea/v2"
|
||||||
|
"charm.land/lipgloss/v2"
|
||||||
"github.com/anotherhadi/spilltea/internal/db"
|
"github.com/anotherhadi/spilltea/internal/db"
|
||||||
"github.com/anotherhadi/spilltea/internal/keys"
|
"github.com/anotherhadi/spilltea/internal/keys"
|
||||||
"github.com/anotherhadi/spilltea/internal/style"
|
"github.com/anotherhadi/spilltea/internal/style"
|
||||||
@@ -241,8 +242,13 @@ func (m Model) updateEditMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
|
|||||||
|
|
||||||
func (m *Model) refreshListViewport() {
|
func (m *Model) refreshListViewport() {
|
||||||
if m.pager.PerPage > 0 {
|
if m.pager.PerPage > 0 {
|
||||||
m.pager.Page = m.cursor / m.pager.PerPage
|
if len(m.entries) == 0 {
|
||||||
m.pager.SetTotalPages(len(m.entries))
|
m.pager.Page = 0
|
||||||
|
m.pager.TotalPages = 0
|
||||||
|
} else {
|
||||||
|
m.pager.Page = m.cursor / m.pager.PerPage
|
||||||
|
m.pager.SetTotalPages(len(m.entries))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
m.listViewport.SetContent(m.renderList())
|
m.listViewport.SetContent(m.renderList())
|
||||||
}
|
}
|
||||||
@@ -259,11 +265,11 @@ func (m *Model) refreshBody() {
|
|||||||
m.requestViewport.SetXOffset(0)
|
m.requestViewport.SetXOffset(0)
|
||||||
|
|
||||||
if e.Sending {
|
if e.Sending {
|
||||||
m.responseViewport.SetContent(style.HighlightHTTP("Sending..."))
|
m.responseViewport.SetContent(lipgloss.Place(m.responseViewport.Width(), m.responseViewport.Height(), lipgloss.Center, lipgloss.Center, style.S.Faint.Render(" (ノ◕ヮ◕)ノ*:・゚\n sending...")))
|
||||||
} else if e.ResponseRaw != "" {
|
} else if e.ResponseRaw != "" {
|
||||||
m.responseViewport.SetContent(style.HighlightHTTP(e.ResponseRaw))
|
m.responseViewport.SetContent(style.HighlightHTTP(e.ResponseRaw))
|
||||||
} else {
|
} else {
|
||||||
m.responseViewport.SetContent("")
|
m.responseViewport.SetContent(lipgloss.Place(m.responseViewport.Width(), m.responseViewport.Height(), lipgloss.Center, lipgloss.Center, style.S.Faint.Render(" ( •_•)>⌐■\npress send to fire")))
|
||||||
}
|
}
|
||||||
m.responseViewport.SetYOffset(0)
|
m.responseViewport.SetYOffset(0)
|
||||||
m.responseViewport.SetXOffset(0)
|
m.responseViewport.SetXOffset(0)
|
||||||
|
|||||||
@@ -37,7 +37,10 @@ func (m *Model) renderListPanel(w, h int) string {
|
|||||||
if m.editing {
|
if m.editing {
|
||||||
panelStyle = s.Panel
|
panelStyle = s.Panel
|
||||||
}
|
}
|
||||||
dots := s.Faint.Render(m.pager.View())
|
var dots string
|
||||||
|
if len(m.entries) > 0 {
|
||||||
|
dots = s.Faint.Render(m.pager.View())
|
||||||
|
}
|
||||||
inner := lipgloss.JoinVertical(lipgloss.Left,
|
inner := lipgloss.JoinVertical(lipgloss.Left,
|
||||||
m.listViewport.View(),
|
m.listViewport.View(),
|
||||||
lipgloss.PlaceHorizontal(m.listViewport.Width(), lipgloss.Center, dots),
|
lipgloss.PlaceHorizontal(m.listViewport.Width(), lipgloss.Center, dots),
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
pre-commit:
|
||||||
|
piped: true
|
||||||
|
commands:
|
||||||
|
check-vendor-hash:
|
||||||
|
glob: "{go.mod,go.sum}"
|
||||||
|
run: .github/scripts/check-vendor-hash.sh
|
||||||
|
stage_fixed: true
|
||||||
|
inject-exec-basics:
|
||||||
|
glob: "{docs/basics.md,cmd/**}"
|
||||||
|
run: python3 .github/scripts/inject-exec.py docs/basics.md
|
||||||
|
stage_fixed: true
|
||||||
|
inject-exec:
|
||||||
|
glob: "{README.md,docs/basics.md,cmd/**}"
|
||||||
|
run: python3 .github/scripts/inject-exec.py README.md
|
||||||
|
stage_fixed: true
|
||||||
|
toc:
|
||||||
|
glob: "{README.md,docs/basics.md,cmd/**}"
|
||||||
|
run: doctoc --notitle README.md
|
||||||
|
stage_fixed: true
|
||||||
+44
-18
@@ -4,8 +4,12 @@ Plugin = {
|
|||||||
Auto-forward requests and exclude them from history based on patterns.
|
Auto-forward requests and exclude them from history based on patterns.
|
||||||
|
|
||||||
**Config**:
|
**Config**:
|
||||||
- `pattern` - whitelist: only intercept matching requests
|
- `pattern` - whitelist: only intercept matching requests/responses and history entries
|
||||||
- `!pattern` - blacklist: don't intercept matching requests and exclude from history
|
- `!pattern` - blacklist: skip matching requests/responses and history entries
|
||||||
|
- `r:pattern` - whitelist for requests/responses only (history unaffected)
|
||||||
|
- `r:!pattern` - blacklist for requests/responses only
|
||||||
|
- `h:pattern` - whitelist for history entries only (requests unaffected)
|
||||||
|
- `h:!pattern` - blacklist for history entries only
|
||||||
- lines starting with `#` are comments
|
- lines starting with `#` are comments
|
||||||
|
|
||||||
Example (ignore static assets):
|
Example (ignore static assets):
|
||||||
@@ -26,6 +30,11 @@ mytarget%.com/
|
|||||||
!%.css$
|
!%.css$
|
||||||
!%.js$
|
!%.js$
|
||||||
!%.png$
|
!%.png$
|
||||||
|
```
|
||||||
|
|
||||||
|
Example (disable history — h: whitelist never matches any real URL):
|
||||||
|
```
|
||||||
|
h:^$
|
||||||
```
|
```
|
||||||
]],
|
]],
|
||||||
priority = 100,
|
priority = 100,
|
||||||
@@ -34,31 +43,48 @@ mytarget%.com/
|
|||||||
on_history_entry = { sync = true },
|
on_history_entry = { sync = true },
|
||||||
}
|
}
|
||||||
|
|
||||||
local whitelist = {}
|
local blacklist = {}
|
||||||
local blacklist = {}
|
local whitelist = {}
|
||||||
|
local blacklist_req = {}
|
||||||
|
local whitelist_req = {}
|
||||||
|
local blacklist_hist = {}
|
||||||
|
local whitelist_hist = {}
|
||||||
|
|
||||||
function on_config(config_text)
|
function on_config(config_text)
|
||||||
whitelist = {}
|
blacklist, whitelist = {}, {}
|
||||||
blacklist = {}
|
blacklist_req, whitelist_req = {}, {}
|
||||||
|
blacklist_hist, whitelist_hist = {}, {}
|
||||||
for line in config_text:gmatch("[^\n]+") do
|
for line in config_text:gmatch("[^\n]+") do
|
||||||
local trimmed = line:match("^%s*(.-)%s*$")
|
local trimmed = line:match("^%s*(.-)%s*$")
|
||||||
if trimmed ~= "" and trimmed:sub(1, 1) ~= "#" then
|
if trimmed ~= "" and trimmed:sub(1, 1) ~= "#" then
|
||||||
if trimmed:sub(1, 1) == "!" then
|
local scope = trimmed:match("^([rh]):")
|
||||||
table.insert(blacklist, trimmed:sub(2))
|
local rest = scope and trimmed:sub(3) or trimmed
|
||||||
|
local is_bl = rest:sub(1, 1) == "!"
|
||||||
|
local pattern = is_bl and rest:sub(2) or rest
|
||||||
|
if scope == "r" then
|
||||||
|
table.insert(is_bl and blacklist_req or whitelist_req, pattern)
|
||||||
|
elseif scope == "h" then
|
||||||
|
table.insert(is_bl and blacklist_hist or whitelist_hist, pattern)
|
||||||
else
|
else
|
||||||
table.insert(whitelist, trimmed)
|
table.insert(is_bl and blacklist or whitelist, pattern)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function should_skip(url)
|
local function check_skip(url, bl_extra, wl_extra)
|
||||||
for _, pattern in ipairs(blacklist) do
|
for _, p in ipairs(blacklist) do
|
||||||
if url:match(pattern) then return true end
|
if url:match(p) then return true end
|
||||||
end
|
end
|
||||||
if #whitelist > 0 then
|
for _, p in ipairs(bl_extra) do
|
||||||
for _, pattern in ipairs(whitelist) do
|
if url:match(p) then return true end
|
||||||
if url:match(pattern) then return false 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
|
||||||
|
if #wl > 0 then
|
||||||
|
for _, p in ipairs(wl) do
|
||||||
|
if url:match(p) then return false end
|
||||||
end
|
end
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
@@ -66,13 +92,13 @@ local function should_skip(url)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function on_request(req)
|
function on_request(req)
|
||||||
if should_skip(req.url) then return "forward" end
|
if check_skip(req.url, blacklist_req, whitelist_req) then return "forward" end
|
||||||
end
|
end
|
||||||
|
|
||||||
function on_response(req)
|
function on_response(req)
|
||||||
if should_skip(req.url) then return "forward" end
|
if check_skip(req.url, blacklist_req, whitelist_req) then return "forward" end
|
||||||
end
|
end
|
||||||
|
|
||||||
function on_history_entry(entry)
|
function on_history_entry(entry)
|
||||||
if should_skip(entry.host .. entry.path) then return "skip" end
|
if check_skip(entry.host .. entry.path, blacklist_hist, whitelist_hist) then return "skip" end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user