Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
10778ef
Tiered discovery: cache probe, multicast bursts, TCP pre-filter, --qu…
bethropolis Jul 24, 2026
d05ddb4
Web share: landing page, QR code, download notifications, --once flag
bethropolis Jul 24, 2026
27c3c80
Config UX: key validation, unset, open, add/remove subcommands, origi…
bethropolis Jul 24, 2026
7b46ad0
Protocol 2.1, HTTPS fingerprint TOFU for --ip, export VerifyDeviceFin…
bethropolis Jul 24, 2026
f069b37
Send PIN: --pin flag for sender authentication
bethropolis Jul 24, 2026
598eabc
Fix .gitignore config entry, README clipboard env vars, send.go PIN URL
bethropolis Jul 24, 2026
9a5ec6d
Fix QR code rendering: use GenerateHalfBlock helper
bethropolis Jul 24, 2026
f2e6a58
Update help: document new flags, examples, config subcommands, env vars
bethropolis Jul 24, 2026
a4fd321
Wire config fields into runtime (BindAddress, FileConflictResolve, St…
bethropolis Jul 25, 2026
65acc1b
Add 'slice' type to configKey validation; change static_peers/trusted…
bethropolis Jul 28, 2026
40a3e8e
Fix clipboard send: remove 204 No Content early return in prepare-upload
bethropolis Jul 28, 2026
db7325f
Fix clipboard send: write immediately in prepare-upload, return 204
bethropolis Jul 28, 2026
eca4d8b
Fix go.mod: run go mod tidy (move qrterminal to direct deps)
bethropolis Jul 29, 2026
0fed8de
docs: update CLI reference, config docs, and help text for v0.6.5
bethropolis Jul 29, 2026
e8d051d
feat: add Termux/Android support to installer and GoReleaser
bethropolis Jul 30, 2026
02d7036
fix: enable FreeBSD compilation
bethropolis Jul 30, 2026
b806792
feat: add Termux clipboard support and installer paths
bethropolis Jul 30, 2026
47c88db
build: add -trimpath and strip to release build flags
bethropolis Jul 31, 2026
9e35831
refactor: replace gorilla/mux and beeep with stdlib equivalents
bethropolis Jul 31, 2026
555c2b1
refactor: replace zap with log/slog via logging.Logger wrapper
bethropolis Jul 31, 2026
7db4653
refactor: replace viper with hand-rolled yaml+env config source
bethropolis Jul 31, 2026
bb506c9
Merge branch 'main' into dev (keep dev content, release prep for v0.6.6)
bethropolis Aug 8, 2026
4c7c801
chore: drop changelog, restore installer box alignment (release prep …
bethropolis Aug 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ builds:
binary: localgo
env:
- CGO_ENABLED=0
flags:
- -trimpath
ldflags:
- -s -w
- -X main.Version={{.Version}}
Expand All @@ -30,13 +32,16 @@ builds:
- linux
- darwin
- windows
- android
goarch:
- amd64
- arm64
# disabling Windows ARM64 for now, as it's not tested
ignore:
- goos: windows
goarch: arm64
- goos: android
goarch: amd64

# ---------------------------------------------------------------------------
# Archives
Expand Down
243 changes: 0 additions & 243 deletions CHANGELOG.md

This file was deleted.

9 changes: 5 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ GIT_COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknow
BUILD_DATE := $(shell date -u '+%Y-%m-%dT%H:%M:%SZ')

LD_BASE := -X main.Version=$(VERSION) -X main.GitCommit=$(GIT_COMMIT) -X main.BuildDate=$(BUILD_DATE)
LDFLAGS := -ldflags "$(LD_BASE)"
LDFLAGS_STRIP := -ldflags "-s -w $(LD_BASE)"
LDFLAGS := -ldflags "-s -w $(LD_BASE)"
LDFLAGS_DEBUG := -ldflags "$(LD_BASE)"
TRIMFLAGS := -trimpath

# Colour helpers — silently degrade when not a tty
ifeq ($(TERM),)
Expand Down Expand Up @@ -53,9 +54,9 @@ all: fmt vet build ## Format, vet, and build
##@ Build

.PHONY: build
build: ## Build the binary for the current platform
build: ## Build the binary for the current platform (stripped, release-style)
$(call log,Building $(BINARY_NAME) $(VERSION))
$(GO) build $(GOFLAGS) $(LDFLAGS) -o $(BINARY_NAME) $(BUILD_DIR)
$(GO) build $(GOFLAGS) $(TRIMFLAGS) $(LDFLAGS) -o $(BINARY_NAME) $(BUILD_DIR)
@printf '%s binary: ./%s%s\n' '$(_GREEN)' '$(BINARY_NAME)' '$(_RESET)'

.PHONY: build-fast
Expand Down
153 changes: 57 additions & 96 deletions cmd/localgo/cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import (
"strconv"
"strings"

"github.com/bethropolis/localgo/pkg/config"
"github.com/bethropolis/localgo/pkg/help"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)

// configKey describes a known config key with its type and valid values.
Expand Down Expand Up @@ -153,31 +153,28 @@ func validateValue(ck configKey, key, raw string) (interface{}, error) {
return raw, nil
}

func newViperForConfig() *viper.Viper {
v := viper.New()
v.SetConfigName("config")
v.SetConfigType("yaml")
v.AddConfigPath("$HOME/.config/localgo/")
v.AddConfigPath("$HOME/.local/etc/localgo/")
v.SetEnvPrefix("LOCALSEND")
v.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
v.AutomaticEnv()

func newConfigSource() *config.Source {
src := config.LoadSource()
for key, ck := range knownConfigKeys {
if ck.defVal != nil {
v.SetDefault(key, ck.defVal)
src.SetDefault(key, ck.defVal)
}
}

_ = v.ReadInConfig()
return v
return src
}

func getConfigPath(v *viper.Viper) string {
if p := v.ConfigFileUsed(); p != "" {
return p
// originFor reports the source of a key's value: file, env, or default.
func originFor(src *config.Source, key string) (string, bool) {
if src.InFile(key) {
return "[file]", true
}
if _, ok := os.LookupEnv("LOCALSEND_" + strings.ToUpper(strings.ReplaceAll(key, "-", "_"))); ok {
return "[env]", true
}
return os.ExpandEnv("$HOME/.config/localgo/config.yaml")
if ck, ok := knownConfigKeys[key]; ok && ck.defVal != nil {
return "[default]", true
}
return "", false
}

var configCmd = &cobra.Command{
Expand All @@ -190,18 +187,18 @@ var configGetCmd = &cobra.Command{
Short: "Get a config value",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
v := newViperForConfig()
src := newConfigSource()
key := strings.ToLower(args[0])

if _, err := validateKey(key); err != nil {
return err
}

if !v.IsSet(key) {
if !src.IsSet(key) {
return fmt.Errorf("key %q not set", key)
}

fmt.Println(v.GetString(key))
fmt.Println(src.GetString(key))
return nil
},
}
Expand All @@ -211,7 +208,7 @@ var configSetCmd = &cobra.Command{
Short: "Set a config value",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
v := newViperForConfig()
src := newConfigSource()
key := strings.ToLower(args[0])

ck, err := validateKey(key)
Expand All @@ -224,18 +221,13 @@ var configSetCmd = &cobra.Command{
return err
}

v.Set(key, val)
src.Set(key, val)

configPath := getConfigPath(v)
if err := os.MkdirAll(filepath.Dir(configPath), 0700); err != nil {
return fmt.Errorf("failed to create config directory: %w", err)
}

if err := v.WriteConfigAs(configPath); err != nil {
return fmt.Errorf("failed to write config: %w", err)
if err := src.Save(); err != nil {
return err
}

fmt.Printf("Set %s = %v in %s\n", key, val, configPath)
fmt.Printf("Set %s = %v in %s\n", key, val, src.FilePath())
return nil
},
}
Expand All @@ -244,33 +236,23 @@ var configListCmd = &cobra.Command{
Use: "list",
Short: "List all config values with origin",
RunE: func(cmd *cobra.Command, args []string) error {
v := newViperForConfig()
settings := v.AllSettings()

if len(settings) == 0 {
fmt.Println("(no settings)")
return nil
}
src := newConfigSource()

fmt.Printf("%-28s %-10s %s\n", "KEY", "ORIGIN", "VALUE")
fmt.Println(strings.Repeat("-", 80))

for _, key := range v.AllKeys() {
val := v.Get(key)
if val == nil {
shown := false
for _, key := range knownKeyNames() {
origin, ok := originFor(src, key)
if !ok {
continue
}
fmt.Printf("%-28s %-10s %v\n", key, origin, src.Get(key))
shown = true
}

origin := "[env]"
if v.InConfig(key) {
origin = "[file]"
} else if _, ok := knownConfigKeys[key]; ok && knownConfigKeys[key].defVal != nil && fmt.Sprint(v.Get(key)) == fmt.Sprint(knownConfigKeys[key].defVal) {
origin = "[default]"
} else if !v.InConfig(key) {
origin = "[env]"
}

fmt.Printf("%-28s %-10s %v\n", key, origin, val)
if !shown {
fmt.Println("(no settings)")
}
return nil
},
Expand All @@ -281,35 +263,24 @@ var configUnsetCmd = &cobra.Command{
Short: "Remove a config key (reverts to default)",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
v := newViperForConfig()
src := newConfigSource()
key := strings.ToLower(args[0])

if _, err := validateKey(key); err != nil {
return err
}

if !v.InConfig(key) {
if !src.InFile(key) {
return fmt.Errorf("key %q is not in config file", key)
}

settings := v.AllSettings()
delete(settings, key)
src.Unset(key)

// Rebuild the config with the key removed
for k, val := range settings {
v.Set(k, val)
}

configPath := getConfigPath(v)
if err := os.MkdirAll(filepath.Dir(configPath), 0700); err != nil {
return fmt.Errorf("failed to create config directory: %w", err)
}

if err := v.WriteConfigAs(configPath); err != nil {
return fmt.Errorf("failed to write config: %w", err)
if err := src.Save(); err != nil {
return err
}

fmt.Printf("Removed %s from %s\n", key, configPath)
fmt.Printf("Removed %s from %s\n", key, src.FilePath())
return nil
},
}
Expand All @@ -318,16 +289,16 @@ var configOpenCmd = &cobra.Command{
Use: "open",
Short: "Open config file in system editor",
RunE: func(cmd *cobra.Command, args []string) error {
v := newViperForConfig()
configPath := getConfigPath(v)
src := newConfigSource()
configPath := src.FilePath()

if err := os.MkdirAll(filepath.Dir(configPath), 0700); err != nil {
return fmt.Errorf("failed to create config directory: %w", err)
}

// If the file doesn't exist yet, create it
if _, err := os.Stat(configPath); os.IsNotExist(err) {
if err := v.WriteConfigAs(configPath); err != nil {
if err := src.Save(); err != nil {
return fmt.Errorf("failed to create config file: %w", err)
}
}
Expand Down Expand Up @@ -367,27 +338,22 @@ var configAddCmd = &cobra.Command{
Short: "Append a value to a list config key",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
v := newViperForConfig()
src := newConfigSource()
key := strings.ToLower(args[0])

if _, err := validateKey(key); err != nil {
return err
}

current := v.GetStringSlice(key)
current := src.GetStringSlice(key)
current = append(current, args[1])
v.Set(key, current)

configPath := getConfigPath(v)
if err := os.MkdirAll(filepath.Dir(configPath), 0700); err != nil {
return fmt.Errorf("failed to create config directory: %w", err)
}
src.Set(key, current)

if err := v.WriteConfigAs(configPath); err != nil {
return fmt.Errorf("failed to write config: %w", err)
if err := src.Save(); err != nil {
return err
}

fmt.Printf("Added %q to %s in %s\n", args[1], key, configPath)
fmt.Printf("Added %q to %s in %s\n", args[1], key, src.FilePath())
return nil
},
}
Expand All @@ -397,14 +363,14 @@ var configRemoveCmd = &cobra.Command{
Short: "Remove a value from a list config key",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
v := newViperForConfig()
src := newConfigSource()
key := strings.ToLower(args[0])

if _, err := validateKey(key); err != nil {
return err
}

current := v.GetStringSlice(key)
current := src.GetStringSlice(key)
filtered := make([]string, 0, len(current))
removed := false
for _, item := range current {
Expand All @@ -419,18 +385,13 @@ var configRemoveCmd = &cobra.Command{
return fmt.Errorf("value %q not found in %s", args[1], key)
}

v.Set(key, filtered)

configPath := getConfigPath(v)
if err := os.MkdirAll(filepath.Dir(configPath), 0700); err != nil {
return fmt.Errorf("failed to create config directory: %w", err)
}
src.Set(key, filtered)

if err := v.WriteConfigAs(configPath); err != nil {
return fmt.Errorf("failed to write config: %w", err)
if err := src.Save(); err != nil {
return err
}

fmt.Printf("Removed %q from %s in %s\n", args[1], key, configPath)
fmt.Printf("Removed %q from %s in %s\n", args[1], key, src.FilePath())
return nil
},
}
Expand All @@ -439,8 +400,8 @@ var configPathCmd = &cobra.Command{
Use: "path",
Short: "Show config file path",
RunE: func(cmd *cobra.Command, args []string) error {
v := newViperForConfig()
path := getConfigPath(v)
src := newConfigSource()
path := src.FilePath()
if _, err := os.Stat(path); os.IsNotExist(err) {
fmt.Println(path + " (file does not exist yet)")
} else {
Expand Down
Loading
Loading