UI#
bgscan’s terminal UI is built on BubbleTea (the Elvish v2 fork at charm.land/bubbletea/v2) and Lipgloss for styling. The architecture is a component tree with an overlay dialog system and a centralized layout manager.
Package map#
| Package | Responsibility |
|---|---|
ui/main/app | Root BubbleTea model. Manages three application stages (splash → startup → workspace). |
ui/main/splash | Splash screen with ASCII art logo animation and version display. |
ui/main/startup | Startup health checks — validates Logger, Config, Xray, DNSTT, Slipstream, Vaydns, and App state. |
ui/main/workspace | Main workspace — header, body (component stack), footer, and overlay dialog stack. |
ui/main/header | Top bar — app title and branding. |
ui/main/body | Central content area — holds a component stack (main menu → sub-screens). |
ui/main/footer | Status bar — app version, current screen name, goroutine count, memory usage. |
ui/components/basic | Reusable widgets: confirm, crud, form, input, inspector, logview, menu, notice, picker, progress, table, tabs. |
ui/components/form | DNS tunnel config forms: dnstt, slipstream, vaydns. |
ui/components/inspector | Per-protocol settings forms (DNS, general, HTTP, ICMP, TCP, Xray). |
ui/components/menus | Screen-level menus: entry (main), logs, outbound, scantype, settings, targetsource, dnstunmenu. |
ui/components/tables | Data tables: iplist, ipviewer, outbounds, resultlist, dnstun (DNS tunnel CRUD). |
ui/components/scanner | Live scan view — tabbed progress bars + result tables per stage. |
ui/shared | Shared infrastructure: layout, dialog, env (keys/modes), component interface, validation. |
ui/theme | Dark/light/auto color palettes + huh form theme adapter. |
Component interface#
Every screen, widget, and dialog implements ui.shared.ui.Component:
type Component interface {
ID() ComponentID
Name() string
Init() tea.Cmd
Update(tea.Msg) (Component, tea.Cmd)
View() string
OnClose() tea.Cmd
Mode() env.Mode
}ID— UUID generated byNewComponentID(). Used to match open/close messages.Name— human-readable label shown in the footer status bar.Init— called once when mounted. Return atea.Cmdfor async setup.Update— message router. Returns the updated component and any commands.View— renders the component as a string.OnClose— cleanup when removed from the stack.Mode— determines which keys act as back/quit (see Input modes).
Root model (app)#
ui/main/app is the top-level BubbleTea model. It manages three application stages and the shared app state:
type model struct {
state *ui.AppState
splash ui.Component
startup ui.Component
workspace ui.Component
stage AppStage
}
func New(cfg *config.ScannerConfig, store *config.Store) tea.ModelThe app progresses through three stages:
| Stage | Component | Description |
|---|---|---|
StageSplash | splash | Animated ASCII logo with glitch effects and version display |
StageStartUP | startup | Sequential health checks (Logger → Config → Xray → DNSTT → Slipstream → Vaydns → App) |
StageWorkspace | workspace | Main workspace with header, body (component stack), footer, and overlay dialog stack |
AppState threads shared dependencies through the whole tree:
type AppState struct {
Layout *layout.Layout
Config *config.ScannerConfig
Store *config.Store
}Components read settings from state.Config and persist edits with state.Store. There is no config singleton to reach for.
Message routing#
The app routes messages differently depending on the current stage:
- Splash stage — the splash animation runs until complete, then transitions to startup.
- Startup stage — health check messages flow from the goroutine via
logChchannel. Critical errors abort subsequent checks. Pressing Enter transitions to workspace. - Workspace stage — the workspace handles all UI messages:
tea.WindowSizeMsg— recalculateslayout.Update(width, height).tea.KeyPressMsg—ctrl+cquits immediately;ctrl+tdumps goroutines.- Overlay intercept — if dialogs exist, the top dialog consumes all key input. Back/quit keys close it.
dialog.OpenDialogMsg— pushes a new overlay onto the stack with placement metadata.ui.CloseComponentMsg— removes the matching overlay by ID and runs itsOnClose.- Base dispatch — if no overlay blocks, the message fans out to header, body, and footer.
Rendering#
During the workspace stage, the rendering is:
┌─────────────────────────────────┐
│ header │
├─────────────────────────────────┤
│ │
│ body │ ← component stack (menu → screen)
│ │
├─────────────────────────────────┤
│ footer │ ← version · status · goroutines · mem
└─────────────────────────────────┘
+ overlay dialogs (centered/positioned)Overlays are rendered on top via bubbletea-overlay. Minimum terminal size is 75×35; below that a “terminal too small” message is shown.
Splash screen#
ui/main/splash displays an animated ASCII art logo of “BGSCAN” with glitch effects:
- 80 animation frames at 30ms tick, then 150 hold frames
- Glitch characters (
▓▒░╬╪┼╫┃━╳▄▀) replace logo characters with decreasing probability - Horizontal sweep accent color highlight at 72% progress
- Horizontal shake and artifact rows during animation
- Version display with fade-in after animation completes
Startup checks#
ui/main/startup runs seven sequential health checks in a goroutine, reporting status through a sidebar with dots (pending/running/success/warn/error), a progress bar, and a scrollable viewport:
| # | Category | What it checks |
|---|---|---|
| 1 | Logger | Initialize core, UI, and debug loggers; register probe schemas |
| 2 | Config | Load config, NormalizeAll, report any clamped values |
| 3 | Xray | Find binary, ensure executable, check version, validate outbound templates |
| 4 | DNSTT | Validate all DNSTT config files |
| 5 | Slipstream | Find binary, ensure executable, verify, validate config files |
| 6 | Vaydns | Validate all VayDNS config files |
| 7 | App | Wait for user to press Enter |
Critical errors abort subsequent checks. The user can scroll with j/k and press Enter to continue when done.
Body — component stack#
ui/main/body holds a stack of components. The top of the stack is the active screen. The body is part of the workspace stage and is not active during splash or startup.
components[0] = mainMenu (entry) ← always present
components[1] = scan source picker ← pushed on top
components[2] = scan type picker ← pushed on top
components[3] = scanner view ← pushed on topNavigation messages:
| Message | Effect |
|---|---|
ui.OpenComponentMsg | Push a new component onto the stack |
ui.CloseComponentMsg | Pop a specific component by ID |
ui.ResetComponentStacksMsg | Clear the stack back to the root menu |
Back key (b / Esc) | Pop the top component (if stack > 1) |
Quit key (q) | Show exit confirmation dialog |
Only the top component receives Update. When it’s popped, the next one down becomes active. The footer status bar updates to show the new top component’s Name().
Overlay dialog system#
ui/shared/dialog provides popup dialogs rendered on top of the base layout.
type OpenDialogMsg struct {
Component ui.Component
XPos DialogPosition // Top, Right, Bottom, Left, Center
YPos DialogPosition
XOffset int
YOffset int
}Dialogs use the rmhubbert/bubbletea-overlay library to position themselves. Most dialogs center on screen; file pickers and confirm dialogs may use different positions.
The dialog stack in app.model supports multiple nested overlays — the topmost consumes all input.
Input modes#
ui/shared/env/type.go defines three modes that control how keyboard input is interpreted:
| Mode | Description | Back keys | Quit keys |
|---|---|---|---|
NormalMode | Default navigation | b, Backspace, Esc | q, ctrl+c |
InputMode | Typing in a text field/picker | Esc | ctrl+c |
ScanMode | A scan is running | (none — UI controls handle pause/stop) | ctrl+c |
IsBackKey(msg, mode) and IsQuitKey(msg, mode) in env/keys.go check the mode-specific key sets. This prevents quit/back from firing while the user is typing in an input field.
Layout#
ui/shared/layout/layout.go is a stateful geometry calculator. It precomputes the position and size of each region (header, body, footer) whenever the terminal is resized:
type Layout struct {
Terminal TerminalSize // raw terminal dimensions
Content ContentSize // drawable area (after margins/padding)
Header ComponentSize
Body ComponentSize
Footer ComponentSize
}Layout.Update(width, height) is called on every tea.WindowSizeMsg. All components receive a reference to the shared *Layout so they can query their region without recomputing.
Reusable widgets (basic)#
ui/components/basic/ contains the building-block widgets used across screens:
| Widget | Purpose |
|---|---|
confirm | Yes/no confirmation dialog (used for exit, delete) |
crud | Generic CRUD table interface (IP lists, outbounds) with a Provider abstraction |
input | Input dialog shell + typed input widgets (textinput, textarea, selectinput, multiselect, toggleinput) |
inspector | Generic key-value settings editor with field formatting and type adapters |
logview | Scrollable streaming log viewer (core/ui/debug) |
menu | Vertical keyboard-navigable menu list |
notice | Transient notification banner |
picker | File picker dialog |
progress | Animated progress bar with ETA and rate display |
table | Sortable, scrollable data table |
tabs | Tabbed container (used by scanner view for per-stage tabs) |
Each widget follows the standard Component interface and is designed to be composed into larger screens.
Settings inspectors and forms#
Settings inspectors (ui/components/inspector/)#
| Form | Config it edits |
|---|---|
general | general_settings.toml |
icmp | icmp_settings.toml |
tcp | tcp_settings.toml |
http | http_settings.toml |
dns | dns_settings.toml |
xray | xray_settings.toml |
Each inspector reads its section from state.Config, renders editable fields, and on save calls the matching state.Store.SaveXxx() method. Fields can be dynamic — for example, TLS options only appear when HTTPS is selected, and tunnel-specific fields only show when the corresponding protocol is enabled.
Every field validates before it commits. The inspector applies the edit to a copy of the section, runs validate.ValidateXxx on it, and rejects the input if that field reports an error, so an invalid value never reaches the config or the disk.
The generic basic/inspector widget handles the field rendering, formatting, and type adaptation; the per-protocol packages just define which fields to show and how to map them to the config struct.
DNS tunnel config forms (ui/components/form/)#
| Form | Protocol | Config struct |
|---|---|---|
dnstt | DNSTT | dns.DNSTTConfig |
slipstream | Slipstream | dns.SlipstreamConfig |
vaydns | VayDNS | dns.VayDNSConfig |
Each form creates a protocol-specific inspector with fields for connection settings, advanced options (VayDNS only), and proxy/authentication. Forms support both creating new configs and editing existing ones. On save, they call the matching service’s SaveConfig or EditConfig method.
Menus and tables#
Menus (ui/components/menus)#
| Menu | Triggered from | What it shows |
|---|---|---|
entry | App start | Main menu (Run Scan, IP Files, Result Files, Xray Outbound, DNS Tunneling, Settings, Logs) |
targetsource | Run Scan | Source picker (IP List vs Result List) |
scantype | After source selection | Scan type picker (ICMP, TCP, HTTP, Xray, DNS Resolve, DNS Tunneling) |
settings | Settings menu item | Settings category picker |
outboundmenu | Xray Outbounds menu item | Outbound management |
dnstunmenu | DNS Tunneling scan type | Protocol selector (DNSTT, Slipstream, VayDNS) |
logs | Logs menu item | Log category picker (core/ui/debug) |
Tables (ui/components/tables)#
| Table | Data source | Provider |
|---|---|---|
iplist | ips/ directory | iplist.Registry |
resultlist | result/ directory | result.GetResultFiles |
outbounds | Xray outbound configs | xray package |
dnstun | DNS tunnel configs | dns.GetAllDNSTunsFile |
ipviewer | In-memory result set | scanner component |
Tables use the basic/table widget and a Provider interface that supplies rows and handles column sorting. resultlist renders its columns from the schema attached to each result.ResultFile, so a new probe’s results display without touching the table code. dnstun is a CRUD table that manages DNS tunnel configurations across all three protocols.
Scanner view#
ui/components/scanner is the live scan dashboard. It wires the Scanner to the UI:
- One tab per scan stage (via
basic/tabs). - Each tab shows a progress bar (
basic/progress) and a live result table (tables/ipviewer). - Stage status is tracked per-stage:
Waiting → PreProcess → Scanning → Ended / Error. - Progress updates flow via
engine.ScanHooks.OnProgress→Progressmessage → scanner componentUpdate. - Successful results are buffered in per-stage batches and rendered in the ipviewer table.
Controls during a scan: pause/resume, stop, switch tabs to view different stages.
Theming#
ui/theme/theme.go provides a centralized color palette:
type Theme struct {
Primary color.Color
Secondary color.Color
Border color.Color
BorderActive color.Color
Text color.Color
Muted color.Color
Timestamp color.Color
Info color.Color
Error color.Color
Success color.Color
Orange color.Color
Yellow color.Color
Purple color.Color
ProgressStart color.Color
ProgressEnd color.Color
}Modes#
| Mode | Behavior |
|---|---|
ModeAuto | Detects terminal background via $COLORFGBG and picks dark/light |
ModeDark | Forces the Dark palette |
ModeLight | Forces the Light palette |
theme.Init() is called from main() before the TUI starts. Components retrieve the active palette via theme.Current() — never hardcode colors.
Huh integration#
ui/theme/huh.go builds a huh.Theme from the centralized palette so that form fields (select, confirm, text input) rendered by the huh library match the rest of the UI instead of huh’s defaults. This is used by the input and inspector widgets.
Debugging#
Goroutine dump#
Pressing Ctrl+T at any time triggers dumpGoroutines() in app/update.go:
- Logs the total goroutine count.
- Writes the full
runtime/pprofgoroutine profile to the debug log. - Filters and logs lines containing
[chan receive],[chan send],[select],[IO wait],[sleep]— useful for spotting stuck workers or probe leaks.
Runtime stats#
The footer component polls Go runtime metrics every second:
runtime.NumGoroutine()— goroutine countruntime.ReadMemStats()— heap alloc and sys memory
These display in the status bar and are useful for monitoring scan resource usage in real time.
Related pages#
- Architecture — high-level project layout
- Core — scanner engine and probe interface