The bubblefetch plugin system supports two extension paths:
- External modules (recommended): drop an executable script in
~/.config/bubblefetch/plugins/external/ - Go plugins: native
.soplugins for power users
External modules are safer and easier to ship across platforms because they are simple executables.
Create an executable file in ~/.config/bubblefetch/plugins/external/ and add its name (without extension) to your modules list.
Example script:
#!/usr/bin/env bash
echo '{"label":"Weather","value":"72°F","icon":""}'Example with git context (branch, dirty, ahead/behind, root):
#!/usr/bin/env bash
root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
[[ -z "$root" ]] && exit 0
branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "detached")"
dirty="clean"
git diff --quiet --ignore-submodules -- || dirty="dirty"
counts="$(git rev-list --left-right --count @{upstream}...HEAD 2>/dev/null || echo "0\t0")"
behind="$(printf "%s" "$counts" | awk '{print $1}')"
ahead="$(printf "%s" "$counts" | awk '{print $2}')"
line1="Branch: ${branch} · ${dirty} · ↑${ahead} ↓${behind}"
line2="Root: ${root}"
printf '{\"label\":\"Git\",\"icon\":\"\",\"lines\":[\"%s\",\"%s\"]}\n' "$line1" "$line2"Make it executable and install:
mkdir -p ~/.config/bubblefetch/plugins/external
chmod +x weather.sh
cp weather.sh ~/.config/bubblefetch/plugins/external/Then add to config:
modules:
- os
- weather
- cpuExternal modules write to stdout. If the output is JSON, bubblefetch will parse it; otherwise it renders the raw text.
Supported JSON fields:
{
"label": "Weather",
"value": "72°F",
"icon": "",
"separator": ": ",
"lines": ["Custom line 1", "Custom line 2"],
"raw": "Fallback text"
}Notes:
label+valuerenders with theme colors.iconis optional and shown like built-in modules.linesrenders multiple lines;raw/textare used as fallback.- Timeout is controlled by
external_module_timeout_ms(default: 250ms). - Bubblefetch sets
BUBBLEFETCH_FORMAT=json,BUBBLEFETCH_MODULE=<name>, andBUBBLEFETCH_CWD=<current dir>env vars.
External modules: ✅ Works anywhere bubblefetch runs.
Go plugins (.so):
- ✅ Linux
- ✅ macOS
- ✅ FreeBSD
- ❌ Windows (Go plugin limitation)
On Windows, use external modules or contribute a built-in module.
Create a file myplugin.go:
package main
import (
"github.com/howieduhzit/bubblefetch/internal/collectors"
"github.com/howieduhzit/bubblefetch/internal/ui/theme"
)
// ModuleName must be a package-level variable named exactly "ModuleName"
var ModuleName = "myplugin"
// Render must have this exact signature
func Render(info *collectors.SystemInfo, styles theme.Styles) string {
// Access system info
hostname := info.Hostname
// Format output with theme styles
label := styles.Label.Render("Custom")
separator := styles.Separator.Render(": ")
value := styles.Value.Render("Hello from " + hostname)
return label + separator + value
}go build -buildmode=plugin -o myplugin.so myplugin.gomkdir -p ~/.config/bubblefetch/plugins
cp myplugin.so ~/.config/bubblefetch/plugins/Add to your ~/.config/bubblefetch/config.yaml:
modules:
- os
- myplugin # Your custom plugin!
- cpuRun bubblefetch and your plugin will appear in the TUI!
The GitHub Pages site reads plugins/manifest.json to list plugins in the
browser. Downloads should point to GitHub Release assets so the repo does not
store binary .so files.
Manifest example:
{
"id": "hello",
"name": "Hello",
"version": "0.1.0",
"downloads": [
{
"label": "Linux (amd64)",
"url": "https://github.com/howieduhzit/bubblefetch/releases/download/plugins-v0.1.0/hello_linux_amd64.so"
}
],
"source": "https://github.com/howieduhzit/bubblefetch/blob/main/plugins/examples/hello.go"
}Go plugins are OS/arch specific. Build on the target platform and upload the
resulting .so to a GitHub Release.
scripts/build-plugin.sh hello plugins/examples/hello.goThen update plugins/manifest.json with the release asset URL.
Every plugin must export exactly these two symbols:
var ModuleName = "pluginname"- Type:
string(must be a pointer to string when loaded) - Purpose: Identifies the plugin module
- Used in: config.yaml
moduleslist - Naming: Use lowercase, alphanumeric characters
func Render(info *collectors.SystemInfo, styles theme.Styles) string- Parameters:
info *collectors.SystemInfo- All collected system informationstyles theme.Styles- Current theme's style definitions
- Returns:
string- The formatted output to display - Purpose: Renders the module's output for the TUI
The collectors.SystemInfo struct provides access to:
type SystemInfo struct {
// Basic info
OS string
Kernel string
Hostname string
Uptime string
// Hardware
CPU string
GPU []string
Memory MemoryInfo
Disk DiskInfo
// Environment
Shell string
Terminal string
DE string // Desktop Environment
WM string // Window Manager
// Network
Network []NetworkInfo
LocalIP string
PublicIP string
// Power
Battery BatteryInfo
}See internal/collectors/types.go for complete struct definitions.
The theme.Styles provides these styled renderers:
styles.Label.Render("Label") // Renders label text
styles.Value.Render("Value") // Renders value text
styles.Separator.Render(": ") // Renders separator
styles.Primary.Render("Text") // Primary color
styles.Secondary.Render("Text") // Secondary color
styles.Accent.Render("Text") // Accent colorUsing theme styles ensures your plugin respects the user's theme choice.
package main
import (
"fmt"
"github.com/howieduhzit/bubblefetch/internal/collectors"
"github.com/howieduhzit/bubblefetch/internal/ui/theme"
)
var ModuleName = "hello"
func Render(info *collectors.SystemInfo, styles theme.Styles) string {
greeting := fmt.Sprintf("Hello from %s!", info.Hostname)
label := styles.Label.Render("Greeting")
separator := styles.Separator.Render(": ")
value := styles.Value.Render(greeting)
return label + separator + value
}package main
import (
"fmt"
"runtime"
"github.com/howieduhzit/bubblefetch/internal/collectors"
"github.com/howieduhzit/bubblefetch/internal/ui/theme"
)
var ModuleName = "cores"
func Render(info *collectors.SystemInfo, styles theme.Styles) string {
cores := runtime.NumCPU()
label := styles.Label.Render("CPU Cores")
separator := styles.Separator.Render(": ")
value := styles.Value.Render(fmt.Sprintf("%d", cores))
return label + separator + value
}package main
import (
"fmt"
"os"
"github.com/howieduhzit/bubblefetch/internal/collectors"
"github.com/howieduhzit/bubblefetch/internal/ui/theme"
)
var ModuleName = "myinfo"
func Render(info *collectors.SystemInfo, styles theme.Styles) string {
// Read custom info from environment or file
customValue := os.Getenv("MY_CUSTOM_VAR")
if customValue == "" {
customValue = "Not set"
}
label := styles.Label.Render("My Info")
separator := styles.Separator.Render(": ")
value := styles.Value.Render(customValue)
return label + separator + value
}go build -buildmode=plugin -o myplugin.so myplugin.goAdd to Makefile:
plugin-myplugin:
go build -buildmode=plugin -o plugins/myplugin.so plugins/myplugin.goThen:
make plugin-mypluginPlugins can import external packages:
# In your plugin directory
go mod init myplugin
go get github.com/some/package
go build -buildmode=plugin -o myplugin.so .Important: Plugins must be built with the same Go version and same dependency versions as bubblefetch to avoid runtime errors.
Default: ~/.config/bubblefetch/plugins/
Custom directory in config.yaml:
plugin_dir: /path/to/my/pluginsExternal modules live in a subfolder:
~/.config/bubblefetch/plugins/external/
Plugins are automatically loaded from the plugin directory when bubblefetch starts.
- Any
.sofile in the directory is loaded as a Go plugin. - Any executable file in
external/is loaded as an external module.
Add plugin names to the modules list:
modules:
- os
- kernel
- myplugin1
- cpu
- myplugin2
- memoryOrder matters - modules display in the order listed.
If a plugin fails to load, bubblefetch will:
- Print a warning to stderr
- Continue loading other plugins
- Skip the failed plugin module
Common errors:
Warning: failed to load plugin myplugin.so: plugin missing ModuleName
Warning: failed to load plugin myplugin.so: Render is not the correct function type
Run with stderr visible:
bubblefetch 2>&1 | grep -i pluginCheck if plugin loaded:
ls -la ~/.config/bubblefetch/plugins/Verify plugin symbols:
nm -D myplugin.so | grep -E "ModuleName|Render"Plugins should be lightweight and fast. Heavy computation can slow down bubblefetch startup.
// Good - fast and simple
func Render(info *collectors.SystemInfo, styles theme.Styles) string {
return styles.Label.Render("User") + styles.Separator.Render(": ") +
styles.Value.Render(os.Getenv("USER"))
}
// Bad - slow external API call
func Render(info *collectors.SystemInfo, styles theme.Styles) string {
weather := fetchWeatherAPI() // Blocks startup!
return formatWeather(weather)
}Return empty string or fallback text on errors:
func Render(info *collectors.SystemInfo, styles theme.Styles) string {
data, err := readCustomData()
if err != nil {
// Don't panic - return empty or fallback
return ""
}
return formatData(data, styles)
}Always use the provided styles instead of hardcoded colors:
// Good - respects user's theme
label := styles.Label.Render("Custom")
value := styles.Value.Render(data)
// Bad - hardcoded colors break themes
label := "\033[1;34mCustom\033[0m"If your plugin has no data to show, return empty string:
func Render(info *collectors.SystemInfo, styles theme.Styles) string {
data := getOptionalData()
if data == "" {
return "" // Module won't display
}
return formatData(data, styles)
}Follow the same format as built-in modules:
Label: Value
Use the helper pattern:
label := styles.Label.Render("Label")
separator := styles.Separator.Render(": ")
value := styles.Value.Render("Value")
return label + separator + valuePlugins cannot:
- Add new data to
SystemInfostruct - Modify existing system info
- Affect JSON/YAML/text export modes
- Change theme styles
- Modify other modules
Plugins can only:
- Read existing
SystemInfodata - Format and display custom output for the TUI
Plugins cannot communicate with each other or share state.
Go plugins only work on Linux, macOS, and FreeBSD. Windows is not supported.
package main
import (
"encoding/json"
"io"
"net/http"
"sync"
"time"
)
var ModuleName = "weather"
var (
weatherCache string
weatherCacheTime time.Time
weatherMutex sync.Mutex
)
func Render(info *collectors.SystemInfo, styles theme.Styles) string {
weatherMutex.Lock()
defer weatherMutex.Unlock()
// Cache for 30 minutes
if time.Since(weatherCacheTime) > 30*time.Minute {
weatherCache = fetchWeather()
weatherCacheTime = time.Now()
}
if weatherCache == "" {
return ""
}
label := styles.Label.Render("Weather")
separator := styles.Separator.Render(": ")
value := styles.Value.Render(weatherCache)
return label + separator + value
}
func fetchWeather() string {
// Quick timeout to avoid blocking
client := &http.Client{Timeout: 2 * time.Second}
resp, err := client.Get("https://wttr.in?format=j1")
if err != nil {
return ""
}
defer resp.Body.Close()
// Parse and return weather
// (implementation details...)
return "Sunny, 72°F"
}- Check file extension: Must be
.so - Check permissions:
chmod +x myplugin.so - Check location: Must be in
~/.config/bubblefetch/plugins/ - Check symbols: Run
nm -D myplugin.so | grep -E "ModuleName|Render"
- Check config: Module name must be in
moduleslist - Check return value: Plugin must return non-empty string
- Check spelling: Module name must match
ModuleNamevariable exactly - Check stderr: Look for load warnings
Error: ModuleName is not *string
Make sure ModuleName is a package-level variable, not a constant:
// Correct
var ModuleName = "myplugin"
// Wrong
const ModuleName = "myplugin"Error: Render is not the correct function type
Signature must match exactly:
func Render(info *collectors.SystemInfo, styles theme.Styles) stringIf you create a useful plugin, consider contributing it:
- Share in Discussions: Post in GitHub Discussions
- Create Example: Add to
plugins/examples/ - Submit PR: Contribute to bubblefetch core
Popular or widely useful plugins may be integrated into bubblefetch as built-in modules.
- Example Plugins:
plugins/examples/ - SystemInfo Struct:
internal/collectors/types.go - Theme Styles:
internal/ui/theme/theme.go - Built-in Modules:
internal/ui/modules/ - Go Plugins: https://pkg.go.dev/plugin
- Create plugin with
ModuleNamevar andRenderfunction - Build with
go build -buildmode=plugin - Copy to
~/.config/bubblefetch/plugins/ - Add module name to config
- Enjoy your custom module!
Happy plugin development! 🔌