diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9cbe351 --- /dev/null +++ b/Makefile @@ -0,0 +1,23 @@ +.PHONY: build test lint demo demo-build demo-run clean + +build: + go build -o snitch . + +test: + go test ./... + +lint: + golangci-lint run + +demo: demo-build demo-run + +demo-build: + docker build -f demo/Dockerfile -t snitch-demo . + +demo-run: + docker run --rm -v $(PWD)/demo:/output snitch-demo + +clean: + rm -f snitch + rm -f demo/demo.gif + diff --git a/cmd/cli_test.go b/cmd/cli_test.go index 6b73ed2..ac69e60 100644 --- a/cmd/cli_test.go +++ b/cmd/cli_test.go @@ -361,8 +361,8 @@ func resetGlobalFlags() { showTimestamp = false sortBy = "" fields = "" - ipv4 = false - ipv6 = false + filterIPv4 = false + filterIPv6 = false colorMode = "auto" numeric = false } diff --git a/cmd/ls.go b/cmd/ls.go index 2a733d4..2f1edcd 100644 --- a/cmd/ls.go +++ b/cmd/ls.go @@ -22,20 +22,15 @@ import ( "golang.org/x/term" ) +// ls-specific flags var ( outputFormat string noHeaders bool showTimestamp bool sortBy string fields string - ipv4 bool - ipv6 bool colorMode string numeric bool - lsTCP bool - lsUDP bool - lsListen bool - lsEstab bool plainOutput bool ) @@ -56,39 +51,16 @@ Available filters: } func runListCommand(outputFormat string, args []string) { - color.Init(colorMode) - - filters, err := parseFilters(args) - if err != nil { - log.Fatalf("Error parsing filters: %v", err) - } - filters.IPv4 = ipv4 - filters.IPv6 = ipv6 - - // apply shortcut flags - if lsTCP && !lsUDP { - filters.Proto = "tcp" - } else if lsUDP && !lsTCP { - filters.Proto = "udp" - } - if lsListen && !lsEstab { - filters.State = "LISTEN" - } else if lsEstab && !lsListen { - filters.State = "ESTABLISHED" - } - - connections, err := collector.GetConnections() + rt, err := NewRuntime(args, colorMode, numeric) if err != nil { log.Fatal(err) } - filteredConnections := collector.FilterConnections(connections, filters) - + // apply sorting if sortBy != "" { - collector.SortConnections(filteredConnections, collector.ParseSortOptions(sortBy)) + rt.SortConnections(collector.ParseSortOptions(sortBy)) } else { - // default sort by local port - collector.SortConnections(filteredConnections, collector.SortOptions{ + rt.SortConnections(collector.SortOptions{ Field: collector.SortByLport, Direction: collector.SortAsc, }) @@ -99,93 +71,26 @@ func runListCommand(outputFormat string, args []string) { selectedFields = strings.Split(fields, ",") } - switch outputFormat { + renderList(rt.Connections, outputFormat, selectedFields) +} + +func renderList(connections []collector.Connection, format string, selectedFields []string) { + switch format { case "json": - printJSON(filteredConnections) + printJSON(connections) case "csv": - printCSV(filteredConnections, !noHeaders, showTimestamp, selectedFields) + printCSV(connections, !noHeaders, showTimestamp, selectedFields) case "table", "wide": if plainOutput { - printPlainTable(filteredConnections, !noHeaders, showTimestamp, selectedFields) + printPlainTable(connections, !noHeaders, showTimestamp, selectedFields) } else { - printStyledTable(filteredConnections, !noHeaders, selectedFields) + printStyledTable(connections, !noHeaders, selectedFields) } default: - log.Fatalf("Invalid output format: %s. Valid formats are: table, wide, json, csv", outputFormat) + log.Fatalf("Invalid output format: %s. Valid formats are: table, wide, json, csv", format) } } -func parseFilters(args []string) (collector.FilterOptions, error) { - filters := collector.FilterOptions{} - for _, arg := range args { - parts := strings.SplitN(arg, "=", 2) - if len(parts) != 2 { - return filters, fmt.Errorf("invalid filter format: %s", arg) - } - key, value := parts[0], parts[1] - switch strings.ToLower(key) { - case "proto": - filters.Proto = value - case "state": - filters.State = value - case "pid": - pid, err := strconv.Atoi(value) - if err != nil { - return filters, fmt.Errorf("invalid pid value: %s", value) - } - filters.Pid = pid - case "proc": - filters.Proc = value - case "lport": - port, err := strconv.Atoi(value) - if err != nil { - return filters, fmt.Errorf("invalid lport value: %s", value) - } - filters.Lport = port - case "rport": - port, err := strconv.Atoi(value) - if err != nil { - return filters, fmt.Errorf("invalid rport value: %s", value) - } - filters.Rport = port - case "user": - uid, err := strconv.Atoi(value) - if err == nil { - filters.UID = uid - } else { - filters.User = value - } - case "laddr": - filters.Laddr = value - case "raddr": - filters.Raddr = value - case "contains": - filters.Contains = value - case "if", "interface": - filters.Interface = value - case "mark": - filters.Mark = value - case "namespace": - filters.Namespace = value - case "inode": - inode, err := strconv.ParseInt(value, 10, 64) - if err != nil { - return filters, fmt.Errorf("invalid inode value: %s", value) - } - filters.Inode = inode - case "since": - since, sinceRel, err := collector.ParseTimeFilter(value) - if err != nil { - return filters, fmt.Errorf("invalid since value: %s", value) - } - filters.Since = since - filters.SinceRel = sinceRel - default: - return filters, fmt.Errorf("unknown filter key: %s", key) - } - } - return filters, nil -} func getFieldMap(c collector.Connection) map[string]string { laddr := c.Laddr @@ -483,20 +388,16 @@ func init() { cfg := config.Get() + // ls-specific flags lsCmd.Flags().StringVarP(&outputFormat, "output", "o", cfg.Defaults.OutputFormat, "Output format (table, wide, json, csv)") lsCmd.Flags().BoolVar(&noHeaders, "no-headers", cfg.Defaults.NoHeaders, "Omit headers for table/csv output") lsCmd.Flags().BoolVar(&showTimestamp, "ts", false, "Include timestamp in output") lsCmd.Flags().StringVarP(&sortBy, "sort", "s", cfg.Defaults.SortBy, "Sort by column (e.g., pid:desc)") lsCmd.Flags().StringVarP(&fields, "fields", "f", strings.Join(cfg.Defaults.Fields, ","), "Comma-separated list of fields to show") - lsCmd.Flags().BoolVarP(&ipv4, "ipv4", "4", cfg.Defaults.IPv4, "Only show IPv4 connections") - lsCmd.Flags().BoolVarP(&ipv6, "ipv6", "6", cfg.Defaults.IPv6, "Only show IPv6 connections") lsCmd.Flags().StringVar(&colorMode, "color", cfg.Defaults.Color, "Color mode (auto, always, never)") lsCmd.Flags().BoolVarP(&numeric, "numeric", "n", cfg.Defaults.Numeric, "Don't resolve hostnames") - - // shortcut filters - lsCmd.Flags().BoolVarP(&lsTCP, "tcp", "t", false, "Show only TCP connections") - lsCmd.Flags().BoolVarP(&lsUDP, "udp", "u", false, "Show only UDP connections") - lsCmd.Flags().BoolVarP(&lsListen, "listen", "l", false, "Show only listening sockets") - lsCmd.Flags().BoolVarP(&lsEstab, "established", "e", false, "Show only established connections") lsCmd.Flags().BoolVarP(&plainOutput, "plain", "p", false, "Plain output (parsable, no styling)") + + // shared filter flags + addFilterFlags(lsCmd) } \ No newline at end of file diff --git a/cmd/ls_test.go b/cmd/ls_test.go index 1930b56..c7c4afc 100644 --- a/cmd/ls_test.go +++ b/cmd/ls_test.go @@ -251,7 +251,7 @@ func TestParseFilters(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - filters, err := parseFilters(tt.args) + filters, err := ParseFilterArgs(tt.args) if tt.expectError { if err == nil { diff --git a/cmd/root.go b/cmd/root.go index e72208e..cccacfa 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -40,12 +40,11 @@ func init() { rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.config/snitch/snitch.toml)") rootCmd.PersistentFlags().Bool("debug", false, "enable debug logs to stderr") - // add top's filter flags to root so `snitch -l` works + // add top's flags to root so `snitch -l` works (defaults to top command) cfg := config.Get() rootCmd.Flags().StringVar(&topTheme, "theme", cfg.Defaults.Theme, "Theme for TUI (dark, light, mono, auto)") rootCmd.Flags().DurationVarP(&topInterval, "interval", "i", 0, "Refresh interval (default 1s)") - rootCmd.Flags().BoolVarP(&topTCP, "tcp", "t", false, "Show only TCP connections") - rootCmd.Flags().BoolVarP(&topUDP, "udp", "u", false, "Show only UDP connections") - rootCmd.Flags().BoolVarP(&topListen, "listen", "l", false, "Show only listening sockets") - rootCmd.Flags().BoolVarP(&topEstab, "established", "e", false, "Show only established connections") + + // shared filter flags for root command + addFilterFlags(rootCmd) } \ No newline at end of file diff --git a/cmd/runtime.go b/cmd/runtime.go new file mode 100644 index 0000000..6ddad0d --- /dev/null +++ b/cmd/runtime.go @@ -0,0 +1,201 @@ +package cmd + +import ( + "fmt" + "snitch/internal/collector" + "snitch/internal/color" + "strconv" + "strings" + + "github.com/spf13/cobra" +) + +// Runtime holds the shared state for all commands. +// it handles common filter logic, fetching, and filtering connections. +type Runtime struct { + // filter options built from flags and args + Filters collector.FilterOptions + + // filtered connections ready for rendering + Connections []collector.Connection + + // common settings + ColorMode string + Numeric bool +} + +// shared filter flags - used by all commands +var ( + filterTCP bool + filterUDP bool + filterListen bool + filterEstab bool + filterIPv4 bool + filterIPv6 bool +) + +// BuildFilters constructs FilterOptions from command args and shortcut flags. +func BuildFilters(args []string) (collector.FilterOptions, error) { + filters, err := ParseFilterArgs(args) + if err != nil { + return filters, err + } + + // apply ipv4/ipv6 flags + filters.IPv4 = filterIPv4 + filters.IPv6 = filterIPv6 + + // apply protocol shortcut flags + if filterTCP && !filterUDP { + filters.Proto = "tcp" + } else if filterUDP && !filterTCP { + filters.Proto = "udp" + } + + // apply state shortcut flags + if filterListen && !filterEstab { + filters.State = "LISTEN" + } else if filterEstab && !filterListen { + filters.State = "ESTABLISHED" + } + + return filters, nil +} + +// FetchConnections gets connections from the collector and applies filters. +func FetchConnections(filters collector.FilterOptions) ([]collector.Connection, error) { + connections, err := collector.GetConnections() + if err != nil { + return nil, err + } + + return collector.FilterConnections(connections, filters), nil +} + +// NewRuntime creates a runtime with fetched and filtered connections. +func NewRuntime(args []string, colorMode string, numeric bool) (*Runtime, error) { + color.Init(colorMode) + + filters, err := BuildFilters(args) + if err != nil { + return nil, fmt.Errorf("failed to parse filters: %w", err) + } + + connections, err := FetchConnections(filters) + if err != nil { + return nil, fmt.Errorf("failed to fetch connections: %w", err) + } + + return &Runtime{ + Filters: filters, + Connections: connections, + ColorMode: colorMode, + Numeric: numeric, + }, nil +} + +// SortConnections sorts the runtime's connections in place. +func (r *Runtime) SortConnections(opts collector.SortOptions) { + collector.SortConnections(r.Connections, opts) +} + +// ParseFilterArgs parses key=value filter arguments. +// exported for testing. +func ParseFilterArgs(args []string) (collector.FilterOptions, error) { + filters := collector.FilterOptions{} + for _, arg := range args { + parts := strings.SplitN(arg, "=", 2) + if len(parts) != 2 { + return filters, fmt.Errorf("invalid filter format: %s (expected key=value)", arg) + } + key, value := parts[0], parts[1] + if err := applyFilter(&filters, key, value); err != nil { + return filters, err + } + } + return filters, nil +} + +// applyFilter applies a single key=value filter to FilterOptions. +func applyFilter(filters *collector.FilterOptions, key, value string) error { + switch strings.ToLower(key) { + case "proto": + filters.Proto = value + case "state": + filters.State = value + case "pid": + pid, err := strconv.Atoi(value) + if err != nil { + return fmt.Errorf("invalid pid value: %s", value) + } + filters.Pid = pid + case "proc": + filters.Proc = value + case "lport": + port, err := strconv.Atoi(value) + if err != nil { + return fmt.Errorf("invalid lport value: %s", value) + } + filters.Lport = port + case "rport": + port, err := strconv.Atoi(value) + if err != nil { + return fmt.Errorf("invalid rport value: %s", value) + } + filters.Rport = port + case "user": + uid, err := strconv.Atoi(value) + if err == nil { + filters.UID = uid + } else { + filters.User = value + } + case "laddr": + filters.Laddr = value + case "raddr": + filters.Raddr = value + case "contains": + filters.Contains = value + case "if", "interface": + filters.Interface = value + case "mark": + filters.Mark = value + case "namespace": + filters.Namespace = value + case "inode": + inode, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return fmt.Errorf("invalid inode value: %s", value) + } + filters.Inode = inode + case "since": + since, sinceRel, err := collector.ParseTimeFilter(value) + if err != nil { + return fmt.Errorf("invalid since value: %s", value) + } + filters.Since = since + filters.SinceRel = sinceRel + default: + return fmt.Errorf("unknown filter key: %s", key) + } + return nil +} + +// FilterFlagsHelp returns the help text for common filter flags. +const FilterFlagsHelp = ` +Filters are specified in key=value format. For example: + snitch ls proto=tcp state=established + +Available filters: + proto, state, pid, proc, lport, rport, user, laddr, raddr, contains, if, mark, namespace, inode, since` + +// addFilterFlags adds the common filter flags to a command. +func addFilterFlags(cmd *cobra.Command) { + cmd.Flags().BoolVarP(&filterTCP, "tcp", "t", false, "Show only TCP connections") + cmd.Flags().BoolVarP(&filterUDP, "udp", "u", false, "Show only UDP connections") + cmd.Flags().BoolVarP(&filterListen, "listen", "l", false, "Show only listening sockets") + cmd.Flags().BoolVarP(&filterEstab, "established", "e", false, "Show only established connections") + cmd.Flags().BoolVarP(&filterIPv4, "ipv4", "4", false, "Only show IPv4 connections") + cmd.Flags().BoolVarP(&filterIPv6, "ipv6", "6", false, "Only show IPv6 connections") +} + diff --git a/cmd/stats.go b/cmd/stats.go index 05ebaea..99b4ce8 100644 --- a/cmd/stats.go +++ b/cmd/stats.go @@ -39,15 +39,12 @@ type InterfaceStats struct { Count int `json:"count"` } +// stats-specific flags var ( statsOutputFormat string statsInterval time.Duration statsCount int statsNoHeaders bool - statsTCP bool - statsUDP bool - statsListen bool - statsEstab bool ) var statsCmd = &cobra.Command{ @@ -67,24 +64,10 @@ Available filters: } func runStatsCommand(args []string) { - filters, err := parseFilters(args) + filters, err := BuildFilters(args) if err != nil { log.Fatalf("Error parsing filters: %v", err) } - filters.IPv4 = ipv4 - filters.IPv6 = ipv6 - - // apply shortcut flags - if statsTCP && !statsUDP { - filters.Proto = "tcp" - } else if statsUDP && !statsTCP { - filters.Proto = "udp" - } - if statsListen && !statsEstab { - filters.State = "LISTEN" - } else if statsEstab && !statsListen { - filters.State = "ESTABLISHED" - } ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -137,13 +120,11 @@ func runStatsCommand(args []string) { } func generateStats(filters collector.FilterOptions) (*StatsData, error) { - connections, err := collector.GetConnections() + filteredConnections, err := FetchConnections(filters) if err != nil { return nil, err } - filteredConnections := collector.FilterConnections(connections, filters) - stats := &StatsData{ Timestamp: time.Now(), Total: len(filteredConnections), @@ -307,16 +288,13 @@ func printStatsTable(stats *StatsData, headers bool) { func init() { rootCmd.AddCommand(statsCmd) + + // stats-specific flags statsCmd.Flags().StringVarP(&statsOutputFormat, "output", "o", "table", "Output format (table, json, csv)") statsCmd.Flags().DurationVarP(&statsInterval, "interval", "i", 0, "Refresh interval (0 = one-shot)") statsCmd.Flags().IntVarP(&statsCount, "count", "c", 0, "Number of iterations (0 = unlimited)") statsCmd.Flags().BoolVar(&statsNoHeaders, "no-headers", false, "Omit headers for table/csv output") - statsCmd.Flags().BoolVarP(&ipv4, "ipv4", "4", false, "Only show IPv4 connections") - statsCmd.Flags().BoolVarP(&ipv6, "ipv6", "6", false, "Only show IPv6 connections") - // shortcut filters - statsCmd.Flags().BoolVarP(&statsTCP, "tcp", "t", false, "Show only TCP connections") - statsCmd.Flags().BoolVarP(&statsUDP, "udp", "u", false, "Show only UDP connections") - statsCmd.Flags().BoolVarP(&statsListen, "listen", "l", false, "Show only listening sockets") - statsCmd.Flags().BoolVarP(&statsEstab, "established", "e", false, "Show only established connections") + // shared filter flags + addFilterFlags(statsCmd) } diff --git a/cmd/top.go b/cmd/top.go index fb90070..1ebf60a 100644 --- a/cmd/top.go +++ b/cmd/top.go @@ -10,13 +10,10 @@ import ( "github.com/spf13/cobra" ) +// top-specific flags var ( topTheme string topInterval time.Duration - topTCP bool - topUDP bool - topListen bool - topEstab bool ) var topCmd = &cobra.Command{ @@ -36,11 +33,11 @@ var topCmd = &cobra.Command{ } // if any filter flag is set, use exclusive mode - if topTCP || topUDP || topListen || topEstab { - opts.TCP = topTCP - opts.UDP = topUDP - opts.Listening = topListen - opts.Established = topEstab + if filterTCP || filterUDP || filterListen || filterEstab { + opts.TCP = filterTCP + opts.UDP = filterUDP + opts.Listening = filterListen + opts.Established = filterEstab opts.Other = false opts.FilterSet = true } @@ -57,10 +54,11 @@ var topCmd = &cobra.Command{ func init() { rootCmd.AddCommand(topCmd) cfg := config.Get() + + // top-specific flags topCmd.Flags().StringVar(&topTheme, "theme", cfg.Defaults.Theme, "Theme for TUI (dark, light, mono, auto)") topCmd.Flags().DurationVarP(&topInterval, "interval", "i", time.Second, "Refresh interval") - topCmd.Flags().BoolVarP(&topTCP, "tcp", "t", false, "Show only TCP connections") - topCmd.Flags().BoolVarP(&topUDP, "udp", "u", false, "Show only UDP connections") - topCmd.Flags().BoolVarP(&topListen, "listen", "l", false, "Show only listening sockets") - topCmd.Flags().BoolVarP(&topEstab, "established", "e", false, "Show only established connections") + + // shared filter flags + addFilterFlags(topCmd) } \ No newline at end of file diff --git a/cmd/trace.go b/cmd/trace.go index a070329..f060395 100644 --- a/cmd/trace.go +++ b/cmd/trace.go @@ -47,12 +47,10 @@ Available filters: } func runTraceCommand(args []string) { - filters, err := parseFilters(args) + filters, err := BuildFilters(args) if err != nil { log.Fatalf("Error parsing filters: %v", err) } - filters.IPv4 = ipv4 - filters.IPv6 = ipv6 ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -222,11 +220,14 @@ func printTraceEventHuman(event TraceEvent) { func init() { rootCmd.AddCommand(traceCmd) + + // trace-specific flags traceCmd.Flags().DurationVarP(&traceInterval, "interval", "i", time.Second, "Polling interval (e.g., 500ms, 2s)") traceCmd.Flags().IntVarP(&traceCount, "count", "c", 0, "Number of events to capture (0 = unlimited)") traceCmd.Flags().StringVarP(&traceOutputFormat, "output", "o", "human", "Output format (human, json)") traceCmd.Flags().BoolVarP(&traceNumeric, "numeric", "n", false, "Don't resolve hostnames") traceCmd.Flags().BoolVar(&traceTimestamp, "ts", false, "Include timestamp in output") - traceCmd.Flags().BoolVarP(&ipv4, "ipv4", "4", false, "Only trace IPv4 connections") - traceCmd.Flags().BoolVarP(&ipv6, "ipv6", "6", false, "Only trace IPv6 connections") + + // shared filter flags + addFilterFlags(traceCmd) } diff --git a/cmd/watch.go b/cmd/watch.go index ed2991d..8c611c6 100644 --- a/cmd/watch.go +++ b/cmd/watch.go @@ -7,7 +7,6 @@ import ( "log" "os" "os/signal" - "snitch/internal/collector" "syscall" "time" @@ -36,17 +35,14 @@ Available filters: } func runWatchCommand(args []string) { - filters, err := parseFilters(args) + filters, err := BuildFilters(args) if err != nil { log.Fatalf("Error parsing filters: %v", err) } - filters.IPv4 = ipv4 - filters.IPv6 = ipv6 ctx, cancel := context.WithCancel(context.Background()) defer cancel() - // Handle interrupts gracefully sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) go func() { @@ -63,18 +59,16 @@ func runWatchCommand(args []string) { case <-ctx.Done(): return case <-ticker.C: - connections, err := collector.GetConnections() + connections, err := FetchConnections(filters) if err != nil { log.Printf("Error getting connections: %v", err) continue } - filteredConnections := collector.FilterConnections(connections, filters) - frame := map[string]interface{}{ "timestamp": time.Now().Format(time.RFC3339Nano), - "connections": filteredConnections, - "count": len(filteredConnections), + "connections": connections, + "count": len(connections), } jsonOutput, err := json.Marshal(frame) @@ -95,8 +89,11 @@ func runWatchCommand(args []string) { func init() { rootCmd.AddCommand(watchCmd) + + // watch-specific flags watchCmd.Flags().DurationVarP(&watchInterval, "interval", "i", time.Second, "Refresh interval (e.g., 500ms, 2s)") watchCmd.Flags().IntVarP(&watchCount, "count", "c", 0, "Number of frames to emit (0 = unlimited)") - watchCmd.Flags().BoolVarP(&ipv4, "ipv4", "4", false, "Only show IPv4 connections") - watchCmd.Flags().BoolVarP(&ipv6, "ipv6", "6", false, "Only show IPv6 connections") + + // shared filter flags + addFilterFlags(watchCmd) } diff --git a/demo/demo.gif b/demo/demo.gif index 803b2cb..7d4f4de 100644 Binary files a/demo/demo.gif and b/demo/demo.gif differ diff --git a/demo/demo.tape b/demo/demo.tape index 2e3ce51..9ea74be 100644 --- a/demo/demo.tape +++ b/demo/demo.tape @@ -1,6 +1,3 @@ -# VHS tape file for snitch demo -# run with: docker build -f demo/Dockerfile -t snitch-demo . && docker run -v $(pwd)/demo:/output snitch-demo - Output demo.gif Set Shell "bash" @@ -11,7 +8,8 @@ Set Height 700 Set Theme "Catppuccin Frappe" Set Padding 15 Set Framerate 24 -Set TypingSpeed 40ms +Set TypingSpeed 30ms +Set PlaybackSpeed 1.5 # force color output Env TERM "xterm-256color" @@ -23,77 +21,74 @@ Env FORCE_COLOR "1" # launch snitch Type "./snitch top" Enter -Sleep 2s +Sleep 1.5s # navigate down through connections Down -Sleep 400ms +Sleep 200ms Down -Sleep 400ms +Sleep 200ms Down -Sleep 400ms +Sleep 200ms Down -Sleep 400ms +Sleep 200ms Down -Sleep 1s +Sleep 600ms # open detail view for selected connection Enter -Sleep 2s +Sleep 1.5s # close detail view Escape -Sleep 1s +Sleep 500ms # search for nginx Type "/" -Sleep 500ms +Sleep 300ms Type "nginx" -Sleep 1s +Sleep 600ms Enter -Sleep 2s +Sleep 1.2s # clear search Type "/" -Sleep 300ms +Sleep 200ms Escape -Sleep 1s +Sleep 500ms # filter: hide udp, show only tcp Type "u" -Sleep 1.5s +Sleep 800ms # show only listening connections Type "e" -Sleep 1.5s +Sleep 800ms Type "o" -Sleep 1.5s +Sleep 800ms # reset to show all Type "a" -Sleep 1.5s +Sleep 800ms # cycle through sort options Type "s" -Sleep 1s +Sleep 500ms Type "s" -Sleep 1s -Type "s" -Sleep 1s +Sleep 500ms # reverse sort order Type "S" -Sleep 1.5s +Sleep 800ms # show help screen Type "?" -Sleep 3s +Sleep 2s # close help Escape -Sleep 1s +Sleep 500ms # quit Type "q" -Sleep 300ms - +Sleep 200ms diff --git a/flake.nix b/flake.nix index 12d4a66..ee5bd83 100644 --- a/flake.nix +++ b/flake.nix @@ -64,7 +64,7 @@ pname = "snitch"; version = self.shortRev or self.dirtyShortRev or "dev"; src = self; - vendorHash = "sha256-BNNbA72puV0QSLkAlgn/buJJt7mIlVkbTEBhTXOg8pY="; + vendorHash = "sha256-fX3wOqeOgjH7AuWGxPQxJ+wbhp240CW8tiF4rVUUDzk="; env.CGO_ENABLED = 0; ldflags = [ "-s" "-w"