diff --git a/cmd/appherder/cli.go b/cmd/appherder/cli.go index e532f8c..93d419f 100644 --- a/cmd/appherder/cli.go +++ b/cmd/appherder/cli.go @@ -224,8 +224,8 @@ func newLinkCommand(a appherder.App) *cobra.Command { return &cobra.Command{ Use: "link APP", Short: "Add an installed AppImage to PATH", - Long: "Creates a symlink in ~/.local/bin so the AppImage can be invoked by its name\n" + - "from a terminal. The app must already be installed in ~/AppImages.", + Long: "Creates a symlink in the configured bin directory so the AppImage can be\n" + + "invoked by its name from a terminal. The app must already be installed.", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { if err := a.Link(args[0]); err != nil { @@ -241,7 +241,7 @@ func newUnlinkCommand(a appherder.App) *cobra.Command { return &cobra.Command{ Use: "unlink APP", Short: "Remove an AppImage symlink from PATH", - Long: "Removes the ~/.local/bin symlink created by `appherder link`.", + Long: "Removes the bin directory symlink created by `appherder link`.", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { if err := a.Unlink(args[0]); err != nil { diff --git a/go.mod b/go.mod index 346263c..7719c65 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/ProtonMail/go-crypto v1.4.1 github.com/adrg/xdg v0.5.3 github.com/alyraffauf/goxdgdesktop v0.1.0 + github.com/pelletier/go-toml/v2 v2.2.4 github.com/spf13/cobra v1.10.2 ) diff --git a/go.sum b/go.sum index 35fb776..513098a 100644 --- a/go.sum +++ b/go.sum @@ -19,6 +19,8 @@ github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXD github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0= github.com/mikelolasagasti/xz v1.0.1/go.mod h1:muAirjiOUxPRXwm9HdDtB3uoRPrGnL85XHtokL9Hcgc= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pierrec/lz4/v4 v4.1.27 h1:+PhzhWDrjRj89TH2sw43nE3+4+W8lSxIuQadEHZyjUk= github.com/pierrec/lz4/v4 v4.1.27/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/internal/appherder/app.go b/internal/appherder/app.go index a1c4058..6f3fae6 100644 --- a/internal/appherder/app.go +++ b/internal/appherder/app.go @@ -1,8 +1,6 @@ package appherder import ( - "fmt" - "os" "path/filepath" "github.com/adrg/xdg" @@ -16,35 +14,43 @@ type App struct { iconsDir string binDir string progress Progress + config Config } -// NewApp returns an App wired to the current user's home directory. The -// applications directory honors XDG_DATA_HOME; the user bin directory -// (~/.local/bin) is not covered by the XDG spec and is derived from home. +// NewApp returns an App wired to the current user's home directory and +// ~/.config/appherder/config.toml. The applications directory honors +// XDG_DATA_HOME. func NewApp() (App, error) { - home, err := os.UserHomeDir() - if err != nil { - return App{}, fmt.Errorf("resolve home directory: %w", err) - } + cfg := loadConfig() return NewAppWithDirs( - filepath.Join(home, "AppImages"), + cfg.AppImagesDir, filepath.Join(xdg.DataHome, "applications"), - filepath.Join(home, "AppImages", ".icons"), - filepath.Join(home, ".local", "bin"), - ), nil + filepath.Join(cfg.AppImagesDir, ".icons"), + cfg.BinDir, + ).withConfig(cfg), nil } // NewAppWithDirs returns an App that uses the given directories directly, -// for tests or non-standard layouts. +// for tests or non-standard layouts. Config defaults are used. func NewAppWithDirs(appimagesDir, applicationsDir, iconsDir, binDir string) App { return App{ appimagesDir: appimagesDir, applicationsDir: applicationsDir, iconsDir: iconsDir, binDir: binDir, + config: Config{ + AppImagesDir: appimagesDir, + MaxSavedVersions: 3, + BinDir: binDir, + }, } } +func (a App) withConfig(cfg Config) App { + a.config = cfg + return a +} + // WithProgress returns a copy of App that reports download progress to p. func (a App) WithProgress(p Progress) App { a.progress = p diff --git a/internal/appherder/appimage.go b/internal/appherder/appimage.go index f331c1e..83cda0e 100644 --- a/internal/appherder/appimage.go +++ b/internal/appherder/appimage.go @@ -227,7 +227,7 @@ func copyTo(src string, dest io.Writer) error { } // saveToVersions hardlinks src into .versions/appName/.appimage, -// pruning older versions to keep at most maxSavedVersions. +// pruning older versions to keep at most MaxSavedVersions from config. func (a App) saveToVersions(src, appName string) error { if _, err := os.Stat(src); os.IsNotExist(err) { return nil @@ -238,7 +238,7 @@ func (a App) saveToVersions(src, appName string) error { return fmt.Errorf("create versions directory: %w", err) } - a.pruneVersions(versionsDir, maxSavedVersions-1) + a.pruneVersions(versionsDir, a.config.MaxSavedVersions-1) dest := filepath.Join(versionsDir, version+".appimage") os.Remove(dest) @@ -248,8 +248,6 @@ func (a App) saveToVersions(src, appName string) error { return nil } -const maxSavedVersions = 3 - // pruneVersions removes the oldest saved versions when the directory holds // more than keep files, sorting by mtime. func (a App) pruneVersions(dir string, keep int) { diff --git a/internal/appherder/config.go b/internal/appherder/config.go new file mode 100644 index 0000000..67bab8e --- /dev/null +++ b/internal/appherder/config.go @@ -0,0 +1,50 @@ +package appherder + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/adrg/xdg" + "github.com/pelletier/go-toml/v2" +) + +type Config struct { + AppImagesDir string `toml:"appimages_dir"` + MaxSavedVersions int `toml:"max_saved_versions"` + BinDir string `toml:"bin_dir"` +} + +func configPath() string { + return filepath.Join(xdg.ConfigHome, "appherder", "config.toml") +} + +func loadConfig() Config { + home, _ := os.UserHomeDir() + cfg := Config{ + AppImagesDir: filepath.Join(home, "AppImages"), + MaxSavedVersions: 3, + BinDir: filepath.Join(home, ".local", "bin"), + } + + data, err := os.ReadFile(configPath()) + if err != nil { + return cfg + } + + if err := toml.Unmarshal(data, &cfg); err != nil { + fmt.Fprintf(os.Stderr, "Warning: Failed to parse config file: %v\n", err) + fmt.Fprintf(os.Stderr, "Using default configuration\n") + return Config{ + AppImagesDir: filepath.Join(home, "AppImages"), + MaxSavedVersions: 3, + BinDir: filepath.Join(home, ".local", "bin"), + } + } + + if cfg.MaxSavedVersions < 1 { + cfg.MaxSavedVersions = 1 + } + + return cfg +}