commands

package
v0.1.9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 20, 2024 License: MIT Imports: 16 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var AppConfigCommand = cli.Command{
	Name:    "app-config",
	Aliases: []string{"ac"},
	Usage:   "Related to application configuration",
	Commands: []*cli.Command{
		&AppConfigSetIdCommand,
	},
}
View Source
var AppConfigSetIdCommand = cli.Command{
	Name:  "set-id",
	Usage: "Update an application's identifier",
	Action: func(_ context.Context, cmd *cli.Command) error {
		utils.LogDebug("reading config")
		config, err := core.GetConfig()
		if err != nil {
			return err
		}

		args := cmd.Args()
		if args.Len() == 0 {
			return errors.New("no application id specified")
		}
		if args.Len() == 1 {
			return errors.New("no new application id specified")
		}
		if args.Len() > 2 {
			return errors.New("unexpected excessive arguments")
		}

		fromAppId := args.Get(0)
		toAppId := args.Get(1)
		utils.LogDebug(fmt.Sprintf("argument from-id: %v", fromAppId))
		utils.LogDebug(fmt.Sprintf("argument to-id: %v", toAppId))

		if _, ok := config.Installed[fromAppId]; !ok {
			return fmt.Errorf(
				"application with id %s is not installed",
				color.CyanString(fromAppId),
			)
		}

		toAppId = core.ConstructAppId(toAppId)
		utils.LogDebug(fmt.Sprintf("clean to-id: %v", toAppId))
		if toAppId == "" {
			return errors.New("invalid application id")
		}

		fromAppConfigPath := core.GetAppConfigPath(config, fromAppId)
		utils.LogDebug(fmt.Sprintf("reading app config from %s", fromAppConfigPath))
		app, err := core.ReadAppConfig(fromAppConfigPath)
		if err != nil {
			return err
		}
		fromAppPaths := app.Paths
		toAppPaths := core.ConstructAppPaths(config, toAppId, &core.ConstructAppPathsOptions{
			Symlink: fromAppPaths.Symlink != "",
		})
		app.Id = toAppId
		app.Paths = *toAppPaths
		delete(config.Installed, fromAppId)
		config.Installed[toAppId] = toAppPaths.Config
		utils.LogDebug(fmt.Sprintf("moving from %s to %s", fromAppPaths.Dir, toAppPaths.Dir))
		if err = os.Rename(fromAppPaths.Dir, toAppPaths.Dir); err != nil {
			return err
		}
		fromAppImagePath := path.Join(toAppPaths.Dir, path.Base(fromAppPaths.AppImage))
		if err = os.Rename(fromAppImagePath, toAppPaths.AppImage); err != nil {
			return err
		}
		fromIconPath := path.Join(toAppPaths.Dir, path.Base(fromAppPaths.Icon))
		if err = os.Rename(fromIconPath, toAppPaths.Icon); err != nil {
			return err
		}
		utils.LogDebug(fmt.Sprintf("saving app config to %s", toAppPaths.Config))
		if err = core.SaveAppConfig(toAppPaths.Config, app); err != nil {
			return err
		}
		utils.LogDebug("saving config")
		if err = core.SaveConfig(config); err != nil {
			return err
		}
		utils.LogDebug(fmt.Sprintf("reading .desktop file at %s", fromAppPaths.Desktop))
		desktopContent, err := os.ReadFile(fromAppPaths.Desktop)
		if err != nil {
			return err
		}
		utils.LogDebug(fmt.Sprintf("uninstalling .desktop file at %s", fromAppPaths.Desktop))
		if err = core.UninstallDesktopFile(fromAppPaths.Desktop); err != nil {
			return err
		}
		utils.LogDebug(fmt.Sprintf("installing .desktop file at %s", fromAppPaths.Desktop))
		if err = core.InstallDesktopFile(toAppPaths, string(desktopContent)); err != nil {
			return err
		}
		if toAppPaths.Symlink != "" {
			utils.LogDebug(fmt.Sprintf("removing symlink at %s", fromAppPaths.Symlink))
			if err = os.Remove(fromAppPaths.Symlink); err != nil {
				return err
			}
			utils.LogDebug(fmt.Sprintf("creating symlink at %s", toAppPaths.Symlink))
			if err = os.Symlink(toAppPaths.AppImage, toAppPaths.Symlink); err != nil {
				return err
			}
		}

		utils.LogLn()
		utils.LogInfo(
			fmt.Sprintf(
				"%s Renamed %s to %s successfully!",
				utils.LogTickPrefix,
				color.CyanString(fromAppId),
				color.CyanString(toAppId),
			),
		)

		return nil
	},
}
View Source
var InitCommand = cli.Command{
	Name:  "init",
	Usage: "Initialize and setup necessities",
	Flags: []cli.Flag{
		&cli.StringFlag{
			Name:  "apps-dir",
			Usage: "AppImages directory",
		},
		&cli.StringFlag{
			Name:  "apps-desktop-dir",
			Usage: ".desktop files directory",
		},
		&cli.StringFlag{
			Name:  "apps-desktop-dir",
			Usage: ".desktop files directory",
		},
		&cli.StringFlag{
			Name:  "apps-link-dir",
			Usage: "AppImage symlinks directory",
		},
		&cli.BoolFlag{
			Name:  "enable-integration-prompt",
			Usage: "Enables AppImageLauncher's integration prompt",
		},
		&cli.BoolFlag{
			Name:  "overwrite",
			Usage: "Overwrite config if exists",
		},
		&cli.BoolFlag{
			Name:    "assume-yes",
			Aliases: []string{"y"},
			Usage:   "Automatically answer 'yes' for questions",
		},
	},
	Action: func(_ context.Context, cmd *cli.Command) error {
		appsDir := cmd.String("apps-dir")
		appsDesktopDir := cmd.String("apps-desktop-dir")
		appsLinkDir := cmd.String("apps-link-dir")
		enableIntegrationPromptSet, enableIntegrationPrompt := utils.CommandBoolSetAndValue(cmd, "enable-integration-prompt")
		overwrite := cmd.Bool("overwrite")
		assumeYes := cmd.Bool("assume-yes")
		utils.LogDebug(fmt.Sprintf("argument apps-dir: %s", appsDir))
		utils.LogDebug(fmt.Sprintf("argument apps-desktop-dir: %s", appsDesktopDir))
		utils.LogDebug(fmt.Sprintf("argument apps-link-dir: %s", appsLinkDir))
		utils.LogDebug(fmt.Sprintf("argument enable-integration-prompt: %v", enableIntegrationPrompt))
		utils.LogDebug(fmt.Sprintf("argument overwrite: %v", overwrite))
		utils.LogDebug(fmt.Sprintf("argument assume-yes: %v", assumeYes))

		reader := bufio.NewReader(os.Stdin)
		configPath, err := core.GetConfigPath()
		if err != nil {
			return err
		}
		configExists, err := utils.FileExists(configPath)
		if err != nil {
			return err
		}
		if configExists {
			utils.LogWarning("config already exists")
			if !overwrite {
				if assumeYes {
					return fmt.Errorf(
						"pass in %s flag to overwrite configuration file",
						color.CyanString("--overwrite"),
					)
				}
				proceed, err := utils.PromptYesNoInput(
					reader,
					"Do you want to re-initiliaze configuration file?",
				)
				if err != nil {
					return err
				}
				if !proceed {
					return nil
				}
			}
		}

		homeDir, err := os.UserHomeDir()
		if err != nil {
			return err
		}

		if appsDir == "" {
			appsDir = path.Join(homeDir, ".local/share", core.AppCodeName, "applications")
			if !assumeYes {
				appsDir, err = utils.PromptTextInput(
					reader,
					"Where do you want to store the AppImages?",
					appsDir,
				)
				if err != nil {
					return err
				}
			}
		}
		if appsDir == "" {
			return errors.New("invalid application storage folder path")
		}
		appsDir, err = utils.ResolvePath(appsDir)
		if err != nil {
			return err
		}

		if appsDesktopDir == "" {
			appsDesktopDir = path.Join(homeDir, ".local/share", "applications")
			if !assumeYes {
				appsDesktopDir, err = utils.PromptTextInput(
					reader,
					"Where do you want to store the .desktop files?",
					appsDesktopDir,
				)
				if err != nil {
					return err
				}
			}
		}
		if appsDesktopDir == "" {
			return errors.New("invalid application desktop folder path")
		}
		appsDesktopDir, err = utils.ResolvePath(appsDesktopDir)
		if err != nil {
			return err
		}

		if appsLinkDir == "" {
			appsLinkDir = path.Join(homeDir, ".local/bin")
			if !assumeYes {
				appsLinkDir, err = utils.PromptTextInput(
					reader,
					"Where do you want to symlink AppImage files?",
					appsLinkDir,
				)
				if err != nil {
					return err
				}
			}
		}
		if appsLinkDir == "" {
			return errors.New("invalid application links folder path")
		}
		appsLinkDir, err = utils.ResolvePath(appsLinkDir)
		if err != nil {
			return err
		}

		if !enableIntegrationPromptSet {
			if !assumeYes {
				enableIntegrationPrompt, err = utils.PromptYesNoInput(
					reader,
					"Do you want to disable AppImageLauncher's integration prompt?",
				)
				if err != nil {
					return err
				}
			}
		}

		utils.LogLn()
		summary := utils.NewLogTable()
		summary.Add(utils.LogRightArrowPrefix, "Configuration file", color.CyanString(configPath))
		summary.Add(utils.LogRightArrowPrefix, "AppImages directory", color.CyanString(appsDir))
		summary.Add(utils.LogRightArrowPrefix, ".desktop files directory", color.CyanString(appsDesktopDir))
		summary.Print()
		utils.LogLn()

		if !assumeYes {
			proceed, err := utils.PromptYesNoInput(reader, "Do you want to proceed?")
			if err != nil {
				return err
			}
			if !proceed {
				utils.LogWarning("aborted...")
				return nil
			}
		}

		if err := os.MkdirAll(path.Dir(configPath), os.ModePerm); err != nil {
			return err
		}
		if err := os.MkdirAll(path.Dir(appsDir), os.ModePerm); err != nil {
			return err
		}
		if err := os.MkdirAll(path.Dir(appsDesktopDir), os.ModePerm); err != nil {
			return err
		}
		config := &core.Config{
			AppsDir:                 appsDir,
			DesktopDir:              appsDesktopDir,
			Installed:               map[string]string{},
			EnableIntegrationPrompt: enableIntegrationPrompt,
			SymlinksDir:             appsLinkDir,
		}
		err = core.SaveConfig(config)
		if err != nil {
			return err
		}
		utils.LogInfo(fmt.Sprintf("%s Generated %s", utils.LogTickPrefix, color.CyanString(configPath)))

		return nil
	},
}
View Source
var InstallCommand = cli.Command{
	Name:    "install",
	Aliases: []string{"add"},
	Usage:   "Install an application",
	Commands: []*cli.Command{
		&InstallGithubCommand,
		&InstallLocalCommand,
		&InstallHttpCommand,
	},
}
View Source
var InstallGithubCommand = cli.Command{
	Name:    "github",
	Aliases: []string{"gh"},
	Usage:   "Install an application from Github",
	Flags: []cli.Flag{
		&cli.StringFlag{
			Name:  "id",
			Usage: "Application identifier",
		},
		&cli.StringFlag{
			Name:    "release",
			Aliases: []string{"r"},
			Usage: fmt.Sprintf(
				"Releases type such as %s",
				strings.Join(githubSourceReleaseStrings, ", "),
			),
			Value: githubSourceReleaseStrings[0],
		},
		&cli.BoolFlag{
			Name:    "link",
			Aliases: []string{"l"},
			Usage:   "Creates a symlink",
		},
		&cli.BoolFlag{
			Name:    "assume-yes",
			Aliases: []string{"y"},
			Usage:   "Automatically answer yes for questions",
		},
	},
	Action: func(_ context.Context, cmd *cli.Command) error {
		utils.LogDebug("reading config")
		config, err := core.GetConfig()
		if err != nil {
			return err
		}

		reader := bufio.NewReader(os.Stdin)
		args := cmd.Args()
		if args.Len() == 0 {
			return errors.New("no url specified")
		}
		if args.Len() > 1 {
			return errors.New("unexpected excessive arguments")
		}

		url := args.Get(0)
		appId := cmd.String("id")
		releaseType := cmd.String("release")
		link := cmd.Bool("link")
		assumeYes := cmd.Bool("assume-yes")
		utils.LogDebug(fmt.Sprintf("argument url: %s", url))
		utils.LogDebug(fmt.Sprintf("argument id: %s", appId))
		utils.LogDebug(fmt.Sprintf("argument release: %v", releaseType))
		utils.LogDebug(fmt.Sprintf("argument link: %v", link))
		utils.LogDebug(fmt.Sprintf("argument assume-yes: %v", assumeYes))

		isValidUrl, ghUsername, ghReponame := core.ParseGithubRepoUrl(url)
		utils.LogDebug(fmt.Sprintf("parsed github url valid: %v", isValidUrl))
		utils.LogDebug(fmt.Sprintf("parsed github owner: %s", ghUsername))
		utils.LogDebug(fmt.Sprintf("parsed github repo: %s", ghReponame))
		if !isValidUrl {
			return errors.New("invalid github repo url")
		}
		if !utils.SliceContains(githubSourceReleaseStrings, releaseType) {
			return errors.New("invalid github release type")
		}

		if appId == "" {
			appId = core.ConstructAppId(ghReponame)
		}
		appId = utils.CleanId(appId)
		utils.LogDebug(fmt.Sprintf("clean id: %s", appId))
		if appId == "" {
			return errors.New("invalid application id")
		}

		source := &core.GithubSource{
			UserName: ghUsername,
			RepoName: ghReponame,
			Release:  core.GithubSourceRelease(releaseType),
		}
		release, err := source.FetchAptRelease()
		if err != nil {
			return err
		}
		utils.LogDebug(fmt.Sprintf("selected github tag name: %s", release.TagName))

		matchScore, asset := release.ChooseAptAsset()
		if matchScore == core.AppImageAssetNoMatch {
			return fmt.Errorf("no valid asset in github tag %s", release.TagName)
		}
		if matchScore == core.AppImageAssetPartialMatch {
			utils.LogWarning("no architecture specified in the asset name, cannot determine compatibility")
		}
		utils.LogDebug(fmt.Sprintf("selected asset url %s", asset.DownloadUrl))

		appPaths := core.ConstructAppPaths(config, appId, &core.ConstructAppPathsOptions{
			Symlink: link,
		})
		if _, ok := config.Installed[appId]; ok {
			utils.LogWarning(fmt.Sprintf("application with id %s already exists", appId))
			if !assumeYes {
				proceed, err := utils.PromptYesNoInput(reader, "Do you want to re-install this application?")
				if err != nil {
					return err
				}
				if !proceed {
					utils.LogWarning("aborted...")
					return nil
				}
			}
		}

		utils.LogLn()
		summary := utils.NewLogTable()
		summary.Add(utils.LogRightArrowPrefix, "Identifier", color.CyanString(appId))
		summary.Add(utils.LogRightArrowPrefix, "Version", color.CyanString(release.TagName))
		summary.Add(utils.LogRightArrowPrefix, "Filename", color.CyanString(asset.Name))
		summary.Add(utils.LogRightArrowPrefix, "AppImage", color.CyanString(appPaths.AppImage))
		summary.Add(utils.LogRightArrowPrefix, ".desktop file", color.CyanString(appPaths.Desktop))
		if appPaths.Symlink != "" {
			summary.Add(utils.LogRightArrowPrefix, "Symlink", color.CyanString(appPaths.Symlink))
		}
		summary.Add(utils.LogRightArrowPrefix, "Download Size", color.CyanString(prettyBytes(asset.Size)))
		summary.Print()
		utils.LogLn()

		if !assumeYes {
			proceed, err := utils.PromptYesNoInput(reader, "Do you want to proceed?")
			if err != nil {
				return err
			}
			if !proceed {
				utils.LogWarning("aborted...")
				return nil
			}
		}

		app := &core.AppConfig{
			Id:      appId,
			Version: release.TagName,
			Source:  core.GithubSourceId,
			Paths:   *appPaths,
		}
		utils.LogLn()
		installed, _ := InstallApps([]InstallableApp{{
			App:    app,
			Source: source,
			Asset:  asset.ToAsset(),
		}})
		if installed != 1 {
			return nil
		}

		utils.LogLn()
		utils.LogInfo(
			fmt.Sprintf(
				"%s Installed %s successfully!",
				utils.LogTickPrefix,
				color.CyanString(app.Id),
			),
		)

		return nil
	},
}
View Source
var InstallHttpCommand = cli.Command{
	Name:    "http",
	Aliases: []string{"network", "from-url"},
	Usage:   "Install AppImage from http url",
	Flags: []cli.Flag{
		&cli.StringFlag{
			Name:  "id",
			Usage: "Application identifier",
		},
		&cli.StringFlag{
			Name:  "version",
			Usage: "Application version",
		},
		&cli.BoolFlag{
			Name:    "link",
			Aliases: []string{"l"},
			Usage:   "Creates a symlink",
		},
		&cli.BoolFlag{
			Name:    "assume-yes",
			Aliases: []string{"y"},
			Usage:   "Automatically answer yes for questions",
		},
	},
	Action: func(_ context.Context, cmd *cli.Command) error {
		utils.LogDebug("reading config")
		config, err := core.GetConfig()
		if err != nil {
			return err
		}

		reader := bufio.NewReader(os.Stdin)
		args := cmd.Args()
		if args.Len() == 0 {
			return errors.New("no url specified")
		}
		if args.Len() > 1 {
			return errors.New("unexpected excessive arguments")
		}

		url := args.Get(0)
		appId := cmd.String("id")
		appVersion := cmd.String("version")
		link := cmd.Bool("link")
		assumeYes := cmd.Bool("assume-yes")
		utils.LogDebug(fmt.Sprintf("argument url: %s", url))
		utils.LogDebug(fmt.Sprintf("argument id: %s", appId))
		utils.LogDebug(fmt.Sprintf("argument link: %v", link))
		utils.LogDebug(fmt.Sprintf("argument assume-yes: %v", assumeYes))

		if url == "" {
			return errors.New("invalid url")
		}

		if appId == "" {
			appId = core.ConstructAppId(path.Base(url))
			if !assumeYes {
				appId, err = utils.PromptTextInput(
					reader,
					"What should be the Application ID?",
					appId,
				)
				if err != nil {
					return err
				}
			}
		}
		appId = utils.CleanId(appId)
		utils.LogDebug(fmt.Sprintf("clean id: %s", appId))
		if appId == "" {
			return errors.New("invalid application id")
		}

		if appVersion == "" {
			appVersion = "0.0.0"
		}

		appPaths := core.ConstructAppPaths(config, appId, &core.ConstructAppPathsOptions{
			Symlink: link,
		})
		if _, ok := config.Installed[appId]; ok {
			utils.LogWarning(
				fmt.Sprintf(
					"application with id %s already exists",
					color.CyanString(appId),
				),
			)
			if !assumeYes {
				proceed, err := utils.PromptYesNoInput(reader, "Do you want to re-install this application?")
				if err != nil {
					return err
				}
				if !proceed {
					utils.LogWarning("aborted...")
					return nil
				}
			}
		}

		utils.LogLn()
		summary := utils.NewLogTable()
		summary.Add(utils.LogRightArrowPrefix, "Identifier", color.CyanString(appId))
		summary.Add(utils.LogRightArrowPrefix, "Version", color.CyanString(appVersion))
		summary.Add(utils.LogRightArrowPrefix, "AppImage", color.CyanString(appPaths.AppImage))
		summary.Add(utils.LogRightArrowPrefix, ".desktop file", color.CyanString(appPaths.Desktop))
		if appPaths.Symlink != "" {
			summary.Add(utils.LogRightArrowPrefix, "Symlink", color.CyanString(appPaths.Symlink))
		}
		summary.Print()

		if !assumeYes {
			utils.LogLn()
			proceed, err := utils.PromptYesNoInput(reader, "Do you want to proceed?")
			if err != nil {
				return err
			}
			if !proceed {
				utils.LogWarning("aborted...")
				return nil
			}
		}

		assetMetadata, err := core.ExtractNetworkAssetMetadata(url)
		if err != nil {
			return err
		}

		app := &core.AppConfig{
			Id:      appId,
			Version: appVersion,
			Source:  core.HttpSourceId,
			Paths:   *appPaths,
		}
		source := &core.HttpSource{}
		asset := &core.Asset{
			Source:   url,
			Size:     assetMetadata.Size,
			Download: core.NetworkAssetDownload(url),
		}

		utils.LogLn()
		installed, _ := InstallApps([]InstallableApp{{
			App:    app,
			Source: source,
			Asset:  asset,
		}})
		if installed != 1 {
			return nil
		}

		utils.LogLn()
		utils.LogInfo(
			fmt.Sprintf(
				"%s Installed %s successfully!",
				utils.LogTickPrefix,
				color.CyanString(app.Id),
			),
		)

		return nil
	},
}
View Source
var InstallLocalCommand = cli.Command{
	Name:  "local",
	Usage: "Install local AppImage",
	Flags: []cli.Flag{
		&cli.StringFlag{
			Name:  "id",
			Usage: "Application identifier",
		},
		&cli.StringFlag{
			Name:  "version",
			Usage: "Application version",
		},
		&cli.BoolFlag{
			Name:    "link",
			Aliases: []string{"l"},
			Usage:   "Creates a symlink",
		},
		&cli.BoolFlag{
			Name:    "assume-yes",
			Aliases: []string{"y"},
			Usage:   "Automatically answer yes for questions",
		},
	},
	Action: func(_ context.Context, cmd *cli.Command) error {
		utils.LogDebug("reading config")
		config, err := core.GetConfig()
		if err != nil {
			return err
		}

		reader := bufio.NewReader(os.Stdin)
		args := cmd.Args()
		if args.Len() == 0 {
			return errors.New("no url specified")
		}
		if args.Len() > 1 {
			return errors.New("unexpected excessive arguments")
		}

		appImagePath := args.Get(0)
		appId := cmd.String("id")
		appVersion := cmd.String("version")
		link := cmd.Bool("link")
		assumeYes := cmd.Bool("assume-yes")
		utils.LogDebug(fmt.Sprintf("argument path: %s", appImagePath))
		utils.LogDebug(fmt.Sprintf("argument id: %s", appId))
		utils.LogDebug(fmt.Sprintf("argument link: %v", link))
		utils.LogDebug(fmt.Sprintf("argument assume-yes: %v", assumeYes))

		if appImagePath == "" {
			return errors.New("invalid appimage path")
		}
		if !path.IsAbs(appImagePath) {
			cwd, err := os.Getwd()
			if err != nil {
				return err
			}
			appImagePath = path.Join(cwd, appImagePath)
		}
		utils.LogDebug(fmt.Sprintf("resolved appimage path: %s", appImagePath))
		appImageFileInfo, err := os.Stat(appImagePath)
		if err != nil {
			return err
		}
		if appImageFileInfo.IsDir() {
			return errors.New("appimage path must be a file")
		}

		if appId == "" {
			appId = core.ConstructAppId(path.Base(appImagePath))
			if !assumeYes {
				appId, err = utils.PromptTextInput(
					reader,
					"What should be the Application ID?",
					appId,
				)
				if err != nil {
					return err
				}
			}
		}
		appId = utils.CleanId(appId)
		utils.LogDebug(fmt.Sprintf("clean id: %s", appId))
		if appId == "" {
			return errors.New("invalid application id")
		}

		if appVersion == "" {
			appVersion = "0.0.0"
		}

		appPaths := core.ConstructAppPaths(config, appId, &core.ConstructAppPathsOptions{
			Symlink: link,
		})
		if _, ok := config.Installed[appId]; ok {
			utils.LogWarning(
				fmt.Sprintf(
					"application with id %s already exists",
					color.CyanString(appId),
				),
			)
			if !assumeYes {
				proceed, err := utils.PromptYesNoInput(reader, "Do you want to re-install this application?")
				if err != nil {
					return err
				}
				if !proceed {
					utils.LogWarning("aborted...")
					return nil
				}
			}
		}

		utils.LogLn()
		summary := utils.NewLogTable()
		summary.Add(utils.LogRightArrowPrefix, "Identifier", color.CyanString(appId))
		summary.Add(utils.LogRightArrowPrefix, "Version", color.CyanString(appVersion))
		summary.Add(utils.LogRightArrowPrefix, "AppImage", color.CyanString(appPaths.AppImage))
		summary.Add(utils.LogRightArrowPrefix, ".desktop file", color.CyanString(appPaths.Desktop))
		if appPaths.Symlink != "" {
			summary.Add(utils.LogRightArrowPrefix, "Symlink", color.CyanString(appPaths.Symlink))
		}
		summary.Print()

		if !assumeYes {
			utils.LogLn()
			proceed, err := utils.PromptYesNoInput(reader, "Do you want to proceed?")
			if err != nil {
				return err
			}
			if !proceed {
				utils.LogWarning("aborted...")
				return nil
			}
		}

		app := &core.AppConfig{
			Id:      appId,
			Version: appVersion,
			Source:  core.LocalSourceId,
			Paths:   *appPaths,
		}
		source := &core.LocalSource{}
		asset := &core.Asset{
			Source:   appImagePath,
			Size:     appImageFileInfo.Size(),
			Download: core.LocalAssetDownload(appImagePath),
		}

		utils.LogLn()
		installed, _ := InstallApps([]InstallableApp{{
			App:    app,
			Source: source,
			Asset:  asset,
		}})
		if installed != 1 {
			return nil
		}

		utils.LogLn()
		utils.LogInfo(
			fmt.Sprintf(
				"%s Installed %s successfully!",
				utils.LogTickPrefix,
				color.CyanString(app.Id),
			),
		)

		return nil
	},
}
View Source
var ListCommand = cli.Command{
	Name:    "list",
	Aliases: []string{"installed"},
	Usage:   "List all installed applications",
	Action: func(_ context.Context, cmd *cli.Command) error {
		utils.LogDebug("reading config")
		config, err := core.GetConfig()
		if err != nil {
			return err
		}

		utils.LogLn()
		summary := utils.NewLogTable()
		headingColor := color.New(color.Underline, color.Bold)
		summary.Add(
			headingColor.Sprint("Index"),
			headingColor.Sprint("Application ID"),
		)
		i := 0
		for appId := range config.Installed {
			i++
			summary.Add(fmt.Sprintf("%d.", i), color.CyanString(appId))
		}
		summary.Print()
		if i == 0 {
			utils.LogInfo(color.HiBlackString("no applications are installed"))
		}
		utils.LogLn()

		return nil
	},
}
View Source
var RunCommand = cli.Command{
	Name:    "run",
	Aliases: []string{"launch"},
	Usage:   "Run an application",
	Flags: []cli.Flag{
		&cli.BoolFlag{
			Name:    "detached",
			Aliases: []string{"d"},
			Usage:   "Run as a detached process",
		},
	},
	Action: func(_ context.Context, cmd *cli.Command) error {
		utils.LogDebug("reading config")
		config, err := core.GetConfig()
		if err != nil {
			return err
		}

		args := cmd.Args()
		hasExecArgs := args.Get(1) == "--"
		if args.Len() == 0 {
			return errors.New("no application id specified")
		}
		if args.Len() > 1 && !hasExecArgs {
			return errors.New("unexpected excessive arguments")
		}

		appId := args.Get(0)
		execArgs := []string{}
		if hasExecArgs {
			execArgs = args.Slice()[2:]
		}
		detached := cmd.Bool("detached")
		utils.LogDebug(fmt.Sprintf("argument id: %s", appId))
		utils.LogDebug(fmt.Sprintf("argument exec-args: %s", strings.Join(execArgs, " ")))
		utils.LogDebug(fmt.Sprintf("argument detached: %v", detached))

		if _, ok := config.Installed[appId]; !ok {
			return fmt.Errorf(
				"application with id %s is not installed",
				color.CyanString(appId),
			)
		}

		appConfigPath := core.GetAppConfigPath(config, appId)
		utils.LogDebug(fmt.Sprintf("reading app config from %s", appConfigPath))
		app, err := core.ReadAppConfig(appConfigPath)
		if err != nil {
			return err
		}

		execPath := app.Paths.AppImage
		execDir, err := os.Getwd()
		if err != nil {
			return err
		}
		utils.LogDebug(fmt.Sprintf("exec path as %s", execPath))
		utils.LogDebug(fmt.Sprintf("exec dir as %s", execDir))

		if detached {
			detachedOptions := &utils.StartDetachedProcessOptions{
				Dir:  execDir,
				Exec: execPath,
				Args: execArgs,
			}
			if err = utils.StartDetachedProcess(detachedOptions); err != nil {
				return err
			}
			utils.LogDebug("launched detached process successfully")
			return nil
		}

		proc := exec.Command(execPath)
		proc.Dir = execDir
		proc.Stdin = os.Stdin
		proc.Stdout = os.Stdout
		proc.Stderr = os.Stderr
		if err = proc.Run(); err != nil {
			return err
		}
		return nil
	},
}
View Source
var SelfUpdateCommand = cli.Command{
	Name:    "self-update",
	Aliases: []string{"self-upgrade"},
	Flags: []cli.Flag{
		&cli.BoolFlag{
			Name:  "reinstall",
			Usage: "Forcefully update",
		},
	},
	Usage: fmt.Sprintf("Update %s", core.AppName),
	Action: func(_ context.Context, cmd *cli.Command) error {
		reinstall := cmd.Bool("reinstall")
		utils.LogDebug(fmt.Sprintf("argument reinstall: %v", reinstall))

		utils.LogDebug("fetching latest release")
		release, err := core.GithubApiFetchLatestRelease(core.AppGithubOwner, core.AppGithubRepo)
		if err != nil {
			return err
		}
		if release.TagName == fmt.Sprintf("v%s", core.AppVersion) && !reinstall {
			utils.LogInfo(
				fmt.Sprintf("%s You are already on the latest version!", utils.LogTickPrefix),
			)
			return nil
		}
		arch := utils.GetSystemArch()
		var asset *core.GithubApiReleaseAsset
		for i := range release.Assets {
			x := release.Assets[i]
			if strings.HasSuffix(x.Name, arch) {
				asset = &x
				break
			}
		}
		if asset == nil {
			return fmt.Errorf(
				"unable to find appropriate binary from release %s",
				release.TagName,
			)
		}

		utils.LogInfo(fmt.Sprintf("Updating to version %s...", color.CyanString(release.TagName)))
		utils.LogDebug(fmt.Sprintf("downloading from %s", asset.DownloadUrl))
		data, err := http.Get(asset.DownloadUrl)
		if err != nil {
			return err
		}
		defer data.Body.Close()
		executablePath, err := os.Executable()
		if err != nil {
			return err
		}
		utils.LogDebug(fmt.Sprintf("current executable path as %s", executablePath))
		tempFile, err := utils.CreateTempFile(executablePath)
		if err != nil {
			return err
		}
		utils.LogDebug(fmt.Sprintf("created %s", tempFile.Name()))
		defer tempFile.Close()
		_, err = io.Copy(tempFile, data.Body)
		if err != nil {
			return err
		}
		utils.LogDebug(fmt.Sprintf("removing %s", executablePath))
		if err = os.Remove(executablePath); err != nil {
			return err
		}
		utils.LogDebug(fmt.Sprintf("renaming %s to %s", tempFile.Name(), executablePath))
		if err = os.Rename(tempFile.Name(), executablePath); err != nil {
			return err
		}
		utils.LogDebug(fmt.Sprintf("changing permissions of %s", executablePath))
		if err = os.Chmod(executablePath, 0755); err != nil {
			return err
		}
		utils.LogInfo(
			fmt.Sprintf(
				"%s Updated to version %s successfully!",
				utils.LogTickPrefix,
				color.CyanString(release.TagName),
			),
		)

		return nil
	},
}
View Source
var TidyBrokenCommand = cli.Command{
	Name:    "tidy-broken",
	Aliases: []string{},
	Usage:   "Remove incomplete files",
	Flags: []cli.Flag{
		&cli.BoolFlag{
			Name:    "assume-yes",
			Aliases: []string{"y"},
			Usage:   "Automatically answer yes for questions",
		},
	},
	Action: func(_ context.Context, cmd *cli.Command) error {
		utils.LogDebug("reading transactions")
		transactions, err := core.GetTransactions()
		if err != nil {
			return err
		}

		reader := bufio.NewReader(os.Stdin)
		assumeYes := cmd.Bool("assume-yes")
		utils.LogDebug(fmt.Sprintf("argument assume-yes: %v", assumeYes))

		utils.LogLn()
		utils.LogInfo("List of affected directories and files:")
		involvedIds := []string{}
		involvedDirs := []string{}
		involvedFiles := []string{}
		for k, v := range transactions.PendingInstallations {
			involvedIds = append(involvedIds, k)
			involvedDirs = append(involvedDirs, v.InvolvedDirs...)
			involvedFiles = append(involvedFiles, v.InvolvedFiles...)
			for _, x := range v.InvolvedDirs {
				utils.LogInfo(
					fmt.Sprintf("%s %s", color.HiBlackString("D"), color.RedString(x)),
				)
			}
			for _, x := range v.InvolvedFiles {
				utils.LogInfo(
					fmt.Sprintf("%s %s", color.HiBlackString("F"), color.RedString(x)),
				)
			}
		}

		if len(involvedDirs)+len(involvedFiles) == 0 {
			utils.LogInfo(color.HiBlackString("no directories or files are affected"))
			utils.LogLn()
			utils.LogInfo(
				fmt.Sprintf("%s Everything is working perfectly!", utils.LogTickPrefix),
			)
			return nil
		}

		if !assumeYes {
			utils.LogLn()
			proceed, err := utils.PromptYesNoInput(reader, "Do you want to proceed?")
			if err != nil {
				return err
			}
			if !proceed {
				utils.LogWarning("aborted...")
				return nil
			}
		}

		removedDirsCount := 0
		removedFilesCount := 0
		utils.LogLn()
		for _, x := range involvedDirs {
			utils.LogDebug(fmt.Sprintf("removing %s", x))
			if err := os.RemoveAll(x); err != nil {
				utils.LogError(err)
				continue
			}
			removedDirsCount++
		}
		for _, x := range involvedFiles {
			utils.LogDebug(fmt.Sprintf("removing %s", x))
			if err := os.Remove(x); err != nil {
				utils.LogError(err)
				continue
			}
			removedFilesCount++
		}
		core.UpdateTransactions(func(transactions *core.Transactions) error {
			for _, x := range involvedIds {
				delete(transactions.PendingInstallations, x)
			}
			return nil
		})

		utils.LogLn()
		utils.LogInfo(
			fmt.Sprintf(
				"%s Removed %d directories and %d files successfully!",
				utils.LogTickPrefix,
				removedDirsCount,
				removedFilesCount,
			),
		)

		return nil
	},
}
View Source
var UninstallCommand = cli.Command{
	Name:    "uninstall",
	Aliases: []string{"remove", "delete"},
	Usage:   "Uninstall an application",
	Flags: []cli.Flag{
		&cli.BoolFlag{
			Name:    "assume-yes",
			Aliases: []string{"y"},
			Usage:   "Automatically answer yes for questions",
		},
	},
	Action: func(_ context.Context, cmd *cli.Command) error {
		config, err := core.GetConfig()
		if err != nil {
			return err
		}

		reader := bufio.NewReader(os.Stdin)
		args := cmd.Args()
		if args.Len() == 0 {
			return errors.New("no application ids specified")
		}

		appIds := args.Slice()
		assumeYes := cmd.Bool("assume-yes")
		utils.LogDebug(fmt.Sprintf("argument ids: %s", strings.Join(appIds, ", ")))
		utils.LogDebug(fmt.Sprintf("argument assume-yes: %v", assumeYes))

		utils.LogLn()
		failed := 0
		uninstallables := []core.AppConfig{}
		for _, appId := range appIds {
			if _, ok := config.Installed[appId]; !ok {
				failed++
				utils.LogError(
					fmt.Sprintf(
						"application with id %s is not installed",
						color.CyanString(appId),
					),
				)
				continue
			}
			appConfigPath := core.GetAppConfigPath(config, appId)
			utils.LogDebug(fmt.Sprintf("reading app config from %s", appConfigPath))
			app, err := core.ReadAppConfig(appConfigPath)
			if err != nil {
				failed++
				utils.LogError(err)
				continue
			}
			uninstallables = append(uninstallables, *app)
		}
		if len(uninstallables) == 0 {
			return nil
		}
		if failed > 0 {
			utils.LogLn()
		}

		summary := utils.NewLogTable()
		headingColor := color.New(color.Underline, color.Bold)
		summary.Add(
			headingColor.Sprint("Index"),
			headingColor.Sprint("Application ID"),
			headingColor.Sprint("Version"),
		)
		i := 0
		for _, x := range uninstallables {
			i++
			summary.Add(
				fmt.Sprintf("%d.", i),
				color.RedString(x.Id),
				x.Version,
			)
		}
		summary.Print()

		if !assumeYes {
			utils.LogLn()
			proceed, err := utils.PromptYesNoInput(reader, "Do you want to proceed?")
			if err != nil {
				return err
			}
			if !proceed {
				utils.LogWarning("aborted...")
				return nil
			}
		}

		utils.LogLn()
		failed = 0
		for _, x := range uninstallables {
			failed += UninstallApp(&x)
		}
		if failed > 0 {
			utils.LogLn()
			utils.LogInfo(
				fmt.Sprintf(
					"%s Uninstalled %s applications with %s errors.",
					utils.LogTickPrefix,
					color.RedString(fmt.Sprint(len(uninstallables))),
					color.RedString(fmt.Sprint(failed)),
				),
			)
		} else {
			utils.LogInfo(
				fmt.Sprintf(
					"%s Uninstalled %s applications successfully!",
					utils.LogTickPrefix,
					color.RedString(fmt.Sprint(len(uninstallables))),
				),
			)
		}

		return nil
	},
}
View Source
var UpdateCommand = cli.Command{
	Name:    "update",
	Aliases: []string{"upgrade"},
	Usage:   "Update an application",
	Flags: []cli.Flag{
		&cli.BoolFlag{
			Name:    "assume-yes",
			Aliases: []string{"y"},
			Usage:   "Automatically answer yes for questions",
		},
		&cli.BoolFlag{
			Name:  "reinstall",
			Usage: "Forcefully update and reinstall application",
		},
	},
	Action: func(_ context.Context, cmd *cli.Command) error {
		utils.LogDebug("reading config")
		config, err := core.GetConfig()
		if err != nil {
			return err
		}

		reader := bufio.NewReader(os.Stdin)
		args := cmd.Args()

		appIds := args.Slice()
		assumeYes := cmd.Bool("assume-yes")
		reinstall := cmd.Bool("reinstall")
		utils.LogDebug(fmt.Sprintf("argument ids: %s", strings.Join(appIds, ", ")))
		utils.LogDebug(fmt.Sprintf("argument assume-yes: %v", assumeYes))
		utils.LogDebug(fmt.Sprintf("argument reinstall: %v", reinstall))

		utils.LogDebug("check for self update")
		if needsSelfUpdate() {
			utils.LogInfo(
				fmt.Sprintf(
					"New version of %s is available! Use %s to update.",
					color.CyanString(core.AppName),
					color.CyanString(fmt.Sprintf("%s self-update", core.AppExecutableName)),
				),
			)
		}

		if len(appIds) == 0 {
			for x := range config.Installed {
				appIds = append(appIds, x)
			}
		}

		updateables, _, err := CheckAppUpdates(config, appIds, reinstall)
		if err != nil {
			return err
		}
		if len(updateables) == 0 {
			utils.LogLn()
			utils.LogInfo(
				fmt.Sprintf(
					"%s Everything is up-to-date.",
					utils.LogTickPrefix,
				),
			)
			return nil
		}

		utils.LogLn()
		summary := utils.NewLogTable()
		headingColor := color.New(color.Underline, color.Bold)
		summary.Add(
			headingColor.Sprint("Index"),
			headingColor.Sprint("Application ID"),
			headingColor.Sprint("Old Version"),
			headingColor.Sprint("New Version"),
		)
		i := 0
		for _, x := range updateables {
			i++
			summary.Add(
				fmt.Sprintf("%d.", i),
				color.CyanString(x.App.Id),
				x.App.Version,
				color.CyanString(x.Update.Version),
			)
		}
		summary.Print()

		if !assumeYes {
			utils.LogLn()
			proceed, err := utils.PromptYesNoInput(reader, "Do you want to proceed?")
			if err != nil {
				return err
			}
			if !proceed {
				utils.LogWarning("aborted...")
				return nil
			}
		}

		utils.LogLn()
		installables := []InstallableApp{}
		for _, x := range updateables {
			x.App.Version = x.Update.Version
			installables = append(installables, InstallableApp{
				App:    x.App,
				Source: x.Source,
				Asset:  x.Update.Asset,
			})
		}
		installed, failed := InstallApps(installables)

		utils.LogLn()
		if installed > 0 {
			utils.LogInfo(
				fmt.Sprintf(
					"%s Updated %s applications successfully!",
					utils.LogTickPrefix,
					color.CyanString(fmt.Sprint(installed)),
				),
			)
		}
		if failed > 0 {
			utils.LogInfo(
				fmt.Sprintf(
					"%s Failed to update %s applications.",
					utils.LogExclamationPrefix,
					color.RedString(fmt.Sprint(failed)),
				),
			)
		}

		return nil
	},
}
View Source
var ViewCommand = cli.Command{
	Name:    "view",
	Aliases: []string{},
	Usage:   "View an installed application",
	Action: func(_ context.Context, cmd *cli.Command) error {
		utils.LogDebug("reading config")
		config, err := core.GetConfig()
		if err != nil {
			return err
		}

		args := cmd.Args()
		if args.Len() == 0 {
			return errors.New("no application id specified")
		}
		if args.Len() > 1 {
			return errors.New("unexpected excessive arguments")
		}

		appId := args.Get(0)
		utils.LogDebug(fmt.Sprintf("argument id: %s", appId))

		if _, ok := config.Installed[appId]; !ok {
			return fmt.Errorf(
				"application with id %s is not installed",
				color.CyanString(appId),
			)
		}

		appConfigPath := core.GetAppConfigPath(config, appId)
		utils.LogDebug(fmt.Sprintf("reading app config from %s", appConfigPath))
		app, err := core.ReadAppConfig(appConfigPath)
		if err != nil {
			return err
		}

		utils.LogLn()
		summary := utils.NewLogTable()
		summary.Add(utils.LogRightArrowPrefix, "Identifier", color.CyanString(app.Id))
		summary.Add(utils.LogRightArrowPrefix, "Version", color.CyanString(app.Version))
		summary.Add(utils.LogRightArrowPrefix, "Source", color.CyanString(string(app.Source)))
		summary.Add(utils.LogRightArrowPrefix, "Directory", color.CyanString(app.Paths.Dir))
		summary.Add(utils.LogRightArrowPrefix, "AppImage", color.CyanString(app.Paths.AppImage))
		summary.Add(utils.LogRightArrowPrefix, "Icon", color.CyanString(app.Paths.Icon))
		summary.Add(utils.LogRightArrowPrefix, ".desktop file", color.CyanString(app.Paths.Desktop))
		summary.Print()
		utils.LogLn()

		return nil
	},
}

Functions

func InstallApps

func InstallApps(apps []InstallableApp) (int, int)

func UninstallApp

func UninstallApp(app *core.AppConfig) int

Types

type Installable

type Installable struct {
	Name        string
	Id          string
	DownloadUrl string
	Size        int
}

type InstallableApp

type InstallableApp struct {
	App    *core.AppConfig
	Source any
	Asset  *core.Asset

	Index          int
	Count          int
	StartedAt      int64
	Progress       int64
	RawProgress    InstallableAppRawProgress
	Speed          int64
	RemainingSecs  int64
	PrintCycle     int
	SkipCycleErase bool
	Status         InstallableAppStatus
}

func (*InstallableApp) Download

func (x *InstallableApp) Download() error

func (*InstallableApp) Install

func (x *InstallableApp) Install() error

func (*InstallableApp) Integrate

func (x *InstallableApp) Integrate() error

func (*InstallableApp) PrintStatus

func (x *InstallableApp) PrintStatus()

func (*InstallableApp) SaveConfig

func (x *InstallableApp) SaveConfig() error

func (*InstallableApp) StartStatusTicker

func (x *InstallableApp) StartStatusTicker() *time.Ticker

func (*InstallableApp) Write

func (x *InstallableApp) Write(data []byte) (n int, err error)

type InstallableAppRawProgress added in v0.1.9

type InstallableAppRawProgress struct {
	Sizes []int
	Times []int64
}

type InstallableAppStatus

type InstallableAppStatus int
const (
	InstallableAppFailed InstallableAppStatus = iota
	InstallableAppDownloading
	InstallableAppIntegrating
	InstallableAppInstalled
)

type UpdatableApp

type UpdatableApp struct {
	App    *core.AppConfig
	Source any
	Update *core.SourceUpdate
}

func CheckAppUpdate

func CheckAppUpdate(config *core.Config, appId string, reinstall bool) (*UpdatableApp, error)

func CheckAppUpdates

func CheckAppUpdates(config *core.Config, appIds []string, reinstall bool) ([]UpdatableApp, int, error)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL