From c6d482d1174c37ecbd5b801370775eabca370a3c Mon Sep 17 00:00:00 2001 From: Amolith Date: Fri, 10 Apr 2026 08:51:54 -0600 Subject: [PATCH] Add isInteractive helper to detect tty and env var The helper detects whether keld is running in an interactive environment by checking if stdin is a terminal and whether KELD_NONINTERACTIVE is set. This mirrors the same check bubbletea performs internally and will guard the TUI entry points against non-interactive invocations like systemd timers. The function is injectable for testing via the isStdinTerminal package variable. Task: td-47ZDQ11 --- cmd/interactive.go | 23 +++++++++++++ cmd/interactive_test.go | 74 +++++++++++++++++++++++++++++++++++++++++ go.mod | 10 +++--- 3 files changed, 102 insertions(+), 5 deletions(-) create mode 100644 cmd/interactive.go create mode 100644 cmd/interactive_test.go diff --git a/cmd/interactive.go b/cmd/interactive.go new file mode 100644 index 0000000000000000000000000000000000000000..436e56d6dd88ca13c0f78450d87f69311942d9ac --- /dev/null +++ b/cmd/interactive.go @@ -0,0 +1,23 @@ +package cmd + +import ( + "os" + + "github.com/charmbracelet/x/term" +) + +// isStdinTerminal is a package-level variable so tests can override it. +// Production code uses term.IsTerminal(os.Stdin.Fd()). +var isStdinTerminal = func() bool { + return term.IsTerminal(os.Stdin.Fd()) +} + +// isInteractive reports whether keld should launch its TUI. +// It returns false when stdin is not a terminal or when +// KELD_NONINTERACTIVE is set to a non-empty value. +func isInteractive() bool { + if os.Getenv("KELD_NONINTERACTIVE") != "" { + return false + } + return isStdinTerminal() +} \ No newline at end of file diff --git a/cmd/interactive_test.go b/cmd/interactive_test.go new file mode 100644 index 0000000000000000000000000000000000000000..fe397446d44bbbfeb7a289184ae86f3147669e3a --- /dev/null +++ b/cmd/interactive_test.go @@ -0,0 +1,74 @@ +package cmd + +import ( + "testing" +) + +func TestIsInteractive(t *testing.T) { + tests := []struct { + name string + stdinIsTerminal bool + envValue string // empty string means env is cleared/unset + want bool + }{ + { + name: "terminal with empty env returns true", + stdinIsTerminal: true, + envValue: "", + want: true, + }, + { + name: "terminal with env=1 returns false", + stdinIsTerminal: true, + envValue: "1", + want: false, + }, + { + name: "terminal with env=true returns false", + stdinIsTerminal: true, + envValue: "true", + want: false, + }, + { + name: "non-terminal with empty env returns false", + stdinIsTerminal: false, + envValue: "", + want: false, + }, + { + name: "non-terminal with env=1 returns false", + stdinIsTerminal: false, + envValue: "1", + want: false, + }, + { + name: "terminal with env=0 returns false", + stdinIsTerminal: true, + envValue: "0", + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Save and restore the original function. + original := isStdinTerminal + t.Cleanup(func() { + isStdinTerminal = original + }) + + // Inject fake terminal check. + isStdinTerminal = func() bool { + return tt.stdinIsTerminal + } + + // Set env var. t.Setenv("", "") clears/unsets it. + t.Setenv("KELD_NONINTERACTIVE", tt.envValue) + + got := isInteractive() + if got != tt.want { + t.Errorf("isInteractive() = %v, want %v", got, tt.want) + } + }) + } +} \ No newline at end of file diff --git a/go.mod b/go.mod index 36b467eb7a7f38e75c061cf9b99fa4b99b0e2f99..7718ff5aa3aab0fdc21bfa09ca2c8d852612cea1 100644 --- a/go.mod +++ b/go.mod @@ -3,16 +3,20 @@ module git.secluded.site/keld go 1.26.1 require ( + charm.land/bubbles/v2 v2.0.0 charm.land/bubbletea/v2 v2.0.2 charm.land/fang/v2 v2.0.1 charm.land/huh/v2 v2.0.3 charm.land/lipgloss/v2 v2.0.2 github.com/BurntSushi/toml v1.6.0 + github.com/charmbracelet/x/term v0.2.2 + github.com/dustin/go-humanize v1.0.1 + github.com/lrstanley/bubbletint/v2 v2.0.1 github.com/spf13/cobra v1.10.2 + github.com/spf13/pflag v1.0.9 ) require ( - charm.land/bubbles/v2 v2.0.0 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/catppuccin/go v0.3.0 // indirect github.com/charmbracelet/colorprofile v0.4.2 // indirect @@ -21,14 +25,11 @@ require ( github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444 // indirect github.com/charmbracelet/x/exp/ordered v0.1.0 // indirect github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect - github.com/charmbracelet/x/term v0.2.2 // indirect github.com/charmbracelet/x/termios v0.1.1 // indirect github.com/charmbracelet/x/windows v0.2.2 // indirect github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect - github.com/dustin/go-humanize v1.0.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/lrstanley/bubbletint/v2 v2.0.1 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mattn/go-runewidth v0.0.20 // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect @@ -38,7 +39,6 @@ require ( github.com/muesli/mango-pflag v0.1.0 // indirect github.com/muesli/roff v0.1.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/spf13/pflag v1.0.9 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.42.0 // indirect