├── src ├── helpers │ ├── repos │ │ ├── errors.go │ │ ├── repo.go │ │ └── github.go │ └── utils │ │ ├── download.go │ │ ├── appimageCatalog.go │ │ ├── appimage.go │ │ ├── util.go │ │ ├── registry.go │ │ └── signature.go ├── commands │ ├── clean.go │ ├── remove.go │ ├── run.go │ ├── search.go │ ├── list.go │ ├── install.go │ └── update.go └── main.go ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug-report.md ├── bread.svg └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── AppImage-Builder.yml ├── make ├── go.mod └── README.md /src/helpers/repos/errors.go: -------------------------------------------------------------------------------- 1 | package repos 2 | 3 | import "errors" 4 | 5 | var InvalidTargetFormat = errors.New("invalid target format") 6 | 7 | var NoAppImageBinariesFound = errors.New("no AppImage found in releases") 8 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | Brief explanation about the changes you are proposing. 3 | 4 | ## Changes proposed 5 | List the changes you made, one or two bullets is ok, 3 or more is maybe that you are doing more than neccessary. 6 | 7 | - Change 1 8 | - Change 2 9 | - etc.. 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | # GoLand 18 | .idea/ 19 | bin/ 20 | build/ 21 | AppDir 22 | appimage-builder-cache 23 | imageHub-* -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/commands/clean.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "os" 5 | "fmt" 6 | "bread/src/helpers/utils" 7 | ) 8 | 9 | type CleanCmd struct { 10 | } 11 | 12 | func (cmd *CleanCmd) Run() (err error) { 13 | // Get the `run-cache` directory path 14 | appTempDir, err := utils.MakeTempAppDirPath() 15 | if err != nil { 16 | return err 17 | } 18 | // Remove that directory 19 | os.RemoveAll(appTempDir) 20 | fmt.Println("Cache Cleaned") 21 | 22 | registry, err := utils.OpenRegistry() 23 | if err != nil { 24 | fmt.Println("Error While Cleaning Registry: " + err.Error()) 25 | fmt.Println("Skipping...") 26 | return err 27 | } else { 28 | registry.Update() 29 | registry.Close() 30 | fmt.Println("Registry Cleaned") 31 | } 32 | 33 | return nil 34 | } -------------------------------------------------------------------------------- /src/helpers/repos/repo.go: -------------------------------------------------------------------------------- 1 | package repos 2 | 3 | import ( 4 | "strings" 5 | "bread/src/helpers/utils" 6 | ) 7 | 8 | type Release struct { 9 | Tag string 10 | Files []utils.BinaryUrl 11 | } 12 | 13 | type Application interface { 14 | Id() string 15 | GetLatestRelease(NoPreRelease bool) (*Release, error) 16 | Download(binaryUrl *utils.BinaryUrl, targetPath string) error 17 | FallBackUpdateInfo() string 18 | } 19 | 20 | // Parse String And Returns A Repo Object And Error (nil if not any) 21 | func ParseTarget(target string, tagName string) (Application, error) { 22 | target = strings.ToLower(target) 23 | // Parse The Repo As A GitHub Repo, And if there is no error return repo 24 | repo, err := NewGitHubRepo(target, tagName) 25 | if err == nil { 26 | return repo, nil 27 | } 28 | 29 | return nil, InvalidTargetFormat 30 | } 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Actual behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Screenshots/Screen recordings** 23 | If applicable, add screenshots/screen recordings to help explain your problem. 24 | 25 | **Specifications** 26 | - OS: [e.g. Ubuntu 20.04] 27 | - Bread Version: [e.g. v0.3.4] 28 | 29 | **Logs** 30 | Attach logs by the bread cli, if Possible 31 | 32 | **Additional context** 33 | Add any other context about the problem here. 34 | -------------------------------------------------------------------------------- /.github/bread.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 ADITYA MISHRA 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Build Action 3 | on: 4 | push: 5 | branches: [ main ] 6 | # paths: 7 | # - 'src/**' 8 | # - 'go.mod' 9 | # - 'go.sum' 10 | # - 'make' 11 | # - 'AppImage-Builder.yml' 12 | # - '.github/**' 13 | 14 | jobs: 15 | test-build: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Set up Go 1.x 19 | uses: actions/setup-go@v2 20 | with: 21 | go-version: ^1.18 22 | 23 | - name: Check out 24 | uses: actions/checkout@v2 25 | 26 | - name: Get dependencies 27 | run: | 28 | sudo apt-get install upx 29 | go get -v -t -d ./... 30 | go mod tidy 31 | if [ -f Gopkg.toml ]; then 32 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 33 | dep ensure 34 | fi 35 | - name: Build 36 | run: ./make --prod 37 | - name: Update Continous Release 38 | uses: "marvinpinto/action-automatic-releases@latest" 39 | with: 40 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 41 | automatic_release_tag: "continuous" 42 | prerelease: true 43 | title: "Continuous Build" 44 | files: | 45 | ./build/bread-* 46 | -------------------------------------------------------------------------------- /AppImage-Builder.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | script: 3 | - rm -rf AppDir | true 4 | - mkdir -p AppDir/bin AppDir/usr/share/icons/hicolor/scalable/apps/ 5 | - cp ./build/bread-${BREAD_VERSION}-x86_64 AppDir/bin 6 | - cp .github/bread.svg AppDir/usr/share/icons/hicolor/scalable/apps/ 7 | 8 | AppDir: 9 | path: ./AppDir 10 | 11 | app_info: 12 | id: user.DEVLOPRR.bread 13 | name: bread 14 | icon: bread 15 | version: !ENV ${BREAD_VERSION} 16 | exec: bin/bread-${BREAD_VERSION}-x86_64 17 | exec_args: $@ 18 | 19 | apt: 20 | arch: amd64 21 | sources: 22 | - sourceline: 'deb http://archive.ubuntu.com/ubuntu/ bionic main restricted universe multiverse' 23 | key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32' 24 | - sourceline: 'deb http://archive.ubuntu.com/ubuntu/ bionic-updates main restricted universe multiverse' 25 | - sourceline: 'deb http://archive.ubuntu.com/ubuntu/ bionic-backports main restricted universe multiverse' 26 | - sourceline: 'deb http://archive.neon.kde.org/user bionic main' 27 | key_url: 'http://archive.neon.kde.org/public.key' 28 | 29 | include: 30 | - libappimage0 31 | exclude: 32 | - libpcre3 33 | - binutils 34 | - dpkg-dev 35 | - gir1.2-freedesktop 36 | - python3 37 | - libxrender1 38 | 39 | AppImage: 40 | update-information: 'gh-releases-zsync|DEVLOPRR|bread|latest|*x86_64.AppImage.zsync' 41 | sign-key: None 42 | arch: x86_64 43 | -------------------------------------------------------------------------------- /src/helpers/utils/download.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/fs" 7 | "io/ioutil" 8 | "net/http" 9 | "os" 10 | "os/signal" 11 | "path/filepath" 12 | "github.com/schollz/progressbar/v3" 13 | ) 14 | 15 | // Download a file from remote 16 | func DownloadFile(url string, filePath string, permission fs.FileMode, barText string) (err error) { 17 | appDir, err := MakeApplicationsDirPath() 18 | if err != nil { 19 | return err 20 | } 21 | 22 | tempDir, err := ioutil.TempDir(appDir, "temp") 23 | if err != nil { 24 | return err 25 | } 26 | 27 | tempFilePath := tempDir + "/" + filepath.Base(filePath) 28 | output, err := os.OpenFile(tempFilePath, os.O_RDWR|os.O_CREATE, permission) 29 | if err != nil { 30 | return err 31 | } 32 | defer output.Close() 33 | 34 | resp, err := http.Get(url) 35 | if err != nil { 36 | return err 37 | } 38 | defer resp.Body.Close() 39 | 40 | bar := progressbar.DefaultBytes( 41 | resp.ContentLength, 42 | barText, 43 | ) 44 | 45 | // Handles Ctrl + C Detection 46 | go func() { 47 | sigchan := make(chan os.Signal, 1) 48 | signal.Notify(sigchan, os.Interrupt) 49 | <-sigchan 50 | 51 | _ = resp.Body.Close() 52 | _ = output.Close() 53 | _ = os.Remove(filePath) 54 | _ = os.RemoveAll(tempDir) 55 | 56 | fmt.Println("Ctrl + C, Removing Downloaded File & Exiting.") 57 | os.Exit(0) 58 | }() 59 | 60 | _, err = io.Copy(io.MultiWriter(output, bar), resp.Body) 61 | err = os.Rename(tempFilePath, filePath) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | return os.RemoveAll(tempDir) 67 | } 68 | -------------------------------------------------------------------------------- /src/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "bread/src/commands" 6 | "github.com/alecthomas/kong" 7 | ) 8 | 9 | // Variable Which will be set on the compile time using ldflags 10 | var VERSION string 11 | 12 | type VersionFlag bool 13 | 14 | var cli struct { 15 | Install commands.InstallCmd `cmd:"" help:"Install an application."` 16 | Run commands.RunCmd `cmd:"" help:"Run an application from Remote."` 17 | List commands.ListCmd `cmd:"" help:"List installed applications."` 18 | Remove commands.RemoveCmd `cmd:"" help:"Remove an application."` 19 | Update commands.UpdateCmd `cmd:"" help:"Update an application."` 20 | Search commands.SearchCmd `cmd:"" help:"Search for appliation from appimage list."` 21 | Clean commands.CleanCmd `cmd:"" help:"Clean all the cache & unused registry entries."` 22 | Version VersionFlag `name:"version" short:"v" help:"Print version information and quit"` 23 | } 24 | 25 | func (v VersionFlag) BeforeApply(app *kong.Kong, vars kong.Vars) error { 26 | if VERSION == "" { 27 | fmt.Println("Unknown Custom Build") 28 | } else { 29 | fmt.Println("Bread v" + VERSION) 30 | } 31 | app.Exit(0) 32 | return nil 33 | } 34 | 35 | func main() { 36 | ctx := kong.Parse( 37 | &cli, 38 | kong.Name("bread"), 39 | kong.Description("Install, update, remove & run AppImage from GitHub using your CLI."), 40 | kong.UsageOnError(), 41 | kong.ConfigureHelp(kong.HelpOptions{ 42 | Compact: true, 43 | })) 44 | // Call the Run() method of the selected parsed command. 45 | err := ctx.Run() 46 | ctx.FatalIfErrorf(err) 47 | } 48 | -------------------------------------------------------------------------------- /make: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eo pipefail 4 | 5 | COMPILER="go" # Compiler To Use For Building 6 | BINARY="bread" # Output Binary Name 7 | DIST="build" # Output Directory Name 8 | 9 | # Simple Hack To Get The Version Number From main.go file 10 | VERSION="0.7.2" 11 | ENTRY_FILE="src/main.go" # Main Entry File To Compile 12 | OUTPUT="$DIST/$BINARY" # Output Path Of Built Binary 13 | COMPRESSED_OUTPUT="$OUTPUT-$VERSION-x86_64" # Output path of the compressed binary 14 | COMMIT_HASH=$(git log --pretty=format:'%h' -n 1) 15 | 16 | if [[ $1 = '' || $1 = '--prod' ]]; then 17 | echo "Compiling '$ENTRY_FILE' into '$DIST'" 18 | if [[ $1 = '--prod' ]]; then 19 | # When building for production use some ldflags and upx to reduce the binary size 20 | ${COMPILER} build -ldflags "-s -w -X main.VERSION=${VERSION}" -o ${OUTPUT} -v ${ENTRY_FILE} 21 | upx -9 -o ${COMPRESSED_OUTPUT} ${OUTPUT} 22 | else 23 | ${COMPILER} build -ldflags "-X main.VERSION=${VERSION}" -o ${OUTPUT} -v ${ENTRY_FILE} 24 | fi 25 | echo "Compiled Successfully into '$OUTPUT'" 26 | elif [[ $1 = 'appimage' ]]; then 27 | echo "Building AppImage" 28 | # Set the bread version to a env variable and call appimage-builder to make the appimage 29 | BREAD_VERSION=$VERSION appimage-builder --skip-test --recipe=AppImage-Builder.yml 30 | elif [[ $1 = 'get-deps' ]]; then 31 | echo "Getting Depedencies" 32 | ${COMPILER} mod tidy 33 | go get -t -u ./... 34 | echo 'Done!' 35 | elif [[ $1 = 'clean' ]]; then 36 | rm -rfv $DIST 37 | rm -rfv appimage-builder-cache 38 | rm -rfv AppDir 39 | rm -rfv bread-*.AppImage* 40 | echo 'Done!' 41 | elif [[ $1 = 'reg' ]]; then 42 | # Simple hacky way to pretty print `.registry.json` in user's home dir 43 | node -e 'console.log(JSON.parse(require("fs").readFileSync(`${require("os").homedir()}/Applications/.registry.json`)))' 44 | else 45 | echo "Build Script '$1' Not Found!" 46 | fi 47 | -------------------------------------------------------------------------------- /src/helpers/utils/appimageCatalog.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "io/ioutil" 5 | "encoding/json" 6 | "os/user" 7 | "path/filepath" 8 | ) 9 | 10 | // Struct Contains AppImage Item Info 11 | type AppImageFeedItem struct { 12 | Name string 13 | Description string 14 | Categories []string 15 | Authors []struct { 16 | Name string 17 | Url string 18 | } 19 | License string 20 | Links []struct { 21 | Type string 22 | Url string 23 | } 24 | Icons []string 25 | Screenshots []string 26 | } 27 | 28 | // Struct Contains Catalog From https://appimage.github.io/feed.json 29 | type AppImageFeed struct { 30 | Version int 31 | Home_page_url string 32 | Feed_url string 33 | Description string 34 | Icon string 35 | Favicon string 36 | Expired bool 37 | Items []AppImageFeedItem 38 | } 39 | 40 | // Get Full Path to `.AppImageFeed.json` in Applications Dir 41 | func makeAppImageCatalogPath() (filePath string, err error) { 42 | usr, err := user.Current() 43 | if err != nil { 44 | return "", err 45 | } 46 | 47 | return filepath.Join(usr.HomeDir, "Applications", ".AppImageFeed.json"), nil 48 | } 49 | 50 | // Read `.AppImageFeed.json` file into a struct 51 | func ReadAppImageCatalog() (aifeedJson *AppImageFeed, err error) { 52 | filePath, err := makeAppImageCatalogPath() 53 | 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | myAppImageJson := &AppImageFeed{} 59 | file, err := ioutil.ReadFile(filePath) 60 | if err != nil { 61 | return nil, err 62 | } 63 | err = json.Unmarshal([]byte(file), &myAppImageJson) 64 | if err != nil { 65 | return nil, err 66 | } 67 | return myAppImageJson, nil 68 | } 69 | 70 | // Get the latest information from API 71 | func FetchAppImageCatalog() (err error) { 72 | filePath, err := makeAppImageCatalogPath() 73 | 74 | if err != nil { 75 | return err 76 | } 77 | 78 | err = DownloadFile("https://appimage.github.io/feed.json", filePath, 0666, "Fetching Json") 79 | return err 80 | } 81 | -------------------------------------------------------------------------------- /src/commands/remove.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "bread/src/helpers/utils" 6 | "github.com/pegvin/libappimage-go" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | type RemoveCmd struct { 12 | Target string `arg:"" name:"target" help:"target to remove" type:"string"` 13 | KeepFile bool `help:"Remove only the application desktop entry."` 14 | } 15 | 16 | // Function which will be executed when `remove` is called. 17 | func (cmd *RemoveCmd) Run() (err error) { 18 | cmd.Target = strings.ToLower(cmd.Target) 19 | registry, err := utils.OpenRegistry() // Open The Registry 20 | if err != nil { 21 | return err 22 | } 23 | defer registry.Close() // Close the registry before function end 24 | 25 | registry.Update() // Update the registry with latest installed appimage info 26 | 27 | // If the user provided string is short like `libresprite` convert it to `libresprite/libresprite` 28 | if len(strings.Split(cmd.Target, "/")) < 2 { 29 | cmd.Target = cmd.Target + "/" + cmd.Target; 30 | } 31 | 32 | entry, ok := registry.Lookup(cmd.Target) // Find the application in the registry 33 | if !ok { 34 | return fmt.Errorf("application not found \"" + cmd.Target + "\"") 35 | } 36 | 37 | err = removeDesktopIntegration(entry.FilePath) // Remove the application desktop integration 38 | if err != nil { 39 | fmt.Println("Desktop integration removal failed: " + err.Error()) 40 | } 41 | 42 | if cmd.KeepFile { 43 | return nil 44 | } 45 | 46 | err = os.Remove(entry.FilePath) 47 | if err != nil { 48 | return fmt.Errorf("Unable to remove AppImage file: " + err.Error()) 49 | } 50 | fmt.Println("Application removed: " + entry.FilePath) 51 | registry.Remove(entry.FilePath) 52 | return err 53 | } 54 | 55 | // Remove the application desktop integration 56 | func removeDesktopIntegration(filePath string) error { 57 | libAppImage, err := libappimagego.NewLibAppImageBindings() 58 | if err != nil { 59 | return err 60 | } 61 | 62 | if libAppImage.ShallNotBeIntegrated(filePath) { 63 | return nil 64 | } 65 | 66 | return libAppImage.Unregister(filePath, false) 67 | } 68 | -------------------------------------------------------------------------------- /src/commands/run.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "os" 5 | "errors" 6 | "bread/src/helpers/repos" 7 | "bread/src/helpers/utils" 8 | "github.com/mgord9518/aisap" 9 | ) 10 | 11 | type RunCmd struct { 12 | Target string `arg:"" name:"target" help:"Target To Run" type:"string"` 13 | Level uint8 `arg:"" help:"Set Permission Level" type:"int" default:"0"` 14 | Arguments []string `arg:"" passthrough:"" optional:"" name:"arguments" help:"Argument to pass to the program" type:"string"` 15 | NoPreRelease bool `short:"n" help:"Disable pre-releases." default:"false"` 16 | } 17 | 18 | func runAppImage(filePath string, permissionLevel uint8, arguments []string) (error) { 19 | appImage, err := aisap.NewAppImage(filePath) 20 | if err != nil { 21 | return nil 22 | } 23 | err = appImage.Perms.SetLevel(int(permissionLevel)) 24 | if err != nil { 25 | return err 26 | } 27 | return appImage.Run(arguments) 28 | } 29 | 30 | func (cmd *RunCmd) Run() (err error) { 31 | if cmd.Level > 4 { 32 | return errors.New("permission level can only be 0, 1, 2 or 3") 33 | } 34 | // Parse The user input 35 | repo, err := repos.ParseTarget(cmd.Target, "") 36 | if err != nil { 37 | return err 38 | } 39 | 40 | // Get The Latest Release 41 | release, err := repo.GetLatestRelease(cmd.NoPreRelease) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | // Show A Prompt To Select A AppImage File. 47 | selectedBinary, err := utils.PromptBinarySelection(release.Files) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | // Make A FilePath Out Of The AppImage Name 53 | targetFilePath, err := utils.MakeTempFilePath(selectedBinary) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | // Check if the FilePath Exist (cached file), Show error 59 | if _, err = os.Stat(targetFilePath); err == nil { 60 | return runAppImage(targetFilePath, cmd.Level, cmd.Arguments) 61 | } 62 | 63 | // Download The AppImage 64 | err = repo.Download(selectedBinary, targetFilePath) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | // Print Signature Info If Exist. 70 | utils.ShowSignature(targetFilePath) 71 | 72 | return runAppImage(targetFilePath, cmd.Level, cmd.Arguments) 73 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module bread 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/DEVLOPRR/appimage-update v0.0.0-20220329072345-3c229fbb98b9 7 | github.com/DEVLOPRR/libappimage-go v0.0.0-20220330021623-5d18529938c8 8 | github.com/ProtonMail/go-crypto v0.0.0-20220113124808-70ae35bab23f 9 | github.com/alecthomas/kong v0.5.0 10 | github.com/google/go-github/v31 v31.0.0 11 | github.com/juju/ansiterm v0.0.0-20210929141451-8b71cc96ebdc 12 | github.com/manifoldco/promptui v0.9.0 13 | github.com/mgord9518/aisap v0.5.4-alpha 14 | github.com/microcosm-cc/bluemonday v1.0.18 15 | github.com/schollz/progressbar/v3 v3.8.6 16 | ) 17 | 18 | require ( 19 | github.com/adrg/xdg v0.4.0 // indirect 20 | github.com/aymerick/douceur v0.2.0 // indirect 21 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect 22 | github.com/gabriel-vasile/mimetype v1.4.0 // indirect 23 | github.com/google/go-cmp v0.5.5 // indirect 24 | github.com/google/go-querystring v1.1.0 // indirect 25 | github.com/gorilla/css v1.0.0 // indirect 26 | github.com/lunixbochs/vtclean v1.0.0 // indirect 27 | github.com/mattn/go-colorable v0.1.12 // indirect 28 | github.com/mattn/go-isatty v0.0.14 // indirect 29 | github.com/mattn/go-runewidth v0.0.13 // indirect 30 | github.com/mgord9518/aisap/helpers v0.0.0-20220323120251-01c3da80e045 // indirect 31 | github.com/mgord9518/aisap/permissions v0.0.0-20220323120251-01c3da80e045 // indirect 32 | github.com/mgord9518/aisap/profiles v0.0.0-20220323120251-01c3da80e045 // indirect 33 | github.com/mgord9518/imgconv v0.0.0-20211227113402-4a8e0ad15713 // indirect 34 | github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect 35 | github.com/pkg/errors v0.9.1 // indirect 36 | github.com/rainycape/dl v0.0.0-20151222075243-1b01514224a1 // indirect 37 | github.com/rivo/uniseg v0.2.0 // indirect 38 | github.com/rustyoz/Mtransform v0.0.0-20190224104252-60c8c35a3681 // indirect 39 | github.com/rustyoz/genericlexer v0.0.0-20190224115003-eb82fd2987bd // indirect 40 | github.com/rustyoz/svg v0.0.0-20200706102315-fe1aeca2ba20 // indirect 41 | golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect 42 | golang.org/x/net v0.0.0-20220401154927-543a649e0bdd // indirect 43 | golang.org/x/sys v0.0.0-20220403020550-483a9cbc67c0 // indirect 44 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect 45 | gopkg.in/ini.v1 v1.66.4 // indirect 46 | ) 47 | -------------------------------------------------------------------------------- /src/commands/search.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "bread/src/helpers/utils" 7 | "github.com/schollz/progressbar/v3" 8 | "github.com/microcosm-cc/bluemonday" 9 | ) 10 | 11 | type SearchCmd struct { 12 | Name string `arg:"" name:"name" help:"name to search for." type:"string"` 13 | } 14 | 15 | func (cmd *SearchCmd) Run() (error) { 16 | var err error 17 | fmt.Println("Updating Catalog Data...") 18 | err = utils.FetchAppImageCatalog() 19 | if err != nil { 20 | return err 21 | } 22 | 23 | cmd.Name = strings.ToLower(cmd.Name) 24 | 25 | jsonData, err := utils.ReadAppImageCatalog() 26 | if err != nil { 27 | return err 28 | } 29 | 30 | var foundItems []utils.AppImageFeedItem 31 | bar := progressbar.Default( 32 | int64(len(jsonData.Items)), 33 | "Searching List", 34 | ) 35 | 36 | // This Loop Will Check if the name of description has our search target 37 | for _, item := range jsonData.Items { 38 | item.Name = strings.ToLower(item.Name) 39 | item.Description = strings.ToLower(item.Description) 40 | if strings.Contains(item.Name, cmd.Name) || strings.Contains(item.Description, cmd.Name) { 41 | // This loop will loop and check if the provider has a github link or not 42 | for providerIndex := range item.Links { 43 | if strings.ToLower(item.Links[providerIndex].Type) != "github" { 44 | // Finally remove all the html from the description 45 | p := bluemonday.StripTagsPolicy() 46 | item.Description = p.Sanitize(item.Description) 47 | // Get the first line of the description 48 | item.Description = strings.Split(item.Description, ".")[0] 49 | 50 | // Make the first element the github url 51 | item.Links[0].Type = "github" 52 | item.Links[0].Url = strings.ToLower(item.Links[providerIndex].Url) 53 | 54 | // Try to convert the URL to short user/repo format 55 | githubUserRepo, err := utils.GetUserRepoFromUrl(item.Links[0].Url) 56 | if err == nil { 57 | item.Links[0].Url = githubUserRepo 58 | } 59 | 60 | // append it to the foundItems 61 | foundItems = append(foundItems, item) 62 | break 63 | } 64 | } 65 | } 66 | bar.Add(1) 67 | } 68 | 69 | bar.Finish() 70 | 71 | if len(foundItems) == 0 { 72 | fmt.Println("Nothing Found in the catalog!") 73 | } else { 74 | for _, foundItems := range foundItems { 75 | fmt.Println("\n" + foundItems.Name + " - " + foundItems.Links[0].Url) 76 | if foundItems.Description != "" { 77 | fmt.Println(" " + foundItems.Description) 78 | } else { 79 | fmt.Println(" No Description provided from Author!") 80 | } 81 | } 82 | } 83 | return nil 84 | } -------------------------------------------------------------------------------- /src/helpers/utils/appimage.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "os" 5 | "bytes" 6 | "github.com/pegvin/libappimage-go" 7 | ) 8 | 9 | // Get AppImage information: isTerminalApp, AppImageType 10 | func GetAppImageInfo(targetFilePath string) (*AppImageInfo, error) { 11 | libAppImage, err := libappimagego.NewLibAppImageBindings() // Load the `libappimage` Library For Integration 12 | if err != nil { 13 | return nil, err 14 | } 15 | 16 | return &AppImageInfo{ 17 | IsTerminalApp: libAppImage.IsTerminalApp(targetFilePath), 18 | AppImageType: libAppImage.GetType(targetFilePath, false), 19 | }, nil 20 | } 21 | 22 | // Remove the application desktop integration 23 | func RemoveDesktopIntegration(filePath string) (error) { 24 | libAppImage, err := libappimagego.NewLibAppImageBindings() 25 | if err != nil { 26 | return err 27 | } 28 | 29 | if libAppImage.ShallNotBeIntegrated(filePath) { 30 | return nil 31 | } 32 | 33 | err = libAppImage.Unregister(filePath, false) 34 | return err 35 | } 36 | 37 | // Integrate The AppImage To Desktop. 38 | func CreateDesktopIntegration(targetFilePath string) (error) { 39 | libAppImage, err := libappimagego.NewLibAppImageBindings() // Load the `libappimage` Library For Integration 40 | if err != nil { 41 | return err 42 | } 43 | 44 | err = libAppImage.Register(targetFilePath, false) // Register The File 45 | if err != nil { 46 | return err 47 | } else { 48 | return nil 49 | } 50 | } 51 | 52 | // check if a file is appimage 53 | func IsAppImageFile(filePath string) bool { 54 | file, err := os.Open(filePath) 55 | if err != nil { 56 | return false 57 | } 58 | 59 | return isAppImageType1File(file) || isAppImageType2File(file) 60 | } 61 | 62 | // Check if appimage is type 2 63 | func isAppImageType2File(file *os.File) bool { 64 | return isElfFile(file) && fileHasBytesAt(file, []byte{0x41, 0x49, 0x02}, 8) 65 | } 66 | 67 | // Check if appimage is type 1 68 | func isAppImageType1File(file *os.File) bool { 69 | return isISO9660(file) || fileHasBytesAt(file, []byte{0x41, 0x49, 0x01}, 8) 70 | } 71 | 72 | // Check if a file is Elf file 73 | func isElfFile(file *os.File) bool { 74 | return fileHasBytesAt(file, []byte{0x7f, 0x45, 0x4c, 0x46}, 0) 75 | } 76 | 77 | // Check if the file is a ISO 9660 Standard File 78 | func isISO9660(file *os.File) bool { 79 | for _, offset := range []int64{32769, 34817, 36865} { 80 | if fileHasBytesAt(file, []byte{'C', 'D', '0', '0', '1'}, offset) { 81 | return true 82 | } 83 | } 84 | 85 | return false 86 | } 87 | 88 | // check if a file has bytes at particular position 89 | func fileHasBytesAt(file *os.File, expectedBytes []byte, offset int64) bool { 90 | readBytes := make([]byte, len(expectedBytes)) 91 | _, _ = file.Seek(offset, 0) 92 | _, _ = file.Read(readBytes) 93 | 94 | return bytes.Equal(readBytes, expectedBytes) 95 | } 96 | -------------------------------------------------------------------------------- /src/commands/list.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "os" 5 | "fmt" 6 | "sort" 7 | "bytes" 8 | "path/filepath" 9 | "bread/src/helpers/utils" 10 | "github.com/juju/ansiterm" 11 | ) 12 | 13 | type ListCmd struct { 14 | ShowSha1 bool `short:"s" name:"show-sha1" help:"Show SHA1 Hashes too." default:"false"` 15 | ShowTagName bool `short:"t" name:"show-tag" help:"Show Release Tags." default:"false"` 16 | } 17 | 18 | // Function which will be executed when `list` is called. 19 | func (r *ListCmd) Run() error { 20 | registry, err := utils.OpenRegistry() // Open The Registry 21 | if err != nil { 22 | return err 23 | } 24 | defer registry.Close() // Close the registry before function return 25 | 26 | registry.Update() // Update the registry with latest application info 27 | if len(registry.Entries) == 0 { 28 | fmt.Println("No installed Applications Found!") 29 | return nil 30 | } 31 | 32 | var buf bytes.Buffer // Buffer which will hold the table 33 | tabWriter := ansiterm.NewTabWriter(&buf, 20, 4, 0, ' ', 0) 34 | tabWriter.SetColorCapable(true) 35 | 36 | tabWriter.SetForeground(ansiterm.BrightGreen) 37 | if r.ShowSha1 { 38 | if r.ShowTagName { 39 | _, _ = tabWriter.Write([]byte("User/Repo\t File Name\t Tag Name \t SHA1 HASH\n")) 40 | _, _ = tabWriter.Write([]byte("---------\t ---------\t ---------\t ---------\n")) 41 | } else { 42 | _, _ = tabWriter.Write([]byte("User/Repo\t File Name\t SHA1 HASH\n")) 43 | _, _ = tabWriter.Write([]byte("---------\t ---------\t ---------\n")) 44 | } 45 | } else { 46 | if r.ShowTagName { 47 | _, _ = tabWriter.Write([]byte("User/Repo\t File Name\t Tag Name \n")) 48 | _, _ = tabWriter.Write([]byte("---------\t ---------\t ---------\n")) 49 | } else { 50 | _, _ = tabWriter.Write([]byte("User/Repo\t File Name\n")) 51 | _, _ = tabWriter.Write([]byte("---------\t ---------\n")) 52 | } 53 | } 54 | 55 | tabWriter.SetForeground(ansiterm.Default) 56 | 57 | var lines [][]string 58 | for fileName, v := range registry.Entries { 59 | var line []string 60 | if r.ShowSha1 { 61 | if r.ShowTagName { 62 | line = []string{v.Repo, filepath.Base(fileName), v.TagName, v.FileSha1} 63 | } else { 64 | line = []string{v.Repo, filepath.Base(fileName), v.FileSha1} 65 | } 66 | } else { 67 | if r.ShowTagName { 68 | line = []string{v.Repo, filepath.Base(fileName), v.TagName} 69 | } else { 70 | line = []string{v.Repo, filepath.Base(fileName)} 71 | } 72 | } 73 | lines = append(lines, line) 74 | } 75 | sort.Slice(lines, func(i int, j int) bool { 76 | return lines[i][1] < lines[j][1] 77 | }) 78 | 79 | for _, line := range lines { 80 | if r.ShowSha1 { 81 | if r.ShowTagName { 82 | _, _ = tabWriter.Write([]byte(line[0] + "\t " + line[1] + "\t " + line[2] + "\t " + line[3] + "\n")) 83 | } else { 84 | _, _ = tabWriter.Write([]byte(line[0] + "\t " + line[1] + "\t " + line[2] + "\n")) 85 | } 86 | } else { 87 | if r.ShowTagName { 88 | _, _ = tabWriter.Write([]byte(line[0] + "\t " + line[1] + "\t " + line[2] + "\t " + "\n")) 89 | } else { 90 | _, _ = tabWriter.Write([]byte(line[0] + "\t " + line[1] + "\n")) 91 | } 92 | } 93 | } 94 | _ = tabWriter.Flush() 95 | _, _ = os.Stdout.Write(buf.Bytes()) 96 | return nil 97 | } 98 | -------------------------------------------------------------------------------- /src/commands/install.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "os" 5 | "fmt" 6 | "errors" 7 | "bread/src/helpers/repos" 8 | "bread/src/helpers/utils" 9 | ) 10 | 11 | type InstallCmd struct { 12 | Target string `arg:"" name:"target" help:"Installation target." type:"string"` 13 | TagName string `arg:"" optional:"" name:"tagname" help:"GitHub Release TagName To Download From." type:"string"` 14 | NoPreRelease bool `short:"n" help:"Disable pre-releases." default:"false"` 15 | } 16 | 17 | // Function Which Will Be Called When `install` is the Command. 18 | func (cmd *InstallCmd) Run() (err error) { 19 | // Parse The user input 20 | repo, err := repos.ParseTarget(cmd.Target, cmd.TagName) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | // Get The Latest Release 26 | release, err := repo.GetLatestRelease(cmd.NoPreRelease) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | if cmd.TagName != "" && release.Tag != cmd.TagName { 32 | fmt.Println("Tag '" + cmd.TagName + "' not found, using latest available '" + release.Tag + "' instead") 33 | } 34 | 35 | // Show A Prompt To Select A AppImage File. 36 | selectedBinary, err := utils.PromptBinarySelection(release.Files) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | // Make A FilePath Out Of The AppImage Name 42 | targetFilePath, err := utils.MakeTargetFilePath(selectedBinary) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | // Check if the FilePath Exist, Show error 48 | if _, err = os.Stat(targetFilePath); err == nil { 49 | return errors.New("the application is installed already") 50 | } 51 | 52 | // Download The AppImage 53 | err = repo.Download(selectedBinary, targetFilePath) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | // Add The Current Application To The Registry `.registry.json` 59 | cmd.addToRegistry(targetFilePath, repo, release.Tag) 60 | 61 | // Integrated The AppImage To Desktop 62 | err = utils.CreateDesktopIntegration(targetFilePath) 63 | if err != nil { 64 | fmt.Println("Integration Failed: " + err.Error()) 65 | } else { 66 | fmt.Println("Integration Complete!") 67 | } 68 | 69 | // Print Signature Info If Exist. 70 | utils.ShowSignature(targetFilePath) 71 | 72 | fmt.Println("Installed '" + repo.Id() + "'!") 73 | return nil 74 | } 75 | 76 | // Function To Add Installed Program To Registry (Installed App information is stored in here). 77 | func (cmd *InstallCmd) addToRegistry(targetFilePath string, repo repos.Application, TagName string) (error) { 78 | sha1, _ := utils.GetFileSHA1(targetFilePath) // Get The Sha1 Hash 79 | appimageInfo, err := utils.GetAppImageInfo(targetFilePath) 80 | if err != nil { 81 | return err 82 | } 83 | 84 | // Make a new entry struct 85 | entry := utils.RegistryEntry{ 86 | Repo: repo.Id(), 87 | TagName: TagName, 88 | FileSha1: sha1, 89 | FilePath: targetFilePath, 90 | IsTerminalApp: appimageInfo.IsTerminalApp, 91 | AppImageType: appimageInfo.AppImageType, 92 | } 93 | 94 | registry, _ := utils.OpenRegistry() // Open The Registry 95 | if registry != nil { 96 | _ = registry.Add(entry) // Add the entry to registry `.registry.json` 97 | _ = registry.Close() // Close the registry 98 | } 99 | return nil 100 | } 101 | -------------------------------------------------------------------------------- /src/helpers/utils/util.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | // #include 4 | // #include 5 | import "C" 6 | 7 | import ( 8 | "os" 9 | "fmt" 10 | "os/user" 11 | "net/url" 12 | "strings" 13 | "path/filepath" 14 | "github.com/manifoldco/promptui" 15 | ) 16 | 17 | type BinaryUrl struct { 18 | FileName string 19 | Url string 20 | } 21 | 22 | type AppImageInfo struct { 23 | IsTerminalApp bool 24 | AppImageType int 25 | } 26 | 27 | // Get the user/repo from a github url 28 | func GetUserRepoFromUrl(gitHubUrl string) (string, error) { 29 | urlParsed, err := url.ParseRequestURI(gitHubUrl) 30 | if err != nil { 31 | return "", err 32 | } 33 | 34 | if urlParsed.Host != "github.com" { 35 | return "", fmt.Errorf("invalid github url") 36 | } 37 | 38 | splitPaths := strings.Split(urlParsed.EscapedPath(), "/") 39 | 40 | if len(splitPaths) < 3 { 41 | return "", fmt.Errorf("invalid github url") 42 | } 43 | 44 | return splitPaths[1] + "/" + splitPaths[2], nil 45 | } 46 | 47 | // Get the Applications directory absolute path 48 | func MakeApplicationsDirPath() (string, error) { 49 | usr, err := user.Current() 50 | if err != nil { 51 | return "", err 52 | } 53 | 54 | applicationsPath := filepath.Join(usr.HomeDir, "Applications") 55 | err = os.MkdirAll(applicationsPath, os.ModePerm) 56 | if err != nil { 57 | return "", err 58 | } 59 | return applicationsPath, nil 60 | } 61 | 62 | // Get the file path of a file in applications folder 63 | func MakeTargetFilePath(link *BinaryUrl) (string, error) { 64 | applicationsPath, err := MakeApplicationsDirPath() 65 | if err != nil { 66 | return "", err 67 | } 68 | 69 | filePath := filepath.Join(applicationsPath, link.FileName) 70 | return filePath, nil 71 | } 72 | 73 | // Make file path from a file in run-cache directory inside Applications directory 74 | func MakeTempFilePath(link *BinaryUrl) (string, error) { 75 | applicationsPath, err := MakeTempAppDirPath() 76 | if err != nil { 77 | return "", err 78 | } 79 | 80 | filePath := filepath.Join(applicationsPath, link.FileName) 81 | return filePath, nil 82 | } 83 | 84 | // Make folder run-cache inside Applications dir and return it's path 85 | func MakeTempAppDirPath() (string, error) { 86 | TempApplicationDirPath, err := MakeApplicationsDirPath() 87 | if err != nil { 88 | return "", err 89 | } 90 | 91 | TempApplicationDirPath = filepath.Join(TempApplicationDirPath, "run-cache") 92 | err = os.MkdirAll(TempApplicationDirPath, os.ModePerm) 93 | if err != nil { 94 | return "", err 95 | } 96 | 97 | return TempApplicationDirPath, nil 98 | } 99 | 100 | // List appimages to select from 101 | func PromptBinarySelection(downloadLinks []BinaryUrl) (result *BinaryUrl, err error) { 102 | if len(downloadLinks) == 1 { 103 | return &downloadLinks[0], nil 104 | } 105 | 106 | prompt := promptui.Select{ 107 | Label: "Select an AppImage to install", 108 | Items: downloadLinks, 109 | Templates: &promptui.SelectTemplates{ 110 | Label: " {{ .FileName }}", 111 | Active: "\U00002713 {{ .FileName }}", 112 | Inactive: " {{ .FileName }}", 113 | Selected: "\U00002713 {{ .FileName }}"}, 114 | } 115 | 116 | i, _, err := prompt.Run() 117 | if err != nil { 118 | return nil, err 119 | } 120 | 121 | return &downloadLinks[i], nil 122 | } 123 | 124 | // read the update info embeded into the appimage file 125 | // func ReadUpdateInfo(appImagePath string) (string, error) { 126 | // elfFile, err := elf.Open(appImagePath) 127 | // if err != nil { 128 | // panic("Unable to open target: \"" + appImagePath + "\"." + err.Error()) 129 | // } 130 | 131 | // updInfo := elfFile.Section(".upd_info") 132 | // if updInfo == nil { 133 | // panic("Missing update section on target elf ") 134 | // } 135 | // sectionData, err := updInfo.Data() 136 | 137 | // if err != nil { 138 | // panic("Unable to parse update section: " + err.Error()) 139 | // } 140 | 141 | // str_end := bytes.Index(sectionData, []byte("\000")) 142 | // if str_end == -1 || str_end == 0 { 143 | // return "", fmt.Errorf("No update information found in: " + appImagePath) 144 | // } 145 | 146 | // update_info := string(sectionData[:str_end]) 147 | // return update_info, nil 148 | // } 149 | -------------------------------------------------------------------------------- /src/helpers/utils/registry.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "os" 5 | "log" 6 | "strings" 7 | "io/ioutil" 8 | "encoding/json" 9 | "path/filepath" 10 | updateUtils "github.com/pegvin/appimage-update/util" 11 | ) 12 | 13 | type RegistryEntry struct { 14 | Repo string 15 | FileSha1 string 16 | // AppName string 17 | // AppVersion string 18 | FilePath string 19 | UpdateInfo string 20 | TagName string 21 | IsTerminalApp bool 22 | AppImageType int 23 | } 24 | 25 | type Registry struct { 26 | Entries map[string]RegistryEntry 27 | } 28 | 29 | // Function to open a registry entry 30 | func OpenRegistry() (registry *Registry, err error) { 31 | path, err := makeRegistryFilePath() // Get the full path to .registry.json 32 | if err != nil { 33 | return 34 | } 35 | 36 | data, err := ioutil.ReadFile(path) // Read file 37 | if err != nil { 38 | return &Registry{Entries: map[string]RegistryEntry{}}, nil // If some error occured return a new empty registry 39 | } 40 | 41 | err = json.Unmarshal(data, ®istry) // Parse JSON data into the struct 42 | if err != nil { 43 | return 44 | } 45 | 46 | return 47 | } 48 | 49 | // Function to close a registry entry 50 | func (registry *Registry) Close() error { 51 | path, err := makeRegistryFilePath() // Get full path to .registry.json 52 | if err != nil { 53 | return err 54 | } 55 | 56 | blob, err := json.Marshal(registry) // Convert registry struct into a blob 57 | if err != nil { 58 | return err 59 | } 60 | 61 | err = ioutil.WriteFile(path, blob, 0666) // write the blob file with 666 permissions 62 | if err != nil { 63 | return err 64 | } 65 | 66 | return nil 67 | } 68 | 69 | // Add a entry to registry 70 | func (registry *Registry) Add(entry RegistryEntry) error { 71 | registry.Entries[entry.FilePath] = entry 72 | return nil 73 | } 74 | 75 | // Remove a entry from registry 76 | func (registry *Registry) Remove(filePath string) { 77 | delete(registry.Entries, filePath) 78 | } 79 | 80 | // Update registry entry 81 | func (registry *Registry) Update() { 82 | applicationsDir, err := MakeApplicationsDirPath() // Applications folder full path 83 | if err != nil { 84 | log.Fatal(err) 85 | } 86 | 87 | files, err := ioutil.ReadDir(applicationsDir) // Read all the files in the folder 88 | if err != nil { 89 | log.Fatal(err) 90 | } 91 | 92 | // Filter out all the appimage files and put them into registry 93 | for _, f := range files { 94 | if strings.HasSuffix(strings.ToLower(f.Name()), ".appimage") { 95 | filePath := filepath.Join(applicationsDir, f.Name()) 96 | _, ok := registry.Entries[filePath] 97 | if !ok { 98 | entry := registry.createEntryFromFile(filePath) 99 | _ = registry.Add(entry) 100 | } 101 | } 102 | } 103 | 104 | // Remove deleted/non-existent files from registry 105 | for filePath := range registry.Entries { 106 | if _, err := os.Stat(filePath); os.IsNotExist(err) { 107 | registry.Remove(filePath) 108 | } 109 | } 110 | } 111 | 112 | // Create a new entry in the registry from a appimage file 113 | func (registry *Registry) createEntryFromFile(filePath string) RegistryEntry { 114 | fileSha1, _ := GetFileSHA1(filePath) 115 | updateInfo, _ := updateUtils.ReadUpdateInfo(filePath) 116 | entry := RegistryEntry{ 117 | Repo: "", 118 | TagName: "", 119 | FileSha1: fileSha1, 120 | // AppName: "", 121 | // AppVersion: "", 122 | FilePath: filePath, 123 | UpdateInfo: updateInfo, 124 | } 125 | return entry 126 | } 127 | 128 | // Lookup a entry in the registry 129 | func (registry *Registry) Lookup(target string) (RegistryEntry, bool) { 130 | applicationsDir, _ := MakeApplicationsDirPath() 131 | possibleFullPath := filepath.Join(applicationsDir, target) 132 | 133 | for _, entry := range registry.Entries { 134 | if entry.FileSha1 == target || entry.FilePath == target || 135 | entry.FilePath == possibleFullPath || entry.Repo == target { 136 | return entry, true 137 | } 138 | } 139 | 140 | if IsAppImageFile(target) { 141 | entry := registry.createEntryFromFile(target) 142 | _ = registry.Add(entry) 143 | 144 | return entry, true 145 | } else { 146 | if IsAppImageFile(possibleFullPath) { 147 | entry := registry.createEntryFromFile(target) 148 | _ = registry.Add(entry) 149 | 150 | return entry, true 151 | } 152 | } 153 | 154 | return RegistryEntry{}, false 155 | } 156 | 157 | // makes the registry file path 158 | func makeRegistryFilePath() (string, error) { 159 | applicationsPath, err := MakeApplicationsDirPath() 160 | if err != nil { 161 | return "", err 162 | } 163 | 164 | filePath := filepath.Join(applicationsPath, ".registry.json") 165 | return filePath, nil 166 | } 167 | -------------------------------------------------------------------------------- /src/helpers/repos/github.go: -------------------------------------------------------------------------------- 1 | package repos 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "bread/src/helpers/utils" 8 | "github.com/google/go-github/v31/github" 9 | ) 10 | 11 | // Struct containing GitHub Repo Details 12 | type GitHubRepo struct { 13 | User string 14 | Project string 15 | Release string 16 | File string 17 | TagName string 18 | UserRepo string 19 | } 20 | 21 | // Parses string to a github repo information, and returns a object and error (if any) 22 | func NewGitHubRepo(target string, tagName string) (appInfo Application, err error) { 23 | appInfo = &GitHubRepo{} 24 | ghSource := GitHubRepo{} 25 | 26 | // parse the target as a github url and get the user/repo from it 27 | userRepo, err := utils.GetUserRepoFromUrl(target) 28 | if err == nil { // If successfull return the information 29 | userRepoSplitted := strings.Split(userRepo, "/") 30 | ghSource = GitHubRepo{ 31 | User: userRepoSplitted[0], 32 | Project: userRepoSplitted[1], 33 | TagName: tagName, 34 | UserRepo: userRepoSplitted[0] + "/" + userRepoSplitted[1], 35 | } 36 | return &ghSource, nil 37 | } else { 38 | // Take the `user/repo` and split `user` and `repo` 39 | targetParts := strings.Split(target, "/") 40 | 41 | // If input is not in format of `user/repo` assume `user` and `repo` are same 42 | if len(targetParts) < 2 { 43 | ghSource = GitHubRepo{ 44 | User: targetParts[0], 45 | Project: targetParts[0], 46 | TagName: tagName, 47 | UserRepo: targetParts[0] + "/" + targetParts[0], 48 | } 49 | } else { 50 | ghSource = GitHubRepo{ 51 | User: targetParts[0], 52 | Project: targetParts[1], 53 | TagName: tagName, 54 | UserRepo: targetParts[0] + "/" + targetParts[1], 55 | } 56 | } 57 | 58 | return &ghSource, nil 59 | } 60 | } 61 | 62 | // Get the github user/repo from the repo information 63 | func (g GitHubRepo) Id() string { 64 | return g.UserRepo 65 | } 66 | 67 | // Gets the latest/specified tagged release from github 68 | func (g GitHubRepo) GetLatestRelease(NoPreRelease bool) (*Release, error) { 69 | client := github.NewClient(nil) // Client For Interacting with github api 70 | 71 | // Get all the releases from the target 72 | releases, _, err := client.Repositories.ListReleases(context.Background(), g.User, g.Project, nil) 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | if g.TagName != "" { 78 | releaseWithTagName := getReleaseFromTagName(releases, g.TagName) 79 | 80 | if releaseWithTagName != nil { 81 | appimageFiles := getAppImageFilesFromRelease(releaseWithTagName) 82 | if len(appimageFiles) > 0 { 83 | return &Release{ 84 | *releaseWithTagName.TagName, 85 | appimageFiles, 86 | }, nil 87 | } 88 | } 89 | } 90 | 91 | // Filter out files which are not AppImage 92 | for _, release := range releases { 93 | if *release.Draft { 94 | continue 95 | } 96 | if *release.Prerelease && NoPreRelease { 97 | continue 98 | } 99 | 100 | downloadLinks := getAppImageFilesFromRelease(release) 101 | if len(downloadLinks) > 0 { 102 | return &Release{ 103 | *release.TagName, 104 | downloadLinks, 105 | }, nil 106 | } 107 | } 108 | 109 | return nil, NoAppImageBinariesFound 110 | } 111 | 112 | // Download appimage from remote 113 | func (g GitHubRepo) Download(binaryUrl *utils.BinaryUrl, targetPath string) (err error) { 114 | err = utils.DownloadFile(binaryUrl.Url, targetPath, 0755, "Downloading") 115 | return err 116 | } 117 | 118 | // Generate a fallback update information for a appimage 119 | func (g GitHubRepo) FallBackUpdateInfo() string { 120 | updateInfo := "gh-releases-direct|" + g.User + "|" + g.Project 121 | if g.Release == "" { 122 | updateInfo += "|latest" 123 | } else { 124 | updateInfo += "|" + g.Release 125 | } 126 | 127 | if g.File == "" { 128 | updateInfo += "|*.AppImage" 129 | } else { 130 | updateInfo += "|" + g.File 131 | } 132 | 133 | return updateInfo 134 | } 135 | 136 | // Gets All The AppImage Files from a github release 137 | func getAppImageFilesFromRelease(release *github.RepositoryRelease) ([]utils.BinaryUrl) { 138 | var downloadLinks []utils.BinaryUrl // Contains Download Links 139 | 140 | for _, asset := range release.Assets { 141 | if strings.HasSuffix(strings.ToLower(*asset.Name), ".appimage") { 142 | downloadLinks = append(downloadLinks, utils.BinaryUrl{ 143 | FileName: *asset.Name, 144 | Url: *asset.BrowserDownloadURL, 145 | }) 146 | } 147 | } 148 | 149 | return downloadLinks 150 | } 151 | 152 | // Gets Release From A Particular Tag Name 153 | func getReleaseFromTagName(releases []*github.RepositoryRelease, tagName string) (*github.RepositoryRelease) { 154 | for _, release := range releases { 155 | if *release.Draft { continue } 156 | if tagName == *release.TagName { 157 | return release 158 | } 159 | } 160 | return nil 161 | } 162 | -------------------------------------------------------------------------------- /src/helpers/utils/signature.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "fmt" 7 | "bytes" 8 | "strings" 9 | "debug/elf" 10 | "crypto/sha1" 11 | "encoding/hex" 12 | "crypto/sha256" 13 | "github.com/ProtonMail/go-crypto/openpgp" 14 | ) 15 | 16 | // Function to verify signature 17 | func VerifySignature(target string) (result *openpgp.Entity, err error) { 18 | key, err := readElfSection(target, ".sig_key") 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | signature, err := readElfSection(target, ".sha256_sig") 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | file, err := newAppImagePreSignatureReader(target) 29 | if err != nil { 30 | return 31 | } 32 | 33 | sha256Hash := sha256.New() 34 | _, err = io.Copy(sha256Hash, file) 35 | 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | verification_target := hex.EncodeToString(sha256Hash.Sum(nil)) 41 | 42 | keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewReader(key)) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | entity, err := openpgp.CheckArmoredDetachedSignature(keyring, strings.NewReader(verification_target), bytes.NewReader(signature), nil) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | return entity, nil 53 | } 54 | 55 | // Function which reads a particular section in the appimage (elf) 56 | func readElfSection(appImagePath string, sectionName string) ([]byte, error) { 57 | elfFile, err := elf.Open(appImagePath) 58 | if err != nil { 59 | panic("Unable to open target: \"" + appImagePath + "\"." + err.Error()) 60 | } 61 | 62 | section := elfFile.Section(sectionName) 63 | if section == nil { 64 | return nil, fmt.Errorf("missing " + sectionName + " section on target elf") 65 | } 66 | sectionData, err := section.Data() 67 | 68 | if err != nil { 69 | return nil, fmt.Errorf("Unable to parse " + sectionName + " section: " + err.Error()) 70 | } 71 | 72 | str_end := bytes.Index(sectionData, []byte("\000")) 73 | if str_end == -1 || str_end == 0 { 74 | return nil, nil 75 | } 76 | 77 | return sectionData[:str_end], nil 78 | } 79 | 80 | // Function which reads the appimage signature 81 | func ReadSignature(appImagePath string) ([]byte, error) { 82 | elfFile, err := elf.Open(appImagePath) 83 | if err != nil { 84 | panic("Unable to open target: \"" + appImagePath + "\"." + err.Error()) 85 | } 86 | 87 | updInfo := elfFile.Section(".sha256_sig") 88 | if updInfo == nil { 89 | panic("Missing .sha256_sig section on target elf ") 90 | } 91 | sectionData, err := updInfo.Data() 92 | 93 | if err != nil { 94 | panic("Unable to parse .sha256_sig section: " + err.Error()) 95 | } 96 | 97 | str_end := bytes.Index(sectionData, []byte("\000")) 98 | if str_end == -1 || str_end == 0 { 99 | return nil, fmt.Errorf("No update information found in: " + appImagePath) 100 | } 101 | 102 | return sectionData[:str_end], nil 103 | } 104 | 105 | type appImagePreSignatureReader struct { 106 | keySectionOffset uint64 107 | keySectionSize uint64 108 | 109 | sigSectionOffset uint64 110 | sigSectionSize uint64 111 | 112 | offset uint64 113 | file *os.File 114 | } 115 | 116 | func newAppImagePreSignatureReader(target string) (*appImagePreSignatureReader, error) { 117 | elfFile, err := elf.Open(target) 118 | if err != nil { 119 | return nil, err 120 | } 121 | 122 | key := elfFile.Section(".sig_key") 123 | if key == nil { 124 | return nil, fmt.Errorf("missing .sig_key section") 125 | } 126 | 127 | signature := elfFile.Section(".sha256_sig") 128 | if signature == nil { 129 | return nil, fmt.Errorf("missing .sha256_sig section") 130 | } 131 | 132 | file, err := os.Open(target) 133 | if err != nil { 134 | return nil, err 135 | } 136 | 137 | return &appImagePreSignatureReader{ 138 | offset: 0, 139 | file: file, 140 | keySectionOffset: key.Offset, 141 | keySectionSize: key.Size, 142 | sigSectionOffset: signature.Offset, 143 | sigSectionSize: signature.Size, 144 | }, nil 145 | } 146 | 147 | func (reader *appImagePreSignatureReader) Read(p []byte) (n int, err error) { 148 | n, err = reader.file.Read(p) 149 | if err != nil { 150 | return 151 | } 152 | 153 | oldOffset := reader.offset 154 | reader.offset += uint64(n) 155 | 156 | if reader.keySectionOffset >= oldOffset && reader.keySectionOffset < reader.offset { 157 | start := reader.keySectionOffset - oldOffset 158 | for i := start; i < uint64(n) && (i-start) < reader.keySectionSize; i++ { 159 | p[i] = 0 160 | } 161 | } 162 | 163 | if reader.sigSectionOffset >= oldOffset && reader.sigSectionOffset < reader.offset { 164 | start := reader.sigSectionOffset - oldOffset 165 | for i := start; i < uint64(n) && (i-start) < reader.sigSectionSize; i++ { 166 | p[i] = 0 167 | } 168 | } 169 | 170 | return n, err 171 | } 172 | 173 | // Show signature of a given file 174 | func ShowSignature(filePath string) (error) { 175 | signingEntity, err := VerifySignature(filePath) 176 | if err != nil { 177 | return err 178 | } 179 | if signingEntity != nil { 180 | fmt.Println("AppImage signed by:") 181 | for _, v := range signingEntity.Identities { 182 | fmt.Println("\t", v.Name) 183 | } 184 | } 185 | return nil 186 | } 187 | 188 | // Get SHA1 Hash of a file 189 | func GetFileSHA1(filePath string) (string, error) { 190 | file, err := os.Open(filePath) 191 | if err != nil { 192 | return "", err 193 | } 194 | 195 | sha1Checksum := sha1.New() 196 | _, err = io.Copy(sha1Checksum, file) 197 | if err != nil { 198 | return "", err 199 | } 200 | return hex.EncodeToString(sha1Checksum.Sum(nil)), nil 201 | } 202 | -------------------------------------------------------------------------------- /src/commands/update.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "bread/src/helpers/repos" 5 | "bread/src/helpers/utils" 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | ) 11 | 12 | type UpdateCmd struct { 13 | Targets []string `arg:"" optional:"" name:"targets" help:"Update the target/all applications." type:"string"` 14 | 15 | Check bool `short:"c" help:"Only check for updates."` 16 | All bool `short:"a" help:"Update/check all applications."` 17 | NoPreRelease bool `short:"n" help:"Disable pre-releases." default:"false"` 18 | } 19 | 20 | // Function Which Will Be Executed When `update` is called. 21 | func (cmd *UpdateCmd) Run() (err error) { 22 | // Variable which will hold if any app was updated. 23 | var howManyUpdates int = 0 24 | 25 | if cmd.All { // if `update all` 26 | cmd.Targets, err = getAllTargets() // Load all the application info into targets 27 | if err != nil { 28 | return err 29 | } 30 | } 31 | 32 | if len(cmd.Targets) == 0 { 33 | if cmd.All { 34 | fmt.Println("No Application Installed") 35 | } else { 36 | fmt.Println("No Application Specified To Update") 37 | } 38 | return nil 39 | } 40 | 41 | fmt.Println("Checking For Updates") 42 | for _, target := range cmd.Targets { 43 | if len(strings.Split(target, "/")) < 2 { 44 | target = strings.ToLower(target + "/" + target) 45 | } else if len(strings.Split(target, "/")) == 2 { 46 | target = strings.ToLower(target) 47 | } 48 | 49 | entry, err := cmd.getRegistryEntry(target) 50 | if err != nil { 51 | continue 52 | } 53 | 54 | repo, err := repos.ParseTarget(target, "") 55 | 56 | if err != nil { 57 | return err 58 | } 59 | 60 | release, err := repo.GetLatestRelease(cmd.NoPreRelease) 61 | if err != nil { 62 | fmt.Println(target, "\U00002192", err) 63 | continue 64 | // return err 65 | } 66 | 67 | if release.Tag == entry.TagName { 68 | continue 69 | } 70 | 71 | if cmd.Check { 72 | fmt.Println(target, "\U00002192", release.Tag) 73 | howManyUpdates++ 74 | continue 75 | } 76 | 77 | fmt.Println("Updating: " + target + "#" + entry.TagName + " \U00002192 " + target + "#" + release.Tag) 78 | 79 | var selectedBinary *utils.BinaryUrl; 80 | for fileIndex := range release.Files { 81 | if filepath.Base(entry.FilePath) == release.Files[fileIndex].FileName { 82 | selectedBinary = &release.Files[fileIndex] 83 | break 84 | } 85 | } 86 | 87 | if selectedBinary == nil { 88 | // Show A Prompt To Select A AppImage File. 89 | selectedBinary, err = utils.PromptBinarySelection(release.Files) 90 | if err != nil { 91 | return err 92 | } 93 | } 94 | 95 | // Make A FilePath Out Of The AppImage Name 96 | targetFilePath, err := utils.MakeTargetFilePath(selectedBinary) 97 | if err != nil { 98 | return err 99 | } 100 | 101 | // Download The AppImage 102 | err = repo.Download(selectedBinary, targetFilePath) 103 | if err != nil { 104 | return err 105 | } 106 | 107 | // Integrated The AppImage To Desktop 108 | err = utils.CreateDesktopIntegration(targetFilePath) 109 | if err != nil { 110 | os.Remove(targetFilePath) 111 | return err 112 | } 113 | 114 | registry, err := utils.OpenRegistry() 115 | if err != nil { 116 | return err 117 | } 118 | 119 | // De-Integrate old app from desktop 120 | err = utils.RemoveDesktopIntegration(entry.FilePath) 121 | if err != nil { 122 | os.Remove(targetFilePath) // If error, remove the newly downloaded appimage. 123 | return err 124 | } else { 125 | err = os.Remove(entry.FilePath) // Remove the old appimage 126 | if err != nil { 127 | fmt.Println("Cannot Remove The Old AppImage.\n", err.Error()) 128 | } 129 | } 130 | 131 | registry.Remove(entry.FilePath) // Remove Old File From Registry 132 | 133 | sha1hash, _ := utils.GetFileSHA1(targetFilePath) 134 | appImageInfo, _ := utils.GetAppImageInfo(targetFilePath) 135 | err = registry.Add(utils.RegistryEntry{ 136 | Repo: target, 137 | FilePath: targetFilePath, 138 | FileSha1: sha1hash, 139 | TagName: release.Tag, 140 | IsTerminalApp: appImageInfo.IsTerminalApp, 141 | AppImageType: appImageInfo.AppImageType, 142 | }) 143 | 144 | if err != nil { 145 | return err 146 | } 147 | 148 | err = registry.Close() 149 | if err != nil { 150 | return err 151 | } 152 | 153 | // Print Signature Info If Exist. 154 | utils.ShowSignature(targetFilePath) 155 | 156 | // Remove the old file 157 | os.Remove(entry.FilePath) 158 | 159 | // utils.ShowSignature(result) 160 | fmt.Println("Updated: " + target) 161 | howManyUpdates++ 162 | } 163 | 164 | if cmd.Check { 165 | if howManyUpdates == 0 { 166 | fmt.Println("No Updates Found!") 167 | } else { 168 | fmt.Println("Update Available For", howManyUpdates, "Application(s)") 169 | } 170 | } else { 171 | if howManyUpdates == 0 { 172 | fmt.Println("No Updates Found!") 173 | } else { 174 | fmt.Println("Updated", howManyUpdates, "Application(s)") 175 | } 176 | } 177 | 178 | return nil 179 | } 180 | 181 | // Get a application from registry 182 | func (cmd *UpdateCmd) getRegistryEntry(target string) (utils.RegistryEntry, error) { 183 | registry, err := utils.OpenRegistry() 184 | if err != nil { 185 | return utils.RegistryEntry{}, err 186 | } 187 | defer registry.Close() 188 | 189 | entry, _ := registry.Lookup(target) 190 | 191 | return entry, nil 192 | } 193 | 194 | // Get all the applications from the registry 195 | func getAllTargets() ([]string, error) { 196 | registry, err := utils.OpenRegistry() 197 | if err != nil { 198 | return nil, err 199 | } 200 | registry.Update() 201 | 202 | var repos []string 203 | for k := range registry.Entries { 204 | entry, _ := registry.Lookup(k) 205 | repos = append(repos, entry.Repo) 206 | } 207 | 208 | return repos, nil 209 | } 210 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Bread ![:bread:](./.github/bread.svg) 3 | 4 | Install, update, remove & run AppImage from GitHub using your CLI. (Fork of [AppImage ClI Tool](https://github.com/AppImageCrafters/appimage-cli-tool)) 5 | 6 | ## Features 7 | - Install from the GitHub Releases 8 | - Automatically Integrate App To Desktop When Installing/Updating 9 | - Run Applications From Remote Without Installing Them 10 | - Update with ease 11 | 12 | ## Getting Started 13 | 14 | ### Installation 15 | 16 |
17 | Arch Linux & it's Derivatives 18 |
19 |

you can use this step if your distribution does provide libappimage v1.0.0 or greater, which is the case on Arch Linux & it's Derivatives, kaOS, KDE Neon, Parabola Linux

20 |

install libappimage dependency

21 |
pacman -S libappimage
22 |

then install bread

23 |
sudo curl -L https://github.com/pegvin/bread/releases/download/v0.7.2/bread-0.7.2-x86_64 -o /usr/local/bin/bread && sudo chmod +x /usr/local/bin/bread
24 |
25 | 26 |
27 | Debian & it's Derivatives 28 |
29 |

you can use this step if your distribution doesn't provide libappimage v1.0.0 or greater, which is the case on Debian & it's derivatives

30 |

get the appimage containing libappimage v1.0.3

31 |
sudo curl -L https://github.com/pegvin/bread/releases/download/v0.7.2/bread-0.7.2-x86_64.AppImage -o /usr/local/bin/bread && sudo chmod +x /usr/local/bin/bread
32 |
33 | 34 | ***Any version of libappimage will work with bread but it is recommended to use v1.0.0 or greater, You can also Refer to this [list](https://repology.org/project/libappimage/versions) to check what version of libappimage your Distribution provides.*** 35 | 36 | --- 37 | 38 | ## Removal 39 | 40 | Just Remove the binary 41 | ```bash 42 | sudo rm -v /usr/local/bin/bread 43 | ``` 44 | 45 | **NOTE** this won't delete the app you've installed. 46 | 47 | --- 48 | 49 | ## Usage 50 | 51 |
52 | NOTE 53 |
54 |

Often there are many times when the GitHub user and repo both are same, for example libresprite, so in this case you can just specify single name like this bread install libresprite, this works with all the commands

55 |
56 | 57 |
58 | Install a application 59 |
60 |

To install an Application from GitHub you can use the install command where user is the github repo owner and repo is the repository name

61 |
bread install user/repo
62 |

To install an application from a different Tag name you can specify the tag name too

63 |
bread install user/repo tagname
64 |
65 | 66 |
67 | Run a application from remote 68 |
69 |

If you want to run a application from remote without installing it you can use the run command

70 |
bread run user/repo
71 |

You can pass CLI arguments to the application too like this

72 |
bread run user/repo -- --arg1 --arg2
73 |

You can clear the download cache using clean command bread clean, Since all the applications you run from remote are cached so that it isn't downloaded everytime

74 |
75 | 76 |
77 | Remove a application 78 |
79 |

you can remove a installed application using the remove command

80 |
bread remove user/repo
81 |
82 | 83 |
84 | Update a applicationn 85 |
86 |

You can update a application using the update command

87 |
bread update user/repo
88 | 89 |

if you just want to check if update is available you can use the --check flag

90 |
bread update user/repo --check
91 | 92 |

if you want to update all the applications you can use the --all flag

93 |
bread update --all
94 | 95 |

the --check & --all flag can be used together

96 |
bread update --all --check
97 | 98 |

the -n or --no-pre-release flag can be used to disable updates for pre-releases.

99 |
bread update --no-pre-release
100 |
101 | 102 |
103 | Search for an application 104 |
105 |

You can search for a application from the AppImage API

106 |
bread search "Your search text"
107 |
108 | 109 |
110 | List all the installed application 111 |
112 |

You can list all the installed applications using list command

113 |
bread list
114 |

If you also want to see the SHA1 Hashes of the applications listed, you can pass the -s or --show-sha1 flag

115 |
bread list --show-sha1
116 |

If you want to see the GitHub release tag name -t or --show-tag flag

117 |
bread list --show-tag
118 |
119 | 120 | --- 121 | 122 | ### Bugs 123 | - Icons not showing in menus until there's a system reboot 124 | - Update Command Crashing 125 | 126 | ### Limits 127 | - Bread uses GitHub API to get information about a repository and it's release, but without authentication GitHub API limits the request per hour. 128 | 129 | --- 130 | 131 | ## Tested On: 132 | - Ubuntu 20.04 - by me 133 | - Debian 11 - by me 134 | - Manjaro Linux - by me 135 | - Arch Linux - by [my brother](https://github.com/idno34) 136 | 137 | --- 138 | 139 | ## File/Folder Layout 140 | Bread installs all the applications inside the `Applications` directory in your Linux Home Directory `~`, inside this directory there can be also a directory named `run-cache` which contains all the appimages you've run from the remote via the `bread run` command. 141 | 142 | In the `Applications` there is also a file named `.registry.json` which contains information related to the installed applications! 143 | In the `Applications` directory there is also a file named `.AppImageFeed.json` which is AppImage Catalog From [AppImage API](https://appimage.github.io/feed.json) 144 | 145 | --- 146 | ## Related: 147 | - [Zap - :zap: Delightful AppImage package manager ](https://github.com/srevinsaju/zap) 148 | - [A AppImage Manager Written in Shell](https://github.com/ivan-hc/AM-Application-Manager) 149 | - [The Original Tool Which Bread is Based On](https://github.com/AppImageCrafters/appimage-cli-tool) 150 | 151 | --- 152 | 153 | ## Building From Source 154 | 155 | Make Sure You Have Go version 1.18.x & [AppImage Builder](https://appimage-builder.readthedocs.io/en/latest/) Installed. 156 | 157 | Get The Repository Via Git: 158 | 159 | ```bash 160 | git clone https://github.com/pegvin/bread 161 | ``` 162 | 163 | Go Inside The Source Code Directory & Get All The Dependencies: 164 | 165 | ```bash 166 | cd bread 167 | go mod tidy 168 | ``` 169 | 170 | Make The Build Script Executable And Run It 171 | 172 | ```bash 173 | chmod +x ./make 174 | ./make --prod 175 | ``` 176 | 177 | And To Build The AppImage Run 178 | 179 | ```bash 180 | ./make appimage 181 | ``` 182 | 183 | --- 184 | ## Build Script 185 | The `make` bash script can build your go code, make appimage out of it, and clean the left over stuff including the genrated builds. 186 | 187 | #### Building in Development Mode 188 | This will build the go code into a binary inside the `build` folder 189 | ``` 190 | ./make 191 | ``` 192 | 193 | #### Building in Production Mode 194 | Building for production requires passing `--prod` flag which will enable some compiler options resulting in a small build size. 195 | ``` 196 | ./make --prod 197 | ``` 198 | 199 | #### Building the AppImage 200 | Bread requires libappimage0 for integrating your apps to desktop, which is done via libappimage, to make End user's life easier we package the libappimage with bread and that's why we build the binaries into AppImages so that user doesn't need to install anything. 201 | 202 | To make a appimage out the pre built binaries 203 | ``` 204 | ./make appimage 205 | ``` 206 | 207 | #### Get Dependency 208 | To install the dependencies require to build go binary 209 | ``` 210 | ./make get-deps 211 | ``` 212 | 213 | --- 214 | 215 | ## Todo 216 | - [ ] Switch To Some Other Language Since Go Module System is Shit 217 | - [ ] Improve UI 218 | - [x] Make AppImages Runnable From Remote Without Installing (Done in v0.3.6) 219 | - [x] Work On Reducing Binary Sizes (Reduced From 11.1MB to 3.1MB) 220 | - [ ] Add 32 Bit Builds (Currently not possible since [DL](https://github.com/rainycape/dl) dependency is not available for 32 bit machines) 221 | - [ ] Add Auto Updater Which Can Update The Bread Itself 222 | - [x] Add `--version` To Get The Version (Done in v0.2.2) 223 | - [x] Mirrors: 224 | - :heavy_multiplication_x: I Would Like To Introduce Concept Of Mirror Lists Which Contain The List Of AppImages With The Download URL, tho currently i am not working on it but in future i might. 225 | - [x] I am dropping this idea, tho i've added a search command which can search for appimages from a central server API 226 | 227 | --- 228 | 229 | # Thanks 230 | --------------------------------------------------------------------------------