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 layout, header/body/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, input, inspector, logview, menu, notice, picker, progress, table, tabs. |
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. |
ui/components/tables | Data tables: iplist, ipviewer, outbounds, resultlist. |
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 holds three base components and a dialog stack:
type model struct {
layout *layout.Layout
dialog []ui.Component
dialogPlacements map[ui.ComponentID]*dialogPosition
header ui.Component
body ui.Component
footer ui.Component
}Message routing (app/update.go)#
The root Update is the central router:
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 (app/view.go)#
The root View renders:
┌─────────────────────────────────┐
│ 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.
Body — component stack#
ui/main/body holds a stack of components. The top of the stack is the active screen.
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#
ui/components/inspector/ contains per-protocol settings forms:
| 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 the current config via config.GetXxx(), renders editable fields, and on save calls config.SaveXxxConfig(). Fields can be dynamic — for example, TLS options only appear when HTTPS is selected, and DNSTT/SlipStream fields only show when those probes are enabled.
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.
Menus and tables#
Menus (ui/components/menus)#
| Menu | Triggered from | What it shows |
|---|---|---|
entry | App start | Main menu (Run Scan, IP Files, Result Files, Settings, Xray Outbounds, Logs) |
targetsource | Run Scan | Source picker (IP List vs Result List) |
scantype | After source selection | Scan type picker (ICMP, TCP, HTTP, DNS, Xray) |
settings | Settings menu item | Settings category picker |
outboundmenu | Xray Outbounds menu item | Outbound management |
logs | Logs menu item | Log category picker (core/ui/debug) |
Tables (ui/components/tables)#
| Table | Data source | Provider |
|---|---|---|
iplist | ips/ directory | iplist.Registry |
resultlist | results/ directory | result.Registry |
outbounds | Xray outbound configs | xray package |
ipviewer | In-memory result set | scanner component |
Tables use the basic/table widget and a Provider interface that supplies rows and handles column sorting.
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
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 during startup. 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