summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-01 18:10:19 +0200
committerPaul Buetow <paul@buetow.org>2026-03-01 18:10:19 +0200
commit5247a201c8ba77329c7b1ee40da7b8e43a56b567 (patch)
treed5cd331e7bd5040ef3267e7f888a8c4463cd2832
parent5fcb97aeb4ec1e291e221794df5d40b27ea60197 (diff)
Release v0.1.0: code quality audit — fix dialog bug, refactor, add testsv0.1.0
- Fix per-keystroke dialog storm when text exceeds maxTextLength - Extract logEntry() for testable persistence with filepath.Join - Extract newInputWidget() and deduplicate shared-text loading - Rename appId to appID per Go naming conventions - Delete unused debugSharedPath function - Add error distinction in readSharedFromCache (log real errors) - Fix ANDORID_NDK_HOME typo to ANDRIOD_NDK_HOME in Magefile - Run gofmt, go mod tidy; promote defaultDirectory to const - Add unit tests for logEntry and readSharedFromCache stub Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
-rw-r--r--FyneApp.toml4
-rw-r--r--Magefile.go334
-rw-r--r--android_shared_android.go36
-rw-r--r--android_shared_stub.go1
-rw-r--r--android_shared_stub_test.go15
-rw-r--r--go.mod6
-rw-r--r--main.go235
-rw-r--r--main_test.go62
8 files changed, 390 insertions, 303 deletions
diff --git a/FyneApp.toml b/FyneApp.toml
index b409766..9a1ae86 100644
--- a/FyneApp.toml
+++ b/FyneApp.toml
@@ -4,5 +4,5 @@ Website = "https://codeberg.org/snonux/quicklogger"
Icon = "icon.png"
Name = "quicklogger"
ID = "org.buetow.quicklogger"
- Version = "0.0.4"
- Build = 52
+ Version = "0.1.0"
+ Build = 55
diff --git a/Magefile.go b/Magefile.go
index 49a5fb4..ef25814 100644
--- a/Magefile.go
+++ b/Magefile.go
@@ -4,198 +4,198 @@
package main
import (
- "io"
- "fmt"
- "os"
- "path/filepath"
- "runtime"
- "strconv"
- "time"
-
- "github.com/magefile/mage/sh"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "runtime"
+ "strconv"
+ "time"
+
+ "github.com/magefile/mage/sh"
)
// Build compiles the quicklogger app into ./bin.
func Build() error {
- outDir := "bin"
- if err := os.MkdirAll(outDir, 0o755); err != nil {
- return err
- }
-
- binName := "quicklogger"
- if runtime.GOOS == "windows" {
- binName += ".exe"
- }
-
- out := filepath.Join(outDir, binName)
- fmt.Printf("Building %s\n", out)
- return sh.RunV("go", "build", "-o", out, ".")
+ outDir := "bin"
+ if err := os.MkdirAll(outDir, 0o755); err != nil {
+ return err
+ }
+
+ binName := "quicklogger"
+ if runtime.GOOS == "windows" {
+ binName += ".exe"
+ }
+
+ out := filepath.Join(outDir, binName)
+ fmt.Printf("Building %s\n", out)
+ return sh.RunV("go", "build", "-o", out, ".")
}
// Run launches the app with `go run .`.
func Run() error {
- fmt.Println("Running quicklogger (verbose build)")
- // Show Go version and key env for diagnostics
- _ = sh.RunV("go", "version")
- _ = sh.RunV("go", "env", "GOVERSION", "GOOS", "GOARCH", "GOMOD", "GOMODCACHE", "GOCACHE")
- // Compile+run with verbose and trace flags to show build steps
- return sh.RunV("go", "run", "-v", "-x", ".")
+ fmt.Println("Running quicklogger (verbose build)")
+ // Show Go version and key env for diagnostics
+ _ = sh.RunV("go", "version")
+ _ = sh.RunV("go", "env", "GOVERSION", "GOOS", "GOARCH", "GOMOD", "GOMODCACHE", "GOCACHE")
+ // Compile+run with verbose and trace flags to show build steps
+ return sh.RunV("go", "run", "-v", "-x", ".")
}
// Clean removes build artifacts (bin/ and local APK).
func Clean() error {
- fmt.Println("Cleaning build artifacts")
- // Remove bin directory
- if err := os.RemoveAll("bin"); err != nil {
- return err
- }
- // Remove generated APK if present
- _ = os.Remove("quicklogger.apk")
- return nil
+ fmt.Println("Cleaning build artifacts")
+ // Remove bin directory
+ if err := os.RemoveAll("bin"); err != nil {
+ return err
+ }
+ // Remove generated APK if present
+ _ = os.Remove("quicklogger.apk")
+ return nil
}
// Android packages an Android APK via Fyne and copies it to ~/Documents/APKs if present.
func Android() error {
- env := map[string]string{}
-
- // Respect existing ANDROID_NDK_HOME. If not set, try legacy var or a common default path.
- ndk := os.Getenv("ANDROID_NDK_HOME")
- if ndk == "" {
- // Some setups export a misspelled var; honor it if present.
- ndk = os.Getenv("ANDORID_NDK_HOME")
- }
- if ndk == "" {
- if home, err := os.UserHomeDir(); err == nil {
- guess := filepath.Join(home, "android-ndk", "android-ndk-r26b")
- env["ANDROID_NDK_HOME"] = guess
- fmt.Printf("ANDROID_NDK_HOME not set, guessing %s\n", guess)
- }
- }
-
- fmt.Println("Packaging Android APK via Fyne")
- if err := sh.RunWithV(env, "fyne", "package", "-os", "android"); err != nil {
- return err
- }
-
- // If ~/Documents/APKs exists, copy the APK there.
- home, err := os.UserHomeDir()
- if err != nil {
- return nil // packaging succeeded; copying is best-effort
- }
- destDir := filepath.Join(home, "Documents", "APKs")
- if st, err := os.Stat(destDir); err == nil && st.IsDir() {
- src := "quicklogger.apk"
- if _, err := os.Stat(src); err == nil {
- dst := filepath.Join(destDir, filepath.Base(src))
- if err := copyFile(src, dst); err != nil {
- return err
- }
- fmt.Printf("Copied %s to %s\n", src, dst)
- }
- }
- return nil
+ env := map[string]string{}
+
+ // Respect existing ANDROID_NDK_HOME. If not set, try legacy var or a common default path.
+ ndk := os.Getenv("ANDROID_NDK_HOME")
+ if ndk == "" {
+ // Some setups export a misspelled var; honor it if present.
+ ndk = os.Getenv("ANDRIOD_NDK_HOME")
+ }
+ if ndk == "" {
+ if home, err := os.UserHomeDir(); err == nil {
+ guess := filepath.Join(home, "android-ndk", "android-ndk-r26b")
+ env["ANDROID_NDK_HOME"] = guess
+ fmt.Printf("ANDROID_NDK_HOME not set, guessing %s\n", guess)
+ }
+ }
+
+ fmt.Println("Packaging Android APK via Fyne")
+ if err := sh.RunWithV(env, "fyne", "package", "-os", "android"); err != nil {
+ return err
+ }
+
+ // If ~/Documents/APKs exists, copy the APK there.
+ home, err := os.UserHomeDir()
+ if err != nil {
+ return nil // packaging succeeded; copying is best-effort
+ }
+ destDir := filepath.Join(home, "Documents", "APKs")
+ if st, err := os.Stat(destDir); err == nil && st.IsDir() {
+ src := "quicklogger.apk"
+ if _, err := os.Stat(src); err == nil {
+ dst := filepath.Join(destDir, filepath.Base(src))
+ if err := copyFile(src, dst); err != nil {
+ return err
+ }
+ fmt.Printf("Copied %s to %s\n", src, dst)
+ }
+ }
+ return nil
}
// AndroidCross builds an Android APK using fyne-cross with Docker/Podman.
// Mirrors steps from README using a Podman socket if available.
func AndroidCross() error {
- env := map[string]string{}
-
- // If DOCKER_HOST is not set, try a sensible Podman socket default.
- if os.Getenv("DOCKER_HOST") == "" {
- uid := os.Geteuid()
- sock := filepath.Join("/run/user", strconv.Itoa(uid), "podman", "podman.sock")
- if _, err := os.Stat(sock); err == nil {
- env["DOCKER_HOST"] = "unix://" + sock
- fmt.Printf("Using Podman socket: %s\n", env["DOCKER_HOST"])
- } else {
- fmt.Println("DOCKER_HOST is not set and no Podman socket found; relying on default Docker daemon.")
- }
- }
-
- // Ensure fyne-cross is available; attempt install if not.
- if _, err := sh.Output("which", "fyne-cross"); err != nil {
- fmt.Println("Installing fyne-cross (requires network access)...")
- if err := sh.RunV("go", "install", "github.com/fyne-io/fyne-cross@latest"); err != nil {
- return fmt.Errorf("fyne-cross not found and installation failed: %w", err)
- }
- }
-
- // Pull/update builder image then build.
- if err := sh.RunWithV(env, "fyne-cross", "android", "--pull"); err != nil {
- return err
- }
- if err := sh.RunWithV(env, "fyne-cross", "android"); err != nil {
- return err
- }
-
- // Copy newest APK to ~/Documents/APKs if available.
- apk, err := newestAPK("fyne-cross/dist/android")
- if err != nil {
- // Not fatal; print a note and finish
- fmt.Printf("Note: could not locate built APK: %v\n", err)
- return nil
- }
- home, herr := os.UserHomeDir()
- if herr != nil {
- return nil
- }
- destDir := filepath.Join(home, "Documents", "APKs")
- if st, err := os.Stat(destDir); err == nil && st.IsDir() {
- dst := filepath.Join(destDir, filepath.Base(apk))
- if err := copyFile(apk, dst); err != nil {
- return err
- }
- fmt.Printf("Copied %s to %s\n", apk, dst)
- }
- return nil
+ env := map[string]string{}
+
+ // If DOCKER_HOST is not set, try a sensible Podman socket default.
+ if os.Getenv("DOCKER_HOST") == "" {
+ uid := os.Geteuid()
+ sock := filepath.Join("/run/user", strconv.Itoa(uid), "podman", "podman.sock")
+ if _, err := os.Stat(sock); err == nil {
+ env["DOCKER_HOST"] = "unix://" + sock
+ fmt.Printf("Using Podman socket: %s\n", env["DOCKER_HOST"])
+ } else {
+ fmt.Println("DOCKER_HOST is not set and no Podman socket found; relying on default Docker daemon.")
+ }
+ }
+
+ // Ensure fyne-cross is available; attempt install if not.
+ if _, err := sh.Output("which", "fyne-cross"); err != nil {
+ fmt.Println("Installing fyne-cross (requires network access)...")
+ if err := sh.RunV("go", "install", "github.com/fyne-io/fyne-cross@latest"); err != nil {
+ return fmt.Errorf("fyne-cross not found and installation failed: %w", err)
+ }
+ }
+
+ // Pull/update builder image then build.
+ if err := sh.RunWithV(env, "fyne-cross", "android", "--pull"); err != nil {
+ return err
+ }
+ if err := sh.RunWithV(env, "fyne-cross", "android"); err != nil {
+ return err
+ }
+
+ // Copy newest APK to ~/Documents/APKs if available.
+ apk, err := newestAPK("fyne-cross/dist/android")
+ if err != nil {
+ // Not fatal; print a note and finish
+ fmt.Printf("Note: could not locate built APK: %v\n", err)
+ return nil
+ }
+ home, herr := os.UserHomeDir()
+ if herr != nil {
+ return nil
+ }
+ destDir := filepath.Join(home, "Documents", "APKs")
+ if st, err := os.Stat(destDir); err == nil && st.IsDir() {
+ dst := filepath.Join(destDir, filepath.Base(apk))
+ if err := copyFile(apk, dst); err != nil {
+ return err
+ }
+ fmt.Printf("Copied %s to %s\n", apk, dst)
+ }
+ return nil
}
func newestAPK(dir string) (string, error) {
- entries, err := os.ReadDir(dir)
- if err != nil {
- return "", err
- }
- var newest string
- var newestMod time.Time
- for _, e := range entries {
- if e.IsDir() {
- continue
- }
- name := e.Name()
- if filepath.Ext(name) != ".apk" {
- continue
- }
- info, err := e.Info()
- if err != nil {
- continue
- }
- if info.ModTime().After(newestMod) {
- newestMod = info.ModTime()
- newest = filepath.Join(dir, name)
- }
- }
- if newest == "" {
- return "", fmt.Errorf("no .apk found in %s", dir)
- }
- return newest, nil
+ entries, err := os.ReadDir(dir)
+ if err != nil {
+ return "", err
+ }
+ var newest string
+ var newestMod time.Time
+ for _, e := range entries {
+ if e.IsDir() {
+ continue
+ }
+ name := e.Name()
+ if filepath.Ext(name) != ".apk" {
+ continue
+ }
+ info, err := e.Info()
+ if err != nil {
+ continue
+ }
+ if info.ModTime().After(newestMod) {
+ newestMod = info.ModTime()
+ newest = filepath.Join(dir, name)
+ }
+ }
+ if newest == "" {
+ return "", fmt.Errorf("no .apk found in %s", dir)
+ }
+ return newest, nil
}
func copyFile(src, dst string) error {
- in, err := os.Open(src)
- if err != nil {
- return err
- }
- defer in.Close()
-
- out, err := os.Create(dst)
- if err != nil {
- return err
- }
- defer func() { _ = out.Close() }()
-
- if _, err = io.Copy(out, in); err != nil {
- return err
- }
- return out.Sync()
+ in, err := os.Open(src)
+ if err != nil {
+ return err
+ }
+ defer in.Close()
+
+ out, err := os.Create(dst)
+ if err != nil {
+ return err
+ }
+ defer func() { _ = out.Close() }()
+
+ if _, err = io.Copy(out, in); err != nil {
+ return err
+ }
+ return out.Sync()
}
diff --git a/android_shared_android.go b/android_shared_android.go
index b736fab..e46fab2 100644
--- a/android_shared_android.go
+++ b/android_shared_android.go
@@ -3,32 +3,24 @@
package main
import (
- "os"
- "path/filepath"
+ "os"
+ "path/filepath"
)
// readSharedFromCache tries to read the shared text written by the Android activity.
// The activity writes into getCacheDir()/quicklogger-shared.txt; on Go, os.TempDir()
// maps to the same location in Android (app cache directory).
func readSharedFromCache() (string, error) {
- dir, derr := os.UserCacheDir()
- if derr != nil || dir == "" {
- dir = os.TempDir()
- }
- path := filepath.Join(dir, "quicklogger-shared.txt")
- b, err := os.ReadFile(path)
- if err != nil {
- return "", err
- }
- // best-effort cleanup; ignore errors
- _ = os.Remove(path)
- return string(b), nil
-}
-
-func debugSharedPath() string {
- dir, _ := os.UserCacheDir()
- if dir == "" {
- dir = os.TempDir()
- }
- return filepath.Join(dir, "quicklogger-shared.txt")
+ dir, derr := os.UserCacheDir()
+ if derr != nil || dir == "" {
+ dir = os.TempDir()
+ }
+ path := filepath.Join(dir, "quicklogger-shared.txt")
+ b, err := os.ReadFile(path)
+ if err != nil {
+ return "", err
+ }
+ // best-effort cleanup; ignore errors
+ _ = os.Remove(path)
+ return string(b), nil
}
diff --git a/android_shared_stub.go b/android_shared_stub.go
index 4e6f27e..9ebe1a3 100644
--- a/android_shared_stub.go
+++ b/android_shared_stub.go
@@ -3,4 +3,3 @@
package main
func readSharedFromCache() (string, error) { return "", nil }
-
diff --git a/android_shared_stub_test.go b/android_shared_stub_test.go
new file mode 100644
index 0000000..cc62f5d
--- /dev/null
+++ b/android_shared_stub_test.go
@@ -0,0 +1,15 @@
+//go:build !android
+
+package main
+
+import "testing"
+
+func TestReadSharedFromCacheStub(t *testing.T) {
+ txt, err := readSharedFromCache()
+ if err != nil {
+ t.Fatalf("stub returned error: %v", err)
+ }
+ if txt != "" {
+ t.Errorf("stub returned non-empty text: %q", txt)
+ }
+}
diff --git a/go.mod b/go.mod
index 033fb60..d5214e4 100644
--- a/go.mod
+++ b/go.mod
@@ -2,7 +2,10 @@ module codeberg.org/snonux/quicklogger
go 1.21.6
-require fyne.io/fyne/v2 v2.4.3
+require (
+ fyne.io/fyne/v2 v2.4.3
+ github.com/magefile/mage v1.15.0
+)
require (
fyne.io/systray v1.10.1-0.20231115130155-104f5ef7839e // indirect
@@ -19,7 +22,6 @@ require (
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/gopherjs/gopherjs v1.17.2 // indirect
github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e // indirect
- github.com/magefile/mage v1.15.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
diff --git a/main.go b/main.go
index 0a32930..ea16eee 100644
--- a/main.go
+++ b/main.go
@@ -1,144 +1,161 @@
package main
import (
- "fmt"
- "os"
- "time"
-
- "fyne.io/fyne/v2"
- "fyne.io/fyne/v2/app"
- "fyne.io/fyne/v2/container"
- "fyne.io/fyne/v2/dialog"
- "fyne.io/fyne/v2/widget"
+ "errors"
+ "fmt"
+ "log"
+ "os"
+ "path/filepath"
+ "time"
+
+ "fyne.io/fyne/v2"
+ "fyne.io/fyne/v2/app"
+ "fyne.io/fyne/v2/container"
+ "fyne.io/fyne/v2/dialog"
+ "fyne.io/fyne/v2/widget"
)
const (
- appId = "org.buetow.quicklogger"
+ appID = "org.buetow.quicklogger"
placeholderText = "Enter text here..."
maxTextLength = 5000 // Limit text length to prevent performance issues
)
-var (
- defaultDirectory = "."
-)
+const defaultDirectory = "."
var windowSize = fyne.NewSize(400, 100)
-func createPreferenceWindow(a fyne.App) fyne.Window {
- window := a.NewWindow("Preferences")
- directoryPreference := widget.NewEntry()
- directoryPreference.SetText(a.Preferences().StringWithFallback("Directory", defaultDirectory))
-
- window.SetContent(container.NewVBox(
- container.NewVBox(
- widget.NewLabel("Directory:"),
- directoryPreference,
- ),
- container.NewHBox(
- widget.NewButton("Save", func() {
- a.Preferences().SetString("Directory", directoryPreference.Text)
- window.Hide()
- }),
- )))
- window.Resize(windowSize)
-
- return window
+// logEntry writes text to a timestamped markdown file in dir.
+// Separates persistence logic from the UI so it can be tested independently.
+func logEntry(dir, text string) error {
+ filename := filepath.Join(dir, "ql-"+time.Now().Format("060102-150405")+".md")
+ return os.WriteFile(filename, []byte(text), 0o644)
}
-func createMainWindow(a fyne.App) fyne.Window {
- // Create main window
- window := a.NewWindow("Quick logger")
-
+// newInputWidget creates the multi-line text entry with platform-appropriate
+// wrapping and row count settings.
+func newInputWidget() *widget.Entry {
input := widget.NewMultiLineEntry()
- // Optimization 1: Disable word wrapping on Android to improve performance
- // Word wrapping causes expensive recalculations on every text change
+ input.SetPlaceHolder(placeholderText)
+
+ // On mobile, disable word wrapping and reduce visible rows to limit
+ // expensive recalculations and rendering area.
if fyne.CurrentDevice().IsMobile() {
input.Wrapping = fyne.TextWrapOff
+ input.SetMinRowsVisible(10)
} else {
input.Wrapping = fyne.TextWrapWord
+ input.SetMinRowsVisible(30)
}
- input.SetPlaceHolder(placeholderText)
- // Optimization 2: Reduce visible rows on mobile to limit rendering area
- if fyne.CurrentDevice().IsMobile() {
- input.SetMinRowsVisible(10)
- } else {
- input.SetMinRowsVisible(30)
- }
-
- // Optimization 3: Add text length indicator
- charCount := widget.NewLabel("0 chars")
- // Optimization 4: Throttle text changes with validation
- input.OnChanged = func(text string) {
- // Update character count
- charCount.SetText(fmt.Sprintf("%d chars", len(text)))
+ return input
+}
+
+func createPreferenceWindow(a fyne.App) fyne.Window {
+ window := a.NewWindow("Preferences")
+ directoryPreference := widget.NewEntry()
+ directoryPreference.SetText(a.Preferences().StringWithFallback("Directory", defaultDirectory))
+
+ window.SetContent(container.NewVBox(
+ container.NewVBox(
+ widget.NewLabel("Directory:"),
+ directoryPreference,
+ ),
+ container.NewHBox(
+ widget.NewButton("Save", func() {
+ a.Preferences().SetString("Directory", directoryPreference.Text)
+ window.Hide()
+ }),
+ )))
+ window.Resize(windowSize)
+
+ return window
+}
+
+func createMainWindow(a fyne.App) fyne.Window {
+ window := a.NewWindow("Quick logger")
+ input := newInputWidget()
+ charCount := widget.NewLabel("0 chars")
- // Warn if text is getting too long
- if len(text) > maxTextLength {
+ // Track whether the length warning has been shown so we don't fire a
+ // modal dialog on every keystroke above the limit.
+ warnShown := false
+ input.OnChanged = func(text string) {
+ charCount.SetText(fmt.Sprintf("%d chars", len(text)))
+ if len(text) > maxTextLength && !warnShown {
+ warnShown = true
dialog.ShowInformation("Text Limit",
fmt.Sprintf("Text is getting long (%d chars). Consider logging to avoid performance issues.", len(text)),
window)
+ } else if len(text) <= maxTextLength {
+ warnShown = false
+ }
+ }
+
+ // resetInput clears the text entry and character count.
+ resetInput := func() {
+ input.SetText("")
+ charCount.SetText("0 chars")
+ }
+
+ logTextButton := widget.NewButton("Log text", func() {
+ dir := a.Preferences().StringWithFallback("Directory", defaultDirectory)
+ if err := logEntry(dir, input.Text); err != nil {
+ dialog.ShowError(err, window)
+ return
+ }
+ resetInput()
+ })
+
+ clearButton := widget.NewButton("Clear", func() {
+ resetInput()
+ window.Canvas().Focus(input)
+ })
+
+ // loadSharedText reads Android-shared text from cache and populates the input.
+ // Used both at startup and when the app returns to the foreground.
+ // A missing cache file is expected (no share pending); real errors are logged.
+ loadSharedText := func() {
+ txt, err := readSharedFromCache()
+ if err != nil {
+ if !errors.Is(err, os.ErrNotExist) {
+ log.Printf("readSharedFromCache: %v", err)
+ }
+ return
+ }
+ if txt != "" {
+ input.SetText(txt)
+ charCount.SetText(fmt.Sprintf("%d chars", len(txt)))
+ window.Canvas().Focus(input)
}
}
- logTextButton := widget.NewButton("Log text", func() {
- filename := fmt.Sprintf("%s/ql-%s.md",
- a.Preferences().StringWithFallback("Directory", defaultDirectory),
- time.Now().Format("060102-150405"),
- )
- if err := os.WriteFile(filename, []byte(input.Text), 0644); err != nil {
- dialog.ShowError(err, window)
- return
- }
- input.SetText("")
- input.SetPlaceHolder(placeholderText)
- // Reset character count
- charCount.SetText("0 chars")
- })
-
- // Optimization 5: Add clear button for quick text clearing
- clearButton := widget.NewButton("Clear", func() {
- input.SetText("")
- charCount.SetText("0 chars")
- window.Canvas().Focus(input)
- })
-
- // If running on Android and shared text file exists, load it into the input
- if fyne.CurrentDevice().IsMobile() {
- if txt, err := readSharedFromCache(); err == nil && txt != "" {
- input.SetText(txt)
- charCount.SetText(fmt.Sprintf("%d chars", len(txt)))
- }
- }
-
- window.SetContent(container.NewVBox(
- input,
- container.NewHBox(
- logTextButton,
- clearButton,
- widget.NewButton("Preferences", func() {
- createPreferenceWindow(a).Show()
- }),
- charCount, // Show character count
- ),
- ))
- window.Resize(windowSize)
- window.Canvas().Focus(input)
-
- // On Android, also check for new shared text whenever app returns to foreground.
- if lc := a.Lifecycle(); lc != nil {
- lc.SetOnEnteredForeground(func() {
- if txt, err := readSharedFromCache(); err == nil && txt != "" {
- input.SetText(txt)
- charCount.SetText(fmt.Sprintf("%d chars", len(txt)))
- window.Canvas().Focus(input)
- }
- })
- }
+ if fyne.CurrentDevice().IsMobile() {
+ loadSharedText()
+ }
+
+ window.SetContent(container.NewVBox(
+ input,
+ container.NewHBox(
+ logTextButton,
+ clearButton,
+ widget.NewButton("Preferences", func() {
+ createPreferenceWindow(a).Show()
+ }),
+ charCount,
+ ),
+ ))
+ window.Resize(windowSize)
+ window.Canvas().Focus(input)
+
+ // On Android, also check for new shared text whenever app returns to foreground.
+ if lc := a.Lifecycle(); lc != nil {
+ lc.SetOnEnteredForeground(loadSharedText)
+ }
return window
}
func main() {
- createMainWindow(app.NewWithID(appId)).ShowAndRun()
+ createMainWindow(app.NewWithID(appID)).ShowAndRun()
}
diff --git a/main_test.go b/main_test.go
new file mode 100644
index 0000000..085dc24
--- /dev/null
+++ b/main_test.go
@@ -0,0 +1,62 @@
+package main
+
+import (
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+)
+
+func TestLogEntryCreatesFile(t *testing.T) {
+ dir := t.TempDir()
+ text := "hello world"
+
+ if err := logEntry(dir, text); err != nil {
+ t.Fatalf("logEntry returned error: %v", err)
+ }
+
+ entries, err := os.ReadDir(dir)
+ if err != nil {
+ t.Fatalf("reading dir: %v", err)
+ }
+ if len(entries) != 1 {
+ t.Fatalf("expected 1 file, got %d", len(entries))
+ }
+
+ name := entries[0].Name()
+ if !strings.HasPrefix(name, "ql-") || !strings.HasSuffix(name, ".md") {
+ t.Errorf("unexpected filename pattern: %s", name)
+ }
+
+ content, err := os.ReadFile(filepath.Join(dir, name))
+ if err != nil {
+ t.Fatalf("reading file: %v", err)
+ }
+ if string(content) != text {
+ t.Errorf("expected %q, got %q", text, string(content))
+ }
+}
+
+func TestLogEntryInvalidDir(t *testing.T) {
+ err := logEntry("/nonexistent/path/that/should/not/exist", "test")
+ if err == nil {
+ t.Fatal("expected error for invalid directory, got nil")
+ }
+}
+
+func TestLogEntryEmptyText(t *testing.T) {
+ dir := t.TempDir()
+ if err := logEntry(dir, ""); err != nil {
+ t.Fatalf("logEntry with empty text returned error: %v", err)
+ }
+
+ entries, _ := os.ReadDir(dir)
+ if len(entries) != 1 {
+ t.Fatalf("expected 1 file, got %d", len(entries))
+ }
+
+ content, _ := os.ReadFile(filepath.Join(dir, entries[0].Name()))
+ if len(content) != 0 {
+ t.Errorf("expected empty file, got %d bytes", len(content))
+ }
+}