Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Repository Guidelines

## Project Structure & Module Organization

SpiceEdit is a Go terminal editor module at `github.com/cloudmanic/spice-edit`; the CLI entry point is `main.go` and the binary is `spiceedit`. Core packages live under `internal/`: `app` owns the event loop and rendering, `editor` owns buffers/tabs/editing behavior, `filetree` manages the sidebar tree, and supporting packages cover clipboard, formatting, icons, config, theme, finder, and versioning. Tests sit beside source files as `*_test.go`. Website assets and docs live in `website/` as a Hugo + Tailwind site. Release packaging includes `Formula/spice-edit.rb`, `install.sh`, and samples under `samples/`.

## Build, Test, and Development Commands

- `make run`: run the editor in the current directory with `go run .`.
- `make build`: compile `./bin/spiceedit`.
- `make build-linux`: cross-compile a static `linux/amd64` binary.
- `make test`: run `go test -race ./...`; use before PRs.
- `make test-short`: quick `go test -short ./...` loop while iterating.
- `make coverage`: write `coverage.out` and `coverage.html`.
- `make tidy`: sync `go.mod` and `go.sum`.
- `make site-install`, `make site-dev`, `make site-build`: manage website deps, local Hugo, and production builds.

## Coding Style & Naming Conventions

Use `gofmt`/`go test` defaults and idiomatic Go names: exported identifiers in `CamelCase`, unexported in `camelCase`, package names short and lowercase. New Go source files should follow the existing header block style. Keep short doc comments above functions, including private helpers, explaining intent. Avoid adding `Ctrl+` shortcuts; editor actions must stay reachable from the main `≡` menu because SSH/tmux workflows may swallow shortcuts or right-click events.

## Testing Guidelines

Every non-trivial source file should have a same-package test file, for example `internal/editor/buffer.go` and `internal/editor/buffer_test.go`. Add regression tests for bug fixes and cover happy paths and obvious failures. Use `t.TempDir()` for filesystem state. For drawing tests, use `tcell.NewSimulationScreen("UTF-8")` and assert screen contents.

## Commit & Pull Request Guidelines

Recent commits use concise, imperative summaries, often with PR numbers, such as `Mute dotfiles in tree + per-tab Nerd Font icons (#32)`. Release automation uses `[skip ci]`; preserve that marker when editing generated release commits or workflows. PRs should describe behavior changes, mention tests run, link issues, and include screenshots or terminal captures for UI/website changes.

## Security & Configuration Tips

Format-on-save commands are project config and require trust prompts; do not bypass that flow. Keep generated artifacts (`bin/`, `coverage.out`, `coverage.html`, `website/public/`, built CSS) out of normal feature commits unless the release or website workflow explicitly requires them.
25 changes: 13 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,20 +178,21 @@ SpiceEdit deliberately avoids `Ctrl+`-style shortcuts (they fight `tmux`,
real terminal). Instead, **`Esc` is the leader key**: tap `Esc`, then
within half a second tap one of the letters below.

| Combo | Action |
| ----------- | --------------- |
| `Esc Esc` | Open ≡ menu |
| `Esc s` | Save |
| `Esc u` | Undo |
| `Esc r` | Redo |
| `Esc w` | Close tab |
| `Esc q` | Quit |
| `Esc n` | New file |
| `Esc t` | Toggle sidebar |
| `Esc f` | Find in file |
| Combo | Action |
| ----------- | -------------------- |
| `Esc Esc` | Open ≡ menu |
| `Esc s` | Save |
| `Esc u` | Undo |
| `Esc r` | Redo |
| `Esc w` | Close tab |
| `Esc q` | Quit |
| `Esc n` | New file |
| `Esc t` | Toggle sidebar |
| `Esc /` | Toggle line comment |
| `Esc f` | Find in file |
| `Esc p` | Find file in project |

A lone `Esc` is harmless — if you don't follow it with a bound letter
A lone `Esc` is harmless — if you don't follow it with a bound key
within the window, your next keystroke goes to the editor as normal,
so accidental `Esc` taps never swallow a real character.

Expand Down
47 changes: 39 additions & 8 deletions internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ const (
minEditorAfterDrag = 40
minWidth = 50
minHeight = 24
statusFlashFor = 3 * time.Second
doubleClickMs = 500 * time.Millisecond
doubleEscMs = 500 * time.Millisecond
wheelLines = 3
statusFlashFor = 3 * time.Second
doubleClickMs = 500 * time.Millisecond
doubleEscMs = 500 * time.Millisecond
wheelLines = 3

// treeRefreshInterval is how often the background goroutine kicks off
// a file-tree reload so the sidebar stays in sync with on-disk changes
Expand Down Expand Up @@ -193,6 +193,7 @@ func builtinMenuGroups() [][]menuItemDef {
{label: "Copy selection", action: (*App).menuCopy, enabled: (*App).hasSelection},
{label: "Cut selection", action: (*App).menuCut, enabled: (*App).hasSelection},
{label: "Paste", action: (*App).menuPaste, enabled: (*App).hasClipboard},
{label: "Toggle line comment", action: (*App).menuToggleLineComment, enabled: (*App).hasCommentableTab},
},
// View toggle
{
Expand Down Expand Up @@ -230,7 +231,7 @@ func (a *App) menuLayout() (items []menuItemDef, dividers []int, modalHeight int
// the menu; if a $FILE-dependent command is invoked with
// no tab open it'll fail with a real error and our info
// modal surfaces it. Better that than getting the
// heuristic wrong half the time.
// heuristic wrong half the time.
ca = append(ca, menuItemDef{
label: a.customActions[i].Label,
action: func(app *App) { app.runCustomAction(i) },
Expand Down Expand Up @@ -299,9 +300,9 @@ type App struct {
lastClick clickRecord
lastTabRects []tabRect

menuOpen bool
hoveredMenuRow int // index into menuItems of the row under the mouse, or -1.
lastEscape time.Time // timestamp of the previous Esc press, for double-tap detection.
menuOpen bool
hoveredMenuRow int // index into menuItems of the row under the mouse, or -1.
lastEscape time.Time // timestamp of the previous Esc press, for double-tap detection.

// Prompt modal — single-line text input with OK / Cancel. Used by
// Rename and New File. See modals.go for render + event handling.
Expand Down Expand Up @@ -1704,6 +1705,17 @@ func (a *App) hasSelection() bool {
return t != nil && t.HasSelection()
}

// hasCommentableTab reports whether the active tab is editable text with a
// known single-line comment marker.
func (a *App) hasCommentableTab() bool {
t := a.activeTabPtr()
if t == nil || t.IsImage() {
return false
}
_, ok := editor.LineCommentPrefix(t.Path)
return ok
}

// hasClipboard reports whether the editor's internal clipboard has content
// to paste.
func (a *App) hasClipboard() bool { return a.clipBuf != "" }
Expand Down Expand Up @@ -1891,6 +1903,25 @@ func (a *App) menuPaste() {
a.pasteClipboard()
}

// menuToggleLineComment comments or uncomments the active line selection.
func (a *App) menuToggleLineComment() {
a.closeMenu()
tab := a.activeTabPtr()
if tab == nil || tab.IsImage() {
return
}
changed, ok := tab.ToggleLineComment()
if !ok {
a.flash("No line comment syntax for this file")
return
}
if !changed {
a.flash("No non-blank lines to comment")
return
}
a.flash("Toggled line comment")
}

// menuRefreshTree forces an immediate sidebar reload. Currently unwired
// from the menu — the 10s background poller covers the common case — but
// the method is kept so re-adding the menu row (see menuItems) only
Expand Down
Loading
Loading