Skip to content

Commit 5cb2552

Browse files
committed
feat(output): Add tree-only mode for structure-only output
Add -T/--tree-only flag to output just the file structure without file contents. This is useful for sharing project layout with LLMs without overwhelming context with full file contents. - Add CLI flag with short (-T) and long (--tree-only) forms - Add T keybinding to toggle tree-only mode in interactive TUI - Update markdown and text templates to conditionally render files - Update documentation with new option and example usage
1 parent 1bae135 commit 5cb2552

8 files changed

Lines changed: 45 additions & 9 deletions

File tree

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ to your clipboard, ready for LLM processing.
2727
-**Temp File**: Generate the output file in your system's temporary directory
2828
- 📋 **Clipboard Integration**: Copy content or output file directly to your clipboard
2929
- 🌲 **Directory Tree View**: Display a tree-style view of your project structure
30+
- 🌳 **Tree-Only Mode**: Output just the file structure without contents using `-T` flag
3031
- 🧮 **Token Estimation**: Get estimated token count for LLM context windows
3132
- 🛡️ **Secret Detection & Redaction**: Uses [gitleaks](https://github.com/gitleaks/gitleaks) to identify potential secrets and prevent sharing sensitive information
3233
- 🔗 **Dependency Resolution**: Automatically include dependencies for Go, JS/TS, Python when using the `--deps` flag
@@ -108,6 +109,7 @@ grab [options] [directory]
108109
| `--theme <name>` | Set the UI theme. Available: catppuccin-latte, catppuccin-frappe, catppuccin-macchiato, catppuccin-mocha, rose-pine, rose-pine-dawn, rose-pine-moon, dracula, nord. (default: `"catppuccin-mocha"`). |
109110
| `--show-tokens` | Show the number of tokens for each file in file tree. |
110111
| `--icons` | Display Nerd Font icons. |
112+
| `-T, --tree-only` | Output only file structure without contents. Useful for sharing project layout with LLMs. |
111113

112114
### 📖 Examples
113115

@@ -177,6 +179,12 @@ grab [options] [directory]
177179
grab git@github.com:user/repo.git
178180
```
179181

182+
12. Output only the project structure (no file contents):
183+
184+
```bash
185+
grab -T -n
186+
```
187+
180188
## ⌨️ Keyboard Controls
181189

182190
### Navigation
@@ -211,6 +219,7 @@ grab [options] [directory]
211219
| Toggle Dependency Resolution | <kbd>D</kbd> | Enable/disable automatic dependency resolution for Go & JS/TS (Default: Off) |
212220
| Cycle output formats | <kbd>F</kbd> | Cycle through available output formats (markdown, text, xml) |
213221
| Toggle Secret Redaction | <kbd>S</kbd> | Enable/disable automatic secret redaction (Default: On) |
222+
| Toggle Tree-Only Mode | <kbd>T</kbd> | Output only file structure without contents (Default: Off) |
214223

215224
### View Options
216225

cmd/grab/main.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ func main() {
5050
var maxFileSizeStr string
5151
var showIcons bool
5252
var showTokenCount bool
53+
var treeOnly bool
5354

5455
flag.BoolVar(&showHelp, "help", false, "Display help information")
5556
flag.BoolVar(&showHelp, "h", false, "Display help information (shorthand)")
@@ -92,6 +93,9 @@ func main() {
9293

9394
flag.BoolVar(&showTokenCount, "show-tokens", false, "Show the number of tokens for each file")
9495

96+
flag.BoolVar(&treeOnly, "tree-only", false, "Output only file structure without contents")
97+
flag.BoolVar(&treeOnly, "T", false, "Output only file structure (shorthand)")
98+
9599
flag.Parse()
96100

97101
if showHelp {
@@ -187,7 +191,7 @@ func main() {
187191
}
188192

189193
if nonInteractive {
190-
runNonInteractive(root, filterMgr, outputPath, useTempFile, formatName, skipRedaction, resolveDeps, maxDepth, maxFileSize)
194+
runNonInteractive(root, filterMgr, outputPath, useTempFile, formatName, skipRedaction, resolveDeps, maxDepth, maxFileSize, treeOnly)
191195
} else {
192196
config := model.Config{
193197
RootPath: root,
@@ -201,6 +205,7 @@ func main() {
201205
ShowTokenCount: showTokenCount,
202206
MaxDepth: maxDepth,
203207
MaxFileSize: maxFileSize,
208+
TreeOnly: treeOnly,
204209
}
205210

206211
m := model.NewModel(config)
@@ -213,7 +218,7 @@ func main() {
213218
}
214219

215220
// runNonInteractive processes files and generates output without user interaction
216-
func runNonInteractive(rootPath string, filterMgr *filesystem.FilterManager, outputPath string, useTempFile bool, formatName string, skipRedaction bool, resolveDeps bool, maxDepth int, maxFileSize int64) {
221+
func runNonInteractive(rootPath string, filterMgr *filesystem.FilterManager, outputPath string, useTempFile bool, formatName string, skipRedaction bool, resolveDeps bool, maxDepth int, maxFileSize int64, treeOnly bool) {
217222
gitIgnoreMgr, err := filesystem.NewGitIgnoreManager(rootPath)
218223
if err != nil {
219224
log.Fatalf("Error reading .gitignore: %v\n", err)
@@ -301,6 +306,7 @@ func runNonInteractive(rootPath string, filterMgr *filesystem.FilterManager, out
301306
format := formats.GetFormat(formatName)
302307
gen.SetFormat(format)
303308
gen.SetRedactionMode(!skipRedaction)
309+
gen.SetTreeOnlyMode(treeOnly)
304310

305311
gen.SelectedFiles = selectedFiles
306312

internal/generator/formats/markdown.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,12 @@ const markdownTemplate = `# Project Structure
4444
4545
` + "```" + `
4646
{{.Structure}}` + "```" + `
47-
47+
{{if .Files}}
4848
# Project Files
4949
{{range .Files}}
5050
## File: ` + "`" + `{{.Path}}` + "`" + `
5151
5252
` + "```" + `{{.Language}}
5353
{{.Content}}
5454
` + "```" + `
55-
{{end}}
56-
`
55+
{{end}}{{end}}`

internal/generator/formats/text.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ PROJECT STRUCTURE
5757
{{separator}}
5858
5959
{{.Structure}}
60-
60+
{{if .Files}}
6161
{{separator}}
6262
PROJECT FILES
6363
{{separator}}
@@ -67,5 +67,4 @@ FILE: {{.Path}}
6767
{{separator .Path}}
6868
6969
{{.Content}}
70-
{{end}}
71-
`
70+
{{end}}{{end}}`

internal/generator/generator.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type Generator struct {
2626
UseGitIgnore bool
2727
ShowHidden bool
2828
RedactSecrets bool
29+
TreeOnly bool
2930
lastSecretCount int
3031
}
3132

@@ -75,6 +76,11 @@ func (g *Generator) SetRedactionMode(redact bool) {
7576
g.RedactSecrets = redact
7677
}
7778

79+
// SetTreeOnlyMode enables or disables tree-only output (structure without file contents).
80+
func (g *Generator) SetTreeOnlyMode(treeOnly bool) {
81+
g.TreeOnly = treeOnly
82+
}
83+
7884
// Generate creates an output file in the specified format
7985
func (g *Generator) Generate() (string, int, int, error) {
8086
if len(g.SelectedFiles) == 0 {
@@ -211,7 +217,9 @@ func (g *Generator) PrepareTemplateData() (TemplateData, error) {
211217
}
212218

213219
var filesData []FileData
214-
collectFiles(rootNode, &filesData, g.RootPath, &g.SecretScanner)
220+
if !g.TreeOnly {
221+
collectFiles(rootNode, &filesData, g.RootPath, &g.SecretScanner)
222+
}
215223

216224
secretCount := 0
217225

internal/model/init_update.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,15 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
471471
}
472472
m.warningMsg = ""
473473
m.refreshViewportContent()
474+
case "T":
475+
m.treeOnly = !m.treeOnly
476+
m.generator.SetTreeOnlyMode(m.treeOnly)
477+
if m.treeOnly {
478+
m.successMsg = "Tree-only mode enabled (structure only)"
479+
} else {
480+
m.successMsg = "Tree-only mode disabled (full output)"
481+
}
482+
m.refreshViewportContent()
474483
case "P":
475484
// Toggle preview pane
476485
m.showPreview = !m.showPreview

internal/model/model.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ type Model struct {
7070
currentPreviewPath string
7171
currentPreviewContent string
7272
currentPreviewIsDir bool
73+
treeOnly bool
7374
lastKeyTime int64 // Last key press time
7475
lastKey string // Last key pressed
7576
tokenCache *TokenCache
@@ -87,6 +88,7 @@ type Config struct {
8788
ResolveDeps bool
8889
ShowIcons bool
8990
ShowTokenCount bool
91+
TreeOnly bool
9092
}
9193

9294
// updatePreview reads the content of the file at the cursor and updates the preview viewport
@@ -193,6 +195,7 @@ func NewModel(config Config) Model {
193195
format := formats.GetFormat(config.Format)
194196
gen.SetFormat(format)
195197
gen.SetRedactionMode(!config.SkipRedaction)
198+
gen.SetTreeOnlyMode(config.TreeOnly)
196199

197200
moduleName := dependencies.ReadGoModFile(config.RootPath)
198201

@@ -225,6 +228,7 @@ func NewModel(config Config) Model {
225228
cursor: 0,
226229
showTokenCount: config.ShowTokenCount,
227230
showPreview: false,
231+
treeOnly: config.TreeOnly,
228232
tokenCache: NewTokenCache(),
229233
}
230234
}

internal/ui/help.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Selection & Output:
2121
D Toggle automatic dependency resolution (Go, TS/JS)
2222
F Cycle through output formats (md, txt, xml)
2323
S Toggle secret redaction (Default: On)
24+
T Toggle tree-only mode (structure without contents)
2425
2526
View Options:
2627
i Toggle .gitignore filter
@@ -66,6 +67,7 @@ const UsageText = `Usage:
6667
rose-pine-moon, dracula, nord. (default: "catppuccin-mocha").
6768
--show-tokens Show the number of tokens for each file in file tree.
6869
--icons Display Nerd Font icons.
70+
-T, --tree-only Output only file structure without contents.
6971
7072
Examples:
7173
# Run interactively in the current directory

0 commit comments

Comments
 (0)