mirror of
https://github.com/skidoodle/ctx.git
synced 2026-04-28 03:07:41 +02:00
init ctx
This commit is contained in:
@@ -0,0 +1,32 @@
|
|||||||
|
name: goreleaser
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
goreleaser:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: stable
|
||||||
|
|
||||||
|
- name: Run GoReleaser
|
||||||
|
uses: goreleaser/goreleaser-action@v6
|
||||||
|
with:
|
||||||
|
distribution: goreleaser
|
||||||
|
version: latest
|
||||||
|
args: release --clean
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ctx*
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
version: 2
|
||||||
|
|
||||||
|
project_name: ctx
|
||||||
|
|
||||||
|
builds:
|
||||||
|
- env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
goos:
|
||||||
|
- linux
|
||||||
|
- windows
|
||||||
|
- darwin
|
||||||
|
goarch:
|
||||||
|
- amd64
|
||||||
|
- arm64
|
||||||
|
ldflags:
|
||||||
|
- -s -w
|
||||||
|
|
||||||
|
archives:
|
||||||
|
- format: tar.gz
|
||||||
|
format_overrides:
|
||||||
|
- goos: windows
|
||||||
|
format: zip
|
||||||
|
name_template: >-
|
||||||
|
{{ .ProjectName }}_
|
||||||
|
{{- .Version }}_
|
||||||
|
{{- .Os }}_
|
||||||
|
{{- .Arch }}
|
||||||
|
|
||||||
|
checksum:
|
||||||
|
name_template: "checksums.txt"
|
||||||
|
|
||||||
|
snapshot:
|
||||||
|
version_template: "{{ .Tag }}-next"
|
||||||
|
|
||||||
|
changelog:
|
||||||
|
sort: asc
|
||||||
|
filters:
|
||||||
|
exclude:
|
||||||
|
- "^docs:"
|
||||||
|
- "^test:"
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 github.com/skidoodle
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# ctx
|
||||||
|
|
||||||
|
CLI to convert a directory tree and file contents into a single text file for LLM context.
|
||||||
|
It respects `.gitignore` and comes with sensible defaults for ignoring binaries and lockfiles.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Go Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go install github.com/skidoodle/ctx@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Binaries
|
||||||
|
|
||||||
|
Download pre-compiled binaries for Windows, macOS, and Linux from the [Releases](https://github.com/skidoodle/ctx/releases) page.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Generate context for the current directory (outputs to `ctx.txt`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ctx .
|
||||||
|
```
|
||||||
|
|
||||||
|
Generate context for a specific folder and save to a custom file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ctx -o context.md ./src
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
`ctx` ignores common artifacts (node_modules, .git, binaries) by default.
|
||||||
|
To edit the global ignore list:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ctx -config
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
MIT
|
||||||
|
```
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
configDirName = "ctx"
|
||||||
|
ignoreFileName = "ignore"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultIgnoreText = `
|
||||||
|
.git
|
||||||
|
.svn
|
||||||
|
.hg
|
||||||
|
.gitignore
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
.vs
|
||||||
|
.settings
|
||||||
|
.classpath
|
||||||
|
.project
|
||||||
|
.DS_Store
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*.tmp
|
||||||
|
*.bak
|
||||||
|
node_modules
|
||||||
|
bower_components
|
||||||
|
jspm_packages
|
||||||
|
vendor
|
||||||
|
.venv
|
||||||
|
venv
|
||||||
|
env
|
||||||
|
.env
|
||||||
|
__pycache__
|
||||||
|
.tox
|
||||||
|
.mypy_cache
|
||||||
|
.pytest_cache
|
||||||
|
.npm
|
||||||
|
.yarn
|
||||||
|
.pnpm-store
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
out
|
||||||
|
target
|
||||||
|
bin
|
||||||
|
obj
|
||||||
|
cmake-build-*
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
pnpm-lock.yaml
|
||||||
|
bun.lockb
|
||||||
|
go.sum
|
||||||
|
Cargo.lock
|
||||||
|
poetry.lock
|
||||||
|
Pipfile.lock
|
||||||
|
*.exe
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
*.test
|
||||||
|
*.class
|
||||||
|
*.jar
|
||||||
|
*.war
|
||||||
|
*.ear
|
||||||
|
*.o
|
||||||
|
*.obj
|
||||||
|
*.sqlite
|
||||||
|
*.db
|
||||||
|
*.sqlitedb
|
||||||
|
*.zip
|
||||||
|
*.tar
|
||||||
|
*.tar.gz
|
||||||
|
*.tgz
|
||||||
|
*.rar
|
||||||
|
*.7z
|
||||||
|
*.jpg
|
||||||
|
*.jpeg
|
||||||
|
*.png
|
||||||
|
*.gif
|
||||||
|
*.ico
|
||||||
|
*.svg
|
||||||
|
*.webp
|
||||||
|
*.mp3
|
||||||
|
*.mp4
|
||||||
|
*.mov
|
||||||
|
*.avi
|
||||||
|
*.pdf
|
||||||
|
*.doc
|
||||||
|
*.docx
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
*.egg-info
|
||||||
|
*_templ.go
|
||||||
|
`
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
ExactIgnores map[string]struct{}
|
||||||
|
ExtIgnores map[string]struct{}
|
||||||
|
ComplexGlobs []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseConfig(text string) *Config {
|
||||||
|
cfg := &Config{
|
||||||
|
ExactIgnores: make(map[string]struct{}),
|
||||||
|
ExtIgnores: make(map[string]struct{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(strings.NewReader(text))
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(line, "*.") && strings.Count(line, "*") == 1 && !strings.Contains(line, "/") {
|
||||||
|
ext := strings.TrimPrefix(line, "*")
|
||||||
|
cfg.ExtIgnores[ext] = struct{}{}
|
||||||
|
} else if !strings.ContainsAny(line, "*?[]") && !strings.Contains(line, "/") {
|
||||||
|
cfg.ExactIgnores[line] = struct{}{}
|
||||||
|
} else {
|
||||||
|
cfg.ComplexGlobs = append(cfg.ComplexGlobs, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) IsIgnored(name, relPath string, isDir bool) bool {
|
||||||
|
if _, ok := c.ExactIgnores[name]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isDir {
|
||||||
|
ext := filepath.Ext(name)
|
||||||
|
if _, ok := c.ExtIgnores[ext]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkPath := filepath.ToSlash(relPath)
|
||||||
|
|
||||||
|
for _, p := range c.ComplexGlobs {
|
||||||
|
cleanPattern := strings.TrimSuffix(p, "/")
|
||||||
|
|
||||||
|
if matched, _ := filepath.Match(cleanPattern, name); matched {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if matched, _ := filepath.Match(cleanPattern, checkPath); matched {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(checkPath, cleanPattern+"/") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func getConfigPath() (string, error) {
|
||||||
|
configDir, err := os.UserConfigDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
appDir := filepath.Join(configDir, configDirName)
|
||||||
|
if err := os.MkdirAll(appDir, 0755); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return filepath.Join(appDir, ignoreFileName), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadConfig() (*Config, error) {
|
||||||
|
path, err := getConfigPath()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
if err := os.WriteFile(path, []byte(defaultIgnoreText), 0644); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return parseConfig(defaultIgnoreText), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return parseConfig(string(data)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func openConfigFile() error {
|
||||||
|
path, err := getConfigPath()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
_, _ = loadConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "windows":
|
||||||
|
cmd = exec.Command("cmd", "/c", "start", "", path)
|
||||||
|
case "darwin":
|
||||||
|
cmd = exec.Command("open", path)
|
||||||
|
default:
|
||||||
|
cmd = exec.Command("xdg-open", path)
|
||||||
|
}
|
||||||
|
return cmd.Start()
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultOutputFilename = "ctx.txt"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := run(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "ctx: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run() error {
|
||||||
|
flag.Usage = usage
|
||||||
|
configFlag := flag.Bool("config", false, "open global ignore file for editing")
|
||||||
|
outputFlag := flag.String("o", defaultOutputFilename, "output filename")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *configFlag {
|
||||||
|
return openConfigFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
args := flag.Args()
|
||||||
|
if len(args) == 0 {
|
||||||
|
usage()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
targetDir := args[0]
|
||||||
|
absPath, err := filepath.Abs(targetDir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("resolving path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
cfg, err := loadConfig()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "ctx: warning: could not load config, using defaults (%v)\n", err)
|
||||||
|
cfg = parseConfig(defaultIgnoreText)
|
||||||
|
}
|
||||||
|
|
||||||
|
scanner := NewScanner(absPath, cfg)
|
||||||
|
files, err := scanner.Scan()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("scanning directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(files) == 0 {
|
||||||
|
return fmt.Errorf("no files found in %s", absPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenCount, err := writeOutput(absPath, files, *outputFlag)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("writing output: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
duration := time.Since(start).Round(time.Millisecond)
|
||||||
|
fmt.Printf("ctx: generated %s (%d files, ~%d tokens) in %v\n",
|
||||||
|
*outputFlag, len(files), tokenCount, duration)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func usage() {
|
||||||
|
fmt.Fprintf(os.Stderr, "Usage: ctx [options] <directory>\n\n")
|
||||||
|
fmt.Fprintf(os.Stderr, "Options:\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " -config Open global ignore file for editing\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " -o <file> Output filename (default %q)\n", defaultOutputFilename)
|
||||||
|
fmt.Fprintf(os.Stderr, "\nExamples:\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " ctx . # Generate context for current dir\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " ctx -o out.txt src/ # Scan src/ and save to out.txt\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " ctx -config # Open global ignore file\n")
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func writeOutput(root string, files []string, outputPath string) (count int64, err error) {
|
||||||
|
f, err := os.Create(outputPath)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if cerr := f.Close(); cerr != nil && err == nil {
|
||||||
|
err = cerr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
bw := bufio.NewWriterSize(f, 1024*1024)
|
||||||
|
defer func() {
|
||||||
|
if ferr := bw.Flush(); ferr != nil && err == nil {
|
||||||
|
err = ferr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
tc := &TokenCounter{w: bw}
|
||||||
|
|
||||||
|
tc.Printf("Project Path: %s\n\n", filepath.Base(root))
|
||||||
|
tc.Println("Source Tree:")
|
||||||
|
tc.Println("")
|
||||||
|
|
||||||
|
tc.Println("```txt")
|
||||||
|
tc.Println(filepath.Base(root))
|
||||||
|
|
||||||
|
if err := writeTree(tc, files); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tc.Println("```")
|
||||||
|
tc.Println("")
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
if file == outputPath || filepath.Base(file) == outputPath {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fullPath := filepath.Join(root, file)
|
||||||
|
content, err := os.ReadFile(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
tc.Printf("Error reading %s: %v\n", file, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ext := strings.TrimPrefix(filepath.Ext(file), ".")
|
||||||
|
if ext == "" {
|
||||||
|
ext = "txt"
|
||||||
|
}
|
||||||
|
|
||||||
|
tc.Printf("`%s`:\n\n", file)
|
||||||
|
tc.Printf("```%s\n", ext)
|
||||||
|
|
||||||
|
if _, err := tc.Write(content); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(content) > 0 && content[len(content)-1] != '\n' {
|
||||||
|
if err := tc.WriteByte('\n'); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tc.Println("```")
|
||||||
|
tc.Println("")
|
||||||
|
}
|
||||||
|
|
||||||
|
return tc.Count, tc.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeTree(w io.Writer, files []string) error {
|
||||||
|
root := make(map[string]any)
|
||||||
|
for _, f := range files {
|
||||||
|
parts := strings.Split(filepath.ToSlash(f), "/")
|
||||||
|
current := root
|
||||||
|
for _, part := range parts {
|
||||||
|
if _, exists := current[part]; !exists {
|
||||||
|
current[part] = make(map[string]any)
|
||||||
|
}
|
||||||
|
current = current[part].(map[string]any)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return printNode(w, root, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func printNode(w io.Writer, node map[string]any, prefix string) error {
|
||||||
|
keys := make([]string, 0, len(node))
|
||||||
|
for k := range node {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
slices.Sort(keys)
|
||||||
|
|
||||||
|
for i, key := range keys {
|
||||||
|
isLast := i == len(keys)-1
|
||||||
|
connector := "├── "
|
||||||
|
if isLast {
|
||||||
|
connector = "└── "
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := fmt.Fprintf(w, "%s%s%s\n", prefix, connector, key); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
children := node[key].(map[string]any)
|
||||||
|
if len(children) > 0 {
|
||||||
|
childPrefix := prefix + "│ "
|
||||||
|
if isLast {
|
||||||
|
childPrefix = prefix + " "
|
||||||
|
}
|
||||||
|
if err := printNode(w, children, childPrefix); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+76
@@ -0,0 +1,76 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Scanner struct {
|
||||||
|
Root string
|
||||||
|
GlobalCfg *Config
|
||||||
|
LocalCfg *Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewScanner(root string, globalCfg *Config) *Scanner {
|
||||||
|
s := &Scanner{
|
||||||
|
Root: root,
|
||||||
|
GlobalCfg: globalCfg,
|
||||||
|
}
|
||||||
|
|
||||||
|
gitIgnorePath := filepath.Join(root, ".gitignore")
|
||||||
|
if data, err := os.ReadFile(gitIgnorePath); err == nil {
|
||||||
|
s.LocalCfg = parseConfig(string(data))
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Scanner) Scan() ([]string, error) {
|
||||||
|
var files []string
|
||||||
|
|
||||||
|
err := filepath.WalkDir(s.Root, func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
relPath, err := filepath.Rel(s.Root, path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if relPath == "." {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
name := d.Name()
|
||||||
|
isDir := d.IsDir()
|
||||||
|
|
||||||
|
if isDir && name == ".git" {
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.GlobalCfg.IsIgnored(name, relPath, isDir) {
|
||||||
|
if isDir {
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.LocalCfg != nil && s.LocalCfg.IsIgnored(name, relPath, isDir) {
|
||||||
|
if isDir {
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isDir {
|
||||||
|
files = append(files, relPath)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
slices.Sort(files)
|
||||||
|
|
||||||
|
return files, err
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TokenCounter struct {
|
||||||
|
w io.Writer
|
||||||
|
Count int64
|
||||||
|
Err error
|
||||||
|
inWord bool
|
||||||
|
inSpace bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *TokenCounter) Write(p []byte) (int, error) {
|
||||||
|
if tc.Err != nil {
|
||||||
|
return 0, tc.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, b := range p {
|
||||||
|
r := rune(b)
|
||||||
|
isSpace := unicode.IsSpace(r)
|
||||||
|
isAlpha := unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_'
|
||||||
|
|
||||||
|
if isAlpha {
|
||||||
|
if !tc.inWord {
|
||||||
|
tc.Count++
|
||||||
|
tc.inWord = true
|
||||||
|
tc.inSpace = false
|
||||||
|
}
|
||||||
|
} else if isSpace {
|
||||||
|
if !tc.inSpace {
|
||||||
|
tc.Count++
|
||||||
|
tc.inSpace = true
|
||||||
|
tc.inWord = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tc.Count++
|
||||||
|
tc.inWord = false
|
||||||
|
tc.inSpace = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := tc.w.Write(p)
|
||||||
|
tc.Err = err
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *TokenCounter) WriteByte(c byte) error {
|
||||||
|
_, err := tc.Write([]byte{c})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *TokenCounter) Printf(format string, a ...any) {
|
||||||
|
if tc.Err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, _ = fmt.Fprintf(tc, format, a...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *TokenCounter) Println(a ...any) {
|
||||||
|
if tc.Err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, _ = fmt.Fprintln(tc, a...)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user