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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ notebook path/to/file.md

- **Block editor** — 15 block types: paragraphs, headings (3 levels), bullet lists, numbered lists, checklists, code blocks, tables, quotes, definitions, callouts, dividers, embeds, and kanban boards. Press **/** to switch types.
- **Tables** — Pipe-delimited GFM tables with per-column widths. Alt+R/C to add rows/columns, Alt+Shift+Backspace/Alt+Shift+D to delete. Press Enter on an empty row to exit the table and drop the row.
- **Kanban boards** — Visual boards with priority cards. Arrows navigate, Shift+arrows move cards, **n** new card, **p** cycle priority, **s** toggle auto-sort. Round-trips as a `kanban` fenced block.
- **Kanban boards** — Visual boards with priority cards. Arrows navigate, Shift+arrows move cards, **n** new card, **Opt+K** copy card, **p** cycle priority, **s** toggle auto-sort. Round-trips as a `kanban` fenced block.
- **Callouts** — Five admonition variants (Note, Tip, Important, Warning, Caution). Ctrl+T to cycle.
- **Definitions** — Term/definition pairs. Press **:** to search and jump to definitions.
- **Embeds** — Reference other notes inline with `![[notebook/note]]`. Click in view mode to expand.
Expand Down
23 changes: 23 additions & 0 deletions internal/editor/kanban.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
"github.com/oobagi/notebook-cli/internal/block"
"github.com/oobagi/notebook-cli/internal/clipboard"
"github.com/oobagi/notebook-cli/internal/format"
"github.com/oobagi/notebook-cli/internal/theme"
)
Expand Down Expand Up @@ -941,6 +942,28 @@ func (m *Model) handleKanbanKey(msg tea.KeyPressMsg) (handled bool, cmd tea.Cmd)
m.kanban.sortByPriority()
}
return true, nil
case "alt+k":
// Copy selected card text to clipboard. Mirrors block-level Opt+K
// but scopes to the focused card instead of the whole board.
c := m.kanban.selectedCard()
if c == nil {
m.status = "No card to copy"
m.statusStyle = statusWarning
return true, m.scheduleStatusDismiss()
}
if c.Text == "" {
m.status = "Card is empty"
m.statusStyle = statusWarning
return true, m.scheduleStatusDismiss()
}
if err := clipboard.Copy(c.Text); err != nil {
m.status = "Could not copy: " + err.Error()
m.statusStyle = statusError
} else {
m.status = "Card copied to clipboard"
m.statusStyle = statusSuccess
}
return true, m.scheduleStatusDismiss()
case "backspace", "delete":
if m.kanban.selectedCard() != nil {
m.pushUndo()
Expand Down
34 changes: 34 additions & 0 deletions internal/editor/kanban_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,40 @@ func TestKanbanAddCard(t *testing.T) {
}
}

func TestKanbanCopyCardToClipboard(t *testing.T) {
m := newKanbanEditor(t)
c := m.kanban.selectedCard()
if c == nil || c.Text == "" {
t.Fatalf("expected a non-empty card selected, got %+v", c)
}
out, cmd := m.Update(tea.KeyPressMsg{Code: 'k', Mod: tea.ModAlt})
m = out.(Model)
if cmd == nil {
t.Errorf("expected status-dismiss cmd, got nil")
}
// Copy succeeds via OSC52 even when pbcopy/xclip aren't available, so
// the status should be the success message regardless of CI host.
if m.status != "Card copied to clipboard" {
t.Errorf("status = %q, want %q", m.status, "Card copied to clipboard")
}
}

func TestKanbanCopyCardOnEmptyColumn(t *testing.T) {
// Move into "Doing" then delete its only card so the column is empty
// and no card is selected — alt+k should report nothing to copy.
m := newKanbanEditor(t)
m = pressKey(m, "right")
m = pressKey(m, "backspace")
if m.kanban.selectedCard() != nil {
t.Fatalf("expected no card selected after deleting last card")
}
out, _ := m.Update(tea.KeyPressMsg{Code: 'k', Mod: tea.ModAlt})
m = out.(Model)
if m.status != "No card to copy" {
t.Errorf("status = %q, want %q", m.status, "No card to copy")
}
}

func TestKanbanDeleteCard(t *testing.T) {
m := newKanbanEditor(t)
before := len(m.kanban.cols[0].Cards)
Expand Down
Loading