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 withCompletionOptions
.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:
- it avoids the startup delay issue
- it provides a central registration point for all shells
- it enables embedding with
bridge.ActionCarapaceBin
- it uses the newest version of Carapace for shell integration
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 macrotools.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:
- The creation of the command structure and registration of completions.
- 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 theLS_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.