Freckles

Freckles is a simple dotfile manager using the symlink approach.

Carapace

Freckles is based on Carapace and serves as an example application.

The following documentation thus covers implementation details and highlights how Carapace provides completion.

Root

Carapace uses Cobra to define (sub)commands and flags:

var rootCmd = &cobra.Command{
	Use:   "freckles",
	Short: "A simple dotfile manager.",
	Run:   func(cmd *cobra.Command, args []string) {},
	CompletionOptions: cobra.CompletionOptions{
		DisableDefaultCmd: true,
	},
}

Cobra has its own completion logic and adds a completion subcommand by default. This can be prevented with CompletionOptions.

Historically, Cobra had no dynamic completion, and development in this regard was quite stale. Without the burden of backward compatibility and the massive amount of dependents, Carapace could develop much more quickly and push the boundary of what's possible.

Here's the cornerstone that ultimately became Carapace.

Gen

Gen adds a hidden _carapace subcommand which handles script generation, completions, and macros.

	carapace.Gen(rootCmd)

Calling Gen with the root command is enough to add completion for commands and flags. But be aware that in Carapace argument completion is explicit. So an undefined completion does not cause an implicit file completion fallback.

Script

Completion scripts are generated with freckles _carapace [shell]. Most of these can be directly sourced.

Carapace has a basic shell detection mechanism, so in most cases [shell] is optional.

Export

Shell scripts in Carapace are just thin layers to integrate with the corresponding shell.

Aside from shell-specific output:

freckles _carapace [shell] freckles [ARGS]...

Export provides a more generic json representation of completions:

freckles _carapace export freckles [ARGS]...

Carapace (binary) essentially acts as a central registry for all of your completions.

With #1336 packages will be able to register completions system-wide using Specs:

# yaml-language-server: $schema=https://carapace.sh/schemas/command.json
name: freckles
parsing: disabled
completion:
  positionalany: ["$carapace.bridge.Carapace([freckles])"]

This has several benefits:

Macro

Actions with the signature of MacroI, MacroN, or MacroV can be exposed as a custom macro for others to consume.

	spec.AddMacro("freckles", spec.MacroN(action.ActionFreckles))
	spec.Register(rootCmd)

Macros provide a way to loosely share Actions between applications.

More on this at Init#Clone and Edit#Action.

Init

Creates a new Git repository at ~/.local/share/freckles.

var initCmd = &cobra.Command{
	Use:   "init",
	Short: "init freckles folder",
	RunE: func(cmd *cobra.Command, args []string) error {
		c := exec.Command("git", "init", freckles.Dir())
		if cmd.Flag("clone").Changed {
			c = exec.Command("git", "clone", cmd.Flag("clone").Value.String(), freckles.Dir())
		}
		c.Stdin = os.Stdin
		c.Stdout = os.Stdout
		c.Stderr = os.Stderr
		if err := c.Run(); err != nil {
			return err
		}

		if _, err := os.Stat(freckles.Dir() + ".frecklesignore"); os.IsNotExist(err) {
			return os.WriteFile(freckles.Dir()+".frecklesignore", []byte(".git\n.frecklesignore\n"), os.ModePerm)
		}
		return nil
	},
}

Clone

Alternatively an existing remote repository can be cloned with the --clone flag.

Completion is provided by ActionRepositorySearch.

	carapace.Gen(initCmd).FlagCompletion(carapace.ActionMap{
		"clone": spec.ActionMacro("$carapace.tools.git.RepositorySearch"),
	})

Here, carapace is invoked with the macro tools.git.RepositorySearch and the current value:

carapace _carapace macro tools.git.RepositorySearch https://github.com/rsteube/do

Which then returns the completion in the Export format.

{
  "version": "v1.8.0",
  "messages": [],
  "nospace": "/",
  "usage": "",
  "values": [
    {
      "value": "https://github.com/rsteube/docker-mdbook",
      "display": "docker-mdbook",
      "description": "mdbook mermaid "
    },
    {
      "value": "https://github.com/rsteube/docker-mdbook-dtmo",
      "display": "docker-mdbook-dtmo",
      "description": "mdbook mdbook-mermaid mdbook-toc"
    },
    {
      "value": "https://github.com/rsteube/dotfiles",
      "display": "dotfiles",
      "style": "red"
    }
  ]
}

Note that for performance reasons only the first 100 search results are presented. Fast response times are important which limits what can be done in Carapace.

Add

Copies a dotfile to the repository and replaces it with a symlink.

var addCmd = &cobra.Command{
	Use:   "add [FILE]...",
	Short: "add dotfiles",
	Args:  cobra.MinimumNArgs(1),
	Run: func(cmd *cobra.Command, args []string) {
		for _, arg := range args {
			freckle := freckles.Freckle{Path: arg}
			if err := freckle.Add(false); err != nil {
				println(err.Error())
			}
		}
	},
}

Completion is provided with ActionFiles and a couple of tricks.

	carapace.Gen(addCmd).PositionalAnyCompletion(
		carapace.ActionCallback(func(c carapace.Context) carapace.Action {
			batch := carapace.Batch(
				carapace.ActionFiles(),
			)
			if c.Value == "" {
				batch = append(batch, carapace.ActionCallback(func(c carapace.Context) carapace.Action {
					c.Value = "."
					return carapace.ActionFiles().Invoke(c).ToA()
				}))
			}
			return batch.ToA().ChdirF(traverse.UserHomeDir)
		}),
	)
}

First of all, dotfiles are located in your home folder. For convenience, the workdir in Context is changed using ChdirF and traverse.UserHomeDir.


Then dotfiles are usually hidden. But ActionFiles only shows them when the . prefix is already present. By altering the value in Context and explicitly invoking ActionFiles with it we can force this behaviour.


Batch not only enables concurrent invocation of Actions. It also provides a neat way to add them conditionally. See ActionRefs for a complex example.

Edit

Opens a dotfile in your editor.

var editCmd = &cobra.Command{
	Use:   "edit [FILE]",
	Short: "edit a dotfile",
	Args:  cobra.ExactArgs(1),
	Run: func(cmd *cobra.Command, args []string) {
		c := exec.Command(editor(), freckles.Dir()+"/"+args[0])
		c.Stdin = os.Stdin
		c.Stdout = os.Stdout
		c.Stderr = os.Stderr
		c.Run()
	},
}

Completion is provided with a custom action.

	carapace.Gen(editCmd).PositionalCompletion(
		action.ActionFreckles(),
	)

Action

Custom Actions are simply functions returning Action.

package action

import (
	"path/filepath"

	"github.com/carapace-sh/carapace"
	"github.com/carapace-sh/carapace/pkg/style"
	"github.com/carapace-sh/freckles/pkg/freckles"
)

func ActionFreckles() carapace.Action {
	return carapace.ActionCallback(func(c carapace.Context) carapace.Action {
		vals := make([]string, 0)
		freckles.Walk(func(freckle freckles.Freckle) error {
			vals = append(vals, freckle.Path)
			return nil
		})
		return carapace.ActionValues(vals...).MultiParts("/").StyleF(func(s string, sc style.Context) string {
			return style.ForPath(filepath.Join(freckles.Dir(), s), sc)
		})
	})
}

There are two main phases in Carapace:

  1. The creation of the command structure and registration of completions.
  2. The parsing of the command line and invocation of the corresponding Action.
carapace.Gen(initCmd).PositionalCompletion(
	carapace.ActionValues(initCmd.Flag("clone").Value.String()), // (1) completes the default value
	carapace.ActionCallback(func(c carapace.Context) carapace.Action {
		return carapace.ActionValues(initCmd.Flag("clone").Value.String()) // (2) completes the parsed value
	}),
)

Custom Actions should almost always be wrapped in ActionCallback so code is only executed when invoked. The Default Actions do this implicitly.


The Walk function returns dotfiles with their full path within the repository: path/to/freckle. By modifying the Action with MultiParts the segments get completed separately.


Additionally, style.ForPath highlights them with the style defined by the LS_COLORS environment variable.

Git

Freckles embeds Git as subcommand to manage the repository.

By passing -C <dir> it acts as if called from within the repository at ~/.local/share/freckles. And with DisableFlagParsing: true every argument is seen as positional and passed along.

var gitCmd = &cobra.Command{
	Use:                "git",
	Short:              "invoke git on freckles directory",
	DisableFlagParsing: true,
	Run: func(cmd *cobra.Command, args []string) {
		c := exec.Command("git", append([]string{"-C", freckles.Dir()}, args...)...)
		c.Stdin = os.Stdin
		c.Stdout = os.Stdout
		c.Stderr = os.Stderr
		c.Run()
	},
}

Completion is provided with ActionCarapaceBin.

	carapace.Gen(gitCmd).PositionalAnyCompletion(
		carapace.ActionCallback(func(c carapace.Context) carapace.Action {
			return bridge.ActionCarapaceBin("git").Chdir(freckles.Dir())
		}),
	)

List

Lists dotfiles in the repository.

Link

Creates symlinks for dotfiles in the repository.

Verify

Verifies the symlink status.