update sigil

This commit is contained in:
Thomas G. Lopes
2026-03-04 19:48:46 +00:00
parent a788760593
commit 0c40072aa8
3 changed files with 149 additions and 8 deletions
+14 -1
View File
@@ -30,7 +30,12 @@ go run . <command>
## `config.lua` ## `config.lua`
```lua ```lua
return { ---@class SigilConfig
---@field target table<string, string|boolean>
---@field ignore? string[]
---@type SigilConfig
local config = {
target = { target = {
linux = "~/.config/nvim", linux = "~/.config/nvim",
macos = "~/Library/Application Support/nvim", macos = "~/Library/Application Support/nvim",
@@ -38,7 +43,14 @@ return {
windows = false, windows = false,
default = "~/.config/nvim", default = "~/.config/nvim",
}, },
ignore = {
"**/.DS_Store",
"**/*.tmp",
"cache/**",
},
} }
return config
``` ```
## Spec formats ## Spec formats
@@ -53,4 +65,5 @@ return {
- Uses `SIGIL_REPO` env var to override the repo path. - Uses `SIGIL_REPO` env var to override the repo path.
- Conflicts are detected (existing non-symlink files will stop apply). - Conflicts are detected (existing non-symlink files will stop apply).
- `config.ignore` supports gitignore-like globs (`*`, `?`, `**`) relative to each package `files/` directory.
- Prefer `sigil add` over manual edits in `files/`. - Prefer `sigil add` over manual edits in `files/`.
+133 -5
View File
@@ -7,7 +7,9 @@ import (
"io" "io"
"io/fs" "io/fs"
"os" "os"
"path"
"path/filepath" "path/filepath"
"regexp"
"runtime" "runtime"
"strings" "strings"
@@ -22,6 +24,8 @@ const (
type packageConfig struct { type packageConfig struct {
targets map[string]string targets map[string]string
disabled map[string]bool disabled map[string]bool
ignore []string
compiledIgnores []*regexp.Regexp
} }
var errTargetDisabled = errors.New("target disabled for this platform") var errTargetDisabled = errors.New("target disabled for this platform")
@@ -262,7 +266,7 @@ func applyCmd(args []string) error {
return err return err
} }
if err := applyPackage(filesDir, targetRoot); err != nil { if err := applyPackage(filesDir, targetRoot, cfg); err != nil {
return fmt.Errorf("%s: %w", entry.Name(), err) return fmt.Errorf("%s: %w", entry.Name(), err)
} }
@@ -410,7 +414,12 @@ func addCmd(args []string) error {
} }
} }
return applyPackage(filesDir, targetRoot) cfg, err := loadConfig(configPath)
if err != nil {
return err
}
return applyPackage(filesDir, targetRoot, cfg)
} }
func unlinkCmd(args []string) error { func unlinkCmd(args []string) error {
@@ -535,7 +544,7 @@ func removeCmd(args []string) error {
return nil return nil
} }
func applyPackage(filesDir, targetRoot string) error { func applyPackage(filesDir, targetRoot string, cfg *packageConfig) error {
if err := ensureDir(targetRoot); err != nil { if err := ensureDir(targetRoot); err != nil {
return err return err
} }
@@ -552,6 +561,14 @@ func applyPackage(filesDir, targetRoot string) error {
if err != nil { if err != nil {
return err return err
} }
if shouldIgnorePath(rel, cfg) {
if entry.IsDir() {
return filepath.SkipDir
}
return nil
}
targetPath := filepath.Join(targetRoot, rel) targetPath := filepath.Join(targetRoot, rel)
if entry.IsDir() { if entry.IsDir() {
@@ -653,7 +670,16 @@ func loadConfig(path string) (*packageConfig, error) {
return nil, errors.New("config.target is empty") return nil, errors.New("config.target is empty")
} }
return &packageConfig{targets: targets, disabled: disabled}, nil ignore, err := parseIgnore(tbl)
if err != nil {
return nil, err
}
compiledIgnores, err := compileIgnorePatterns(ignore)
if err != nil {
return nil, err
}
return &packageConfig{targets: targets, disabled: disabled, ignore: ignore, compiledIgnores: compiledIgnores}, nil
} }
func statusCmd() error { func statusCmd() error {
@@ -723,6 +749,108 @@ func selectTarget(cfg *packageConfig) (string, error) {
return "", fmt.Errorf("missing target for %s and default", osKey) return "", fmt.Errorf("missing target for %s and default", osKey)
} }
func parseIgnore(cfgTbl *lua.LTable) ([]string, error) {
ignoreVal := cfgTbl.RawGetString("ignore")
if ignoreVal == lua.LNil {
return nil, nil
}
ignoreTbl, ok := ignoreVal.(*lua.LTable)
if !ok {
return nil, errors.New("config.ignore must be an array of strings")
}
ignore := make([]string, 0, ignoreTbl.Len())
var parseErr error
ignoreTbl.ForEach(func(k, v lua.LValue) {
if parseErr != nil {
return
}
if _, ok := k.(lua.LNumber); !ok {
parseErr = errors.New("config.ignore must be an array of strings")
return
}
s, ok := v.(lua.LString)
if !ok {
parseErr = errors.New("config.ignore must contain only strings")
return
}
pattern := strings.TrimSpace(string(s))
if pattern == "" {
parseErr = errors.New("config.ignore cannot contain empty patterns")
return
}
ignore = append(ignore, pattern)
})
if parseErr != nil {
return nil, parseErr
}
return ignore, nil
}
func compileIgnorePatterns(patterns []string) ([]*regexp.Regexp, error) {
compiled := make([]*regexp.Regexp, 0, len(patterns))
for _, pattern := range patterns {
re, err := globToRegexp(pattern)
if err != nil {
return nil, fmt.Errorf("invalid ignore pattern %q: %w", pattern, err)
}
compiled = append(compiled, re)
}
return compiled, nil
}
func globToRegexp(pattern string) (*regexp.Regexp, error) {
pattern = strings.ReplaceAll(pattern, "\\", "/")
pattern = strings.TrimPrefix(pattern, "./")
if strings.HasPrefix(pattern, "/") {
pattern = strings.TrimPrefix(pattern, "/")
}
var b strings.Builder
b.WriteString("^")
for i := 0; i < len(pattern); {
if i+1 < len(pattern) && pattern[i] == '*' && pattern[i+1] == '*' {
b.WriteString(".*")
i += 2
continue
}
ch := pattern[i]
switch ch {
case '*':
b.WriteString("[^/]*")
case '?':
b.WriteString("[^/]")
default:
b.WriteString(regexp.QuoteMeta(string(ch)))
}
i++
}
b.WriteString("$")
return regexp.Compile(b.String())
}
func shouldIgnorePath(rel string, cfg *packageConfig) bool {
if cfg == nil || len(cfg.compiledIgnores) == 0 {
return false
}
normalized := path.Clean(filepath.ToSlash(rel))
for _, re := range cfg.compiledIgnores {
if re.MatchString(normalized) {
return true
}
}
return false
}
func repoPath() (string, error) { func repoPath() (string, error) {
if override := os.Getenv("SIGIL_REPO"); override != "" { if override := os.Getenv("SIGIL_REPO"); override != "" {
return filepath.Abs(expandHome(override)) return filepath.Abs(expandHome(override))
@@ -772,7 +900,7 @@ func writeConfig(path, targetRoot string) error {
} }
prettyTarget := compressHome(targetRoot) prettyTarget := compressHome(targetRoot)
content := fmt.Sprintf("return {\n\ttarget = {\n\t\t%s = %q,\n\t\tdefault = %q,\n\t},\n}\n", osKey, prettyTarget, prettyTarget) content := fmt.Sprintf("---@class SigilConfig\n---@field target table<string, string|boolean>\n---@field ignore? string[]\n\n---@type SigilConfig\nlocal config = {\n\ttarget = {\n\t\t%s = %q,\n\t\tdefault = %q,\n\t},\n\tignore = {\n\t\t-- \"**/.DS_Store\",\n\t\t-- \"**/*.tmp\",\n\t\t-- \"cache/**\",\n\t},\n}\n\nreturn config\n", osKey, prettyTarget, prettyTarget)
return os.WriteFile(path, []byte(content), 0o644) return os.WriteFile(path, []byte(content), 0o644)
} }
Executable
BIN
View File
Binary file not shown.