1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 13:32:50 -05:00

Compare commits

...

1 Commits

Author SHA1 Message Date
purian23
d1ecb5af70 Chroma core - syntax & markdown previews 2026-01-19 20:19:15 -05:00
5 changed files with 584 additions and 156 deletions

View File

@@ -0,0 +1,193 @@
package main
import (
"bytes"
"fmt"
"io"
"os"
"strings"
"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/formatters/html"
"github.com/alecthomas/chroma/v2/lexers"
"github.com/alecthomas/chroma/v2/styles"
"github.com/spf13/cobra"
"github.com/yuin/goldmark"
highlighting "github.com/yuin/goldmark-highlighting/v2"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
ghtml "github.com/yuin/goldmark/renderer/html"
)
var (
chromaLanguage string
chromaStyle string
chromaInline bool
chromaMarkdown bool
)
var chromaCmd = &cobra.Command{
Use: "chroma [file]",
Short: "Syntax highlight source code",
Long: `Generate syntax-highlighted HTML from source code.
Reads from file or stdin, outputs HTML with syntax highlighting.
Language is auto-detected from filename or can be specified with --language.
Examples:
dms chroma main.go
dms chroma --language python script.py
echo "def foo(): pass" | dms chroma -l python
cat code.rs | dms chroma -l rust --style dracula
dms chroma --markdown README.md
dms chroma --markdown --style github-dark notes.md
dms chroma list-languages
dms chroma list-styles`,
Args: cobra.MaximumNArgs(1),
Run: runChroma,
}
var chromaListLanguagesCmd = &cobra.Command{
Use: "list-languages",
Short: "List all supported languages",
Run: func(cmd *cobra.Command, args []string) {
for _, name := range lexers.Names(true) {
fmt.Println(name)
}
},
}
var chromaListStylesCmd = &cobra.Command{
Use: "list-styles",
Short: "List all available color styles",
Run: func(cmd *cobra.Command, args []string) {
for _, name := range styles.Names() {
fmt.Println(name)
}
},
}
func init() {
chromaCmd.Flags().StringVarP(&chromaLanguage, "language", "l", "", "Language for highlighting (auto-detect if not specified)")
chromaCmd.Flags().StringVarP(&chromaStyle, "style", "s", "monokai", "Color style (monokai, dracula, github, etc.)")
chromaCmd.Flags().BoolVar(&chromaInline, "inline", false, "Output inline styles instead of CSS classes")
chromaCmd.Flags().BoolVarP(&chromaMarkdown, "markdown", "m", false, "Render markdown with syntax-highlighted code blocks")
chromaCmd.AddCommand(chromaListLanguagesCmd)
chromaCmd.AddCommand(chromaListStylesCmd)
}
func runChroma(cmd *cobra.Command, args []string) {
var source string
var filename string
// Read from file or stdin
if len(args) > 0 {
filename = args[0]
content, err := os.ReadFile(filename)
if err != nil {
fmt.Fprintf(os.Stderr, "Error reading file: %v\n", err)
os.Exit(1)
}
source = string(content)
} else {
content, err := io.ReadAll(os.Stdin)
if err != nil {
fmt.Fprintf(os.Stderr, "Error reading stdin: %v\n", err)
os.Exit(1)
}
source = string(content)
}
// Handle empty input
if strings.TrimSpace(source) == "" {
return
}
// Handle Markdown rendering
if chromaMarkdown {
md := goldmark.New(
goldmark.WithExtensions(
extension.GFM,
highlighting.NewHighlighting(
highlighting.WithStyle(chromaStyle),
highlighting.WithFormatOptions(
html.WithClasses(!chromaInline),
),
),
),
goldmark.WithParserOptions(
parser.WithAutoHeadingID(),
),
goldmark.WithRendererOptions(
ghtml.WithHardWraps(),
ghtml.WithXHTML(),
),
)
var buf bytes.Buffer
if err := md.Convert([]byte(source), &buf); err != nil {
fmt.Fprintf(os.Stderr, "Markdown rendering error: %v\n", err)
os.Exit(1)
}
fmt.Print(buf.String())
return
}
// Detect or use specified lexer
var lexer chroma.Lexer
if chromaLanguage != "" {
lexer = lexers.Get(chromaLanguage)
if lexer == nil {
fmt.Fprintf(os.Stderr, "Unknown language: %s\n", chromaLanguage)
os.Exit(1)
}
} else if filename != "" {
lexer = lexers.Match(filename)
}
// Try content analysis if no lexer found
if lexer == nil {
lexer = lexers.Analyse(source)
}
// Fallback to plaintext
if lexer == nil {
lexer = lexers.Fallback
}
lexer = chroma.Coalesce(lexer)
// Get style
style := styles.Get(chromaStyle)
if style == nil {
style = styles.Fallback
}
// Create HTML formatter
var formatter *html.Formatter
if chromaInline {
formatter = html.New(
html.WithClasses(false),
html.TabWidth(4),
)
} else {
formatter = html.New(
html.WithClasses(true),
html.TabWidth(4),
)
}
// Tokenize
iterator, err := lexer.Tokenise(nil, source)
if err != nil {
fmt.Fprintf(os.Stderr, "Tokenization error: %v\n", err)
os.Exit(1)
}
// Format and output
if err := formatter.Format(os.Stdout, style, iterator); err != nil {
fmt.Fprintf(os.Stderr, "Formatting error: %v\n", err)
os.Exit(1)
}
}

View File

@@ -517,5 +517,6 @@ func getCommonCommands() []*cobra.Command {
clipboardCmd,
doctorCmd,
configCmd,
chromaCmd,
}
}

View File

@@ -4,6 +4,7 @@ go 1.24.6
require (
github.com/Wifx/gonetworkmanager/v2 v2.2.0
github.com/alecthomas/chroma/v2 v2.17.2
github.com/charmbracelet/bubbles v0.21.0
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0
@@ -28,6 +29,7 @@ require (
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
github.com/cloudflare/circl v1.6.2 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/go-git/gcfg/v2 v2.0.2 // indirect
github.com/go-git/go-billy/v6 v6.0.0-20260114122816-19306b749ecc // indirect
@@ -38,6 +40,8 @@ require (
github.com/pjbgf/sha1cd v0.5.0 // indirect
github.com/sergi/go-diff v1.4.0 // indirect
github.com/stretchr/objx v0.5.3 // indirect
github.com/yuin/goldmark v1.7.16 // indirect
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/net v0.49.0 // indirect
)

View File

@@ -4,6 +4,14 @@ github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBi
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
github.com/Wifx/gonetworkmanager/v2 v2.2.0 h1:kPstgsQtY8CmDOOFZd81ytM9Gi3f6ImzPCKF7nNhQ2U=
github.com/Wifx/gonetworkmanager/v2 v2.2.0/go.mod h1:fMDb//SHsKWxyDUAwXvCqurV3npbIyyaQWenGpZ/uXg=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
github.com/alecthomas/chroma/v2 v2.17.2 h1:Rm81SCZ2mPoH+Q8ZCc/9YvzPUN/E7HgPiPJD8SLV6GI=
github.com/alecthomas/chroma/v2 v2.17.2/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk=
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
@@ -24,16 +32,12 @@ github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoF
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
github.com/charmbracelet/x/ansi v0.11.3 h1:6DcVaqWI82BBVM/atTyq6yBoRLZFBsnoDoX9GCu2YOI=
github.com/charmbracelet/x/ansi v0.11.3/go.mod h1:yI7Zslym9tCJcedxz5+WBq+eUGMJT0bM06Fqy1/Y4dI=
github.com/charmbracelet/x/ansi v0.11.4 h1:6G65PLu6HjmE858CnTUQY1LXT3ZUWwfvqEROLF8vqHI=
github.com/charmbracelet/x/ansi v0.11.4/go.mod h1:/5AZ+UfWExW3int5H5ugnsG/PWjNcSQcwYsHBlPFQN4=
github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4=
github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/clipperhouse/displaywidth v0.6.2 h1:ZDpTkFfpHOKte4RG5O/BOyf3ysnvFswpyYrV7z2uAKo=
github.com/clipperhouse/displaywidth v0.6.2/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
github.com/clipperhouse/displaywidth v0.7.0 h1:QNv1GYsnLX9QBrcWUtMlogpTXuM5FVnBwKWp1O5NwmE=
github.com/clipperhouse/displaywidth v0.7.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
@@ -48,6 +52,10 @@ github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
@@ -58,14 +66,10 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo=
github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs=
github.com/go-git/go-billy/v6 v6.0.0-20251217170237-e9738f50a3cd h1:Gd/f9cGi/3h1JOPaa6er+CkKUGyGX2DBJdFbDKVO+R0=
github.com/go-git/go-billy/v6 v6.0.0-20251217170237-e9738f50a3cd/go.mod h1:d3XQcsHu1idnquxt48kAv+h+1MUiYKLH/e7LAzjP+pI=
github.com/go-git/go-billy/v6 v6.0.0-20260114122816-19306b749ecc h1:rhkjrnRkamkRC7woapp425E4CAH6RPcqsS9X8LA93IY=
github.com/go-git/go-billy/v6 v6.0.0-20260114122816-19306b749ecc/go.mod h1:X1oe0Z2qMsa9hkar3AAPuL9hu4Mi3ztXEjdqRhr6fcc=
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20251229094738-4b14af179146 h1:xYfxAopYyL44ot6dMBIb1Z1njFM0ZBQ99HdIB99KxLs=
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20251229094738-4b14af179146/go.mod h1:QE/75B8tBSLNGyUUbA9tw3EGHoFtYOtypa2h8YJxsWI=
github.com/go-git/go-git/v6 v6.0.0-20251231065035-29ae690a9f19 h1:0lz2eJScP8v5YZQsrEw+ggWC5jNySjg4bIZo5BIh6iI=
github.com/go-git/go-git/v6 v6.0.0-20251231065035-29ae690a9f19/go.mod h1:L+Evfcs7EdTqxwv854354cb6+++7TFL3hJn3Wy4g+3w=
github.com/go-git/go-git/v6 v6.0.0-20260114124804-a8db3a6585a6 h1:Yo1MlE8LpvD0pr7mZ04b6hKZKQcPvLrQFgyY1jNMEyU=
github.com/go-git/go-git/v6 v6.0.0-20260114124804-a8db3a6585a6/go.mod h1:enMzPHv+9hL4B7tH7OJGQKNzCkMzXovUoaiXfsLF7Xs=
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
@@ -78,6 +82,8 @@ github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUv
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83 h1:B+A58zGFuDrvEZpPN+yS6swJA0nzqgZvDzgl/OPyefU=
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83/go.mod h1:iHAf8OIncO2gcQ8XOjS7CMJ2aPbX2Bs0wl5pZyanEqk=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
@@ -133,42 +139,35 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8=
golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU=
golang.org/x/image v0.35.0 h1:LKjiHdgMtO8z7Fh18nGY6KDcoEtVfsgLDPeLyguqb7I=
golang.org/x/image v0.35.0/go.mod h1:MwPLTVgvxSASsxdLzKrl8BRFuyqMyGhLwmC+TO1Sybk=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -177,5 +176,6 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -1,3 +1,4 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
@@ -6,8 +7,6 @@ import qs.Common
import qs.Services
import qs.Widgets
pragma ComponentBehavior: Bound
Column {
id: root
@@ -22,55 +21,181 @@ Column {
property int currentMatchIndex: -1
property int matchCount: 0
signal saveRequested()
signal openRequested()
signal newRequested()
signal escapePressed()
signal contentChanged()
signal settingsRequested()
// Plugin-provided markdown/syntax highlighting (via builtInPluginSettings)
property bool pluginInstalled: SettingsData.getBuiltInPluginSetting("dankNotepadMarkdown", "enabled", false)
property bool pluginMarkdownEnabled: SettingsData.getBuiltInPluginSetting("dankNotepadMarkdown", "markdownPreview", false)
property bool pluginSyntaxEnabled: SettingsData.getBuiltInPluginSetting("dankNotepadMarkdown", "syntaxHighlighting", false)
property string pluginHighlightedHtml: SettingsData.getBuiltInPluginSetting("dankNotepadMarkdown", "highlightedHtml", "")
property string pluginFileExtension: SettingsData.getBuiltInPluginSetting("dankNotepadMarkdown", "currentFileExtension", "")
// Local toggle for markdown preview (can be toggled from UI)
property bool markdownPreviewActive: pluginMarkdownEnabled
// Toggle markdown preview
function toggleMarkdownPreview() {
if (!markdownPreviewActive) {
// Entering preview mode
syncContentToPlugin();
markdownPreviewActive = true;
} else {
// Exiting preview mode
markdownPreviewActive = false;
}
}
// Local toggle for syntax highlighting preview (read-only view with colors)
property bool syntaxPreviewActive: false
// Store original text when entering syntax preview mode
property string syntaxPreviewOriginalText: ""
// Function to refresh plugin settings (called from Connections inside TextArea)
function refreshPluginSettings() {
pluginInstalled = SettingsData.getBuiltInPluginSetting("dankNotepadMarkdown", "enabled", false);
pluginMarkdownEnabled = SettingsData.getBuiltInPluginSetting("dankNotepadMarkdown", "markdownPreview", false);
pluginSyntaxEnabled = SettingsData.getBuiltInPluginSetting("dankNotepadMarkdown", "syntaxHighlighting", false);
pluginHighlightedHtml = SettingsData.getBuiltInPluginSetting("dankNotepadMarkdown", "highlightedHtml", "");
pluginFileExtension = SettingsData.getBuiltInPluginSetting("dankNotepadMarkdown", "currentFileExtension", "");
console.warn("NotepadTextEditor: Plugin settings refreshed. MdEnabled:", pluginMarkdownEnabled, "HtmlLength:", pluginHighlightedHtml.length);
}
// Toggle syntax preview mode
function toggleSyntaxPreview() {
if (!syntaxPreviewActive) {
// Entering preview mode
syncContentToPlugin();
syntaxPreviewActive = true;
} else {
// Exiting preview mode
syntaxPreviewActive = false;
}
}
// File extension detection for current tab
readonly property string currentFilePath: currentTab?.filePath || ""
readonly property string currentFileExtension: {
if (!currentFilePath)
return "";
var parts = currentFilePath.split('.');
return parts.length > 1 ? parts[parts.length - 1].toLowerCase() : "";
}
onCurrentTabChanged: handleCurrentTabChanged()
Component.onCompleted: handleCurrentTabChanged()
function handleCurrentTabChanged() {
if (!currentTab)
return;
// Reset preview state ONLY when tab actually changes
markdownPreviewActive = false;
syntaxPreviewActive = false;
syntaxPreviewOriginalText = "";
textArea.readOnly = false;
syncContentToPlugin();
}
function syncContentToPlugin() {
if (!currentTab)
return;
// Notify plugin of content update
// console.warn("NotepadTextEditor: Pushing content to plugin. Length:", textArea.text.length, "Path:", currentFilePath);
SettingsData.setBuiltInPluginSetting("dankNotepadMarkdown", "currentFilePath", currentFilePath);
SettingsData.setBuiltInPluginSetting("dankNotepadMarkdown", "currentFileExtension", currentFileExtension);
SettingsData.setBuiltInPluginSetting("dankNotepadMarkdown", "sourceContent", textArea.text);
SettingsData.setBuiltInPluginSetting("dankNotepadMarkdown", "currentTabChanged", Date.now());
}
// Debounce content updates to plugin to keep preview ready
Timer {
id: syncTimer
interval: 500
repeat: false
onTriggered: syncContentToPlugin()
}
Connections {
target: textArea
function onTextChanged() {
if (!markdownPreviewActive && !syntaxPreviewActive) {
syncTimer.restart();
}
}
}
readonly property string fileExtension: {
if (!currentFilePath)
return "";
var parts = currentFilePath.split('.');
return parts.length > 1 ? parts[parts.length - 1].toLowerCase() : "";
}
readonly property bool isMarkdownFile: fileExtension === "md" || fileExtension === "markdown" || fileExtension === "mdown"
readonly property bool isCodeFile: fileExtension !== "" && fileExtension !== "txt" && !isMarkdownFile
signal saveRequested
signal openRequested
signal newRequested
signal escapePressed
signal contentChanged
signal settingsRequested
function hasUnsavedChanges() {
if (!currentTab || !contentLoaded) {
return false
return false;
}
if (currentTab.isTemporary) {
return textArea.text.length > 0
return textArea.text.length > 0;
}
return textArea.text !== lastSavedContent
// If in preview mode, compare original text
if (markdownPreviewActive || syntaxPreviewActive) {
return syntaxPreviewOriginalText !== lastSavedContent;
}
return textArea.text !== lastSavedContent;
}
function loadCurrentTabContent() {
if (!currentTab) return
if (!currentTab)
return;
contentLoaded = false;
// Reset preview states on load
markdownPreviewActive = false;
syntaxPreviewActive = false;
syntaxPreviewOriginalText = "";
contentLoaded = false
NotepadStorageService.loadTabContent(
NotepadStorageService.currentTabIndex,
(content) => {
lastSavedContent = content
textArea.text = content
contentLoaded = true
}
)
NotepadStorageService.loadTabContent(NotepadStorageService.currentTabIndex, content => {
lastSavedContent = content;
textArea.text = content;
contentLoaded = true;
textArea.readOnly = false;
});
}
function saveCurrentTabContent() {
if (!currentTab || !contentLoaded) return
if (!currentTab || !contentLoaded)
return;
NotepadStorageService.saveTabContent(
NotepadStorageService.currentTabIndex,
textArea.text
)
lastSavedContent = textArea.text
// If in preview mode, save the original text, NOT the HTML
var contentToSave = (markdownPreviewActive || syntaxPreviewActive) ? syntaxPreviewOriginalText : textArea.text;
NotepadStorageService.saveTabContent(NotepadStorageService.currentTabIndex, contentToSave);
lastSavedContent = contentToSave;
}
function autoSaveToSession() {
if (!currentTab || !contentLoaded) return
saveCurrentTabContent()
if (!currentTab || !contentLoaded)
return;
saveCurrentTabContent();
}
function setTextDocumentLineHeight() {
return
return;
}
property string lastTextForLineModel: ""
@@ -78,100 +203,102 @@ Column {
function updateLineModel() {
if (!SettingsData.notepadShowLineNumbers) {
lineModel = []
lastTextForLineModel = ""
return
lineModel = [];
lastTextForLineModel = "";
return;
}
// In preview mode, line numbers might not match visual lines correctly due to wrapping/HTML
// But for now let's use the current text (plain or HTML)
if (textArea.text !== lastTextForLineModel || lineModel.length === 0) {
lastTextForLineModel = textArea.text
lineModel = textArea.text.split('\n')
lastTextForLineModel = textArea.text;
lineModel = textArea.text.split('\n');
}
}
function performSearch() {
let matches = []
currentMatchIndex = -1
let matches = [];
currentMatchIndex = -1;
if (!searchQuery || searchQuery.length === 0) {
searchMatches = []
matchCount = 0
textArea.select(0, 0)
return
searchMatches = [];
matchCount = 0;
textArea.select(0, 0);
return;
}
const text = textArea.text
const query = searchQuery.toLowerCase()
let index = 0
const text = textArea.text;
const query = searchQuery.toLowerCase();
let index = 0;
while (index < text.length) {
const foundIndex = text.toLowerCase().indexOf(query, index)
if (foundIndex === -1) break
const foundIndex = text.toLowerCase().indexOf(query, index);
if (foundIndex === -1)
break;
matches.push({
start: foundIndex,
end: foundIndex + searchQuery.length
})
index = foundIndex + 1
});
index = foundIndex + 1;
}
searchMatches = matches
matchCount = matches.length
searchMatches = matches;
matchCount = matches.length;
if (matchCount > 0) {
currentMatchIndex = 0
highlightCurrentMatch()
currentMatchIndex = 0;
highlightCurrentMatch();
} else {
textArea.select(0, 0)
textArea.select(0, 0);
}
}
function highlightCurrentMatch() {
if (currentMatchIndex >= 0 && currentMatchIndex < searchMatches.length) {
const match = searchMatches[currentMatchIndex]
const match = searchMatches[currentMatchIndex];
textArea.cursorPosition = match.start
textArea.moveCursorSelection(match.end, TextEdit.SelectCharacters)
textArea.cursorPosition = match.start;
textArea.moveCursorSelection(match.end, TextEdit.SelectCharacters);
const flickable = textArea.parent
const flickable = textArea.parent;
if (flickable && flickable.contentY !== undefined) {
const lineHeight = textArea.font.pixelSize * 1.5
const approxLine = textArea.text.substring(0, match.start).split('\n').length
const targetY = approxLine * lineHeight - flickable.height / 2
flickable.contentY = Math.max(0, Math.min(targetY, flickable.contentHeight - flickable.height))
const lineHeight = textArea.font.pixelSize * 1.5;
const approxLine = textArea.text.substring(0, match.start).split('\n').length;
const targetY = approxLine * lineHeight - flickable.height / 2;
flickable.contentY = Math.max(0, Math.min(targetY, flickable.contentHeight - flickable.height));
}
}
}
function findNext() {
if (matchCount === 0 || searchMatches.length === 0) return
currentMatchIndex = (currentMatchIndex + 1) % matchCount
highlightCurrentMatch()
if (matchCount === 0 || searchMatches.length === 0)
return;
currentMatchIndex = (currentMatchIndex + 1) % matchCount;
highlightCurrentMatch();
}
function findPrevious() {
if (matchCount === 0 || searchMatches.length === 0) return
currentMatchIndex = currentMatchIndex <= 0 ? matchCount - 1 : currentMatchIndex - 1
highlightCurrentMatch()
if (matchCount === 0 || searchMatches.length === 0)
return;
currentMatchIndex = currentMatchIndex <= 0 ? matchCount - 1 : currentMatchIndex - 1;
highlightCurrentMatch();
}
function showSearch() {
searchVisible = true
searchVisible = true;
Qt.callLater(() => {
searchField.forceActiveFocus()
})
searchField.forceActiveFocus();
});
}
function hideSearch() {
searchVisible = false
searchQuery = ""
searchMatches = []
matchCount = 0
currentMatchIndex = -1
textArea.select(0, 0)
textArea.forceActiveFocus()
searchVisible = false;
searchQuery = "";
searchMatches = [];
matchCount = 0;
currentMatchIndex = -1;
textArea.select(0, 0);
textArea.forceActiveFocus();
}
spacing: Theme.spacingM
@@ -221,43 +348,43 @@ Column {
clip: true
Component.onCompleted: {
text = root.searchQuery
text = root.searchQuery;
}
Connections {
target: root
function onSearchQueryChanged() {
if (searchField.text !== root.searchQuery) {
searchField.text = root.searchQuery
searchField.text = root.searchQuery;
}
}
}
onTextChanged: {
if (root.searchQuery !== text) {
root.searchQuery = text
root.performSearch()
root.searchQuery = text;
root.performSearch();
}
}
Keys.onEscapePressed: event => {
root.hideSearch()
event.accepted = true
root.hideSearch();
event.accepted = true;
}
Keys.onReturnPressed: event => {
if (event.modifiers & Qt.ShiftModifier) {
root.findPrevious()
root.findPrevious();
} else {
root.findNext()
root.findNext();
}
event.accepted = true
event.accepted = true;
}
Keys.onEnterPressed: event => {
if (event.modifiers & Qt.ShiftModifier) {
root.findPrevious()
root.findPrevious();
} else {
root.findNext()
root.findNext();
}
event.accepted = true
event.accepted = true;
}
}
@@ -325,6 +452,7 @@ Column {
DankFlickable {
id: flickable
visible: !root.markdownPreviewActive && !root.syntaxPreviewActive
anchors.fill: parent
anchors.margins: 1
clip: true
@@ -397,6 +525,7 @@ Column {
focus: true
activeFocusOnTab: true
textFormat: TextEdit.PlainText
// readOnly: root.syntaxPreviewActive || root.markdownPreviewActive // Handled by visibility now
inputMethodHints: Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase
persistentSelection: true
tabStopDistance: 40
@@ -416,31 +545,45 @@ Column {
SequentialAnimation on opacity {
running: textArea.activeFocus
loops: Animation.Infinite
PropertyAnimation { from: 1.0; to: 0.0; duration: 650; easing.type: Easing.InOutQuad }
PropertyAnimation { from: 0.0; to: 1.0; duration: 650; easing.type: Easing.InOutQuad }
PropertyAnimation {
from: 1.0
to: 0.0
duration: 650
easing.type: Easing.InOutQuad
}
PropertyAnimation {
from: 0.0
to: 1.0
duration: 650
easing.type: Easing.InOutQuad
}
}
}
Component.onCompleted: {
loadCurrentTabContent()
setTextDocumentLineHeight()
root.updateLineModel()
loadCurrentTabContent();
setTextDocumentLineHeight();
root.updateLineModel();
Qt.callLater(() => {
textArea.forceActiveFocus()
})
textArea.forceActiveFocus();
});
}
Connections {
target: NotepadStorageService
function onCurrentTabIndexChanged() {
loadCurrentTabContent()
// Exit syntax preview mode when switching tabs
if (root.syntaxPreviewActive) {
root.syntaxPreviewActive = false;
}
loadCurrentTabContent();
Qt.callLater(() => {
textArea.forceActiveFocus()
})
textArea.forceActiveFocus();
});
}
function onTabsChanged() {
if (NotepadStorageService.tabs.length > 0 && !contentLoaded) {
loadCurrentTabContent()
loadCurrentTabContent();
}
}
}
@@ -448,46 +591,49 @@ Column {
Connections {
target: SettingsData
function onNotepadShowLineNumbersChanged() {
root.updateLineModel()
root.updateLineModel();
}
function onBuiltInPluginSettingsChanged() {
root.refreshPluginSettings();
}
}
onTextChanged: {
if (contentLoaded && text !== lastSavedContent) {
autoSaveTimer.restart()
autoSaveTimer.restart();
}
root.contentChanged()
root.updateLineModel()
root.contentChanged();
root.updateLineModel();
}
Keys.onEscapePressed: (event) => {
root.escapePressed()
event.accepted = true
Keys.onEscapePressed: event => {
root.escapePressed();
event.accepted = true;
}
Keys.onPressed: (event) => {
Keys.onPressed: event => {
if (event.modifiers & Qt.ControlModifier) {
switch (event.key) {
case Qt.Key_S:
event.accepted = true
root.saveRequested()
break
event.accepted = true;
root.saveRequested();
break;
case Qt.Key_O:
event.accepted = true
root.openRequested()
break
event.accepted = true;
root.openRequested();
break;
case Qt.Key_N:
event.accepted = true
root.newRequested()
break
event.accepted = true;
root.newRequested();
break;
case Qt.Key_A:
event.accepted = true
selectAll()
break
event.accepted = true;
selectAll();
break;
case Qt.Key_F:
event.accepted = true
root.showSearch()
break
event.accepted = true;
root.showSearch();
break;
}
}
}
@@ -495,6 +641,11 @@ Column {
background: Rectangle {
color: "transparent"
}
// Make links clickable in markdown preview mode
onLinkActivated: link => {
Qt.openUrlExternally(link);
}
}
StyledText {
@@ -511,6 +662,47 @@ Column {
z: textArea.z + 1
}
}
// Dedicated Flickable for Preview Mode
DankFlickable {
id: previewFlickable
visible: root.markdownPreviewActive || root.syntaxPreviewActive
anchors.fill: parent
anchors.margins: 1
clip: true
contentWidth: width - 11
TextArea.flickable: TextArea {
id: previewAreaReal
text: root.pluginHighlightedHtml
textFormat: TextEdit.RichText
readOnly: true
// Copy styling from main textArea
placeholderText: ""
font.family: SettingsData.notepadUseMonospace ? SettingsData.monoFontFamily : (SettingsData.notepadFontFamily || SettingsData.fontFamily)
font.pixelSize: SettingsData.notepadFontSize * SettingsData.fontScale
font.letterSpacing: 0
color: Theme.surfaceText
selectedTextColor: Theme.background
selectionColor: Theme.primary
selectByMouse: true
selectByKeyboard: true
wrapMode: TextArea.Wrap
focus: true
activeFocusOnTab: true
leftPadding: Theme.spacingM
topPadding: Theme.spacingM
rightPadding: Theme.spacingM
bottomPadding: Theme.spacingM
// Make links clickable
onLinkActivated: link => {
Qt.openUrlExternally(link);
}
}
}
}
Column {
@@ -575,6 +767,44 @@ Column {
color: Theme.surfaceTextMedium
}
}
// Markdown preview toggle (only visible when plugin installed and viewing .md file)
Row {
spacing: Theme.spacingS
visible: root.pluginInstalled && root.isMarkdownFile
DankActionButton {
iconName: root.markdownPreviewActive ? "visibility" : "visibility_off"
iconSize: Theme.iconSize - 2
iconColor: root.markdownPreviewActive ? Theme.primary : Theme.surfaceTextMedium
onClicked: root.toggleMarkdownPreview()
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("Preview")
font.pixelSize: Theme.fontSizeSmall
color: root.markdownPreviewActive ? Theme.primary : Theme.surfaceTextMedium
}
}
// Syntax highlighting toggle (only visible when plugin installed and viewing code file)
Row {
spacing: Theme.spacingS
visible: root.pluginInstalled && root.pluginSyntaxEnabled && root.isCodeFile
DankActionButton {
iconName: root.syntaxPreviewActive ? "code" : "code_off"
iconSize: Theme.iconSize - 2
iconColor: root.syntaxPreviewActive ? Theme.primary : Theme.surfaceTextMedium
onClicked: root.toggleSyntaxPreview()
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: root.syntaxPreviewActive ? I18n.tr("Edit") : I18n.tr("Highlight")
font.pixelSize: Theme.fontSizeSmall
color: root.syntaxPreviewActive ? Theme.primary : Theme.surfaceTextMedium
}
}
}
DankActionButton {
@@ -608,29 +838,29 @@ Column {
StyledText {
text: {
if (autoSaveTimer.running) {
return I18n.tr("Auto-saving...")
return I18n.tr("Auto-saving...");
}
if (hasUnsavedChanges()) {
if (currentTab && currentTab.isTemporary) {
return I18n.tr("Unsaved note...")
return I18n.tr("Unsaved note...");
} else {
return I18n.tr("Unsaved changes")
return I18n.tr("Unsaved changes");
}
} else {
return I18n.tr("Saved")
return I18n.tr("Saved");
}
}
font.pixelSize: Theme.fontSizeSmall
color: {
if (autoSaveTimer.running) {
return Theme.primary
return Theme.primary;
}
if (hasUnsavedChanges()) {
return Theme.warning
return Theme.warning;
} else {
return Theme.success
return Theme.success;
}
}
opacity: textArea.text.length > 0 ? 1.0 : 0.0
@@ -643,7 +873,7 @@ Column {
interval: 2000
repeat: false
onTriggered: {
autoSaveToSession()
autoSaveToSession();
}
}
}