v0.30.0 lw --tui, compose pipeline on screen

This commit is contained in:
lixen committed 2026-10-09 19:30:50 -04:00
1 parent 2d72772943
commit 883af0fcaa
20 files changed
+2442 -69

No files matched your search

+2 -2
View File
@@ -52,13 +52,13 @@ var shorts = map[string]struct {
top bool
}{
"c": {"config", true}, "h": {"help", true}, "p": {"preset", true},
"q": {"quiet", true}, "t": {"check", true}, "V": {"version", true},
"q": {"quiet", true}, "t": {"check", true}, "T": {"tui", true}, "V": {"version", true},
"u": {"user", false},
}
// switches are lw's own options that take no value and are no setting, so no
// file or environment variable turns one on
var switches = []string{"check", "dump", "version", "schema"}
var switches = []string{"check", "dump", "version", "schema", "tui"}
// usageError is a command-line mistake: exit status 2 rather than 1
type usageError string
+14 -1
View File
@@ -39,6 +39,7 @@ func TestCommandLineGrammar(t *testing.T) {
{[]string{"--quiet", "-h"}, invocation{help: true, load: config.Args{Overrides: []string{"--quiet"}}}},
{[]string{"-c", "-h"}, invocation{help: true}},
{[]string{"help"}, invocation{help: true}},
{[]string{"-T", "-c", "a.toml"}, invocation{on: map[string]bool{"tui": true}, load: config.Args{File: "a.toml"}}},
{[]string{"tls", "ca", "-h"}, invocation{command: &commands[1], args: []string{"ca", "-h"}}},
{[]string{"-q", "-t", "-V", "-p", "tail,path=x", "-c=a.toml", "--logging.level", "debug", "--logging.file.retention_hours=-1", "--dump"},
invocation{on: map[string]bool{"check": true, "version": true, "dump": true},
@@ -52,7 +53,7 @@ func TestCommandLineGrammar(t *testing.T) {
}
for _, argv := range [][]string{{"-c"}, {"--config"}, {"--config="}, {"-c="}, {"-c", "--quiet"},
{"-v"}, {"-qt"}, {"-u", "x"}, {"-config", "x"}, {"--logging.level"}, {"--logging.file.retention_hours", "-1"},
{"--check=false"}, {"--version=true"}, {"--source"}, {"--sink=", "x"}, {"-p"}, {"--filter", "--sink", "x"}} {
{"--check=false"}, {"--version=true"}, {"--tui=x"}, {"-T", "x"}, {"--source"}, {"--sink=", "x"}, {"-p"}, {"--filter", "--sink", "x"}} {
if _, err := parseCommandLine(argv); err == nil {
t.Errorf("%q accepted", argv)
}
@@ -435,3 +436,15 @@ func TestCompletionScriptsAreCurrent(t *testing.T) {
}
}
}
// A named configuration file that is missing is a usage error, on the screen
// as without it
func TestAMissingNamedFileIsAUsageError(t *testing.T) {
inv, err := parseCommandLine([]string{"-T", "-c", filepath.Join(t.TempDir(), "none.toml")})
if err != nil {
t.Fatal(err)
}
if code, exit := composeOnScreen(&inv); code != 2 || !exit {
t.Fatalf("exit %d, %v", code, exit)
}
}
+5
View File
@@ -18,6 +18,8 @@ Usage:
lw -p|--preset SPEC [options] Run a preset's pipeline
lw -t|--check [options] Build every pipeline and plugin, report, exit
lw --dump [options] Print the effective configuration as TOML, exit
lw -T|--tui [options] Compose pipelines on screen, then run them or
print them as flags, variables or a file
lw help | -h | --help
lw -V | --version | --schema
@@ -31,6 +33,9 @@ after '='; short options do not combine. lw takes options only.
else ./logwisp.toml)
-t, --check Build every pipeline and plugin, report, exit
--dump Print the effective configuration, exit
-T, --tui Compose the pipelines on a full-screen
terminal, starting from the ones lw would run
or a preset; then run them, or print them
-q, --quiet Silence lw's own log and notices; pipeline
output still flows
--color [WHEN] Level names in color on console sinks: auto
+54 -6
View File
@@ -13,6 +13,7 @@ import (
"github.com/lixenwraith/logwisp/internal/compose"
"github.com/lixenwraith/logwisp/internal/config"
"github.com/lixenwraith/logwisp/internal/core"
"github.com/lixenwraith/logwisp/internal/tui"
"github.com/lixenwraith/logwisp/internal/version"
"github.com/lixenwraith/log"
@@ -45,18 +46,19 @@ func main() {
os.Exit(0)
}
if inv.on["tui"] {
if code, exit := composeOnScreen(&inv); exit {
os.Exit(code)
}
}
// --- 1. Initial setup ---
// Emulates nohup
signal.Ignore(syscall.SIGHUP)
manager, err := config.Load(inv.load)
if err != nil {
if errors.Is(err, config.ErrConfigNotFound) {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(2)
}
fmt.Fprintf(os.Stderr, "Error: Failed to load config: %v\n", err)
os.Exit(1)
os.Exit(loadFailed(err))
}
defer manager.Close()
cfg, err := manager.Snapshot()
@@ -216,3 +218,49 @@ func readsStdin(cfg *config.Config) bool {
}
return false
}
// loadFailed reports a configuration that did not load and returns lw's exit
// code: a named file that is missing is a usage error
func loadFailed(err error) int {
if errors.Is(err, config.ErrConfigNotFound) {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
return 2
}
fmt.Fprintf(os.Stderr, "Error: Failed to load config: %v\n", err)
return 1
}
// composeOnScreen is lw --tui: it starts from the pipelines lw would load, or
// the preset menu in place of the built-in pipe, then prints a form and exits,
// or leaves inv to run the pipelines composed in place of the loaded ones
func composeOnScreen(inv *invocation) (code int, exit bool) {
fail := func(err error) (int, bool) {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
return 1, true
}
manager, err := config.Load(inv.load)
if err != nil {
return loadFailed(err), true
}
cfg, err := manager.Snapshot()
builtIn := manager.BuiltIn()
manager.Close()
start := &compose.Composition{}
if err == nil && !builtIn {
start, err = compose.FromConfig(cfg)
}
if err != nil {
return fail(err)
}
res, err := tui.Run(start, cfg.Color)
switch {
case err != nil:
return fail(err)
case res.Exit == tui.Print:
fmt.Print(res.Output)
case res.Exit == tui.Start:
inv.load.Specs = config.PipelineSpecs(res.Pipelines)
return 0, false
}
return 0, true
}