From d1ecb5af702206dafaba311766b1b98e3306f481 Mon Sep 17 00:00:00 2001 From: purian23 Date: Mon, 19 Jan 2026 20:19:15 -0500 Subject: [PATCH] Chroma core - syntax & markdown previews --- core/cmd/dms/commands_chroma.go | 193 +++++++ core/cmd/dms/commands_common.go | 1 + core/go.mod | 4 + core/go.sum | 44 +- .../Modules/Notepad/NotepadTextEditor.qml | 498 +++++++++++++----- 5 files changed, 584 insertions(+), 156 deletions(-) create mode 100644 core/cmd/dms/commands_chroma.go diff --git a/core/cmd/dms/commands_chroma.go b/core/cmd/dms/commands_chroma.go new file mode 100644 index 00000000..b7e601c3 --- /dev/null +++ b/core/cmd/dms/commands_chroma.go @@ -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) + } +} diff --git a/core/cmd/dms/commands_common.go b/core/cmd/dms/commands_common.go index e1e8ce9d..68ba1f66 100644 --- a/core/cmd/dms/commands_common.go +++ b/core/cmd/dms/commands_common.go @@ -517,5 +517,6 @@ func getCommonCommands() []*cobra.Command { clipboardCmd, doctorCmd, configCmd, + chromaCmd, } } diff --git a/core/go.mod b/core/go.mod index f9fd8477..d3aac359 100644 --- a/core/go.mod +++ b/core/go.mod @@ -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 ) diff --git a/core/go.sum b/core/go.sum index 3a3a5610..6abf1234 100644 --- a/core/go.sum +++ b/core/go.sum @@ -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= diff --git a/quickshell/Modules/Notepad/NotepadTextEditor.qml b/quickshell/Modules/Notepad/NotepadTextEditor.qml index fed8e2e7..1e02e92a 100644 --- a/quickshell/Modules/Notepad/NotepadTextEditor.qml +++ b/quickshell/Modules/Notepad/NotepadTextEditor.qml @@ -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(); } } }