package main import ( "fmt" "os" "path/filepath" "strings" "time" "verstak/internal/core/activity" "verstak/internal/core/files" "verstak/internal/core/nodes" syncsvc "verstak/internal/core/sync" "verstak/internal/core/util" ) func (a *App) CaptureText(text string) (*InboxNodeDTO, error) { if err := a.requireVault(); err != nil { return nil, err } text = strings.TrimSpace(text) if text == "" { return nil, fmt.Errorf("text required") } title := firstLineTitle(text, "Captured text") content := "# " + title + "\n\n" + text + "\n" return a.createCaptureNote(title, content, "text", "clipboard") } func (a *App) CaptureURL(rawURL, title string) (*InboxNodeDTO, error) { if err := a.requireVault(); err != nil { return nil, err } rawURL = strings.TrimSpace(rawURL) if rawURL == "" { return nil, fmt.Errorf("url required") } title = strings.TrimSpace(title) if title == "" { title = rawURL } title = firstLineTitle(title, rawURL) content := "# " + title + "\n\n" + rawURL + "\n" return a.createCaptureNote(title, content, "url", "clipboard") } func (a *App) createCaptureNote(title, content, kind, source string) (*InboxNodeDTO, error) { node, err := a.nodes.Create(nil, nodes.TypeNote, title, 0, "", "") if err != nil { return nil, fmt.Errorf("create node: %w", err) } inboxDir := filepath.Join(a.vault, ".verstak", "inbox") if err := os.MkdirAll(inboxDir, 0o750); err != nil { return nil, fmt.Errorf("create inbox dir: %w", err) } filename := node.ID + ".md" absPath := filepath.Join(inboxDir, filename) if err := os.WriteFile(absPath, []byte(content), 0o640); err != nil { return nil, fmt.Errorf("write capture note: %w", err) } relPath, _ := filepath.Rel(a.vault, absPath) now := time.Now().UTC() fileRec := &files.Record{ ID: util.UUID7(), NodeID: node.ID, Filename: filename, Path: relPath, StorageMode: "vault", Size: int64(len(content)), MIME: "text/markdown", CreatedAt: now, UpdatedAt: now, } if _, err := a.db.Exec( `INSERT INTO files (id,node_id,filename,path,storage_mode,size,mime,created_at,updated_at,missing) VALUES (?,?,?,?,?,?,?,?,?,0)`, fileRec.ID, fileRec.NodeID, fileRec.Filename, fileRec.Path, fileRec.StorageMode, fileRec.Size, fileRec.MIME, fileRec.CreatedAt.Format(time.RFC3339), fileRec.UpdatedAt.Format(time.RFC3339)); err != nil { return nil, fmt.Errorf("insert capture file: %w", err) } if _, err := a.db.Exec(`INSERT INTO notes (node_id, file_id, format) VALUES (?,?,?)`, node.ID, fileRec.ID, "markdown"); err != nil { return nil, fmt.Errorf("insert capture note: %w", err) } if err := a.nodes.MetaSet(node.ID, "capture.inbox", "true"); err != nil { return nil, err } if err := a.nodes.MetaSet(node.ID, "capture.kind", kind); err != nil { return nil, err } if err := a.nodes.MetaSet(node.ID, "capture.source", source); err != nil { return nil, err } if err := a.nodes.MetaSet(node.ID, "capture.created_at", now.Format(time.RFC3339)); err != nil { return nil, err } _ = a.activity.Record("", activity.TargetNote, node.ID, "", activity.TypeNoteCreated, title, `{"capture":true}`) _ = a.sync.RecordOp(syncsvc.EntityNote, node.ID, syncsvc.OpCreate, notePayload(node, fileRec, content)) return a.inboxNodeDTO(node) } func (a *App) inboxNodeDTO(n *nodes.Node) (*InboxNodeDTO, error) { dto := &InboxNodeDTO{NodeDTO: toNodeDTO(n)} if kind, ok, err := a.nodes.MetaGet(n.ID, "capture.kind"); err == nil && ok { dto.CaptureKind = kind } if source, ok, err := a.nodes.MetaGet(n.ID, "capture.source"); err == nil && ok { dto.CaptureSource = source } return dto, nil } func firstLineTitle(text, fallback string) string { for _, line := range strings.Split(text, "\n") { line = strings.TrimSpace(line) if line != "" { if len(line) > 80 { return line[:80] } return line } } return fallback }