Docs GitHub
-- --
Theme:
Language:
EN فا

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#

PackageResponsibility
ui/main/appRoot BubbleTea model. Manages three application stages (splash → startup → workspace).
ui/main/splashSplash screen with ASCII art logo animation and version display.
ui/main/startupStartup health checks — validates Logger, Config, Xray, DNSTT, Slipstream, Vaydns, and App state.
ui/main/workspaceMain workspace — header, body (component stack), footer, and overlay dialog stack.
ui/main/headerTop bar — app title and branding.
ui/main/bodyCentral content area — holds a component stack (main menu → sub-screens).
ui/main/footerStatus bar — app version, current screen name, goroutine count, memory usage.
ui/components/basicReusable widgets: confirm, crud, form, input, inspector, logview, menu, notice, picker, progress, table, tabs.
ui/components/formDNS tunnel config forms: dnstt, slipstream, vaydns.
ui/components/inspectorPer-protocol settings forms (DNS, general, HTTP, ICMP, TCP, Xray).
ui/components/menusScreen-level menus: entry (main), logs, outbound, scantype, settings, targetsource, dnstunmenu.
ui/components/tablesData tables: iplist, ipviewer, outbounds, resultlist, dnstun (DNS tunnel CRUD).
ui/components/scannerLive scan view — tabbed progress bars + result tables per stage.
ui/sharedShared infrastructure: layout, dialog, env (keys/modes), component interface, validation.
ui/themeDark/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 by NewComponentID(). Used to match open/close messages.
  • Name — human-readable label shown in the footer status bar.
  • Init — called once when mounted. Return a tea.Cmd for 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.Model

The app progresses through three stages:

StageComponentDescription
StageSplashsplashAnimated ASCII logo with glitch effects and version display
StageStartUPstartupSequential health checks (Logger → Config → Xray → DNSTT → Slipstream → Vaydns → App)
StageWorkspaceworkspaceMain 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:

  1. Splash stage — the splash animation runs until complete, then transitions to startup.
  2. Startup stage — health check messages flow from the goroutine via logCh channel. Critical errors abort subsequent checks. Pressing Enter transitions to workspace.
  3. Workspace stage — the workspace handles all UI messages:
    • tea.WindowSizeMsg — recalculates layout.Update(width, height).
    • tea.KeyPressMsgctrl+c quits immediately; ctrl+t dumps 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 its OnClose.
    • 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:

#CategoryWhat it checks
1LoggerInitialize core, UI, and debug loggers; register probe schemas
2ConfigLoad config, NormalizeAll, report any clamped values
3XrayFind binary, ensure executable, check version, validate outbound templates
4DNSTTValidate all DNSTT config files
5SlipstreamFind binary, ensure executable, verify, validate config files
6VaydnsValidate all VayDNS config files
7AppWait 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 top

Navigation messages:

MessageEffect
ui.OpenComponentMsgPush a new component onto the stack
ui.CloseComponentMsgPop a specific component by ID
ui.ResetComponentStacksMsgClear 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:

ModeDescriptionBack keysQuit keys
NormalModeDefault navigationb, Backspace, Escq, ctrl+c
InputModeTyping in a text field/pickerEscctrl+c
ScanModeA 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:

WidgetPurpose
confirmYes/no confirmation dialog (used for exit, delete)
crudGeneric CRUD table interface (IP lists, outbounds) with a Provider abstraction
inputInput dialog shell + typed input widgets (textinput, textarea, selectinput, multiselect, toggleinput)
inspectorGeneric key-value settings editor with field formatting and type adapters
logviewScrollable streaming log viewer (core/ui/debug)
menuVertical keyboard-navigable menu list
noticeTransient notification banner
pickerFile picker dialog
progressAnimated progress bar with ETA and rate display
tableSortable, scrollable data table
tabsTabbed 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/)#

FormConfig it edits
generalgeneral_settings.toml
icmpicmp_settings.toml
tcptcp_settings.toml
httphttp_settings.toml
dnsdns_settings.toml
xrayxray_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/)#

FormProtocolConfig struct
dnsttDNSTTdns.DNSTTConfig
slipstreamSlipstreamdns.SlipstreamConfig
vaydnsVayDNSdns.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.


MenuTriggered fromWhat it shows
entryApp startMain menu (Run Scan, IP Files, Result Files, Xray Outbound, DNS Tunneling, Settings, Logs)
targetsourceRun ScanSource picker (IP List vs Result List)
scantypeAfter source selectionScan type picker (ICMP, TCP, HTTP, Xray, DNS Resolve, DNS Tunneling)
settingsSettings menu itemSettings category picker
outboundmenuXray Outbounds menu itemOutbound management
dnstunmenuDNS Tunneling scan typeProtocol selector (DNSTT, Slipstream, VayDNS)
logsLogs menu itemLog category picker (core/ui/debug)

Tables (ui/components/tables)#

TableData sourceProvider
iplistips/ directoryiplist.Registry
resultlistresult/ directoryresult.GetResultFiles
outboundsXray outbound configsxray package
dnstunDNS tunnel configsdns.GetAllDNSTunsFile
ipviewerIn-memory result setscanner 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.OnProgressProgress message → scanner component Update.
  • 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#

ModeBehavior
ModeAutoDetects terminal background via $COLORFGBG and picks dark/light
ModeDarkForces the Dark palette
ModeLightForces 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/pprof goroutine 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 count
  • runtime.ReadMemStats() — heap alloc and sys memory

These display in the status bar and are useful for monitoring scan resource usage in real time.


  • Architecture — high-level project layout
  • Core — scanner engine and probe interface