From 81a2634ffa83d2e2ca80a93c4f20e5f5b8e637d7 Mon Sep 17 00:00:00 2001 From: Fmat34 Date: Mon, 4 May 2026 23:19:17 +0200 Subject: [PATCH 01/20] feat: parseArgs splits GUI files from CLI subcommand args --- cmd/cli/root.go | 17 +++++++++++++++++ main.go | 20 ++++++++++++++++++++ main_test.go | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 main_test.go diff --git a/cmd/cli/root.go b/cmd/cli/root.go index b917de2..7e07ed6 100644 --- a/cmd/cli/root.go +++ b/cmd/cli/root.go @@ -38,6 +38,23 @@ func Execute(runGUI func()) { } } +// IsKnownSubcommand reports whether name is a registered Cobra subcommand on +// the root command (or one of its aliases). Used by main.go to disambiguate +// file paths from CLI verbs. +func IsKnownSubcommand(name string) bool { + for _, c := range rootCmd.Commands() { + if c.Name() == name { + return true + } + for _, alias := range c.Aliases { + if alias == name { + return true + } + } + } + return false +} + var guiFunc func() func init() { diff --git a/main.go b/main.go index 0457bf2..c9f75b3 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ import ( "embed" "log" "os" + "strings" "github.com/wailsapp/wails/v2" "github.com/wailsapp/wails/v2/pkg/options" @@ -20,6 +21,25 @@ import ( //go:embed all:frontend/dist var assets embed.FS +// parseArgs splits process args into CLI args (route to Cobra) vs GUI files +// (open as tabs in the GUI). Cobra subcommand or any leading `-` flag → CLI. +// Otherwise everything is treated as a GUI file path. +func parseArgs(args []string) (cliArgs, guiFiles []string) { + if len(args) == 0 { + return nil, nil + } + for _, a := range args { + if strings.HasPrefix(a, "-") { + return args, nil + } + if cli.IsKnownSubcommand(a) { + return args, nil + } + break + } + return nil, args +} + func main() { if len(os.Args) == 1 { runGUI() diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..6bf93fa --- /dev/null +++ b/main_test.go @@ -0,0 +1,33 @@ +package main + +import ( + "reflect" + "testing" +) + +func TestParseArgs(t *testing.T) { + cases := []struct { + name string + args []string + wantCLI []string + wantGUI []string + }{ + {"no args", nil, nil, nil}, + {"cobra subcommand", []string{"pdf", "info", "x.pdf"}, []string{"pdf", "info", "x.pdf"}, nil}, + {"single pdf path", []string{"foo.pdf"}, nil, []string{"foo.pdf"}}, + {"multiple pdf paths", []string{"a.pdf", "b.pdf"}, nil, []string{"a.pdf", "b.pdf"}}, + {"global flag before subcommand", []string{"--verbose", "pdf", "info", "x.pdf"}, []string{"--verbose", "pdf", "info", "x.pdf"}, nil}, + {"dot-slash workaround", []string{"./pdf"}, nil, []string{"./pdf"}}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + cli, gui := parseArgs(tc.args) + if !reflect.DeepEqual(cli, tc.wantCLI) { + t.Errorf("CLI: got %v want %v", cli, tc.wantCLI) + } + if !reflect.DeepEqual(gui, tc.wantGUI) { + t.Errorf("GUI: got %v want %v", gui, tc.wantGUI) + } + }) + } +} From dfb6d1a6e996d6bd156289dc49d44f4f72efe589 Mon Sep 17 00:00:00 2001 From: Fmat34 Date: Mon, 4 May 2026 23:36:19 +0200 Subject: [PATCH 02/20] refactor: route parsed args through runGUI initialFiles --- app.go | 3 ++- cmd/cli/root.go | 10 +++++++++- main.go | 10 ++++++---- main_test.go | 1 + 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/app.go b/app.go index b907abb..5f7e8ea 100644 --- a/app.go +++ b/app.go @@ -8,7 +8,8 @@ import ( // App exposes system dialogs to the frontend via Wails bindings. type App struct { - ctx context.Context + ctx context.Context + initialFiles []string } // NewApp creates a new App instance. diff --git a/cmd/cli/root.go b/cmd/cli/root.go index 7e07ed6..fc5c813 100644 --- a/cmd/cli/root.go +++ b/cmd/cli/root.go @@ -32,7 +32,15 @@ var rootCmd = &cobra.Command{ } func Execute(runGUI func()) { - guiFunc = runGUI + ExecuteWithArgs(os.Args[1:], func([]string) { runGUI() }) +} + +// ExecuteWithArgs runs the Cobra root command with explicit args. The runGUI +// callback is wired so that the `gui` subcommand opens an empty GUI; passing +// startup files happens via main()'s direct call to runGUI(initialFiles). +func ExecuteWithArgs(args []string, runGUI func([]string)) { + guiFunc = func() { runGUI(nil) } + rootCmd.SetArgs(args) if err := rootCmd.Execute(); err != nil { os.Exit(1) } diff --git a/main.go b/main.go index c9f75b3..0938d94 100644 --- a/main.go +++ b/main.go @@ -41,8 +41,9 @@ func parseArgs(args []string) (cliArgs, guiFiles []string) { } func main() { - if len(os.Args) == 1 { - runGUI() + cliArgs, guiFiles := parseArgs(os.Args[1:]) + if cliArgs == nil { + runGUI(guiFiles) return } @@ -51,11 +52,12 @@ func main() { // No-op on Linux/macOS. attachParentConsole() - cli.Execute(runGUI) + cli.ExecuteWithArgs(cliArgs, runGUI) } -func runGUI() { +func runGUI(initialFiles []string) { app := NewApp() + app.initialFiles = initialFiles configService, err := config.NewService() if err != nil { diff --git a/main_test.go b/main_test.go index 6bf93fa..dab95f2 100644 --- a/main_test.go +++ b/main_test.go @@ -18,6 +18,7 @@ func TestParseArgs(t *testing.T) { {"multiple pdf paths", []string{"a.pdf", "b.pdf"}, nil, []string{"a.pdf", "b.pdf"}}, {"global flag before subcommand", []string{"--verbose", "pdf", "info", "x.pdf"}, []string{"--verbose", "pdf", "info", "x.pdf"}, nil}, {"dot-slash workaround", []string{"./pdf"}, nil, []string{"./pdf"}}, + {"unknown subcommand routes to GUI", []string{"pdf-info", "x.pdf"}, nil, []string{"pdf-info", "x.pdf"}}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { From 39debdd1555e40f5794c10336a9b357912712834 Mon Sep 17 00:00:00 2001 From: Fmat34 Date: Mon, 4 May 2026 23:39:20 +0200 Subject: [PATCH 03/20] feat: filter dropped paths to PDFs and emit open-files event --- app.go | 21 +++++++++++++++++++++ app_test.go | 9 +++++++++ 2 files changed, 30 insertions(+) diff --git a/app.go b/app.go index 5f7e8ea..58c3e82 100644 --- a/app.go +++ b/app.go @@ -2,6 +2,8 @@ package main import ( "context" + "path/filepath" + "strings" "github.com/wailsapp/wails/v2/pkg/runtime" ) @@ -46,3 +48,22 @@ func (a *App) ShowMessageDialog(title, message string) error { }) return err } + +// filterPDFPaths returns only paths whose extension is .pdf (case-insensitive). +func filterPDFPaths(paths []string) []string { + out := make([]string, 0, len(paths)) + for _, p := range paths { + if strings.EqualFold(filepath.Ext(p), ".pdf") { + out = append(out, p) + } + } + return out +} + +// onFileDrop is the Wails drag-drop callback. Filters non-PDFs and forwards +// the path list to the frontend via the "open-files" event. Empty filtered +// lists still emit so the frontend can show a "no PDF files" toast. +func (a *App) onFileDrop(_, _ int, paths []string) { + pdfs := filterPDFPaths(paths) + runtime.EventsEmit(a.ctx, "open-files", pdfs) +} diff --git a/app_test.go b/app_test.go index 57e5410..8614c6f 100644 --- a/app_test.go +++ b/app_test.go @@ -2,6 +2,7 @@ package main import ( "context" + "reflect" "testing" "github.com/wailsapp/wails/v2/pkg/runtime" @@ -278,3 +279,11 @@ func TestApp_ErrorHandling(t *testing.T) { // We skip this test as it requires Wails runtime t.Skip("Requires Wails runtime context (tested in E2E)") } + +func TestFilterPDFPaths(t *testing.T) { + got := filterPDFPaths([]string{"a.pdf", "b.txt", "C.PDF", "/x/y/z.png", "doc.pdf"}) + want := []string{"a.pdf", "C.PDF", "doc.pdf"} + if !reflect.DeepEqual(got, want) { + t.Errorf("got %v want %v", got, want) + } +} From 8e16931694d7b504be1bf969aad4b86ba54e0d72 Mon Sep 17 00:00:00 2001 From: Fmat34 Date: Mon, 4 May 2026 23:43:27 +0200 Subject: [PATCH 04/20] feat: resolve and forward second-instance paths to frontend --- app.go | 24 ++++++++++++++++++++++++ app_test.go | 21 +++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/app.go b/app.go index 58c3e82..61ebddd 100644 --- a/app.go +++ b/app.go @@ -5,6 +5,7 @@ import ( "path/filepath" "strings" + "github.com/wailsapp/wails/v2/pkg/options" "github.com/wailsapp/wails/v2/pkg/runtime" ) @@ -67,3 +68,26 @@ func (a *App) onFileDrop(_, _ int, paths []string) { pdfs := filterPDFPaths(paths) runtime.EventsEmit(a.ctx, "open-files", pdfs) } + +// resolveSecondInstancePaths takes args from a second-instance launch (which +// may be relative to that process's working directory) and returns absolute +// paths to PDFs only. The .pdf filter ensures we never emit non-PDF paths +// even if a user explicitly passes one on the second-instance command line. +func resolveSecondInstancePaths(args []string, cwd string) []string { + pdfs := filterPDFPaths(args) + out := make([]string, 0, len(pdfs)) + for _, p := range pdfs { + if !filepath.IsAbs(p) { + p = filepath.Join(cwd, p) + } + out = append(out, p) + } + return out +} + +// onSecondInstance is the Wails SingleInstanceLock callback. Forwards the +// args from a second-launch process to the frontend as "open-files". +func (a *App) onSecondInstance(data options.SecondInstanceData) { + paths := resolveSecondInstancePaths(data.Args, data.WorkingDirectory) + runtime.EventsEmit(a.ctx, "open-files", paths) +} diff --git a/app_test.go b/app_test.go index 8614c6f..47bb6ee 100644 --- a/app_test.go +++ b/app_test.go @@ -2,6 +2,8 @@ package main import ( "context" + "os" + "path/filepath" "reflect" "testing" @@ -287,3 +289,22 @@ func TestFilterPDFPaths(t *testing.T) { t.Errorf("got %v want %v", got, want) } } + +func TestResolveSecondInstancePaths(t *testing.T) { + cwd, err := os.MkdirTemp("", "lankir-test-cwd-*") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(cwd) + + abs := filepath.Join(cwd, "abs.pdf") + got := resolveSecondInstancePaths([]string{"rel.pdf", abs, "other.txt"}, cwd) + + want := []string{ + filepath.Join(cwd, "rel.pdf"), + abs, + } + if !reflect.DeepEqual(got, want) { + t.Errorf("got %v want %v", got, want) + } +} From c1f3ec2dbba5662c89071bd57fe7acb22ec058a5 Mon Sep 17 00:00:00 2001 From: Fmat34 Date: Mon, 4 May 2026 23:46:54 +0200 Subject: [PATCH 05/20] feat: OpenInNewWindow spawns detached lankir with env signal --- app.go | 34 ++++++++++++++++++++++++++++++++++ app_test.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/app.go b/app.go index 61ebddd..47739d0 100644 --- a/app.go +++ b/app.go @@ -2,6 +2,8 @@ package main import ( "context" + "os" + "os/exec" "path/filepath" "strings" @@ -91,3 +93,35 @@ func (a *App) onSecondInstance(data options.SecondInstanceData) { paths := resolveSecondInstancePaths(data.Args, data.WorkingDirectory) runtime.EventsEmit(a.ctx, "open-files", paths) } + +// newWindowEnv returns a copy of env with any prior LANKIR_NEW_WINDOW entry +// removed and a single LANKIR_NEW_WINDOW=1 appended. The child process uses +// this signal to skip SingleInstanceLock acquisition (wired in main.go). +func newWindowEnv(env []string) []string { + out := make([]string, 0, len(env)+1) + for _, e := range env { + if !strings.HasPrefix(e, "LANKIR_NEW_WINDOW=") { + out = append(out, e) + } + } + return append(out, "LANKIR_NEW_WINDOW=1") +} + +// OpenInNewWindow spawns a detached lankir process with the same executable +// and the given file path as an argument. Bypasses SingleInstanceLock via +// LANKIR_NEW_WINDOW=1. Empty filePath spawns an empty new window. +// +// Exported because the frontend calls it via Wails bindings (App.OpenInNewWindow). +func (a *App) OpenInNewWindow(filePath string) error { + exe, err := os.Executable() + if err != nil { + return err + } + args := []string{} + if filePath != "" { + args = append(args, filePath) + } + cmd := exec.Command(exe, args...) + cmd.Env = newWindowEnv(os.Environ()) + return cmd.Start() +} diff --git a/app_test.go b/app_test.go index 47bb6ee..d014e81 100644 --- a/app_test.go +++ b/app_test.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "reflect" + "strings" "testing" "github.com/wailsapp/wails/v2/pkg/runtime" @@ -308,3 +309,36 @@ func TestResolveSecondInstancePaths(t *testing.T) { t.Errorf("got %v want %v", got, want) } } + +func TestNewWindowEnv(t *testing.T) { + in := []string{"PATH=/bin", "LANKIR_NEW_WINDOW=1", "HOME=/root", "LANKIR_NEW_WINDOW=stale"} + got := newWindowEnv(in) + + // Must contain exactly one LANKIR_NEW_WINDOW=1 (we strip any prior and re-add). + count := 0 + for _, e := range got { + if e == "LANKIR_NEW_WINDOW=1" { + count++ + } + if strings.HasPrefix(e, "LANKIR_NEW_WINDOW=") && e != "LANKIR_NEW_WINDOW=1" { + t.Errorf("non-canonical LANKIR_NEW_WINDOW entry: %q", e) + } + } + if count != 1 { + t.Errorf("expected exactly one LANKIR_NEW_WINDOW=1; got %d in %v", count, got) + } + + // Other env entries preserved. + hasPath, hasHome := false, false + for _, e := range got { + if e == "PATH=/bin" { + hasPath = true + } + if e == "HOME=/root" { + hasHome = true + } + } + if !hasPath || !hasHome { + t.Errorf("non-LANKIR env not preserved: %v", got) + } +} From 79786e26ca6d49fb47b338fd36d77f9ff1c84381 Mon Sep 17 00:00:00 2001 From: Fmat34 Date: Mon, 4 May 2026 23:57:32 +0200 Subject: [PATCH 06/20] feat: enable wails drag-drop and single-instance lock --- main.go | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/main.go b/main.go index 0938d94..9ed69a1 100644 --- a/main.go +++ b/main.go @@ -11,6 +11,7 @@ import ( "github.com/wailsapp/wails/v2/pkg/options" "github.com/wailsapp/wails/v2/pkg/options/assetserver" "github.com/wailsapp/wails/v2/pkg/options/linux" + "github.com/wailsapp/wails/v2/pkg/runtime" "github.com/Matbe34/lankir/cmd/cli" "github.com/Matbe34/lankir/internal/config" @@ -56,6 +57,8 @@ func main() { } func runGUI(initialFiles []string) { + skipLock := os.Getenv("LANKIR_NEW_WINDOW") == "1" + app := NewApp() app.initialFiles = initialFiles @@ -73,9 +76,19 @@ func runGUI(initialFiles []string) { pdfService.Startup(ctx) recentFilesService.Startup(ctx) signatureService.Startup(ctx) + + // Wire drag-drop. Wails calls app.onFileDrop with absolute paths + // whenever the user drops files onto the window. + runtime.OnFileDrop(ctx, app.onFileDrop) + + // Hand off any startup files (from CLI args or OS "Open with") to + // the frontend via the same event drop and second-instance use. + if len(app.initialFiles) > 0 { + runtime.EventsEmit(ctx, "open-files", app.initialFiles) + } } - err = wails.Run(&options.App{ + opts := &options.App{ Title: "Lankir", Width: 1400, Height: 900, @@ -96,9 +109,19 @@ func runGUI(initialFiles []string) { WindowIsTranslucent: false, WebviewGpuPolicy: linux.WebviewGpuPolicyAlways, }, - }) + DragAndDrop: &options.DragAndDrop{ + EnableFileDrop: true, + }, + } - if err != nil { + if !skipLock { + opts.SingleInstanceLock = &options.SingleInstanceLock{ + UniqueId: "com.lankir.singleinstance", + OnSecondInstanceLaunch: app.onSecondInstance, + } + } + + if err := wails.Run(opts); err != nil { log.Fatal("Error:", err.Error()) } } From 0183a91bd8866c2811508d8581b3b892ec261306 Mon Sep 17 00:00:00 2001 From: Fmat34 Date: Mon, 4 May 2026 23:59:28 +0200 Subject: [PATCH 07/20] build: register application/pdf MIME on linux --- packaging/lankir.desktop | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packaging/lankir.desktop b/packaging/lankir.desktop index 4f549aa..d76a8dc 100644 --- a/packaging/lankir.desktop +++ b/packaging/lankir.desktop @@ -1,10 +1,11 @@ [Desktop Entry] Name=Lankir Comment=Modern PDF Editor and Signer for Linux -Exec=/usr/bin/lankir +Exec=/usr/bin/lankir %f Icon=lankir Terminal=false Type=Application Categories=Office;Productivity; Keywords=pdf;sign;signature;document;editor; StartupWMClass=Lankir +MimeType=application/pdf; From 3f4a212d5c8c354db321cd866d76f8ab7d8c0664 Mon Sep 17 00:00:00 2001 From: Fmat34 Date: Tue, 5 May 2026 00:03:36 +0200 Subject: [PATCH 08/20] feat: add fileOpener as single source for opening PDFs --- frontend/src/js/fileOpener.js | 30 +++++++++++++++ frontend/tests/fileOpener.test.js | 63 +++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 frontend/src/js/fileOpener.js create mode 100644 frontend/tests/fileOpener.test.js diff --git a/frontend/src/js/fileOpener.js b/frontend/src/js/fileOpener.js new file mode 100644 index 0000000..e45c089 --- /dev/null +++ b/frontend/src/js/fileOpener.js @@ -0,0 +1,30 @@ +import { createPDFTab, switchToTab } from './pdfManager.js'; +import { showMessage } from './messageDialog.js'; + +/** + * Open each path as a new tab in the current window. Skips empty input with a + * single info toast; per-file errors get a toast and don't stop the batch. + * + * Funnels three sources: drag-drop, OS "Open with" via SingleInstanceLock, + * and startup `initialFiles`. All three emit the Wails "open-files" event; + * subscribe in app.js with `runtime.EventsOn('open-files', handleOpenFiles)`. + * + * @param {string[]} paths Absolute file paths. + */ +export async function handleOpenFiles(paths) { + if (!Array.isArray(paths) || paths.length === 0) { + showMessage('No PDF files in drop', 'Open files', 'info'); + return; + } + + let lastTab = null; + for (const path of paths) { + try { + const metadata = await window.go.pdf.PDFService.OpenPDFByPath(path); + lastTab = createPDFTab(metadata.filePath, metadata); + } catch (err) { + showMessage(`Failed to open ${path}: ${err?.message ?? err}`, 'Open failed', 'error'); + } + } + if (lastTab) switchToTab(lastTab); +} diff --git a/frontend/tests/fileOpener.test.js b/frontend/tests/fileOpener.test.js new file mode 100644 index 0000000..0d17d7e --- /dev/null +++ b/frontend/tests/fileOpener.test.js @@ -0,0 +1,63 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +const openMock = vi.fn(); +const createTabMock = vi.fn(); +const switchTabMock = vi.fn(); +const showMessageMock = vi.fn(); + +vi.mock('../src/js/pdfManager.js', () => ({ + createPDFTab: createTabMock, + switchToTab: switchTabMock, +})); +vi.mock('../src/js/messageDialog.js', () => ({ + showMessage: showMessageMock, +})); + +beforeEach(() => { + openMock.mockReset(); + createTabMock.mockReset(); + switchTabMock.mockReset(); + showMessageMock.mockReset(); + globalThis.window = globalThis.window || {}; + window.go = { pdf: { PDFService: { OpenPDFByPath: openMock } } }; +}); + +describe('handleOpenFiles', () => { + it('opens a single PDF as a new tab and switches to it', async () => { + openMock.mockResolvedValue({ filePath: '/x/foo.pdf', pageCount: 1 }); + createTabMock.mockReturnValue('tab-1'); + + const { handleOpenFiles } = await import('../src/js/fileOpener.js'); + await handleOpenFiles(['/x/foo.pdf']); + + expect(openMock).toHaveBeenCalledWith('/x/foo.pdf'); + expect(createTabMock).toHaveBeenCalledWith('/x/foo.pdf', expect.any(Object)); + expect(switchTabMock).toHaveBeenCalledWith('tab-1'); + }); + + it('shows a toast and skips backend calls when batch is empty', async () => { + const { handleOpenFiles } = await import('../src/js/fileOpener.js'); + await handleOpenFiles([]); + + expect(openMock).not.toHaveBeenCalled(); + expect(createTabMock).not.toHaveBeenCalled(); + expect(showMessageMock).toHaveBeenCalledWith( + expect.stringMatching(/no pdf/i), + expect.any(String), + 'info' + ); + }); + + it('shows a per-file toast on backend error and continues', async () => { + openMock.mockRejectedValueOnce(new Error('boom')).mockResolvedValueOnce({ filePath: '/x/b.pdf' }); + createTabMock.mockReturnValue('tab-b'); + + const { handleOpenFiles } = await import('../src/js/fileOpener.js'); + await handleOpenFiles(['/x/a.pdf', '/x/b.pdf']); + + expect(openMock).toHaveBeenCalledTimes(2); + expect(showMessageMock).toHaveBeenCalledWith(expect.stringMatching(/a\.pdf/), expect.any(String), 'error'); + expect(createTabMock).toHaveBeenCalledTimes(1); + expect(createTabMock).toHaveBeenCalledWith('/x/b.pdf', expect.any(Object)); + }); +}); From 9c7066248b1c4f5f1d018b6677208fc08c8a0b5c Mon Sep 17 00:00:00 2001 From: Fmat34 Date: Tue, 5 May 2026 00:05:10 +0200 Subject: [PATCH 09/20] docs: clarify GUI vs CLI dispatch in --help --- cmd/cli/root.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/cmd/cli/root.go b/cmd/cli/root.go index fc5c813..58bda2b 100644 --- a/cmd/cli/root.go +++ b/cmd/cli/root.go @@ -25,7 +25,15 @@ var rootCmd = &cobra.Command{ - Digital signatures with PKCS#11, PKCS#12, and NSS support - Certificate management - Signature profiles -- Configuration management`, +- Configuration management + +Passing a PDF path as the first argument opens it in the GUI: + lankir document.pdf + +If you have a file literally named after a subcommand (e.g. "pdf"), pass it +with a leading "./" to disambiguate: + lankir ./pdf +`, PersistentPreRun: func(cmd *cobra.Command, args []string) { setupLogger() }, From 9e36ff791c6155ce18a7c098b16545dce93b2942 Mon Sep 17 00:00:00 2001 From: Fmat34 Date: Tue, 5 May 2026 00:13:27 +0200 Subject: [PATCH 10/20] feat: subscribe to open-files event in main app init --- frontend/src/js/app.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/src/js/app.js b/frontend/src/js/app.js index 3d04e59..20065e5 100644 --- a/frontend/src/js/app.js +++ b/frontend/src/js/app.js @@ -11,6 +11,8 @@ import { initSettings, getSetting } from './settings.js'; import { themeManager } from './themeManager.js'; import { initLoadingIndicator } from './loadingIndicator.js'; import { state } from './state.js'; +import { handleOpenFiles } from './fileOpener.js'; +import * as runtime from '../wailsjs/runtime/runtime.js'; document.addEventListener('DOMContentLoaded', async () => { @@ -35,6 +37,8 @@ document.addEventListener('DOMContentLoaded', async () => { updateStatus('Ready'); loadRecentFilesWelcome(); + runtime.EventsOn('open-files', handleOpenFiles); + const leftSidebar = document.getElementById('leftSidebar'); const rightSidebar = document.getElementById('rightSidebar'); const expandLeft = document.getElementById('expandLeft'); From b80cc5f68f35ceea2038fd74f801a3a41e5f7902 Mon Sep 17 00:00:00 2001 From: Fmat34 Date: Tue, 5 May 2026 00:14:50 +0200 Subject: [PATCH 11/20] feat: mark viewer and welcome screen as drop targets --- frontend/src/style.css | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/frontend/src/style.css b/frontend/src/style.css index e71f5fd..a60317f 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -1342,4 +1342,11 @@ #pdfTabsContainer::-webkit-scrollbar-thumb:hover { @apply bg-text-secondary; } +} + +/* Drop-target marker for Wails drag-drop. Wails fires onFileDrop only when + the cursor is over an element with this CSS custom property set to "drop". */ +.pdf-viewer, +.welcome-screen { + --wails-drop-target: drop; } \ No newline at end of file From 5e6dc0282941a3d9ef71a1fa21879045705359ff Mon Sep 17 00:00:00 2001 From: Fmat34 Date: Tue, 5 May 2026 00:18:27 +0200 Subject: [PATCH 12/20] feat: add Ctrl+Shift+N to spawn a new lankir window --- frontend/src/js/app.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/frontend/src/js/app.js b/frontend/src/js/app.js index 20065e5..e77e6c4 100644 --- a/frontend/src/js/app.js +++ b/frontend/src/js/app.js @@ -10,7 +10,7 @@ import { initMessageDialog } from './messageDialog.js'; import { initSettings, getSetting } from './settings.js'; import { themeManager } from './themeManager.js'; import { initLoadingIndicator } from './loadingIndicator.js'; -import { state } from './state.js'; +import { state, getActivePDF } from './state.js'; import { handleOpenFiles } from './fileOpener.js'; import * as runtime from '../wailsjs/runtime/runtime.js'; @@ -139,6 +139,16 @@ document.addEventListener('DOMContentLoaded', async () => { return; } + // New window (Ctrl+Shift+N) — spawn a separate lankir process via SingleInstanceLock bypass + if (matchShortcut(e, cfg.newWindow || 'Control+Shift+N')) { + e.preventDefault(); + const active = getActivePDF(); + window.go.main.App.OpenInNewWindow(active?.filePath ?? '').catch(err => { + console.error('OpenInNewWindow failed:', err); + }); + return; + } + // Sign PDF if (!isTyping && matchShortcut(e, cfg.sign || 'Alt+s')) { e.preventDefault(); From be06fec6c77ae4d2534defd43b78f455cf939725 Mon Sep 17 00:00:00 2001 From: Fmat34 Date: Tue, 5 May 2026 09:24:49 +0200 Subject: [PATCH 13/20] fix: disable webview drop so dragged PDFs don't hijack the window --- main.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index 9ed69a1..6c97dc9 100644 --- a/main.go +++ b/main.go @@ -110,7 +110,8 @@ func runGUI(initialFiles []string) { WebviewGpuPolicy: linux.WebviewGpuPolicyAlways, }, DragAndDrop: &options.DragAndDrop{ - EnableFileDrop: true, + EnableFileDrop: true, + DisableWebViewDrop: true, // prevent the webview from navigating to dropped files }, } From 11b47ffb3ddf2e1f53c84eff62b6e730ef79b768 Mon Sep 17 00:00:00 2001 From: Fmat34 Date: Tue, 5 May 2026 09:59:03 +0200 Subject: [PATCH 14/20] fix: route all drops to wails handler and link against webkit2gtk-4.1 --- Taskfile.yml | 2 +- main.go | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Taskfile.yml b/Taskfile.yml index 8294748..8bcf8a3 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -22,7 +22,7 @@ tasks: - echo "Building frontend..." - cd {{.FRONTEND_DIR}} && ./build.sh - echo "Building application..." - - wails build -clean + - wails build -clean -tags webkit2_41 - echo "✓ Build complete! Binary at {{.BUILD_DIR}}/{{.BINARY_NAME}}" build-static: diff --git a/main.go b/main.go index 6c97dc9..42c24bd 100644 --- a/main.go +++ b/main.go @@ -110,8 +110,14 @@ func runGUI(initialFiles []string) { WebviewGpuPolicy: linux.WebviewGpuPolicyAlways, }, DragAndDrop: &options.DragAndDrop{ - EnableFileDrop: true, - DisableWebViewDrop: true, // prevent the webview from navigating to dropped files + // EnableFileDrop installs a Wails-side drop callback that fires only on + // elements marked with --wails-drop-target: drop. Drops on any other + // element fall through to the WebView (which would try to navigate to + // the file). To avoid that, the CSS marker covers the entire body in + // style.css. Do NOT also set DisableWebViewDrop: that calls + // gtk_drag_dest_unset on Linux/GTK and prevents the drag-drop signal + // from firing at all, which silently disables drop everywhere. + EnableFileDrop: true, }, } From 43d0755dea430d78f97c59ad7789292c0a432b95 Mon Sep 17 00:00:00 2001 From: Fmat34 Date: Tue, 5 May 2026 10:00:29 +0200 Subject: [PATCH 15/20] fix: extend drop-target marker to body for full-window drop coverage --- frontend/src/style.css | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/style.css b/frontend/src/style.css index a60317f..504ba80 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -1345,7 +1345,11 @@ } /* Drop-target marker for Wails drag-drop. Wails fires onFileDrop only when - the cursor is over an element with this CSS custom property set to "drop". */ + the cursor is over an element with this CSS custom property set to "drop". + Cover the entire viewport so any drop is captured — without this, drops on + unmarked elements fall through to the WebView which would try to navigate + to the dropped file. */ +body, .pdf-viewer, .welcome-screen { --wails-drop-target: drop; From d9c2cc3a45795048ec845c7d4737e6aaa846cb78 Mon Sep 17 00:00:00 2001 From: Fmat34 Date: Tue, 5 May 2026 10:22:01 +0200 Subject: [PATCH 16/20] fix: replace broken wails native drag-drop with html5 implementation --- app.go | 8 ------ frontend/src/js/app.js | 3 ++- frontend/src/js/fileOpener.js | 48 ++++++++++++++++++++++++++++++++--- frontend/src/style.css | 10 -------- main.go | 24 ++++++------------ 5 files changed, 55 insertions(+), 38 deletions(-) diff --git a/app.go b/app.go index 47739d0..34815ad 100644 --- a/app.go +++ b/app.go @@ -63,14 +63,6 @@ func filterPDFPaths(paths []string) []string { return out } -// onFileDrop is the Wails drag-drop callback. Filters non-PDFs and forwards -// the path list to the frontend via the "open-files" event. Empty filtered -// lists still emit so the frontend can show a "no PDF files" toast. -func (a *App) onFileDrop(_, _ int, paths []string) { - pdfs := filterPDFPaths(paths) - runtime.EventsEmit(a.ctx, "open-files", pdfs) -} - // resolveSecondInstancePaths takes args from a second-instance launch (which // may be relative to that process's working directory) and returns absolute // paths to PDFs only. The .pdf filter ensures we never emit non-PDF paths diff --git a/frontend/src/js/app.js b/frontend/src/js/app.js index e77e6c4..6c9921d 100644 --- a/frontend/src/js/app.js +++ b/frontend/src/js/app.js @@ -11,7 +11,7 @@ import { initSettings, getSetting } from './settings.js'; import { themeManager } from './themeManager.js'; import { initLoadingIndicator } from './loadingIndicator.js'; import { state, getActivePDF } from './state.js'; -import { handleOpenFiles } from './fileOpener.js'; +import { handleOpenFiles, registerDragDrop } from './fileOpener.js'; import * as runtime from '../wailsjs/runtime/runtime.js'; @@ -38,6 +38,7 @@ document.addEventListener('DOMContentLoaded', async () => { loadRecentFilesWelcome(); runtime.EventsOn('open-files', handleOpenFiles); + registerDragDrop(); const leftSidebar = document.getElementById('leftSidebar'); const rightSidebar = document.getElementById('rightSidebar'); diff --git a/frontend/src/js/fileOpener.js b/frontend/src/js/fileOpener.js index e45c089..b5dcdd5 100644 --- a/frontend/src/js/fileOpener.js +++ b/frontend/src/js/fileOpener.js @@ -5,9 +5,9 @@ import { showMessage } from './messageDialog.js'; * Open each path as a new tab in the current window. Skips empty input with a * single info toast; per-file errors get a toast and don't stop the batch. * - * Funnels three sources: drag-drop, OS "Open with" via SingleInstanceLock, - * and startup `initialFiles`. All three emit the Wails "open-files" event; - * subscribe in app.js with `runtime.EventsOn('open-files', handleOpenFiles)`. + * Funnels two sources: HTML5 drag-drop (wired by registerDragDrop) and the + * Wails "open-files" event (OS "Open with" via SingleInstanceLock + startup + * initialFiles). * * @param {string[]} paths Absolute file paths. */ @@ -28,3 +28,45 @@ export async function handleOpenFiles(paths) { } if (lastTab) switchToTab(lastTab); } + +/** + * Wails v2.10.2's native Linux drag-drop (`options.DragAndDrop.EnableFileDrop`) + * is broken — its GTK handler returns FALSE so WebKit's default file-navigation + * fires too, hijacking the window with the built-in PDF viewer. Setting + * `DisableWebViewDrop: true` removes the webview as a drop target entirely, + * silencing the callback. + * + * This implementation bypasses the Wails plumbing entirely: standard HTML5 + * dragover/drop with `e.preventDefault()` blocks WebKit's navigation, and + * `dataTransfer.getData('text/uri-list')` gives us absolute paths (file:// + * URIs) without any backend round-trip. Works the same on all OSes. + */ +export function registerDragDrop() { + const onDragOver = (e) => { + e.preventDefault(); + if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'; + }; + const onDrop = (e) => { + e.preventDefault(); + const paths = extractFilePaths(e.dataTransfer); + const pdfs = paths.filter(p => /\.pdf$/i.test(p)); + if (pdfs.length === 0) { + if (paths.length > 0) showMessage('No PDF files in drop', 'Open files', 'info'); + return; + } + handleOpenFiles(pdfs); + }; + window.addEventListener('dragover', onDragOver); + window.addEventListener('drop', onDrop); +} + +function extractFilePaths(dt) { + if (!dt) return []; + const uriList = dt.getData('text/uri-list'); + if (!uriList) return []; + return uriList + .split(/\r?\n/) + .map(s => s.trim()) + .filter(s => s.startsWith('file://')) + .map(s => decodeURIComponent(s.slice('file://'.length))); +} diff --git a/frontend/src/style.css b/frontend/src/style.css index 504ba80..a081e5c 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -1344,13 +1344,3 @@ } } -/* Drop-target marker for Wails drag-drop. Wails fires onFileDrop only when - the cursor is over an element with this CSS custom property set to "drop". - Cover the entire viewport so any drop is captured — without this, drops on - unmarked elements fall through to the WebView which would try to navigate - to the dropped file. */ -body, -.pdf-viewer, -.welcome-screen { - --wails-drop-target: drop; -} \ No newline at end of file diff --git a/main.go b/main.go index 42c24bd..e1c4c0f 100644 --- a/main.go +++ b/main.go @@ -77,12 +77,14 @@ func runGUI(initialFiles []string) { recentFilesService.Startup(ctx) signatureService.Startup(ctx) - // Wire drag-drop. Wails calls app.onFileDrop with absolute paths - // whenever the user drops files onto the window. - runtime.OnFileDrop(ctx, app.onFileDrop) - - // Hand off any startup files (from CLI args or OS "Open with") to - // the frontend via the same event drop and second-instance use. + // Drag-drop is handled in the frontend via HTML5 dragover/drop events + // (see frontend/src/js/fileOpener.js::registerDragDrop). Wails v2.10.2's + // native Linux DragAndDrop.EnableFileDrop is broken — its GTK handler + // returns FALSE, so WebKit's default file-navigation hijacks the + // window. Bypassing it entirely keeps drag-drop working. + + // Hand off any startup files (from CLI args or OS "Open with") to the + // frontend via the same event used by SingleInstanceLock. if len(app.initialFiles) > 0 { runtime.EventsEmit(ctx, "open-files", app.initialFiles) } @@ -109,16 +111,6 @@ func runGUI(initialFiles []string) { WindowIsTranslucent: false, WebviewGpuPolicy: linux.WebviewGpuPolicyAlways, }, - DragAndDrop: &options.DragAndDrop{ - // EnableFileDrop installs a Wails-side drop callback that fires only on - // elements marked with --wails-drop-target: drop. Drops on any other - // element fall through to the WebView (which would try to navigate to - // the file). To avoid that, the CSS marker covers the entire body in - // style.css. Do NOT also set DisableWebViewDrop: that calls - // gtk_drag_dest_unset on Linux/GTK and prevents the drag-drop signal - // from firing at all, which silently disables drop everywhere. - EnableFileDrop: true, - }, } if !skipLock { From ba6652a3dd87599ed79422dae7ce2029699fdee8 Mon Sep 17 00:00:00 2001 From: Fmat34 Date: Tue, 5 May 2026 11:52:08 +0200 Subject: [PATCH 17/20] fix: use patched wails fork for working linux drag-drop --- app.go | 7 +++++ frontend/src/js/app.js | 3 +-- frontend/src/js/fileOpener.js | 48 +++-------------------------------- frontend/src/style.css | 8 ++++++ go.mod | 2 ++ go.sum | 4 +-- main.go | 13 ++++++---- 7 files changed, 31 insertions(+), 54 deletions(-) diff --git a/app.go b/app.go index 34815ad..548759c 100644 --- a/app.go +++ b/app.go @@ -63,6 +63,13 @@ func filterPDFPaths(paths []string) []string { return out } +// onFileDrop is the Wails drag-drop callback. Filters non-PDFs and forwards +// the path list to the frontend via the "open-files" event. +func (a *App) onFileDrop(_, _ int, paths []string) { + pdfs := filterPDFPaths(paths) + runtime.EventsEmit(a.ctx, "open-files", pdfs) +} + // resolveSecondInstancePaths takes args from a second-instance launch (which // may be relative to that process's working directory) and returns absolute // paths to PDFs only. The .pdf filter ensures we never emit non-PDF paths diff --git a/frontend/src/js/app.js b/frontend/src/js/app.js index 6c9921d..e77e6c4 100644 --- a/frontend/src/js/app.js +++ b/frontend/src/js/app.js @@ -11,7 +11,7 @@ import { initSettings, getSetting } from './settings.js'; import { themeManager } from './themeManager.js'; import { initLoadingIndicator } from './loadingIndicator.js'; import { state, getActivePDF } from './state.js'; -import { handleOpenFiles, registerDragDrop } from './fileOpener.js'; +import { handleOpenFiles } from './fileOpener.js'; import * as runtime from '../wailsjs/runtime/runtime.js'; @@ -38,7 +38,6 @@ document.addEventListener('DOMContentLoaded', async () => { loadRecentFilesWelcome(); runtime.EventsOn('open-files', handleOpenFiles); - registerDragDrop(); const leftSidebar = document.getElementById('leftSidebar'); const rightSidebar = document.getElementById('rightSidebar'); diff --git a/frontend/src/js/fileOpener.js b/frontend/src/js/fileOpener.js index b5dcdd5..e45c089 100644 --- a/frontend/src/js/fileOpener.js +++ b/frontend/src/js/fileOpener.js @@ -5,9 +5,9 @@ import { showMessage } from './messageDialog.js'; * Open each path as a new tab in the current window. Skips empty input with a * single info toast; per-file errors get a toast and don't stop the batch. * - * Funnels two sources: HTML5 drag-drop (wired by registerDragDrop) and the - * Wails "open-files" event (OS "Open with" via SingleInstanceLock + startup - * initialFiles). + * Funnels three sources: drag-drop, OS "Open with" via SingleInstanceLock, + * and startup `initialFiles`. All three emit the Wails "open-files" event; + * subscribe in app.js with `runtime.EventsOn('open-files', handleOpenFiles)`. * * @param {string[]} paths Absolute file paths. */ @@ -28,45 +28,3 @@ export async function handleOpenFiles(paths) { } if (lastTab) switchToTab(lastTab); } - -/** - * Wails v2.10.2's native Linux drag-drop (`options.DragAndDrop.EnableFileDrop`) - * is broken — its GTK handler returns FALSE so WebKit's default file-navigation - * fires too, hijacking the window with the built-in PDF viewer. Setting - * `DisableWebViewDrop: true` removes the webview as a drop target entirely, - * silencing the callback. - * - * This implementation bypasses the Wails plumbing entirely: standard HTML5 - * dragover/drop with `e.preventDefault()` blocks WebKit's navigation, and - * `dataTransfer.getData('text/uri-list')` gives us absolute paths (file:// - * URIs) without any backend round-trip. Works the same on all OSes. - */ -export function registerDragDrop() { - const onDragOver = (e) => { - e.preventDefault(); - if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'; - }; - const onDrop = (e) => { - e.preventDefault(); - const paths = extractFilePaths(e.dataTransfer); - const pdfs = paths.filter(p => /\.pdf$/i.test(p)); - if (pdfs.length === 0) { - if (paths.length > 0) showMessage('No PDF files in drop', 'Open files', 'info'); - return; - } - handleOpenFiles(pdfs); - }; - window.addEventListener('dragover', onDragOver); - window.addEventListener('drop', onDrop); -} - -function extractFilePaths(dt) { - if (!dt) return []; - const uriList = dt.getData('text/uri-list'); - if (!uriList) return []; - return uriList - .split(/\r?\n/) - .map(s => s.trim()) - .filter(s => s.startsWith('file://')) - .map(s => decodeURIComponent(s.slice('file://'.length))); -} diff --git a/frontend/src/style.css b/frontend/src/style.css index a081e5c..187d063 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -1344,3 +1344,11 @@ } } +/* Wails drag-drop target. Wails fires our OnFileDrop callback only when the + element under the cursor at drop time has --wails-drop-target: drop. CSS + custom properties inherit, so marking body covers every drop on the + window. */ +body { + --wails-drop-target: drop; +} + diff --git a/go.mod b/go.mod index 4f34129..604115b 100644 --- a/go.mod +++ b/go.mod @@ -54,3 +54,5 @@ require ( ) replace github.com/github/smimesign => github.com/nikwo/smimesign-fork v0.0.0-20221130091550-0956e442b5b6 + +replace github.com/wailsapp/wails/v2 => github.com/Matbe34/wails/v2 v2.10.3-0.20260505094453-fe793ce5bfc8 diff --git a/go.sum b/go.sum index d6dc269..53ddd65 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/Matbe34/wails/v2 v2.10.3-0.20260505094453-fe793ce5bfc8 h1:c1Fu+wjKJMGtARrvVNeDABZvIBR5qvGq16wf+WXZYvg= +github.com/Matbe34/wails/v2 v2.10.3-0.20260505094453-fe793ce5bfc8/go.mod h1:XuN4IUOPpzBrHUkEd7sCU5ln4T/p1wQedfxP7fKik+4= github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= github.com/certifi/gocertifi v0.0.0-20180118203423-deb3ae2ef261/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4= @@ -92,8 +94,6 @@ github.com/wailsapp/go-webview2 v1.0.19 h1:7U3QcDj1PrBPaxJNCui2k1SkWml+Q5kvFUFyT github.com/wailsapp/go-webview2 v1.0.19/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= -github.com/wailsapp/wails/v2 v2.10.2 h1:29U+c5PI4K4hbx8yFbFvwpCuvqK9VgNv8WGobIlKlXk= -github.com/wailsapp/wails/v2 v2.10.2/go.mod h1:XuN4IUOPpzBrHUkEd7sCU5ln4T/p1wQedfxP7fKik+4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= diff --git a/main.go b/main.go index e1c4c0f..609b9c4 100644 --- a/main.go +++ b/main.go @@ -77,11 +77,11 @@ func runGUI(initialFiles []string) { recentFilesService.Startup(ctx) signatureService.Startup(ctx) - // Drag-drop is handled in the frontend via HTML5 dragover/drop events - // (see frontend/src/js/fileOpener.js::registerDragDrop). Wails v2.10.2's - // native Linux DragAndDrop.EnableFileDrop is broken — its GTK handler - // returns FALSE, so WebKit's default file-navigation hijacks the - // window. Bypassing it entirely keeps drag-drop working. + // Wire drag-drop. The Wails v2 fork in our go.mod replace directive + // patches the Linux GTK handler to return TRUE so WebKit's default + // file-navigation does not hijack the window. See wailsapp/wails#3686 + // and Matbe34/wails fix/linux-drag-drop-return-true branch. + runtime.OnFileDrop(ctx, app.onFileDrop) // Hand off any startup files (from CLI args or OS "Open with") to the // frontend via the same event used by SingleInstanceLock. @@ -111,6 +111,9 @@ func runGUI(initialFiles []string) { WindowIsTranslucent: false, WebviewGpuPolicy: linux.WebviewGpuPolicyAlways, }, + DragAndDrop: &options.DragAndDrop{ + EnableFileDrop: true, + }, } if !skipLock { From b95b2d1ea6e2712cbf8d96004674b921b6c602e9 Mon Sep 17 00:00:00 2001 From: Fmat34 Date: Tue, 5 May 2026 11:57:47 +0200 Subject: [PATCH 18/20] fix: suppress webview drop default via JS preventDefault (wails#3686 workaround) --- frontend/src/js/app.js | 3 ++- frontend/src/js/fileOpener.js | 25 ++++++++++++++++++++++--- frontend/src/style.css | 8 -------- go.mod | 2 -- go.sum | 4 ++-- 5 files changed, 26 insertions(+), 16 deletions(-) diff --git a/frontend/src/js/app.js b/frontend/src/js/app.js index e77e6c4..420ba15 100644 --- a/frontend/src/js/app.js +++ b/frontend/src/js/app.js @@ -11,7 +11,7 @@ import { initSettings, getSetting } from './settings.js'; import { themeManager } from './themeManager.js'; import { initLoadingIndicator } from './loadingIndicator.js'; import { state, getActivePDF } from './state.js'; -import { handleOpenFiles } from './fileOpener.js'; +import { handleOpenFiles, suppressWebViewDropDefault } from './fileOpener.js'; import * as runtime from '../wailsjs/runtime/runtime.js'; @@ -38,6 +38,7 @@ document.addEventListener('DOMContentLoaded', async () => { loadRecentFilesWelcome(); runtime.EventsOn('open-files', handleOpenFiles); + suppressWebViewDropDefault(); const leftSidebar = document.getElementById('leftSidebar'); const rightSidebar = document.getElementById('rightSidebar'); diff --git a/frontend/src/js/fileOpener.js b/frontend/src/js/fileOpener.js index e45c089..dc0f166 100644 --- a/frontend/src/js/fileOpener.js +++ b/frontend/src/js/fileOpener.js @@ -1,13 +1,32 @@ import { createPDFTab, switchToTab } from './pdfManager.js'; import { showMessage } from './messageDialog.js'; +/** + * Workaround for wailsapp/wails#3686 (closed 2025-08-02 with this fix + * accepted by the maintainer). On Linux WebKit2GTK, dropping a file on the + * window triggers WebKit's default file-navigation in addition to Wails' + * Go-side OnFileDrop callback, hijacking the app UI with a chrome-style + * file viewer. Calling preventDefault on every drag-related JS event + * blocks the WebKit default; Wails' Go callback still fires via the GTK + * signal path and is unaffected. + */ +export function suppressWebViewDropDefault() { + const events = [ + 'drop', 'dragover', 'dragleave', 'drag', 'dragend', 'dragenter', 'dragstart', + ]; + for (const evt of events) { + window.addEventListener(evt, (e) => e.preventDefault()); + } +} + /** * Open each path as a new tab in the current window. Skips empty input with a * single info toast; per-file errors get a toast and don't stop the batch. * - * Funnels three sources: drag-drop, OS "Open with" via SingleInstanceLock, - * and startup `initialFiles`. All three emit the Wails "open-files" event; - * subscribe in app.js with `runtime.EventsOn('open-files', handleOpenFiles)`. + * Funnels three sources: drag-drop (Wails OnFileDrop emits "open-files"), + * OS "Open with" via SingleInstanceLock, and startup `initialFiles`. All + * three emit the Wails "open-files" event; subscribe in app.js with + * `runtime.EventsOn('open-files', handleOpenFiles)`. * * @param {string[]} paths Absolute file paths. */ diff --git a/frontend/src/style.css b/frontend/src/style.css index 187d063..a081e5c 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -1344,11 +1344,3 @@ } } -/* Wails drag-drop target. Wails fires our OnFileDrop callback only when the - element under the cursor at drop time has --wails-drop-target: drop. CSS - custom properties inherit, so marking body covers every drop on the - window. */ -body { - --wails-drop-target: drop; -} - diff --git a/go.mod b/go.mod index 604115b..4f34129 100644 --- a/go.mod +++ b/go.mod @@ -54,5 +54,3 @@ require ( ) replace github.com/github/smimesign => github.com/nikwo/smimesign-fork v0.0.0-20221130091550-0956e442b5b6 - -replace github.com/wailsapp/wails/v2 => github.com/Matbe34/wails/v2 v2.10.3-0.20260505094453-fe793ce5bfc8 diff --git a/go.sum b/go.sum index 53ddd65..d6dc269 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -github.com/Matbe34/wails/v2 v2.10.3-0.20260505094453-fe793ce5bfc8 h1:c1Fu+wjKJMGtARrvVNeDABZvIBR5qvGq16wf+WXZYvg= -github.com/Matbe34/wails/v2 v2.10.3-0.20260505094453-fe793ce5bfc8/go.mod h1:XuN4IUOPpzBrHUkEd7sCU5ln4T/p1wQedfxP7fKik+4= github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= github.com/certifi/gocertifi v0.0.0-20180118203423-deb3ae2ef261/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4= @@ -94,6 +92,8 @@ github.com/wailsapp/go-webview2 v1.0.19 h1:7U3QcDj1PrBPaxJNCui2k1SkWml+Q5kvFUFyT github.com/wailsapp/go-webview2 v1.0.19/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= +github.com/wailsapp/wails/v2 v2.10.2 h1:29U+c5PI4K4hbx8yFbFvwpCuvqK9VgNv8WGobIlKlXk= +github.com/wailsapp/wails/v2 v2.10.2/go.mod h1:XuN4IUOPpzBrHUkEd7sCU5ln4T/p1wQedfxP7fKik+4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= From 9ee266985976b85b54a667d628d2483300426244 Mon Sep 17 00:00:00 2001 From: Fmat34 Date: Tue, 5 May 2026 13:01:31 +0200 Subject: [PATCH 19/20] fix: suppress empty drop and second-instance fires to silence false toast --- app.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/app.go b/app.go index 548759c..5265041 100644 --- a/app.go +++ b/app.go @@ -64,8 +64,15 @@ func filterPDFPaths(paths []string) []string { } // onFileDrop is the Wails drag-drop callback. Filters non-PDFs and forwards -// the path list to the frontend via the "open-files" event. +// the path list to the frontend via the "open-files" event. Wails on Linux +// can fire this twice per drop (once with paths, once empty as WebKit's +// internal handling re-triggers the GTK signal); skip the empty fires so the +// frontend doesn't show a spurious "No PDF files in drop" toast right after +// a successful tab open. func (a *App) onFileDrop(_, _ int, paths []string) { + if len(paths) == 0 { + return + } pdfs := filterPDFPaths(paths) runtime.EventsEmit(a.ctx, "open-files", pdfs) } @@ -87,9 +94,14 @@ func resolveSecondInstancePaths(args []string, cwd string) []string { } // onSecondInstance is the Wails SingleInstanceLock callback. Forwards the -// args from a second-launch process to the frontend as "open-files". +// args from a second-launch process to the frontend as "open-files". Skips +// emitting when the second instance had no PDF args so a bare relaunch +// doesn't pop a "No PDF files" toast in the first window. func (a *App) onSecondInstance(data options.SecondInstanceData) { paths := resolveSecondInstancePaths(data.Args, data.WorkingDirectory) + if len(paths) == 0 { + return + } runtime.EventsEmit(a.ctx, "open-files", paths) } From 6a88dfdf53976d1dcb6c7b31d9f245fd1c6d2107 Mon Sep 17 00:00:00 2001 From: Fmat34 Date: Tue, 5 May 2026 22:41:40 +0200 Subject: [PATCH 20/20] chore: bump version to 0.1.2-rc1 for entry-points test build --- wails.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wails.json b/wails.json index a48d250..43b82f1 100644 --- a/wails.json +++ b/wails.json @@ -11,7 +11,7 @@ "info": { "companyName": "Lankir", "productName": "Lankir", - "productVersion": "0.1.1", + "productVersion": "0.1.2-rc1", "copyright": "Copyright © 2025", "comments": "A modern PDF editor for Linux" },