Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions cmd/subcommands/acci-ping/acci-ping.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Use of this source code is governed by a GPL-2 license that can be found in the LICENSE file.
//
// Copyright 2024-2025 Lexer747
// Copyright 2024-2026 Lexer747
//
// SPDX-License-Identifier: GPL-2.0-only

Expand Down Expand Up @@ -41,15 +41,15 @@ type Config struct {

func GetFlags(info *application.BuildInfo) *Config {
f := flag.NewFlagSet("", flag.ContinueOnError)
tf := tabflags.NewAutoCompleteFlagSet(f, false, "")
tf := tabflags.NewAutoCompleteFlagSet(f, tabflags.Nothing, "")
sf := application.NewSharedFlags(tf)
ret := &Config{
BuildInfo: info,
SharedFlags: sf,
FlagSet: tf,

filePath: tf.String("file", "", "the file to write the pings into. (default data not saved)",
tabflags.AutoComplete{WantsFile: true, FileExt: ".pings"}),
tabflags.AutoComplete{Completion: tabflags.File, FileExt: ".pings"}),
hideHelpOnStart: tf.Bool("hide-help", false, "if this flag is used the help box will be hidden by default"),
pingBufferingLimit: new(int),
pingsPerMinute: tf.Float64("pings-per-minute", 60.0,
Expand All @@ -64,7 +64,7 @@ func GetFlags(info *application.BuildInfo) *Config {
"There's also the builtin themes:\n"+strings.Join(themes.DescribeBuiltins(), "\n")+
"\nSee the docs "+ansi.Blue("https://github.com/Lexer747/acci-ping/blob/main/docs/themes.md")+
" for how to create custom themes.",
tabflags.AutoComplete{Choices: themes.GetBuiltInNames(), WantsFile: true, FileExt: ".json"}),
tabflags.AutoComplete{Choices: themes.GetBuiltInNames(), Completion: tabflags.File, FileExt: ".json"}),
debuggingTermSize: tf.String("debug-term-size", "", "switches the terminal to fixed mode and no iteractivity",
tabflags.AutoComplete{Choices: []string{"15x80", "20x85", "HxW"}}),
followingOnStart: tf.Bool("follow", false, "if this flag is used the graph will be shown in following mode immediately"),
Expand Down
6 changes: 3 additions & 3 deletions cmd/subcommands/drawframe/drawframe.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Use of this source code is governed by a GPL-2 license that can be found in the LICENSE file.
//
// Copyright 2024-2025 Lexer747
// Copyright 2024-2026 Lexer747
//
// SPDX-License-Identifier: GPL-2.0-only

Expand Down Expand Up @@ -43,7 +43,7 @@ type Config struct {

func GetFlags(info *application.BuildInfo) *Config {
f := flag.NewFlagSet("", flag.ContinueOnError)
tf := tabflags.NewAutoCompleteFlagSet(f, true, ".pings")
tf := tabflags.NewAutoCompleteFlagSet(f, tabflags.File|tabflags.Folder, ".pings")
sf := application.NewSharedFlags(tf)
ret := &Config{
BuildInfo: info,
Expand All @@ -60,7 +60,7 @@ func GetFlags(info *application.BuildInfo) *Config {
"There's also the builtin themes:\n"+strings.Join(themes.DescribeBuiltins(), "\n")+
"\nSee the docs "+ansi.Blue("https://github.com/Lexer747/acci-ping/blob/main/docs/themes.md")+
" for how to create custom themes.",
tabflags.AutoComplete{Choices: themes.GetBuiltInNames(), WantsFile: true, FileExt: ".json"}),
tabflags.AutoComplete{Choices: themes.GetBuiltInNames(), Completion: tabflags.File, FileExt: ".json"}),
yAxisScale: tf.Bool("log-scale", false, "switches the y-axis to be in logarithmic scaling instead of linear"),
}
f.Usage = func() {
Expand Down
2 changes: 1 addition & 1 deletion cmd/subcommands/ping/ping.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ type Config struct {

func GetFlags() *Config {
f := flag.NewFlagSet("", flag.ContinueOnError)
tf := tabflags.NewAutoCompleteFlagSet(f, false, "")
tf := tabflags.NewAutoCompleteFlagSet(f, tabflags.Nothing, "")
ret := &Config{
url: tf.String("url", "www.google.com", "the url to target for ping testing", tabflags.AutoComplete{}),
count: tf.Int("n", 4, "the number of packets to send. 0 or smaller means continuous running."),
Expand Down
4 changes: 2 additions & 2 deletions cmd/subcommands/rawdata/rawdata.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Use of this source code is governed by a GPL-2 license that can be found in the LICENSE file.
//
// Copyright 2024-2025 Lexer747
// Copyright 2024-2026 Lexer747
//
// SPDX-License-Identifier: GPL-2.0-only

Expand All @@ -27,7 +27,7 @@ type Config struct {

func GetFlags() *Config {
f := flag.NewFlagSet("", flag.ContinueOnError)
tf := tabflags.NewAutoCompleteFlagSet(f, true, ".pings")
tf := tabflags.NewAutoCompleteFlagSet(f, tabflags.File, ".pings")
ret := &Config{
FlagSet: tf,
printAll: tf.Bool("all", false, "prints all raw values otherwise only summarises '.pings' files"),
Expand Down
4 changes: 2 additions & 2 deletions cmd/subcommands/version/version.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Use of this source code is governed by a GPL-2 license that can be found in the LICENSE file.
//
// Copyright 2025 Lexer747
// Copyright 2025-2026 Lexer747
//
// SPDX-License-Identifier: GPL-2.0-only

Expand All @@ -23,7 +23,7 @@ type Config struct {

func GetFlags(info *application.BuildInfo) *Config {
f := flag.NewFlagSet("", flag.ContinueOnError)
tf := tabflags.NewAutoCompleteFlagSet(f, false, "")
tf := tabflags.NewAutoCompleteFlagSet(f, tabflags.Nothing, "")
ret := &Config{
BuildInfo: info,
FlagSet: tf,
Expand Down
6 changes: 5 additions & 1 deletion cmd/tab_completion/acci-ping
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
_acci-ping()
{
# $COMP_WORDS # an array of all the words typed after the name of the program the compspec belongs to
# $COMP_CWORD # an index of the COMP_WORDS array pointing to the word the current cursor is at - in other words, the index of the word the cursor was when the tab key was pressed
# $COMP_CWORD # an index of the COMP_WORDS array pointing to the word the current cursor is at - in other
# words, the index of the word the cursor was when the tab key was pressed
# $COMP_LINE # the current command line

# These hashes are just the sha256 of `autoCompleteString` and is instead of using a delimiter which is
# more normal, comma, spaces, etc. we use this delimiter which is incredibly unlikely to clash and there by
# reducing the need to escape inputs to zero.
local reply
reply=$(acci-ping b2827fc8fc8c8267cb15f5a925de7e4712aa04ef2fbd43458326b595d66a36d9 "$COMP_CWORD" b2827fc8fc8c8267cb15f5a925de7e4712aa04ef2fbd43458326b595d66a36d9 "${COMP_WORDS[*]}" b2827fc8fc8c8267cb15f5a925de7e4712aa04ef2fbd43458326b595d66a36d9 "$COMP_LINE")
IFS=" " read -r -a COMPREPLY <<< "$reply"
Expand Down
114 changes: 79 additions & 35 deletions cmd/tab_completion/tab_completion.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Use of this source code is governed by a GPL-2 license that can be found in the LICENSE file.
//
// Copyright 2025 Lexer747
// Copyright 2025-2026 Lexer747
//
// SPDX-License-Identifier: GPL-2.0-only

Expand Down Expand Up @@ -45,9 +45,13 @@ func Run(args []string, base Command, subCommands []Command) {
}
// Catch panic's in the case of a mistake in this algorithm simply return no autocomplete suggestions.
defer func() {
if err := recover(); err != nil {
slog.Error("caught panic", "err", err)
tabExitFailure()
if recovered := recover(); recovered != nil {
if err, ok := recovered.(error); ok {
tabExitFailure(errors.Wrap(err, "panicked"))
} else {
slog.Error("caught panic", "recovered", recovered)
tabExitFailure(errors.Errorf("panicked"))
}
}
}()
slog.Debug("auto complete called with", "args", args)
Expand All @@ -67,14 +71,14 @@ func Run(args []string, base Command, subCommands []Command) {

index, err := strconv.Atoi(_COMP_CWORD)
if err != nil {
tabExitFailure()
tabExitFailure(err)
}

slog.Debug("inputs", "index", index, "_COMP_CWORD", _COMP_CWORD, "_COMP_LINE", _COMP_LINE, "_COMP_WORDS", _COMP_WORDS)
choices, err := getChoices(index, _COMP_LINE, base, subCommands)
slog.Debug("results", "choices", choices, "err", err)
if err != nil {
tabExitFailure()
tabExitFailure(err)
return
}
tabExit(choices)
Expand All @@ -100,11 +104,11 @@ func getChoices(index int, _COMP_LINE []string, base Command, cmds []Command) ([
// determine if we have a subcommand selected or are embracing the base and selecting flags
isBase := strings.HasPrefix(_COMP_LINE[1], "-")
var flags *tabflags.FlagSet
var wantsFile bool
var wants tabflags.Wants
var curCommand, fileExt string
if isBase {
flags = base.Fs
wantsFile = base.Fs.WantsFile()
wants = base.Fs.Wants()
curCommand = base.Cmd
fileExt = base.Fs.FileExt()
} else {
Expand All @@ -115,72 +119,111 @@ func getChoices(index int, _COMP_LINE []string, base Command, cmds []Command) ([
}
cmd := choice[0]
flags = cmd.Fs
wantsFile = cmd.Fs.WantsFile()
wants = cmd.Fs.Wants()
curCommand = cmd.Cmd
fileExt = cmd.Fs.FileExt()
}
cur := _COMP_LINE[len(_COMP_LINE)-1]
return suggestionAutoComplete(index, _COMP_LINE, flags, wantsFile, fileExt, cur, curCommand), nil
return suggestionAutoComplete(index, _COMP_LINE, flags, wants, fileExt, cur, curCommand)
}
slog.Error("unable to solve")
return nil, errors.Errorf("unexpected inputs")
}

func suggestionAutoComplete(
index int,
_COMP_LINE []string,
flags *tabflags.FlagSet,
wantsFile bool,
wants tabflags.Wants,
fileExt, cur, curCommand string,
) []string {
) ([]string, error) {
alreadySet := _COMP_LINE[1:index]
var files []string
if wantsFile {
files = getWorkingDirFiles()
if fileExt != "" {
files = sliceutils.Filter(files, func(path string) bool { return filepath.Ext(path) == fileExt })
}
files, err := getLocalOptions(wants, fileExt)
if err != nil {
return nil, err
}

prev := _COMP_LINE[index-1]
ac := flags.GetAutoCompleteFor(prev)
if ac == nil {
includeDebug := strings.HasPrefix(cur, "-d")
names := flags.GetNames(includeDebug, alreadySet)
return filterByPrefix(slices.Concat(names, files), cur)
return filterByPrefix(slices.Concat(names, files), cur), nil
}

// suggest continuation
if prev == curCommand {
return returnFlagSetNames(cur, flags, files, alreadySet)
return returnFlagSetNames(cur, flags, files, alreadySet), nil
}
toSuggest := *ac
if toSuggest.WantsFile {
files = getWorkingDirFiles()
if toSuggest.FileExt != "" {
files = sliceutils.Filter(files, func(path string) bool { return filepath.Ext(path) == toSuggest.FileExt })
}
files, err = getLocalOptions(toSuggest.Completion, toSuggest.FileExt)
if err != nil {
return nil, err
}
if len(toSuggest.Choices) == 0 {
return returnFlagSetNames(cur, flags, files, alreadySet)
return returnFlagSetNames(cur, flags, files, alreadySet), nil
}
return filterByPrefix(slices.Concat(toSuggest.Choices, files), cur)
return filterByPrefix(slices.Concat(toSuggest.Choices, files), cur), nil
}

func getWorkingDirFiles() (files []string) {
// getLocalOptions searches the working dir for the given matching set of possible continuations. The
// continuations depend on the parameters passed, see [tabflags.Wants] and for files, further filtered by the
// file extension.
func getLocalOptions(wants tabflags.Wants, fileExt string) ([]string, error) {
var files []string
if wants.WantsFolderOrFile() {
entries, err := getWorkingDirFiles()
if err != nil {
return files, err
}
var filterMap func(path os.DirEntry) (string, bool)
switch {
case fileExt != "" && wants.IsSet(tabflags.Folder):
// folders are requested so don't filter them out
filterMap = func(path os.DirEntry) (string, bool) {
n := path.Name()
return n, path.IsDir() || filepath.Ext(n) == fileExt
}
case wants.IsSet(tabflags.Folder):
filterMap = func(path os.DirEntry) (string, bool) {
n := path.Name()
return n, path.IsDir()
}
case fileExt != "":
filterMap = func(path os.DirEntry) (string, bool) {
n := path.Name()
return n, filepath.Ext(n) == fileExt
}
default:
filterMap = func(path os.DirEntry) (string, bool) { return path.Name(), true }
}
files = sliceutils.FilterMap(entries, filterMap)
}
return files, nil
}

func getWorkingDirFiles() (entries []os.DirEntry, err error) {
f, err := os.Open("./")
if err != nil {
slog.Error("failed to get files", "err", err)
return entries, errors.Wrapf(err, "failed to open working dir %q", currentDirErrorString())
}
files, err = f.Readdirnames(0)
entries, err = f.ReadDir(0)
if err != nil {
slog.Error("failed to get files", "err", err)
return entries, errors.Wrapf(err, "failed to get entries %q", currentDirErrorString())
}
err = f.Close()
if err != nil {
slog.Error("failed to close dir", "err", err)
return entries, errors.Wrapf(err, "failed to close working dir %q", currentDirErrorString())
}
return entries, nil
}

func currentDirErrorString() string {
dir, err := os.Getwd()
if err == nil {
return dir
} else {
return "unknown dir: err(" + err.Error() + ")"
}
return files
}

func returnFlagSetNames(cur string, flags *tabflags.FlagSet, files, alreadySet []string) []string {
Expand Down Expand Up @@ -253,7 +296,8 @@ func tabExit(results []string) {
fmt.Fprint(os.Stdout, strings.Join(results, " "))
exit.Success()
}
func tabExitFailure() {
func tabExitFailure(err error) {
slog.Error("tabExitFailure", "err", err)
tabExit([]string{})
}

Expand Down
Loading