Skip to content
Open
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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@
*obj/
*.DS_Store
docker-compose.yml
cmd/dist/*
cmd/dist/*
src/core
tests/*
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"ASPNETCORE_ENVIRONMENT": "Development",
"SYNOLOGY_SUPPORT": "false",
"SYNOLOGY_SIZE": "SM",
"DATA_DIR": "../tests/posts",
"DATA_DIR": "../tests",
"CONFIG_DIR": "/tmp",
"DOMAIN": "localhost:8080",
"PASSWORD": "",
Expand Down
3 changes: 2 additions & 1 deletion cmd/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ require (
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/sys v0.1.0 // indirect
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
2 changes: 2 additions & 0 deletions cmd/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,6 @@ golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
216 changes: 216 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
package main

import (
"bufio"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"sort"
"strings"
"sync"
"time"

"github.com/manifoldco/promptui"
"github.com/spf13/cobra"
"gopkg.in/yaml.v2"
)

var (
Expand All @@ -29,6 +33,218 @@ type BlogPost struct {
Draft bool
}

type PostMeta struct {
Title string `yaml:"title"`
Date string `yaml:"date"`
Public bool `yaml:"public"`
Draft bool `yaml:"draft"`
}

type SearchResult struct {
Path string
Filename string
Date time.Time // Add explicit date field for searching
Meta PostMeta
}

func (s SearchResult) String() string {
return fmt.Sprintf("%s (%s) - %s", s.Filename, s.Date.Format("2006-01-02"), s.Meta.Title)
}

func init() {
searchCmd := &cobra.Command{
Use: "search",
Short: "Search blog posts by filename, title, or date",
RunE: searchPosts,
}
rootCmd.AddCommand(searchCmd)
}

func searchPosts(cmd *cobra.Command, args []string) error {
results, err := findAllPosts(BaseDir)
if err != nil {
return err
}

// Sort results by date descending
sort.Slice(results, func(i, j int) bool {
return results[i].Date.After(results[j].Date)
})

searcher := &promptui.Select{
Label: "Search posts (type to filter by title, filename, or date)",
Items: results,
Size: 15,
Templates: &promptui.SelectTemplates{
Label: "{{ . | cyan }}",
Active: "\u279C {{ .Filename | cyan }} ({{ .Date.Format \"2006-01-02\" }}) - {{ .Meta.Title }}",
Inactive: " {{ .Filename | white }} ({{ .Date.Format \"2006-01-02\" }}) - {{ .Meta.Title }}",
Selected: "\u2713 {{ .Filename | green }} ({{ .Date.Format \"2006-01-02\" }}) - {{ .Meta.Title }}",
Details: `
{{ "File:" | faint }} {{ .Filename }}
{{ "Date:" | faint }} {{ .Date.Format "2006-01-02" }}
{{ "Title:" | faint }} {{ .Meta.Title }}
{{ "Path:" | faint }} {{ .Path }}
{{ "Draft:" | faint }} {{ .Meta.Draft }}
{{ "Public:" | faint }} {{ .Meta.Public }}`,
},
Keys: &promptui.SelectKeys{
Prev: promptui.Key{Code: 107, Display: "k"}, // k key
Next: promptui.Key{Code: 106, Display: "j"}, // j key
PageUp: promptui.Key{Code: 2, Display: "b"}, // ctrl+b
PageDown: promptui.Key{Code: 6, Display: "f"}, // ctrl+f
},
Searcher: func(input string, index int) bool {
result := results[index]

// Convert everything to lowercase for case-insensitive search
title := strings.ToLower(result.Meta.Title)
filename := strings.ToLower(result.Filename)
date := result.Date.Format("2006-01-02")
searchInput := strings.ToLower(input)

// Search in title, filename, and date
return strings.Contains(title, searchInput) ||
strings.Contains(filename, searchInput) ||
strings.Contains(date, searchInput)
},
}

index, _, err := searcher.Run()
if err != nil {
if err == promptui.ErrInterrupt || err == promptui.ErrAbort {
fmt.Println("Search cancelled")
return nil
}
return err
}

selected := results[index]
dir := filepath.Dir(selected.Path)

// First open the directory
if err := openFile(dir); err != nil {
return fmt.Errorf("error opening directory: %v", err)
}

// Then open the file
return openFile(selected.Path)
}

func findAllPosts(root string) ([]SearchResult, error) {
var results []SearchResult
resultChan := make(chan SearchResult)
doneChan := make(chan bool)

// Use a semaphore to limit concurrent goroutines
sem := make(chan struct{}, runtime.NumCPU())
var wg sync.WaitGroup

// Start a goroutine to collect results
go func() {
for result := range resultChan {
results = append(results, result)
}
doneChan <- true
}()

err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}

if !info.IsDir() && strings.HasSuffix(info.Name(), ".md") {
wg.Add(1)
go func(path string, info os.FileInfo) {
defer wg.Done()
sem <- struct{}{} // Acquire semaphore
defer func() { <-sem }() // Release semaphore

meta, err := parseMarkdownFrontMatter(path)
if err != nil {
fmt.Printf("Warning: Could not parse %s: %v\n", path, err)
return
}

// Parse the date from meta
date, err := time.Parse(time.RFC3339[:10], meta.Date)
if err != nil {
// Try parsing with multiple date formats
formats := []string{
"2006-01-02",
"2006-01-02T15:04:05Z",
time.RFC3339,
}

parsed := false
for _, format := range formats {
if date, err = time.Parse(format, meta.Date); err == nil {
parsed = true
break
}
}

if !parsed {
fmt.Printf("Warning: Invalid date format in %s: %v\n", path, err)
date = info.ModTime() // Use file modification time as fallback
}
}

resultChan <- SearchResult{
Path: path,
Filename: info.Name(),
Date: date,
Meta: meta,
}
}(path, info)
}
return nil
})

// Wait for all goroutines to complete
wg.Wait()
close(resultChan)
<-doneChan

return results, err
}

func parseMarkdownFrontMatter(filepath string) (PostMeta, error) {
var meta PostMeta
file, err := os.Open(filepath)
if err != nil {
return meta, err
}
defer file.Close()

scanner := bufio.NewScanner(file)
var frontMatter strings.Builder
inFrontMatter := false
yamlStart := false

for scanner.Scan() {
line := scanner.Text()
if line == "---" {
if !inFrontMatter {
inFrontMatter = true
yamlStart = true
continue
} else {
break
}
}
if inFrontMatter && yamlStart {
frontMatter.WriteString(line + "\n")
}
}

if err := yaml.Unmarshal([]byte(frontMatter.String()), &meta); err != nil {
return meta, err
}

return meta, nil
}

func createMarkdown(file string, post BlogPost) error {
publicText := "true"
if !post.Public {
Expand Down
6 changes: 5 additions & 1 deletion src/Controllers/HomeController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly MonitorLoop _monitorLoop;

public HomeController(ILogger<HomeController> logger)
public HomeController(ILogger<HomeController> logger, MonitorLoop monitorLoop)
{
_logger = logger;
_monitorLoop = monitorLoop;
}

[AllowAnonymous]
[Route("/login")]
public IActionResult Login(string returnUrl = null)

Check warning on line 17 in src/Controllers/HomeController.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

Cannot convert null literal to non-nullable reference type.
{
if (User.Claims.Any())
return Redirect("/");
Expand Down Expand Up @@ -84,6 +86,8 @@
[Route("")]
public IActionResult Index()
{
_monitorLoop.Execute();

ViewBag.Home = "class = active";
return View(Cache.Models.OrderByDescending(f => f.Date));
}
Expand Down
Loading
Loading