mirror of
https://github.com/anotherhadi/spilltea.git
synced 2026-07-06 20:42:33 +02:00
Compare commits
32 Commits
v0.0.5
..
6128dcc15e
| Author | SHA1 | Date | |
|---|---|---|---|
| 6128dcc15e | |||
| 41d5366952 | |||
| 8683d17242 | |||
| 2e61ffa125 | |||
| 9ac6040629 | |||
| c7523e69e7 | |||
| aa458c142f | |||
| 0b395e018a | |||
| 5841ed0a95 | |||
| 9cabe81771 | |||
| 021090f52c | |||
| 0b9e1a1cf0 | |||
| f60cdf2033 | |||
| 6af1388652 | |||
| a708830309 | |||
| 44b3c67a37 | |||
| 6e673f5c11 | |||
| 2c63cdbeff | |||
| 0e982c6ade | |||
| 04ba32cbd5 | |||
| f7e9da94cc | |||
| 9253d85c81 | |||
| 1bb547870e | |||
| 4251e4fb2a | |||
| b547a79d6e | |||
| fe58468abf | |||
| 3542098905 | |||
| f78b3f7174 | |||
| 722021ba02 | |||
| e18f660e83 | |||
| 67fe8eb911 | |||
| af872afbe8 |
@@ -1,27 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
CURRENT_HASH=$(grep -oP '(?<=vendorHash = ")[^"]+' flake.nix)
|
|
||||||
|
|
||||||
go mod vendor
|
|
||||||
|
|
||||||
COMPUTED_HASH=$(nix hash path vendor/)
|
|
||||||
|
|
||||||
rm -rf vendor/
|
|
||||||
|
|
||||||
if [ "$CURRENT_HASH" = "$COMPUTED_HASH" ]; then
|
|
||||||
echo "vendorHash is up to date"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Updating vendorHash in flake.nix..."
|
|
||||||
python3 -c "
|
|
||||||
import sys
|
|
||||||
with open('flake.nix', 'r') as f:
|
|
||||||
content = f.read()
|
|
||||||
content = content.replace('$CURRENT_HASH', '$COMPUTED_HASH')
|
|
||||||
with open('flake.nix', 'w') as f:
|
|
||||||
f.write(content)
|
|
||||||
"
|
|
||||||
echo " Old: $CURRENT_HASH"
|
|
||||||
echo " New: $COMPUTED_HASH"
|
|
||||||
@@ -23,6 +23,6 @@ jobs:
|
|||||||
- uses: goreleaser/goreleaser-action@v6
|
- uses: goreleaser/goreleaser-action@v6
|
||||||
with:
|
with:
|
||||||
version: "~> v2"
|
version: "~> v2"
|
||||||
args: release --clean
|
args: release --clean --config .github/.goreleaser.yaml
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
.claude/
|
.claude/
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
result/
|
result/
|
||||||
|
.pre-commit-config.yaml
|
||||||
|
|||||||
@@ -9,10 +9,10 @@
|
|||||||
> A minimal, terminal-based HTTP(S) proxy for pentesters and CTF players.
|
> A minimal, terminal-based HTTP(S) proxy for pentesters and CTF players.
|
||||||
> Think Burp Suite or Caido, but entirely in your terminal.
|
> Think Burp Suite or Caido, but entirely in your terminal.
|
||||||
|
|
||||||
[](go.mod)
|

|
||||||
[](https://github.com/anotherhadi/spilltea/releases)
|

|
||||||
[](LICENSE)
|

|
||||||
[](https://goreportcard.com/report/github.com/anotherhadi/spilltea)
|
[](https://ko-fi.com/anotherhadi)
|
||||||
|
|
||||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
- [Configuration](#configuration)
|
- [Configuration](#configuration)
|
||||||
- [CLI Flags](#cli-flags)
|
- [CLI Flags](#cli-flags)
|
||||||
- [Plugin System](#plugin-system)
|
- [Plugin System](#plugin-system)
|
||||||
|
- [Vim / Neovim Integration](#vim--neovim-integration)
|
||||||
- [Deployment](#deployment)
|
- [Deployment](#deployment)
|
||||||
- [Tech Stack](#tech-stack)
|
- [Tech Stack](#tech-stack)
|
||||||
|
|
||||||
@@ -39,6 +40,7 @@ It is intentionally minimal. No Electron, no browser, no bloat. Just a fast, key
|
|||||||
<img alt="demo" src="./.github/assets/demo.gif" width="700" />
|
<img alt="demo" src="./.github/assets/demo.gif" width="700" />
|
||||||
|
|
||||||
<!-- exec: cat ./docs/legal-disclaimer.md -->
|
<!-- exec: cat ./docs/legal-disclaimer.md -->
|
||||||
|
|
||||||
## Legal Disclaimer
|
## Legal Disclaimer
|
||||||
|
|
||||||
**This tool is provided for educational purposes and authorized security testing only.**
|
**This tool is provided for educational purposes and authorized security testing only.**
|
||||||
@@ -46,6 +48,7 @@ It is intentionally minimal. No Electron, no browser, no bloat. Just a fast, key
|
|||||||
Use Spilltea only on systems and networks you own or have explicit written permission to test. Intercepting network traffic without authorization may violate local laws (such as the Computer Fraud and Abuse Act, GDPR, or equivalent legislation in your jurisdiction).
|
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.
|
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 -->
|
<!-- endexec -->
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
@@ -100,6 +103,7 @@ environment.systemPackages = [ inputs.spilltea.packages.${pkgs.system}.default ]
|
|||||||
</details>
|
</details>
|
||||||
|
|
||||||
<!-- exec: cat ./docs/basics.md -->
|
<!-- 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.
|
||||||
@@ -115,6 +119,8 @@ On startup, you choose:
|
|||||||
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`.
|
||||||
Check the default configuration with all the options [here](./internal/config/default_config.yaml)
|
Check the default configuration with all the options [here](./internal/config/default_config.yaml)
|
||||||
|
|
||||||
|
Colors and styles can be customized using [ilovetui](https://github.com/anotherhadi/ilovetui), which applies theme changes across all compatible TUI applications at once.
|
||||||
|
|
||||||
## CLI Flags
|
## CLI Flags
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -130,6 +136,7 @@ Usage: spilltea [flags]
|
|||||||
--upstream-proxy string upstream proxy URL, e.g. http://user:pass@host:8888 (overrides config)
|
--upstream-proxy string upstream proxy URL, e.g. http://user:pass@host:8888 (overrides config)
|
||||||
-v, --version print version
|
-v, --version print version
|
||||||
```
|
```
|
||||||
|
|
||||||
<!-- endexec -->
|
<!-- endexec -->
|
||||||
|
|
||||||
## Plugin System
|
## Plugin System
|
||||||
@@ -137,6 +144,15 @@ Usage: spilltea [flags]
|
|||||||
Spilltea supports plugins written in **Lua**. Plugins are loaded from `~/.config/spilltea/plugins/` by default and do not require recompilation or access to the source code.
|
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/).
|
For a full reference and examples, see the [plugin documentation](./docs/plugins.md) or [plugin examples](./plugins/).
|
||||||
|
|
||||||
|
## Vim / Neovim Integration
|
||||||
|
|
||||||
|
Spilltea pairs naturally with Neovim. If you live in your editor, these plugins let you inspect and transform captured traffic without switching context:
|
||||||
|
|
||||||
|
- **[sttr.nvim](https://github.com/anotherhadi/sttr.nvim)**: Run string transformations (base64, URL encode/decode, hex, hashing, and more) on any selected text directly from Neovim. Handy for decoding values spotted in the proxy history.
|
||||||
|
- **[jwt-tui.nvim](https://github.com/anotherhadi/jwt-tui.nvim)**: Decode and inspect JWT tokens in a floating TUI window. Great for quickly reading tokens intercepted through the proxy.
|
||||||
|
|
||||||
|
Both plugins are built to feel native in a terminal workflow, just like Spilltea.
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
spilltea runs **locally** on the machine used for pentesting or CTF. There is no separate server component.
|
spilltea runs **locally** on the machine used for pentesting or CTF. There is no separate server component.
|
||||||
|
|||||||
+19
-4
@@ -44,7 +44,8 @@ func main() {
|
|||||||
)
|
)
|
||||||
flag.CommandLine.SetOutput(os.Stdout)
|
flag.CommandLine.SetOutput(os.Stdout)
|
||||||
flag.Usage = func() {
|
flag.Usage = func() {
|
||||||
fmt.Println("Usage: spilltea [flags]\n")
|
fmt.Println("Usage: spilltea [flags]")
|
||||||
|
fmt.Println("")
|
||||||
flag.PrintDefaults()
|
flag.PrintDefaults()
|
||||||
}
|
}
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
@@ -121,7 +122,7 @@ func main() {
|
|||||||
config.Global.App.UpstreamProxy = *flagUpstreamProxy
|
config.Global.App.UpstreamProxy = *flagUpstreamProxy
|
||||||
}
|
}
|
||||||
|
|
||||||
style.Init(config.Global)
|
style.Init()
|
||||||
icons.Init(config.Global)
|
icons.Init(config.Global)
|
||||||
keys.Init(config.Global)
|
keys.Init(config.Global)
|
||||||
|
|
||||||
@@ -136,17 +137,31 @@ func main() {
|
|||||||
}
|
}
|
||||||
broker := intercept.NewBroker()
|
broker := intercept.NewBroker()
|
||||||
m := appUI.New(broker, project.Name, project.Path)
|
m := appUI.New(broker, project.Name, project.Path)
|
||||||
if _, err := tea.NewProgram(m).Run(); err != nil {
|
final, err := tea.NewProgram(m).Run()
|
||||||
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "tui: %v\n", err)
|
fmt.Fprintf(os.Stderr, "tui: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
if app, ok := final.(appUI.Model); ok {
|
||||||
|
if ferr := app.FatalErr(); ferr != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "error: %v\n", ferr)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run home + app in a single program to avoid a blank flash on transition.
|
// Run home + app in a single program to avoid a blank flash on transition.
|
||||||
root := rootModel{home: homeUI.New(projectDir)}
|
root := rootModel{home: homeUI.New(projectDir)}
|
||||||
if _, err := tea.NewProgram(root).Run(); err != nil {
|
final, err := tea.NewProgram(root).Run()
|
||||||
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "tui: %v\n", err)
|
fmt.Fprintf(os.Stderr, "tui: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
if r, ok := final.(rootModel); ok {
|
||||||
|
if ferr := r.FatalErr(); ferr != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "error: %v\n", ferr)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,3 +58,12 @@ func (m rootModel) View() tea.View {
|
|||||||
}
|
}
|
||||||
return m.home.View()
|
return m.home.View()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m rootModel) FatalErr() error {
|
||||||
|
if m.state == rootStateApp {
|
||||||
|
if app, ok := m.app.(appUI.Model); ok {
|
||||||
|
return app.FatalErr()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
package spilltea
|
|
||||||
|
|
||||||
import "embed"
|
|
||||||
|
|
||||||
//go:embed docs
|
|
||||||
var DocsFS embed.FS
|
|
||||||
@@ -13,6 +13,8 @@ On startup, you choose:
|
|||||||
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`.
|
||||||
Check the default configuration with all the options [here](./internal/config/default_config.yaml)
|
Check the default configuration with all the options [here](./internal/config/default_config.yaml)
|
||||||
|
|
||||||
|
Colors and styles can be customized using [ilovetui](https://github.com/anotherhadi/ilovetui), which applies theme changes across all compatible TUI applications at once.
|
||||||
|
|
||||||
## CLI Flags
|
## CLI Flags
|
||||||
|
|
||||||
<!-- exec: echo '```' && go run ./cmd/spilltea -h && echo '```' -->
|
<!-- exec: echo '```' && go run ./cmd/spilltea -h && echo '```' -->
|
||||||
|
|||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
## How do I convert a temporary session into a regular project?
|
||||||
|
|
||||||
|
Temporary sessions are stored under `/tmp/spilltea/<random-id>/` and will be lost on reboot. To keep the data, move the directory into the Spilltea projects folder and give it a name.
|
||||||
|
|
||||||
|
**1. Find the temporary session directory**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ls /tmp/spilltea/
|
||||||
|
```
|
||||||
|
|
||||||
|
You will see one or more directories with a random hex name (e.g. `a1b2c3d4`).
|
||||||
|
|
||||||
|
**2. Move it to the projects folder**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mv /tmp/spilltea/<random-id> ~/.local/share/spilltea/<project-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
The project name must only contain lowercase letters, digits, `-`, and `_`.
|
||||||
|
|
||||||
|
**3. Open the project**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
spilltea -P my-project
|
||||||
|
```
|
||||||
|
|
||||||
|
Or simply launch Spilltea and pick `my-project` from the project list.
|
||||||
+77
-51
@@ -1,5 +1,7 @@
|
|||||||
# Plugins
|
# Plugins
|
||||||
|
|
||||||
|
> **Warning:** Plugins can execute arbitrary shell commands, read and write files via `shell_pipe`, and access all intercepted traffic. Only load plugins you trust and have reviewed. You are solely responsible for the plugins you run.
|
||||||
|
|
||||||
Spilltea supports Lua plugins that can intercept, modify, and analyze HTTP traffic.
|
Spilltea supports Lua plugins that can intercept, modify, and analyze HTTP traffic.
|
||||||
You can found some pre-built plugins [here](../../plugins/).
|
You can found some pre-built plugins [here](../../plugins/).
|
||||||
|
|
||||||
@@ -31,14 +33,14 @@ Plugin = {
|
|||||||
|
|
||||||
### Hook reference
|
### Hook reference
|
||||||
|
|
||||||
| Hook | When called | Sync/async | Return value |
|
| Hook | When called | Sync/async | Return value |
|
||||||
| ------------------------- | ------------------------------------- | ------------ | ----------------------------------------------- |
|
| ------------------------- | ------------------------------------- | ------------ | ----------------------------------------------------------------------------------------- |
|
||||||
| `on_config(config_text)` | At startup and on config save | always sync | ignored |
|
| `on_config()` | At startup and on config save | always sync | ignored |
|
||||||
| `on_start()` | Once at startup, after `on_config` | configurable | `false` to self-disable the plugin, otherwise ignored |
|
| `on_start()` | Once at startup, after `on_config` | configurable | `false` to self-disable the plugin, otherwise ignored |
|
||||||
| `on_quit()` | When the app exits | always sync | ignored |
|
| `on_quit()` | When the app exits | always sync | ignored |
|
||||||
| `on_request(req)` | Every request, before auto-forward | configurable | `"drop"`, `"forward"`, or `nil` (sync only) |
|
| `on_request(req)` | Every request, before auto-forward | configurable | `"drop"`, `"forward"`, or `nil` (nil does nothing and le the user/TUI choose) (sync only) |
|
||||||
| `on_response(req, res)` | Every response | configurable | `"drop"`, `"forward"`, or `nil` (sync only) |
|
| `on_response(req, res)` | Every response | configurable | `"drop"`, `"forward"`, or `nil` (nil does nothing and le the user/TUI choose) (sync only) |
|
||||||
| `on_history_entry(entry)` | Sync: before DB insert / Async: after | configurable | `"skip"` (don't save), `"keep"` or `nil` (save) -- sync only |
|
| `on_history_entry(entry)` | Sync: before DB insert / Async: after | configurable | `"skip"` (don't save), `"keep"` or `nil` (save) (sync only) |
|
||||||
|
|
||||||
## Request and response objects
|
## Request and response objects
|
||||||
|
|
||||||
@@ -120,17 +122,77 @@ if err then
|
|||||||
else
|
else
|
||||||
log("output: " .. out)
|
log("output: " .. out)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Send an HTTP request directly (bypasses the proxy pipeline and other plugins).
|
||||||
|
-- method: HTTP method ("GET", "POST", etc.)
|
||||||
|
-- url: full URL to request
|
||||||
|
-- headers: table of request headers (optional, pass {} if unused)
|
||||||
|
-- body: request body string (optional, pass "" if unused)
|
||||||
|
-- options: optional table of options:
|
||||||
|
-- insecure = true skip TLS certificate verification
|
||||||
|
-- Returns: response table, error string (nil on success).
|
||||||
|
local res, err = send_request("POST", "https://example.com/api", {
|
||||||
|
["Authorization"] = "Bearer " .. token,
|
||||||
|
["Content-Type"] = "application/json",
|
||||||
|
}, '{"key":"value"}', { insecure = true })
|
||||||
|
if err then
|
||||||
|
log("request failed: " .. err)
|
||||||
|
else
|
||||||
|
log(tostring(res.status_code))
|
||||||
|
log(res.body)
|
||||||
|
log(res.headers["Content-Type"] or "")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Return the plugin's config section as a Lua table (parsed from YAML).
|
||||||
|
-- Returns an empty table if no config is set.
|
||||||
|
local cfg = get_config()
|
||||||
```
|
```
|
||||||
|
|
||||||
### Finding deduplication
|
### Finding deduplication
|
||||||
|
|
||||||
A finding is identified by `(plugin_name, key)`. If a finding with that pair already exists in the database it will **not** be re-created, even across restarts. If the user **dismisses** a finding it is permanently hidden and will never reappear, even if the plugin generates it again.
|
A finding is identified by `(plugin_name, key)`. If a finding with that pair already exists in the database it will **not** be re-created, even across restarts.
|
||||||
|
If the user **dismisses** a finding it is permanently hidden and will never reappear, even if the plugin generates it again.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Each plugin gets a **config textarea** on the Plugins page. The raw text is passed as-is to `on_config(config_text)`. Parse it however you like (line by line, key=value, JSON, etc.).
|
Plugin configuration is stored in a `plugins.yaml` file alongside the project database.
|
||||||
|
Each plugin is keyed by its filename (without the `.lua` extension) and has an `enable` toggle and an optional `config` block (arbitrary YAML).
|
||||||
|
|
||||||
`on_config` is called once at startup (before `on_start`) and again every time the user saves the config in the UI.
|
```yaml
|
||||||
|
plugins:
|
||||||
|
my_plugin:
|
||||||
|
enable: true
|
||||||
|
config:
|
||||||
|
some_key: some_value
|
||||||
|
list:
|
||||||
|
- item1
|
||||||
|
- item2
|
||||||
|
other_plugin:
|
||||||
|
enable: false
|
||||||
|
```
|
||||||
|
|
||||||
|
The config block is edited from the **Plugins** page in the TUI.
|
||||||
|
Inside a plugin, call `get_config()` to retrieve the config as a Lua table.
|
||||||
|
|
||||||
|
Global defaults for any plugin can be set in `~/.config/spilltea/config.yaml` under a `plugins` key with the same structure as `plugins.yaml`.
|
||||||
|
These defaults are applied the first time a plugin is loaded in a project; once the plugin has an entry in the project's `plugins.yaml`, the project config takes full precedence and the global defaults are ignored.
|
||||||
|
|
||||||
|
`on_config()` is called once at startup (before `on_start`) and again every time the user saves the config in the TUI.
|
||||||
|
It is the right place to read `get_config()` and populate local variables.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local items = {}
|
||||||
|
|
||||||
|
function on_config()
|
||||||
|
items = {}
|
||||||
|
local cfg = get_config()
|
||||||
|
if cfg and cfg.list then
|
||||||
|
for _, v in ipairs(cfg.list) do
|
||||||
|
table.insert(items, v)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
## Sync vs async
|
## Sync vs async
|
||||||
|
|
||||||
@@ -139,49 +201,13 @@ Each plugin gets a **config textarea** on the Plugins page. The raw text is pass
|
|||||||
|
|
||||||
`on_config` and `on_quit` are always synchronous regardless of the Plugin table declaration.
|
`on_config` and `on_quit` are always synchronous regardless of the Plugin table declaration.
|
||||||
|
|
||||||
### Return values for sync hooks
|
Sync `on_history_entry` runs **before** the DB insert, so it can prevent an entry from ever appearing in history.
|
||||||
|
Async `on_history_entry` runs **after** the insert and cannot affect it.
|
||||||
**`on_start`:**
|
|
||||||
|
|
||||||
| Return value | Effect |
|
|
||||||
| ------------ | -------------------------------------------------------------------------------------------- |
|
|
||||||
| `false` | The plugin is disabled immediately and the state is persisted (equivalent to toggling it off). |
|
|
||||||
| anything else | Ignored. |
|
|
||||||
|
|
||||||
This is useful for prerequisite checks (binary not found, config invalid, etc.) so the plugin does not silently run in a broken state:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
function on_start()
|
|
||||||
local h = io.popen("command -v mytool 2>/dev/null")
|
|
||||||
local result = h and h:read("*a") or ""
|
|
||||||
if h then h:close() end
|
|
||||||
if result:match("^%s*$") then
|
|
||||||
notif("MyPlugin", "mytool not found, plugin disabled", "error")
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
**`on_request` and `on_response`:**
|
|
||||||
|
|
||||||
| Return value | Effect |
|
|
||||||
| ------------ | --------------------------------------------------------------------------------- |
|
|
||||||
| `"drop"` | The flow is dropped immediately and never shown in the intercept panel. |
|
|
||||||
| `"forward"` | The flow is forwarded immediately without going through the intercept panel. |
|
|
||||||
| `nil` | Normal behaviour: the flow appears in the intercept panel for the user to decide. |
|
|
||||||
|
|
||||||
**`on_history_entry` (sync only):**
|
|
||||||
|
|
||||||
| Return value | Effect |
|
|
||||||
| ----------------- | --------------------------------- |
|
|
||||||
| `"skip"` | The entry is not saved to the DB. |
|
|
||||||
| `"keep"` or `nil` | The entry is saved normally. |
|
|
||||||
|
|
||||||
Sync `on_history_entry` runs **before** the DB insert, so it can prevent an entry from ever appearing in history. Async `on_history_entry` runs **after** the insert and cannot affect it.
|
|
||||||
|
|
||||||
## Priority
|
## Priority
|
||||||
|
|
||||||
Plugins with a higher `priority` value run before plugins with a lower value (default `0`). This matters for sync hooks that return a decision: the first plugin to return a non-nil value short-circuits the remaining plugins.
|
Plugins with a higher `priority` value run before plugins with a lower value (default `0`).
|
||||||
|
This matters for sync hooks that return a decision: the first plugin to return a non-nil value short-circuits the remaining plugins.
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
Plugin = {
|
Plugin = {
|
||||||
|
|||||||
Generated
+115
@@ -1,5 +1,103 @@
|
|||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
|
"flake-compat": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1767039857,
|
||||||
|
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "flake-compat",
|
||||||
|
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "flake-compat",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731533236,
|
||||||
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"git-hooks": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-compat": "flake-compat",
|
||||||
|
"gitignore": "gitignore",
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1778507602,
|
||||||
|
"narHash": "sha256-kTwur1wV+01SdqskVMSo6JMEpg71ps3HpbFY2GsflKs=",
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "git-hooks.nix",
|
||||||
|
"rev": "61ab0e80d9c7ab14c256b5b453d8b3fb0189ba0a",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "git-hooks.nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gitignore": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"git-hooks",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1709087332,
|
||||||
|
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "gitignore.nix",
|
||||||
|
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "gitignore.nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gomod2nix": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1770585520,
|
||||||
|
"narHash": "sha256-yBz9Ozd5Wb56i3e3cHZ8WcbzCQ9RlVaiW18qDYA/AzA=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "gomod2nix",
|
||||||
|
"rev": "1201ddd1279c35497754f016ef33d5e060f3da8d",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "gomod2nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1777954456,
|
"lastModified": 1777954456,
|
||||||
@@ -18,8 +116,25 @@
|
|||||||
},
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
|
"git-hooks": "git-hooks",
|
||||||
|
"gomod2nix": "gomod2nix",
|
||||||
"nixpkgs": "nixpkgs"
|
"nixpkgs": "nixpkgs"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": "root",
|
"root": "root",
|
||||||
|
|||||||
@@ -1,55 +1,41 @@
|
|||||||
{
|
{
|
||||||
description = "Spilltea: A minimal, terminal-based HTTP(S) proxy for pentesters and CTF players.";
|
description = "Spilltea: A minimal, terminal-based HTTP(S) proxy for pentesters and CTF players.";
|
||||||
|
|
||||||
inputs = {nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";};
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
gomod2nix = {
|
||||||
|
url = "github:nix-community/gomod2nix";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
git-hooks = {
|
||||||
|
url = "github:cachix/git-hooks.nix";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
outputs = {
|
outputs = {
|
||||||
self,
|
self,
|
||||||
nixpkgs,
|
nixpkgs,
|
||||||
|
gomod2nix,
|
||||||
|
git-hooks,
|
||||||
}: let
|
}: let
|
||||||
supportedSystems = ["x86_64-linux" "aarch64-linux"];
|
supportedSystems = ["x86_64-linux" "aarch64-linux"];
|
||||||
|
|
||||||
forAllSystems = f:
|
forAllSystems = f:
|
||||||
nixpkgs.lib.genAttrs supportedSystems
|
nixpkgs.lib.genAttrs supportedSystems
|
||||||
(system: f system (import nixpkgs {inherit system;}));
|
(system: f system (import nixpkgs {inherit system;}));
|
||||||
|
|
||||||
pname = "spilltea";
|
|
||||||
version = "0.0.5";
|
|
||||||
|
|
||||||
ldflags = ["-s" "-w" "-X main.version=${version}"];
|
|
||||||
in {
|
in {
|
||||||
packages = forAllSystems (system: pkgs: let
|
packages = forAllSystems (system: pkgs:
|
||||||
pkg = pkgs.buildGoModule {
|
import ./nix/package.nix {
|
||||||
inherit pname version ldflags;
|
inherit pkgs;
|
||||||
|
buildGoApplication = gomod2nix.legacyPackages.${system}.buildGoApplication;
|
||||||
src = ./.;
|
});
|
||||||
outputs = ["out"];
|
|
||||||
|
|
||||||
vendorHash = "sha256-1iPwFsyzdonak9EWMRnudwcCQZfI+Uvre38+puG4s0s=";
|
|
||||||
|
|
||||||
meta = with pkgs.lib; {
|
|
||||||
description = "A minimal, terminal-based HTTP(S) proxy for pentesters and CTF players.";
|
|
||||||
homepage = "https://github.com/anotherhadi/spilltea";
|
|
||||||
platforms = platforms.unix;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
in {
|
|
||||||
"${pname}" = pkg;
|
|
||||||
default = pkg;
|
|
||||||
});
|
|
||||||
|
|
||||||
devShells = forAllSystems (system: pkgs: {
|
devShells = forAllSystems (system: pkgs: {
|
||||||
default = pkgs.mkShell {
|
default = import ./nix/shell.nix {
|
||||||
packages = with pkgs; [
|
inherit pkgs;
|
||||||
go
|
gitHooksLib = git-hooks.lib.${system};
|
||||||
python3
|
gomod2nixPkgs = gomod2nix.legacyPackages.${system};
|
||||||
lefthook
|
|
||||||
doctoc
|
|
||||||
];
|
|
||||||
|
|
||||||
shellHook = ''
|
|
||||||
lefthook install
|
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ require (
|
|||||||
charm.land/bubbletea/v2 v2.0.6
|
charm.land/bubbletea/v2 v2.0.6
|
||||||
charm.land/glamour/v2 v2.0.0
|
charm.land/glamour/v2 v2.0.0
|
||||||
charm.land/lipgloss/v2 v2.0.3
|
charm.land/lipgloss/v2 v2.0.3
|
||||||
|
github.com/andybalholm/brotli v1.0.4
|
||||||
|
github.com/anotherhadi/ilovetui v0.1.6
|
||||||
github.com/charmbracelet/x/ansi v0.11.7
|
github.com/charmbracelet/x/ansi v0.11.7
|
||||||
|
github.com/klauspost/compress v1.17.8
|
||||||
github.com/lqqyt2423/go-mitmproxy v1.8.11
|
github.com/lqqyt2423/go-mitmproxy v1.8.11
|
||||||
github.com/sirupsen/logrus v1.9.4
|
github.com/sirupsen/logrus v1.9.4
|
||||||
github.com/spf13/pflag v1.0.10
|
github.com/spf13/pflag v1.0.10
|
||||||
@@ -20,7 +23,6 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/alecthomas/chroma/v2 v2.24.1 // indirect
|
github.com/alecthomas/chroma/v2 v2.24.1 // indirect
|
||||||
github.com/andybalholm/brotli v1.0.4 // indirect
|
|
||||||
github.com/atotto/clipboard v0.1.4 // indirect
|
github.com/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
|
||||||
@@ -39,7 +41,6 @@ require (
|
|||||||
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/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.22 // 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
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs
|
|||||||
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||||
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
||||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||||
|
github.com/anotherhadi/ilovetui v0.1.6 h1:NKg+T1DpV08Q4r+iowFrXF+0bTd6Y2f4OFpFwhsfsyY=
|
||||||
|
github.com/anotherhadi/ilovetui v0.1.6/go.mod h1:HVai6u5NGKSMOpmioYpwrN0lSxQjc7HtISUc5hTwvOw=
|
||||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||||
github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
|
github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
type Colors struct {
|
|
||||||
Base00 string `mapstructure:"base00"`
|
|
||||||
Base01 string `mapstructure:"base01"`
|
|
||||||
Base02 string `mapstructure:"base02"`
|
|
||||||
Base03 string `mapstructure:"base03"`
|
|
||||||
Base04 string `mapstructure:"base04"`
|
|
||||||
Base05 string `mapstructure:"base05"`
|
|
||||||
Base06 string `mapstructure:"base06"`
|
|
||||||
Base07 string `mapstructure:"base07"`
|
|
||||||
Base08 string `mapstructure:"base08"`
|
|
||||||
Base09 string `mapstructure:"base09"`
|
|
||||||
Base0A string `mapstructure:"base0a"`
|
|
||||||
Base0B string `mapstructure:"base0b"`
|
|
||||||
Base0C string `mapstructure:"base0c"`
|
|
||||||
Base0D string `mapstructure:"base0d"`
|
|
||||||
Base0E string `mapstructure:"base0e"`
|
|
||||||
Base0F string `mapstructure:"base0f"`
|
|
||||||
}
|
|
||||||
@@ -31,7 +31,6 @@ type Config struct {
|
|||||||
} `mapstructure:"app"`
|
} `mapstructure:"app"`
|
||||||
|
|
||||||
TUI struct {
|
TUI struct {
|
||||||
Colors Colors `mapstructure:"colors"`
|
|
||||||
UseNerdfontIcons bool `mapstructure:"use_nerdfont_icons"`
|
UseNerdfontIcons bool `mapstructure:"use_nerdfont_icons"`
|
||||||
DefaultSidebarState string `mapstructure:"default_sidebar_state"`
|
DefaultSidebarState string `mapstructure:"default_sidebar_state"`
|
||||||
PrettyPrintBody bool `mapstructure:"pretty_print_body"`
|
PrettyPrintBody bool `mapstructure:"pretty_print_body"`
|
||||||
@@ -41,6 +40,7 @@ type Config struct {
|
|||||||
DefaultInterceptEnabled bool `mapstructure:"default_intercept_enabled"`
|
DefaultInterceptEnabled bool `mapstructure:"default_intercept_enabled"`
|
||||||
DefaultCaptureResponse bool `mapstructure:"default_capture_response"`
|
DefaultCaptureResponse bool `mapstructure:"default_capture_response"`
|
||||||
AutoForwardRegex []string `mapstructure:"auto_forward_regex"`
|
AutoForwardRegex []string `mapstructure:"auto_forward_regex"`
|
||||||
|
QueueSize int `mapstructure:"queue_size"`
|
||||||
} `mapstructure:"intercept"`
|
} `mapstructure:"intercept"`
|
||||||
|
|
||||||
Replay struct {
|
Replay struct {
|
||||||
@@ -53,6 +53,13 @@ type Config struct {
|
|||||||
} `mapstructure:"history"`
|
} `mapstructure:"history"`
|
||||||
|
|
||||||
Keybindings Keybindings `mapstructure:"keybindings"`
|
Keybindings Keybindings `mapstructure:"keybindings"`
|
||||||
|
|
||||||
|
Plugins map[string]GlobalPlugin `mapstructure:"plugins"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GlobalPlugin struct {
|
||||||
|
Enable *bool `mapstructure:"enable"`
|
||||||
|
Config interface{} `mapstructure:"config"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var Global *Config
|
var Global *Config
|
||||||
@@ -82,7 +89,7 @@ func WriteDefaultConfig(path string) error {
|
|||||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||||
return fmt.Errorf("create config dir: %w", err)
|
return fmt.Errorf("create config dir: %w", err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(path, defaultConfig, 0o644); err != nil {
|
if err := os.WriteFile(path, defaultConfig, 0o600); err != nil {
|
||||||
return fmt.Errorf("write config: %w", err)
|
return fmt.Errorf("write config: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -5,13 +5,14 @@ app:
|
|||||||
project_dir: ~/.local/share/spilltea
|
project_dir: ~/.local/share/spilltea
|
||||||
plugins_dir: ~/.config/spilltea/plugins
|
plugins_dir: ~/.config/spilltea/plugins
|
||||||
upstream_proxy: "" # e.g. http://corporate-proxy:8888 or http://user:pass@host:8888
|
upstream_proxy: "" # e.g. http://corporate-proxy:8888 or http://user:pass@host:8888
|
||||||
proxy_auth: "" # require basic auth to use the proxy, format: user:pass (empty = disabled)
|
proxy_auth: "" # require basic auth to use the proxy, format: user:pass (empty = disabled). Also run: chmod 600 ~/.config/spilltea/config.yaml
|
||||||
max_body_size_mb: 50 # max response body size read into memory for large streamed responses (MB)
|
max_body_size_mb: 50 # max response body size read into memory for large streamed responses (MB)
|
||||||
external_editor: "" # override $EDITOR for external editing (e.g. nvim, code --wait)
|
external_editor: "" # override $EDITOR for external editing (e.g. nvim, code --wait)
|
||||||
|
|
||||||
intercept:
|
intercept:
|
||||||
default_intercept_enabled: true
|
default_intercept_enabled: true
|
||||||
default_capture_response: false
|
default_capture_response: false
|
||||||
|
queue_size: 64 # max pending intercepted requests/responses before the proxy blocks
|
||||||
auto_forward_regex:
|
auto_forward_regex:
|
||||||
- '\.(js|css|png|gif|ico|woff2?|ttf|svg)(\?.*)?$'
|
- '\.(js|css|png|gif|ico|woff2?|ttf|svg)(\?.*)?$'
|
||||||
|
|
||||||
@@ -26,23 +27,6 @@ tui:
|
|||||||
use_nerdfont_icons: false
|
use_nerdfont_icons: false
|
||||||
default_sidebar_state: "expanded" # hidden, collapsed, expanded
|
default_sidebar_state: "expanded" # hidden, collapsed, expanded
|
||||||
pretty_print_body: true # auto-indent JSON and HTML response bodies
|
pretty_print_body: true # auto-indent JSON and HTML response bodies
|
||||||
colors:
|
|
||||||
base00: "110F12" # Default Background
|
|
||||||
base01: "1C1920" # Lighter Background (status bars, line numbers)
|
|
||||||
base02: "1D1A26" # Selection Background
|
|
||||||
base03: "514D63" # Comments, Invisibles, faint text
|
|
||||||
base04: "8E8AA0" # Dark Foreground (status bars)
|
|
||||||
base05: "C2BED6" # Default Foreground, Caret, Delimiters
|
|
||||||
base06: "D8D5EA" # Light Foreground (rarely used)
|
|
||||||
base07: "EAE7F7" # Light Background (rarely used)
|
|
||||||
base08: "E07080" # Red: errors, diff deleted
|
|
||||||
base09: "D49070" # Orange: integers, constants, warnings
|
|
||||||
base0a: "C4B060" # Yellow: classes, search highlight
|
|
||||||
base0b: "80B880" # Green: strings, diff inserted, success
|
|
||||||
base0c: "70B8C0" # Cyan: support, regex, escape chars
|
|
||||||
base0d: "9E97F8" # Blue/Accent: functions, headings, primary
|
|
||||||
base0e: "C090E8" # Purple: keywords, storage
|
|
||||||
base0f: "D080A0" # Pink: deprecated, embedded language tags
|
|
||||||
|
|
||||||
keybindings:
|
keybindings:
|
||||||
global:
|
global:
|
||||||
|
|||||||
+14
-6
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
type DB struct {
|
type DB struct {
|
||||||
conn *sql.DB
|
conn *sql.DB
|
||||||
|
roConn *sql.DB
|
||||||
path string
|
path string
|
||||||
dedupMu sync.Mutex
|
dedupMu sync.Mutex
|
||||||
}
|
}
|
||||||
@@ -27,6 +28,17 @@ func Open(path string) (*DB, error) {
|
|||||||
conn.Close()
|
conn.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
roConn, err := sql.Open("sqlite", path)
|
||||||
|
if err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := roConn.Exec("PRAGMA query_only=ON"); err != nil {
|
||||||
|
conn.Close()
|
||||||
|
roConn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
d.roConn = roConn
|
||||||
return d, nil
|
return d, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,12 +72,7 @@ CREATE TABLE IF NOT EXISTS replay_entries (
|
|||||||
status_code INTEGER NOT NULL,
|
status_code INTEGER NOT NULL,
|
||||||
error_msg TEXT NOT NULL
|
error_msg TEXT NOT NULL
|
||||||
);
|
);
|
||||||
CREATE TABLE IF NOT EXISTS plugins (
|
CREATE TABLE IF NOT EXISTS findings (
|
||||||
name TEXT PRIMARY KEY,
|
|
||||||
enabled INTEGER NOT NULL DEFAULT 1,
|
|
||||||
config_text TEXT NOT NULL DEFAULT ''
|
|
||||||
);
|
|
||||||
CREATE TABLE IF NOT EXISTS findings (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
plugin_name TEXT NOT NULL,
|
plugin_name TEXT NOT NULL,
|
||||||
dedup_key TEXT NOT NULL,
|
dedup_key TEXT NOT NULL,
|
||||||
@@ -94,6 +101,7 @@ func (d *DB) Close() error {
|
|||||||
if d == nil {
|
if d == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
_ = d.roConn.Close()
|
||||||
return d.conn.Close()
|
return d.conn.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+24
-12
@@ -4,6 +4,7 @@ import (
|
|||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -63,7 +64,11 @@ func (d *DB) InsertEntry(e Entry, body string) (Entry, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return e, err
|
return e, err
|
||||||
}
|
}
|
||||||
e.ID, _ = res.LastInsertId()
|
var idErr error
|
||||||
|
e.ID, idErr = res.LastInsertId()
|
||||||
|
if idErr != nil {
|
||||||
|
log.Printf("db: LastInsertId: %v", idErr)
|
||||||
|
}
|
||||||
return e, nil
|
return e, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,19 +118,11 @@ func (d *DB) SearchEntries(term string) ([]Entry, error) {
|
|||||||
|
|
||||||
// QueryEntries runs a WHERE expression supplied by the user against the entries
|
// QueryEntries runs a WHERE expression supplied by the user against the entries
|
||||||
// table (e.g. "status_code = 404" or "host LIKE '%example.com%'").
|
// table (e.g. "status_code = 404" or "host LIKE '%example.com%'").
|
||||||
// It opens a dedicated read-only connection so that any DML or DDL in the
|
// Uses the persistent read-only connection (PRAGMA query_only=ON) so that any
|
||||||
// user-supplied expression is rejected by SQLite before it can execute.
|
// DML or DDL in the user-supplied expression is rejected by SQLite before it executes.
|
||||||
func (d *DB) QueryEntries(where string) ([]Entry, error) {
|
func (d *DB) QueryEntries(where string) ([]Entry, error) {
|
||||||
roConn, err := sql.Open("sqlite", d.path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer roConn.Close()
|
|
||||||
if _, err := roConn.Exec("PRAGMA query_only=ON"); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
q := "SELECT id, timestamp, method, host, path, status_code, request_raw, response_raw, flagged FROM entries WHERE " + strings.TrimSpace(where)
|
q := "SELECT id, timestamp, method, host, path, status_code, request_raw, response_raw, flagged FROM entries WHERE " + strings.TrimSpace(where)
|
||||||
rows, err := roConn.Query(q)
|
rows, err := d.roConn.Query(q)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -147,3 +144,18 @@ func (d *DB) DeleteAllEntries() error {
|
|||||||
_, err := d.conn.Exec(`DELETE FROM entries`)
|
_, err := d.conn.Exec(`DELETE FROM entries`)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteAllExceptFlagged deletes all unflagged entries. If there are no
|
||||||
|
// unflagged entries (only flagged ones remain), it deletes everything.
|
||||||
|
func (d *DB) DeleteAllExceptFlagged() error {
|
||||||
|
var count int
|
||||||
|
if err := d.conn.QueryRow(`SELECT COUNT(*) FROM entries WHERE flagged = 0`).Scan(&count); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if count > 0 {
|
||||||
|
_, err := d.conn.Exec(`DELETE FROM entries WHERE flagged = 0`)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err := d.conn.Exec(`DELETE FROM entries`)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ func (d *DB) UpsertFinding(f Finding) (bool, error) {
|
|||||||
func (d *DB) LoadFindings() ([]Finding, error) {
|
func (d *DB) LoadFindings() ([]Finding, error) {
|
||||||
rows, err := d.conn.Query(
|
rows, err := d.conn.Query(
|
||||||
`SELECT id, plugin_name, dedup_key, title, description, severity, created_at
|
`SELECT id, plugin_name, dedup_key, title, description, severity, created_at
|
||||||
FROM findings WHERE dismissed = 0 ORDER BY id DESC`,
|
FROM findings WHERE dismissed = 0 ORDER BY id ASC`,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
package db
|
|
||||||
|
|
||||||
type PluginState struct {
|
|
||||||
Name string
|
|
||||||
Enabled bool
|
|
||||||
ConfigText string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DB) SavePluginState(name string, enabled bool, configText string) error {
|
|
||||||
_, err := d.conn.Exec(
|
|
||||||
`INSERT INTO plugins (name, enabled, config_text) VALUES (?, ?, ?)
|
|
||||||
ON CONFLICT(name) DO UPDATE SET enabled = excluded.enabled, config_text = excluded.config_text`,
|
|
||||||
name, enabled, configText,
|
|
||||||
)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DB) LoadPluginStates() ([]PluginState, error) {
|
|
||||||
rows, err := d.conn.Query(`SELECT name, enabled, config_text FROM plugins`)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var out []PluginState
|
|
||||||
for rows.Next() {
|
|
||||||
var s PluginState
|
|
||||||
var enabled int
|
|
||||||
if err := rows.Scan(&s.Name, &enabled, &s.ConfigText); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
s.Enabled = enabled != 0
|
|
||||||
out = append(out, s)
|
|
||||||
}
|
|
||||||
return out, rows.Err()
|
|
||||||
}
|
|
||||||
@@ -58,9 +58,13 @@ func (b *Broker) SetOnNewEntry(cb func(db.Entry)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewBroker() *Broker {
|
func NewBroker() *Broker {
|
||||||
|
size := config.Global.Intercept.QueueSize
|
||||||
|
if size <= 0 {
|
||||||
|
size = 64
|
||||||
|
}
|
||||||
b := &Broker{
|
b := &Broker{
|
||||||
Incoming: make(chan *PendingRequest, 64),
|
Incoming: make(chan *PendingRequest, size),
|
||||||
IncomingResponse: make(chan *PendingResponse, 64),
|
IncomingResponse: make(chan *PendingResponse, size),
|
||||||
}
|
}
|
||||||
b.SetAutoForwardRegex(config.Global.Intercept.AutoForwardRegex)
|
b.SetAutoForwardRegex(config.Global.Intercept.AutoForwardRegex)
|
||||||
return b
|
return b
|
||||||
|
|||||||
@@ -3,14 +3,20 @@ package plugins
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/anotherhadi/spilltea/internal/config"
|
||||||
"github.com/anotherhadi/spilltea/internal/db"
|
"github.com/anotherhadi/spilltea/internal/db"
|
||||||
goproxy "github.com/lqqyt2423/go-mitmproxy/proxy"
|
goproxy "github.com/lqqyt2423/go-mitmproxy/proxy"
|
||||||
lua "github.com/yuin/gopher-lua"
|
lua "github.com/yuin/gopher-lua"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newLuaState(mgr *Manager, p *Plugin) *lua.LState {
|
func newLuaState(mgr *Manager, p *Plugin) *lua.LState {
|
||||||
@@ -175,6 +181,99 @@ func registerUtilities(L *lua.LState, mgr *Manager, p *Plugin) {
|
|||||||
return 0
|
return 0
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
L.SetGlobal("get_config", L.NewFunction(func(L *lua.LState) int {
|
||||||
|
// p.mu is already held by the hook caller - do not lock again.
|
||||||
|
configText := p.ConfigText
|
||||||
|
if configText == "" {
|
||||||
|
L.Push(L.NewTable())
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
var data interface{}
|
||||||
|
if err := yaml.Unmarshal([]byte(configText), &data); err != nil || data == nil {
|
||||||
|
L.Push(L.NewTable())
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
lv := goToLuaValue(L, data)
|
||||||
|
if _, ok := lv.(*lua.LTable); !ok {
|
||||||
|
L.Push(L.NewTable())
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
L.Push(lv)
|
||||||
|
return 1
|
||||||
|
}))
|
||||||
|
|
||||||
|
L.SetGlobal("send_request", L.NewFunction(func(L *lua.LState) int {
|
||||||
|
method := L.CheckString(1)
|
||||||
|
rawURL := L.CheckString(2)
|
||||||
|
hdrs := L.OptTable(3, L.NewTable())
|
||||||
|
body := L.OptString(4, "")
|
||||||
|
opts := L.OptTable(5, L.NewTable())
|
||||||
|
|
||||||
|
insecure := false
|
||||||
|
if v, ok := L.GetField(opts, "insecure").(lua.LBool); ok {
|
||||||
|
insecure = bool(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
transport := &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: insecure},
|
||||||
|
}
|
||||||
|
if up := config.Global.App.UpstreamProxy; up != "" {
|
||||||
|
if proxyURL, err := url.Parse(up); err == nil {
|
||||||
|
transport.Proxy = http.ProxyURL(proxyURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
client := &http.Client{
|
||||||
|
Transport: transport,
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
var bodyReader io.Reader
|
||||||
|
if body != "" {
|
||||||
|
bodyReader = strings.NewReader(body)
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest(method, rawURL, bodyReader)
|
||||||
|
if err != nil {
|
||||||
|
L.Push(lua.LNil)
|
||||||
|
L.Push(lua.LString(err.Error()))
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
hdrs.ForEach(func(k, v lua.LValue) {
|
||||||
|
ks, kok := k.(lua.LString)
|
||||||
|
vs, vok := v.(lua.LString)
|
||||||
|
if kok && vok {
|
||||||
|
req.Header.Set(string(ks), string(vs))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
L.Push(lua.LNil)
|
||||||
|
L.Push(lua.LString(err.Error()))
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
L.Push(lua.LNil)
|
||||||
|
L.Push(lua.LString(err.Error()))
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
result := L.NewTable()
|
||||||
|
L.SetField(result, "status_code", lua.LNumber(resp.StatusCode))
|
||||||
|
L.SetField(result, "body", lua.LString(string(respBody)))
|
||||||
|
respHeaders := L.NewTable()
|
||||||
|
for k, vals := range resp.Header {
|
||||||
|
L.SetField(respHeaders, k, lua.LString(strings.Join(vals, ", ")))
|
||||||
|
}
|
||||||
|
L.SetField(result, "headers", respHeaders)
|
||||||
|
|
||||||
|
L.Push(result)
|
||||||
|
L.Push(lua.LNil)
|
||||||
|
return 2
|
||||||
|
}))
|
||||||
|
|
||||||
L.SetGlobal("shell_pipe", L.NewFunction(func(L *lua.LState) int {
|
L.SetGlobal("shell_pipe", L.NewFunction(func(L *lua.LState) int {
|
||||||
cmd := L.CheckString(1)
|
cmd := L.CheckString(1)
|
||||||
input := L.OptString(2, "")
|
input := L.OptString(2, "")
|
||||||
@@ -201,6 +300,35 @@ func registerUtilities(L *lua.LState, mgr *Manager, p *Plugin) {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func goToLuaValue(L *lua.LState, v interface{}) lua.LValue {
|
||||||
|
switch val := v.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
t := L.NewTable()
|
||||||
|
for k, v2 := range val {
|
||||||
|
L.SetField(t, k, goToLuaValue(L, v2))
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
case []interface{}:
|
||||||
|
t := L.NewTable()
|
||||||
|
for i, v2 := range val {
|
||||||
|
L.RawSetInt(t, i+1, goToLuaValue(L, v2))
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
case string:
|
||||||
|
return lua.LString(val)
|
||||||
|
case int:
|
||||||
|
return lua.LNumber(val)
|
||||||
|
case float64:
|
||||||
|
return lua.LNumber(val)
|
||||||
|
case bool:
|
||||||
|
if val {
|
||||||
|
return lua.LTrue
|
||||||
|
}
|
||||||
|
return lua.LFalse
|
||||||
|
}
|
||||||
|
return lua.LNil
|
||||||
|
}
|
||||||
|
|
||||||
func luaTableString(t *lua.LTable, key string) string {
|
func luaTableString(t *lua.LTable, key string) string {
|
||||||
v := t.RawGetString(key)
|
v := t.RawGetString(key)
|
||||||
if s, ok := v.(lua.LString); ok {
|
if s, ok := v.(lua.LString); ok {
|
||||||
@@ -241,6 +369,21 @@ func pushRequest(L *lua.LState, f *goproxy.Flow) *lua.LTable {
|
|||||||
return 0
|
return 0
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
L.SetField(t, "set_path", L.NewFunction(func(L *lua.LState) int {
|
||||||
|
r.URL.Path = L.CheckString(2)
|
||||||
|
return 0
|
||||||
|
}))
|
||||||
|
|
||||||
|
L.SetField(t, "set_url", L.NewFunction(func(L *lua.LState) int {
|
||||||
|
parsed, err := url.Parse(L.CheckString(2))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[plugin] set_url: %v", err)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
r.URL = parsed
|
||||||
|
return 0
|
||||||
|
}))
|
||||||
|
|
||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+70
-54
@@ -13,14 +13,23 @@ import (
|
|||||||
"github.com/anotherhadi/spilltea/internal/intercept"
|
"github.com/anotherhadi/spilltea/internal/intercept"
|
||||||
goproxy "github.com/lqqyt2423/go-mitmproxy/proxy"
|
goproxy "github.com/lqqyt2423/go-mitmproxy/proxy"
|
||||||
lua "github.com/yuin/gopher-lua"
|
lua "github.com/yuin/gopher-lua"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// GlobalDefault holds global defaults for a single plugin, read from config.yaml.
|
||||||
|
type GlobalDefault struct {
|
||||||
|
Enable *bool
|
||||||
|
Config interface{}
|
||||||
|
}
|
||||||
|
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
plugins []*Plugin
|
plugins []*Plugin
|
||||||
|
|
||||||
db *db.DB
|
db *db.DB
|
||||||
broker *intercept.Broker
|
pluginsFile *PluginsFile
|
||||||
|
broker *intercept.Broker
|
||||||
|
globalDefaults map[string]GlobalDefault
|
||||||
|
|
||||||
Notifs chan PluginNotifMsg
|
Notifs chan PluginNotifMsg
|
||||||
Quit chan string
|
Quit chan string
|
||||||
@@ -43,6 +52,14 @@ func (m *Manager) SetDB(d *db.DB) {
|
|||||||
m.db = d
|
m.db = d
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Manager) SetPluginsFile(pf *PluginsFile) {
|
||||||
|
m.pluginsFile = pf
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) SetGlobalDefaults(defaults map[string]GlobalDefault) {
|
||||||
|
m.globalDefaults = defaults
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Manager) LoadFromDir(dir string) error {
|
func (m *Manager) LoadFromDir(dir string) error {
|
||||||
entries, err := os.ReadDir(dir)
|
entries, err := os.ReadDir(dir)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
@@ -52,17 +69,6 @@ func (m *Manager) LoadFromDir(dir string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var states map[string]db.PluginState
|
|
||||||
if m.db != nil {
|
|
||||||
list, err := m.db.LoadPluginStates()
|
|
||||||
if err == nil {
|
|
||||||
states = make(map[string]db.PluginState, len(list))
|
|
||||||
for _, s := range list {
|
|
||||||
states[s.Name] = s
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
if e.IsDir() || !strings.HasSuffix(e.Name(), ".lua") {
|
if e.IsDir() || !strings.HasSuffix(e.Name(), ".lua") {
|
||||||
continue
|
continue
|
||||||
@@ -73,9 +79,23 @@ func (m *Manager) LoadFromDir(dir string) error {
|
|||||||
log.Printf("plugin load error %s: %v", path, err)
|
log.Printf("plugin load error %s: %v", path, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if s, ok := states[p.Name]; ok {
|
if m.pluginsFile != nil {
|
||||||
p.Enabled = s.Enabled
|
if enabled, configText, found := m.pluginsFile.get(p.ID); found {
|
||||||
p.ConfigText = s.ConfigText
|
p.Enabled = enabled
|
||||||
|
p.ConfigText = configText
|
||||||
|
} else {
|
||||||
|
if def, ok := m.globalDefaults[p.ID]; ok {
|
||||||
|
if def.Enable != nil {
|
||||||
|
p.Enabled = *def.Enable
|
||||||
|
}
|
||||||
|
if def.Config != nil {
|
||||||
|
if raw, err := yaml.Marshal(def.Config); err == nil {
|
||||||
|
p.ConfigText = string(raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.pluginsFile.register(p.ID, p.Enabled, p.ConfigText)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
m.plugins = append(m.plugins, p)
|
m.plugins = append(m.plugins, p)
|
||||||
@@ -89,6 +109,7 @@ func (m *Manager) LoadFromDir(dir string) error {
|
|||||||
|
|
||||||
func (m *Manager) loadPlugin(path string) (*Plugin, error) {
|
func (m *Manager) loadPlugin(path string) (*Plugin, error) {
|
||||||
p := &Plugin{
|
p := &Plugin{
|
||||||
|
ID: strings.TrimSuffix(filepath.Base(path), ".lua"),
|
||||||
FilePath: path,
|
FilePath: path,
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
hooks: make(map[string]HookConfig),
|
hooks: make(map[string]HookConfig),
|
||||||
@@ -109,7 +130,7 @@ func (m *Manager) loadPlugin(path string) (*Plugin, error) {
|
|||||||
p.Name = string(s)
|
p.Name = string(s)
|
||||||
}
|
}
|
||||||
if p.Name == "" {
|
if p.Name == "" {
|
||||||
p.Name = strings.TrimSuffix(filepath.Base(path), ".lua")
|
p.Name = p.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
if s, ok := pluginTable.RawGetString("description").(lua.LString); ok {
|
if s, ok := pluginTable.RawGetString("description").(lua.LString); ok {
|
||||||
@@ -131,12 +152,6 @@ func (m *Manager) loadPlugin(path string) (*Plugin, error) {
|
|||||||
"on_response": false,
|
"on_response": false,
|
||||||
"on_history_entry": false,
|
"on_history_entry": false,
|
||||||
}
|
}
|
||||||
// Fixed-sync hooks: always sync, not configurable.
|
|
||||||
fixedSyncHooks := map[string]struct{}{
|
|
||||||
"on_config": {},
|
|
||||||
"on_quit": {},
|
|
||||||
}
|
|
||||||
|
|
||||||
for hookName, defaultSync := range configurableHooks {
|
for hookName, defaultSync := range configurableHooks {
|
||||||
if tbl, ok := pluginTable.RawGetString(hookName).(*lua.LTable); ok {
|
if tbl, ok := pluginTable.RawGetString(hookName).(*lua.LTable); ok {
|
||||||
p.hooks[hookName] = HookConfig{Sync: tbl.RawGetString("sync") == lua.LTrue}
|
p.hooks[hookName] = HookConfig{Sync: tbl.RawGetString("sync") == lua.LTrue}
|
||||||
@@ -146,9 +161,9 @@ func (m *Manager) loadPlugin(path string) (*Plugin, error) {
|
|||||||
p.hooks[hookName] = HookConfig{Sync: defaultSync}
|
p.hooks[hookName] = HookConfig{Sync: defaultSync}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for hookName := range fixedSyncHooks {
|
for _, fixedSync := range []string{"on_config", "on_quit"} {
|
||||||
if p.L.GetGlobal(hookName) != lua.LNil {
|
if p.L.GetGlobal(fixedSync) != lua.LNil {
|
||||||
p.hooks[hookName] = HookConfig{Sync: true}
|
p.hooks[fixedSync] = HookConfig{Sync: true}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,11 +178,11 @@ func (m *Manager) GetPlugins() []*Plugin {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) TogglePlugin(name string) {
|
func (m *Manager) TogglePlugin(id string) {
|
||||||
m.mu.RLock()
|
m.mu.RLock()
|
||||||
var found *Plugin
|
var found *Plugin
|
||||||
for _, p := range m.plugins {
|
for _, p := range m.plugins {
|
||||||
if p.Name == name {
|
if p.ID == id {
|
||||||
found = p
|
found = p
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -179,10 +194,11 @@ func (m *Manager) TogglePlugin(name string) {
|
|||||||
found.mu.Lock()
|
found.mu.Lock()
|
||||||
found.Enabled = !found.Enabled
|
found.Enabled = !found.Enabled
|
||||||
enabled := found.Enabled
|
enabled := found.Enabled
|
||||||
configText := found.ConfigText
|
|
||||||
found.mu.Unlock()
|
found.mu.Unlock()
|
||||||
if m.db != nil {
|
if m.pluginsFile != nil {
|
||||||
_ = m.db.SavePluginState(name, enabled, configText)
|
if err := m.pluginsFile.setEnabled(id, enabled); err != nil {
|
||||||
|
log.Printf("plugin %s: save state: %v", id, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if !enabled {
|
if !enabled {
|
||||||
return
|
return
|
||||||
@@ -194,8 +210,10 @@ func (m *Manager) TogglePlugin(name string) {
|
|||||||
disableIfFalse := func(p *Plugin, ret lua.LValue) {
|
disableIfFalse := func(p *Plugin, ret lua.LValue) {
|
||||||
if ret == lua.LFalse {
|
if ret == lua.LFalse {
|
||||||
p.Enabled = false
|
p.Enabled = false
|
||||||
if m.db != nil {
|
if m.pluginsFile != nil {
|
||||||
_ = m.db.SavePluginState(p.Name, false, p.ConfigText)
|
if err := m.pluginsFile.setEnabled(p.ID, false); err != nil {
|
||||||
|
log.Printf("plugin %s: save state: %v", p.ID, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -222,11 +240,11 @@ func (m *Manager) TogglePlugin(name string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) SaveConfig(name, configText string) {
|
func (m *Manager) SaveConfig(id, configText string) {
|
||||||
m.mu.RLock()
|
m.mu.RLock()
|
||||||
var found *Plugin
|
var found *Plugin
|
||||||
for _, p := range m.plugins {
|
for _, p := range m.plugins {
|
||||||
if p.Name == name {
|
if p.ID == id {
|
||||||
found = p
|
found = p
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -237,39 +255,35 @@ func (m *Manager) SaveConfig(name, configText string) {
|
|||||||
}
|
}
|
||||||
found.mu.Lock()
|
found.mu.Lock()
|
||||||
found.ConfigText = configText
|
found.ConfigText = configText
|
||||||
enabled := found.Enabled
|
|
||||||
_, hasOnConfig := found.hooks["on_config"]
|
|
||||||
found.mu.Unlock()
|
found.mu.Unlock()
|
||||||
if m.db != nil {
|
if m.pluginsFile != nil {
|
||||||
_ = m.db.SavePluginState(name, enabled, configText)
|
if err := m.pluginsFile.setConfig(id, configText); err != nil {
|
||||||
|
log.Printf("plugin %s: save config: %v", id, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if !hasOnConfig {
|
if _, ok := found.hooks["on_config"]; !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// on_config is always sync.
|
|
||||||
found.mu.Lock()
|
found.mu.Lock()
|
||||||
if _, err := callHook(found, "on_config", lua.LString(configText)); err != nil {
|
if _, err := callHook(found, "on_config"); err != nil {
|
||||||
log.Printf("plugin %s on_config (config reload): %v", name, err)
|
log.Printf("plugin %s on_config: %v", id, err)
|
||||||
}
|
}
|
||||||
found.mu.Unlock()
|
found.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) RunOnStart() {
|
func (m *Manager) RunOnStart() {
|
||||||
// on_config runs first, always sync, for every enabled plugin that has it.
|
|
||||||
for _, p := range m.GetPlugins() {
|
for _, p := range m.GetPlugins() {
|
||||||
if !p.Enabled {
|
if !p.Enabled {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if _, ok := p.hooks["on_config"]; !ok {
|
if _, ok := p.hooks["on_config"]; ok {
|
||||||
continue
|
p.mu.Lock()
|
||||||
|
if _, err := callHook(p, "on_config"); err != nil {
|
||||||
|
log.Printf("plugin %s on_config: %v", p.Name, err)
|
||||||
|
}
|
||||||
|
p.mu.Unlock()
|
||||||
}
|
}
|
||||||
p.mu.Lock()
|
|
||||||
if _, err := callHook(p, "on_config", lua.LString(p.ConfigText)); err != nil {
|
|
||||||
log.Printf("plugin %s on_config: %v", p.Name, err)
|
|
||||||
}
|
|
||||||
p.mu.Unlock()
|
|
||||||
}
|
}
|
||||||
// on_start runs after, sync or async depending on plugin config.
|
|
||||||
for _, p := range m.GetPlugins() {
|
for _, p := range m.GetPlugins() {
|
||||||
if !p.Enabled {
|
if !p.Enabled {
|
||||||
continue
|
continue
|
||||||
@@ -281,8 +295,10 @@ func (m *Manager) RunOnStart() {
|
|||||||
disableIfFalse := func(p *Plugin, ret lua.LValue) {
|
disableIfFalse := func(p *Plugin, ret lua.LValue) {
|
||||||
if ret == lua.LFalse {
|
if ret == lua.LFalse {
|
||||||
p.Enabled = false
|
p.Enabled = false
|
||||||
if m.db != nil {
|
if m.pluginsFile != nil {
|
||||||
_ = m.db.SavePluginState(p.Name, false, p.ConfigText)
|
if err := m.pluginsFile.setEnabled(p.ID, false); err != nil {
|
||||||
|
log.Printf("plugin %s: save state: %v", p.ID, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package plugins
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type pluginFileEntry struct {
|
||||||
|
Enable bool `yaml:"enable"`
|
||||||
|
Config interface{} `yaml:"config,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type pluginsFileData struct {
|
||||||
|
Plugins map[string]pluginFileEntry `yaml:"plugins"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PluginsFile struct {
|
||||||
|
path string
|
||||||
|
data pluginsFileData
|
||||||
|
}
|
||||||
|
|
||||||
|
func OpenPluginsFile(dbPath string) (*PluginsFile, error) {
|
||||||
|
path := filepath.Join(filepath.Dir(dbPath), "plugins.yaml")
|
||||||
|
pf := &PluginsFile{
|
||||||
|
path: path,
|
||||||
|
data: pluginsFileData{Plugins: make(map[string]pluginFileEntry)},
|
||||||
|
}
|
||||||
|
raw, err := os.ReadFile(path)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return pf, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := yaml.Unmarshal(raw, &pf.data); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if pf.data.Plugins == nil {
|
||||||
|
pf.data.Plugins = make(map[string]pluginFileEntry)
|
||||||
|
}
|
||||||
|
return pf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pf *PluginsFile) save() error {
|
||||||
|
raw, err := yaml.Marshal(&pf.data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.WriteFile(pf.path, raw, 0o600)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pf *PluginsFile) get(id string) (enabled bool, config string, found bool) {
|
||||||
|
e, ok := pf.data.Plugins[id]
|
||||||
|
if !ok {
|
||||||
|
return false, "", false
|
||||||
|
}
|
||||||
|
if e.Config == nil {
|
||||||
|
return e.Enable, "", true
|
||||||
|
}
|
||||||
|
raw, err := yaml.Marshal(e.Config)
|
||||||
|
if err != nil {
|
||||||
|
return e.Enable, "", true
|
||||||
|
}
|
||||||
|
return e.Enable, string(raw), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pf *PluginsFile) register(id string, defaultEnabled bool, configText string) {
|
||||||
|
if _, ok := pf.data.Plugins[id]; !ok {
|
||||||
|
entry := pluginFileEntry{Enable: defaultEnabled}
|
||||||
|
if configText != "" {
|
||||||
|
var parsed interface{}
|
||||||
|
if err := yaml.Unmarshal([]byte(configText), &parsed); err == nil {
|
||||||
|
entry.Config = parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pf.data.Plugins[id] = entry
|
||||||
|
_ = pf.save()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pf *PluginsFile) setEnabled(id string, enabled bool) error {
|
||||||
|
e := pf.data.Plugins[id]
|
||||||
|
e.Enable = enabled
|
||||||
|
pf.data.Plugins[id] = e
|
||||||
|
return pf.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pf *PluginsFile) setConfig(id string, configText string) error {
|
||||||
|
e := pf.data.Plugins[id]
|
||||||
|
if configText == "" {
|
||||||
|
e.Config = nil
|
||||||
|
} else {
|
||||||
|
var parsed interface{}
|
||||||
|
if err := yaml.Unmarshal([]byte(configText), &parsed); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
e.Config = parsed
|
||||||
|
}
|
||||||
|
pf.data.Plugins[id] = e
|
||||||
|
return pf.save()
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ type HookConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Plugin struct {
|
type Plugin struct {
|
||||||
|
ID string
|
||||||
Name string
|
Name string
|
||||||
Description string
|
Description string
|
||||||
FilePath string
|
FilePath string
|
||||||
@@ -37,6 +38,7 @@ func (p *Plugin) HookConfig(name string) (HookConfig, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Info struct {
|
type Info struct {
|
||||||
|
ID string
|
||||||
Name string
|
Name string
|
||||||
Description string
|
Description string
|
||||||
FilePath string
|
FilePath string
|
||||||
@@ -57,6 +59,7 @@ func (p *Plugin) Info() Info {
|
|||||||
hooks[k] = v
|
hooks[k] = v
|
||||||
}
|
}
|
||||||
return Info{
|
return Info{
|
||||||
|
ID: p.ID,
|
||||||
Name: p.Name,
|
Name: p.Name,
|
||||||
Description: p.Description,
|
Description: p.Description,
|
||||||
FilePath: p.FilePath,
|
FilePath: p.FilePath,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package proxy
|
package proxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/subtle"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -46,7 +47,6 @@ func (a *interceptAddon) Request(f *goproxy.Flow) {
|
|||||||
switch a.plugins.RunSyncOnRequest(f) {
|
switch a.plugins.RunSyncOnRequest(f) {
|
||||||
case intercept.Drop:
|
case intercept.Drop:
|
||||||
f.Response = dropResponse()
|
f.Response = dropResponse()
|
||||||
go a.plugins.RunAsyncOnRequest(f)
|
|
||||||
return
|
return
|
||||||
case intercept.Forward:
|
case intercept.Forward:
|
||||||
go a.plugins.RunAsyncOnRequest(f)
|
go a.plugins.RunAsyncOnRequest(f)
|
||||||
@@ -133,7 +133,9 @@ func Start(broker *intercept.Broker, mgr *plugins.Manager) error {
|
|||||||
wantUser, wantPass := parts[0], parts[1]
|
wantUser, wantPass := parts[0], parts[1]
|
||||||
p.SetAuthProxy(func(res http.ResponseWriter, req *http.Request) (bool, error) {
|
p.SetAuthProxy(func(res http.ResponseWriter, req *http.Request) (bool, error) {
|
||||||
user, pass, ok := parseBasicProxyAuth(req.Header.Get("Proxy-Authorization"))
|
user, pass, ok := parseBasicProxyAuth(req.Header.Get("Proxy-Authorization"))
|
||||||
if !ok || user != wantUser || pass != wantPass {
|
userOK := subtle.ConstantTimeCompare([]byte(user), []byte(wantUser))
|
||||||
|
passOK := subtle.ConstantTimeCompare([]byte(pass), []byte(wantPass))
|
||||||
|
if !ok || userOK&passOK != 1 {
|
||||||
res.Header().Set("Proxy-Authenticate", `Basic realm="spilltea"`)
|
res.Header().Set("Proxy-Authenticate", `Basic realm="spilltea"`)
|
||||||
return false, fmt.Errorf("invalid credentials")
|
return false, fmt.Errorf("invalid credentials")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
package style
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"charm.land/lipgloss/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PanelContentH returns the usable inner content height for a panel rendered by
|
|
||||||
// RenderWithTitle. It subtracts the two border lines (top + bottom) from the
|
|
||||||
// total panel height.
|
|
||||||
func PanelContentH(totalH int) int {
|
|
||||||
h := totalH - 2
|
|
||||||
if h < 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return h
|
|
||||||
}
|
|
||||||
|
|
||||||
// RenderWithTitle renders a lipgloss bordered box with a title embedded in the
|
|
||||||
// top border, matching the border's own foreground color. height is the total
|
|
||||||
// desired output height (including both border lines).
|
|
||||||
func RenderWithTitle(border lipgloss.Style, title, content string, width, height int) string {
|
|
||||||
boxH := height - 1
|
|
||||||
if contentH := boxH - 1; contentH > 0 {
|
|
||||||
lines := strings.Split(content, "\n")
|
|
||||||
if len(lines) > contentH {
|
|
||||||
content = strings.Join(lines[:contentH], "\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
box := border.BorderTop(false).Width(width).Height(boxH).Render(content)
|
|
||||||
|
|
||||||
boxWidth := lipgloss.Width(strings.SplitN(box, "\n", 2)[0])
|
|
||||||
label := " " + title + " "
|
|
||||||
fillW := boxWidth - lipgloss.Width(label) - 2
|
|
||||||
if fillW < 0 {
|
|
||||||
fillW = 0
|
|
||||||
}
|
|
||||||
bc := lipgloss.NewStyle().Foreground(border.GetBorderTopForeground())
|
|
||||||
topLine := bc.Render("╭ ") + bc.Render(title) + bc.Render(" "+strings.Repeat("─", fillW)+"╮")
|
|
||||||
|
|
||||||
return lipgloss.JoinVertical(lipgloss.Left, topLine, box)
|
|
||||||
}
|
|
||||||
@@ -1,64 +1,23 @@
|
|||||||
package style
|
package style
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
|
||||||
|
|
||||||
"charm.land/bubbles/v2/paginator"
|
|
||||||
"charm.land/bubbles/v2/textarea"
|
|
||||||
"charm.land/bubbles/v2/viewport"
|
|
||||||
"charm.land/lipgloss/v2"
|
"charm.land/lipgloss/v2"
|
||||||
|
ilovetui "github.com/anotherhadi/ilovetui"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewViewport() viewport.Model {
|
|
||||||
vp := viewport.New()
|
|
||||||
vp.MouseWheelEnabled = false
|
|
||||||
return vp
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewPaginator() paginator.Model {
|
|
||||||
p := paginator.New()
|
|
||||||
p.Type = paginator.Dots
|
|
||||||
p.ActiveDot = S.PagerDotActive
|
|
||||||
p.InactiveDot = S.PagerDotInactive
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTextarea(showLineNumbers bool) textarea.Model {
|
|
||||||
ta := textarea.New()
|
|
||||||
ta.Prompt = ""
|
|
||||||
ta.ShowLineNumbers = showLineNumbers
|
|
||||||
ta.CharLimit = 0
|
|
||||||
ta.EndOfBufferCharacter = '~'
|
|
||||||
ts := ta.Styles()
|
|
||||||
ts.Focused.Base = lipgloss.NewStyle()
|
|
||||||
ts.Blurred.Base = lipgloss.NewStyle()
|
|
||||||
ts.Focused.Text = lipgloss.NewStyle().Foreground(S.Text)
|
|
||||||
ts.Focused.CursorLine = lipgloss.NewStyle().Background(S.Selection).Foreground(S.Text)
|
|
||||||
ts.Focused.CursorLineNumber = lipgloss.NewStyle().Background(S.Selection).Foreground(S.Primary).Bold(true)
|
|
||||||
ts.Focused.LineNumber = lipgloss.NewStyle().Foreground(S.Subtle)
|
|
||||||
ts.Focused.Placeholder = lipgloss.NewStyle().Foreground(S.Subtle)
|
|
||||||
ts.Focused.EndOfBuffer = lipgloss.NewStyle().Foreground(S.SubtleBg)
|
|
||||||
ts.Blurred.Text = lipgloss.NewStyle().Foreground(S.MutedFg)
|
|
||||||
ts.Blurred.LineNumber = lipgloss.NewStyle().Foreground(S.SubtleBg)
|
|
||||||
ts.Blurred.Placeholder = lipgloss.NewStyle().Foreground(S.Subtle)
|
|
||||||
ts.Blurred.EndOfBuffer = lipgloss.NewStyle().Foreground(S.SubtleBg)
|
|
||||||
ta.SetStyles(ts)
|
|
||||||
return ta
|
|
||||||
}
|
|
||||||
|
|
||||||
func SeverityStyle(sev string) lipgloss.Style {
|
func SeverityStyle(sev string) lipgloss.Style {
|
||||||
base := lipgloss.NewStyle().Bold(true)
|
base := lipgloss.NewStyle().Bold(true)
|
||||||
switch sev {
|
switch sev {
|
||||||
case "critical":
|
case "critical":
|
||||||
return base.Foreground(S.Error)
|
return base.Foreground(ilovetui.S.Error)
|
||||||
case "high":
|
case "high":
|
||||||
return base.Foreground(S.Warning)
|
return base.Foreground(ilovetui.S.Warning)
|
||||||
case "medium":
|
case "medium":
|
||||||
return base.Foreground(S.Primary)
|
return base.Foreground(ilovetui.S.Primary)
|
||||||
case "low":
|
case "low":
|
||||||
return base.Foreground(S.Success)
|
return base.Foreground(ilovetui.S.Success)
|
||||||
default:
|
default:
|
||||||
return base.Foreground(S.Subtle)
|
return base.Foreground(ilovetui.S.Subtle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,22 +25,12 @@ func StatusStyle(code, width int) lipgloss.Style {
|
|||||||
base := lipgloss.NewStyle().Bold(true).Width(width)
|
base := lipgloss.NewStyle().Bold(true).Width(width)
|
||||||
switch {
|
switch {
|
||||||
case code >= 500:
|
case code >= 500:
|
||||||
return base.Foreground(S.Error)
|
return base.Foreground(ilovetui.S.Error)
|
||||||
case code >= 400:
|
case code >= 400:
|
||||||
return base.Foreground(S.Warning)
|
return base.Foreground(ilovetui.S.Warning)
|
||||||
case code >= 300:
|
case code >= 300:
|
||||||
return base.Foreground(S.Primary)
|
return base.Foreground(ilovetui.S.Primary)
|
||||||
default:
|
default:
|
||||||
return base.Foreground(S.Success)
|
return base.Foreground(ilovetui.S.Success)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SplitH splits totalHeight into top and bottom sections, accounting for the
|
|
||||||
// status bar height.
|
|
||||||
func SplitH(totalHeight int, statusBar string, ratio float64) (top, bottom int) {
|
|
||||||
statusH := strings.Count(statusBar, "\n") + 1
|
|
||||||
available := totalHeight - statusH
|
|
||||||
top = int(float64(available) * ratio)
|
|
||||||
bottom = available - top
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,236 +0,0 @@
|
|||||||
package style
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/anotherhadi/spilltea/internal/config"
|
|
||||||
|
|
||||||
"charm.land/glamour/v2/ansi"
|
|
||||||
)
|
|
||||||
|
|
||||||
func GlamourStyleConfig(cfg *config.Config) ansi.StyleConfig {
|
|
||||||
c := cfg.TUI.Colors
|
|
||||||
|
|
||||||
str := func(s string) *string { return &s }
|
|
||||||
hex := func(base string) *string { return str("#" + base) }
|
|
||||||
boolPtr := func(b bool) *bool { return &b }
|
|
||||||
uintPtr := func(u uint) *uint { return &u }
|
|
||||||
|
|
||||||
return ansi.StyleConfig{
|
|
||||||
Document: ansi.StyleBlock{
|
|
||||||
StylePrimitive: ansi.StylePrimitive{
|
|
||||||
BlockPrefix: "\n",
|
|
||||||
BlockSuffix: "\n",
|
|
||||||
Color: hex(c.Base05),
|
|
||||||
},
|
|
||||||
Margin: uintPtr(2),
|
|
||||||
},
|
|
||||||
BlockQuote: ansi.StyleBlock{
|
|
||||||
StylePrimitive: ansi.StylePrimitive{
|
|
||||||
Color: hex(c.Base03),
|
|
||||||
Italic: boolPtr(true),
|
|
||||||
},
|
|
||||||
Indent: uintPtr(1),
|
|
||||||
IndentToken: str("│ "),
|
|
||||||
},
|
|
||||||
List: ansi.StyleList{
|
|
||||||
LevelIndent: 2,
|
|
||||||
},
|
|
||||||
Heading: ansi.StyleBlock{
|
|
||||||
StylePrimitive: ansi.StylePrimitive{
|
|
||||||
BlockSuffix: "\n",
|
|
||||||
Color: hex(c.Base0D),
|
|
||||||
Bold: boolPtr(true),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
H1: ansi.StyleBlock{
|
|
||||||
StylePrimitive: ansi.StylePrimitive{
|
|
||||||
Prefix: " ",
|
|
||||||
Suffix: " ",
|
|
||||||
Color: hex(c.Base07),
|
|
||||||
BackgroundColor: hex(c.Base0D),
|
|
||||||
Bold: boolPtr(true),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
H2: ansi.StyleBlock{
|
|
||||||
StylePrimitive: ansi.StylePrimitive{
|
|
||||||
Prefix: "## ",
|
|
||||||
Color: hex(c.Base0D),
|
|
||||||
Bold: boolPtr(true),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
H3: ansi.StyleBlock{
|
|
||||||
StylePrimitive: ansi.StylePrimitive{
|
|
||||||
Prefix: "### ",
|
|
||||||
Color: hex(c.Base0C),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
H4: ansi.StyleBlock{
|
|
||||||
StylePrimitive: ansi.StylePrimitive{
|
|
||||||
Prefix: "#### ",
|
|
||||||
Color: hex(c.Base0B),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
H5: ansi.StyleBlock{
|
|
||||||
StylePrimitive: ansi.StylePrimitive{
|
|
||||||
Prefix: "##### ",
|
|
||||||
Color: hex(c.Base09),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
H6: ansi.StyleBlock{
|
|
||||||
StylePrimitive: ansi.StylePrimitive{
|
|
||||||
Prefix: "###### ",
|
|
||||||
Color: hex(c.Base08),
|
|
||||||
Bold: boolPtr(false),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Strikethrough: ansi.StylePrimitive{
|
|
||||||
CrossedOut: boolPtr(true),
|
|
||||||
},
|
|
||||||
Emph: ansi.StylePrimitive{
|
|
||||||
Italic: boolPtr(true),
|
|
||||||
},
|
|
||||||
Strong: ansi.StylePrimitive{
|
|
||||||
Bold: boolPtr(true),
|
|
||||||
},
|
|
||||||
HorizontalRule: ansi.StylePrimitive{
|
|
||||||
Color: hex(c.Base03),
|
|
||||||
Format: "\n--------\n",
|
|
||||||
},
|
|
||||||
Item: ansi.StylePrimitive{
|
|
||||||
BlockPrefix: "• ",
|
|
||||||
},
|
|
||||||
Enumeration: ansi.StylePrimitive{
|
|
||||||
BlockPrefix: ". ",
|
|
||||||
},
|
|
||||||
Task: ansi.StyleTask{
|
|
||||||
Ticked: "[✓] ",
|
|
||||||
Unticked: "[ ] ",
|
|
||||||
},
|
|
||||||
Link: ansi.StylePrimitive{
|
|
||||||
Color: hex(c.Base0C),
|
|
||||||
Underline: boolPtr(true),
|
|
||||||
},
|
|
||||||
LinkText: ansi.StylePrimitive{
|
|
||||||
Color: hex(c.Base0D),
|
|
||||||
Bold: boolPtr(true),
|
|
||||||
},
|
|
||||||
Image: ansi.StylePrimitive{
|
|
||||||
Color: hex(c.Base0C),
|
|
||||||
Underline: boolPtr(true),
|
|
||||||
},
|
|
||||||
ImageText: ansi.StylePrimitive{
|
|
||||||
Color: hex(c.Base04),
|
|
||||||
Format: "Image: {{.text}} ->",
|
|
||||||
},
|
|
||||||
Code: ansi.StyleBlock{
|
|
||||||
StylePrimitive: ansi.StylePrimitive{
|
|
||||||
Prefix: " ",
|
|
||||||
Suffix: " ",
|
|
||||||
Color: hex(c.Base0B),
|
|
||||||
BackgroundColor: hex(c.Base01),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
CodeBlock: ansi.StyleCodeBlock{
|
|
||||||
StyleBlock: ansi.StyleBlock{
|
|
||||||
StylePrimitive: ansi.StylePrimitive{
|
|
||||||
Color: hex(c.Base04),
|
|
||||||
},
|
|
||||||
Margin: uintPtr(2),
|
|
||||||
},
|
|
||||||
Chroma: &ansi.Chroma{
|
|
||||||
Text: ansi.StylePrimitive{
|
|
||||||
Color: hex(c.Base05),
|
|
||||||
},
|
|
||||||
Error: ansi.StylePrimitive{
|
|
||||||
Color: hex(c.Base07),
|
|
||||||
BackgroundColor: hex(c.Base08),
|
|
||||||
},
|
|
||||||
Comment: ansi.StylePrimitive{
|
|
||||||
Color: hex(c.Base03),
|
|
||||||
Italic: boolPtr(true),
|
|
||||||
},
|
|
||||||
CommentPreproc: ansi.StylePrimitive{
|
|
||||||
Color: hex(c.Base09),
|
|
||||||
},
|
|
||||||
Keyword: ansi.StylePrimitive{
|
|
||||||
Color: hex(c.Base0E),
|
|
||||||
},
|
|
||||||
KeywordReserved: ansi.StylePrimitive{
|
|
||||||
Color: hex(c.Base0E),
|
|
||||||
},
|
|
||||||
KeywordNamespace: ansi.StylePrimitive{
|
|
||||||
Color: hex(c.Base0D),
|
|
||||||
},
|
|
||||||
KeywordType: ansi.StylePrimitive{
|
|
||||||
Color: hex(c.Base0A),
|
|
||||||
},
|
|
||||||
Operator: ansi.StylePrimitive{
|
|
||||||
Color: hex(c.Base05),
|
|
||||||
},
|
|
||||||
Punctuation: ansi.StylePrimitive{
|
|
||||||
Color: hex(c.Base05),
|
|
||||||
},
|
|
||||||
Name: ansi.StylePrimitive{
|
|
||||||
Color: hex(c.Base05),
|
|
||||||
},
|
|
||||||
NameBuiltin: ansi.StylePrimitive{
|
|
||||||
Color: hex(c.Base0D),
|
|
||||||
},
|
|
||||||
NameTag: ansi.StylePrimitive{
|
|
||||||
Color: hex(c.Base08),
|
|
||||||
},
|
|
||||||
NameAttribute: ansi.StylePrimitive{
|
|
||||||
Color: hex(c.Base0A),
|
|
||||||
},
|
|
||||||
NameClass: ansi.StylePrimitive{
|
|
||||||
Color: hex(c.Base0A),
|
|
||||||
Bold: boolPtr(true),
|
|
||||||
Underline: boolPtr(true),
|
|
||||||
},
|
|
||||||
NameConstant: ansi.StylePrimitive{
|
|
||||||
Color: hex(c.Base09),
|
|
||||||
},
|
|
||||||
NameDecorator: ansi.StylePrimitive{
|
|
||||||
Color: hex(c.Base0C),
|
|
||||||
},
|
|
||||||
NameFunction: ansi.StylePrimitive{
|
|
||||||
Color: hex(c.Base0D),
|
|
||||||
},
|
|
||||||
LiteralNumber: ansi.StylePrimitive{
|
|
||||||
Color: hex(c.Base09),
|
|
||||||
},
|
|
||||||
LiteralString: ansi.StylePrimitive{
|
|
||||||
Color: hex(c.Base0B),
|
|
||||||
},
|
|
||||||
LiteralStringEscape: ansi.StylePrimitive{
|
|
||||||
Color: hex(c.Base0C),
|
|
||||||
},
|
|
||||||
GenericDeleted: ansi.StylePrimitive{
|
|
||||||
Color: hex(c.Base08),
|
|
||||||
},
|
|
||||||
GenericEmph: ansi.StylePrimitive{
|
|
||||||
Italic: boolPtr(true),
|
|
||||||
},
|
|
||||||
GenericInserted: ansi.StylePrimitive{
|
|
||||||
Color: hex(c.Base0B),
|
|
||||||
},
|
|
||||||
GenericStrong: ansi.StylePrimitive{
|
|
||||||
Bold: boolPtr(true),
|
|
||||||
},
|
|
||||||
GenericSubheading: ansi.StylePrimitive{
|
|
||||||
Color: hex(c.Base04),
|
|
||||||
},
|
|
||||||
Background: ansi.StylePrimitive{
|
|
||||||
BackgroundColor: hex(c.Base01),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Table: ansi.StyleTable{
|
|
||||||
StyleBlock: ansi.StyleBlock{
|
|
||||||
StylePrimitive: ansi.StylePrimitive{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
DefinitionDescription: ansi.StylePrimitive{
|
|
||||||
BlockPrefix: "\n> ",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+27
-25
@@ -3,12 +3,13 @@ package style
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"image/color"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"charm.land/lipgloss/v2"
|
"charm.land/lipgloss/v2"
|
||||||
|
ilovetui "github.com/anotherhadi/ilovetui"
|
||||||
"github.com/anotherhadi/spilltea/internal/config"
|
"github.com/anotherhadi/spilltea/internal/config"
|
||||||
"golang.org/x/net/html"
|
"golang.org/x/net/html"
|
||||||
|
"image/color"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Paint(c color.Color, s string) string {
|
func Paint(c color.Color, s string) string {
|
||||||
@@ -18,6 +19,7 @@ func Paint(c color.Color, s string) string {
|
|||||||
func HighlightHTTP(raw string) string {
|
func HighlightHTTP(raw string) string {
|
||||||
raw = strings.ReplaceAll(raw, "\r\n", "\n")
|
raw = strings.ReplaceAll(raw, "\r\n", "\n")
|
||||||
raw = strings.ReplaceAll(raw, "\r", "\n")
|
raw = strings.ReplaceAll(raw, "\r", "\n")
|
||||||
|
raw = strings.ReplaceAll(raw, "\t", " ")
|
||||||
idx := strings.Index(raw, "\n\n")
|
idx := strings.Index(raw, "\n\n")
|
||||||
if idx == -1 {
|
if idx == -1 {
|
||||||
return highlightHeaders(raw)
|
return highlightHeaders(raw)
|
||||||
@@ -74,8 +76,8 @@ func highlightHeaders(raw string) string {
|
|||||||
} else if trimmed == "" {
|
} else if trimmed == "" {
|
||||||
out.WriteString(line)
|
out.WriteString(line)
|
||||||
} else if idx := strings.Index(trimmed, ": "); idx != -1 {
|
} else if idx := strings.Index(trimmed, ": "); idx != -1 {
|
||||||
out.WriteString(Paint(S.Subtle, trimmed[:idx+2]))
|
out.WriteString(Paint(ilovetui.S.Subtle, trimmed[:idx+2]))
|
||||||
out.WriteString(Paint(S.Text, trimmed[idx+2:]))
|
out.WriteString(Paint(ilovetui.S.Text, trimmed[idx+2:]))
|
||||||
} else {
|
} else {
|
||||||
out.WriteString(line)
|
out.WriteString(line)
|
||||||
}
|
}
|
||||||
@@ -94,16 +96,16 @@ func highlightStatusLine(line string) string {
|
|||||||
switch parts[0] {
|
switch parts[0] {
|
||||||
case "GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "CONNECT", "TRACE":
|
case "GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "CONNECT", "TRACE":
|
||||||
result := S.Method(parts[0]).Width(0).Render(parts[0]) + " "
|
result := S.Method(parts[0]).Width(0).Render(parts[0]) + " "
|
||||||
result += Paint(S.Primary, parts[1])
|
result += Paint(ilovetui.S.Primary, parts[1])
|
||||||
if len(parts) == 3 {
|
if len(parts) == 3 {
|
||||||
result += " " + Paint(S.Subtle, parts[2])
|
result += " " + Paint(ilovetui.S.Subtle, parts[2])
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
result := Paint(S.Subtle, parts[0]) + " "
|
result := Paint(ilovetui.S.Subtle, parts[0]) + " "
|
||||||
result += Paint(S.Warning, parts[1])
|
result += Paint(ilovetui.S.Warning, parts[1])
|
||||||
if len(parts) == 3 {
|
if len(parts) == 3 {
|
||||||
result += " " + Paint(S.MutedFg, parts[2])
|
result += " " + Paint(ilovetui.S.Muted, parts[2])
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -133,9 +135,9 @@ func highlightJSON(s string) string {
|
|||||||
k++
|
k++
|
||||||
}
|
}
|
||||||
if k < n && s[k] == ':' {
|
if k < n && s[k] == ':' {
|
||||||
out.WriteString(Paint(S.Primary, str))
|
out.WriteString(Paint(ilovetui.S.Primary, str))
|
||||||
} else {
|
} else {
|
||||||
out.WriteString(Paint(S.Success, str))
|
out.WriteString(Paint(ilovetui.S.Success, str))
|
||||||
}
|
}
|
||||||
i = j
|
i = j
|
||||||
case (ch >= '0' && ch <= '9') || (ch == '-' && i+1 < n && s[i+1] >= '0' && s[i+1] <= '9'):
|
case (ch >= '0' && ch <= '9') || (ch == '-' && i+1 < n && s[i+1] >= '0' && s[i+1] <= '9'):
|
||||||
@@ -146,19 +148,19 @@ func highlightJSON(s string) string {
|
|||||||
for j < n && ((s[j] >= '0' && s[j] <= '9') || s[j] == '.' || s[j] == 'e' || s[j] == 'E' || s[j] == '+' || s[j] == '-') {
|
for j < n && ((s[j] >= '0' && s[j] <= '9') || s[j] == '.' || s[j] == 'e' || s[j] == 'E' || s[j] == '+' || s[j] == '-') {
|
||||||
j++
|
j++
|
||||||
}
|
}
|
||||||
out.WriteString(Paint(S.Warning, s[i:j]))
|
out.WriteString(Paint(ilovetui.S.Warning, s[i:j]))
|
||||||
i = j
|
i = j
|
||||||
case i+4 <= n && s[i:i+4] == "true":
|
case i+4 <= n && s[i:i+4] == "true":
|
||||||
out.WriteString(Paint(S.Error, "true"))
|
out.WriteString(Paint(ilovetui.S.Error, "true"))
|
||||||
i += 4
|
i += 4
|
||||||
case i+5 <= n && s[i:i+5] == "false":
|
case i+5 <= n && s[i:i+5] == "false":
|
||||||
out.WriteString(Paint(S.Error, "false"))
|
out.WriteString(Paint(ilovetui.S.Error, "false"))
|
||||||
i += 5
|
i += 5
|
||||||
case i+4 <= n && s[i:i+4] == "null":
|
case i+4 <= n && s[i:i+4] == "null":
|
||||||
out.WriteString(Paint(S.Error, "null"))
|
out.WriteString(Paint(ilovetui.S.Error, "null"))
|
||||||
i += 4
|
i += 4
|
||||||
case ch == '{' || ch == '}' || ch == '[' || ch == ']' || ch == ':' || ch == ',':
|
case ch == '{' || ch == '}' || ch == '[' || ch == ']' || ch == ':' || ch == ',':
|
||||||
out.WriteString(Paint(S.Subtle, string(ch)))
|
out.WriteString(Paint(ilovetui.S.Subtle, string(ch)))
|
||||||
i++
|
i++
|
||||||
default:
|
default:
|
||||||
out.WriteByte(ch)
|
out.WriteByte(ch)
|
||||||
@@ -264,11 +266,11 @@ func highlightHTML(s string) string {
|
|||||||
if i+4 <= n && s[i:i+4] == "<!--" {
|
if i+4 <= n && s[i:i+4] == "<!--" {
|
||||||
end := strings.Index(s[i:], "-->")
|
end := strings.Index(s[i:], "-->")
|
||||||
if end == -1 {
|
if end == -1 {
|
||||||
out.WriteString(Paint(S.Subtle, s[i:]))
|
out.WriteString(Paint(ilovetui.S.Subtle, s[i:]))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
end = i + end + 3
|
end = i + end + 3
|
||||||
out.WriteString(Paint(S.Subtle, s[i:end]))
|
out.WriteString(Paint(ilovetui.S.Subtle, s[i:end]))
|
||||||
i = end
|
i = end
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -277,10 +279,10 @@ func highlightHTML(s string) string {
|
|||||||
i++
|
i++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
out.WriteString(Paint(S.Subtle, "<"))
|
out.WriteString(Paint(ilovetui.S.Subtle, "<"))
|
||||||
i++
|
i++
|
||||||
if i < n && (s[i] == '/' || s[i] == '!') {
|
if i < n && (s[i] == '/' || s[i] == '!') {
|
||||||
out.WriteString(Paint(S.Subtle, string(s[i])))
|
out.WriteString(Paint(ilovetui.S.Subtle, string(s[i])))
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
j := i
|
j := i
|
||||||
@@ -288,7 +290,7 @@ func highlightHTML(s string) string {
|
|||||||
j++
|
j++
|
||||||
}
|
}
|
||||||
if j > i {
|
if j > i {
|
||||||
out.WriteString(Paint(S.Primary, s[i:j]))
|
out.WriteString(Paint(ilovetui.S.Primary, s[i:j]))
|
||||||
i = j
|
i = j
|
||||||
}
|
}
|
||||||
for i < n && s[i] != '>' {
|
for i < n && s[i] != '>' {
|
||||||
@@ -298,10 +300,10 @@ func highlightHTML(s string) string {
|
|||||||
out.WriteByte(ch)
|
out.WriteByte(ch)
|
||||||
i++
|
i++
|
||||||
case ch == '/':
|
case ch == '/':
|
||||||
out.WriteString(Paint(S.Subtle, "/"))
|
out.WriteString(Paint(ilovetui.S.Subtle, "/"))
|
||||||
i++
|
i++
|
||||||
case ch == '=':
|
case ch == '=':
|
||||||
out.WriteString(Paint(S.Subtle, "="))
|
out.WriteString(Paint(ilovetui.S.Subtle, "="))
|
||||||
i++
|
i++
|
||||||
case ch == '"' || ch == '\'':
|
case ch == '"' || ch == '\'':
|
||||||
q := ch
|
q := ch
|
||||||
@@ -312,19 +314,19 @@ func highlightHTML(s string) string {
|
|||||||
if j < n {
|
if j < n {
|
||||||
j++
|
j++
|
||||||
}
|
}
|
||||||
out.WriteString(Paint(S.Success, s[i:j]))
|
out.WriteString(Paint(ilovetui.S.Success, s[i:j]))
|
||||||
i = j
|
i = j
|
||||||
default:
|
default:
|
||||||
j = i
|
j = i
|
||||||
for j < n && s[j] != '=' && s[j] != ' ' && s[j] != '>' && s[j] != '/' && s[j] != '\t' && s[j] != '\n' {
|
for j < n && s[j] != '=' && s[j] != ' ' && s[j] != '>' && s[j] != '/' && s[j] != '\t' && s[j] != '\n' {
|
||||||
j++
|
j++
|
||||||
}
|
}
|
||||||
out.WriteString(Paint(S.Warning, s[i:j]))
|
out.WriteString(Paint(ilovetui.S.Warning, s[i:j]))
|
||||||
i = j
|
i = j
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if i < n && s[i] == '>' {
|
if i < n && s[i] == '>' {
|
||||||
out.WriteString(Paint(S.Subtle, ">"))
|
out.WriteString(Paint(ilovetui.S.Subtle, ">"))
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-77
@@ -1,106 +1,48 @@
|
|||||||
package style
|
package style
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"image/color"
|
|
||||||
|
|
||||||
"charm.land/bubbles/v2/help"
|
|
||||||
"charm.land/lipgloss/v2"
|
"charm.land/lipgloss/v2"
|
||||||
"github.com/anotherhadi/spilltea/internal/config"
|
ilovetui "github.com/anotherhadi/ilovetui"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Styles struct {
|
type Styles struct {
|
||||||
Primary color.Color
|
|
||||||
Success color.Color
|
|
||||||
Error color.Color
|
|
||||||
Warning color.Color
|
|
||||||
SubtleBg color.Color
|
|
||||||
Selection color.Color
|
|
||||||
Text color.Color
|
|
||||||
MutedFg color.Color
|
|
||||||
Subtle color.Color
|
|
||||||
|
|
||||||
Bold lipgloss.Style
|
|
||||||
Faint lipgloss.Style
|
|
||||||
|
|
||||||
Panel lipgloss.Style
|
|
||||||
PanelFocused lipgloss.Style
|
|
||||||
PanelEditing lipgloss.Style
|
PanelEditing lipgloss.Style
|
||||||
|
|
||||||
PagerDotActive string
|
methodGet lipgloss.Style
|
||||||
PagerDotInactive string
|
methodPost lipgloss.Style
|
||||||
|
methodPutPatch lipgloss.Style
|
||||||
|
methodDelete lipgloss.Style
|
||||||
|
methodDefault lipgloss.Style
|
||||||
}
|
}
|
||||||
|
|
||||||
var S *Styles
|
var S *Styles
|
||||||
|
|
||||||
func Init(cfg *config.Config) {
|
func Init() {
|
||||||
c := cfg.TUI.Colors
|
methodBase := lipgloss.NewStyle().Bold(true).Width(7)
|
||||||
|
|
||||||
subtleBg := lipgloss.Color("#" + c.Base01) // Lighter Background (status bars)
|
|
||||||
selection := lipgloss.Color("#" + c.Base02) // Selection Background
|
|
||||||
subtle := lipgloss.Color("#" + c.Base03) // Faint text, borders
|
|
||||||
mutedFg := lipgloss.Color("#" + c.Base04) // Muted foreground
|
|
||||||
text := lipgloss.Color("#" + c.Base05) // Default Foreground
|
|
||||||
errCol := lipgloss.Color("#" + c.Base08) // Red: errors
|
|
||||||
warning := lipgloss.Color("#" + c.Base09) // Orange: warnings
|
|
||||||
success := lipgloss.Color("#" + c.Base0B) // Green: success
|
|
||||||
primary := lipgloss.Color("#" + c.Base0D) // Accent: primary
|
|
||||||
purple := lipgloss.Color("#" + c.Base0E) // Purple: editing
|
|
||||||
|
|
||||||
S = &Styles{
|
S = &Styles{
|
||||||
Primary: primary,
|
|
||||||
Success: success,
|
|
||||||
Error: errCol,
|
|
||||||
Warning: warning,
|
|
||||||
SubtleBg: subtleBg,
|
|
||||||
Selection: selection,
|
|
||||||
MutedFg: mutedFg,
|
|
||||||
Text: text,
|
|
||||||
Subtle: subtle,
|
|
||||||
|
|
||||||
Bold: lipgloss.NewStyle().Bold(true),
|
|
||||||
Faint: lipgloss.NewStyle().Foreground(subtle).Faint(true),
|
|
||||||
|
|
||||||
Panel: lipgloss.NewStyle().
|
|
||||||
Border(lipgloss.RoundedBorder()).
|
|
||||||
BorderForeground(subtle),
|
|
||||||
|
|
||||||
PanelFocused: lipgloss.NewStyle().
|
|
||||||
Border(lipgloss.RoundedBorder()).
|
|
||||||
BorderForeground(primary),
|
|
||||||
|
|
||||||
PanelEditing: lipgloss.NewStyle().
|
PanelEditing: lipgloss.NewStyle().
|
||||||
Border(lipgloss.RoundedBorder()).
|
Border(lipgloss.RoundedBorder()).
|
||||||
BorderForeground(purple),
|
BorderForeground(ilovetui.S.Base0E),
|
||||||
|
|
||||||
PagerDotActive: lipgloss.NewStyle().Foreground(primary).SetString("•").String(),
|
methodGet: methodBase.Foreground(ilovetui.S.Success),
|
||||||
PagerDotInactive: lipgloss.NewStyle().Foreground(subtle).SetString("•").String(),
|
methodPost: methodBase.Foreground(ilovetui.S.Warning),
|
||||||
|
methodPutPatch: methodBase.Foreground(ilovetui.S.Primary),
|
||||||
|
methodDelete: methodBase.Foreground(ilovetui.S.Error),
|
||||||
|
methodDefault: methodBase.Foreground(ilovetui.S.Text),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHelp() help.Model {
|
|
||||||
h := help.New()
|
|
||||||
h.Styles.ShortKey = lipgloss.NewStyle().Foreground(S.Primary)
|
|
||||||
h.Styles.ShortDesc = lipgloss.NewStyle().Foreground(S.MutedFg)
|
|
||||||
h.Styles.ShortSeparator = lipgloss.NewStyle().Foreground(S.Subtle)
|
|
||||||
h.Styles.FullKey = lipgloss.NewStyle().Foreground(S.Primary)
|
|
||||||
h.Styles.FullDesc = lipgloss.NewStyle().Foreground(S.MutedFg)
|
|
||||||
h.Styles.FullSeparator = lipgloss.NewStyle().Foreground(S.Subtle)
|
|
||||||
h.Styles.Ellipsis = lipgloss.NewStyle().Foreground(S.Subtle)
|
|
||||||
return h
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Styles) Method(method string) lipgloss.Style {
|
func (s *Styles) Method(method string) lipgloss.Style {
|
||||||
base := lipgloss.NewStyle().Bold(true).Width(7)
|
|
||||||
switch method {
|
switch method {
|
||||||
case "GET":
|
case "GET":
|
||||||
return base.Foreground(s.Success)
|
return s.methodGet
|
||||||
case "POST":
|
case "POST":
|
||||||
return base.Foreground(s.Warning)
|
return s.methodPost
|
||||||
case "PUT", "PATCH":
|
case "PUT", "PATCH":
|
||||||
return base.Foreground(s.Primary)
|
return s.methodPutPatch
|
||||||
case "DELETE":
|
case "DELETE":
|
||||||
return base.Foreground(s.Error)
|
return s.methodDelete
|
||||||
default:
|
default:
|
||||||
return base.Foreground(s.Text)
|
return s.methodDefault
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ type Model struct {
|
|||||||
database *db.DB
|
database *db.DB
|
||||||
logFile *os.File
|
logFile *os.File
|
||||||
pluginManager *plugins.Manager
|
pluginManager *plugins.Manager
|
||||||
|
fatalErr error
|
||||||
|
|
||||||
width int
|
width int
|
||||||
height int
|
height int
|
||||||
@@ -106,6 +107,20 @@ func New(broker *intercept.Broker, name, path string) Model {
|
|||||||
m.findingsPage.SetDB(d)
|
m.findingsPage.SetDB(d)
|
||||||
mgr.SetDB(d)
|
mgr.SetDB(d)
|
||||||
|
|
||||||
|
pf, err := plugins.OpenPluginsFile(path)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("plugins file: %v", err)
|
||||||
|
}
|
||||||
|
mgr.SetPluginsFile(pf)
|
||||||
|
|
||||||
|
if len(cfg.Plugins) > 0 {
|
||||||
|
defaults := make(map[string]plugins.GlobalDefault, len(cfg.Plugins))
|
||||||
|
for id, gp := range cfg.Plugins {
|
||||||
|
defaults[id] = plugins.GlobalDefault{Enable: gp.Enable, Config: gp.Config}
|
||||||
|
}
|
||||||
|
mgr.SetGlobalDefaults(defaults)
|
||||||
|
}
|
||||||
|
|
||||||
pluginsDir := config.ExpandPath(cfg.App.PluginsDir)
|
pluginsDir := config.ExpandPath(cfg.App.PluginsDir)
|
||||||
if err := mgr.LoadFromDir(pluginsDir); err != nil {
|
if err := mgr.LoadFromDir(pluginsDir); err != nil {
|
||||||
log.Printf("plugins: %v", err)
|
log.Printf("plugins: %v", err)
|
||||||
@@ -121,6 +136,8 @@ func New(broker *intercept.Broker, name, path string) Model {
|
|||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m Model) FatalErr() error { return m.fatalErr }
|
||||||
|
|
||||||
func (m Model) Init() tea.Cmd {
|
func (m Model) Init() tea.Cmd {
|
||||||
mgr := m.pluginManager
|
mgr := m.pluginManager
|
||||||
return tea.Batch(
|
return tea.Batch(
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ type pageEntry struct {
|
|||||||
isEditing func(m *Model) bool
|
isEditing func(m *Model) bool
|
||||||
// resize propagates a new (w, h) to the page model.
|
// resize propagates a new (w, h) to the page model.
|
||||||
resize func(m *Model, w, h int)
|
resize func(m *Model, w, h int)
|
||||||
|
// hasUpdate reports whether the page has unseen updates.
|
||||||
|
hasUpdate func(m *Model) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var pageRegistry = []pageEntry{
|
var pageRegistry = []pageEntry{
|
||||||
@@ -52,6 +54,7 @@ var pageRegistry = []pageEntry{
|
|||||||
},
|
},
|
||||||
isEditing: func(m *Model) bool { return m.intercept.IsEditing() },
|
isEditing: func(m *Model) bool { return m.intercept.IsEditing() },
|
||||||
resize: func(m *Model, w, h int) { m.intercept.SetSize(w, h) },
|
resize: func(m *Model, w, h int) { m.intercept.SetSize(w, h) },
|
||||||
|
hasUpdate: func(m *Model) bool { return m.intercept.HasUnread() },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: pageHistory,
|
id: pageHistory,
|
||||||
@@ -114,7 +117,8 @@ var pageRegistry = []pageEntry{
|
|||||||
m.findingsPage = updated.(findingsUI.Model)
|
m.findingsPage = updated.(findingsUI.Model)
|
||||||
return cmd
|
return cmd
|
||||||
},
|
},
|
||||||
resize: func(m *Model, w, h int) { m.findingsPage.SetSize(w, h) },
|
resize: func(m *Model, w, h int) { m.findingsPage.SetSize(w, h) },
|
||||||
|
hasUpdate: func(m *Model) bool { return m.findingsPage.HasUnread() },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: pageDocs,
|
id: pageDocs,
|
||||||
|
|||||||
+13
-10
@@ -5,7 +5,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"charm.land/lipgloss/v2"
|
"charm.land/lipgloss/v2"
|
||||||
"github.com/anotherhadi/spilltea/internal/style"
|
ilovetui "github.com/anotherhadi/ilovetui"
|
||||||
)
|
)
|
||||||
|
|
||||||
type sidebarState string
|
type sidebarState string
|
||||||
@@ -42,7 +42,6 @@ func (m *Model) renderSidebar() string {
|
|||||||
if m.sidebarState == sidebarHidden {
|
if m.sidebarState == sidebarHidden {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
s := style.S
|
|
||||||
// content width inside bordered panel
|
// content width inside bordered panel
|
||||||
inner := m.getSidebarWidth() - 2
|
inner := m.getSidebarWidth() - 2
|
||||||
|
|
||||||
@@ -50,22 +49,26 @@ func (m *Model) renderSidebar() string {
|
|||||||
if m.sidebarState == sidebarCollapsed {
|
if m.sidebarState == sidebarCollapsed {
|
||||||
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(ilovetui.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(ilovetui.S.Primary).Bold(true)
|
||||||
badgeNormal := lipgloss.NewStyle().Foreground(s.Subtle)
|
badgeNormal := lipgloss.NewStyle().Foreground(ilovetui.S.Subtle)
|
||||||
textSelected := lipgloss.NewStyle().Foreground(s.Primary)
|
textSelected := lipgloss.NewStyle().Foreground(ilovetui.S.Primary)
|
||||||
textNormal := lipgloss.NewStyle().Foreground(s.Text)
|
textNormal := lipgloss.NewStyle().Foreground(ilovetui.S.Text)
|
||||||
lineStyle := lipgloss.NewStyle().Width(inner).Padding(0, 1)
|
lineStyle := lipgloss.NewStyle().Width(inner).Padding(0, 1)
|
||||||
|
|
||||||
var items strings.Builder
|
var items strings.Builder
|
||||||
|
badgeUnread := lipgloss.NewStyle().Foreground(ilovetui.S.Warning).Bold(true)
|
||||||
|
|
||||||
for i, entry := range sidebarEntries {
|
for i, entry := range sidebarEntries {
|
||||||
selected := entry.id == m.page
|
selected := entry.id == m.page
|
||||||
badgeStyle, textStyle := badgeNormal, textNormal
|
badgeStyle, textStyle := badgeNormal, textNormal
|
||||||
if selected {
|
if selected {
|
||||||
badgeStyle, textStyle = badgeSelected, textSelected
|
badgeStyle, textStyle = badgeSelected, textSelected
|
||||||
|
} else if entry.hasUpdate != nil && entry.hasUpdate(m) {
|
||||||
|
badgeStyle = badgeUnread
|
||||||
}
|
}
|
||||||
icon := ""
|
icon := ""
|
||||||
if entry.icon != nil {
|
if entry.icon != nil {
|
||||||
@@ -91,13 +94,13 @@ func (m *Model) renderSidebar() string {
|
|||||||
}
|
}
|
||||||
parts := []string{
|
parts := []string{
|
||||||
title,
|
title,
|
||||||
lipgloss.NewStyle().Width(inner).Foreground(s.Subtle).Padding(0, 1).Render(name),
|
lipgloss.NewStyle().Width(inner).Foreground(ilovetui.S.Subtle).Padding(0, 1).Render(name),
|
||||||
}
|
}
|
||||||
parts = append(parts,
|
parts = append(parts,
|
||||||
lipgloss.NewStyle().Foreground(s.Subtle).Render(divider),
|
lipgloss.NewStyle().Foreground(ilovetui.S.Subtle).Render(divider),
|
||||||
items.String(),
|
items.String(),
|
||||||
)
|
)
|
||||||
body := lipgloss.JoinVertical(lipgloss.Left, parts...)
|
body := lipgloss.JoinVertical(lipgloss.Left, parts...)
|
||||||
|
|
||||||
return s.Panel.Width(m.getSidebarWidth()).Height(m.height).Render(body)
|
return ilovetui.S.Panel.Width(m.getSidebarWidth()).Height(m.height).Render(body)
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-10
@@ -37,6 +37,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
case intercept.RequestArrivedMsg:
|
case intercept.RequestArrivedMsg:
|
||||||
updated, cmd := m.intercept.Update(msg)
|
updated, cmd := m.intercept.Update(msg)
|
||||||
m.intercept = updated.(interceptUI.Model)
|
m.intercept = updated.(interceptUI.Model)
|
||||||
|
if m.page == pageIntercept {
|
||||||
|
m.intercept.ClearUnread()
|
||||||
|
}
|
||||||
return m, tea.Batch(cmd, intercept.WaitForRequest(m.broker))
|
return m, tea.Batch(cmd, intercept.WaitForRequest(m.broker))
|
||||||
case intercept.ResponseArrivedMsg:
|
case intercept.ResponseArrivedMsg:
|
||||||
updated, cmd := m.intercept.Update(msg)
|
updated, cmd := m.intercept.Update(msg)
|
||||||
@@ -104,16 +107,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
case proxyPkg.ErrMsg:
|
case proxyPkg.ErrMsg:
|
||||||
if msg.Err != nil {
|
if msg.Err != nil {
|
||||||
log.Printf("proxy error: %v", msg.Err)
|
log.Printf("proxy error: %v", msg.Err)
|
||||||
return m, tea.Batch(
|
m.fatalErr = msg.Err
|
||||||
func() tea.Msg {
|
return m, tea.Quit
|
||||||
return notificationsUI.NotificationMsg{
|
|
||||||
Title: "Proxy Error",
|
|
||||||
Body: msg.Err.Error(),
|
|
||||||
Kind: notificationsUI.KindError,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
tea.Quit,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
||||||
@@ -129,6 +124,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
case findingsUI.FindingsLoadedMsg:
|
case findingsUI.FindingsLoadedMsg:
|
||||||
updated, cmd := m.findingsPage.Update(msg)
|
updated, cmd := m.findingsPage.Update(msg)
|
||||||
m.findingsPage = updated.(findingsUI.Model)
|
m.findingsPage = updated.(findingsUI.Model)
|
||||||
|
if m.page == pageFindings {
|
||||||
|
m.findingsPage.ClearUnread()
|
||||||
|
}
|
||||||
return m, cmd
|
return m, cmd
|
||||||
|
|
||||||
case replayUI.SendToReplayMsg:
|
case replayUI.SendToReplayMsg:
|
||||||
@@ -258,8 +256,12 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, m.history.RefreshCmd()
|
return m, m.history.RefreshCmd()
|
||||||
}
|
}
|
||||||
if p == pageFindings {
|
if p == pageFindings {
|
||||||
|
m.findingsPage.ClearUnread()
|
||||||
return m, findingsUI.RefreshCmd(m.database)
|
return m, findingsUI.RefreshCmd(m.database)
|
||||||
}
|
}
|
||||||
|
if p == pageIntercept {
|
||||||
|
m.intercept.ClearUnread()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package app
|
|||||||
import (
|
import (
|
||||||
tea "charm.land/bubbletea/v2"
|
tea "charm.land/bubbletea/v2"
|
||||||
"charm.land/lipgloss/v2"
|
"charm.land/lipgloss/v2"
|
||||||
"github.com/anotherhadi/spilltea/internal/style"
|
ilovetui "github.com/anotherhadi/ilovetui"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (m Model) View() tea.View {
|
func (m Model) View() tea.View {
|
||||||
@@ -52,5 +52,5 @@ func (m *Model) renderActivePage() string {
|
|||||||
return e.render(m)
|
return e.render(m)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return style.S.Faint.Render("Work in progress")
|
return ilovetui.S.Faint.Render("Work in progress")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"charm.land/bubbles/v2/list"
|
"charm.land/bubbles/v2/list"
|
||||||
tea "charm.land/bubbletea/v2"
|
tea "charm.land/bubbletea/v2"
|
||||||
"charm.land/lipgloss/v2"
|
"charm.land/lipgloss/v2"
|
||||||
"github.com/anotherhadi/spilltea/internal/style"
|
ilovetui "github.com/anotherhadi/ilovetui"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -14,7 +14,6 @@ const (
|
|||||||
popupH = 20
|
popupH = 20
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
type OpenMsg struct {
|
type OpenMsg struct {
|
||||||
RawRequest string
|
RawRequest string
|
||||||
Scheme string
|
Scheme string
|
||||||
@@ -48,20 +47,18 @@ type Model struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func New() Model {
|
func New() Model {
|
||||||
s := style.S
|
|
||||||
|
|
||||||
delegate := list.NewDefaultDelegate()
|
delegate := list.NewDefaultDelegate()
|
||||||
delegate.SetSpacing(0)
|
delegate.SetSpacing(0)
|
||||||
delegate.Styles.NormalTitle = lipgloss.NewStyle().Foreground(s.Text).PaddingLeft(2)
|
delegate.Styles.NormalTitle = lipgloss.NewStyle().Foreground(ilovetui.S.Text).PaddingLeft(2)
|
||||||
delegate.Styles.NormalDesc = lipgloss.NewStyle().Foreground(s.Subtle).PaddingLeft(2)
|
delegate.Styles.NormalDesc = lipgloss.NewStyle().Foreground(ilovetui.S.Subtle).PaddingLeft(2)
|
||||||
delegate.Styles.SelectedTitle = lipgloss.NewStyle().
|
delegate.Styles.SelectedTitle = lipgloss.NewStyle().
|
||||||
Border(lipgloss.NormalBorder(), false, false, false, true).
|
Border(lipgloss.NormalBorder(), false, false, false, true).
|
||||||
BorderForeground(s.Primary).
|
BorderForeground(ilovetui.S.Primary).
|
||||||
Foreground(s.Primary).Bold(true).PaddingLeft(1)
|
Foreground(ilovetui.S.Primary).Bold(true).PaddingLeft(1)
|
||||||
delegate.Styles.SelectedDesc = lipgloss.NewStyle().
|
delegate.Styles.SelectedDesc = lipgloss.NewStyle().
|
||||||
Border(lipgloss.NormalBorder(), false, false, false, true).
|
Border(lipgloss.NormalBorder(), false, false, false, true).
|
||||||
BorderForeground(s.Primary).
|
BorderForeground(ilovetui.S.Primary).
|
||||||
Foreground(s.MutedFg).PaddingLeft(1)
|
Foreground(ilovetui.S.Muted).PaddingLeft(1)
|
||||||
|
|
||||||
l := list.New(allItems, delegate, popupW, 8)
|
l := list.New(allItems, delegate, popupW, 8)
|
||||||
l.SetShowTitle(false)
|
l.SetShowTitle(false)
|
||||||
@@ -126,7 +123,7 @@ func (m Model) popupHeight() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) listHeight() int {
|
func (m Model) listHeight() int {
|
||||||
return style.PanelContentH(m.popupHeight()) - 1
|
return ilovetui.ContentHeight(m.popupHeight()) - 1
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) extract(id string) string {
|
func (m Model) extract(id string) string {
|
||||||
|
|||||||
@@ -2,14 +2,12 @@ package copy
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"charm.land/lipgloss/v2"
|
"charm.land/lipgloss/v2"
|
||||||
"github.com/anotherhadi/spilltea/internal/style"
|
ilovetui "github.com/anotherhadi/ilovetui"
|
||||||
copyasUI "github.com/anotherhadi/spilltea/internal/ui/components/copyas"
|
copyasUI "github.com/anotherhadi/spilltea/internal/ui/components/copyas"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (m *Model) View(background string) string {
|
func (m *Model) View(background string) string {
|
||||||
s := style.S
|
hint := lipgloss.NewStyle().Foreground(ilovetui.S.Subtle).
|
||||||
|
|
||||||
hint := lipgloss.NewStyle().Foreground(s.Subtle).
|
|
||||||
Render(" enter: copy • /: filter • esc: cancel")
|
Render(" enter: copy • /: filter • esc: cancel")
|
||||||
|
|
||||||
inner := lipgloss.JoinVertical(lipgloss.Left,
|
inner := lipgloss.JoinVertical(lipgloss.Left,
|
||||||
@@ -19,10 +17,10 @@ func (m *Model) View(background string) string {
|
|||||||
|
|
||||||
border := lipgloss.NewStyle().
|
border := lipgloss.NewStyle().
|
||||||
Border(lipgloss.RoundedBorder()).
|
Border(lipgloss.RoundedBorder()).
|
||||||
BorderForeground(s.Primary)
|
BorderForeground(ilovetui.S.Primary)
|
||||||
|
|
||||||
popupH := m.popupHeight()
|
popupH := m.popupHeight()
|
||||||
popup := style.RenderWithTitle(border, "Copy", inner, m.popupInnerWidth()+2, popupH)
|
popup := ilovetui.RenderWithTitle(border, "Copy", inner, m.popupInnerWidth()+2, popupH)
|
||||||
|
|
||||||
return copyasUI.OverlayCenter(background, popup, m.width, m.height)
|
return copyasUI.OverlayCenter(background, popup, m.width, m.height)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"charm.land/bubbles/v2/list"
|
"charm.land/bubbles/v2/list"
|
||||||
tea "charm.land/bubbletea/v2"
|
tea "charm.land/bubbletea/v2"
|
||||||
"charm.land/lipgloss/v2"
|
"charm.land/lipgloss/v2"
|
||||||
"github.com/anotherhadi/spilltea/internal/style"
|
ilovetui "github.com/anotherhadi/ilovetui"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -12,7 +12,6 @@ const (
|
|||||||
popupH = 20
|
popupH = 20
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
type OpenMsg struct {
|
type OpenMsg struct {
|
||||||
RawRequest string
|
RawRequest string
|
||||||
Scheme string
|
Scheme string
|
||||||
@@ -49,20 +48,18 @@ type Model struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func New() Model {
|
func New() Model {
|
||||||
s := style.S
|
|
||||||
|
|
||||||
delegate := list.NewDefaultDelegate()
|
delegate := list.NewDefaultDelegate()
|
||||||
delegate.SetSpacing(0)
|
delegate.SetSpacing(0)
|
||||||
delegate.Styles.NormalTitle = lipgloss.NewStyle().Foreground(s.Text).PaddingLeft(2)
|
delegate.Styles.NormalTitle = lipgloss.NewStyle().Foreground(ilovetui.S.Text).PaddingLeft(2)
|
||||||
delegate.Styles.NormalDesc = lipgloss.NewStyle().Foreground(s.Subtle).PaddingLeft(2)
|
delegate.Styles.NormalDesc = lipgloss.NewStyle().Foreground(ilovetui.S.Subtle).PaddingLeft(2)
|
||||||
delegate.Styles.SelectedTitle = lipgloss.NewStyle().
|
delegate.Styles.SelectedTitle = lipgloss.NewStyle().
|
||||||
Border(lipgloss.NormalBorder(), false, false, false, true).
|
Border(lipgloss.NormalBorder(), false, false, false, true).
|
||||||
BorderForeground(s.Primary).
|
BorderForeground(ilovetui.S.Primary).
|
||||||
Foreground(s.Primary).Bold(true).PaddingLeft(1)
|
Foreground(ilovetui.S.Primary).Bold(true).PaddingLeft(1)
|
||||||
delegate.Styles.SelectedDesc = lipgloss.NewStyle().
|
delegate.Styles.SelectedDesc = lipgloss.NewStyle().
|
||||||
Border(lipgloss.NormalBorder(), false, false, false, true).
|
Border(lipgloss.NormalBorder(), false, false, false, true).
|
||||||
BorderForeground(s.Primary).
|
BorderForeground(ilovetui.S.Primary).
|
||||||
Foreground(s.MutedFg).PaddingLeft(1)
|
Foreground(ilovetui.S.Muted).PaddingLeft(1)
|
||||||
|
|
||||||
l := list.New(allFormats, delegate, popupW, 8)
|
l := list.New(allFormats, delegate, popupW, 8)
|
||||||
l.SetShowTitle(false)
|
l.SetShowTitle(false)
|
||||||
@@ -117,5 +114,5 @@ func (m Model) popupHeight() int {
|
|||||||
|
|
||||||
// listHeight = panel content area - hint line (1)
|
// listHeight = panel content area - hint line (1)
|
||||||
func (m Model) listHeight() int {
|
func (m Model) listHeight() int {
|
||||||
return style.PanelContentH(m.popupHeight()) - 1
|
return ilovetui.ContentHeight(m.popupHeight()) - 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,12 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"charm.land/lipgloss/v2"
|
"charm.land/lipgloss/v2"
|
||||||
"github.com/anotherhadi/spilltea/internal/style"
|
ilovetui "github.com/anotherhadi/ilovetui"
|
||||||
"github.com/charmbracelet/x/ansi"
|
"github.com/charmbracelet/x/ansi"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (m *Model) View(background string) string {
|
func (m *Model) View(background string) string {
|
||||||
s := style.S
|
hint := lipgloss.NewStyle().Foreground(ilovetui.S.Subtle).
|
||||||
|
|
||||||
hint := lipgloss.NewStyle().Foreground(s.Subtle).
|
|
||||||
Render(" enter: copy • /: filter • esc: cancel")
|
Render(" enter: copy • /: filter • esc: cancel")
|
||||||
|
|
||||||
inner := lipgloss.JoinVertical(lipgloss.Left,
|
inner := lipgloss.JoinVertical(lipgloss.Left,
|
||||||
@@ -21,17 +19,15 @@ func (m *Model) View(background string) string {
|
|||||||
|
|
||||||
border := lipgloss.NewStyle().
|
border := lipgloss.NewStyle().
|
||||||
Border(lipgloss.RoundedBorder()).
|
Border(lipgloss.RoundedBorder()).
|
||||||
BorderForeground(s.Primary)
|
BorderForeground(ilovetui.S.Primary)
|
||||||
|
|
||||||
popupH := m.popupHeight()
|
popupH := m.popupHeight()
|
||||||
popup := style.RenderWithTitle(border, "Copy as", inner, m.popupInnerWidth()+2, popupH)
|
popup := ilovetui.RenderWithTitle(border, "Copy as", inner, m.popupInnerWidth()+2, popupH)
|
||||||
|
|
||||||
return OverlayCenter(background, popup, m.width, m.height)
|
return OverlayCenter(background, popup, m.width, m.height)
|
||||||
}
|
}
|
||||||
|
|
||||||
func OverlayCenter(bg, popup string, w, h int) string {
|
func OverlayCenter(bg, popup string, w, h int) string {
|
||||||
s := style.S
|
|
||||||
|
|
||||||
stripped := ansi.Strip(bg)
|
stripped := ansi.Strip(bg)
|
||||||
rawLines := strings.Split(stripped, "\n")
|
rawLines := strings.Split(stripped, "\n")
|
||||||
bgRunes := make([][]rune, h)
|
bgRunes := make([][]rune, h)
|
||||||
@@ -67,7 +63,7 @@ func OverlayCenter(bg, popup string, w, h int) string {
|
|||||||
startX = 0
|
startX = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
dim := lipgloss.NewStyle().Foreground(s.Subtle).Faint(true)
|
dim := lipgloss.NewStyle().Foreground(ilovetui.S.Subtle).Faint(true)
|
||||||
|
|
||||||
result := make([]string, h)
|
result := make([]string, h)
|
||||||
for y := 0; y < h; y++ {
|
for y := 0; y < h; y++ {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
tea "charm.land/bubbletea/v2"
|
tea "charm.land/bubbletea/v2"
|
||||||
"charm.land/lipgloss/v2"
|
"charm.land/lipgloss/v2"
|
||||||
"github.com/anotherhadi/spilltea/internal/style"
|
ilovetui "github.com/anotherhadi/ilovetui"
|
||||||
"github.com/charmbracelet/x/ansi"
|
"github.com/charmbracelet/x/ansi"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -76,7 +76,6 @@ func (m Model) View(background string) string {
|
|||||||
return background
|
return background
|
||||||
}
|
}
|
||||||
|
|
||||||
s := style.S
|
|
||||||
const popupW = 34
|
const popupW = 34
|
||||||
|
|
||||||
var popups []string
|
var popups []string
|
||||||
@@ -89,17 +88,17 @@ func (m Model) View(background string) string {
|
|||||||
var accent color.Color
|
var accent color.Color
|
||||||
switch n.kind {
|
switch n.kind {
|
||||||
case KindSuccess:
|
case KindSuccess:
|
||||||
accent = s.Success
|
accent = ilovetui.S.Success
|
||||||
case KindWarning:
|
case KindWarning:
|
||||||
accent = s.Warning
|
accent = ilovetui.S.Warning
|
||||||
case KindError:
|
case KindError:
|
||||||
accent = s.Error
|
accent = ilovetui.S.Error
|
||||||
default:
|
default:
|
||||||
accent = s.Primary
|
accent = ilovetui.S.Primary
|
||||||
}
|
}
|
||||||
|
|
||||||
titleStr := lipgloss.NewStyle().Foreground(accent).Bold(true).Render(n.title)
|
titleStr := lipgloss.NewStyle().Foreground(accent).Bold(true).Render(n.title)
|
||||||
bodyStr := lipgloss.NewStyle().Foreground(s.Text).Width(popupW).Render(n.body)
|
bodyStr := lipgloss.NewStyle().Foreground(ilovetui.S.Text).Width(popupW).Render(n.body)
|
||||||
|
|
||||||
inner := lipgloss.JoinVertical(lipgloss.Left, titleStr, bodyStr)
|
inner := lipgloss.JoinVertical(lipgloss.Left, titleStr, bodyStr)
|
||||||
box := lipgloss.NewStyle().
|
box := lipgloss.NewStyle().
|
||||||
|
|||||||
+10
-12
@@ -8,6 +8,7 @@ import (
|
|||||||
"charm.land/bubbles/v2/viewport"
|
"charm.land/bubbles/v2/viewport"
|
||||||
tea "charm.land/bubbletea/v2"
|
tea "charm.land/bubbletea/v2"
|
||||||
"charm.land/lipgloss/v2"
|
"charm.land/lipgloss/v2"
|
||||||
|
ilovetui "github.com/anotherhadi/ilovetui"
|
||||||
"github.com/anotherhadi/spilltea/internal/keys"
|
"github.com/anotherhadi/spilltea/internal/keys"
|
||||||
"github.com/anotherhadi/spilltea/internal/style"
|
"github.com/anotherhadi/spilltea/internal/style"
|
||||||
"github.com/anotherhadi/spilltea/internal/util"
|
"github.com/anotherhadi/spilltea/internal/util"
|
||||||
@@ -85,10 +86,9 @@ func wordDiff(leftLine, rightLine string) (leftRendered, rightRendered string) {
|
|||||||
segs[lo], segs[hi] = segs[hi], segs[lo]
|
segs[lo], segs[hi] = segs[hi], segs[lo]
|
||||||
}
|
}
|
||||||
|
|
||||||
s := style.S
|
boldErr := lipgloss.NewStyle().Foreground(ilovetui.S.Error).Bold(true)
|
||||||
boldErr := lipgloss.NewStyle().Foreground(s.Error).Bold(true)
|
boldOk := lipgloss.NewStyle().Foreground(ilovetui.S.Success).Bold(true)
|
||||||
boldOk := lipgloss.NewStyle().Foreground(s.Success).Bold(true)
|
dim := lipgloss.NewStyle().Foreground(ilovetui.S.Subtle)
|
||||||
dim := lipgloss.NewStyle().Foreground(s.Subtle)
|
|
||||||
|
|
||||||
var lb, rb strings.Builder
|
var lb, rb strings.Builder
|
||||||
for _, seg := range segs {
|
for _, seg := range segs {
|
||||||
@@ -212,9 +212,9 @@ type Model struct {
|
|||||||
|
|
||||||
func New() Model {
|
func New() Model {
|
||||||
return Model{
|
return Model{
|
||||||
leftViewport: style.NewViewport(),
|
leftViewport: ilovetui.NewViewport(),
|
||||||
rightViewport: style.NewViewport(),
|
rightViewport: ilovetui.NewViewport(),
|
||||||
help: style.NewHelp(),
|
help: ilovetui.NewHelp(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,7 +255,7 @@ func (m *Model) recalcSizes() {
|
|||||||
rightInner = 0
|
rightInner = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
viewportH := style.PanelContentH(panelH)
|
viewportH := ilovetui.ContentHeight(panelH)
|
||||||
|
|
||||||
m.leftViewport.SetWidth(leftInner)
|
m.leftViewport.SetWidth(leftInner)
|
||||||
m.leftViewport.SetHeight(viewportH)
|
m.leftViewport.SetHeight(viewportH)
|
||||||
@@ -296,13 +296,11 @@ func hlLines(raw string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) refreshViewports() {
|
func (m *Model) refreshViewports() {
|
||||||
s := style.S
|
|
||||||
|
|
||||||
if m.left.raw == "" {
|
if m.left.raw == "" {
|
||||||
placeholder := lipgloss.Place(
|
placeholder := lipgloss.Place(
|
||||||
m.leftViewport.Width(), m.leftViewport.Height(),
|
m.leftViewport.Width(), m.leftViewport.Height(),
|
||||||
lipgloss.Center, lipgloss.Center,
|
lipgloss.Center, lipgloss.Center,
|
||||||
s.Faint.Render(util.CenterLines("<(^_^)>", "send two entries here to compare")),
|
ilovetui.S.Faint.Render(util.CenterLines("<(^_^)>", "send two entries here to compare")),
|
||||||
)
|
)
|
||||||
m.leftViewport.SetContent(placeholder)
|
m.leftViewport.SetContent(placeholder)
|
||||||
m.rightViewport.SetContent("")
|
m.rightViewport.SetContent("")
|
||||||
@@ -314,7 +312,7 @@ func (m *Model) refreshViewports() {
|
|||||||
placeholder := lipgloss.Place(
|
placeholder := lipgloss.Place(
|
||||||
m.rightViewport.Width(), m.rightViewport.Height(),
|
m.rightViewport.Width(), m.rightViewport.Height(),
|
||||||
lipgloss.Center, lipgloss.Center,
|
lipgloss.Center, lipgloss.Center,
|
||||||
s.Faint.Render(util.CenterLines("(・3・)", "waiting for second entry…")),
|
ilovetui.S.Faint.Render(util.CenterLines("(・3・)", "waiting for second entry…")),
|
||||||
)
|
)
|
||||||
m.rightViewport.SetContent(placeholder)
|
m.rightViewport.SetContent(placeholder)
|
||||||
return
|
return
|
||||||
|
|||||||
+11
-14
@@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
tea "charm.land/bubbletea/v2"
|
tea "charm.land/bubbletea/v2"
|
||||||
"charm.land/lipgloss/v2"
|
"charm.land/lipgloss/v2"
|
||||||
|
ilovetui "github.com/anotherhadi/ilovetui"
|
||||||
"github.com/anotherhadi/spilltea/internal/icons"
|
"github.com/anotherhadi/spilltea/internal/icons"
|
||||||
"github.com/anotherhadi/spilltea/internal/style"
|
"github.com/anotherhadi/spilltea/internal/style"
|
||||||
"github.com/charmbracelet/x/ansi"
|
"github.com/charmbracelet/x/ansi"
|
||||||
@@ -26,8 +27,6 @@ func (m Model) View() tea.View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) renderPanels(panelH int) string {
|
func (m *Model) renderPanels(panelH int) string {
|
||||||
s := style.S
|
|
||||||
|
|
||||||
leftW := m.width / 2
|
leftW := m.width / 2
|
||||||
rightW := m.width - leftW
|
rightW := m.width - leftW
|
||||||
|
|
||||||
@@ -46,20 +45,20 @@ func (m *Model) renderPanels(panelH int) string {
|
|||||||
rightTitle = ansi.Truncate(rightTitle, maxW, "…")
|
rightTitle = ansi.Truncate(rightTitle, maxW, "…")
|
||||||
}
|
}
|
||||||
|
|
||||||
leftBorder := s.Panel
|
leftBorder := ilovetui.S.Panel
|
||||||
rightBorder := s.Panel
|
rightBorder := ilovetui.S.Panel
|
||||||
switch m.focus {
|
switch m.focus {
|
||||||
case bothSlots:
|
case bothSlots:
|
||||||
leftBorder = s.PanelFocused
|
leftBorder = ilovetui.S.PanelFocused
|
||||||
rightBorder = s.PanelFocused
|
rightBorder = ilovetui.S.PanelFocused
|
||||||
case leftSlot:
|
case leftSlot:
|
||||||
leftBorder = s.PanelFocused
|
leftBorder = ilovetui.S.PanelFocused
|
||||||
case rightSlot:
|
case rightSlot:
|
||||||
rightBorder = s.PanelFocused
|
rightBorder = ilovetui.S.PanelFocused
|
||||||
}
|
}
|
||||||
|
|
||||||
left := style.RenderWithTitle(leftBorder, leftTitle, m.leftViewport.View(), leftW, panelH)
|
left := ilovetui.RenderWithTitle(leftBorder, leftTitle, ilovetui.ViewportView(&m.leftViewport), leftW, panelH)
|
||||||
right := style.RenderWithTitle(rightBorder, rightTitle, m.rightViewport.View(), rightW, panelH)
|
right := ilovetui.RenderWithTitle(rightBorder, rightTitle, ilovetui.ViewportView(&m.rightViewport), rightW, panelH)
|
||||||
|
|
||||||
return lipgloss.JoinHorizontal(lipgloss.Top, left, right)
|
return lipgloss.JoinHorizontal(lipgloss.Top, left, right)
|
||||||
}
|
}
|
||||||
@@ -69,12 +68,11 @@ func (m *Model) renderStatusBar() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func renderLeftLines(lines []diffLine) string {
|
func renderLeftLines(lines []diffLine) string {
|
||||||
s := style.S
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
for _, l := range lines {
|
for _, l := range lines {
|
||||||
switch l.kind {
|
switch l.kind {
|
||||||
case lineRemoved:
|
case lineRemoved:
|
||||||
sb.WriteString(style.Paint(s.Error, "- ") + l.text + "\n")
|
sb.WriteString(style.Paint(ilovetui.S.Error, "- ") + l.text + "\n")
|
||||||
case lineAdded:
|
case lineAdded:
|
||||||
sb.WriteString("\n")
|
sb.WriteString("\n")
|
||||||
default:
|
default:
|
||||||
@@ -85,12 +83,11 @@ func renderLeftLines(lines []diffLine) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func renderRightLines(lines []diffLine) string {
|
func renderRightLines(lines []diffLine) string {
|
||||||
s := style.S
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
for _, l := range lines {
|
for _, l := range lines {
|
||||||
switch l.kind {
|
switch l.kind {
|
||||||
case lineAdded:
|
case lineAdded:
|
||||||
sb.WriteString(style.Paint(s.Success, "+ ") + l.text + "\n")
|
sb.WriteString(style.Paint(ilovetui.S.Success, "+ ") + l.text + "\n")
|
||||||
case lineRemoved:
|
case lineRemoved:
|
||||||
sb.WriteString("\n")
|
sb.WriteString("\n")
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ import (
|
|||||||
"charm.land/bubbles/v2/viewport"
|
"charm.land/bubbles/v2/viewport"
|
||||||
tea "charm.land/bubbletea/v2"
|
tea "charm.land/bubbletea/v2"
|
||||||
"charm.land/lipgloss/v2"
|
"charm.land/lipgloss/v2"
|
||||||
|
ilovetui "github.com/anotherhadi/ilovetui"
|
||||||
"github.com/anotherhadi/spilltea/internal/keys"
|
"github.com/anotherhadi/spilltea/internal/keys"
|
||||||
"github.com/anotherhadi/spilltea/internal/style"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func readDoc(name string) string {
|
func readDoc(name string) string {
|
||||||
@@ -58,12 +58,12 @@ func New() Model {
|
|||||||
ti := textinput.New()
|
ti := textinput.New()
|
||||||
ti.Prompt = "/"
|
ti.Prompt = "/"
|
||||||
s := ti.Styles()
|
s := ti.Styles()
|
||||||
s.Focused.Prompt = lipgloss.NewStyle().Foreground(style.S.Primary)
|
s.Focused.Prompt = lipgloss.NewStyle().Foreground(ilovetui.S.Primary)
|
||||||
ti.SetStyles(s)
|
ti.SetStyles(s)
|
||||||
|
|
||||||
return Model{
|
return Model{
|
||||||
viewport: viewport.New(),
|
viewport: viewport.New(),
|
||||||
help: style.NewHelp(),
|
help: ilovetui.NewHelp(),
|
||||||
searchInput: ti,
|
searchInput: ti,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,8 +182,8 @@ func injectHighlightsInLine(ansiLine string, intervals [][]int, currentIdx int)
|
|||||||
return ansiLine
|
return ansiLine
|
||||||
}
|
}
|
||||||
|
|
||||||
normalOpen, normalClose := lipglossAnsiCodes(lipgloss.NewStyle().Background(style.S.SubtleBg))
|
normalOpen, normalClose := lipglossAnsiCodes(lipgloss.NewStyle().Background(ilovetui.S.SubtleBg))
|
||||||
currentOpen, currentClose := lipglossAnsiCodes(lipgloss.NewStyle().Background(style.S.Primary).Foreground(style.S.Text))
|
currentOpen, currentClose := lipglossAnsiCodes(lipgloss.NewStyle().Background(ilovetui.S.Primary).Foreground(ilovetui.S.Text))
|
||||||
|
|
||||||
type injection struct {
|
type injection struct {
|
||||||
visPos int
|
visPos int
|
||||||
|
|||||||
@@ -9,15 +9,15 @@ import (
|
|||||||
tea "charm.land/bubbletea/v2"
|
tea "charm.land/bubbletea/v2"
|
||||||
"charm.land/glamour/v2"
|
"charm.land/glamour/v2"
|
||||||
"charm.land/lipgloss/v2"
|
"charm.land/lipgloss/v2"
|
||||||
|
ilovetui "github.com/anotherhadi/ilovetui"
|
||||||
"github.com/anotherhadi/spilltea/internal/config"
|
"github.com/anotherhadi/spilltea/internal/config"
|
||||||
"github.com/anotherhadi/spilltea/internal/style"
|
|
||||||
"github.com/charmbracelet/x/ansi"
|
"github.com/charmbracelet/x/ansi"
|
||||||
)
|
)
|
||||||
|
|
||||||
func windowStyle() lipgloss.Style {
|
func windowStyle() lipgloss.Style {
|
||||||
return lipgloss.NewStyle().
|
return lipgloss.NewStyle().
|
||||||
Border(lipgloss.RoundedBorder()).
|
Border(lipgloss.RoundedBorder()).
|
||||||
BorderForeground(style.S.Subtle).
|
BorderForeground(ilovetui.S.Subtle).
|
||||||
Padding(0, 0)
|
Padding(0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ func (e Model) View() tea.View {
|
|||||||
countText = fmt.Sprintf("%d/%d", e.matchIndex+1, len(e.matches))
|
countText = fmt.Sprintf("%d/%d", e.matchIndex+1, len(e.matches))
|
||||||
}
|
}
|
||||||
count := lipgloss.NewStyle().Padding(0, 1).
|
count := lipgloss.NewStyle().Padding(0, 1).
|
||||||
Foreground(style.S.MutedFg).
|
Foreground(ilovetui.S.Muted).
|
||||||
Render(countText)
|
Render(countText)
|
||||||
statusBar = lipgloss.JoinHorizontal(lipgloss.Top, statusBar, count)
|
statusBar = lipgloss.JoinHorizontal(lipgloss.Top, statusBar, count)
|
||||||
}
|
}
|
||||||
@@ -61,7 +61,7 @@ func (m *Model) renderMarkdown() {
|
|||||||
|
|
||||||
width := m.viewport.Width() - 2
|
width := m.viewport.Width() - 2
|
||||||
renderer, _ := glamour.NewTermRenderer(
|
renderer, _ := glamour.NewTermRenderer(
|
||||||
glamour.WithStyles(style.GlamourStyleConfig(cfg)),
|
glamour.WithStyles(ilovetui.GlamourStyleConfig()),
|
||||||
glamour.WithWordWrap(width),
|
glamour.WithWordWrap(width),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -11,17 +11,18 @@ import (
|
|||||||
tea "charm.land/bubbletea/v2"
|
tea "charm.land/bubbletea/v2"
|
||||||
"charm.land/glamour/v2"
|
"charm.land/glamour/v2"
|
||||||
"charm.land/lipgloss/v2"
|
"charm.land/lipgloss/v2"
|
||||||
"github.com/anotherhadi/spilltea/internal/config"
|
ilovetui "github.com/anotherhadi/ilovetui"
|
||||||
"github.com/anotherhadi/spilltea/internal/db"
|
"github.com/anotherhadi/spilltea/internal/db"
|
||||||
"github.com/anotherhadi/spilltea/internal/keys"
|
"github.com/anotherhadi/spilltea/internal/keys"
|
||||||
"github.com/anotherhadi/spilltea/internal/style"
|
|
||||||
"github.com/anotherhadi/spilltea/internal/util"
|
"github.com/anotherhadi/spilltea/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Model struct {
|
type Model struct {
|
||||||
database *db.DB
|
database *db.DB
|
||||||
findings []db.Finding
|
findings []db.Finding
|
||||||
cursor int
|
cursor int
|
||||||
|
hasUnread bool
|
||||||
|
knownCount int
|
||||||
|
|
||||||
listViewport viewport.Model
|
listViewport viewport.Model
|
||||||
bodyViewport viewport.Model
|
bodyViewport viewport.Model
|
||||||
@@ -37,15 +38,18 @@ type Model struct {
|
|||||||
|
|
||||||
func New() Model {
|
func New() Model {
|
||||||
return Model{
|
return Model{
|
||||||
listViewport: style.NewViewport(),
|
listViewport: ilovetui.NewViewport(),
|
||||||
bodyViewport: style.NewViewport(),
|
bodyViewport: ilovetui.NewViewport(),
|
||||||
pager: style.NewPaginator(),
|
pager: ilovetui.NewPaginator(),
|
||||||
help: style.NewHelp(),
|
help: ilovetui.NewHelp(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) Init() tea.Cmd { return nil }
|
func (m Model) Init() tea.Cmd { return nil }
|
||||||
|
|
||||||
|
func (m Model) HasUnread() bool { return m.hasUnread }
|
||||||
|
func (m *Model) ClearUnread() { m.hasUnread = false; m.knownCount = len(m.findings) }
|
||||||
|
|
||||||
func (m *Model) CurrentMarkdown() string {
|
func (m *Model) CurrentMarkdown() string {
|
||||||
if len(m.findings) == 0 {
|
if len(m.findings) == 0 {
|
||||||
return ""
|
return ""
|
||||||
@@ -71,9 +75,9 @@ func (m *Model) recalcSizes() {
|
|||||||
m.help.SetWidth(m.width - 2)
|
m.help.SetWidth(m.width - 2)
|
||||||
inner := m.width - 2
|
inner := m.width - 2
|
||||||
|
|
||||||
listH, bodyH := style.SplitH(m.height, m.renderStatusBar(), 0.35)
|
listH, bodyH := ilovetui.SplitH(m.height, m.renderStatusBar(), 0.35)
|
||||||
|
|
||||||
listVH := style.PanelContentH(listH) - 1 // -1 for the pager dots row
|
listVH := ilovetui.ContentHeight(listH) - 1 // -1 for the pager dots row
|
||||||
if listVH < 0 {
|
if listVH < 0 {
|
||||||
listVH = 0
|
listVH = 0
|
||||||
}
|
}
|
||||||
@@ -84,7 +88,7 @@ func (m *Model) recalcSizes() {
|
|||||||
m.pager.PerPage = 1
|
m.pager.PerPage = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
bodyVH := style.PanelContentH(bodyH)
|
bodyVH := ilovetui.ContentHeight(bodyH)
|
||||||
m.bodyViewport.SetWidth(inner)
|
m.bodyViewport.SetWidth(inner)
|
||||||
m.bodyViewport.SetHeight(bodyVH)
|
m.bodyViewport.SetHeight(bodyVH)
|
||||||
|
|
||||||
@@ -143,7 +147,7 @@ func (m *Model) refreshBodyScroll(reset bool) {
|
|||||||
|
|
||||||
func (m *Model) renderMarkdownCached(src string, width int) string {
|
func (m *Model) renderMarkdownCached(src string, width int) string {
|
||||||
if src == "" {
|
if src == "" {
|
||||||
return style.S.Faint.Render(util.CenterLines("(ㆆ _ ㆆ)", "no description"))
|
return ilovetui.S.Faint.Render(util.CenterLines("(ㆆ _ ㆆ)", "no description"))
|
||||||
}
|
}
|
||||||
tmpl, err := template.New("").Parse(src)
|
tmpl, err := template.New("").Parse(src)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -159,7 +163,7 @@ func (m *Model) renderMarkdownCached(src string, width int) string {
|
|||||||
// Rebuild renderer if width changed or not yet built.
|
// Rebuild renderer if width changed or not yet built.
|
||||||
if m.renderer == nil || m.rendererWidth != width {
|
if m.renderer == nil || m.rendererWidth != width {
|
||||||
r, err := glamour.NewTermRenderer(
|
r, err := glamour.NewTermRenderer(
|
||||||
glamour.WithStyles(style.GlamourStyleConfig(config.Global)),
|
glamour.WithStyles(ilovetui.GlamourStyleConfig()),
|
||||||
glamour.WithWordWrap(width),
|
glamour.WithWordWrap(width),
|
||||||
)
|
)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
prevID = m.findings[m.cursor].ID
|
prevID = m.findings[m.cursor].ID
|
||||||
}
|
}
|
||||||
m.findings = msg.Findings
|
m.findings = msg.Findings
|
||||||
|
if len(m.findings) > m.knownCount {
|
||||||
|
m.hasUnread = true
|
||||||
|
}
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
tea "charm.land/bubbletea/v2"
|
tea "charm.land/bubbletea/v2"
|
||||||
"charm.land/lipgloss/v2"
|
"charm.land/lipgloss/v2"
|
||||||
|
ilovetui "github.com/anotherhadi/ilovetui"
|
||||||
"github.com/anotherhadi/spilltea/internal/icons"
|
"github.com/anotherhadi/spilltea/internal/icons"
|
||||||
"github.com/anotherhadi/spilltea/internal/style"
|
"github.com/anotherhadi/spilltea/internal/style"
|
||||||
"github.com/anotherhadi/spilltea/internal/util"
|
"github.com/anotherhadi/spilltea/internal/util"
|
||||||
@@ -16,7 +17,7 @@ func (m Model) View() tea.View {
|
|||||||
return tea.NewView("Loading...")
|
return tea.NewView("Loading...")
|
||||||
}
|
}
|
||||||
|
|
||||||
listH, bodyH := style.SplitH(m.height, m.renderStatusBar(), 0.35)
|
listH, bodyH := ilovetui.SplitH(m.height, m.renderStatusBar(), 0.35)
|
||||||
|
|
||||||
content := lipgloss.JoinVertical(lipgloss.Left,
|
content := lipgloss.JoinVertical(lipgloss.Left,
|
||||||
m.renderListPanel(m.width, listH),
|
m.renderListPanel(m.width, listH),
|
||||||
@@ -27,44 +28,35 @@ 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
|
|
||||||
var dots string
|
var dots string
|
||||||
if len(m.findings) > 0 {
|
if len(m.findings) > 0 {
|
||||||
dots = s.Faint.Render(m.pager.View())
|
dots = ilovetui.S.Faint.Render(m.pager.View())
|
||||||
}
|
}
|
||||||
inner := lipgloss.JoinVertical(lipgloss.Left,
|
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),
|
||||||
)
|
)
|
||||||
return style.RenderWithTitle(s.PanelFocused, icons.I.Findings+"Findings", inner, w, h)
|
return ilovetui.RenderWithTitle(ilovetui.S.PanelFocused, icons.I.Findings+"Findings", inner, w, h)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) renderBodyPanel(h int) string {
|
func (m *Model) renderBodyPanel(h int) string {
|
||||||
s := style.S
|
|
||||||
title := "Description"
|
title := "Description"
|
||||||
if len(m.findings) > 0 {
|
if len(m.findings) > 0 {
|
||||||
title = m.findings[m.cursor].Title
|
title = m.findings[m.cursor].Title
|
||||||
}
|
}
|
||||||
return style.RenderWithTitle(s.Panel, title, m.bodyViewport.View(), m.width, h)
|
return ilovetui.RenderWithTitle(ilovetui.S.Panel, title, ilovetui.ViewportView(&m.bodyViewport), m.width, h)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) renderList() string {
|
func (m *Model) renderList() string {
|
||||||
s := style.S
|
|
||||||
if len(m.findings) == 0 {
|
if len(m.findings) == 0 {
|
||||||
return lipgloss.Place(
|
return lipgloss.Place(
|
||||||
m.listViewport.Width(), m.listViewport.Height(),
|
m.listViewport.Width(), m.listViewport.Height(),
|
||||||
lipgloss.Center, lipgloss.Center,
|
lipgloss.Center, lipgloss.Center,
|
||||||
s.Faint.Render(util.CenterLines("(҂◡_◡) ᕤ", "no findings")),
|
ilovetui.S.Faint.Render(util.CenterLines("(҂◡_◡) ᕤ", "no findings")),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
start, end := m.pager.GetSliceBounds(len(m.findings))
|
start, end := util.PageBounds(m.pager, len(m.findings))
|
||||||
if start < 0 {
|
|
||||||
start = 0
|
|
||||||
}
|
|
||||||
if end < start {
|
|
||||||
end = start
|
|
||||||
}
|
|
||||||
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
for i, f := range m.findings[start:end] {
|
for i, f := range m.findings[start:end] {
|
||||||
@@ -82,18 +74,18 @@ func (m *Model) renderList() string {
|
|||||||
titleW = 0
|
titleW = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
pluginStr := s.Faint.Width(8).Render(util.Truncate(f.PluginName, 8))
|
pluginStr := ilovetui.S.Faint.Width(8).Render(util.Truncate(f.PluginName, 8))
|
||||||
|
|
||||||
var line string
|
var line string
|
||||||
if selected {
|
if selected {
|
||||||
bg := lipgloss.NewStyle().Background(s.Selection)
|
bg := lipgloss.NewStyle().Background(ilovetui.S.Selection)
|
||||||
line = lipgloss.JoinHorizontal(lipgloss.Top,
|
line = lipgloss.JoinHorizontal(lipgloss.Top,
|
||||||
bg.Bold(true).Foreground(s.Primary).Width(2).Render(">"),
|
bg.Bold(true).Foreground(ilovetui.S.Primary).Width(2).Render(">"),
|
||||||
sevStyle.Background(s.Selection).Width(8).Render(f.Severity),
|
sevStyle.Background(ilovetui.S.Selection).Width(8).Render(f.Severity),
|
||||||
bg.Width(1).Render(""),
|
bg.Width(1).Render(""),
|
||||||
bg.Foreground(s.Subtle).Width(8).Render(util.Truncate(f.PluginName, 8)),
|
bg.Foreground(ilovetui.S.Subtle).Width(8).Render(util.Truncate(f.PluginName, 8)),
|
||||||
bg.Width(1).Render(""),
|
bg.Width(1).Render(""),
|
||||||
bg.Foreground(s.Subtle).Width(10).Render(ts),
|
bg.Foreground(ilovetui.S.Subtle).Width(10).Render(ts),
|
||||||
bg.Width(1).Render(""),
|
bg.Width(1).Render(""),
|
||||||
bg.Bold(true).Width(titleW).Render(f.Title),
|
bg.Bold(true).Width(titleW).Render(f.Title),
|
||||||
)
|
)
|
||||||
@@ -104,9 +96,9 @@ func (m *Model) renderList() string {
|
|||||||
" ",
|
" ",
|
||||||
pluginStr,
|
pluginStr,
|
||||||
" ",
|
" ",
|
||||||
s.Faint.Width(10).Render(ts),
|
ilovetui.S.Faint.Width(10).Render(ts),
|
||||||
" ",
|
" ",
|
||||||
s.Bold.Render(f.Title),
|
ilovetui.S.Bold.Render(f.Title),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
sb.WriteString(fmt.Sprintf("%s\n", line))
|
sb.WriteString(fmt.Sprintf("%s\n", line))
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import (
|
|||||||
"charm.land/bubbles/v2/textinput"
|
"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"
|
||||||
|
ilovetui "github.com/anotherhadi/ilovetui"
|
||||||
"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/util"
|
"github.com/anotherhadi/spilltea/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -44,10 +44,10 @@ func New() Model {
|
|||||||
ti := textinput.New()
|
ti := textinput.New()
|
||||||
ti.Prompt = ""
|
ti.Prompt = ""
|
||||||
return Model{
|
return Model{
|
||||||
listViewport: style.NewViewport(),
|
listViewport: ilovetui.NewViewport(),
|
||||||
bodyViewport: style.NewViewport(),
|
bodyViewport: ilovetui.NewViewport(),
|
||||||
pager: style.NewPaginator(),
|
pager: ilovetui.NewPaginator(),
|
||||||
help: style.NewHelp(),
|
help: ilovetui.NewHelp(),
|
||||||
searchInput: ti,
|
searchInput: ti,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,14 +124,14 @@ func (m *Model) recalcSizes() {
|
|||||||
// 2 (padding) + 2 (prefix char + space)
|
// 2 (padding) + 2 (prefix char + space)
|
||||||
m.searchInput.SetWidth(m.width - 4)
|
m.searchInput.SetWidth(m.width - 4)
|
||||||
|
|
||||||
listH, bodyH := style.SplitH(m.height, m.renderStatusBar(), 0.35)
|
listH, bodyH := ilovetui.SplitH(m.height, m.renderStatusBar(), 0.35)
|
||||||
|
|
||||||
inner := m.width - 2
|
inner := m.width - 2
|
||||||
if inner < 0 {
|
if inner < 0 {
|
||||||
inner = 0
|
inner = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
listVH := style.PanelContentH(listH) - 1 // -1 for the pager dots row
|
listVH := ilovetui.ContentHeight(listH) - 1 // -1 for the pager dots row
|
||||||
if listVH < 0 {
|
if listVH < 0 {
|
||||||
listVH = 0
|
listVH = 0
|
||||||
}
|
}
|
||||||
@@ -142,7 +142,7 @@ func (m *Model) recalcSizes() {
|
|||||||
m.pager.PerPage = 1
|
m.pager.PerPage = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
bodyVH := style.PanelContentH(bodyH)
|
bodyVH := ilovetui.ContentHeight(bodyH)
|
||||||
m.bodyViewport.SetWidth(inner)
|
m.bodyViewport.SetWidth(inner)
|
||||||
m.bodyViewport.SetHeight(bodyVH)
|
m.bodyViewport.SetHeight(bodyVH)
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,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"
|
"charm.land/lipgloss/v2"
|
||||||
|
ilovetui "github.com/anotherhadi/ilovetui"
|
||||||
"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"
|
||||||
@@ -249,11 +250,21 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
case key.Matches(msg, h.DeleteAll):
|
case key.Matches(msg, h.DeleteAll):
|
||||||
if m.database != nil {
|
if m.database != nil {
|
||||||
if m.searchKind != searchKindOff {
|
if m.searchKind != searchKindOff {
|
||||||
|
hasUnflagged := false
|
||||||
for _, e := range m.entries {
|
for _, e := range m.entries {
|
||||||
|
if !e.Flagged {
|
||||||
|
hasUnflagged = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, e := range m.entries {
|
||||||
|
if hasUnflagged && e.Flagged {
|
||||||
|
continue
|
||||||
|
}
|
||||||
m.database.DeleteEntry(e.ID)
|
m.database.DeleteEntry(e.ID)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
m.database.DeleteAllEntries()
|
m.database.DeleteAllExceptFlagged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return m, m.clearSearch()
|
return m, m.clearSearch()
|
||||||
@@ -335,7 +346,7 @@ func (m *Model) refreshBody() {
|
|||||||
}
|
}
|
||||||
if raw == "" {
|
if raw == "" {
|
||||||
w, h := m.bodyViewport.Width(), m.bodyViewport.Height()
|
w, h := m.bodyViewport.Width(), m.bodyViewport.Height()
|
||||||
m.bodyViewport.SetContent(lipgloss.Place(w, h, lipgloss.Center, lipgloss.Center, style.S.Faint.Render(util.CenterLines("(˘・_・˘)", "no response stored"))))
|
m.bodyViewport.SetContent(lipgloss.Place(w, h, lipgloss.Center, lipgloss.Center, ilovetui.S.Faint.Render(util.CenterLines("(˘・_・˘)", "no response stored"))))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
m.bodyViewport.SetContent(style.HighlightHTTP(raw))
|
m.bodyViewport.SetContent(style.HighlightHTTP(raw))
|
||||||
|
|||||||
+24
-33
@@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
tea "charm.land/bubbletea/v2"
|
tea "charm.land/bubbletea/v2"
|
||||||
"charm.land/lipgloss/v2"
|
"charm.land/lipgloss/v2"
|
||||||
|
ilovetui "github.com/anotherhadi/ilovetui"
|
||||||
"github.com/anotherhadi/spilltea/internal/icons"
|
"github.com/anotherhadi/spilltea/internal/icons"
|
||||||
"github.com/anotherhadi/spilltea/internal/keys"
|
"github.com/anotherhadi/spilltea/internal/keys"
|
||||||
"github.com/anotherhadi/spilltea/internal/style"
|
"github.com/anotherhadi/spilltea/internal/style"
|
||||||
@@ -17,7 +18,7 @@ func (m Model) View() tea.View {
|
|||||||
return tea.NewView("Loading...")
|
return tea.NewView("Loading...")
|
||||||
}
|
}
|
||||||
|
|
||||||
listH, bodyH := style.SplitH(m.height, m.renderStatusBar(), 0.35)
|
listH, bodyH := ilovetui.SplitH(m.height, m.renderStatusBar(), 0.35)
|
||||||
|
|
||||||
content := lipgloss.JoinVertical(lipgloss.Left,
|
content := lipgloss.JoinVertical(lipgloss.Left,
|
||||||
m.renderListPanel(m.width, listH),
|
m.renderListPanel(m.width, listH),
|
||||||
@@ -28,60 +29,56 @@ 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
|
|
||||||
var dots string
|
var dots string
|
||||||
if len(m.entries) > 0 {
|
if len(m.entries) > 0 {
|
||||||
dots = s.Faint.Render(m.pager.View())
|
dots = ilovetui.S.Faint.Render(m.pager.View())
|
||||||
}
|
}
|
||||||
inner := lipgloss.JoinVertical(lipgloss.Left,
|
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),
|
||||||
)
|
)
|
||||||
return style.RenderWithTitle(s.PanelFocused, icons.I.History+"History", inner, w, h)
|
return ilovetui.RenderWithTitle(ilovetui.S.PanelFocused, icons.I.History+"History", inner, w, h)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) renderBodyPanel(h int) string {
|
func (m *Model) renderBodyPanel(h int) string {
|
||||||
s := style.S
|
|
||||||
title := icons.I.Request + "Request"
|
title := icons.I.Request + "Request"
|
||||||
if m.focusedPanel == panelResponse {
|
if m.focusedPanel == panelResponse {
|
||||||
title = icons.I.Response + "Response"
|
title = icons.I.Response + "Response"
|
||||||
}
|
}
|
||||||
return style.RenderWithTitle(s.Panel, title, m.bodyViewport.View(), m.width, h)
|
return ilovetui.RenderWithTitle(ilovetui.S.Panel, title, ilovetui.ViewportView(&m.bodyViewport), m.width, h)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) renderStatusBar() string {
|
func (m *Model) renderStatusBar() string {
|
||||||
s := style.S
|
|
||||||
pad := lipgloss.NewStyle().Padding(0, 1)
|
pad := lipgloss.NewStyle().Padding(0, 1)
|
||||||
escKey := keys.Keys.Global.Escape.Help().Key
|
escKey := keys.Keys.Global.Escape.Help().Key
|
||||||
switch m.searchKind {
|
switch m.searchKind {
|
||||||
case searchKindFulltext:
|
case searchKindFulltext:
|
||||||
filterKey := keys.Keys.History.Filter.Help().Key
|
filterKey := keys.Keys.History.Filter.Help().Key
|
||||||
if m.searchAccepted {
|
if m.searchAccepted {
|
||||||
accent := lipgloss.NewStyle().Foreground(s.Primary)
|
accent := lipgloss.NewStyle().Foreground(ilovetui.S.Primary)
|
||||||
filterLine := pad.Render(accent.Render(filterKey) + " " + s.Bold.Render(m.searchInput.Value()) + s.Faint.Render(" "+escKey+" to clear"))
|
filterLine := pad.Render(accent.Render(filterKey) + " " + ilovetui.S.Bold.Render(m.searchInput.Value()) + ilovetui.S.Faint.Render(" "+escKey+" to clear"))
|
||||||
return lipgloss.JoinVertical(lipgloss.Left, filterLine, pad.Render(m.help.View(historyKeyMap{width: m.width})))
|
return lipgloss.JoinVertical(lipgloss.Left, filterLine, pad.Render(m.help.View(historyKeyMap{width: m.width})))
|
||||||
}
|
}
|
||||||
return pad.Render(s.Faint.Render(filterKey) + " " + m.searchInput.View())
|
return pad.Render(ilovetui.S.Faint.Render(filterKey) + " " + m.searchInput.View())
|
||||||
case searchKindSQL:
|
case searchKindSQL:
|
||||||
sqlKey := keys.Keys.History.SqlQuery.Help().Key
|
sqlKey := keys.Keys.History.SqlQuery.Help().Key
|
||||||
if m.searchAccepted {
|
if m.searchAccepted {
|
||||||
accent := lipgloss.NewStyle().Foreground(s.Primary)
|
accent := lipgloss.NewStyle().Foreground(ilovetui.S.Primary)
|
||||||
filterLine := pad.Render(accent.Render(sqlKey) + " " + s.Bold.Render(m.searchInput.Value()) + s.Faint.Render(" "+escKey+" to clear"))
|
filterLine := pad.Render(accent.Render(sqlKey) + " " + ilovetui.S.Bold.Render(m.searchInput.Value()) + ilovetui.S.Faint.Render(" "+escKey+" to clear"))
|
||||||
return lipgloss.JoinVertical(lipgloss.Left, filterLine, pad.Render(m.help.View(historyKeyMap{width: m.width})))
|
return lipgloss.JoinVertical(lipgloss.Left, filterLine, pad.Render(m.help.View(historyKeyMap{width: m.width})))
|
||||||
}
|
}
|
||||||
return pad.Render(s.Faint.Render(sqlKey) + " " + m.searchInput.View())
|
return pad.Render(ilovetui.S.Faint.Render(sqlKey) + " " + m.searchInput.View())
|
||||||
default:
|
default:
|
||||||
return pad.Render(m.help.View(historyKeyMap{width: m.width}))
|
return pad.Render(m.help.View(historyKeyMap{width: m.width}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) renderList() string {
|
func (m *Model) renderList() string {
|
||||||
s := style.S
|
|
||||||
if m.searchErr != "" {
|
if m.searchErr != "" {
|
||||||
return lipgloss.Place(
|
return lipgloss.Place(
|
||||||
m.listViewport.Width(), m.listViewport.Height(),
|
m.listViewport.Width(), m.listViewport.Height(),
|
||||||
lipgloss.Center, lipgloss.Center,
|
lipgloss.Center, lipgloss.Center,
|
||||||
lipgloss.NewStyle().Foreground(s.Error).Render(m.searchErr),
|
lipgloss.NewStyle().Foreground(ilovetui.S.Error).Render(m.searchErr),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if len(m.entries) == 0 {
|
if len(m.entries) == 0 {
|
||||||
@@ -92,24 +89,18 @@ func (m *Model) renderList() string {
|
|||||||
return lipgloss.Place(
|
return lipgloss.Place(
|
||||||
m.listViewport.Width(), m.listViewport.Height(),
|
m.listViewport.Width(), m.listViewport.Height(),
|
||||||
lipgloss.Center, lipgloss.Center,
|
lipgloss.Center, lipgloss.Center,
|
||||||
s.Faint.Render(msg),
|
ilovetui.S.Faint.Render(msg),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
start, end := m.pager.GetSliceBounds(len(m.entries))
|
start, end := util.PageBounds(m.pager, len(m.entries))
|
||||||
if start < 0 {
|
|
||||||
start = 0
|
|
||||||
}
|
|
||||||
if end < start {
|
|
||||||
end = start
|
|
||||||
}
|
|
||||||
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
for i, e := range m.entries[start:end] {
|
for i, e := range m.entries[start:end] {
|
||||||
globalIdx := start + i
|
globalIdx := start + i
|
||||||
selected := globalIdx == m.cursor
|
selected := globalIdx == m.cursor
|
||||||
|
|
||||||
selBg := s.Selection
|
selBg := ilovetui.S.Selection
|
||||||
w := m.listViewport.Width()
|
w := m.listViewport.Width()
|
||||||
|
|
||||||
statusStr := fmt.Sprintf("%3d", e.StatusCode)
|
statusStr := fmt.Sprintf("%3d", e.StatusCode)
|
||||||
@@ -121,7 +112,7 @@ func (m *Model) renderList() string {
|
|||||||
|
|
||||||
ts := e.Timestamp.Format("15:04:05")
|
ts := e.Timestamp.Format("15:04:05")
|
||||||
statusSt := style.StatusStyle(e.StatusCode, 3)
|
statusSt := style.StatusStyle(e.StatusCode, 3)
|
||||||
flagSt := lipgloss.NewStyle().Foreground(s.Primary)
|
flagSt := lipgloss.NewStyle().Foreground(ilovetui.S.Primary)
|
||||||
|
|
||||||
var line string
|
var line string
|
||||||
if selected {
|
if selected {
|
||||||
@@ -134,13 +125,13 @@ func (m *Model) renderList() string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
line = lipgloss.JoinHorizontal(lipgloss.Top,
|
line = lipgloss.JoinHorizontal(lipgloss.Top,
|
||||||
bg.Bold(true).Foreground(s.Primary).Width(2).Render(">"),
|
bg.Bold(true).Foreground(ilovetui.S.Primary).Width(2).Render(">"),
|
||||||
bg.Foreground(s.Primary).Width(2).Render(flagStr),
|
bg.Foreground(ilovetui.S.Primary).Width(2).Render(flagStr),
|
||||||
s.Method(e.Method).Background(selBg).Render(e.Method),
|
style.S.Method(e.Method).Background(selBg).Render(e.Method),
|
||||||
bg.Width(1).Render(""),
|
bg.Width(1).Render(""),
|
||||||
statusSt.Background(selBg).Render(statusStr),
|
statusSt.Background(selBg).Render(statusStr),
|
||||||
bg.Width(1).Render(""),
|
bg.Width(1).Render(""),
|
||||||
bg.Foreground(s.Subtle).Width(10).Render(ts),
|
bg.Foreground(ilovetui.S.Subtle).Width(10).Render(ts),
|
||||||
bg.Width(1).Render(""),
|
bg.Width(1).Render(""),
|
||||||
bg.Bold(true).Width(hostPathW).Render(e.Host+e.Path),
|
bg.Bold(true).Width(hostPathW).Render(e.Host+e.Path),
|
||||||
)
|
)
|
||||||
@@ -155,14 +146,14 @@ func (m *Model) renderList() string {
|
|||||||
line = lipgloss.JoinHorizontal(lipgloss.Top,
|
line = lipgloss.JoinHorizontal(lipgloss.Top,
|
||||||
" ",
|
" ",
|
||||||
flagSt.Width(2).Render(flagStr),
|
flagSt.Width(2).Render(flagStr),
|
||||||
s.Method(e.Method).Render(e.Method),
|
style.S.Method(e.Method).Render(e.Method),
|
||||||
" ",
|
" ",
|
||||||
statusSt.Render(statusStr),
|
statusSt.Render(statusStr),
|
||||||
" ",
|
" ",
|
||||||
s.Faint.Width(10).Render(ts),
|
ilovetui.S.Faint.Width(10).Render(ts),
|
||||||
" ",
|
" ",
|
||||||
s.Bold.Render(e.Host),
|
ilovetui.S.Bold.Render(e.Host),
|
||||||
s.Faint.Render(e.Path),
|
ilovetui.S.Faint.Render(e.Path),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
sb.WriteString(line + "\n")
|
sb.WriteString(line + "\n")
|
||||||
|
|||||||
+10
-12
@@ -14,10 +14,10 @@ import (
|
|||||||
"charm.land/bubbles/v2/textinput"
|
"charm.land/bubbles/v2/textinput"
|
||||||
tea "charm.land/bubbletea/v2"
|
tea "charm.land/bubbletea/v2"
|
||||||
"charm.land/lipgloss/v2"
|
"charm.land/lipgloss/v2"
|
||||||
|
ilovetui "github.com/anotherhadi/ilovetui"
|
||||||
"github.com/anotherhadi/spilltea/internal/db"
|
"github.com/anotherhadi/spilltea/internal/db"
|
||||||
"github.com/anotherhadi/spilltea/internal/icons"
|
"github.com/anotherhadi/spilltea/internal/icons"
|
||||||
"github.com/anotherhadi/spilltea/internal/keys"
|
"github.com/anotherhadi/spilltea/internal/keys"
|
||||||
"github.com/anotherhadi/spilltea/internal/style"
|
|
||||||
"github.com/anotherhadi/spilltea/internal/ui/components/teapot"
|
"github.com/anotherhadi/spilltea/internal/ui/components/teapot"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -88,19 +88,18 @@ type homeDelegate struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newHomeDelegate() homeDelegate {
|
func newHomeDelegate() homeDelegate {
|
||||||
s := style.S
|
|
||||||
leftBorder := lipgloss.Border{Left: "│"}
|
leftBorder := lipgloss.Border{Left: "│"}
|
||||||
return homeDelegate{
|
return homeDelegate{
|
||||||
normalTitle: lipgloss.NewStyle().Foreground(s.Text).PaddingLeft(4),
|
normalTitle: lipgloss.NewStyle().Foreground(ilovetui.S.Text).PaddingLeft(4),
|
||||||
normalDesc: lipgloss.NewStyle().Foreground(s.Subtle).Faint(true).PaddingLeft(4),
|
normalDesc: lipgloss.NewStyle().Foreground(ilovetui.S.Subtle).Faint(true).PaddingLeft(4),
|
||||||
selectedTitle: lipgloss.NewStyle().
|
selectedTitle: lipgloss.NewStyle().
|
||||||
Border(leftBorder, false, false, false, true).
|
Border(leftBorder, false, false, false, true).
|
||||||
BorderForeground(s.Primary).
|
BorderForeground(ilovetui.S.Primary).
|
||||||
Foreground(s.Primary).Bold(true).PaddingLeft(3),
|
Foreground(ilovetui.S.Primary).Bold(true).PaddingLeft(3),
|
||||||
selectedDesc: lipgloss.NewStyle().
|
selectedDesc: lipgloss.NewStyle().
|
||||||
Border(leftBorder, false, false, false, true).
|
Border(leftBorder, false, false, false, true).
|
||||||
BorderForeground(s.Primary).
|
BorderForeground(ilovetui.S.Primary).
|
||||||
Foreground(s.MutedFg).PaddingLeft(3),
|
Foreground(ilovetui.S.Muted).PaddingLeft(3),
|
||||||
filterMatch: lipgloss.NewStyle().Underline(true),
|
filterMatch: lipgloss.NewStyle().Underline(true),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -304,14 +303,13 @@ func buildItems(projects []Project) []list.Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) renderHelpLine() string {
|
func (m Model) renderHelpLine() string {
|
||||||
s := style.S
|
|
||||||
k := keys.Keys.Home
|
k := keys.Keys.Home
|
||||||
fs := m.list.FilterState()
|
fs := m.list.FilterState()
|
||||||
|
|
||||||
kStyle := lipgloss.NewStyle().Foreground(s.MutedFg).Inline(true)
|
kStyle := lipgloss.NewStyle().Foreground(ilovetui.S.Muted).Inline(true)
|
||||||
dStyle := s.Faint.Inline(true)
|
dStyle := ilovetui.S.Faint.Inline(true)
|
||||||
|
|
||||||
sep := s.Faint.Inline(true).Render(" • ")
|
sep := ilovetui.S.Faint.Inline(true).Render(" • ")
|
||||||
item := func(keyStr, desc string) string {
|
item := func(keyStr, desc string) string {
|
||||||
return kStyle.Render(keyStr) + " " + dStyle.Render(desc)
|
return kStyle.Render(keyStr) + " " + dStyle.Render(desc)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,14 +138,14 @@ func sanitizeName(s string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func IsValidProjectName(s string) bool {
|
func IsValidProjectName(s string) bool {
|
||||||
if s == "tmp" {
|
if s == "tmp" || s == "temp" || s == "temporary" {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return s != "" && s == sanitizeName(s)
|
return s != "" && s == sanitizeName(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
func OpenProject(projectDir, name string) (*Project, error) {
|
func OpenProject(projectDir, name string) (*Project, error) {
|
||||||
if name == "tmp" {
|
if name == "tmp" || name == "temp" || name == "temporary" {
|
||||||
dir := tempDir()
|
dir := tempDir()
|
||||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -5,29 +5,28 @@ import (
|
|||||||
|
|
||||||
tea "charm.land/bubbletea/v2"
|
tea "charm.land/bubbletea/v2"
|
||||||
"charm.land/lipgloss/v2"
|
"charm.land/lipgloss/v2"
|
||||||
"github.com/anotherhadi/spilltea/internal/style"
|
ilovetui "github.com/anotherhadi/ilovetui"
|
||||||
"github.com/anotherhadi/spilltea/internal/ui/components/teapot"
|
"github.com/anotherhadi/spilltea/internal/ui/components/teapot"
|
||||||
)
|
)
|
||||||
|
|
||||||
const inputPanelMaxW = 44
|
const inputPanelMaxW = 44
|
||||||
|
|
||||||
func (m Model) View() tea.View {
|
func (m Model) View() tea.View {
|
||||||
s := style.S
|
|
||||||
iw := m.innerW()
|
iw := m.innerW()
|
||||||
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
sb.WriteString("\n")
|
sb.WriteString("\n")
|
||||||
if m.height > teapotMinH {
|
if m.height > teapotMinH {
|
||||||
frames := teapot.TeapotFrames()
|
frames := teapot.TeapotFrames()
|
||||||
frame := lipgloss.NewStyle().Foreground(s.Primary).Render(frames[m.teapotFrame])
|
frame := lipgloss.NewStyle().Foreground(ilovetui.S.Primary).Render(frames[m.teapotFrame])
|
||||||
sb.WriteString(center(iw, frame))
|
sb.WriteString(center(iw, frame))
|
||||||
sb.WriteString("\n\n")
|
sb.WriteString("\n\n")
|
||||||
} else {
|
} else {
|
||||||
sb.WriteString("\n")
|
sb.WriteString("\n")
|
||||||
}
|
}
|
||||||
sb.WriteString(center(iw, lipgloss.NewStyle().Bold(true).Foreground(s.Primary).Render("SPILLTEA")))
|
sb.WriteString(center(iw, lipgloss.NewStyle().Bold(true).Foreground(ilovetui.S.Primary).Render("SPILLTEA")))
|
||||||
sb.WriteString("\n")
|
sb.WriteString("\n")
|
||||||
sb.WriteString(center(iw, s.Faint.Render("choose a project to get started")))
|
sb.WriteString(center(iw, ilovetui.S.Faint.Render("choose a project to get started")))
|
||||||
sb.WriteString("\n\n")
|
sb.WriteString("\n\n")
|
||||||
|
|
||||||
if m.mode == modeNaming {
|
if m.mode == modeNaming {
|
||||||
@@ -50,7 +49,6 @@ func (m Model) View() tea.View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) renderNamingPanel() string {
|
func (m Model) renderNamingPanel() string {
|
||||||
s := style.S
|
|
||||||
iw := m.innerW()
|
iw := m.innerW()
|
||||||
|
|
||||||
panelW := inputPanelMaxW
|
panelW := inputPanelMaxW
|
||||||
@@ -63,15 +61,15 @@ func (m Model) renderNamingPanel() string {
|
|||||||
innerW := inputPanelInnerW(iw)
|
innerW := inputPanelInnerW(iw)
|
||||||
inputLine := lipgloss.NewStyle().Width(innerW).Render(m.nameInput.View())
|
inputLine := lipgloss.NewStyle().Width(innerW).Render(m.nameInput.View())
|
||||||
|
|
||||||
label := lipgloss.NewStyle().Foreground(s.MutedFg).Render("Project name")
|
label := lipgloss.NewStyle().Foreground(ilovetui.S.Muted).Render("Project name")
|
||||||
panel := lipgloss.NewStyle().
|
panel := lipgloss.NewStyle().
|
||||||
Border(lipgloss.RoundedBorder()).
|
Border(lipgloss.RoundedBorder()).
|
||||||
BorderForeground(s.Primary).
|
BorderForeground(ilovetui.S.Primary).
|
||||||
Padding(1, 2).
|
Padding(1, 2).
|
||||||
Width(panelW).
|
Width(panelW).
|
||||||
Render(label + "\n" + inputLine)
|
Render(label + "\n" + inputLine)
|
||||||
|
|
||||||
hint := s.Faint.Render("[enter] confirm [esc] cancel")
|
hint := ilovetui.S.Faint.Render("[enter] confirm [esc] cancel")
|
||||||
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
sb.WriteString(center(iw, panel))
|
sb.WriteString(center(iw, panel))
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
ilovetui "github.com/anotherhadi/ilovetui"
|
||||||
"github.com/anotherhadi/spilltea/internal/intercept"
|
"github.com/anotherhadi/spilltea/internal/intercept"
|
||||||
"github.com/anotherhadi/spilltea/internal/style"
|
"github.com/anotherhadi/spilltea/internal/style"
|
||||||
"github.com/anotherhadi/spilltea/internal/util"
|
"github.com/anotherhadi/spilltea/internal/util"
|
||||||
@@ -157,20 +158,20 @@ func (m *Model) listHalfWidths() (leftW, rightW int) {
|
|||||||
func (m *Model) recalcSizes() {
|
func (m *Model) recalcSizes() {
|
||||||
m.help.SetWidth(m.width - 2)
|
m.help.SetWidth(m.width - 2)
|
||||||
|
|
||||||
listH, bodyH := style.SplitH(m.height, m.renderStatusBar(), 0.35)
|
listH, bodyH := ilovetui.SplitH(m.height, m.renderStatusBar(), 0.35)
|
||||||
|
|
||||||
bodyInner := m.width - 2
|
bodyInner := m.width - 2
|
||||||
if bodyInner < 0 {
|
if bodyInner < 0 {
|
||||||
bodyInner = 0
|
bodyInner = 0
|
||||||
}
|
}
|
||||||
bodyVH := style.PanelContentH(bodyH)
|
bodyVH := ilovetui.ContentHeight(bodyH)
|
||||||
|
|
||||||
m.textarea.SetWidth(bodyInner)
|
m.textarea.SetWidth(bodyInner)
|
||||||
m.textarea.SetHeight(bodyVH)
|
m.textarea.SetHeight(bodyVH)
|
||||||
m.bodyViewport.SetWidth(bodyInner)
|
m.bodyViewport.SetWidth(bodyInner)
|
||||||
m.bodyViewport.SetHeight(bodyVH)
|
m.bodyViewport.SetHeight(bodyVH)
|
||||||
|
|
||||||
listVH := style.PanelContentH(listH) - 1 // -1 for the pager dots row
|
listVH := ilovetui.ContentHeight(listH) - 1 // -1 for the pager dots row
|
||||||
if listVH < 0 {
|
if listVH < 0 {
|
||||||
listVH = 0
|
listVH = 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
package intercept
|
package intercept
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"charm.land/bubbles/v2/help"
|
|
||||||
"charm.land/bubbles/v2/key"
|
"charm.land/bubbles/v2/key"
|
||||||
"github.com/anotherhadi/spilltea/internal/icons"
|
"github.com/anotherhadi/spilltea/internal/icons"
|
||||||
"github.com/anotherhadi/spilltea/internal/keys"
|
"github.com/anotherhadi/spilltea/internal/keys"
|
||||||
"github.com/anotherhadi/spilltea/internal/style"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func newHelp() help.Model { return style.NewHelp() }
|
|
||||||
|
|
||||||
type interceptKeyMap struct{ width int }
|
type interceptKeyMap struct{ width int }
|
||||||
|
|
||||||
func iconBinding(b key.Binding, icon string) key.Binding {
|
func iconBinding(b key.Binding, icon string) key.Binding {
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import (
|
|||||||
"charm.land/bubbles/v2/textarea"
|
"charm.land/bubbles/v2/textarea"
|
||||||
"charm.land/bubbles/v2/viewport"
|
"charm.land/bubbles/v2/viewport"
|
||||||
tea "charm.land/bubbletea/v2"
|
tea "charm.land/bubbletea/v2"
|
||||||
|
ilovetui "github.com/anotherhadi/ilovetui"
|
||||||
"github.com/anotherhadi/spilltea/internal/config"
|
"github.com/anotherhadi/spilltea/internal/config"
|
||||||
"github.com/anotherhadi/spilltea/internal/intercept"
|
"github.com/anotherhadi/spilltea/internal/intercept"
|
||||||
"github.com/anotherhadi/spilltea/internal/style"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type panel int
|
type panel int
|
||||||
@@ -30,6 +30,7 @@ type Model struct {
|
|||||||
|
|
||||||
editing bool
|
editing bool
|
||||||
interceptEnabled bool
|
interceptEnabled bool
|
||||||
|
hasUnread bool
|
||||||
pendingEdits map[*intercept.PendingRequest]string
|
pendingEdits map[*intercept.PendingRequest]string
|
||||||
pendingResponseEdits map[*intercept.PendingResponse]string
|
pendingResponseEdits map[*intercept.PendingResponse]string
|
||||||
|
|
||||||
@@ -47,14 +48,14 @@ type Model struct {
|
|||||||
|
|
||||||
func New(broker *intercept.Broker) Model {
|
func New(broker *intercept.Broker) Model {
|
||||||
cfg := config.Global
|
cfg := config.Global
|
||||||
ta := style.NewTextarea(false)
|
ta := ilovetui.NewTextarea(false)
|
||||||
ta.Blur()
|
ta.Blur()
|
||||||
|
|
||||||
lv := style.NewViewport()
|
lv := ilovetui.NewViewport()
|
||||||
rv := style.NewViewport()
|
rv := ilovetui.NewViewport()
|
||||||
bv := style.NewViewport()
|
bv := ilovetui.NewViewport()
|
||||||
p := style.NewPaginator()
|
p := ilovetui.NewPaginator()
|
||||||
rp := style.NewPaginator()
|
rp := ilovetui.NewPaginator()
|
||||||
|
|
||||||
broker.SetCaptureResponse(cfg.Intercept.DefaultCaptureResponse)
|
broker.SetCaptureResponse(cfg.Intercept.DefaultCaptureResponse)
|
||||||
|
|
||||||
@@ -68,7 +69,7 @@ func New(broker *intercept.Broker) Model {
|
|||||||
textarea: ta,
|
textarea: ta,
|
||||||
pager: p,
|
pager: p,
|
||||||
responsePager: rp,
|
responsePager: rp,
|
||||||
help: newHelp(),
|
help: ilovetui.NewHelp(),
|
||||||
pendingEdits: make(map[*intercept.PendingRequest]string),
|
pendingEdits: make(map[*intercept.PendingRequest]string),
|
||||||
pendingResponseEdits: make(map[*intercept.PendingResponse]string),
|
pendingResponseEdits: make(map[*intercept.PendingResponse]string),
|
||||||
}
|
}
|
||||||
@@ -76,6 +77,9 @@ func New(broker *intercept.Broker) Model {
|
|||||||
|
|
||||||
func (m Model) Init() tea.Cmd { return nil }
|
func (m Model) Init() tea.Cmd { return nil }
|
||||||
|
|
||||||
|
func (m Model) HasUnread() bool { return m.hasUnread }
|
||||||
|
func (m *Model) ClearUnread() { m.hasUnread = false }
|
||||||
|
|
||||||
func (m Model) IsEditing() bool { return m.editing }
|
func (m Model) IsEditing() bool { return m.editing }
|
||||||
|
|
||||||
func (m Model) IsResponseFocused() bool {
|
func (m Model) IsResponseFocused() bool {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
wasEmpty := len(m.queue) == 0
|
wasEmpty := len(m.queue) == 0
|
||||||
m.queue = append(m.queue, msg.Req)
|
m.queue = append(m.queue, msg.Req)
|
||||||
|
m.hasUnread = true
|
||||||
m.refreshListViewport()
|
m.refreshListViewport()
|
||||||
if wasEmpty && (!m.captureResponse || m.focusedPanel == panelRequests) {
|
if wasEmpty && (!m.captureResponse || m.focusedPanel == panelRequests) {
|
||||||
m.refreshBody()
|
m.refreshBody()
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
tea "charm.land/bubbletea/v2"
|
tea "charm.land/bubbletea/v2"
|
||||||
"charm.land/lipgloss/v2"
|
"charm.land/lipgloss/v2"
|
||||||
|
ilovetui "github.com/anotherhadi/ilovetui"
|
||||||
"github.com/anotherhadi/spilltea/internal/icons"
|
"github.com/anotherhadi/spilltea/internal/icons"
|
||||||
"github.com/anotherhadi/spilltea/internal/style"
|
"github.com/anotherhadi/spilltea/internal/style"
|
||||||
"github.com/anotherhadi/spilltea/internal/util"
|
"github.com/anotherhadi/spilltea/internal/util"
|
||||||
@@ -16,7 +17,7 @@ func (m Model) View() tea.View {
|
|||||||
return tea.NewView("Loading...")
|
return tea.NewView("Loading...")
|
||||||
}
|
}
|
||||||
|
|
||||||
listH, bodyH := style.SplitH(m.height, m.renderStatusBar(), 0.35)
|
listH, bodyH := ilovetui.SplitH(m.height, m.renderStatusBar(), 0.35)
|
||||||
|
|
||||||
var listRow string
|
var listRow string
|
||||||
if m.captureResponse {
|
if m.captureResponse {
|
||||||
@@ -38,17 +39,15 @@ 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
|
|
||||||
|
|
||||||
focused := !m.editing && (!m.captureResponse || m.focusedPanel == panelRequests)
|
focused := !m.editing && (!m.captureResponse || m.focusedPanel == panelRequests)
|
||||||
border := s.Panel
|
border := ilovetui.S.Panel
|
||||||
if focused {
|
if focused {
|
||||||
border = s.PanelFocused
|
border = ilovetui.S.PanelFocused
|
||||||
}
|
}
|
||||||
|
|
||||||
var dots string
|
var dots string
|
||||||
if len(m.queue) > 0 {
|
if len(m.queue) > 0 {
|
||||||
dots = s.Faint.Render(m.pager.View())
|
dots = ilovetui.S.Faint.Render(m.pager.View())
|
||||||
}
|
}
|
||||||
inner := lipgloss.JoinVertical(lipgloss.Left,
|
inner := lipgloss.JoinVertical(lipgloss.Left,
|
||||||
m.listViewport.View(),
|
m.listViewport.View(),
|
||||||
@@ -57,46 +56,42 @@ func (m *Model) renderListPanel(w, h int) string {
|
|||||||
|
|
||||||
title := icons.I.Request + "Requests"
|
title := icons.I.Request + "Requests"
|
||||||
if !m.interceptEnabled {
|
if !m.interceptEnabled {
|
||||||
title += " " + lipgloss.NewStyle().Foreground(style.S.Error).Render("[intercept off]")
|
title += " " + lipgloss.NewStyle().Foreground(ilovetui.S.Error).Render("[intercept off]")
|
||||||
}
|
}
|
||||||
return style.RenderWithTitle(border, title, inner, w, h)
|
return ilovetui.RenderWithTitle(border, title, inner, w, h)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) renderResponseListPanel(w, h int) string {
|
func (m *Model) renderResponseListPanel(w, h int) string {
|
||||||
s := style.S
|
|
||||||
|
|
||||||
focused := !m.editing && m.focusedPanel == panelResponses
|
focused := !m.editing && m.focusedPanel == panelResponses
|
||||||
border := s.Panel
|
border := ilovetui.S.Panel
|
||||||
if focused {
|
if focused {
|
||||||
border = s.PanelFocused
|
border = ilovetui.S.PanelFocused
|
||||||
}
|
}
|
||||||
|
|
||||||
dots := s.Faint.Render(m.responsePager.View())
|
dots := ilovetui.S.Faint.Render(m.responsePager.View())
|
||||||
inner := lipgloss.JoinVertical(lipgloss.Left,
|
inner := lipgloss.JoinVertical(lipgloss.Left,
|
||||||
m.responseViewport.View(),
|
m.responseViewport.View(),
|
||||||
lipgloss.PlaceHorizontal(m.responseViewport.Width(), lipgloss.Center, dots),
|
lipgloss.PlaceHorizontal(m.responseViewport.Width(), lipgloss.Center, dots),
|
||||||
)
|
)
|
||||||
|
|
||||||
return style.RenderWithTitle(border, icons.I.Response+"Responses", inner, w, h)
|
return ilovetui.RenderWithTitle(border, icons.I.Response+"Responses", inner, w, h)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) renderBodyPanel(h int) string {
|
func (m *Model) renderBodyPanel(h int) string {
|
||||||
s := style.S
|
|
||||||
|
|
||||||
var body string
|
var body string
|
||||||
if m.editing {
|
if m.editing {
|
||||||
body = m.textarea.View()
|
body = m.textarea.View()
|
||||||
} else {
|
} else {
|
||||||
body = m.bodyViewport.View()
|
body = ilovetui.ViewportView(&m.bodyViewport)
|
||||||
}
|
}
|
||||||
|
|
||||||
border := s.Panel
|
border := ilovetui.S.Panel
|
||||||
if m.editing {
|
if m.editing {
|
||||||
border = s.PanelFocused
|
border = ilovetui.S.PanelFocused
|
||||||
}
|
}
|
||||||
|
|
||||||
title := icons.I.Detail + "Details"
|
title := icons.I.Detail + "Details"
|
||||||
return style.RenderWithTitle(border, title, body, m.width, h)
|
return ilovetui.RenderWithTitle(border, title, body, m.width, h)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) renderStatusBar() string {
|
func (m *Model) renderStatusBar() string {
|
||||||
@@ -105,17 +100,10 @@ func (m *Model) renderStatusBar() string {
|
|||||||
|
|
||||||
func (m *Model) renderList() string {
|
func (m *Model) renderList() string {
|
||||||
if len(m.queue) == 0 {
|
if len(m.queue) == 0 {
|
||||||
return lipgloss.Place(m.listViewport.Width(), m.listViewport.Height(), lipgloss.Center, lipgloss.Center, style.S.Faint.Render(util.CenterLines("(。◕‿‿◕。)", "waiting for a request")))
|
return lipgloss.Place(m.listViewport.Width(), m.listViewport.Height(), lipgloss.Center, lipgloss.Center, ilovetui.S.Faint.Render(util.CenterLines("(。◕‿‿◕。)", "waiting for a request")))
|
||||||
}
|
}
|
||||||
|
|
||||||
s := style.S
|
start, end := util.PageBounds(m.pager, len(m.queue))
|
||||||
start, end := m.pager.GetSliceBounds(len(m.queue))
|
|
||||||
if start < 0 {
|
|
||||||
start = 0
|
|
||||||
}
|
|
||||||
if end < start {
|
|
||||||
end = start
|
|
||||||
}
|
|
||||||
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
for i, req := range m.queue[start:end] {
|
for i, req := range m.queue[start:end] {
|
||||||
@@ -127,7 +115,7 @@ func (m *Model) renderList() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
selected := globalIdx == m.cursor
|
selected := globalIdx == m.cursor
|
||||||
selBg := s.Selection
|
selBg := ilovetui.S.Selection
|
||||||
|
|
||||||
w := m.listViewport.Width()
|
w := m.listViewport.Width()
|
||||||
const fixedW = 2 + 7 + 2
|
const fixedW = 2 + 7 + 2
|
||||||
@@ -140,18 +128,18 @@ func (m *Model) renderList() string {
|
|||||||
if selected {
|
if selected {
|
||||||
bg := lipgloss.NewStyle().Background(selBg)
|
bg := lipgloss.NewStyle().Background(selBg)
|
||||||
line = lipgloss.JoinHorizontal(lipgloss.Top,
|
line = lipgloss.JoinHorizontal(lipgloss.Top,
|
||||||
bg.Bold(true).Foreground(s.Primary).Width(2).Render(">"),
|
bg.Bold(true).Foreground(ilovetui.S.Primary).Width(2).Render(">"),
|
||||||
s.Method(r.Method).Background(selBg).Render(r.Method),
|
style.S.Method(r.Method).Background(selBg).Render(r.Method),
|
||||||
bg.Width(2).Render(""),
|
bg.Width(2).Render(""),
|
||||||
bg.Bold(true).Width(hostPathW).Render(r.URL.Host+path),
|
bg.Bold(true).Width(hostPathW).Render(r.URL.Host+path),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
line = lipgloss.JoinHorizontal(lipgloss.Top,
|
line = lipgloss.JoinHorizontal(lipgloss.Top,
|
||||||
" ",
|
" ",
|
||||||
s.Method(r.Method).Render(r.Method),
|
style.S.Method(r.Method).Render(r.Method),
|
||||||
s.Faint.Render(" "),
|
ilovetui.S.Faint.Render(" "),
|
||||||
s.Bold.Render(r.URL.Host),
|
ilovetui.S.Bold.Render(r.URL.Host),
|
||||||
s.Faint.Render(path),
|
ilovetui.S.Faint.Render(path),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
sb.WriteString(line + "\n")
|
sb.WriteString(line + "\n")
|
||||||
@@ -161,17 +149,10 @@ func (m *Model) renderList() string {
|
|||||||
|
|
||||||
func (m *Model) renderResponseList() string {
|
func (m *Model) renderResponseList() string {
|
||||||
if len(m.responseQueue) == 0 {
|
if len(m.responseQueue) == 0 {
|
||||||
return lipgloss.Place(m.responseViewport.Width(), m.responseViewport.Height(), lipgloss.Center, lipgloss.Center, style.S.Faint.Render(util.CenterLines("(҂◡_◡)", "no response yet")))
|
return lipgloss.Place(m.responseViewport.Width(), m.responseViewport.Height(), lipgloss.Center, lipgloss.Center, ilovetui.S.Faint.Render(util.CenterLines("(҂◡_◡)", "no response yet")))
|
||||||
}
|
}
|
||||||
|
|
||||||
s := style.S
|
start, end := util.PageBounds(m.responsePager, len(m.responseQueue))
|
||||||
start, end := m.responsePager.GetSliceBounds(len(m.responseQueue))
|
|
||||||
if start < 0 {
|
|
||||||
start = 0
|
|
||||||
}
|
|
||||||
if end < start {
|
|
||||||
end = start
|
|
||||||
}
|
|
||||||
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
for i, resp := range m.responseQueue[start:end] {
|
for i, resp := range m.responseQueue[start:end] {
|
||||||
@@ -189,7 +170,7 @@ func (m *Model) renderResponseList() string {
|
|||||||
statusStr := fmt.Sprintf("%d", code)
|
statusStr := fmt.Sprintf("%d", code)
|
||||||
|
|
||||||
selected := globalIdx == m.responseCursor
|
selected := globalIdx == m.responseCursor
|
||||||
selBg := s.Selection
|
selBg := ilovetui.S.Selection
|
||||||
|
|
||||||
statusSt := style.StatusStyle(code, 7)
|
statusSt := style.StatusStyle(code, 7)
|
||||||
|
|
||||||
@@ -204,7 +185,7 @@ func (m *Model) renderResponseList() string {
|
|||||||
if selected {
|
if selected {
|
||||||
bg := lipgloss.NewStyle().Background(selBg)
|
bg := lipgloss.NewStyle().Background(selBg)
|
||||||
line = lipgloss.JoinHorizontal(lipgloss.Top,
|
line = lipgloss.JoinHorizontal(lipgloss.Top,
|
||||||
bg.Bold(true).Foreground(s.Primary).Width(2).Render(">"),
|
bg.Bold(true).Foreground(ilovetui.S.Primary).Width(2).Render(">"),
|
||||||
statusSt.Background(selBg).Render(statusStr),
|
statusSt.Background(selBg).Render(statusStr),
|
||||||
bg.Width(2).Render(""),
|
bg.Width(2).Render(""),
|
||||||
bg.Bold(true).Width(hostPathW).Render(f.Request.URL.Host+path),
|
bg.Bold(true).Width(hostPathW).Render(f.Request.URL.Host+path),
|
||||||
@@ -213,9 +194,9 @@ func (m *Model) renderResponseList() string {
|
|||||||
line = lipgloss.JoinHorizontal(lipgloss.Top,
|
line = lipgloss.JoinHorizontal(lipgloss.Top,
|
||||||
" ",
|
" ",
|
||||||
statusSt.Render(statusStr),
|
statusSt.Render(statusStr),
|
||||||
s.Faint.Render(" "),
|
ilovetui.S.Faint.Render(" "),
|
||||||
s.Bold.Render(f.Request.URL.Host),
|
ilovetui.S.Bold.Render(f.Request.URL.Host),
|
||||||
s.Faint.Render(path),
|
ilovetui.S.Faint.Render(path),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
sb.WriteString(line + "\n")
|
sb.WriteString(line + "\n")
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import (
|
|||||||
"charm.land/bubbles/v2/textinput"
|
"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"
|
||||||
|
ilovetui "github.com/anotherhadi/ilovetui"
|
||||||
"github.com/anotherhadi/spilltea/internal/keys"
|
"github.com/anotherhadi/spilltea/internal/keys"
|
||||||
"github.com/anotherhadi/spilltea/internal/plugins"
|
"github.com/anotherhadi/spilltea/internal/plugins"
|
||||||
"github.com/anotherhadi/spilltea/internal/style"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Model struct {
|
type Model struct {
|
||||||
@@ -36,7 +36,7 @@ type Model struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func New(mgr *plugins.Manager) Model {
|
func New(mgr *plugins.Manager) Model {
|
||||||
ta := style.NewTextarea(true)
|
ta := ilovetui.NewTextarea(true)
|
||||||
ta.Placeholder = "plugin configuration..."
|
ta.Placeholder = "plugin configuration..."
|
||||||
ta.Blur()
|
ta.Blur()
|
||||||
|
|
||||||
@@ -45,12 +45,12 @@ func New(mgr *plugins.Manager) Model {
|
|||||||
|
|
||||||
return Model{
|
return Model{
|
||||||
manager: mgr,
|
manager: mgr,
|
||||||
listViewport: style.NewViewport(),
|
listViewport: ilovetui.NewViewport(),
|
||||||
detailViewport: style.NewViewport(),
|
detailViewport: ilovetui.NewViewport(),
|
||||||
textarea: ta,
|
textarea: ta,
|
||||||
filterInput: fi,
|
filterInput: fi,
|
||||||
pager: style.NewPaginator(),
|
pager: ilovetui.NewPaginator(),
|
||||||
help: style.NewHelp(),
|
help: ilovetui.NewHelp(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,14 +70,14 @@ func (m *Model) recalcSizes() {
|
|||||||
}
|
}
|
||||||
m.help.SetWidth(m.width - 2)
|
m.help.SetWidth(m.width - 2)
|
||||||
|
|
||||||
listH, detailH := style.SplitH(m.height, m.renderStatusBar(), 0.4)
|
listH, detailH := ilovetui.SplitH(m.height, m.renderStatusBar(), 0.4)
|
||||||
|
|
||||||
inner := m.width - 2
|
inner := m.width - 2
|
||||||
if inner < 0 {
|
if inner < 0 {
|
||||||
inner = 0
|
inner = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
listVH := style.PanelContentH(listH) - 1 // -1 for the pager dots row
|
listVH := ilovetui.ContentHeight(listH) - 1 // -1 for the pager dots row
|
||||||
if listVH < 0 {
|
if listVH < 0 {
|
||||||
listVH = 0
|
listVH = 0
|
||||||
}
|
}
|
||||||
@@ -90,7 +90,7 @@ func (m *Model) recalcSizes() {
|
|||||||
|
|
||||||
m.filterInput.SetWidth(inner - 2)
|
m.filterInput.SetWidth(inner - 2)
|
||||||
|
|
||||||
detailContentH := style.PanelContentH(detailH)
|
detailContentH := ilovetui.ContentHeight(detailH)
|
||||||
const headerH = 2
|
const headerH = 2
|
||||||
const configFixedH = 2 // blank line + label line
|
const configFixedH = 2 // blank line + label line
|
||||||
textareaH := max(3, detailContentH/3)
|
textareaH := max(3, detailContentH/3)
|
||||||
|
|||||||
@@ -59,11 +59,11 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.textarea.Blur()
|
m.textarea.Blur()
|
||||||
if info, ok := m.selected(); ok && m.manager != nil {
|
if info, ok := m.selected(); ok && m.manager != nil {
|
||||||
val := m.textarea.Value()
|
val := m.textarea.Value()
|
||||||
m.manager.SaveConfig(info.Name, val)
|
m.manager.SaveConfig(info.ID, val)
|
||||||
// Update cached info.
|
// Update cached info.
|
||||||
m.filtered[m.cursor].ConfigText = val
|
m.filtered[m.cursor].ConfigText = val
|
||||||
for i := range m.items {
|
for i := range m.items {
|
||||||
if m.items[i].Name == info.Name {
|
if m.items[i].ID == info.ID {
|
||||||
m.items[i].ConfigText = val
|
m.items[i].ConfigText = val
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -107,10 +107,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
|
|
||||||
case key.Matches(msg, pk.Toggle):
|
case key.Matches(msg, pk.Toggle):
|
||||||
if info, ok := m.selected(); ok && m.manager != nil {
|
if info, ok := m.selected(); ok && m.manager != nil {
|
||||||
m.manager.TogglePlugin(info.Name)
|
m.manager.TogglePlugin(info.ID)
|
||||||
m.filtered[m.cursor].Enabled = !info.Enabled
|
m.filtered[m.cursor].Enabled = !info.Enabled
|
||||||
for i := range m.items {
|
for i := range m.items {
|
||||||
if m.items[i].Name == info.Name {
|
if m.items[i].ID == info.ID {
|
||||||
m.items[i].Enabled = !info.Enabled
|
m.items[i].Enabled = !info.Enabled
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-41
@@ -7,19 +7,18 @@ import (
|
|||||||
tea "charm.land/bubbletea/v2"
|
tea "charm.land/bubbletea/v2"
|
||||||
"charm.land/glamour/v2"
|
"charm.land/glamour/v2"
|
||||||
"charm.land/lipgloss/v2"
|
"charm.land/lipgloss/v2"
|
||||||
"github.com/anotherhadi/spilltea/internal/config"
|
ilovetui "github.com/anotherhadi/ilovetui"
|
||||||
"github.com/anotherhadi/spilltea/internal/icons"
|
"github.com/anotherhadi/spilltea/internal/icons"
|
||||||
"github.com/anotherhadi/spilltea/internal/keys"
|
"github.com/anotherhadi/spilltea/internal/keys"
|
||||||
"github.com/anotherhadi/spilltea/internal/style"
|
|
||||||
"github.com/anotherhadi/spilltea/internal/util"
|
"github.com/anotherhadi/spilltea/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
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(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, style.S.Faint.Render(util.CenterLines("(._.)~*.'", "no plugins loaded"))))
|
return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, ilovetui.S.Faint.Render(util.CenterLines("(._.)~*.'", "no plugins loaded"))))
|
||||||
}
|
}
|
||||||
|
|
||||||
listH, detailH := style.SplitH(m.height, m.renderStatusBar(), 0.4)
|
listH, detailH := ilovetui.SplitH(m.height, m.renderStatusBar(), 0.4)
|
||||||
|
|
||||||
content := lipgloss.JoinVertical(lipgloss.Left,
|
content := lipgloss.JoinVertical(lipgloss.Left,
|
||||||
m.renderListPanel(m.width, listH),
|
m.renderListPanel(m.width, listH),
|
||||||
@@ -30,36 +29,34 @@ 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
|
panelStyle := ilovetui.S.PanelFocused
|
||||||
panelStyle := s.PanelFocused
|
|
||||||
if m.editing {
|
if m.editing {
|
||||||
panelStyle = s.Panel
|
panelStyle = ilovetui.S.Panel
|
||||||
}
|
}
|
||||||
var dots string
|
var dots string
|
||||||
if len(m.filtered) > 0 {
|
if len(m.filtered) > 0 {
|
||||||
dots = s.Faint.Render(m.pager.View())
|
dots = ilovetui.S.Faint.Render(m.pager.View())
|
||||||
}
|
}
|
||||||
inner := lipgloss.JoinVertical(lipgloss.Left,
|
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),
|
||||||
)
|
)
|
||||||
return style.RenderWithTitle(panelStyle, icons.I.Plugin+"Plugins", inner, w, h)
|
return ilovetui.RenderWithTitle(panelStyle, icons.I.Plugin+"Plugins", inner, w, h)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) renderDetailPanel(h int) string {
|
func (m *Model) renderDetailPanel(h int) string {
|
||||||
s := style.S
|
panelStyle := ilovetui.S.Panel
|
||||||
panelStyle := s.Panel
|
|
||||||
if m.editing {
|
if m.editing {
|
||||||
panelStyle = s.PanelFocused
|
panelStyle = ilovetui.S.PanelFocused
|
||||||
}
|
}
|
||||||
info, ok := m.selected()
|
info, ok := m.selected()
|
||||||
if !ok {
|
if !ok {
|
||||||
return style.RenderWithTitle(panelStyle, icons.I.Detail+"Detail", "", m.width, h)
|
return ilovetui.RenderWithTitle(panelStyle, icons.I.Detail+"Detail", "", m.width, h)
|
||||||
}
|
}
|
||||||
|
|
||||||
statusSt := lipgloss.NewStyle().Foreground(s.Error)
|
statusSt := lipgloss.NewStyle().Foreground(ilovetui.S.Error)
|
||||||
if info.Enabled {
|
if info.Enabled {
|
||||||
statusSt = lipgloss.NewStyle().Foreground(s.Success)
|
statusSt = lipgloss.NewStyle().Foreground(ilovetui.S.Success)
|
||||||
}
|
}
|
||||||
status := "disabled"
|
status := "disabled"
|
||||||
if info.Enabled {
|
if info.Enabled {
|
||||||
@@ -69,26 +66,26 @@ func (m *Model) renderDetailPanel(h int) string {
|
|||||||
pad := lipgloss.NewStyle().Padding(0, 1)
|
pad := lipgloss.NewStyle().Padding(0, 1)
|
||||||
|
|
||||||
header := pad.Render(
|
header := pad.Render(
|
||||||
s.Bold.Render(info.Name) + " " + statusSt.Render(status) + "\n" +
|
ilovetui.S.Bold.Render(info.Name) + " " + statusSt.Render(status) + "\n" +
|
||||||
s.Faint.Render(filepath.Base(info.FilePath)),
|
ilovetui.S.Faint.Render(filepath.Base(info.FilePath)),
|
||||||
)
|
)
|
||||||
|
|
||||||
parts := []string{header, m.detailViewport.View()}
|
parts := []string{header, ilovetui.ViewportView(&m.detailViewport)}
|
||||||
|
|
||||||
if m.hasConfig() {
|
if m.hasConfig() {
|
||||||
var configLabel string
|
var configLabel string
|
||||||
if m.editing {
|
if m.editing {
|
||||||
escKey := keys.Keys.Global.Escape.Help().Key
|
escKey := keys.Keys.Global.Escape.Help().Key
|
||||||
configLabel = pad.Render(s.Faint.Render("editing config (" + escKey + " to save):"))
|
configLabel = pad.Render(ilovetui.S.Faint.Render("editing config (" + escKey + " to save):"))
|
||||||
} else {
|
} else {
|
||||||
editKey := keys.Keys.Plugins.EditConfig.Help().Key
|
editKey := keys.Keys.Plugins.EditConfig.Help().Key
|
||||||
configLabel = pad.Render(s.Faint.Render("config (" + editKey + " to edit):"))
|
configLabel = pad.Render(ilovetui.S.Faint.Render("config (" + editKey + " to edit):"))
|
||||||
}
|
}
|
||||||
parts = append(parts, "", configLabel, pad.Render(m.textarea.View()))
|
parts = append(parts, "", configLabel, pad.Render(m.textarea.View()))
|
||||||
}
|
}
|
||||||
|
|
||||||
inner := lipgloss.JoinVertical(lipgloss.Left, parts...)
|
inner := lipgloss.JoinVertical(lipgloss.Left, parts...)
|
||||||
return style.RenderWithTitle(panelStyle, icons.I.Detail+"Detail", inner, m.width, h)
|
return ilovetui.RenderWithTitle(panelStyle, icons.I.Detail+"Detail", inner, m.width, h)
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderPluginDescription(desc string, width int) string {
|
func renderPluginDescription(desc string, width int) string {
|
||||||
@@ -100,7 +97,7 @@ func renderPluginDescription(desc string, width int) string {
|
|||||||
desc = strings.Join(lines, "\n")
|
desc = strings.Join(lines, "\n")
|
||||||
|
|
||||||
r, err := glamour.NewTermRenderer(
|
r, err := glamour.NewTermRenderer(
|
||||||
glamour.WithStyles(style.GlamourStyleConfig(config.Global)),
|
glamour.WithStyles(ilovetui.GlamourStyleConfig()),
|
||||||
glamour.WithWordWrap(width),
|
glamour.WithWordWrap(width),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -114,23 +111,21 @@ func renderPluginDescription(desc string, width int) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) renderStatusBar() string {
|
func (m *Model) renderStatusBar() string {
|
||||||
s := style.S
|
|
||||||
pad := lipgloss.NewStyle().Padding(0, 1)
|
pad := lipgloss.NewStyle().Padding(0, 1)
|
||||||
filterKey := keys.Keys.Plugins.Filter.Help().Key
|
filterKey := keys.Keys.Plugins.Filter.Help().Key
|
||||||
if m.filtering {
|
if m.filtering {
|
||||||
return pad.Render(s.Faint.Render(filterKey) + " " + m.filterInput.View())
|
return pad.Render(ilovetui.S.Faint.Render(filterKey) + " " + m.filterInput.View())
|
||||||
}
|
}
|
||||||
if m.filter != "" {
|
if m.filter != "" {
|
||||||
escKey := keys.Keys.Global.Escape.Help().Key
|
escKey := keys.Keys.Global.Escape.Help().Key
|
||||||
accent := lipgloss.NewStyle().Foreground(s.Primary)
|
accent := lipgloss.NewStyle().Foreground(ilovetui.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) + " " + ilovetui.S.Bold.Render(m.filter) + ilovetui.S.Faint.Render(" "+escKey+" to clear"))
|
||||||
return lipgloss.JoinVertical(lipgloss.Left, filterLine, pad.Render(m.help.View(pluginsKeyMap{editing: m.editing, hasConfig: m.hasConfig(), width: m.width})))
|
return lipgloss.JoinVertical(lipgloss.Left, filterLine, pad.Render(m.help.View(pluginsKeyMap{editing: m.editing, hasConfig: m.hasConfig(), width: m.width})))
|
||||||
}
|
}
|
||||||
return pad.Render(m.help.View(pluginsKeyMap{editing: m.editing, hasConfig: m.hasConfig(), width: m.width}))
|
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 {
|
||||||
s := style.S
|
|
||||||
if len(m.filtered) == 0 {
|
if len(m.filtered) == 0 {
|
||||||
msg := util.CenterLines("(ง •̀_•́)ง", "no plugins", "", "spilltea --add-default-plugins")
|
msg := util.CenterLines("(ง •̀_•́)ง", "no plugins", "", "spilltea --add-default-plugins")
|
||||||
if m.filter != "" {
|
if m.filter != "" {
|
||||||
@@ -139,27 +134,21 @@ func (m *Model) renderList() string {
|
|||||||
return lipgloss.Place(
|
return lipgloss.Place(
|
||||||
m.listViewport.Width(), m.listViewport.Height(),
|
m.listViewport.Width(), m.listViewport.Height(),
|
||||||
lipgloss.Center, lipgloss.Center,
|
lipgloss.Center, lipgloss.Center,
|
||||||
s.Faint.Render(msg),
|
ilovetui.S.Faint.Render(msg),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
start, end := m.pager.GetSliceBounds(len(m.filtered))
|
start, end := util.PageBounds(m.pager, len(m.filtered))
|
||||||
if start < 0 {
|
|
||||||
start = 0
|
|
||||||
}
|
|
||||||
if end < start {
|
|
||||||
end = start
|
|
||||||
}
|
|
||||||
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
for i, p := range m.filtered[start:end] {
|
for i, p := range m.filtered[start:end] {
|
||||||
globalIdx := start + i
|
globalIdx := start + i
|
||||||
selected := globalIdx == m.cursor
|
selected := globalIdx == m.cursor
|
||||||
|
|
||||||
enabledSt := lipgloss.NewStyle().Foreground(s.Error)
|
enabledSt := lipgloss.NewStyle().Foreground(ilovetui.S.Error)
|
||||||
enabledStr := "off"
|
enabledStr := "off"
|
||||||
if p.Enabled {
|
if p.Enabled {
|
||||||
enabledSt = lipgloss.NewStyle().Foreground(s.Success)
|
enabledSt = lipgloss.NewStyle().Foreground(ilovetui.S.Success)
|
||||||
enabledStr = "on "
|
enabledStr = "on "
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,10 +161,10 @@ func (m *Model) renderList() string {
|
|||||||
|
|
||||||
var line string
|
var line string
|
||||||
if selected {
|
if selected {
|
||||||
bg := lipgloss.NewStyle().Background(s.Selection)
|
bg := lipgloss.NewStyle().Background(ilovetui.S.Selection)
|
||||||
line = lipgloss.JoinHorizontal(lipgloss.Top,
|
line = lipgloss.JoinHorizontal(lipgloss.Top,
|
||||||
bg.Bold(true).Foreground(s.Primary).Width(2).Render(">"),
|
bg.Bold(true).Foreground(ilovetui.S.Primary).Width(2).Render(">"),
|
||||||
enabledSt.Background(s.Selection).Width(3).Render(enabledStr),
|
enabledSt.Background(ilovetui.S.Selection).Width(3).Render(enabledStr),
|
||||||
bg.Width(1).Render(""),
|
bg.Width(1).Render(""),
|
||||||
bg.Bold(true).Width(nameW).Render(p.Name),
|
bg.Bold(true).Width(nameW).Render(p.Name),
|
||||||
)
|
)
|
||||||
@@ -184,7 +173,7 @@ func (m *Model) renderList() string {
|
|||||||
" ",
|
" ",
|
||||||
enabledSt.Width(3).Render(enabledStr),
|
enabledSt.Width(3).Render(enabledStr),
|
||||||
" ",
|
" ",
|
||||||
s.Bold.Render(p.Name),
|
ilovetui.S.Bold.Render(p.Name),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
sb.WriteString(line + "\n")
|
sb.WriteString(line + "\n")
|
||||||
|
|||||||
+10
-10
@@ -9,9 +9,9 @@ import (
|
|||||||
"charm.land/bubbles/v2/textarea"
|
"charm.land/bubbles/v2/textarea"
|
||||||
"charm.land/bubbles/v2/viewport"
|
"charm.land/bubbles/v2/viewport"
|
||||||
tea "charm.land/bubbletea/v2"
|
tea "charm.land/bubbletea/v2"
|
||||||
|
ilovetui "github.com/anotherhadi/ilovetui"
|
||||||
"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"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type SendToReplayMsg struct {
|
type SendToReplayMsg struct {
|
||||||
@@ -61,15 +61,15 @@ type Model struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func New() Model {
|
func New() Model {
|
||||||
ta := style.NewTextarea(false)
|
ta := ilovetui.NewTextarea(false)
|
||||||
ta.Blur()
|
ta.Blur()
|
||||||
return Model{
|
return Model{
|
||||||
listViewport: style.NewViewport(),
|
listViewport: ilovetui.NewViewport(),
|
||||||
requestViewport: style.NewViewport(),
|
requestViewport: ilovetui.NewViewport(),
|
||||||
responseViewport: style.NewViewport(),
|
responseViewport: ilovetui.NewViewport(),
|
||||||
textarea: ta,
|
textarea: ta,
|
||||||
pager: style.NewPaginator(),
|
pager: ilovetui.NewPaginator(),
|
||||||
help: style.NewHelp(),
|
help: ilovetui.NewHelp(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,13 +149,13 @@ func (m *Model) SetSize(w, h int) {
|
|||||||
func (m *Model) recalcSizes() {
|
func (m *Model) recalcSizes() {
|
||||||
m.help.SetWidth(m.width - 2)
|
m.help.SetWidth(m.width - 2)
|
||||||
|
|
||||||
listH, bodyH := style.SplitH(m.height, m.renderStatusBar(), 0.35)
|
listH, bodyH := ilovetui.SplitH(m.height, m.renderStatusBar(), 0.35)
|
||||||
|
|
||||||
listInner := m.width - 2
|
listInner := m.width - 2
|
||||||
if listInner < 0 {
|
if listInner < 0 {
|
||||||
listInner = 0
|
listInner = 0
|
||||||
}
|
}
|
||||||
listVH := style.PanelContentH(listH) - 1 // -1 for the pager dots row
|
listVH := ilovetui.ContentHeight(listH) - 1 // -1 for the pager dots row
|
||||||
if listVH < 0 {
|
if listVH < 0 {
|
||||||
listVH = 0
|
listVH = 0
|
||||||
}
|
}
|
||||||
@@ -175,7 +175,7 @@ func (m *Model) recalcSizes() {
|
|||||||
if rightInner < 0 {
|
if rightInner < 0 {
|
||||||
rightInner = 0
|
rightInner = 0
|
||||||
}
|
}
|
||||||
bodyVH := style.PanelContentH(bodyH)
|
bodyVH := ilovetui.ContentHeight(bodyH)
|
||||||
|
|
||||||
m.requestViewport.SetWidth(leftInner)
|
m.requestViewport.SetWidth(leftInner)
|
||||||
m.requestViewport.SetHeight(bodyVH)
|
m.requestViewport.SetHeight(bodyVH)
|
||||||
|
|||||||
@@ -16,12 +16,13 @@ import (
|
|||||||
tea "charm.land/bubbletea/v2"
|
tea "charm.land/bubbletea/v2"
|
||||||
"charm.land/lipgloss/v2"
|
"charm.land/lipgloss/v2"
|
||||||
"github.com/andybalholm/brotli"
|
"github.com/andybalholm/brotli"
|
||||||
|
ilovetui "github.com/anotherhadi/ilovetui"
|
||||||
"github.com/anotherhadi/spilltea/internal/config"
|
"github.com/anotherhadi/spilltea/internal/config"
|
||||||
"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"
|
||||||
"github.com/anotherhadi/spilltea/internal/util"
|
|
||||||
diffUI "github.com/anotherhadi/spilltea/internal/ui/diff"
|
diffUI "github.com/anotherhadi/spilltea/internal/ui/diff"
|
||||||
|
"github.com/anotherhadi/spilltea/internal/util"
|
||||||
"github.com/klauspost/compress/zstd"
|
"github.com/klauspost/compress/zstd"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -181,6 +182,9 @@ func (m Model) updateNormalMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
|
|||||||
|
|
||||||
case key.Matches(msg, r.EditExt):
|
case key.Matches(msg, r.EditExt):
|
||||||
if len(m.entries) > 0 {
|
if len(m.entries) > 0 {
|
||||||
|
if m.focusedPanel == panelResponse {
|
||||||
|
return m, util.OpenExternalEditorView(m.entries[m.cursor].ResponseRaw)
|
||||||
|
}
|
||||||
return m, util.OpenExternalEditor(m.entries[m.cursor].RequestRaw)
|
return m, util.OpenExternalEditor(m.entries[m.cursor].RequestRaw)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,11 +351,11 @@ func (m *Model) refreshBody() {
|
|||||||
m.requestViewport.SetXOffset(0)
|
m.requestViewport.SetXOffset(0)
|
||||||
|
|
||||||
if e.Sending {
|
if e.Sending {
|
||||||
m.responseViewport.SetContent(lipgloss.Place(m.responseViewport.Width(), m.responseViewport.Height(), lipgloss.Center, lipgloss.Center, style.S.Faint.Render(util.CenterLines("(ノ◕ヮ◕)ノ*:・゚", "sending..."))))
|
m.responseViewport.SetContent(lipgloss.Place(m.responseViewport.Width(), m.responseViewport.Height(), lipgloss.Center, lipgloss.Center, ilovetui.S.Faint.Render(util.CenterLines("(ノ◕ヮ◕)ノ*:・゚", "sending..."))))
|
||||||
} else if e.ResponseRaw != "" {
|
} else if e.ResponseRaw != "" {
|
||||||
m.responseViewport.SetContent(style.HighlightHTTP(e.ResponseRaw))
|
m.responseViewport.SetContent(style.HighlightHTTP(e.ResponseRaw))
|
||||||
} else {
|
} else {
|
||||||
m.responseViewport.SetContent(lipgloss.Place(m.responseViewport.Width(), m.responseViewport.Height(), lipgloss.Center, lipgloss.Center, style.S.Faint.Render(util.CenterLines("( •_•)>⌐■", "press send to fire"))))
|
m.responseViewport.SetContent(lipgloss.Place(m.responseViewport.Width(), m.responseViewport.Height(), lipgloss.Center, lipgloss.Center, ilovetui.S.Faint.Render(util.CenterLines("( •_•)>⌐■", "press send to fire"))))
|
||||||
}
|
}
|
||||||
m.responseViewport.SetYOffset(0)
|
m.responseViewport.SetYOffset(0)
|
||||||
m.responseViewport.SetXOffset(0)
|
m.responseViewport.SetXOffset(0)
|
||||||
|
|||||||
+25
-34
@@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
tea "charm.land/bubbletea/v2"
|
tea "charm.land/bubbletea/v2"
|
||||||
"charm.land/lipgloss/v2"
|
"charm.land/lipgloss/v2"
|
||||||
|
ilovetui "github.com/anotherhadi/ilovetui"
|
||||||
"github.com/anotherhadi/spilltea/internal/icons"
|
"github.com/anotherhadi/spilltea/internal/icons"
|
||||||
"github.com/anotherhadi/spilltea/internal/style"
|
"github.com/anotherhadi/spilltea/internal/style"
|
||||||
"github.com/anotherhadi/spilltea/internal/util"
|
"github.com/anotherhadi/spilltea/internal/util"
|
||||||
@@ -16,7 +17,7 @@ func (m Model) View() tea.View {
|
|||||||
return tea.NewView("Loading...")
|
return tea.NewView("Loading...")
|
||||||
}
|
}
|
||||||
|
|
||||||
listH, bodyH := style.SplitH(m.height, m.renderStatusBar(), 0.35)
|
listH, bodyH := ilovetui.SplitH(m.height, m.renderStatusBar(), 0.35)
|
||||||
leftW, rightW := m.bodyHalfWidths()
|
leftW, rightW := m.bodyHalfWidths()
|
||||||
|
|
||||||
bodyRow := lipgloss.JoinHorizontal(lipgloss.Top,
|
bodyRow := lipgloss.JoinHorizontal(lipgloss.Top,
|
||||||
@@ -33,45 +34,42 @@ 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
|
panelStyle := ilovetui.S.Panel
|
||||||
panelStyle := s.Panel
|
|
||||||
if !m.editing && m.focusedPanel == panelList {
|
if !m.editing && m.focusedPanel == panelList {
|
||||||
panelStyle = s.PanelFocused
|
panelStyle = ilovetui.S.PanelFocused
|
||||||
}
|
}
|
||||||
var dots string
|
var dots string
|
||||||
if len(m.entries) > 0 {
|
if len(m.entries) > 0 {
|
||||||
dots = s.Faint.Render(m.pager.View())
|
dots = ilovetui.S.Faint.Render(m.pager.View())
|
||||||
}
|
}
|
||||||
inner := lipgloss.JoinVertical(lipgloss.Left,
|
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),
|
||||||
)
|
)
|
||||||
return style.RenderWithTitle(panelStyle, icons.I.Replay+"Replay", inner, w, h)
|
return ilovetui.RenderWithTitle(panelStyle, icons.I.Replay+"Replay", inner, w, h)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) renderRequestPanel(w, h int) string {
|
func (m *Model) renderRequestPanel(w, h int) string {
|
||||||
s := style.S
|
|
||||||
var body string
|
var body string
|
||||||
border := s.Panel
|
border := ilovetui.S.Panel
|
||||||
if m.editing {
|
if m.editing {
|
||||||
body = m.textarea.View()
|
body = m.textarea.View()
|
||||||
border = s.PanelFocused
|
border = ilovetui.S.PanelFocused
|
||||||
} else {
|
} else {
|
||||||
body = m.requestViewport.View()
|
body = ilovetui.ViewportView(&m.requestViewport)
|
||||||
if m.focusedPanel == panelRequest {
|
if m.focusedPanel == panelRequest {
|
||||||
border = s.PanelFocused
|
border = ilovetui.S.PanelFocused
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return style.RenderWithTitle(border, icons.I.Request+"Request", body, w, h)
|
return ilovetui.RenderWithTitle(border, icons.I.Request+"Request", body, w, h)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) renderResponsePanel(w, h int) string {
|
func (m *Model) renderResponsePanel(w, h int) string {
|
||||||
s := style.S
|
border := ilovetui.S.Panel
|
||||||
border := s.Panel
|
|
||||||
if !m.editing && m.focusedPanel == panelResponse {
|
if !m.editing && m.focusedPanel == panelResponse {
|
||||||
border = s.PanelFocused
|
border = ilovetui.S.PanelFocused
|
||||||
}
|
}
|
||||||
return style.RenderWithTitle(border, icons.I.Response+"Response", m.responseViewport.View(), w, h)
|
return ilovetui.RenderWithTitle(border, icons.I.Response+"Response", ilovetui.ViewportView(&m.responseViewport), w, h)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) renderStatusBar() string {
|
func (m *Model) renderStatusBar() string {
|
||||||
@@ -83,24 +81,17 @@ func (m *Model) renderList() string {
|
|||||||
return lipgloss.Place(
|
return lipgloss.Place(
|
||||||
m.listViewport.Width(), m.listViewport.Height(),
|
m.listViewport.Width(), m.listViewport.Height(),
|
||||||
lipgloss.Center, lipgloss.Center,
|
lipgloss.Center, lipgloss.Center,
|
||||||
style.S.Faint.Render(util.CenterLines("(╥﹏╥)", "send a request from History or Intercept")),
|
ilovetui.S.Faint.Render(util.CenterLines("(╥﹏╥)", "send a request from History or Intercept")),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
s := style.S
|
start, end := util.PageBounds(m.pager, len(m.entries))
|
||||||
start, end := m.pager.GetSliceBounds(len(m.entries))
|
|
||||||
if start < 0 {
|
|
||||||
start = 0
|
|
||||||
}
|
|
||||||
if end < start {
|
|
||||||
end = start
|
|
||||||
}
|
|
||||||
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
for i, e := range m.entries[start:end] {
|
for i, e := range m.entries[start:end] {
|
||||||
globalIdx := start + i
|
globalIdx := start + i
|
||||||
selected := globalIdx == m.cursor
|
selected := globalIdx == m.cursor
|
||||||
selBg := s.Selection
|
selBg := ilovetui.S.Selection
|
||||||
|
|
||||||
w := m.listViewport.Width()
|
w := m.listViewport.Width()
|
||||||
const fixedW = 2 + 7 + 1 + 3 + 1
|
const fixedW = 2 + 7 + 1 + 3 + 1
|
||||||
@@ -115,8 +106,8 @@ func (m *Model) renderList() string {
|
|||||||
if selected {
|
if selected {
|
||||||
bg := lipgloss.NewStyle().Background(selBg)
|
bg := lipgloss.NewStyle().Background(selBg)
|
||||||
line = lipgloss.JoinHorizontal(lipgloss.Top,
|
line = lipgloss.JoinHorizontal(lipgloss.Top,
|
||||||
bg.Bold(true).Foreground(s.Primary).Width(2).Render(">"),
|
bg.Bold(true).Foreground(ilovetui.S.Primary).Width(2).Render(">"),
|
||||||
s.Method(e.Method).Background(selBg).Render(e.Method),
|
style.S.Method(e.Method).Background(selBg).Render(e.Method),
|
||||||
bg.Width(1).Render(""),
|
bg.Width(1).Render(""),
|
||||||
statusSt.Background(selBg).Render(statusStr),
|
statusSt.Background(selBg).Render(statusStr),
|
||||||
bg.Width(1).Render(""),
|
bg.Width(1).Render(""),
|
||||||
@@ -125,12 +116,12 @@ func (m *Model) renderList() string {
|
|||||||
} else {
|
} else {
|
||||||
line = lipgloss.JoinHorizontal(lipgloss.Top,
|
line = lipgloss.JoinHorizontal(lipgloss.Top,
|
||||||
" ",
|
" ",
|
||||||
s.Method(e.Method).Render(e.Method),
|
style.S.Method(e.Method).Render(e.Method),
|
||||||
" ",
|
" ",
|
||||||
statusSt.Render(statusStr),
|
statusSt.Render(statusStr),
|
||||||
" ",
|
" ",
|
||||||
s.Bold.Render(e.Host),
|
ilovetui.S.Bold.Render(e.Host),
|
||||||
s.Faint.Render(e.Path),
|
ilovetui.S.Faint.Render(e.Path),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
sb.WriteString(line + "\n")
|
sb.WriteString(line + "\n")
|
||||||
@@ -142,11 +133,11 @@ func entryStatus(e Entry) (string, lipgloss.Style) {
|
|||||||
base := lipgloss.NewStyle().Bold(true).Width(3)
|
base := lipgloss.NewStyle().Bold(true).Width(3)
|
||||||
switch {
|
switch {
|
||||||
case e.Sending:
|
case e.Sending:
|
||||||
return "···", base.Foreground(style.S.Subtle)
|
return "···", base.Foreground(ilovetui.S.Subtle)
|
||||||
case e.Err != nil:
|
case e.Err != nil:
|
||||||
return "ERR", base.Foreground(style.S.Error)
|
return "ERR", base.Foreground(ilovetui.S.Error)
|
||||||
case e.StatusCode == 0:
|
case e.StatusCode == 0:
|
||||||
return "---", base.Foreground(style.S.Subtle)
|
return "---", base.Foreground(ilovetui.S.Subtle)
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%3d", e.StatusCode), style.StatusStyle(e.StatusCode, 3)
|
return fmt.Sprintf("%3d", e.StatusCode), style.StatusStyle(e.StatusCode, 3)
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-3
@@ -1,6 +1,7 @@
|
|||||||
package util
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
|
||||||
@@ -14,7 +15,7 @@ type EditorFinishedMsg struct {
|
|||||||
Err error
|
Err error
|
||||||
}
|
}
|
||||||
|
|
||||||
func OpenExternalEditor(content string) tea.Cmd {
|
func resolveEditor() string {
|
||||||
editor := config.Global.App.ExternalEditor
|
editor := config.Global.App.ExternalEditor
|
||||||
if editor == "" {
|
if editor == "" {
|
||||||
editor = os.Getenv("EDITOR")
|
editor = os.Getenv("EDITOR")
|
||||||
@@ -22,15 +23,27 @@ func OpenExternalEditor(content string) tea.Cmd {
|
|||||||
if editor == "" {
|
if editor == "" {
|
||||||
editor = "vi"
|
editor = "vi"
|
||||||
}
|
}
|
||||||
|
return editor
|
||||||
|
}
|
||||||
|
|
||||||
|
func openWithEditor(content string, callback func(string, error) tea.Msg) tea.Cmd {
|
||||||
f, err := os.CreateTemp("", "spilltea-*.http")
|
f, err := os.CreateTemp("", "spilltea-*.http")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
tmpPath := f.Name()
|
tmpPath := f.Name()
|
||||||
_, _ = f.WriteString(content)
|
if _, err := f.WriteString(content); err != nil {
|
||||||
|
log.Printf("editor: writing temp file: %v", err)
|
||||||
|
}
|
||||||
f.Close()
|
f.Close()
|
||||||
return tea.ExecProcess(exec.Command(editor, tmpPath), func(err error) tea.Msg {
|
return tea.ExecProcess(exec.Command(resolveEditor(), tmpPath), func(err error) tea.Msg {
|
||||||
defer os.Remove(tmpPath)
|
defer os.Remove(tmpPath)
|
||||||
|
return callback(tmpPath, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func OpenExternalEditor(content string) tea.Cmd {
|
||||||
|
return openWithEditor(content, func(tmpPath string, err error) tea.Msg {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return EditorFinishedMsg{Err: err}
|
return EditorFinishedMsg{Err: err}
|
||||||
}
|
}
|
||||||
@@ -41,3 +54,9 @@ func OpenExternalEditor(content string) tea.Cmd {
|
|||||||
return EditorFinishedMsg{Content: string(data)}
|
return EditorFinishedMsg{Content: string(data)}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func OpenExternalEditorView(content string) tea.Cmd {
|
||||||
|
return openWithEditor(content, func(_ string, _ error) tea.Msg {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package util
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"charm.land/bubbles/v2/paginator"
|
||||||
"charm.land/lipgloss/v2"
|
"charm.land/lipgloss/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,6 +29,18 @@ func CenterLines(lines ...string) string {
|
|||||||
return strings.Join(centered, "\n")
|
return strings.Join(centered, "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PageBounds returns clamped start/end indices for rendering a paginated list.
|
||||||
|
func PageBounds(p paginator.Model, total int) (start, end int) {
|
||||||
|
start, end = p.GetSliceBounds(total)
|
||||||
|
if start < 0 {
|
||||||
|
start = 0
|
||||||
|
}
|
||||||
|
if end < start {
|
||||||
|
end = start
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// InferScheme returns "http" for port 80, "https" otherwise.
|
// InferScheme returns "http" for port 80, "https" otherwise.
|
||||||
func InferScheme(host string) string {
|
func InferScheme(host string) string {
|
||||||
if strings.HasSuffix(host, ":80") {
|
if strings.HasSuffix(host, ":80") {
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
schema = 3
|
||||||
|
|
||||||
|
[mod]
|
||||||
|
[mod.'charm.land/bubbles/v2']
|
||||||
|
version = 'v2.1.0'
|
||||||
|
hash = 'sha256-2OmqpBrl+taOJzAhVM6OReLmoYRxZOXx9JqFNjQjsPA='
|
||||||
|
|
||||||
|
[mod.'charm.land/bubbletea/v2']
|
||||||
|
version = 'v2.0.6'
|
||||||
|
hash = 'sha256-1jxXmcnI4peUE0Xs3HGe57pIhRONx235aAaeqm2r434='
|
||||||
|
|
||||||
|
[mod.'charm.land/glamour/v2']
|
||||||
|
version = 'v2.0.0'
|
||||||
|
hash = 'sha256-CZYlNGw2MihqnSHf1Xxqz55NnqW9fVpLxyvLItryIw4='
|
||||||
|
|
||||||
|
[mod.'charm.land/lipgloss/v2']
|
||||||
|
version = 'v2.0.3'
|
||||||
|
hash = 'sha256-/RFkSUscU3NwymzT+PfizGf3XyQIdVGQlX7vxktCUGk='
|
||||||
|
|
||||||
|
[mod.'github.com/alecthomas/chroma/v2']
|
||||||
|
version = 'v2.24.1'
|
||||||
|
hash = 'sha256-DufsljWRKireFuLFcnPozuF0N3UoRYGlEfNFMD+z0ng='
|
||||||
|
|
||||||
|
[mod.'github.com/andybalholm/brotli']
|
||||||
|
version = 'v1.0.4'
|
||||||
|
hash = 'sha256-gAnPRdGP4yna4hiRIEDyBtDOVJqd7RU27wlPu96Rdf8='
|
||||||
|
|
||||||
|
[mod.'github.com/anotherhadi/ilovetui']
|
||||||
|
version = 'v0.1.6'
|
||||||
|
hash = 'sha256-7E+7UFks5vM3XWCvX2joFmHGcW7qqnoox6ZPFglaLO4='
|
||||||
|
|
||||||
|
[mod.'github.com/atotto/clipboard']
|
||||||
|
version = 'v0.1.4'
|
||||||
|
hash = 'sha256-ZZ7U5X0gWOu8zcjZcWbcpzGOGdycwq0TjTFh/eZHjXk='
|
||||||
|
|
||||||
|
[mod.'github.com/aymerick/douceur']
|
||||||
|
version = 'v0.2.0'
|
||||||
|
hash = 'sha256-NiBX8EfOvLXNiK3pJaZX4N73YgfzdrzRXdiBFe3X3sE='
|
||||||
|
|
||||||
|
[mod.'github.com/charmbracelet/colorprofile']
|
||||||
|
version = 'v0.4.3'
|
||||||
|
hash = 'sha256-y+QDUxGOKhugEMQLRUTZYT2C+wKqYHnMLJ44jbh7+JA='
|
||||||
|
|
||||||
|
[mod.'github.com/charmbracelet/ultraviolet']
|
||||||
|
version = 'v0.0.0-20260511121909-c840852527f3'
|
||||||
|
hash = 'sha256-oWEEaaetCmXvflD03MWy8qD105Kd62iI62+lxofE9Ns='
|
||||||
|
|
||||||
|
[mod.'github.com/charmbracelet/x/ansi']
|
||||||
|
version = 'v0.11.7'
|
||||||
|
hash = 'sha256-q8BZJq4K7NE5ETocN9/G/EoV0dUyD703ONSfHiUYzWQ='
|
||||||
|
|
||||||
|
[mod.'github.com/charmbracelet/x/exp/slice']
|
||||||
|
version = 'v0.0.0-20260517005351-920740d613be'
|
||||||
|
hash = 'sha256-bMsbEzP1gHA2OJx4zMYZUI3UFhcTG+mcFm8rRY+Khh8='
|
||||||
|
|
||||||
|
[mod.'github.com/charmbracelet/x/term']
|
||||||
|
version = 'v0.2.2'
|
||||||
|
hash = 'sha256-KF7IU1Luxl/sZP6XjomWB2e3lxSUS4/5AahhapGir/4='
|
||||||
|
|
||||||
|
[mod.'github.com/charmbracelet/x/termios']
|
||||||
|
version = 'v0.1.1'
|
||||||
|
hash = 'sha256-sri3LpHCBhGvnJldDzBxwbbZpeSGZVCJFOUL45uBFds='
|
||||||
|
|
||||||
|
[mod.'github.com/charmbracelet/x/windows']
|
||||||
|
version = 'v0.2.2'
|
||||||
|
hash = 'sha256-CvmE8kAC5wlPSeWjl2hc5xizvGS2FeOLHw84froldkk='
|
||||||
|
|
||||||
|
[mod.'github.com/clipperhouse/displaywidth']
|
||||||
|
version = 'v0.11.0'
|
||||||
|
hash = 'sha256-WokyTaofEy95xlshqK5YDzpemhXV5oaQifxS9YyfCXo='
|
||||||
|
|
||||||
|
[mod.'github.com/clipperhouse/uax29/v2']
|
||||||
|
version = 'v2.7.0'
|
||||||
|
hash = 'sha256-GO3az7WiGcwU0OvmocwdfR5ohGRL8NbjscIaMyhAdxE='
|
||||||
|
|
||||||
|
[mod.'github.com/dlclark/regexp2']
|
||||||
|
version = 'v1.12.0'
|
||||||
|
hash = 'sha256-PVX2rDCkiG0vyA1CbDi3bzLeZ2T8hcqJv3pZ5YwGzMI='
|
||||||
|
|
||||||
|
[mod.'github.com/dustin/go-humanize']
|
||||||
|
version = 'v1.0.1'
|
||||||
|
hash = 'sha256-yuvxYYngpfVkUg9yAmG99IUVmADTQA0tMbBXe0Fq0Mc='
|
||||||
|
|
||||||
|
[mod.'github.com/fsnotify/fsnotify']
|
||||||
|
version = 'v1.10.1'
|
||||||
|
hash = 'sha256-6LBLgsh4nKkMpgRKVsYFEaGDSU1fncBcWVSjKBdfgjU='
|
||||||
|
|
||||||
|
[mod.'github.com/go-viper/mapstructure/v2']
|
||||||
|
version = 'v2.5.0'
|
||||||
|
hash = 'sha256-LbrCBANBprVI84M0CWrXc7rriJL5ac5VKbh58LBTw7U='
|
||||||
|
|
||||||
|
[mod.'github.com/golang/groupcache']
|
||||||
|
version = 'v0.0.0-20210331224755-41bb18bfe9da'
|
||||||
|
hash = 'sha256-7Gs7CS9gEYZkbu5P4hqPGBpeGZWC64VDwraSKFF+VR0='
|
||||||
|
|
||||||
|
[mod.'github.com/google/uuid']
|
||||||
|
version = 'v1.6.0'
|
||||||
|
hash = 'sha256-VWl9sqUzdOuhW0KzQlv0gwwUQClYkmZwSydHG2sALYw='
|
||||||
|
|
||||||
|
[mod.'github.com/gorilla/css']
|
||||||
|
version = 'v1.0.1'
|
||||||
|
hash = 'sha256-6JwNHqlY2NpZ0pSQTyYPSpiNqjXOdFHqrUT10sv3y8A='
|
||||||
|
|
||||||
|
[mod.'github.com/gorilla/websocket']
|
||||||
|
version = 'v1.5.0'
|
||||||
|
hash = 'sha256-EYVgkSEMo4HaVrsWKqnsYRp8SSS8gNf7t+Elva02Ofc='
|
||||||
|
|
||||||
|
[mod.'github.com/klauspost/compress']
|
||||||
|
version = 'v1.17.8'
|
||||||
|
hash = 'sha256-8rgCCfHX29le8m6fyVn6gwFde5TPUHjwQqZqv9JIubs='
|
||||||
|
|
||||||
|
[mod.'github.com/lqqyt2423/go-mitmproxy']
|
||||||
|
version = 'v1.8.11'
|
||||||
|
hash = 'sha256-PoCL6TOt99JsenH4XFx+7MqIzzYnnLHYwUpEQ7kxxyg='
|
||||||
|
|
||||||
|
[mod.'github.com/lucasb-eyer/go-colorful']
|
||||||
|
version = 'v1.4.0'
|
||||||
|
hash = 'sha256-i/3GDHKEMLCy0kc3mtyk58UWYOPmKoUVaq6QCAWXKP0='
|
||||||
|
|
||||||
|
[mod.'github.com/mattn/go-isatty']
|
||||||
|
version = 'v0.0.22'
|
||||||
|
hash = 'sha256-6O/0jc33pKUzlzUGpH8Ekk54XgJvx6Qe7kJtbcNJAV4='
|
||||||
|
|
||||||
|
[mod.'github.com/mattn/go-runewidth']
|
||||||
|
version = 'v0.0.23'
|
||||||
|
hash = 'sha256-SmChZ2U1aR8pW3LPhdM7KcVF5TO6VcHgRzBtUXbBWJA='
|
||||||
|
|
||||||
|
[mod.'github.com/microcosm-cc/bluemonday']
|
||||||
|
version = 'v1.0.27'
|
||||||
|
hash = 'sha256-EZSya9FLPQ83CL7N2cZy21fdS35hViTkiMK5f3op8Es='
|
||||||
|
|
||||||
|
[mod.'github.com/muesli/cancelreader']
|
||||||
|
version = 'v0.2.2'
|
||||||
|
hash = 'sha256-uEPpzwRJBJsQWBw6M71FDfgJuR7n55d/7IV8MO+rpwQ='
|
||||||
|
|
||||||
|
[mod.'github.com/ncruces/go-strftime']
|
||||||
|
version = 'v1.0.0'
|
||||||
|
hash = 'sha256-GYIwYDONuv/yTE0AEugCHQbtV3oiBaco93xUNYFcVBQ='
|
||||||
|
|
||||||
|
[mod.'github.com/pelletier/go-toml/v2']
|
||||||
|
version = 'v2.3.1'
|
||||||
|
hash = 'sha256-5H8+UOtPOs+Yc+8oVT/3bugCCdbq3jFMH6eOW8dadyg='
|
||||||
|
|
||||||
|
[mod.'github.com/remyoudompheng/bigfft']
|
||||||
|
version = 'v0.0.0-20230129092748-24d4a6f8daec'
|
||||||
|
hash = 'sha256-vYmpyCE37eBYP/navhaLV4oX4/nu0Z/StAocLIFqrmM='
|
||||||
|
|
||||||
|
[mod.'github.com/rivo/uniseg']
|
||||||
|
version = 'v0.4.7'
|
||||||
|
hash = 'sha256-rDcdNYH6ZD8KouyyiZCUEy8JrjOQoAkxHBhugrfHjFo='
|
||||||
|
|
||||||
|
[mod.'github.com/sagikazarmark/locafero']
|
||||||
|
version = 'v0.12.0'
|
||||||
|
hash = 'sha256-EXk9S5Z5sYyApAzCgHIugsGMbt/pHWRfHYFZH5D+5Ws='
|
||||||
|
|
||||||
|
[mod.'github.com/sahilm/fuzzy']
|
||||||
|
version = 'v0.1.1'
|
||||||
|
hash = 'sha256-f2VsDI6G+V2w31tSDzbZPi9EI2E7jRV6Aq8yeOorSZY='
|
||||||
|
|
||||||
|
[mod.'github.com/satori/go.uuid']
|
||||||
|
version = 'v1.2.0'
|
||||||
|
hash = 'sha256-y/lSGbnZa7mYJCs30a3LTyjfCFQSaYp8GbVR8dwtmsg='
|
||||||
|
|
||||||
|
[mod.'github.com/sirupsen/logrus']
|
||||||
|
version = 'v1.9.4'
|
||||||
|
hash = 'sha256-ltRvmtM3XTCAFwY0IesfRqYIivyXPPuvkFjL4ARh1wg='
|
||||||
|
|
||||||
|
[mod.'github.com/spf13/afero']
|
||||||
|
version = 'v1.15.0'
|
||||||
|
hash = 'sha256-LhcezbOqfuBzacytbqck0hNUxi6NbWNhifUc5/9uHQ8='
|
||||||
|
|
||||||
|
[mod.'github.com/spf13/cast']
|
||||||
|
version = 'v1.10.0'
|
||||||
|
hash = 'sha256-dQ6Qqf26IZsa6XsGKP7GDuCj+WmSsBmkBwGTDfue/rk='
|
||||||
|
|
||||||
|
[mod.'github.com/spf13/pflag']
|
||||||
|
version = 'v1.0.10'
|
||||||
|
hash = 'sha256-uDPnWjHpSrzXr17KEYEA1yAbizfcsfo5AyztY2tS6ZU='
|
||||||
|
|
||||||
|
[mod.'github.com/spf13/viper']
|
||||||
|
version = 'v1.21.0'
|
||||||
|
hash = 'sha256-A9A8i7HH/ge4j3hw7G++HNj8BjhhpZKvxHhfY+QAxkI='
|
||||||
|
|
||||||
|
[mod.'github.com/subosito/gotenv']
|
||||||
|
version = 'v1.6.0'
|
||||||
|
hash = 'sha256-LspbjTniiq2xAICSXmgqP7carwlNaLqnCTQfw2pa80A='
|
||||||
|
|
||||||
|
[mod.'github.com/xo/terminfo']
|
||||||
|
version = 'v0.0.0-20220910002029-abceb7e1c41e'
|
||||||
|
hash = 'sha256-GyCDxxMQhXA3Pi/TsWXpA8cX5akEoZV7CFx4RO3rARU='
|
||||||
|
|
||||||
|
[mod.'github.com/yuin/goldmark']
|
||||||
|
version = 'v1.8.2'
|
||||||
|
hash = 'sha256-LoWfW1Tb6mNuMR7SoA/4SJv4pTKfsVXqeXEVm4uEQ7Q='
|
||||||
|
|
||||||
|
[mod.'github.com/yuin/goldmark-emoji']
|
||||||
|
version = 'v1.0.6'
|
||||||
|
hash = 'sha256-+d6bZzOPE+JSFsZbQNZMCWE+n3jgcQnkPETVk47mxSY='
|
||||||
|
|
||||||
|
[mod.'github.com/yuin/gopher-lua']
|
||||||
|
version = 'v1.1.2'
|
||||||
|
hash = 'sha256-2YMCxv7RO3uqq6OTjvE0kR9nTt+n2cF+MrLtGEW68po='
|
||||||
|
|
||||||
|
[mod.'go.uber.org/atomic']
|
||||||
|
version = 'v1.11.0'
|
||||||
|
hash = 'sha256-TyYws/cSPVqYNffFX0gbDml1bD4bBGcysrUWU7mHPIY='
|
||||||
|
|
||||||
|
[mod.'go.yaml.in/yaml/v3']
|
||||||
|
version = 'v3.0.4'
|
||||||
|
hash = 'sha256-NkGFiDPoCxbr3LFsI6OCygjjkY0rdmg5ggvVVwpyDQ4='
|
||||||
|
|
||||||
|
[mod.'golang.org/x/net']
|
||||||
|
version = 'v0.54.0'
|
||||||
|
hash = 'sha256-/EoIXzTQzK/yP/lxOyx0Z/bhns4FdPTIF4uyt4gIP80='
|
||||||
|
|
||||||
|
[mod.'golang.org/x/sync']
|
||||||
|
version = 'v0.20.0'
|
||||||
|
hash = 'sha256-ybcjhCfK6lroUM0yswUvWooW8MOQZBXyiSqoxG6Uy0Y='
|
||||||
|
|
||||||
|
[mod.'golang.org/x/sys']
|
||||||
|
version = 'v0.44.0'
|
||||||
|
hash = 'sha256-JDlj+PKsG6I6kjv5JyOUNreY51u5An0oZ5OZMHZSk+A='
|
||||||
|
|
||||||
|
[mod.'golang.org/x/text']
|
||||||
|
version = 'v0.37.0'
|
||||||
|
hash = 'sha256-8XDOnlPIybcDRy89fkjG5VqtIt5Ku+LmaqYhgKl7i1E='
|
||||||
|
|
||||||
|
[mod.'gopkg.in/yaml.v3']
|
||||||
|
version = 'v3.0.1'
|
||||||
|
hash = 'sha256-FqL9TKYJ0XkNwJFnq9j0VvJ5ZUU1RvH/52h/f5bkYAU='
|
||||||
|
|
||||||
|
[mod.'modernc.org/libc']
|
||||||
|
version = 'v1.72.3'
|
||||||
|
hash = 'sha256-eOCoqSnX/VzpG63nh1j3JpXRndnZZcn2cDTeutexXAI='
|
||||||
|
|
||||||
|
[mod.'modernc.org/mathutil']
|
||||||
|
version = 'v1.7.1'
|
||||||
|
hash = 'sha256-COZ5rF2GhQVR1r6a0DanJ8qwQ94JSKdQxTMWrDzE0Cc='
|
||||||
|
|
||||||
|
[mod.'modernc.org/memory']
|
||||||
|
version = 'v1.11.0'
|
||||||
|
hash = 'sha256-MkybF8vvrxXS5j7O8w3skwTo0aMo1yjWS0K440rYcHM='
|
||||||
|
|
||||||
|
[mod.'modernc.org/sqlite']
|
||||||
|
version = 'v1.50.1'
|
||||||
|
hash = 'sha256-JbGP3eerH/6mgzI0aq9SpLLwe6ld6PV/zJReG8mLQN0='
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
pkgs,
|
||||||
|
buildGoApplication,
|
||||||
|
}: let
|
||||||
|
pname = "spilltea";
|
||||||
|
version = "0.0.6";
|
||||||
|
ldflags = ["-s" "-w" "-X main.version=${version}"];
|
||||||
|
pkg = buildGoApplication {
|
||||||
|
inherit pname version ldflags;
|
||||||
|
src = ../.;
|
||||||
|
modules = ./gomod2nix.toml;
|
||||||
|
meta = with pkgs.lib; {
|
||||||
|
description = "A minimal, terminal-based HTTP(S) proxy for pentesters and CTF players.";
|
||||||
|
homepage = "https://github.com/anotherhadi/spilltea";
|
||||||
|
platforms = platforms.unix;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in {
|
||||||
|
"${pname}" = pkg;
|
||||||
|
default = pkg;
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
{
|
||||||
|
pkgs,
|
||||||
|
gitHooksLib,
|
||||||
|
gomod2nixPkgs,
|
||||||
|
}: let
|
||||||
|
hooks = gitHooksLib.run {
|
||||||
|
src = ../.;
|
||||||
|
hooks = {
|
||||||
|
gofmt.enable = true;
|
||||||
|
govet.enable = true;
|
||||||
|
stylua.enable = true;
|
||||||
|
|
||||||
|
gomod2nix = {
|
||||||
|
enable = true;
|
||||||
|
name = "gomod2nix";
|
||||||
|
entry = "gomod2nix --outdir ./nix";
|
||||||
|
language = "system";
|
||||||
|
files = "go\\.(mod|sum)$";
|
||||||
|
pass_filenames = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
inject-exec-basics = {
|
||||||
|
enable = true;
|
||||||
|
name = "inject-exec-basics";
|
||||||
|
entry = "python3 .github/scripts/inject-exec.py docs/basics.md";
|
||||||
|
language = "system";
|
||||||
|
files = "(docs/basics\\.md|cmd/)";
|
||||||
|
pass_filenames = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
inject-exec = {
|
||||||
|
enable = true;
|
||||||
|
name = "inject-exec";
|
||||||
|
entry = "python3 .github/scripts/inject-exec.py README.md";
|
||||||
|
language = "system";
|
||||||
|
files = "(README\\.md|docs/basics\\.md|cmd/)";
|
||||||
|
pass_filenames = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
doctoc = {
|
||||||
|
enable = true;
|
||||||
|
name = "doctoc";
|
||||||
|
entry = "doctoc --notitle README.md";
|
||||||
|
language = "system";
|
||||||
|
files = "(README\\.md|docs/basics\\.md|cmd/)";
|
||||||
|
pass_filenames = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in
|
||||||
|
pkgs.mkShell {
|
||||||
|
packages = with pkgs;
|
||||||
|
[
|
||||||
|
go
|
||||||
|
python3
|
||||||
|
doctoc
|
||||||
|
stylua
|
||||||
|
trufflehog
|
||||||
|
gomod2nixPkgs.gomod2nix
|
||||||
|
]
|
||||||
|
++ hooks.enabledPackages;
|
||||||
|
|
||||||
|
shellHook = hooks.shellHook;
|
||||||
|
}
|
||||||
+21
-15
@@ -1,28 +1,34 @@
|
|||||||
Plugin = {
|
Plugin = {
|
||||||
name = "Inject Header",
|
name = "Inject Header",
|
||||||
description = [[
|
description = [[
|
||||||
Inject custom headers into every intercepted request.
|
Inject custom headers into every intercepted request.
|
||||||
|
|
||||||
**Config**:
|
**Config**:
|
||||||
- one 'Header-Name: value' per line.
|
```yaml
|
||||||
|
headers:
|
||||||
|
- "X-My-Header: myvalue"
|
||||||
|
```
|
||||||
]],
|
]],
|
||||||
on_request = { sync = true },
|
on_request = { sync = true },
|
||||||
}
|
}
|
||||||
|
|
||||||
local headers = {}
|
local headers = {}
|
||||||
|
|
||||||
function on_config(config_text)
|
function on_config()
|
||||||
headers = {}
|
headers = {}
|
||||||
for line in config_text:gmatch("[^\n]+") do
|
local cfg = get_config()
|
||||||
local name, value = line:match("^([^:]+):%s*(.+)$")
|
if cfg and cfg.headers then
|
||||||
if name and value then
|
for _, line in ipairs(cfg.headers) do
|
||||||
table.insert(headers, { name = name, value = value })
|
local name, value = line:match("^([^:]+):%s*(.+)$")
|
||||||
end
|
if name and value then
|
||||||
end
|
table.insert(headers, { name = name, value = value })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function on_request(req)
|
function on_request(req)
|
||||||
for _, h in ipairs(headers) do
|
for _, h in ipairs(headers) do
|
||||||
req:set_header(h.name, h.value)
|
req:set_header(h.name, h.value)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+54
-50
@@ -1,70 +1,74 @@
|
|||||||
Plugin = {
|
Plugin = {
|
||||||
name = "IP Filter (Whitelist/Blacklist)",
|
name = "IP Filter (Whitelist/Blacklist)",
|
||||||
description = [[
|
description = [[
|
||||||
Checks that the proxy's outbound IP is in an allowed list on startup.
|
Checks that the proxy's outbound IP is in an allowed list on startup.
|
||||||
|
|
||||||
**Config**:
|
**Config**:
|
||||||
- one IP per line
|
```yaml
|
||||||
- prefix with `!` for a blacklist entry (blocked)
|
ips:
|
||||||
- prefix with `#` to comment it out (ignored)
|
- "1.2.3.4" # whitelist entry
|
||||||
- if no IPs are configured, the check is skipped
|
- "!5.6.7.8" # blacklist entry (blocked)
|
||||||
|
```
|
||||||
|
|
||||||
|
- If no IPs are configured, the check is skipped.
|
||||||
]],
|
]],
|
||||||
on_start = { sync = false },
|
on_start = { sync = false },
|
||||||
disable_by_default = true,
|
disable_by_default = true,
|
||||||
}
|
}
|
||||||
|
|
||||||
local whitelist = {}
|
local whitelist = {}
|
||||||
local blacklist = {}
|
local blacklist = {}
|
||||||
|
|
||||||
function on_config(config_text)
|
function on_config()
|
||||||
whitelist = {}
|
whitelist, blacklist = {}, {}
|
||||||
blacklist = {}
|
local cfg = get_config()
|
||||||
|
if cfg and cfg.ips then
|
||||||
for line in config_text:gmatch("[^\n]+") do
|
for _, entry in ipairs(cfg.ips) do
|
||||||
local trimmed = line:match("^%s*(.-)%s*$")
|
local trimmed = entry:match("^%s*(.-)%s*$")
|
||||||
if trimmed ~= "" and trimmed:sub(1, 1) ~= "#" then
|
if trimmed ~= "" then
|
||||||
if trimmed:sub(1, 1) == "!" then
|
if trimmed:sub(1, 1) == "!" then
|
||||||
local ip = trimmed:sub(2):match("^%s*(.-)%s*$")
|
local ip = trimmed:sub(2):match("^%s*(.-)%s*$")
|
||||||
if ip ~= "" then
|
if ip ~= "" then
|
||||||
table.insert(blacklist, ip)
|
table.insert(blacklist, ip)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
table.insert(whitelist, trimmed)
|
table.insert(whitelist, trimmed)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function on_start()
|
function on_start()
|
||||||
if #whitelist == 0 and #blacklist == 0 then
|
if #whitelist == 0 and #blacklist == 0 then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local result, err = shell_pipe("curl -sf https://api.ipify.org 2>/dev/null")
|
local result, err = shell_pipe("curl -sf https://api.ipify.org 2>/dev/null")
|
||||||
result = result and result:match("^%s*(.-)%s*$") or nil
|
result = result and result:match("^%s*(.-)%s*$") or nil
|
||||||
|
|
||||||
if err or not result or result == "" then
|
if err or not result or result == "" then
|
||||||
log("could not determine outbound IP, skipping check")
|
log("could not determine outbound IP, skipping check")
|
||||||
notif("IP Filter", "Could not determine outbound IP, skipping check", "warning")
|
notif("IP Filter", "Could not determine outbound IP, skipping check", "warning")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
for _, ip in ipairs(blacklist) do
|
for _, ip in ipairs(blacklist) do
|
||||||
if result == ip then
|
if result == ip then
|
||||||
notif("IP Filter", "Outbound IP " .. result .. " is blacklisted!", "error")
|
notif("IP Filter", "Outbound IP " .. result .. " is blacklisted!", "error")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if #whitelist == 0 then
|
if #whitelist == 0 then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
for _, ip in ipairs(whitelist) do
|
for _, ip in ipairs(whitelist) do
|
||||||
if result == ip then
|
if result == ip then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
notif("IP Filter", "Outbound IP " .. result .. " is not in the whitelist!", "error")
|
notif("IP Filter", "Outbound IP " .. result .. " is not in the whitelist!", "error")
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
Plugin = {
|
||||||
|
name = "Match & Replace",
|
||||||
|
description = [[
|
||||||
|
Automatically find and replace content in requests and responses.
|
||||||
|
|
||||||
|
**Config**:
|
||||||
|
```yaml
|
||||||
|
rules:
|
||||||
|
- on: "request" # "request", "response", or "both" (default: "both")
|
||||||
|
url: "example%.com" # optional: Lua pattern to filter by URL
|
||||||
|
target: "body" # "body", "path", "url", or "header:Name"
|
||||||
|
find: "old_string" # Lua pattern to search
|
||||||
|
replace: "new_string" # replacement string (supports %1, %2 for captures)
|
||||||
|
```
|
||||||
|
|
||||||
|
Example (inject a debug flag in JSON bodies):
|
||||||
|
```yaml
|
||||||
|
rules:
|
||||||
|
- on: "both"
|
||||||
|
url: "api%.example%.com"
|
||||||
|
target: "body"
|
||||||
|
find: '"debug":false'
|
||||||
|
replace: '"debug":true'
|
||||||
|
```
|
||||||
|
|
||||||
|
Example (replace an Authorization header):
|
||||||
|
```yaml
|
||||||
|
rules:
|
||||||
|
- on: "request"
|
||||||
|
target: "header:Authorization"
|
||||||
|
find: "Bearer .*"
|
||||||
|
replace: "Bearer MY_NEW_TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
Example (rewrite a path prefix):
|
||||||
|
```yaml
|
||||||
|
rules:
|
||||||
|
- on: "request"
|
||||||
|
url: "staging%.example%.com"
|
||||||
|
target: "path"
|
||||||
|
find: "^/v1/"
|
||||||
|
replace: "/v2/"
|
||||||
|
```
|
||||||
|
]],
|
||||||
|
priority = 50,
|
||||||
|
on_request = { sync = true },
|
||||||
|
on_response = { sync = true },
|
||||||
|
}
|
||||||
|
|
||||||
|
local rules = {}
|
||||||
|
|
||||||
|
function on_config()
|
||||||
|
rules = {}
|
||||||
|
local cfg = get_config()
|
||||||
|
if not cfg or not cfg.rules then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
for _, r in ipairs(cfg.rules) do
|
||||||
|
if r.find and r.find ~= "" then
|
||||||
|
table.insert(rules, {
|
||||||
|
on = r.on or "both",
|
||||||
|
url = r.url,
|
||||||
|
target = r.target or "body",
|
||||||
|
find = r.find,
|
||||||
|
replace = r.replace or "",
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function should_apply(rule, req, context)
|
||||||
|
if rule.on ~= "both" and rule.on ~= context then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
if rule.url and not req.url:match(rule.url) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function apply(rule, req, res, context)
|
||||||
|
local target = rule.target
|
||||||
|
if target == "body" then
|
||||||
|
local obj = (context == "response") and res or req
|
||||||
|
local body = obj:get_body()
|
||||||
|
local new_body = body:gsub(rule.find, rule.replace)
|
||||||
|
if new_body ~= body then
|
||||||
|
obj:set_body(new_body)
|
||||||
|
end
|
||||||
|
elseif target == "path" then
|
||||||
|
local new_path = req.path:gsub(rule.find, rule.replace)
|
||||||
|
if new_path ~= req.path then
|
||||||
|
req:set_path(new_path)
|
||||||
|
end
|
||||||
|
elseif target == "url" then
|
||||||
|
local new_url = req.url:gsub(rule.find, rule.replace)
|
||||||
|
if new_url ~= req.url then
|
||||||
|
req:set_url(new_url)
|
||||||
|
end
|
||||||
|
elseif target:sub(1, 7) == "header:" then
|
||||||
|
local name = target:sub(8)
|
||||||
|
local obj = (context == "response") and res or req
|
||||||
|
local val = obj.headers[name] or ""
|
||||||
|
local new_val = val:gsub(rule.find, rule.replace)
|
||||||
|
if new_val ~= val then
|
||||||
|
obj:set_header(name, new_val)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function on_request(req)
|
||||||
|
for _, rule in ipairs(rules) do
|
||||||
|
if should_apply(rule, req, "request") then
|
||||||
|
apply(rule, req, nil, "request")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function on_response(req, res)
|
||||||
|
for _, rule in ipairs(rules) do
|
||||||
|
if should_apply(rule, req, "response") then
|
||||||
|
apply(rule, req, res, "response")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
+91
-74
@@ -1,104 +1,121 @@
|
|||||||
Plugin = {
|
Plugin = {
|
||||||
name = "Scopes",
|
name = "Scopes",
|
||||||
description = [[
|
description = [[
|
||||||
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/responses and history entries
|
```yaml
|
||||||
- `!pattern` - blacklist: skip matching requests/responses and history entries
|
patterns:
|
||||||
- `r:pattern` - whitelist for requests/responses only (history unaffected)
|
- "pattern" # whitelist: only intercept matching requests/responses and history
|
||||||
- `r:!pattern` - blacklist for requests/responses only
|
- "!pattern" # blacklist: skip matching requests/responses and history
|
||||||
- `h:pattern` - whitelist for history entries only (requests unaffected)
|
- "r:pattern" # whitelist for requests/responses only
|
||||||
- `h:!pattern` - blacklist for history entries only
|
- "r:!pattern" # blacklist for requests/responses only
|
||||||
- lines starting with `#` are comments
|
- "h:pattern" # whitelist for history only
|
||||||
|
- "h:!pattern" # blacklist for history only
|
||||||
|
```
|
||||||
|
|
||||||
Example (ignore static assets):
|
Example (ignore static assets):
|
||||||
```
|
```yaml
|
||||||
!%.css$
|
patterns:
|
||||||
!%.js$
|
- "!%.css$"
|
||||||
!%.png$
|
- "!%.js$"
|
||||||
|
- "!%.png$"
|
||||||
```
|
```
|
||||||
|
|
||||||
Example (focus on mytarget.com, skip everything else):
|
Example (focus on mytarget.com):
|
||||||
```
|
```yaml
|
||||||
mytarget%.com/
|
patterns:
|
||||||
|
- "mytarget%.com/"
|
||||||
```
|
```
|
||||||
|
|
||||||
Example (intercept mytarget.com except its static assets):
|
Example (disable history):
|
||||||
```
|
```yaml
|
||||||
mytarget%.com/
|
patterns:
|
||||||
!%.css$
|
- "h:^$"
|
||||||
!%.js$
|
|
||||||
!%.png$
|
|
||||||
```
|
|
||||||
|
|
||||||
Example (disable history: whitelist never matches any real URL):
|
|
||||||
```
|
|
||||||
h:^$
|
|
||||||
```
|
```
|
||||||
]],
|
]],
|
||||||
priority = 100,
|
priority = 100,
|
||||||
on_request = { sync = true },
|
on_request = { sync = true },
|
||||||
on_response = { sync = true },
|
on_response = { sync = true },
|
||||||
on_history_entry = { sync = true },
|
on_history_entry = { sync = true },
|
||||||
}
|
}
|
||||||
|
|
||||||
local blacklist = {}
|
local blacklist = {}
|
||||||
local whitelist = {}
|
local whitelist = {}
|
||||||
local blacklist_req = {}
|
local blacklist_req = {}
|
||||||
local whitelist_req = {}
|
local whitelist_req = {}
|
||||||
local blacklist_hist = {}
|
local blacklist_hist = {}
|
||||||
local whitelist_hist = {}
|
local whitelist_hist = {}
|
||||||
|
|
||||||
function on_config(config_text)
|
function on_config()
|
||||||
blacklist, whitelist = {}, {}
|
blacklist, whitelist = {}, {}
|
||||||
blacklist_req, whitelist_req = {}, {}
|
blacklist_req, whitelist_req = {}, {}
|
||||||
blacklist_hist, whitelist_hist = {}, {}
|
blacklist_hist, whitelist_hist = {}, {}
|
||||||
for line in config_text:gmatch("[^\n]+") do
|
local cfg = get_config()
|
||||||
local trimmed = line:match("^%s*(.-)%s*$")
|
if not cfg or not cfg.patterns then
|
||||||
if trimmed ~= "" and trimmed:sub(1, 1) ~= "#" then
|
return
|
||||||
local scope = trimmed:match("^([rh]):")
|
end
|
||||||
local rest = scope and trimmed:sub(3) or trimmed
|
for _, line in ipairs(cfg.patterns) do
|
||||||
local is_bl = rest:sub(1, 1) == "!"
|
local trimmed = line:match("^%s*(.-)%s*$")
|
||||||
local pattern = is_bl and rest:sub(2) or rest
|
if trimmed ~= "" and trimmed:sub(1, 1) ~= "#" then
|
||||||
if scope == "r" then
|
local scope = trimmed:match("^([rh]):")
|
||||||
table.insert(is_bl and blacklist_req or whitelist_req, pattern)
|
local rest = scope and trimmed:sub(3) or trimmed
|
||||||
elseif scope == "h" then
|
local is_bl = rest:sub(1, 1) == "!"
|
||||||
table.insert(is_bl and blacklist_hist or whitelist_hist, pattern)
|
local pattern = is_bl and rest:sub(2) or rest
|
||||||
else
|
if scope == "r" then
|
||||||
table.insert(is_bl and blacklist or whitelist, pattern)
|
table.insert(is_bl and blacklist_req or whitelist_req, pattern)
|
||||||
end
|
elseif scope == "h" then
|
||||||
end
|
table.insert(is_bl and blacklist_hist or whitelist_hist, pattern)
|
||||||
end
|
else
|
||||||
|
table.insert(is_bl and blacklist or whitelist, pattern)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function check_skip(url, bl_extra, wl_extra)
|
local function check_skip(url, bl_extra, wl_extra)
|
||||||
for _, p in ipairs(blacklist) do
|
for _, p in ipairs(blacklist) do
|
||||||
if url:match(p) then return true end
|
if url:match(p) then
|
||||||
end
|
return true
|
||||||
for _, p in ipairs(bl_extra) do
|
end
|
||||||
if url:match(p) then return true end
|
end
|
||||||
end
|
for _, p in ipairs(bl_extra) do
|
||||||
local wl = {}
|
if url:match(p) then
|
||||||
for _, p in ipairs(whitelist) do wl[#wl + 1] = p end
|
return true
|
||||||
for _, p in ipairs(wl_extra) do wl[#wl + 1] = p end
|
end
|
||||||
if #wl > 0 then
|
end
|
||||||
for _, p in ipairs(wl) do
|
local wl = {}
|
||||||
if url:match(p) then return false end
|
for _, p in ipairs(whitelist) do
|
||||||
end
|
wl[#wl + 1] = p
|
||||||
return true
|
end
|
||||||
end
|
for _, p in ipairs(wl_extra) do
|
||||||
return false
|
wl[#wl + 1] = p
|
||||||
|
end
|
||||||
|
if #wl > 0 then
|
||||||
|
for _, p in ipairs(wl) do
|
||||||
|
if url:match(p) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
function on_request(req)
|
function on_request(req)
|
||||||
if check_skip(req.url, blacklist_req, whitelist_req) then return "forward" end
|
if check_skip(req.url, blacklist_req, whitelist_req) then
|
||||||
|
return "forward"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function on_response(req)
|
function on_response(req)
|
||||||
if check_skip(req.url, blacklist_req, whitelist_req) then return "forward" end
|
if check_skip(req.url, blacklist_req, whitelist_req) then
|
||||||
|
return "forward"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function on_history_entry(entry)
|
function on_history_entry(entry)
|
||||||
if check_skip(entry.host .. entry.path, blacklist_hist, whitelist_hist) then return "skip" end
|
if check_skip(entry.host .. entry.path, blacklist_hist, whitelist_hist) then
|
||||||
|
return "skip"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+168
-120
@@ -1,70 +1,91 @@
|
|||||||
Plugin = {
|
Plugin = {
|
||||||
name = "Secret Scan",
|
name = "Secret Scan",
|
||||||
description = [[
|
description = [[
|
||||||
Scans HTML, JavaScript and JSON content (requests and responses) for hardcoded
|
Scans HTML, JavaScript and JSON content (requests and responses) for hardcoded
|
||||||
secrets by matching common secret key names followed by a non-trivial value.
|
secrets by matching common secret key names followed by a non-trivial value.
|
||||||
|
|
||||||
Uses `grep -E` (available on all Unix systems, no extra dependencies).
|
Uses `grep -E` (available on all Unix systems, no extra dependencies).
|
||||||
]],
|
]],
|
||||||
on_request = { sync = false },
|
on_request = { sync = false },
|
||||||
on_response = { sync = false },
|
on_response = { sync = false },
|
||||||
disable_by_default = true,
|
disable_by_default = true,
|
||||||
}
|
}
|
||||||
|
|
||||||
local CONTENT_TYPES = {
|
local CONTENT_TYPES = {
|
||||||
"text/html",
|
"text/html",
|
||||||
"text/javascript",
|
"text/javascript",
|
||||||
"application/javascript",
|
"application/javascript",
|
||||||
"application/json",
|
"application/json",
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Key name alternation (case-insensitive via grep -i)
|
-- Key name alternation (case-insensitive via grep -i)
|
||||||
-- Suffixes are required (no bare generic keyword alone).
|
-- Suffixes are required (no bare generic keyword alone).
|
||||||
local KEYS = {
|
local KEYS = {
|
||||||
"access(_key|_token)", "accessid_secret", "account(_key|_sid)",
|
"access(_key|_token)",
|
||||||
"admin_pass(word)?", "admin_user",
|
"accessid_secret",
|
||||||
"(algolia|aws|gcp|azure|heroku|firebase|github|gitlab|slack|datadog|stripe|twilio|vercel|supabase|sendgrid|cloudinary|cloudflare|bitbucket|npm|netlify|auth0|okta|sentry)(_?(api|secret|access)(_?(key|token|id|sid|secret))?|_?(key|token|id|sid|secret))",
|
"account(_key|_sid)",
|
||||||
"ansible_vault_password", "aos_key",
|
"admin_pass(word)?",
|
||||||
"api(_key|_secret|_token)",
|
"admin_user",
|
||||||
"app_(id|key|secret)", "application(_key|_id|_secret)",
|
"(algolia|aws|gcp|azure|heroku|firebase|github|gitlab|slack|datadog|stripe|twilio|vercel|supabase|sendgrid|cloudinary|cloudflare|bitbucket|npm|netlify|auth0|okta|sentry)(_?(api|secret|access)(_?(key|token|id|sid|secret))?|_?(key|token|id|sid|secret))",
|
||||||
"auth(_token|_secret|orization)", "authkey", "authsecret",
|
"ansible_vault_password",
|
||||||
"bearer_?token",
|
"aos_key",
|
||||||
"bucket(_password|_key)",
|
"api(_key|_secret|_token)",
|
||||||
"cert_?pass(word)?", "certificate_password",
|
"app_(id|key|secret)",
|
||||||
"client(_id|_secret)",
|
"application(_key|_id|_secret)",
|
||||||
"codecov_token", "consumer_(key|secret)",
|
"auth(_token|_secret|orization)",
|
||||||
"connection_?string", "credentials?", "crypt(_key|_secret)",
|
"authkey",
|
||||||
"db_(password|passwd|user(name)?)",
|
"authsecret",
|
||||||
"deploy(_key|_password|_token)",
|
"bearer_?token",
|
||||||
"docker_?pass(word)?", "dockerhub_?password",
|
"bucket(_password|_key)",
|
||||||
"encryption_(key|password)",
|
"cert_?pass(word)?",
|
||||||
"jwt_secret", "json_web_token",
|
"certificate_password",
|
||||||
"keycloak_secret", "kubernetes_token",
|
"client(_id|_secret)",
|
||||||
"ldap_(password|bindpw)", "login(_password|_token)",
|
"codecov_token",
|
||||||
"mail_?password", "mail_smtp_pass",
|
"consumer_(key|secret)",
|
||||||
"mysql_password", "mongo_password",
|
"connection_?string",
|
||||||
"netlify_token", "npm(_token|_auth_token)",
|
"credentials?",
|
||||||
"oauth(_token|_secret)",
|
"crypt(_key|_secret)",
|
||||||
"openai_(api_key|secret)",
|
"db_(password|passwd|user(name)?)",
|
||||||
"pass(word)?", "passwd",
|
"deploy(_key|_password|_token)",
|
||||||
"private(_key|_token)",
|
"docker_?pass(word)?",
|
||||||
"rds_password",
|
"dockerhub_?password",
|
||||||
"s3(_key|_secret|_access_key_id)",
|
"encryption_(key|password)",
|
||||||
"secret(_key|_token|_id)", "security_token",
|
"jwt_secret",
|
||||||
"sendgrid_api_key",
|
"json_web_token",
|
||||||
"ses_(smtp|access|secret)",
|
"keycloak_secret",
|
||||||
"service(_account|_key|_token)",
|
"kubernetes_token",
|
||||||
"smtp_pass(word)?", "smtp_secret",
|
"ldap_(password|bindpw)",
|
||||||
"sonar_token",
|
"login(_password|_token)",
|
||||||
"ssh(_key|_private_key|_rsa)",
|
"mail_?password",
|
||||||
"supabase(_anon|_service)?_key",
|
"mail_smtp_pass",
|
||||||
"symfony_secret",
|
"mysql_password",
|
||||||
"telegram_bot_token",
|
"mongo_password",
|
||||||
"token",
|
"netlify_token",
|
||||||
"travis_token",
|
"npm(_token|_auth_token)",
|
||||||
"vault(_token|_secret)",
|
"oauth(_token|_secret)",
|
||||||
"webhook(_secret|_token)",
|
"openai_(api_key|secret)",
|
||||||
"zapier_webhook_token",
|
"pass(word)?",
|
||||||
|
"passwd",
|
||||||
|
"private(_key|_token)",
|
||||||
|
"rds_password",
|
||||||
|
"s3(_key|_secret|_access_key_id)",
|
||||||
|
"secret(_key|_token|_id)",
|
||||||
|
"security_token",
|
||||||
|
"sendgrid_api_key",
|
||||||
|
"ses_(smtp|access|secret)",
|
||||||
|
"service(_account|_key|_token)",
|
||||||
|
"smtp_pass(word)?",
|
||||||
|
"smtp_secret",
|
||||||
|
"sonar_token",
|
||||||
|
"ssh(_key|_private_key|_rsa)",
|
||||||
|
"supabase(_anon|_service)?_key",
|
||||||
|
"symfony_secret",
|
||||||
|
"telegram_bot_token",
|
||||||
|
"token",
|
||||||
|
"travis_token",
|
||||||
|
"vault(_token|_secret)",
|
||||||
|
"webhook(_secret|_token)",
|
||||||
|
"zapier_webhook_token",
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Built once at load time.
|
-- Built once at load time.
|
||||||
@@ -74,93 +95,120 @@ local KEYS = {
|
|||||||
-- [[:space:]]*[:=] REQUIRED: actual = or : assignment operator
|
-- [[:space:]]*[:=] REQUIRED: actual = or : assignment operator
|
||||||
-- [[:space:]]*"? optional whitespace + opening quote
|
-- [[:space:]]*"? optional whitespace + opening quote
|
||||||
-- [a-zA-Z0-9+/=_.-]{8,} the secret value, at least 8 chars
|
-- [a-zA-Z0-9+/=_.-]{8,} the secret value, at least 8 chars
|
||||||
local KEY_PAT = "(" .. table.concat(KEYS, "|") .. ")"
|
local KEY_PAT = "(" .. table.concat(KEYS, "|") .. ")"
|
||||||
local FULL_PAT = KEY_PAT .. '[a-z0-9._-]{0,20}[^=:a-zA-Z0-9_]{0,3}[[:space:]]*[:=][[:space:]]*"?[a-zA-Z0-9+/=_.-]{8,}'
|
local FULL_PAT = KEY_PAT .. '[a-z0-9._-]{0,20}[^=:a-zA-Z0-9_]{0,3}[[:space:]]*[:=][[:space:]]*"?[a-zA-Z0-9+/=_.-]{8,}'
|
||||||
local GREP_CMD = "grep -Eoni '" .. FULL_PAT .. "'"
|
local GREP_CMD = "grep -Eoni '" .. FULL_PAT .. "'"
|
||||||
|
|
||||||
local function is_relevant(ct)
|
local function is_relevant(ct)
|
||||||
if not ct or ct == "" then return false end
|
if not ct or ct == "" then
|
||||||
ct = ct:lower()
|
return false
|
||||||
for _, t in ipairs(CONTENT_TYPES) do
|
end
|
||||||
if ct:find(t, 1, true) then return true end
|
ct = ct:lower()
|
||||||
end
|
for _, t in ipairs(CONTENT_TYPES) do
|
||||||
return false
|
if ct:find(t, 1, true) then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
local function build_context(lines, linenum)
|
local function build_context(lines, linenum)
|
||||||
local lo = math.max(1, linenum - 6)
|
local lo = math.max(1, linenum - 6)
|
||||||
local hi = math.min(#lines, linenum + 6)
|
local hi = math.min(#lines, linenum + 6)
|
||||||
|
|
||||||
local before, after = {}, {}
|
local before, after = {}, {}
|
||||||
for i = lo, linenum - 1 do
|
for i = lo, linenum - 1 do
|
||||||
local l = lines[i] or ""
|
local l = lines[i] or ""
|
||||||
if #l > 120 then l = l:sub(1, 120) .. "..." end
|
if #l > 120 then
|
||||||
table.insert(before, l)
|
l = l:sub(1, 120) .. "..."
|
||||||
end
|
end
|
||||||
for i = linenum + 1, hi do
|
table.insert(before, l)
|
||||||
local l = lines[i] or ""
|
end
|
||||||
if #l > 120 then l = l:sub(1, 120) .. "..." end
|
for i = linenum + 1, hi do
|
||||||
table.insert(after, l)
|
local l = lines[i] or ""
|
||||||
end
|
if #l > 120 then
|
||||||
|
l = l:sub(1, 120) .. "..."
|
||||||
|
end
|
||||||
|
table.insert(after, l)
|
||||||
|
end
|
||||||
|
|
||||||
local matched_line = lines[linenum] or ""
|
local matched_line = lines[linenum] or ""
|
||||||
if #matched_line > 200 then matched_line = matched_line:sub(1, 200) .. "..." end
|
if #matched_line > 200 then
|
||||||
|
matched_line = matched_line:sub(1, 200) .. "..."
|
||||||
|
end
|
||||||
|
|
||||||
local parts = {}
|
local parts = {}
|
||||||
if #before > 0 then
|
if #before > 0 then
|
||||||
table.insert(parts, "```\n" .. table.concat(before, "\n") .. "\n```")
|
table.insert(parts, "```\n" .. table.concat(before, "\n") .. "\n```")
|
||||||
end
|
end
|
||||||
table.insert(parts, "> **`" .. matched_line .. "`**")
|
table.insert(parts, "> **`" .. matched_line .. "`**")
|
||||||
if #after > 0 then
|
if #after > 0 then
|
||||||
table.insert(parts, "```\n" .. table.concat(after, "\n") .. "\n```")
|
table.insert(parts, "```\n" .. table.concat(after, "\n") .. "\n```")
|
||||||
end
|
end
|
||||||
return table.concat(parts, "\n\n")
|
return table.concat(parts, "\n\n")
|
||||||
end
|
end
|
||||||
|
|
||||||
local function scan(label, ct, body, host, path)
|
local function scan(label, ct, body, host, path)
|
||||||
if not is_relevant(ct) then return end
|
if not is_relevant(ct) then
|
||||||
if not body or body == "" then return end
|
return
|
||||||
|
end
|
||||||
|
if not body or body == "" then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
local out, err = shell_pipe(GREP_CMD, body)
|
local out, err = shell_pipe(GREP_CMD, body)
|
||||||
if err and err ~= "" then
|
if err and err ~= "" then
|
||||||
log("grep error on " .. label .. " for " .. host .. path .. ": " .. err)
|
log("grep error on " .. label .. " for " .. host .. path .. ": " .. err)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
if not out or out == "" then return end
|
if not out or out == "" then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
local lines = {}
|
local lines = {}
|
||||||
for line in (body .. "\n"):gmatch("([^\n]*)\n") do
|
for line in (body .. "\n"):gmatch("([^\n]*)\n") do
|
||||||
table.insert(lines, line)
|
table.insert(lines, line)
|
||||||
end
|
end
|
||||||
|
|
||||||
for entry in out:gmatch("[^\n]+") do
|
for entry in out:gmatch("[^\n]+") do
|
||||||
local linenum_str, matched = entry:match("^(%d+):(.+)$")
|
local linenum_str, matched = entry:match("^(%d+):(.+)$")
|
||||||
if linenum_str then
|
if linenum_str then
|
||||||
local linenum = tonumber(linenum_str)
|
local linenum = tonumber(linenum_str)
|
||||||
matched = matched:match("^%s*(.-)%s*$")
|
matched = matched:match("^%s*(.-)%s*$")
|
||||||
if matched ~= "" then
|
if matched ~= "" then
|
||||||
local display = matched
|
local display = matched
|
||||||
if #display > 200 then display = display:sub(1, 200) .. "..." end
|
if #display > 200 then
|
||||||
local ctx = build_context(lines, linenum)
|
display = display:sub(1, 200) .. "..."
|
||||||
create_finding({
|
end
|
||||||
title = "Potential secret in " .. label .. " (" .. host .. ")",
|
local ctx = build_context(lines, linenum)
|
||||||
description = "**Host:** `" .. host .. "` \n**Path:** `" .. path .. "`\n\n**Match:** `" .. display .. "`\n\n" .. ctx,
|
create_finding({
|
||||||
key = host .. "|" .. path .. "|" .. label .. "|" .. matched,
|
title = "Potential secret in " .. label .. " (" .. host .. ")",
|
||||||
severity = "high",
|
description = "**Host:** `"
|
||||||
})
|
.. host
|
||||||
end
|
.. "`\n"
|
||||||
end
|
.. "\n**Path:** `"
|
||||||
end
|
.. path
|
||||||
|
.. "`\n"
|
||||||
|
.. "\n**Match:** `"
|
||||||
|
.. display
|
||||||
|
.. "`\n\n"
|
||||||
|
.. ctx,
|
||||||
|
key = host .. "|" .. path .. "|" .. label .. "|" .. matched,
|
||||||
|
severity = "high",
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function on_request(req)
|
function on_request(req)
|
||||||
scan("request", req.headers["Content-Type"] or "", req:get_body(), req.host, req.path)
|
scan("request", req.headers["Content-Type"] or "", req:get_body(), req.host, req.path)
|
||||||
end
|
end
|
||||||
|
|
||||||
function on_response(req, res)
|
function on_response(req, res)
|
||||||
local ct = ""
|
local ct = ""
|
||||||
if res.headers then
|
if res.headers then
|
||||||
ct = res.headers["Content-Type"] or ""
|
ct = res.headers["Content-Type"] or ""
|
||||||
end
|
end
|
||||||
scan("response", ct, res:get_body(), req.host, req.path)
|
scan("response", ct, res:get_body(), req.host, req.path)
|
||||||
end
|
end
|
||||||
|
|||||||
+51
-45
@@ -1,63 +1,69 @@
|
|||||||
Plugin = {
|
Plugin = {
|
||||||
name = "TruffleHog",
|
name = "TruffleHog",
|
||||||
description = [[
|
description = [[
|
||||||
Scans request and response bodies for secrets using [TruffleHog](https://github.com/trufflesecurity/trufflehog).
|
Scans request and response bodies for secrets using [TruffleHog](https://github.com/trufflesecurity/trufflehog).
|
||||||
|
|
||||||
Requires `trufflehog` v3+ to be installed and available in PATH.
|
Requires `trufflehog` v3+ to be installed and available in PATH.
|
||||||
|
|
||||||
Each finding is stored on the **Findings** page with the matched detector output.
|
|
||||||
Findings are deduplicated per host+path+body content so repeated requests do not create duplicates.
|
|
||||||
]],
|
]],
|
||||||
on_start = { sync = false },
|
on_start = { sync = false },
|
||||||
on_request = { sync = false },
|
on_request = { sync = false },
|
||||||
on_response = { sync = false },
|
on_response = { sync = false },
|
||||||
disable_by_default = true,
|
disable_by_default = true,
|
||||||
}
|
}
|
||||||
|
|
||||||
function on_start()
|
function on_start()
|
||||||
local handle = io.popen("command -v trufflehog 2>/dev/null")
|
local result, _ = shell_pipe("command -v trufflehog 2>/dev/null")
|
||||||
local result = handle and handle:read("*a") or ""
|
if not result or result:match("^%s*$") then
|
||||||
if handle then handle:close() end
|
log("trufflehog is not installed or not in PATH")
|
||||||
if not result or result:match("^%s*$") then
|
notif("TruffleHog", "trufflehog is not installed or not in PATH, plugin disabled", "error")
|
||||||
log("trufflehog is not installed or not in PATH")
|
return false
|
||||||
notif("TruffleHog", "trufflehog is not installed or not in PATH, plugin disabled", "error")
|
end
|
||||||
return false
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function scan(label, content, host, path)
|
local function scan(label, content, host, path)
|
||||||
if not content or content == "" then return end
|
if not content or content == "" then
|
||||||
local out, err = shell_pipe("f=$(mktemp) && cat > \"$f\" && trufflehog filesystem --no-color \"$f\"; rc=$?; rm -f \"$f\"; exit $rc", content)
|
return
|
||||||
if err and err ~= "" then
|
end
|
||||||
log("trufflehog error on " .. label .. ": " .. err)
|
local out, err = shell_pipe(
|
||||||
return
|
'f=$(mktemp) && cat > "$f" && trufflehog filesystem --no-color "$f"; rc=$?; rm -f "$f"; exit $rc',
|
||||||
end
|
content
|
||||||
if not out or out == "" then return end
|
)
|
||||||
local blocks = {}
|
if err and err ~= "" then
|
||||||
local current = nil
|
log("trufflehog error on " .. label .. ": " .. err)
|
||||||
for line in out:gmatch("[^\n]+") do
|
return
|
||||||
if line:match("^Found ") then
|
end
|
||||||
if current then table.insert(blocks, current) end
|
if not out or out == "" then
|
||||||
current = line
|
return
|
||||||
elseif current then
|
end
|
||||||
current = current .. "\n" .. line
|
local blocks = {}
|
||||||
end
|
local current = nil
|
||||||
end
|
for line in out:gmatch("[^\n]+") do
|
||||||
if current then table.insert(blocks, current) end
|
if line:match("^Found ") then
|
||||||
for _, block in ipairs(blocks) do
|
if current then
|
||||||
create_finding({
|
table.insert(blocks, current)
|
||||||
title = "Secret detected in " .. label .. " (" .. host .. ")",
|
end
|
||||||
description = "**Host:** `" .. host .. "` \n**Path:** `" .. path .. "`\n\n```\n" .. block .. "\n```",
|
current = line
|
||||||
key = host .. "|" .. path .. "|" .. label .. "|" .. block,
|
elseif current then
|
||||||
severity = "high",
|
current = current .. "\n" .. line
|
||||||
})
|
end
|
||||||
end
|
end
|
||||||
|
if current then
|
||||||
|
table.insert(blocks, current)
|
||||||
|
end
|
||||||
|
for _, block in ipairs(blocks) do
|
||||||
|
create_finding({
|
||||||
|
title = "Secret detected in " .. label .. " (" .. host .. ")",
|
||||||
|
description = "**Host:** `" .. host .. "`\n\n**Path:** `" .. path .. "`\n\n```\n" .. block .. "\n```",
|
||||||
|
key = host .. "|" .. path .. "|" .. label .. "|" .. block,
|
||||||
|
severity = "high",
|
||||||
|
})
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function on_request(req)
|
function on_request(req)
|
||||||
scan("request", req:get_body(), req.host, req.path)
|
scan("request", req:get_body(), req.host, req.path)
|
||||||
end
|
end
|
||||||
|
|
||||||
function on_response(req, res)
|
function on_response(req, res)
|
||||||
scan("response", res:get_body(), req.host, req.path)
|
scan("response", res:get_body(), req.host, req.path)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//go:embed docs
|
||||||
|
var DocsFS embed.FS
|
||||||
|
|
||||||
//go:embed plugins/*.lua
|
//go:embed plugins/*.lua
|
||||||
var PluginsFS embed.FS
|
var PluginsFS embed.FS
|
||||||
|
|
||||||
Reference in New Issue
Block a user