Compare commits

..

9 commits
v0.1.5 ... main

Author SHA1 Message Date
Leon Mika 9f14a895fa Fixed caret position for append transforms
All checks were successful
Build / build (push) Successful in 39s
Release Build / build (push) Successful in 3m59s
2026-05-04 22:00:12 +10:00
Leon Mika 44a5b664a0 Removed UCL processors
All checks were successful
Build / build (push) Successful in 40s
2026-05-04 21:34:39 +10:00
Leon Mika b4cb97f6d0 Another fix to the release
All checks were successful
Build / build (push) Successful in 33s
Release Build / build (push) Successful in 3m40s
2026-05-04 21:18:07 +10:00
Leon Mika d7f2fd7d09 Attempting to fix the release build
Some checks failed
Build / build (push) Has been cancelled
Release Build / build (push) Successful in 3m41s
2026-05-04 21:12:00 +10:00
Leon Mika 7606bf4c06 Updated assets
Some checks failed
Build / build (push) Successful in 38s
Release Build / build (push) Has been cancelled
2026-05-04 20:45:09 +10:00
exe.dev user e221723c1e Keep status bar pinned at bottom regardless of editor content size
All checks were successful
Build / build (push) Successful in 41s
Release Build / build (push) Successful in 1m53s
Co-authored-by: Shelley <shelley@exe.dev>
2026-05-03 22:13:37 +00:00
exe.dev user b2bed26be5 Add Lines: Sort A-Z, Sort Z-A, and Unique processors
Co-authored-by: Shelley <shelley@exe.dev>
2026-05-03 22:11:04 +00:00
Leon Mika 1f61932393 Enabled context menu
All checks were successful
Build / build (push) Successful in 31s
Release Build / build (push) Successful in 1m53s
2026-05-03 14:58:01 +10:00
Leon Mika d841acd457 Added dark mode and fixed the icon
Some checks failed
Build / build (push) Successful in 36s
Release Build / build (push) Has been cancelled
2026-05-03 11:31:55 +10:00
13 changed files with 140 additions and 38 deletions

View file

@ -26,14 +26,14 @@ jobs:
npm install npm install
wails build -clean -platform darwin/arm64 -ldflags "-X main.VersionNumber=`git describe --tags --abbrev=0`" wails build -clean -platform darwin/arm64 -ldflags "-X main.VersionNumber=`git describe --tags --abbrev=0`"
- name: Release - name: Release
uses: https://lmika.dev/actions/wails-release@v1.0.2 uses: https://lmika.dev/actions/wails-release@v1.0.3
with: with:
developer-id-cert-base64: ${{ secrets.MACOS_SIGN_P12 }} developer-id-cert-base64: ${{ secrets.MACOS_SIGN_P12 }}
developer-id-cert-password: ${{ secrets.MACOS_SIGN_PASSWORD }} developer-id-cert-password: ${{ secrets.MACOS_SIGN_PASSWORD }}
notarization-api-key-base64: ${{ secrets.MACOS_NOTARY_KEY }} notarization-api-key-base64: ${{ secrets.MACOS_NOTARY_KEY }}
notarization-api-key-id: ${{ secrets.MACOS_NOTARY_KEY_ID }} notarization-api-key-id: ${{ secrets.MACOS_NOTARY_KEY_ID }}
notarization-api-issuer-id: ${{ secrets.MACOS_NOTARY_ISSUER_ID }} notarization-api-issuer-id: ${{ secrets.MACOS_NOTARY_ISSUER_ID }}
extra-build-flags: '-ldflags "-X main.VersionNumber=${{ forgejo.ref_name }}"' extra-build-flags: -ldflags "-X main.VersionNumber=${{ github.ref_name }}"
s3-bucket: lmika-public-files s3-bucket: lmika-public-files
s3-key: Apps/Dequoter/{version}/{filename} s3-key: Apps/Dequoter/{version}/{filename}
s3-region: ap-southeast-2 s3-region: ap-southeast-2

Binary file not shown.

Before

Width:  |  Height:  |  Size: 539 KiB

After

Width:  |  Height:  |  Size: 404 KiB

Binary file not shown.

View file

@ -6,9 +6,12 @@
.editor-mountpoint { .editor-mountpoint {
flex-grow: 1; flex-grow: 1;
flex-shrink: 1; flex-shrink: 1;
min-height: 0;
overflow: hidden;
} }
.status-bar { .status-bar {
flex-shrink: 0;
height: 2em; height: 2em;
background-color: rgb(245, 245, 245); background-color: rgb(245, 245, 245);
border-top: solid thin rgb(200, 200, 200); border-top: solid thin rgb(200, 200, 200);
@ -142,3 +145,29 @@ dialog#command-dialog option {
option .option-label { option .option-label {
padding: 10px; padding: 10px;
} }
@media (prefers-color-scheme: dark) {
.status-bar {
background-color: rgb(40, 44, 52);
border-top-color: rgb(70, 74, 82);
color: rgb(220, 220, 220);
}
dialog#prompt-dialog,
dialog#command-dialog {
background: rgba(40, 44, 52, 0.55);
border-color: rgba(120, 120, 120, 0.4);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
color: rgb(220, 220, 220);
}
dialog#prompt-dialog .prompt-label {
color: rgba(255, 255, 255, 0.65);
}
dialog#prompt-dialog input,
dialog#command-dialog input,
dialog#command-dialog select {
color: rgb(230, 230, 230);
}
}

View file

@ -3,12 +3,17 @@ import './app.css';
import {basicSetup} from "codemirror"; import {basicSetup} from "codemirror";
import {EditorView, keymap} from "@codemirror/view"; import {EditorView, keymap} from "@codemirror/view";
import {Compartment} from "@codemirror/state";
import {oneDark} from "@codemirror/theme-one-dark";
import {Application} from "@hotwired/stimulus"; import {Application} from "@hotwired/stimulus";
import {textProcessor} from "./services.js"; import {textProcessor} from "./services.js";
import {multiCursorKeymap, commandPalette} from "./cmplugins.js"; import {multiCursorKeymap, commandPalette} from "./cmplugins.js";
import {indentWithTab} from "@codemirror/commands"; import {indentWithTab} from "@codemirror/commands";
const darkMode = window.matchMedia("(prefers-color-scheme: dark)");
const themeCompartment = new Compartment();
import {StatusController} from "./controllers/status_controller.js"; import {StatusController} from "./controllers/status_controller.js";
import {CommandsController} from "./controllers/commands_controller.js"; import {CommandsController} from "./controllers/commands_controller.js";
import {PromptController} from "./controllers/prompt_controller.js"; import {PromptController} from "./controllers/prompt_controller.js";
@ -24,9 +29,16 @@ const view = new EditorView({
EditorView.lineWrapping, EditorView.lineWrapping,
multiCursorKeymap, multiCursorKeymap,
commandPalette, commandPalette,
themeCompartment.of(darkMode.matches ? oneDark : []),
] ]
}) })
darkMode.addEventListener("change", (e) => {
view.dispatch({
effects: themeCompartment.reconfigure(e.matches ? oneDark : []),
});
});
window.Stimulus = Application.start() window.Stimulus = Application.start()
Stimulus.register("commands", CommandsController); Stimulus.register("commands", CommandsController);
Stimulus.register("prompt", PromptController); Stimulus.register("prompt", PromptController);

View file

@ -12,11 +12,19 @@ class TextProcessor {
setCodeMirrorEditor(editor) { setCodeMirrorEditor(editor) {
this._editor = editor; this._editor = editor;
window.runtime.EventsOn("process-text-response", (data) => { window.runtime.EventsOn("process-text-response", (data) => {
const cursorPos = this._editor.state.selection.main.head;
const appendInsertPos = this._appendInsertPos;
this._appendInsertPos = undefined;
let newSelection;
const changes = data.output.map(span => { const changes = data.output.map(span => {
if (span.append) { if (span.append) {
const insertAt = appendInsertPos !== undefined ? appendInsertPos : span.pos + span.len;
if (cursorPos === insertAt) {
newSelection = { anchor: insertAt + span.text.length };
}
return { return {
from: span.pos + span.len, from: insertAt,
to: span.pos + span.len, to: insertAt,
insert: span.text, insert: span.text,
}; };
} else { } else {
@ -27,7 +35,11 @@ class TextProcessor {
}; };
} }
}); });
this._editor.dispatch({ changes: changes }); const transaction = { changes: changes };
if (newSelection) {
transaction.selection = newSelection;
}
this._editor.dispatch(transaction);
}); });
window.runtime.EventsOn("request-text-process", (data) => { window.runtime.EventsOn("request-text-process", (data) => {
this.runTextCommand(data.action); this.runTextCommand(data.action);
@ -44,12 +56,14 @@ class TextProcessor {
let inputs = []; let inputs = [];
if (shouldBeAll) { if (shouldBeAll) {
this._appendInsertPos = this._editor.state.selection.main.head;
inputs.push({ inputs.push({
text: this._editor.state.doc.toString(), text: this._editor.state.doc.toString(),
pos: 0, pos: 0,
len: this._editor.state.doc.length, len: this._editor.state.doc.length,
}); });
} else { } else {
this._appendInsertPos = undefined;
inputs = ranges.map(r => ({ inputs = ranges.map(r => ({
text: this._editor.state.doc.slice(r.from, r.to).toString(), text: this._editor.state.doc.slice(r.from, r.to).toString(),
pos: r.from, pos: r.from,

View file

@ -1,4 +1,6 @@
html {} html {
color-scheme: light dark;
}
body { body {
margin: 0; margin: 0;

2
go.mod
View file

@ -3,6 +3,7 @@ module dequoter
go 1.25 go 1.25
require ( require (
github.com/google/uuid v1.6.0
github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f
github.com/wailsapp/wails/v2 v2.10.2 github.com/wailsapp/wails/v2 v2.10.2
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
@ -49,7 +50,6 @@ require (
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect github.com/gorilla/websocket v1.5.3 // indirect
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
github.com/labstack/echo/v4 v4.13.3 // indirect github.com/labstack/echo/v4 v4.13.3 // indirect

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 404 KiB

View file

@ -60,6 +60,7 @@ func main() {
Bind: []interface{}{ Bind: []interface{}{
app, app,
}, },
EnableDefaultContextMenu: true,
Menu: appMenu, Menu: appMenu,
Mac: &mac.Options{ Mac: &mac.Options{
About: &mac.AboutInfo{ About: &mac.AboutInfo{

13
package-lock.json generated
View file

@ -5,6 +5,7 @@
"packages": { "packages": {
"": { "": {
"dependencies": { "dependencies": {
"@codemirror/theme-one-dark": "^6.1.3",
"@hotwired/stimulus": "^3.2.2", "@hotwired/stimulus": "^3.2.2",
"codemirror": "^6.0.2", "codemirror": "^6.0.2",
"mousetrap": "^1.6.5" "mousetrap": "^1.6.5"
@ -79,6 +80,18 @@
"@marijn/find-cluster-break": "^1.0.0" "@marijn/find-cluster-break": "^1.0.0"
} }
}, },
"node_modules/@codemirror/theme-one-dark": {
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz",
"integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/highlight": "^1.0.0"
}
},
"node_modules/@codemirror/view": { "node_modules/@codemirror/view": {
"version": "6.38.2", "version": "6.38.2",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.2.tgz", "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.2.tgz",

View file

@ -1,5 +1,6 @@
{ {
"dependencies": { "dependencies": {
"@codemirror/theme-one-dark": "^6.1.3",
"@hotwired/stimulus": "^3.2.2", "@hotwired/stimulus": "^3.2.2",
"codemirror": "^6.0.2", "codemirror": "^6.0.2",
"mousetrap": "^1.6.5" "mousetrap": "^1.6.5"

View file

@ -8,11 +8,13 @@ import (
"errors" "errors"
"fmt" "fmt"
"regexp" "regexp"
"sort"
"strconv" "strconv"
"strings" "strings"
"text/template" "text/template"
"github.com/google/uuid"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"ucl.lmika.dev/ucl" "ucl.lmika.dev/ucl"
) )
@ -59,14 +61,25 @@ var TextFilters = map[string]TextProcessor{
return TextFilterResponse{Output: strings.ToLower(input)}, nil return TextFilterResponse{Output: strings.ToLower(input)}, nil
}, },
}, },
"quote": {
Label: "String: Quote",
Filter: func(ctx context.Context, input string) (resp TextFilterResponse, err error) {
return TextFilterResponse{Output: strconv.Quote(input)}, nil
},
},
"unquote": { "unquote": {
Label: "String: Unquote", Label: "String: Unquote",
Filter: func(ctx context.Context, input string) (resp TextFilterResponse, err error) { Filter: func(ctx context.Context, input string) (resp TextFilterResponse, err error) {
out, err := strconv.Unquote(input) endNL := ""
if strings.HasSuffix(input, "\n") {
endNL = "\n"
}
out, err := strconv.Unquote(strings.TrimSpace(input))
if err != nil { if err != nil {
return TextFilterResponse{}, err return TextFilterResponse{}, err
} }
return TextFilterResponse{Output: out}, nil return TextFilterResponse{Output: out + endNL}, nil
}, },
}, },
"join-lines-with-commas": { "join-lines-with-commas": {
@ -175,6 +188,20 @@ var TextFilters = map[string]TextProcessor{
}, },
}, },
"uuid": {
Label: "Generate: UUID v4",
Filter: func(ctx context.Context, input string) (resp TextFilterResponse, err error) {
u, err := uuid.NewRandom()
if err != nil {
return TextFilterResponse{}, fmt.Errorf("failed to generate UUID: %w", err)
}
return TextFilterResponse{
Output: u.String(),
Append: true,
}, nil
},
},
"lines": { "lines": {
Label: "Lines: Count", Label: "Lines: Count",
Analyze: func(ctx context.Context, input string) (result string, err error) { Analyze: func(ctx context.Context, input string) (result string, err error) {
@ -264,19 +291,38 @@ var TextFilters = map[string]TextProcessor{
}, },
}, },
"ucl-evaluate": { "sort-lines-asc": {
Label: "UCL: Evaluate", Label: "Lines: Sort A-Z",
Description: "Evaluates the input as a UCL expression and displays the result in the status bar.", Filter: func(ctx context.Context, input string) (resp TextFilterResponse, err error) {
Analyze: func(ctx context.Context, input string) (result string, err error) { lines := strings.Split(input, "\n")
res, err := uclInstFromContext(ctx).EvalString(ctx, input) sort.Strings(lines)
if err != nil { return TextFilterResponse{Output: strings.Join(lines, "\n")}, nil
if errors.Is(err, ucl.ErrNotConvertable) { },
return "Evaluated successfully. No result.", err },
}
return "", err
}
return fmt.Sprintf("Result: %v", res), nil "sort-lines-desc": {
Label: "Lines: Sort Z-A",
Filter: func(ctx context.Context, input string) (resp TextFilterResponse, err error) {
lines := strings.Split(input, "\n")
sort.Sort(sort.Reverse(sort.StringSlice(lines)))
return TextFilterResponse{Output: strings.Join(lines, "\n")}, nil
},
},
"unique-lines": {
Label: "Lines: Unique",
Filter: func(ctx context.Context, input string) (resp TextFilterResponse, err error) {
lines := strings.Split(input, "\n")
seen := make(map[string]struct{}, len(lines))
dst := make([]string, 0, len(lines))
for _, line := range lines {
if _, ok := seen[line]; ok {
continue
}
seen[line] = struct{}{}
dst = append(dst, line)
}
return TextFilterResponse{Output: strings.Join(dst, "\n")}, nil
}, },
}, },
@ -320,22 +366,6 @@ var TextFilters = map[string]TextProcessor{
}, },
}, },
}, },
"ucl-replace": {
Label: "UCL: Replace",
Description: "Evaluates the input as a UCL expression and replaces it with the result.",
Filter: func(ctx context.Context, input string) (resp TextFilterResponse, err error) {
res, err := uclInstFromContext(ctx).EvalString(ctx, input)
if err != nil {
if errors.Is(err, ucl.ErrNotConvertable) {
return TextFilterResponse{}, err
}
return TextFilterResponse{}, err
}
return TextFilterResponse{Output: fmt.Sprint(res)}, nil
},
},
} }
type strPseudoVar struct { type strPseudoVar struct {