8 Commits

Author SHA1 Message Date
021ea61c9b remove binary 2025-05-16 10:25:29 +02:00
7f93ae2c20 blob detection 2025-05-16 10:21:28 +02:00
6e63af80a6 better suffix check 2025-05-07 14:30:59 +02:00
8eebdc0d38 ignore ttf 2025-05-07 11:30:01 +02:00
454554f8e0 add typst export 2025-05-07 11:00:32 +02:00
c0fe9a4a46 fix heading problem 2025-05-02 13:21:27 +02:00
214bf9acf5 added markdown support 2025-05-02 13:18:47 +02:00
158e963c7b version2 (#1)
Co-authored-by: u80864958 <niklas.breitenstein@bit.admin.ch>
Reviewed-on: #1
2025-04-02 17:29:26 +02:00
15 changed files with 501 additions and 143 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
**/__debug_bin*
main
/pat

5
.vscode/launch.json vendored
View File

@ -4,6 +4,7 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
@ -11,8 +12,8 @@
"type": "go", "type": "go",
"request": "launch", "request": "launch",
"mode": "auto", "mode": "auto",
"program": "/home/schreifuchs/go/src/git.schreifuchs.ch/schreifuchs/pat", "program": "${workspaceFolder}/cmd/main.go",
"args": ["logger"] "args": [ "."]
} }
] ]
} }

View File

@ -1,18 +1,22 @@
# PAT # PAT
## What it Does ## What it Does
`pat` is a command-line tool for concatenating and displaying the contents of files and directories. It: `pat` is a command-line tool for concatenating and displaying the contents of files and directories. It:
1. Processes the specified files and directories recursively. 1. Processes the specified files and directories recursively.
2. Appends content to a structured output with file paths and a delimiter for clarity. 2. Appends content to a structured output with file paths and a delimiter for clarity.
3. Copies the resulting output to the system clipboard for convenient sharing or reuse. 3. Copies the resulting output to the system clipboard for convenient sharing or reuse.
Example use case: Example use case:
- Aggregate and view the contents of multiple files or directories in one command. - Aggregate and view the contents of multiple files or directories in one command.
- Automatically copy the aggregated result to the clipboard for seamless integration with other tools or platforms. - Automatically copy the aggregated result to the clipboard for seamless integration with other tools or platforms.
- Export your codebase to a markdown document.
- Export your codebase to a typst document.
## Dependencies ## Dependencies
1. **Golang** (only to install / build): 1. **Golang** (only to install / build):
- The application requires the Go programming language (`>= 1.18`) to compile and run. - The application requires the Go programming language (`>= 1.18`) to compile and run.
- Dependency: `golang.design/x/clipboard` for clipboard interaction. - Dependency: `golang.design/x/clipboard` for clipboard interaction.
@ -21,31 +25,43 @@ Example use case:
## Installation ## Installation
Install go-cat directly using go install: Install pat:
``` sh ```sh
go install git.schreifuchs.ch/schreifuchs/pat@latest git clone --depth 1 https://git.schreifuchs.ch/schreifuchs/pat.git
cd pat
go build -o=pat cmd/main.go
mv pat $GOPATH/bin/
``` ```
In one go:
```sh
git clone --depth 1 https://git.schreifuchs.ch/schreifuchs/pat.git && cd pat && go build -o=pat cmd/main.go && mv pat $GOPATH/bin/
```
The binary will be placed in your $GOPATH/bin directory. Ensure $GOPATH/bin is in your system's PATH to run it directly. The binary will be placed in your $GOPATH/bin directory. Ensure $GOPATH/bin is in your system's PATH to run it directly.
## Example Usage ## Example Usage
Concatenate files and directories:
### Concatenate files and directories:
```bash ```bash
./pat file1.txt folder/ pat file1.txt folder/
``` ```
Output is printed to the terminal and copied to the clipboard, allowing you to paste it elsewhere. Output is printed to the terminal and copied to the clipboard, allowing you to paste it elsewhere.
### Create printed
```sh
pat -t -i .gitignore . | typst compile /dev/stdin pat.pdf
```
--- ---
#### Notes #### Notes
- The tool uses `---------------------------------------------------------------------------` as a delimiter to separate file contents for readability. - The tool uses `---------------------------------------------------------------------------` as a delimiter to separate file contents for readability.
- If clipboard functionality fails (e.g., unsupported environment), the application will still display the result in the terminal. - If clipboard functionality fails (e.g., unsupported environment), the application will still display the result in the terminal.

57
cmd/main.go Normal file
View File

@ -0,0 +1,57 @@
package main
import (
"flag"
"fmt"
"os"
"git.schreifuchs.ch/schreifuchs/pat/pkg/cat"
"git.schreifuchs.ch/schreifuchs/pat/pkg/clip"
"git.schreifuchs.ch/schreifuchs/pat/pkg/ignore"
)
const DELEMITTER = "-(%s)--------------------------------------------------------------------------\n"
func main() {
ignorePath := flag.String("i", "", "set path to gitignore, if no gitignore parent dirs will be searched")
hiddenFiles := flag.Bool("h", false, "show hidden files")
delemitter := flag.String("d", DELEMITTER, "delemitter to use to split files when not in markdown mode must contain %s for filename")
markdown := flag.Bool("m", false, "markdown mode, outputs files in markdown")
typst := flag.Bool("t", false, "typst mode, outputs files in typst")
flag.Parse()
cats, err := cat.Path(flag.Args()...)
if err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
if *ignorePath != "" {
i, err := ignore.FindGitignore(*ignorePath)
if err != nil {
fmt.Printf("can't get gitignore: %v", err)
os.Exit(1)
}
cats = cats.Ignored(i)
}
if !*hiddenFiles {
cats = cats.Ignored(ignore.Filesystem{})
}
var out string
if *markdown {
out = cats.ToMarkdown()
} else if *typst {
out = cats.ToTypst()
} else {
out = cats.ToString(*delemitter)
}
fmt.Print(out)
if err = clip.Copy(out); err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
}

13
go.mod
View File

@ -2,11 +2,16 @@ module git.schreifuchs.ch/schreifuchs/pat
go 1.23.4 go 1.23.4
require golang.design/x/clipboard v0.7.0 require (
github.com/denormal/go-gitignore v0.0.0-20180930084346-ae8ad1d07817
golang.design/x/clipboard v0.7.0
)
require ( require (
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 // indirect
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect
golang.org/x/image v0.6.0 // indirect golang.org/x/exp/shiny v0.0.0-20250305212735-054e65f0b394 // indirect
golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c // indirect golang.org/x/image v0.25.0 // indirect
golang.org/x/sys v0.5.0 // indirect golang.org/x/mobile v0.0.0-20250305212854-3a7bc9f8a4de // indirect
golang.org/x/sys v0.31.0 // indirect
) )

12
go.sum
View File

@ -1,4 +1,8 @@
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ=
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
github.com/denormal/go-gitignore v0.0.0-20180930084346-ae8ad1d07817 h1:0nsrg//Dc7xC74H/TZ5sYR8uk4UQRNjsw8zejqH5a4Q=
github.com/denormal/go-gitignore v0.0.0-20180930084346-ae8ad1d07817/go.mod h1:C/+sI4IFnEpCn6VQ3GIPEp+FrQnQw+YQP3+n+GdGq7o=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.design/x/clipboard v0.7.0 h1:4Je8M/ys9AJumVnl8m+rZnIvstSnYj1fvzqYrU3TXvo= golang.design/x/clipboard v0.7.0 h1:4Je8M/ys9AJumVnl8m+rZnIvstSnYj1fvzqYrU3TXvo=
golang.design/x/clipboard v0.7.0/go.mod h1:PQIvqYO9GP29yINEfsEn5zSQKAz3UgXmZKzDA6dnq2E= golang.design/x/clipboard v0.7.0/go.mod h1:PQIvqYO9GP29yINEfsEn5zSQKAz3UgXmZKzDA6dnq2E=
@ -7,12 +11,18 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU= golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU=
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
golang.org/x/exp/shiny v0.0.0-20250305212735-054e65f0b394 h1:bFYqOIMdeiCEdzPJkLiOoMDzW/v3tjW4AA/RmUZYsL8=
golang.org/x/exp/shiny v0.0.0-20250305212735-054e65f0b394/go.mod h1:ygj7T6vSGhhm/9yTpOQQNvuAUFziTH7RUiH74EoE2C8=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.6.0 h1:bR8b5okrPI3g/gyZakLZHeWxAR8Dn5CyxXv1hLH5g/4= golang.org/x/image v0.6.0 h1:bR8b5okrPI3g/gyZakLZHeWxAR8Dn5CyxXv1hLH5g/4=
golang.org/x/image v0.6.0/go.mod h1:MXLdDR43H7cDJq5GEGXEVeeNhPgi+YYEQ2pC1byI1x0= golang.org/x/image v0.6.0/go.mod h1:MXLdDR43H7cDJq5GEGXEVeeNhPgi+YYEQ2pC1byI1x0=
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c h1:Gk61ECugwEHL6IiyyNLXNzmu8XslmRP2dS0xjIYhbb4= golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c h1:Gk61ECugwEHL6IiyyNLXNzmu8XslmRP2dS0xjIYhbb4=
golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY= golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY=
golang.org/x/mobile v0.0.0-20250305212854-3a7bc9f8a4de h1:WuckfUoaRGJfaQTPZvlmcaQwg4Xj9oS2cvvh3dUqpDo=
golang.org/x/mobile v0.0.0-20250305212854-3a7bc9f8a4de/go.mod h1:/IZuixag1ELW37+FftdmIt59/3esqpAWM/QqWtf7HUI=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
@ -33,6 +43,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=

124
main.go
View File

@ -1,124 +0,0 @@
package main
import (
"bufio"
"fmt"
"io"
"os"
"os/exec"
"path"
"strings"
"golang.design/x/clipboard"
)
const DELEMITTER = "---------------------------------------------------------------------------\n"
func main() {
if len(os.Args) < 2 {
fmt.Println("Usage: go-cat <file-or-directory> [<file-or-directory>...]")
os.Exit(1)
}
var sb strings.Builder
for _, path := range os.Args[1:] {
err := processPath(&sb, path)
if err != nil {
fmt.Fprintf(os.Stderr, "Error processing %s: %v\n", path, err)
}
}
str := sb.String()
fmt.Println(str)
if err := writeClipboard(str); err != nil {
panic(err)
}
}
func writeClipboard(str string) error {
if os.Getenv("WAYLAND_DISPLAY") != "" {
cmd := exec.Command("wl-copy")
p, err := cmd.StdinPipe()
if err != nil {
return err
}
if err := cmd.Start(); err != nil {
return err
}
p.Write([]byte(str))
p.Close()
return cmd.Wait()
}
if err := clipboard.Init(); err != nil {
return err
}
clipboard.Write(clipboard.FmtText, []byte(str))
return nil
}
func processPath(sb *strings.Builder, path string) error {
info, err := os.Stat(path)
if err != nil {
return err
}
if info.IsDir() {
return catDir(sb, path)
}
return catFile(sb, path)
}
func catDir(sb *strings.Builder, dir string) error {
files, err := os.ReadDir(dir)
if err != nil {
return err
}
fmt.Printf("Directory: %s\n", dir)
for _, file := range files {
i, err := file.Info()
if err != nil {
continue
}
path := path.Join(dir, i.Name())
if !file.IsDir() {
catFile(sb, path)
} else {
catDir(sb, path)
}
}
return nil
}
func catFile(sb *strings.Builder, filePath string) error {
file, err := os.Open(filePath)
if err != nil {
return err
}
defer file.Close()
sb.WriteString(DELEMITTER)
sb.WriteString(filePath + "\n")
reader := bufio.NewReader(file)
for {
line, err := reader.ReadString('\n')
if err != nil && err != io.EOF {
return err
}
sb.WriteString(line)
if err == io.EOF {
break
}
}
return nil
}

73
pkg/cat/cater.go Normal file
View File

@ -0,0 +1,73 @@
package cat
import (
"fmt"
"os"
"strings"
)
type Cater []entry
func Path(paths ...string) (c Cater, err error) {
c = make(Cater, 0, 10)
var p os.FileInfo
for _, path := range paths {
p, err = os.Stat(path)
if err != nil {
return
}
var e entry
if p.IsDir() {
e, err = c.dir(path)
} else {
e, err = c.file(path)
}
if err != nil {
return
}
c = append(c, e)
}
return
}
func (c Cater) Ignored(ignore ignorer) Cater {
cat := make(Cater, 0, len(c))
ok := func(e entry) bool {
return !ignore.Ignore(e.fqname)
}
for _, entry := range c {
cat = append(cat, entry.filter(ok))
}
return cat
}
func (c Cater) ToString(delemiter string) string {
var sb strings.Builder
var entries []entry
entries = c
for len(entries) > 0 {
n := make([]entry, 0, len(entries))
for _, e := range entries {
if len(e.children) > 0 {
n = append(n, e.children...)
continue
}
sb.WriteString(fmt.Sprintf(delemiter, e.fqname))
sb.WriteString(e.content)
}
entries = n
}
return sb.String()
}
type ignorer interface {
// Ignore() returns true when the given path shall be Ignored.
Ignore(path string) bool
}

32
pkg/cat/entry.go Normal file
View File

@ -0,0 +1,32 @@
package cat
type entry struct {
name string
fqname string
content string
children []entry
}
func (e entry) filter(ok func(e entry) bool) entry {
children := make([]entry, 0, len(e.children))
for _, entry := range e.children {
if !ok(entry) {
continue
}
children = append(children, entry.filter(ok))
}
return entry{
name: e.name,
fqname: e.fqname,
content: e.content,
children: children,
}
}
func (e entry) traverse(lvl int, do func(e entry, lvl int)) {
do(e, lvl)
for _, entry := range e.children {
entry.traverse(lvl+1, do)
}
}

39
pkg/cat/markdown.go Normal file
View File

@ -0,0 +1,39 @@
package cat
import (
"fmt"
"strings"
)
func (c Cater) ToMarkdown() string {
var sb strings.Builder
write := func(e entry, lvl int) {
for range lvl {
sb.WriteString("#")
}
sb.WriteString(fmt.Sprintf(" %s (`%s`)\n", e.name, e.fqname))
if len(e.content) > 0 {
prts := strings.Split(e.name, ".")
sb.WriteString(
fmt.Sprintf(
"```%s\n%s\n```\n\n",
prts[len(prts)-1],
strings.ReplaceAll(
e.content,
"```",
"\\`\\`\\`",
),
),
)
}
}
for _, e := range c {
e.traverse(1, write)
}
return sb.String()
}

81
pkg/cat/read.go Normal file
View File

@ -0,0 +1,81 @@
package cat
import (
"bufio"
"io"
"os"
"path"
"strings"
"unicode/utf8"
)
func (c Cater) dir(dir string) (e entry, err error) {
files, err := os.ReadDir(dir)
if err != nil {
return
}
e.fqname = dir
e.name = name(dir)
e.children = []entry{}
for _, file := range files {
i, err := file.Info()
if err != nil {
return e, err
}
path := path.Join(dir, i.Name())
var ent entry
if !file.IsDir() {
ent, err = c.file(path)
} else {
ent, err = c.dir(path)
}
if err != nil {
return e, err
}
e.children = append(e.children, ent)
}
return
}
func (c Cater) file(filePath string) (e entry, err error) {
file, err := os.Open(filePath)
if err != nil {
return
}
defer file.Close()
e.fqname = filePath
e.name = name(filePath)
e.children = []entry{}
// read file into strings.Builder
var sb strings.Builder
reader := bufio.NewReader(file)
for {
line, err := reader.ReadString('\n')
if err != nil && err != io.EOF {
return e, err
}
if !utf8.Valid([]byte(line)) {
e.content = "blob\n"
return e, nil
}
sb.WriteString(line)
if err == io.EOF {
break
}
}
e.content = sb.String()
return
}
func name(name string) string {
ps := strings.Split(name, "/")
return ps[len(ps)-1]
}

42
pkg/cat/typst.go Normal file
View File

@ -0,0 +1,42 @@
package cat
import (
"fmt"
"strings"
)
func (c Cater) ToTypst() string {
var sb strings.Builder
write := func(e entry, lvl int) {
for range lvl {
sb.WriteString("=")
}
sb.WriteString(fmt.Sprintf(" %s (`%s`)\n", e.name, e.fqname))
if len(e.content) > 0 {
prts := strings.Split(e.name, ".")
sb.WriteString(
fmt.Sprintf(
"```%s\n%s\n```\n\n",
prts[len(prts)-1],
strings.ReplaceAll(
e.content,
"```",
"\\`\\`\\`",
),
),
)
}
}
for _, e := range c {
sb.WriteString("= Export\n")
sb.WriteString("#outline()\n")
e.traverse(1, write)
}
return sb.String()
}

44
pkg/clip/coppy.go Normal file
View File

@ -0,0 +1,44 @@
package clip
import (
"os"
"os/exec"
"golang.design/x/clipboard"
)
// Copy copies a given string to the clipboard, handling both Wayland and X11 environments.
// If WAYLAND_DISPLAY is set, it uses wl-copy to copy the string in a Wayland environment.
// Otherwise, it initializes the clipboard and writes the string using the clipboard package for X11.
// Parameters:
//
// str: The string to be copied to the clipboard.
//
// Returns:
//
// error: If an error occurs during copying (e.g., command execution or clipboard initialization fails).
func Copy(str string) error {
if os.Getenv("WAYLAND_DISPLAY") != "" {
cmd := exec.Command("wl-copy")
p, err := cmd.StdinPipe()
if err != nil {
return err
}
if err := cmd.Start(); err == nil {
p.Write([]byte(str))
p.Close()
return cmd.Wait()
}
}
if err := clipboard.Init(); err != nil {
return err
}
clipboard.Write(clipboard.FmtText, []byte(str))
return nil
}

12
pkg/ignore/filesystem.go Normal file
View File

@ -0,0 +1,12 @@
package ignore
import (
"strings"
)
type Filesystem struct{}
func (f Filesystem) Ignore(name string) bool {
parts := strings.Split(name, "/")
return strings.HasPrefix(parts[len(parts)-1], ".")
}

65
pkg/ignore/gitignore.go Normal file
View File

@ -0,0 +1,65 @@
package ignore
import (
"errors"
"os"
"path"
ignore "github.com/denormal/go-gitignore"
)
const (
gitIgnore = ".gitignore"
)
var (
ErrNotFound = errors.New("Not Found")
)
// getIgnore attempts to find and parse a .gitignore file recursively.
// It searches for the .gitignore file starting from the given path and traversing up the directory tree.
// It returns a pointer to the parsed ignore.GitIgnore object if found, otherwise nil.
func FindGitignore(name string) (ignore.GitIgnore, error) {
for {
stat, err := os.Stat(name)
if err != nil {
return nil, err
}
// If the current path is a directory, iterate through its contents.
if stat.IsDir() {
dir, err := os.ReadDir(name)
if err != nil {
name, _ = path.Split(name)
if name == "" {
return nil, ErrNotFound
}
}
for _, e := range dir {
if !e.IsDir() && e.Name() == gitIgnore {
if ignore, err := ignore.NewFromFile(path.Join(name, e.Name())); err == nil {
return ignore, nil
}
return nil, err
}
}
}
// If the current path is the .gitignore file itself.
if stat.Name() == gitIgnore {
if ignore, err := ignore.NewFromFile(name); err == nil {
return ignore, nil
}
return nil, err
}
name, _ = path.Split(name)
if name == "" {
return nil, ErrNotFound
}
}
}