Skip to content

cmd

Go
import "github.com/andreoliwa/logseq-doctor/cmd"

Index

Constants

Go
const (
    defaultServePort     = 8091
    defaultPocketBaseURL = "http://127.0.0.1:8090"
    pbReadyTimeout       = 10 * time.Second
    shutdownTimeout      = 5 * time.Second
    readHeaderTimeout    = 5 * time.Second
)

Go
const groomDefaultLimit = 10

Go
const groomDefaultOlderThan = "1 year"

Go
const groomDefaultTermWidth = 80

Go
const groomFetchMultiplier = 5 // fetch 5× the limit to absorb tasks filtered out by HasFutureDate

Go
const groomNameMargin = 2 // leading space + right margin

Go
const groomSep = "────────────────────────────────────────────"

Variables

Go
var backlogCSS []byte

Go
var backlogCmd = &cobra.Command{
    Use:   "backlog [partial page names]",
    Short: "Aggregate tasks from multiple pages into a backlog",
    Long: `The backlog command aggregates tasks from one or more pages into a unified backlog.

If partial page names are provided, only page titles that contain the provided names are processed.

Each line on the "backlog" page that includes references to other pages or tags generates a separate backlog.
The first page in the line determines the name of the backlog page.
Tasks are retrieved from all provided pages or tags.
This setup enables users to rearrange tasks using the arrow keys and manage task states (start/stop)
directly within the interface.`,
    Run: func(_ *cobra.Command, args []string) {
        path := os.Getenv("LOGSEQ_GRAPH_PATH")
        logseqAPI := logseqapi.NewLogseqAPI(path,
            os.Getenv("LOGSEQ_HOST_URL"), os.Getenv("LOGSEQ_API_TOKEN"))
        graph := logseqapi.OpenGraphFromPath(path)
        reader := backlog.NewPageConfigReader(graph, "backlog")
        proc := backlog.NewBacklog(graph, logseqAPI, reader, time.Now)

        err := proc.ProcessAll(args)
        if err != nil {
            fmt.Println(err)
            os.Exit(1)
        }
    },
}

Go
var backlogHTML []byte

contentCmd represents the content command.

Go
var contentCmd = &cobra.Command{
    Use:   "content",
    Short: "Append raw Markdown content to Logseq",
    Long: `Append raw Markdown content to Logseq.

Pipe your content via stdin.
For now, it will be appended at the end of the current journal page.`,
    Run: func(_ *cobra.Command, _ []string) {
        graph := api.OpenGraphFromPath(os.Getenv("LOGSEQ_GRAPH_PATH"))
        stdin := internal.ReadFromStdin()

        var targetDate time.Time

        if journalFlag != "" {
            parsedDate, err := time.Parse("2006-01-02", journalFlag)
            if err != nil {
                log.Fatalln("Invalid journal date format. Use YYYY-MM-DD:", err)
            }

            targetDate = parsedDate
        } else {
            targetDate = time.Now()
        }

        _, err := internal.AppendRawMarkdownToJournal(graph, targetDate, stdin)
        if err != nil {
            log.Fatalln(err)
        }
    },
}

Go
var dashboardCmd = &cobra.Command{
    Use:     "dashboard",
    Aliases: DashboardAliases(),
    Short:   "Start PocketBase and the backlog web UI",
    Long: `Starts PocketBase as a managed subprocess, waits for it to be ready,
then serves the backlog dashboard at http://localhost:8091 (configurable).

Environment variables:
  POCKETBASE_URL       PocketBase URL (default http://127.0.0.1:8090)
  POCKETBASE_USERNAME  PocketBase admin email
  POCKETBASE_PASSWORD  PocketBase admin password
  LOGSEQ_GRAPH_PATH    Path to Logseq graph (required for write-back)
  LQD_SERVE_PORT       HTTP server port (default 8091)`,
    RunE: runDashboard,
}

errGroomNoCollection is returned when the lqd_tasks collection does not exist in PocketBase.

Go
var errGroomNoCollection = errors.New("no tasks found. Run 'lqd sync --init' first")

groomStyles holds the lipgloss styles for the groom TUI.

Go
var groomStyles = struct {
    separator lipgloss.Style
    header    lipgloss.Style
    taskName  lipgloss.Style
    label     lipgloss.Style
    value     lipgloss.Style
    age       lipgloss.Style
    actions   lipgloss.Style
    prompt    lipgloss.Style
    success   lipgloss.Style
    warning   lipgloss.Style
    errStyle  lipgloss.Style
    link      lipgloss.Style
}{
    separator: lipgloss.NewStyle().Foreground(lipgloss.Color("240")),
    header:    lipgloss.NewStyle().Foreground(lipgloss.Color("33")).Bold(true),
    taskName:  lipgloss.NewStyle().Foreground(lipgloss.Color("255")).Bold(true),
    label:     lipgloss.NewStyle().Foreground(lipgloss.Color("245")),
    value:     lipgloss.NewStyle().Foreground(lipgloss.Color("252")),
    age:       lipgloss.NewStyle().Foreground(lipgloss.Color("208")).Italic(true),
    actions:   lipgloss.NewStyle().Foreground(lipgloss.Color("255")).Bold(true),
    prompt:    lipgloss.NewStyle().Foreground(lipgloss.Color("33")).Bold(true),
    success:   lipgloss.NewStyle().Foreground(lipgloss.Color("82")),
    warning:   lipgloss.NewStyle().Foreground(lipgloss.Color("226")),
    errStyle:  lipgloss.NewStyle().Foreground(lipgloss.Color("196")).Bold(true),
    link:      lipgloss.NewStyle().Foreground(lipgloss.Color("39")).Underline(true),
}

Go
var journalFlag string //nolint:gochecknoglobals

mdCmd represents the md command using the default dependencies.

Go
var mdCmd = NewMdCmd(nil) //nolint:gochecknoglobals

rootCmd represents the base command when called without any subcommands.

Go
var rootCmd = &cobra.Command{
    Use:   "lqd",
    Short: "Logseq Doctor heals your Markdown files for Logseq",
    Long: `Logseq Doctor heals your Markdown files for Logseq.

Convert flat Markdown to Logseq outline, clean up Markdown,
prevent invalid content, and more stuff to come.

"lqdpy" is the CLI tool originally written in Python; "lqd" is the Go version.
The intention is to slowly convert everything to Go.`,
}

taskCmd represents the task command using the default dependencies.

Go
var taskCmd = NewTaskCmd() //nolint:gochecknoglobals

tidyUpCmd represents the tidyUp command.

Go
var tidyUpCmd = &cobra.Command{
    Use:   "tidy-up file1.md [file2.md ...]",
    Short: "Tidy up your Markdown files.",

    Long: `Tidy up your Markdown files, checking for invalid content and fixing some of them automatically.

- Check for forbidden references to pages/tags
- Check for running tasks (DOING)
- Check for double spaces`,
    Args: cobra.MinimumNArgs(1),
    Run: func(_ *cobra.Command, args []string) {
        graph := api.OpenGraphFromPath(os.Getenv("LOGSEQ_GRAPH_PATH"))

        exitCode := 0

        for _, path := range args {
            if internal.TidyUpOneFile(graph, path) != 0 {
                exitCode = 1
            }
        }

        os.Exit(exitCode)
    },
}

func BuildHTTPMux

Go
func BuildHTTPMux(pbURL, token, graphPath string) *http.ServeMux

BuildHTTPMux creates the HTTP mux with all routes registered.

func DashboardAliases

Go
func DashboardAliases() []string

DashboardAliases returns the Cobra aliases for the dashboard command. Exported so tests can verify the alias without accessing the unexported global.

func Execute

Go
func Execute()

Execute adds all child commands to the root command and sets flags appropriately. This is called by main.main(). It only needs to happen once to the rootCmd.

func NewGroomCmd

Go
func NewGroomCmd(deps *GroomDependencies) *cobra.Command

NewGroomCmd creates a new groom command with the specified dependencies. If deps is nil, it uses default (production) implementations.

func NewMdCmd

Go
func NewMdCmd(deps *MdDependencies) *cobra.Command

NewMdCmd creates a new md command with the specified dependencies. If deps is nil, it uses default implementations.

func NewSyncCmd

Go
func NewSyncCmd(deps *SyncDependencies) *cobra.Command

NewSyncCmd creates a new sync command with the specified dependencies. If deps is nil, it uses default (production) implementations.

func NewTaskAddCmd

Go
func NewTaskAddCmd(deps *TaskAddDependencies) *cobra.Command

NewTaskAddCmd creates a new task add subcommand with the specified dependencies. If deps is nil, it uses default implementations.

func NewTaskCmd

Go
func NewTaskCmd() *cobra.Command

NewTaskCmd creates the parent task command.

func ParseDateFromJournalFlag

Go
func ParseDateFromJournalFlag(journalFlag string, timeNow func() time.Time) (time.Time, error)

ParseDateFromJournalFlag parses the journal flag and returns the target date. If journalFlag is empty, it returns the current time from timeNow. If journalFlag is not empty, it parses it as YYYY-MM-DD format. Returns an error if the date format is invalid.

func ResolveEnvWithDefault

Go
func ResolveEnvWithDefault(key, fallback string) string

ResolveEnvWithDefault returns the env var value or fallback if unset.

func ResolvePort

Go
func ResolvePort(cmd *cobra.Command) int

ResolvePort returns the effective port: flag > env var > default.

func addJournalFlag

Go
func addJournalFlag(cmd *cobra.Command, flagVar *string)

addJournalFlag adds a --journal/-j flag to the command.

func addKeyFlag

Go
func addKeyFlag(cmd *cobra.Command, flagVar *string)

addKeyFlag adds a --key/-k flag to the command.

func addPageFlag

Go
func addPageFlag(cmd *cobra.Command, flagVar *string, what string)

addPageFlag adds a --page/-p flag to the command with customizable help text.

func addParentFlag

Go
func addParentFlag(cmd *cobra.Command, flagVar *string, what string)

addParentFlag adds a --parent flag to the command with customizable help text.

func applyChanges

Go
func applyChanges(pbClient *pocketbase.Client, desired []map[string]any) error

func authenticate

Go
func authenticate(pbURL, pbUser, pbPass string) (string, error)

authenticate obtains a PocketBase token. Returns "" and nil when credentials are absent.

func backlogUnrankedSectionTexts

Go
func backlogUnrankedSectionTexts() []string

unrankedSectionTexts lists the header texts that mark the start of an unranked section on a backlog page. Any block ref that is a child of one of these headers backlogUnrankedSectionTexts returns the header texts that mark the start of an unranked section on a backlog page. Any block ref that is a child of one of these headers is assigned SectionUnranked during sync.

func buildDesiredRecords

Go
func buildDesiredRecords(tasks []logseqapi.TaskJSON, ranks map[string][]lqdsync.RankInfo, tagsByUUID map[string]string, currentTime func() time.Time) []map[string]any

func collectBacklogRefs

Go
func collectBacklogRefs(graph *logseq.Graph, config *backlog.Config) (map[string][]lqdsync.RankInfo, []string)

collectBacklogRefs scans the Focus page and all configured backlog pages, returning a rank map (uuid → []RankInfo) and the ordered list of backlog names. Each ref is classified as SectionRanked or SectionUnranked based on which section header it lives under on the page.

func collectFocusRefs

Go
func collectFocusRefs(graph *logseq.Graph, focusPagePath string, ranks map[string][]lqdsync.RankInfo, backlogOrder *[]string)

func collectPageRefs

Go
func collectPageRefs(graph *logseq.Graph, backlogPagePath string, ranks map[string][]lqdsync.RankInfo, backlogOrder *[]string)

func fetchGroomTasks

Go
func fetchGroomTasks(now, thresholdDate time.Time, limit int) (*pocketbase.Client, []map[string]any, error)

fetchGroomTasks initialises PocketBase, checks the collection, and fetches matching tasks. Returns (nil, nil, nil) with a printed message when there are no tasks.

func fetchLogseqTasks

Go
func fetchLogseqTasks(logseqAPI logseqapi.LogseqAPI) ([]logseqapi.TaskJSON, error)

func gracefulShutdown

Go
func gracefulShutdown(srv *http.Server) error

gracefulShutdown shuts the server down with a fresh timeout context. The parent context is already cancelled at this point, so a new one is needed.

func groomHandleTask

Go
func groomHandleTask(graph *logseq.Graph, api api.LogseqAPI, backlogConfig *backlog.Config, pbUpdater func(recordID string, groomedAt time.Time) error, task map[string]any, now time.Time, counts *groom.Counts) bool

groomHandleTask reads keypresses for a single task until a valid action is taken. Returns true if the user requested quit.

func groomPrintApplyError

Go
func groomPrintApplyError(applyErr error, counts *groom.Counts)

func groomSyncPocketBase

Go
func groomSyncPocketBase(pbUpdater func(recordID string, groomedAt time.Time) error, action *groom.Action, task map[string]any, now time.Time)

func handleConfig

Go
func handleConfig(writer http.ResponseWriter, graphPath string)

handleConfig returns UI configuration derived from the server environment.

func handleMoveToUnranked

Go
func handleMoveToUnranked(writer http.ResponseWriter, req *http.Request, graphPath, pbURL, token string)

handleMoveToUnranked handles POST /internal/move-to-unranked.

func hasRankInBacklog

Go
func hasRankInBacklog(rankInfos []lqdsync.RankInfo, backlogName string) bool

hasRankInBacklog reports whether the given backlog name already has an entry in the rank slice.

func init

Go
func init()

func initCollection

Go
func initCollection(client *pocketbase.Client) error

func logseqGraphName

Go
func logseqGraphName() string

logseqGraphName extracts the graph name from the graph path for deep links.

func openGroomResources

Go
func openGroomResources() (*logseq.Graph, api.LogseqAPI, *backlog.Config, error)

openGroomResources opens the Logseq graph, API, and reads the backlog config.

func printTaskCard

Go
func printTaskCard(task map[string]any, index, total int, now time.Time, termWidth int)

printTaskCard prints a single task card to stdout.

func processGroomTasks

Go
func processGroomTasks(tasks []map[string]any, now time.Time, graph *logseq.Graph, api api.LogseqAPI, backlogConfig *backlog.Config, pbUpdater func(recordID string, groomedAt time.Time) error) groom.Counts

processGroomTasks presents tasks one at a time in a plain scrolling terminal loop. Each task card is printed, the user presses a key, the result is printed, then the next task scrolls into view. No alternate screen — every action is permanently visible.

func readKey

Go
func readKey() (string, error)

readKey reads a single keypress from /dev/tty without requiring Enter.

func resolveBacklogPage

Go
func resolveBacklogPage(graphPath, shortName string) string

resolveBacklogPage maps a short backlog name (e.g. "self") to its full page title (e.g. "Backlogs/self") by reading the backlog config page from the graph. Falls back to the short name if the config cannot be read or the name is not found.

func runDashboard

Go
func runDashboard(cmd *cobra.Command, _ []string) error

func runGroomWith

Go
func runGroomWith(now time.Time, olderThan string, limit int)

runGroomWith is the testable core of runGroom.

func runSyncPipeline

Go
func runSyncPipeline(graph *logseq.Graph, logseqAPI logseqapi.LogseqAPI, pbClient *pocketbase.Client, currentTime func() time.Time) error

func runSyncWith

Go
func runSyncWith(currentTime func() time.Time, initFlag bool)

runSyncWith is the testable core of runSync.

func startHTTPServer

Go
func startHTTPServer(ctx context.Context, port int, mux http.Handler) error

startHTTPServer runs the server until a signal arrives or a listen error occurs.

func terminalWidth

Go
func terminalWidth() int

terminalWidth returns the current terminal width, falling back to groomDefaultTermWidth.

func updateGroomCounts

Go
func updateGroomCounts(counts *groom.Counts, actionName string)

updateGroomCounts increments the appropriate counter for the given action name.

type GroomDependencies

GroomDependencies holds all injectable dependencies for the groom command. This enables unit testing without connecting to PocketBase, Logseq, or a terminal.

Go
type GroomDependencies struct {
    TimeNow func() time.Time
}

type MdDependencies

MdDependencies holds all the dependencies for the md command.

Go
type MdDependencies struct {
    InsertFn  func(*internal.InsertMarkdownOptions) error
    OpenGraph func(string) *logseq.Graph
    ReadStdin func() string
    TimeNow   func() time.Time
}

type SyncDependencies

SyncDependencies holds all injectable dependencies for the sync command. This enables unit testing without connecting to PocketBase or Logseq.

Go
type SyncDependencies struct {
    TimeNow func() time.Time
}

type TaskAddDependencies

TaskAddDependencies holds all the dependencies for the task add command.

Go
type TaskAddDependencies struct {
    AddTaskFn func(*logseqext.AddTaskOptions) error
    OpenGraph func(string) *logseq.Graph
    TimeNow   func() time.Time
}

Generated by gomarkdoc