diff --git a/README.md b/README.md index 7bef7da..68901ac 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ downloading. ```sh omarchy-send # uses config / sensible defaults -omarchy-send --alias my-server # override the advertised name for this run +omarchy-send --alias my-server # override the advertised device name omarchy-send --port 53317 # override the listen port omarchy-send --dir ~/Downloads # override the receive directory omarchy-send --auto-accept # accept incoming transfers without a prompt @@ -67,6 +67,15 @@ On Omarchy, the TUI reads the active theme's `~/.config/omarchy/current/theme/co and matches it. Elsewhere (headless / over SSH) it falls back to **ANSI palette colours**, so it tracks whatever colour scheme the connecting terminal uses. +### Device name + +On first run, each device is given a random, friendly display name such as +**"Crimson Quasar"** (a colour + a celestial object), generated once and saved. +This avoids broadcasting your machine's hostname to everyone on the network — +handy on a laptop joining untrusted Wi-Fi. The hostname is still carried in the +device-model field, and you can set any name you like with `--alias` or the `e` +key in the Settings tab. + Config (including the generated TLS identity) is stored at `~/.config/omarchy-send/config.json`. Received files default to `~/Omarchy-Send/`. diff --git a/internal/config/config.go b/internal/config/config.go index f5f40d5..a42d38e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -49,7 +49,11 @@ func defaults() Config { } home, _ := os.UserHomeDir() return Config{ - Alias: host, + // Alias is intentionally empty here; Load generates a random sci-fi + // alias once on first run (see randomAlias) and persists it. The + // hostname is still carried in DeviceModel so the machine stays + // identifiable to peers that look past the display name. + Alias: "", Port: protocol.DefaultPort, ReceiveDir: filepath.Join(home, "Omarchy-Send"), DeviceModel: host, @@ -85,8 +89,10 @@ func Load() (Config, error) { // Backfill anything still empty after unmarshalling an older/partial file. d := defaults() + // Generate a sci-fi alias once on first run (no file, or a file with no + // alias). It is persisted below, so the name stays stable across restarts. if cfg.Alias == "" { - cfg.Alias = d.Alias + cfg.Alias = randomAlias() } if cfg.Port == 0 { cfg.Port = d.Port diff --git a/internal/config/names.go b/internal/config/names.go new file mode 100644 index 0000000..5faff6e --- /dev/null +++ b/internal/config/names.go @@ -0,0 +1,59 @@ +package config + +import ( + "crypto/rand" + "math/big" +) + +// Colour + celestial-object name parts, all copyright-safe. Colours are generic +// English; the objects are public-domain astronomical terms — types of stars +// and galaxies (Nova, Quasar, Pulsar, Spiral, …). No trademarked or franchise +// names are used, so a generated alias reads like a celestial body ("Crimson +// Nova") with no IP exposure. +// +// This exists because the alias is broadcast in plaintext over multicast to the +// whole subnet every few seconds while the TUI is open. On a roaming laptop the +// machine hostname would leak to strangers on untrusted networks; a randomised +// alias avoids that, matching LocalSend's behaviour. The hostname is still +// carried in DeviceModel and any alias is overridable via --alias or Settings. +var ( + aliasColours = []string{ + "Crimson", "Scarlet", "Azure", "Cobalt", "Emerald", "Amber", + "Ivory", "Onyx", "Silver", "Graphite", "Burgundy", "Teal", + "Indigo", "Bronze", "Pearl", "Slate", "Olive", "Maroon", + "Turquoise", "Gold", "Copper", "Jade", "Ruby", "Sapphire", + "Midnight", "Sand", "Charcoal", "Cream", + "Coral", "Violet", "Lavender", "Plum", "Rose", "Salmon", + "Peach", "Saffron", "Mustard", "Lime", "Mint", "Sage", + "Forest", "Cyan", "Sky", "Navy", "Steel", "Pewter", + "Ash", "Ebony", "Obsidian", "Cherry", "Rust", "Sienna", + "Tan", "Khaki", "Brass", "Platinum", + } + aliasObjects = []string{ + "Nova", "Supernova", "Pulsar", "Quasar", "Magnetar", "Nebula", + "Comet", "Meteor", "Aurora", "Corona", "Eclipse", "Cosmos", + "Galaxy", "Halo", "Spiral", "Starburst", "Dwarf", "Giant", + "Supergiant", "Hypergiant", "Cluster", "Drift", + "Blazar", "Protostar", "Neutron", "Binary", "Cepheid", "Andromeda", + "Whirlpool", "Pinwheel", "Sombrero", "Triangulum", "Sunflower", "Flare", + "Plasma", "Photon", "Zenith", "Apogee", "Solstice", "Equinox", + "Meridian", "Vortex", "Ember", "Beacon", + } +) + +// randomAlias returns a colour + celestial-object display name, e.g. +// "Crimson Nova". It is generated once on first run and persisted by Load, so a +// device keeps the same name across restarts. +func randomAlias() string { + return pick(aliasColours) + " " + pick(aliasObjects) +} + +// pick chooses a uniformly random element using crypto/rand (no seeding, always +// unpredictable). It falls back to the first element only if the RNG errors. +func pick(s []string) string { + n, err := rand.Int(rand.Reader, big.NewInt(int64(len(s)))) + if err != nil { + return s[0] + } + return s[n.Int64()] +} diff --git a/internal/config/names_test.go b/internal/config/names_test.go new file mode 100644 index 0000000..c42a88e --- /dev/null +++ b/internal/config/names_test.go @@ -0,0 +1,64 @@ +package config + +import ( + "strings" + "testing" +) + +// randomAlias must always be a "Colour Object" pair drawn from the curated, +// copyright-safe word lists. +func TestRandomAliasFormat(t *testing.T) { + colours := make(map[string]bool, len(aliasColours)) + for _, w := range aliasColours { + colours[w] = true + } + objects := make(map[string]bool, len(aliasObjects)) + for _, w := range aliasObjects { + objects[w] = true + } + + for i := 0; i < 500; i++ { + a := randomAlias() + parts := strings.SplitN(a, " ", 2) + if len(parts) != 2 { + t.Fatalf("alias %q is not two words", a) + } + if !colours[parts[0]] { + t.Fatalf("alias %q: colour %q not in aliasColours", a, parts[0]) + } + if !objects[parts[1]] { + t.Fatalf("alias %q: object %q not in aliasObjects", a, parts[1]) + } + } +} + +// The word lists must contain no duplicates, or the real pool is smaller than +// it looks. +func TestAliasWordsUnique(t *testing.T) { + for _, list := range []struct { + name string + words []string + }{ + {"aliasColours", aliasColours}, + {"aliasObjects", aliasObjects}, + } { + seen := make(map[string]bool, len(list.words)) + for _, w := range list.words { + if seen[w] { + t.Errorf("%s: duplicate word %q", list.name, w) + } + seen[w] = true + } + } +} + +// Sanity check that the generator actually varies (not stuck on one value). +func TestRandomAliasVaries(t *testing.T) { + seen := make(map[string]struct{}) + for i := 0; i < 200; i++ { + seen[randomAlias()] = struct{}{} + } + if len(seen) < 10 { + t.Fatalf("randomAlias produced only %d distinct values over 200 draws", len(seen)) + } +}