v0.36.0 doc sweep update
Package / arch (push) Canceled after 0s
Package / user (push) Canceled after 0s
Package / deb (push) Canceled after 0s
Package / freebsd (push) Canceled after 0s
Package / image (push) Canceled after 0s
Test / test (push) Canceled after 0s
Test / e2e (push) Canceled after 0s

This commit is contained in:
lixen committed 2026-10-14 11:33:04 -04:00
1 parent de87b9a008
commit a63b512733
51 files changed
+641 -420

No files matched your search

+2 -1
View File
@@ -573,7 +573,8 @@ func (p *Policy) Rejected() uint64 {
// Stats reports policy state for a plugin's stats details map. Merge it in
// with maps.Copy so rejections surface in the status reporter and in the
// http sink's status endpoint. allowed counts admissions (certificate checks,
// SCRAM logins); rejected counts every refusal.
// SCRAM logins), rejected refusals; a throttled or busy login counts in
// auth_throttled or auth_busy, a forwarded client the acl refuses in acl_denied.
func (p *Policy) Stats() map[string]any {
if p == nil {
return map[string]any{"auth": MethodNone}
+2 -1
View File
@@ -196,7 +196,8 @@ type asked struct {
tty bool
}
// startupOver is set once lw's first service is built
// startupOver is set once lw builds the service it runs, after any run on the
// --tui screen
var startupOver atomic.Bool
// EndStartup stops reading descriptors, pipes, FIFOs and terminals: at a
+4
View File
@@ -1,3 +1,7 @@
// Package chain is the protocol between logwisp nodes: the hello a dialer
// opens with (ProtocolVersion, node, SCRAM), the NDJSON entries after it, the
// headers and /auth path of its HTTP mapping, and the reconnect backoff the
// chain sinks share.
package chain
import (
+4 -3
View File
@@ -1,7 +1,8 @@
// Package compose is the configuration engine lw --tui and the website share:
// pipelines started empty, from a preset, a configuration or a command line,
// edited, validated, and written as a command line, environment or file. It
// reads no file and links no network or terminal code, so it builds as WebAssembly.
// pipelines started empty, from a preset, a configuration or a pasted form,
// edited, validated, and written as a command line, environment or file that
// reads back to the same pipelines; Schema is lw --schema. It reads no file and
// links no TLS, HTTP or terminal code, so it builds as WebAssembly.
package compose
import (
+8 -2
View File
@@ -1,3 +1,8 @@
// Package config is lw's configuration: the settings and pipeline tables, the
// plugin catalogue whose option structs' tags are the schema, the pipeline
// flags, LOGWISP_ variables and presets that build pipelines, and the rules
// Decode and ValidateConfig apply. Load's Manager merges the file, environment
// and command line, and rereads the file on reload.
package config
// --- LogWisp Configuration Options ---
@@ -31,7 +36,7 @@ type LogConfig struct {
// File output settings (when Output includes "file" or "all")
File *LogFileConfig `toml:"file"`
// Console output settings
// Accepted for older files: logging.output chooses the console
Console *LogConsoleConfig `toml:"console"`
}
@@ -44,7 +49,8 @@ type LogFileConfig struct {
RetentionHours float64 `toml:"retention_hours" help:"hours a .log file is kept; 0 keeps them"`
}
// LogConsoleConfig defines settings for console-based application logging
// LogConsoleConfig is logging.console, whose target has no effect:
// logging.output chooses the console stream
type LogConsoleConfig struct {
Target string `toml:"target" lw:"enum=stdout|stderr|split" help:"accepted without effect: logging.output chooses the console"`
}
+5 -4
View File
@@ -231,9 +231,9 @@ func settingKeys(v reflect.Value, prefix string) []Key {
}
// Decode reads a plugin's config map into its options: unknown keys, kinds,
// the defaults, the tag rules, the tls, auth and acl rules of its side, then the
// options' own Check; a leading ~ in a path is the home directory. It reads no
// file.
// the defaults, the tag rules and the options' own Check, then the host, tls,
// acl and auth rules of its side; a leading ~ in a path is the home directory.
// It reads no file.
func Decode[T any](role, typ string, m map[string]any) (*T, error) {
v, err := decode(role, typ, m)
if err != nil {
@@ -272,7 +272,8 @@ func decode(role, typ string, m map[string]any) (any, error) {
}
// Settle gives a decoded table its defaults, applies the tag rules, then its
// own Check: Decode's last steps, for the flow stages a file decodes typed
// own Check, as Decode does before its side's rules, for the flow stages a
// file decodes typed
func Settle(v any) error {
Fill(v)
if err := checkTags(reflect.ValueOf(v).Elem(), ""); err != nil {
+2 -1
View File
@@ -238,7 +238,8 @@ func SpecPipelines(specs []Spec, isDir IsDir) ([]PipelineConfig, error) {
}
// buildPipelines turns specs, in order, into pipelines; specs before the first
// --pipeline go to one named "cli". Every call returns fresh maps.
// --pipeline go to one named after the preset that starts it, or else "cli".
// Every call returns fresh maps.
func buildPipelines(specs []pipelineSpec, isDir IsDir) ([]PipelineConfig, error) {
var pipelines []PipelineConfig
var seen map[*specKind]bool
+8 -5
View File
@@ -25,7 +25,8 @@ import (
// ValidateConfig checks a configuration without reading a file it names:
// the settings, then each pipeline's flow stages, and its sources and sinks
// decoded in full. Its error joins every problem, one a part, each a
// *KeyError naming the path of the refused key. lw --check opens the files.
// *KeyError naming the refused key's path, save "no pipelines configured".
// lw --check opens the files.
func ValidateConfig(cfg *Config) error {
if cfg == nil {
return fmt.Errorf("config is nil")
@@ -521,9 +522,11 @@ func (c *FilterConfig) Check() error {
return nil
}
// Scan decodes a flow stage's command-line map into target, rejecting keys
// target does not declare: a misspelled key must fail, not be dropped. Its
// values take their kinds as a plugin's do, a wrong one refused at its key.
// Scan decodes a table into target: a flow stage from a spec or an edit, a
// file's root or the website's pipelines. It refuses a key target does not
// declare, nested tables' too: a misspelled key must fail, not be dropped.
// Values outside a list of tables take their kinds as a plugin's do, a wrong
// one refused at its key.
func Scan(configMap map[string]any, target any) error {
if err := checkKeys(configMap, reflect.TypeOf(target), ""); err != nil {
return err
@@ -580,7 +583,7 @@ func checkKeys(m map[string]any, t reflect.Type, prefix string) error {
// checkFileKeys rejects keys the configuration file declares that Config does
// not: a misspelled table path such as plugin_sinks.confg.tls would otherwise
// drop the whole table. Plugin config maps are checked by Scan instead.
// drop the whole table. Plugin config maps are checked by Decode instead.
func checkFileKeys(path string) error {
data, err := os.ReadFile(path)
if errors.Is(err, fs.ErrNotExist) {
+6 -1
View File
@@ -1,3 +1,7 @@
// Package core holds what the parts of a pipeline share: the LogEntry and the
// TransportEvent sinks get, the Tap a viewer reads a flow's decisions from,
// the level names, plugin capabilities, size limits and timings, and Network
// and Listen, which keep a listener or dialer to its host's address family.
package core
import (
@@ -29,7 +33,8 @@ type TransportEvent struct {
// Tapped is what a pipeline's flow decided about one entry: Stage names the
// stage that dropped Event.Entry (rate_limit, filters at Index, format), or is
// "" when it passed and Event.Payload is what the sinks got.
// "" when it passed and Event.Payload is what dispatch offered the sinks; a
// full sink input drops it for that sink only.
type Tapped struct {
Pipeline string
Source string // the source's id; "" for a heartbeat
+5 -9
View File
@@ -37,7 +37,6 @@ func NewFilter(cfg config.FilterConfig, logger *log.Logger) (*Filter, error) {
logger: logger,
}
// Compile patterns
for i, pattern := range cfg.Patterns {
re, err := regexp.Compile(pattern)
if err != nil {
@@ -82,8 +81,8 @@ func (f *Filter) Apply(entry core.LogEntry) bool {
"logic", f.config.Logic,
"entry_level", entry.Level,
"entry_source", entry.Source,
"entry_message", entry.Message[:min(100, len(entry.Message))], // First 100 chars
"text_to_match", text[:min(150, len(text))], // First 150 chars
"entry_message", entry.Message[:min(100, len(entry.Message))],
"text_to_match", text[:min(150, len(text))],
"patterns", f.config.Patterns)
for i, pattern := range f.config.Patterns {
@@ -103,7 +102,6 @@ func (f *Filter) Apply(entry core.LogEntry) bool {
"component", "filter",
"matched", matched)
// Determine if we should pass or drop
shouldPass := false
switch f.config.Type {
case config.FilterTypeInclude:
@@ -137,11 +135,12 @@ func (f *Filter) GetStats() map[string]any {
}
}
// UpdatePatterns allows for dynamic, thread-safe updates to the filter's regex patterns
// UpdatePatterns replaces the patterns under f.mu, which Apply does not take:
// it is safe only before entries flow
func (f *Filter) UpdatePatterns(patterns []string) error {
compiled := make([]*regexp.Regexp, 0, len(patterns))
// Compile all patterns first
// Compiled before the swap, so a bad pattern leaves the filter as it was
for i, pattern := range patterns {
re, err := regexp.Compile(pattern)
if err != nil {
@@ -150,7 +149,6 @@ func (f *Filter) UpdatePatterns(patterns []string) error {
compiled = append(compiled, re)
}
// Update atomically
f.mu.Lock()
f.patterns = compiled
f.config.Patterns = patterns
@@ -166,7 +164,6 @@ func (f *Filter) UpdatePatterns(patterns []string) error {
func (f *Filter) matches(text string) bool {
switch f.config.Logic {
case config.FilterLogicOr:
// Match any pattern
for _, re := range f.patterns {
if re.MatchString(text) {
return true
@@ -175,7 +172,6 @@ func (f *Filter) matches(text string) bool {
return false
case config.FilterLogicAnd:
// Must match all patterns
for _, re := range f.patterns {
if !re.MatchString(text) {
return false
+1 -7
View File
@@ -83,7 +83,6 @@ func (hg *HeartbeatGenerator) generateHeartbeat(t time.Time) core.TransportEvent
Message: "heartbeat",
}
// Add stats if configured
if hg.config.IncludeStats {
fields := map[string]any{
"type": "heartbeat",
@@ -98,13 +97,11 @@ func (hg *HeartbeatGenerator) generateHeartbeat(t time.Time) core.TransportEvent
entry.Fields = fieldsJSON
}
// Use formatter to generate payload
var payload []byte
var err error
// Check if we need special formatting for heartbeat
// Unreachable: heartbeat.format's enum (txt, json, raw) refuses "comment"
if hg.config.Format == "comment" {
// SSE comment format - bypass formatter for this special case
if hg.config.IncludeStats {
beatNum := hg.beatCount.Load()
payload = []byte(": heartbeat " + t.Format(time.RFC3339) +
@@ -115,7 +112,6 @@ func (hg *HeartbeatGenerator) generateHeartbeat(t time.Time) core.TransportEvent
} else {
// Use flow's formatter for consistent formatting
if adapter, ok := hg.formatter.(*format.FormatterAdapter); ok {
// Customize flags for heartbeat if needed
// The formatter shows a timestamp unless told otherwise
customFlags := formatter.FlagDefault
if !hg.config.IncludeTimestamp {
@@ -123,14 +119,12 @@ func (hg *HeartbeatGenerator) generateHeartbeat(t time.Time) core.TransportEvent
}
payload, err = adapter.FormatWithFlags(entry, customFlags)
} else {
// Fallback to standard format
payload, err = hg.formatter.Format(entry)
}
if err != nil {
hg.logger.Error("msg", "Failed to format heartbeat",
"error", err)
// Fallback to simple text
payload = []byte("heartbeat: " + t.Format(time.RFC3339) + "\n")
}
}
+4 -4
View File
@@ -1,8 +1,8 @@
// Package netacl is the address seam of the network listeners, beside tlsx
// and authz: a Policy reads the PROXY header of the proxies it trusts, then
// refuses a peer by its client's address, or past its client's limits, before
// TLS or a byte of the protocol is read. New returns nil when nothing is set,
// and every method tolerates a nil receiver.
// and authz: a Policy reads its trusted proxies' PROXY headers, refuses a peer
// by address or connection count before TLS, and an HTTP request past its rate
// or from a forwarded client the rules refuse. Its Table also throttles SCRAM.
// New returns nil when nothing is set; every method tolerates a nil receiver.
package netacl
import (
+6 -1
View File
@@ -1,3 +1,8 @@
// Package pipeline runs one pipeline: its sources through the flow to its
// sinks, each plugin made by the factory its type registered. Sinks start
// before the sources and stop after them, so entries in flight arrive; a full
// sink drops its event unless it declares core.CapBackpressure. A tap gives a
// viewer a copy of what the flow decides.
package pipeline
import (
@@ -39,7 +44,7 @@ type Pipeline struct {
cancel context.CancelFunc
wg sync.WaitGroup
running atomic.Bool
halt chan struct{} // closed by Stop: backpressure sinks drop again
halt chan struct{} // closed by Stop: backpressure events may then drop, room or not
finished chan struct{} // closed when every source has ended
finishedOnce sync.Once
}
+2 -2
View File
@@ -143,8 +143,8 @@ func TestStopReleasesStalledBackpressureSink(t *testing.T) {
}
// The tap gets each entry's verdict under its source's id: the stage that
// dropped it, or what the sinks got. Drops come only when All is set, and
// heartbeats come as passed, from no source.
// dropped it, or what dispatch offered the sinks. Drops come only when All is
// set, and heartbeats come as passed, from no source.
func TestTapSeesEachVerdict(t *testing.T) {
const n = 1000
verdict := func(msg string) (string, int) {
+3
View File
@@ -1,3 +1,6 @@
// Package plugin maps each source and sink type to the constructor its
// package registers in init; a type without a row in config's catalogue is
// refused.
package plugin
import (
+3
View File
@@ -1,3 +1,6 @@
// Package service holds the pipelines of one configuration: it builds all of
// them or none, starts and stops them together, gives a viewer's tap to each,
// and reports when every one has finished.
package service
import (
+10 -9
View File
@@ -1,3 +1,7 @@
// Package session tracks the sessions of a pipeline's plugins: a client
// connection, or a plugin's own input or output. A Manager per pipeline sweeps
// the idle ones; each plugin reaches it through a Proxy that sees only its
// instance's sessions.
package session
import (
@@ -11,7 +15,7 @@ import (
"github.com/lixenwraith/logwisp/internal/core"
)
// Session represents a connection session
// Session is a client connection, or a plugin's own input or output
type Session struct {
InstanceID string // Plugin instance identifier
ID string // Unique session identifier
@@ -20,8 +24,7 @@ type Session struct {
LastActivity time.Time // Last activity timestamp
Metadata map[string]any // Optional metadata (e.g., TLS info)
// Connection context
Source string // Source type: "tcp_source", "http_source", "tcp_sink", etc.
Source string // the owning plugin's instance ID, which Proxy sets
}
// Manager handles the lifecycle of sessions
@@ -34,7 +37,7 @@ type Manager struct {
cleanupTicker *time.Ticker
done chan struct{}
// Expiry callbacks by source type
// Expiry callbacks by plugin instance
expiryCallbacks map[string]func(sessionID, remoteAddr string)
callbacksMu sync.RWMutex
}
@@ -52,7 +55,6 @@ func NewManager(maxIdleTime time.Duration) *Manager {
expiryCallbacks: make(map[string]func(sessionID, remoteAddr string)),
}
// Start cleanup routine
m.startCleanup()
return m
@@ -145,7 +147,7 @@ func (m *Manager) GetSessionCount() int {
return len(m.sessions)
}
// GetSessionsBySource returns snapshots of sessions matching a specific source type.
// GetSessionsBySource returns snapshots of sessions matching a specific plugin instance.
func (m *Manager) GetSessionsBySource(source string) []*Session {
m.mu.RLock()
defer m.mu.RUnlock()
@@ -159,7 +161,7 @@ func (m *Manager) GetSessionsBySource(source string) []*Session {
return sessions
}
// GetActiveSessionsBySource returns snapshots of active sessions for a given source.
// GetActiveSessionsBySource returns snapshots of active sessions for a given plugin instance.
func (m *Manager) GetActiveSessionsBySource(source string) []*Session {
m.mu.RLock()
defer m.mu.RUnlock()
@@ -240,7 +242,7 @@ func (m *Manager) RegisterExpiryCallback(source string, callback func(sessionID,
m.expiryCallbacks[source] = callback
}
// UnregisterExpiryCallback removes an expiry callback for a given source type
// UnregisterExpiryCallback removes an expiry callback for a given plugin instance
func (m *Manager) UnregisterExpiryCallback(source string) {
m.callbacksMu.Lock()
defer m.callbacksMu.Unlock()
@@ -290,7 +292,6 @@ func (m *Manager) cleanupIdleSessions() {
for _, session := range expiredSessions {
if callback, exists := callbacks[session.Source]; exists {
// Call callback to notify owner
go callback(session.ID, session.RemoteAddr)
}
}
+2 -2
View File
@@ -82,8 +82,8 @@ func NewConsoleSinkPlugin(
output := Stream(opts.Target)
// inline decides auto: a terminal, NO_COLOR unset, TERM not dumb. Paint
// returns its text unchanged exactly when color is off.
// inline decides auto: a terminal, NO_COLOR empty or unset, TERM not
// dumb. Paint returns its text unchanged exactly when color is off.
printer := inline.New(output)
if opts.Color != "auto" {
printer.SetColor(opts.Color == "always")
+1 -5
View File
@@ -56,7 +56,6 @@ func NewFileSinkPlugin(
logger *log.Logger,
proxy *session.Proxy,
) (sink.Sink, error) {
// Create empty config struct
opts, err := config.Decode[config.FileSinkOptions]("sink", "file", configMap)
if err != nil {
return nil, err
@@ -71,7 +70,7 @@ func NewFileSinkPlugin(
writerConfig.RetentionPeriodHrs = opts.RetentionHours
writerConfig.BufferSize = opts.BufferSize
writerConfig.FlushIntervalMs = opts.FlushIntervalMs
// Sink logic
// The payload is already formatted: the writer adds nothing and writes the file only
writerConfig.EnableConsole = false
writerConfig.EnableFile = true
writerConfig.ShowTimestamp = false
@@ -97,7 +96,6 @@ func NewFileSinkPlugin(
}
fs.lastProcessed.Store(time.Time{})
// Create session for file output
fs.session = proxy.CreateSession(
fmt.Sprintf("file:///%s/%s", opts.Directory, opts.Name),
map[string]any{
@@ -173,12 +171,10 @@ func (fs *FileSink) Stop() {
<-fs.exited
}
// Remove session
if fs.session != nil {
fs.proxy.RemoveSession(fs.session.ID)
}
// Shutdown the writer with timeout
if err := fs.writer.Shutdown(core.LoggerShutdownTimeout); err != nil {
fs.logger.Error("msg", "Error shutting down file writer",
"component", "file_sink",
+5 -5
View File
@@ -452,7 +452,6 @@ func (h *HTTPSink) handleStream(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Accel-Buffering", "no")
w.WriteHeader(http.StatusOK)
// Connected event with metadata, parity with fasthttp sink
info, _ := json.Marshal(map[string]any{
"client_id": strconv.FormatUint(id, 10),
"session_id": sess.ID,
@@ -621,10 +620,11 @@ func (h *HTTPSink) GetStats() sink.SinkStats {
// handlers; absent when auth is disabled
type identityKey struct{}
// authMiddleware gates every endpoint on the policy: a client certificate
// or a bearer token. The rejection carries no detail: the status endpoint
// already exposes host, port, and throughput counters, so a refusal should
// not add the shape of the policy on top of that.
// authMiddleware gates stream and status on the policy: a client
// certificate, a bearer token or, behind a proxy, the session cookie. The
// rejection carries no detail: the status endpoint already exposes host,
// port, and throughput counters, so a refusal should not add the shape of
// the policy on top of that.
func (h *HTTPSink) authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ident, status, err := h.auth.AuthorizeRequest(r)
+1 -1
View File
@@ -373,7 +373,7 @@ func TestProxiedStreamSessionKeepsTheProxy(t *testing.T) {
// The constructor refuses what it cannot serve: the login pages outside
// scram's proxy mode, a viewer without the login page, a path under /auth, and
// a path ServeMux or a URL would not take literally, which panicked at Start.
// a path ServeMux or a URL would not take literally.
func TestUnservableOptionsAreRefused(t *testing.T) {
scram := map[string]any{"type": "scram", "credentials_file": scramCredentials(t)}
pki := testutil.NewPKI(t, "viewer-01")
+2 -2
View File
@@ -53,7 +53,7 @@ type HTTPChainSink struct {
tlsEnabled bool
mtls bool
// Authorization: pins the downstream server's identity
// Authorization: mtls pins the downstream server's identity; scram logs this sink in
auth *authz.Policy
client *http.Client
@@ -189,7 +189,7 @@ func (t *HTTPChainSink) Capabilities() []core.Capability {
caps = append(caps, core.CapTLS)
}
if t.auth.Enabled() {
caps = append(caps, core.CapAuth) // pins the server identity
caps = append(caps, core.CapAuth) // pins the server, or logs in under scram
}
return caps
}
+1 -1
View File
@@ -9,7 +9,7 @@ import (
// Sink represents an output data stream.
type Sink interface {
// Capabilities returns a slice of supported Source capabilities
// Capabilities returns a slice of supported Sink capabilities
Capabilities() []core.Capability
// Input returns the channel for sending transport events to this sink.
+5 -4
View File
@@ -221,8 +221,8 @@ func (t *TCPSink) Start(ctx context.Context) error {
go t.acceptLoop()
go t.broadcastLoop()
// Pipeline context cancellation mirrors gnet engine stop: cease accepting
// and tear down existing connections
// Pipeline context cancellation stops accepting and tears down the
// connections, as Stop does
go func() {
select {
case <-ctx.Done():
@@ -299,7 +299,8 @@ func (t *TCPSink) acceptLoop() {
}
if t.config.MaxConnections > 0 && t.activeConns.Load() >= t.config.MaxConnections {
// Load/admit race can over-admit by a conn under burst; acceptable
// The count rises only in handleConn, so a burst can over-admit by
// the conns accepted before their handleConn runs; acceptable
t.rejectedConns.Add(1)
conn.Close()
continue
@@ -328,7 +329,7 @@ func (t *TCPSink) handleConn(conn net.Conn) {
defer t.wg.Done()
remote := conn.RemoteAddr().String()
// Counted from accept: max_connections bounds concurrent handshakes too
// Counted before the handshake: max_connections bounds concurrent handshakes too
count := t.activeConns.Add(1)
defer func() {
conn.Close()
+2 -2
View File
@@ -44,7 +44,7 @@ type TCPChainSink struct {
network string
tlsConfig *tls.Config
// Authorization: pins the downstream server's identity
// Authorization: mtls pins the downstream server's identity; scram logs this sink in
auth *authz.Policy
input chan core.TransportEvent
@@ -158,7 +158,7 @@ func (t *TCPChainSink) Capabilities() []core.Capability {
caps = append(caps, core.CapTLS)
}
if t.auth.Enabled() {
caps = append(caps, core.CapAuth) // pins the server identity
caps = append(caps, core.CapAuth) // pins the server, or logs in under scram
}
return caps
}
+1 -1
View File
@@ -158,7 +158,7 @@ func TestRemoveWatcherPreservesReplacement(t *testing.T) {
}
// A line longer than core.MaxLogEntryBytes continues in the next entry, and the
// lines around it are read once: the watcher used to stop at it for good
// lines around it are read once
func TestLongLineContinuesInTheNextEntry(t *testing.T) {
path := filepath.Join(t.TempDir(), "app.log")
long := strings.Repeat("x", core.MaxLogEntryBytes+10)
+1 -1
View File
@@ -20,7 +20,7 @@ type Source interface {
// Stop gracefully shuts down the source
Stop()
// SourceStats contains statistics about a source
// GetStats returns the source's statistics
GetStats() SourceStats
}
+5 -3
View File
@@ -1,6 +1,8 @@
// Package tlsx builds crypto/tls configurations from config.TLSOptions.
// It is the single seam between declarative TLS config and the stdlib;
// each network plugin calls exactly one constructor.
// Package tlsx is the TLS seam of the network plugins, beside authz and
// netacl: Server and Client build a plugin's crypto/tls configuration from
// config.TLSOptions, a listener without files gets a certificate issued on the
// process key, lw tls makes its CA and certificates here, and LogWarnings
// reports settings that work but are likely mistakes.
package tlsx
import (
+4 -3
View File
@@ -1,7 +1,8 @@
// Package tui is lw --tui: pipelines composed on a full-screen canvas drawn as
// the website draws them, then run or written as a command line, environment
// or file. The pipelines are a compose.Composition; this package only draws
// them and turns keys into the engine's edits.
// the website draws them, run in place with a live view of what their stages
// pass and drop, then left to run or written as a command line, environment
// or file. The pipelines are a compose.Composition; this package draws them,
// turns keys into the engine's edits and runs them through a Runner from cmd/lw.
package tui
import (
+2 -2
View File
@@ -187,8 +187,8 @@ func TestARowCountsAllItHolds(t *testing.T) {
// The canvas cursor picks the point and v the verdicts at it: a source's
// entries, what reaches a stage or a filter, passes it or it drops, the
// heartbeats, what the sinks got and what the flow dropped on the way; lw's
// own log shows from warn up at every point, and whole at its own
// heartbeats, what the flow offered the sinks and what it dropped on the way;
// lw's own log shows from warn up at every point, and whole at its own
func TestThePointAndModePickTheRows(t *testing.T) {
c := pipeline(t, []string{"null", "null"}, []string{"null"})
for _, typ := range []string{"include", "exclude"} {