@@ -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()
+}
@@ -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)
+ }
+ })
+ }
+}
@@ -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