Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 8 additions & 3 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ builds:
goos:
- linux
- darwin
# No windows - symlinks not supported well
- windows # Windows applies themes by copying instead of symlinking
goarch:
- amd64
- arm64
Expand All @@ -33,7 +33,7 @@ archives:
name_template: "stellar-{{ .Os }}-{{ .Arch }}"
files:
- stellar
format: binary
formats: [binary]

# Checksums
checksum:
Expand Down Expand Up @@ -69,9 +69,14 @@ release:
```bash
# Update stellar
stellar update
# Install stellar
# Install stellar (Linux / macOS)
curl -fsSL https://raw.githubusercontent.com/a3chron/stellar/main/install.sh | bash
```

```powershell
# Install stellar (Windows)
irm https://raw.githubusercontent.com/a3chron/stellar/main/install.ps1 | iex
```

**Full Changelog**: https://github.com/a3chron/stellar/compare/{{ .PreviousTag }}...{{ .Tag }}

Expand Down
59 changes: 48 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,22 @@ Easily get and switch between starship configs

## Installation

### Linux / macOS

Just run the [install script](https://raw.githubusercontent.com/a3chron/stellar/main/install.sh)
(which will download the binary and move it to `~/.local/bin`)
```bash
curl -fsSL https://raw.githubusercontent.com/a3chron/stellar/main/install.sh | bash
```

### Windows

Run the [PowerShell install script](https://raw.githubusercontent.com/a3chron/stellar/main/install.ps1)
(which downloads the binary to `%LOCALAPPDATA%\stellar\bin` and adds it to your PATH)
```powershell
irm https://raw.githubusercontent.com/a3chron/stellar/main/install.ps1 | iex
```

Check that stellar is installed with `stellar --version` or `stellar --help`,
and search for a theme you like on the [stellar hub](https://stellar-hub.vercel.app) to apply.

Expand All @@ -30,15 +40,21 @@ Some [basic usage](#basic-usage) covered here, for more info, run `stellar --hel
<span id="windows" />

> [!NOTE]
> stellar is not yet available for windows, because stellar uses symlinks, and windows is weird with symlinks.
> Windows users would need to either:
> - Run it with admin privileges
> - Enable Developer Mode in Windows 10+
> - (Use WSL -> not really "Windows")
>
> which is not optimal, and means we will have to do a special case for windows, which may take some time
> On Linux and macOS, stellar applies a theme by **symlinking** `~/.config/starship.toml` to the cached
> config, which gives you hot-reload while editing local configs.
>
> Windows handles symlinks poorly (they need Developer Mode or admin privileges), so on Windows stellar
> **copies** the theme file over `starship.toml` instead. Everything works the same, with one caveat:
> editing a local theme file does **not** live-update `starship.toml` (it's a copy, not a link) — just
> re-run `stellar apply <author>/<theme>` after editing.
>
> In the meantime, you can try out the [starship theme switcher](https://github.com/a3chron/starship-theme-switcher), the first version of stellar, with a lot of features missing, but should be able to run on anything at least.
> You can force copy mode anywhere (e.g. for testing) by setting `STELLAR_APPLY_MODE=copy`.
>
> **Release note:** the backup folder name is now the sanitized OS username (e.g. `john.doe` -> `john-doe`,
> spaces and other characters `starship.toml`'s theme-identifier parser rejects are replaced with `-`), so a
> raw Windows `DOMAIN\user`-style name always produces a valid, restorable `stellar apply <author>/backup`
> hint. Backups created before this change under the old, unsanitized folder name are left in place at
> their original path and are not migrated automatically.

## Why use

Expand Down Expand Up @@ -112,11 +128,24 @@ with beeing able to update either metadata like the theme name, description, pre

When you first use `stellar apply`, if you have an existing `~/.config/starship.toml` that's not managed by stellar, it will be automatically backed up to `~/.config/stellar/<username>/backup/1.0.toml` before creating the symlink.

This ensures your carefully crafted config is never lost :) You can apply it anytime with:
Backups are versioned. If stellar later finds another unmanaged `starship.toml` (for example one you restored or hand-wrote), it backs that up too as `2.0.toml`, then `3.0.toml`, and so on, so an earlier backup is never overwritten.

This also works in copy mode (the Windows default): if you edit the applied `starship.toml` directly, stellar notices the file no longer matches the theme it applied and backs up your edits before applying the next theme.

Stellar recognizes its own applied file by a checksum recorded in `~/.config/stellar/config.json`, independent of apply mode (symlink or copy) or OS. This means editing a cached theme file directly and re-applying it, or running `stellar clean`/`stellar clean --all` and then applying another theme, never creates a spurious backup — only a config file you actually hand-edited yourself gets preserved.

`stellar clean` (with or without `--all`) never deletes your backups either — they're preserved automatically and only removed if you explicitly run `stellar remove <username>/backup`.

This ensures your carefully crafted config is never lost :) You can apply the newest backup anytime with:
```bash
stellar apply <username>/backup
```

To restore a specific one — for instance your very first original config — pin the version:
```bash
stellar apply <username>/backup@1.0
```

You can also rename the backup folder to give it a proper theme name:
```bash
mv ~/.config/stellar/<username>/backup ~/.config/stellar/<username>/my-custom-theme
Expand All @@ -143,6 +172,10 @@ and then switch to it using `stellar apply ...`.

Because stellar is using a symlink to the currently selected config file, you get hot-reload as well for editing configs, just like with the usual `starship.toml`.

> [!NOTE]
> On Windows stellar copies the config instead of symlinking it (see the [Windows note](#windows)),
> so editing a local theme file does **not** hot-reload — re-run `stellar apply <author>/<theme>` after editing.

## Troubleshooting

### "Theme not found online, using local cache"
Expand Down Expand Up @@ -222,8 +255,13 @@ When adding new CLI features, please add corresponding E2E tests in `cmd/e2e_tes

- [x] Allow removing several themes at once: `stellar remove a3chron/ctp-green a3chron/ctp-red`
- [x] Preview: maybe cache in /tmp, os not downloading two times, but also not saving previewed themes in stellar cache
- [x] Add tests
- [x] **Windows support**: apply themes by copying instead of symlinking (`STELLAR_APPLY_MODE`), Windows release binary, PowerShell installer, and `stellar update`

- [ ] **Preview: fix bash formatting**
- [ ] **`stellar preview` on Windows**: `cmd/preview.go` only spawns terminals on macOS/Linux and returns "unsupported platform" on Windows. Needs a Windows Terminal / PowerShell branch that opens a shell with `STARSHIP_CONFIG` set.
- [ ] **Windows packaging**: consider scoop/winget packaging (leftover `stellar.exe.old` from self-update is already cleaned up on the next run).
- [ ] **CI test job**: the release workflow runs no `go test` today; add one (ideally with a `windows-latest` runner) to guard the copy path natively.
- [ ] **`stellar publish` command**: Upload local themes directly to stellar-hub
- Challenge: Need to implement CLI authentication (OAuth flow with browser redirect or API keys)
- Would read from `~/.config/stellar/<author>/<theme>/<version>.toml`
Expand All @@ -233,8 +271,7 @@ When adding new CLI features, please add corresponding E2E tests in `cmd/e2e_tes
- Requires authentication (same challenge as publish)
- Upload new version of already published theme
- Interactive prompts for version notes, dependencies, etc.
- [ ] Add progress bars for downloads?
- [x] Add tests
- [ ] Add progress bars for downloads
- [ ] Get into nix pckgs

<br />
Expand Down
33 changes: 16 additions & 17 deletions cmd/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"fmt"
"log"
"os"
"os/user"
"strings"

"github.com/a3chron/stellar/internal/api"
Expand All @@ -20,14 +19,6 @@ import (
var forceApply bool
var updateTheme bool

func getCurrentUsername() string {
currentUser, err := user.Current()
if err != nil {
return "local"
}
return currentUser.Username
}

// promptConfirmation asks for user confirmation, defaults to No
func promptConfirmation(prompt string) bool {
reader := bufio.NewReader(os.Stdin)
Expand All @@ -42,6 +33,15 @@ func promptConfirmation(prompt string) bool {
return response == "y" || response == "yes"
}

// printBackupNotice tells the user their original starship.toml was preserved
// and how to restore it. Shared by apply and rollback so both surface the
// same notice whenever backupOriginalConfig actually creates a backup.
func printBackupNotice(info *symlink.BackupInfo) {
color.Yellow("Your original starship.toml has been backed up to:")
color.Yellow(" %s", info.Path)
color.Cyan("\nYou can apply it later with: stellar apply %s \n", info.Identifier)
}

var applyCmd = &cobra.Command{
Use: "apply [author/theme[@version]]",
Short: "Apply a Starship theme",
Expand Down Expand Up @@ -174,14 +174,15 @@ var applyCmd = &cobra.Command{
return err
}

// 5. Create symlink FIRST (before saving config)
// This ensures that if symlink fails, config remains unchanged
backupPath, err := symlink.CreateSymlink(themePath)
// 5. Apply the theme FIRST (before saving config)
// This ensures that if applying fails, config remains unchanged
backupInfo, err := symlink.ApplyTheme(themePath, cfg)
if err != nil {
return err
}

// 6. Update config only AFTER symlink succeeds
// 6. Update config only AFTER applying succeeds
// (ApplyTheme has already recorded cfg.AppliedHash for the applied file)
cfg.PreviousTheme = cfg.CurrentTheme
cfg.PreviousPath = cfg.CurrentPath
cfg.CurrentTheme = t.String()
Expand All @@ -194,10 +195,8 @@ var applyCmd = &cobra.Command{
}

// Notify user if their original config was backed up
if backupPath != "" {
color.Yellow("Your original starship.toml has been backed up to:")
color.Yellow(" %s", backupPath)
color.Cyan("\nYou can apply it later with: stellar apply %s/backup \n", getCurrentUsername())
if backupInfo != nil {
printBackupNotice(backupInfo)
}

color.Green("Applied %s", t)
Expand Down
105 changes: 91 additions & 14 deletions cmd/current.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,26 @@ import (
"github.com/spf13/cobra"
)

// printReapplyHint prints the two-line diagnostic tail shown whenever
// starship.toml is in a state that a re-apply would fix (missing, broken
// symlink, replaced by a directory, unknown path): what config.json believes
// is applied, and the exact command to re-apply it.
func printReapplyHint(cfg *config.Config) {
fmt.Printf("Config says: %s\n", cfg.CurrentTheme)
fmt.Println("\nRe-apply with: stellar apply " + cfg.CurrentTheme)
}

// printThemeFileMissing prints the "Theme file missing" diagnostic. expectedLabel
// is the only part that differs between call sites (the symlink branch points at
// the link target, the regular-file branch at the cached theme file), so it's
// passed in; everything else is identical.
func printThemeFileMissing(cfg *config.Config, expectedLabel string) {
color.Red("Theme file missing")
fmt.Printf("Theme: %s\n", cfg.CurrentTheme)
fmt.Printf("%s: %s\n", expectedLabel, cfg.CurrentPath)
fmt.Println("\nRe-download with: stellar apply " + cfg.CurrentTheme)
}

var currentCmd = &cobra.Command{
Use: "current",
Short: "Show the currently applied theme",
Expand All @@ -27,22 +47,80 @@ var currentCmd = &cobra.Command{
return nil
}

// Verify symlink is still valid
target, err := symlink.GetCurrentTarget()
starshipConfig, err := symlink.StarshipConfigPath()
if err != nil {
color.Red("Symlink broken or missing")
fmt.Printf("Config says: %s\n", cfg.CurrentTheme)
fmt.Println("\nRe-apply with: stellar apply " + cfg.CurrentTheme)
return nil
return fmt.Errorf("failed to resolve starship config path: %w", err)
}

// Check if target exists
if _, err := os.Stat(target); os.IsNotExist(err) {
color.Red("Theme file missing")
fmt.Printf("Theme: %s\n", cfg.CurrentTheme)
fmt.Printf("Expected at: %s\n", cfg.CurrentPath)
fmt.Println("\nRe-download with: stellar apply " + cfg.CurrentTheme)
// Branch on what's actually on disk (via a single Lstat), not on
// STELLAR_APPLY_MODE/GOOS: a copy-mode config inspected with
// STELLAR_APPLY_MODE=symlink (or vice versa) must still get the right
// diagnostics, since the env var only affects how a *future* apply
// behaves, not what's currently on disk.
info, statErr := os.Lstat(starshipConfig)
switch {
case os.IsNotExist(statErr):
color.Red("Starship config missing")
printReapplyHint(cfg)
return nil

case statErr == nil && info.Mode()&os.ModeSymlink != 0:
// Symlink mode. Verify the link points at an existing theme file.
target, terr := symlink.GetCurrentTarget()
if terr != nil {
color.Red("Symlink broken or missing")
printReapplyHint(cfg)
return nil
}

if _, err := os.Stat(target); os.IsNotExist(err) {
printThemeFileMissing(cfg, "Expected at")
return nil
}

case statErr == nil && info.IsDir():
// Something (not stellar) replaced starship.toml with a
// directory. Neither symlink nor copy mode ever produces this,
// so there's nothing further to check.
color.Red("Starship config path is a directory, not a file")
printReapplyHint(cfg)
return nil

case statErr == nil:
// Regular file (copy mode, or a symlink-mode config someone
// hand-edited into a plain file). There's no link to read, so the
// managed-file check below (IsManaged) verifies the file's
// *content* still matches what was applied, not just that some file
// is present at cfg.CurrentPath.
if cfg.CurrentPath == "" {
color.Red("Current theme path is unknown")
printReapplyHint(cfg)
return nil
}

if _, err := os.Stat(cfg.CurrentPath); os.IsNotExist(err) {
printThemeFileMissing(cfg, "Cached theme file missing, expected at")
return nil
}

// Report "modified" using the same predicate apply uses to decide
// whether it would back up this file, so `stellar current` never
// disagrees with what the next `stellar apply` actually does.
modified := !symlink.IsManaged(starshipConfig, cfg)

if modified {
color.Red("Starship config was modified or replaced")
fmt.Printf("Theme: %s\n", cfg.CurrentTheme)
fmt.Println("starship.toml no longer matches the theme that was applied.")
fmt.Println("The next `stellar apply` will automatically back up your current starship.toml before applying.")
fmt.Println("\nRe-apply with: stellar apply " + cfg.CurrentTheme)
return nil
}

default:
// Some other Lstat error (e.g. permission denied). Surface it
// rather than silently reporting a healthy state.
return fmt.Errorf("failed to inspect starship config: %w", statErr)
}

// All good - display current theme
Expand All @@ -52,8 +130,7 @@ var currentCmd = &cobra.Command{
fmt.Printf(" Path: %s\n", cfg.CurrentPath)
fmt.Println()

// Show symlink info
starshipConfig, _ := symlink.StarshipConfigPath()
// Show starship config path
fmt.Printf(" Starship config: %s\n", starshipConfig)

return nil
Expand Down
Loading