mirror of
https://github.com/anotherhadi/spilltea.git
synced 2026-05-20 17:52:33 +02:00
Edit plugins id & docs
Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
This commit is contained in:
+29
-61
@@ -33,14 +33,14 @@ Plugin = {
|
||||
|
||||
### Hook reference
|
||||
|
||||
| Hook | When called | Sync/async | Return value |
|
||||
| ------------------------- | ------------------------------------- | ------------ | ----------------------------------------------- |
|
||||
| `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_quit()` | When the app exits | always sync | ignored |
|
||||
| `on_request(req)` | Every request, before auto-forward | configurable | `"drop"`, `"forward"`, or `nil` (sync only) |
|
||||
| `on_response(req, res)` | Every response | configurable | `"drop"`, `"forward"`, or `nil` (sync only) |
|
||||
| `on_history_entry(entry)` | Sync: before DB insert / Async: after | configurable | `"skip"` (don't save), `"keep"` or `nil` (save) -- sync only |
|
||||
| Hook | When called | Sync/async | Return value |
|
||||
| ------------------------- | ------------------------------------- | ------------ | ----------------------------------------------------------------------------------------- |
|
||||
| `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_quit()` | When the app exits | always sync | ignored |
|
||||
| `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` (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) |
|
||||
|
||||
## Request and response objects
|
||||
|
||||
@@ -130,28 +130,32 @@ local cfg = get_config()
|
||||
|
||||
### 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
|
||||
|
||||
Plugin configuration is stored in a `plugins.yaml` file alongside the project database. Each plugin has an `enable` toggle and an optional `config` block (arbitrary YAML).
|
||||
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).
|
||||
|
||||
```yaml
|
||||
plugins:
|
||||
my_plugin:
|
||||
enable: true
|
||||
config: |
|
||||
some_key: some_value
|
||||
list:
|
||||
- item1
|
||||
- item2
|
||||
other_plugin:
|
||||
enable: false
|
||||
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.
|
||||
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.
|
||||
|
||||
`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.
|
||||
`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 = {}
|
||||
@@ -174,49 +178,13 @@ end
|
||||
|
||||
`on_config` and `on_quit` are always synchronous regardless of the Plugin table declaration.
|
||||
|
||||
### Return values for sync hooks
|
||||
|
||||
**`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.
|
||||
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
|
||||
|
||||
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
|
||||
Plugin = {
|
||||
|
||||
+17
-16
@@ -68,11 +68,11 @@ func (m *Manager) LoadFromDir(dir string) error {
|
||||
continue
|
||||
}
|
||||
if m.pluginsFile != nil {
|
||||
if enabled, configText, found := m.pluginsFile.get(p.Name); found {
|
||||
if enabled, configText, found := m.pluginsFile.get(p.ID); found {
|
||||
p.Enabled = enabled
|
||||
p.ConfigText = configText
|
||||
} else {
|
||||
m.pluginsFile.register(p.Name, p.Enabled)
|
||||
m.pluginsFile.register(p.ID, p.Enabled)
|
||||
}
|
||||
}
|
||||
m.mu.Lock()
|
||||
@@ -87,6 +87,7 @@ func (m *Manager) LoadFromDir(dir string) error {
|
||||
|
||||
func (m *Manager) loadPlugin(path string) (*Plugin, error) {
|
||||
p := &Plugin{
|
||||
ID: strings.TrimSuffix(filepath.Base(path), ".lua"),
|
||||
FilePath: path,
|
||||
Enabled: true,
|
||||
hooks: make(map[string]HookConfig),
|
||||
@@ -107,7 +108,7 @@ func (m *Manager) loadPlugin(path string) (*Plugin, error) {
|
||||
p.Name = string(s)
|
||||
}
|
||||
if p.Name == "" {
|
||||
p.Name = strings.TrimSuffix(filepath.Base(path), ".lua")
|
||||
p.Name = p.ID
|
||||
}
|
||||
|
||||
if s, ok := pluginTable.RawGetString("description").(lua.LString); ok {
|
||||
@@ -155,11 +156,11 @@ func (m *Manager) GetPlugins() []*Plugin {
|
||||
return out
|
||||
}
|
||||
|
||||
func (m *Manager) TogglePlugin(name string) {
|
||||
func (m *Manager) TogglePlugin(id string) {
|
||||
m.mu.RLock()
|
||||
var found *Plugin
|
||||
for _, p := range m.plugins {
|
||||
if p.Name == name {
|
||||
if p.ID == id {
|
||||
found = p
|
||||
break
|
||||
}
|
||||
@@ -173,8 +174,8 @@ func (m *Manager) TogglePlugin(name string) {
|
||||
enabled := found.Enabled
|
||||
found.mu.Unlock()
|
||||
if m.pluginsFile != nil {
|
||||
if err := m.pluginsFile.setEnabled(name, enabled); err != nil {
|
||||
log.Printf("plugin %s: save state: %v", name, err)
|
||||
if err := m.pluginsFile.setEnabled(id, enabled); err != nil {
|
||||
log.Printf("plugin %s: save state: %v", id, err)
|
||||
}
|
||||
}
|
||||
if !enabled {
|
||||
@@ -188,8 +189,8 @@ func (m *Manager) TogglePlugin(name string) {
|
||||
if ret == lua.LFalse {
|
||||
p.Enabled = false
|
||||
if m.pluginsFile != nil {
|
||||
if err := m.pluginsFile.setEnabled(p.Name, false); err != nil {
|
||||
log.Printf("plugin %s: save state: %v", p.Name, err)
|
||||
if err := m.pluginsFile.setEnabled(p.ID, false); err != nil {
|
||||
log.Printf("plugin %s: save state: %v", p.ID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -217,11 +218,11 @@ func (m *Manager) TogglePlugin(name string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) SaveConfig(name, configText string) {
|
||||
func (m *Manager) SaveConfig(id, configText string) {
|
||||
m.mu.RLock()
|
||||
var found *Plugin
|
||||
for _, p := range m.plugins {
|
||||
if p.Name == name {
|
||||
if p.ID == id {
|
||||
found = p
|
||||
break
|
||||
}
|
||||
@@ -234,8 +235,8 @@ func (m *Manager) SaveConfig(name, configText string) {
|
||||
found.ConfigText = configText
|
||||
found.mu.Unlock()
|
||||
if m.pluginsFile != nil {
|
||||
if err := m.pluginsFile.setConfig(name, configText); err != nil {
|
||||
log.Printf("plugin %s: save config: %v", name, err)
|
||||
if err := m.pluginsFile.setConfig(id, configText); err != nil {
|
||||
log.Printf("plugin %s: save config: %v", id, err)
|
||||
}
|
||||
}
|
||||
if _, ok := found.hooks["on_config"]; !ok {
|
||||
@@ -243,7 +244,7 @@ func (m *Manager) SaveConfig(name, configText string) {
|
||||
}
|
||||
found.mu.Lock()
|
||||
if _, err := callHook(found, "on_config"); err != nil {
|
||||
log.Printf("plugin %s on_config: %v", name, err)
|
||||
log.Printf("plugin %s on_config: %v", id, err)
|
||||
}
|
||||
found.mu.Unlock()
|
||||
}
|
||||
@@ -273,8 +274,8 @@ func (m *Manager) RunOnStart() {
|
||||
if ret == lua.LFalse {
|
||||
p.Enabled = false
|
||||
if m.pluginsFile != nil {
|
||||
if err := m.pluginsFile.setEnabled(p.Name, false); err != nil {
|
||||
log.Printf("plugin %s: save state: %v", p.Name, err)
|
||||
if err := m.pluginsFile.setEnabled(p.ID, false); err != nil {
|
||||
log.Printf("plugin %s: save state: %v", p.ID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
)
|
||||
|
||||
type pluginFileEntry struct {
|
||||
Enable bool `yaml:"enable"`
|
||||
Config string `yaml:"config,omitempty"`
|
||||
Enable bool `yaml:"enable"`
|
||||
Config interface{} `yaml:"config,omitempty"`
|
||||
}
|
||||
|
||||
type pluginsFileData struct {
|
||||
@@ -51,28 +51,46 @@ func (pf *PluginsFile) save() error {
|
||||
return os.WriteFile(pf.path, raw, 0o600)
|
||||
}
|
||||
|
||||
func (pf *PluginsFile) get(name string) (enabled bool, config string, found bool) {
|
||||
e, ok := pf.data.Plugins[name]
|
||||
return e.Enable, e.Config, ok
|
||||
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(name string, defaultEnabled bool) {
|
||||
if _, ok := pf.data.Plugins[name]; !ok {
|
||||
pf.data.Plugins[name] = pluginFileEntry{Enable: defaultEnabled}
|
||||
func (pf *PluginsFile) register(id string, defaultEnabled bool) {
|
||||
if _, ok := pf.data.Plugins[id]; !ok {
|
||||
pf.data.Plugins[id] = pluginFileEntry{Enable: defaultEnabled}
|
||||
_ = pf.save()
|
||||
}
|
||||
}
|
||||
|
||||
func (pf *PluginsFile) setEnabled(name string, enabled bool) error {
|
||||
e := pf.data.Plugins[name]
|
||||
func (pf *PluginsFile) setEnabled(id string, enabled bool) error {
|
||||
e := pf.data.Plugins[id]
|
||||
e.Enable = enabled
|
||||
pf.data.Plugins[name] = e
|
||||
pf.data.Plugins[id] = e
|
||||
return pf.save()
|
||||
}
|
||||
|
||||
func (pf *PluginsFile) setConfig(name string, configText string) error {
|
||||
e := pf.data.Plugins[name]
|
||||
e.Config = configText
|
||||
pf.data.Plugins[name] = e
|
||||
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 {
|
||||
ID string
|
||||
Name string
|
||||
Description string
|
||||
FilePath string
|
||||
@@ -37,6 +38,7 @@ func (p *Plugin) HookConfig(name string) (HookConfig, bool) {
|
||||
}
|
||||
|
||||
type Info struct {
|
||||
ID string
|
||||
Name string
|
||||
Description string
|
||||
FilePath string
|
||||
@@ -57,6 +59,7 @@ func (p *Plugin) Info() Info {
|
||||
hooks[k] = v
|
||||
}
|
||||
return Info{
|
||||
ID: p.ID,
|
||||
Name: p.Name,
|
||||
Description: p.Description,
|
||||
FilePath: p.FilePath,
|
||||
|
||||
@@ -59,11 +59,11 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.textarea.Blur()
|
||||
if info, ok := m.selected(); ok && m.manager != nil {
|
||||
val := m.textarea.Value()
|
||||
m.manager.SaveConfig(info.Name, val)
|
||||
m.manager.SaveConfig(info.ID, val)
|
||||
// Update cached info.
|
||||
m.filtered[m.cursor].ConfigText = val
|
||||
for i := range m.items {
|
||||
if m.items[i].Name == info.Name {
|
||||
if m.items[i].ID == info.ID {
|
||||
m.items[i].ConfigText = val
|
||||
break
|
||||
}
|
||||
@@ -107,10 +107,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
case key.Matches(msg, pk.Toggle):
|
||||
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
|
||||
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
|
||||
break
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user