Browse Source

Merge 7da452011f into d8882fc307

pull/79/merge
Thomas Dickson 1 year ago committed by GitHub
parent
commit
071b2ba95f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 125
      README.md
  2. 3
      contrib/completion/zsh/_fdroidcl
  3. 5
      defaults.go
  4. 5
      go.mod
  5. 2
      go.sum
  6. 25
      main.go
  7. 67
      scan.go
  8. 292
      setup.go

125
README.md

@ -4,67 +4,77 @@
[F-Droid](https://f-droid.org/) desktop client. Requires Go 1.19 or later.
go install mvdan.cc/fdroidcl@latest
```sh
go install mvdan.cc/fdroidcl@latest
```
While the Android client integrates with the system with regular update checks
and notifications, this is a simple command line client that talks to connected
devices via [ADB](https://developer.android.com/tools/help/adb.html).
### Quickstart
## Quickstart
Download the index:
fdroidcl update
```sh
fdroidcl update
```
Show all available apps:
fdroidcl search
```sh
fdroidcl search
```
Install an app:
fdroidcl install org.adaway
```sh
fdroidcl install org.adaway
```
Show all available updates, and install them:
fdroidcl search -u
fdroidcl install -u
Unofficial packages are available on: [Debian](https://packages.debian.org/buster/fdroidcl) and [Ubuntu](https://packages.ubuntu.com/eoan/fdroidcl).
### Commands
update Update the index
search [<regexp...>] Search available apps
show <appid...> Show detailed info about apps
install [<appid...>] Install or upgrade apps
uninstall <appid...> Uninstall an app
download <appid...> Download an app
devices List connected devices
list (categories/users) List all known values of a kind
repo Manage repositories
clean Clean index and/or cache
defaults Reset to the default settings
version Print version information
```sh
fdroidcl search -u
fdroidcl install -u
```
Unofficial packages are available on:
[Debian](https://packages.debian.org/buster/fdroidcl) and
[Ubuntu](https://packages.ubuntu.com/eoan/fdroidcl).
## Commands
```text
update Update the index
search [<regexp...>] Search available apps
show <appid...> Show detailed info about apps
install [<appid...>] Install or upgrade apps
uninstall <appid...> Uninstall an app
download <appid...> Download an app
devices List connected devices
scan Scan for known fdroid apps on a device
list (categories/users) List all known values of a kind
repo Manage repositories
setups Manage setups
clean Clean index and/or cache
defaults Reset to the default settings
version Print version information
```
An appid is just an app's unique package name. A specific version of an app can
be selected by following the appid with a colon and the version code. The
'search' and 'show' commands can be used to find these strings. For example:
$ fdroidcl search redreader
$ fdroidcl show org.quantumbadger.redreader
$ fdroidcl install org.quantumbadger.redreader:85
### Config
You can configure what repositories to use in the `config.json` file. On Linux,
you will likely find it at `~/.config/fdroidcl/config.json`.
You can run `fdroidcl defaults` to create the config with the default settings.
```sh
fdroidcl search redreader
fdroidcl show org.quantumbadger.redreader
fdroidcl install org.quantumbadger.redreader:85
```
#### *new: you can manage the repositories now directly via cli*
### *new: you can manage the repositories now directly via cli*
```
```text
usage: fdroidcl repo
List, add, remove, enable or disable repositories.
@ -82,25 +92,60 @@ Modify repositories:
$ fdroidcl repo disable <NAME>
```
### Advantages over the Android client
### *new: you can manage automating installs using setups*
```text
usage: fdroidcl setup
List, add, remove, edit, and import/export setups.
Setups allow for mass installs onto an android device, excellent for backups.
List setups:
$ fdroidcl setup Show all setups
$ fdroidcl setup list <NAME> Show details about one setup
Modify setups:
$ fdroidcl setup new <NAME>
$ fdroidcl setup remove <NAME>
$ fdroidcl setup apply <NAME>
$ fdroidcl setup add-app <NAME> <APP-ID>
$ fdroidcl setup rm-app <NAME> <APP-ID>
$ fdroidcl setup add-repo <NAME> <REPO-NAME>
$ fdroidcl setup rm-repo <NAME> <REPO-NAME>
Export setups:
$ fdroidcl setup import <FILENAME>
$ fdroidcl setup export <NAME>
```
## Config
You can configure what repositories to use in the `config.toml` file. On Linux,
you will likely find it at `~/.config/fdroidcl/config.toml`.
You can run `fdroidcl defaults` to create the config with the default settings.
## Advantages over the Android client
* Command line interface
* Batch install/update/remove apps without root nor system privileges
* No need to install a client on the device
### What it will never do
## What it will never do
* Run as a daemon, e.g. periodic index updates
* Act as an F-Droid server
* Swap apps with devices
### Caveats
## Caveats
* Index verification relies on HTTPS (not the JAR signature)
* The tool can only interact with one device at a time
* Hardware compatibility of packages is not checked
### FAQ
## FAQ
* What's the point of a desktop client?

3
contrib/completion/zsh/_fdroidcl

@ -8,9 +8,12 @@ _fdroidcl() {
'show:show detailed info about an app'
'list:list all known values of a kind'
'devices:list connected devices'
'scan:list all fdroid apps on a device'
'download:download an app'
'install:install or upgrade an app'
'uninstall:uninstall an app'
'setup:manage setups'
'repo:manage repos'
'defaults:reset to the default settings')
_arguments \

5
defaults.go

@ -4,9 +4,10 @@
package main
import (
"encoding/json"
"fmt"
"os"
"github.com/pelletier/go-toml/v2"
)
var cmdDefaults = &Command{
@ -26,7 +27,7 @@ func runDefaults(args []string) error {
}
func writeConfig(c *userConfig) error {
b, err := json.MarshalIndent(c, "", "\t")
b, err := toml.Marshal(c)
if err != nil {
return fmt.Errorf("cannot encode config: %v", err)
}

5
go.mod

@ -1,6 +1,8 @@
module mvdan.cc/fdroidcl
go 1.19
go 1.21.0
toolchain go1.23.0
require (
github.com/kr/pretty v0.3.1
@ -12,6 +14,7 @@ require (
github.com/kr/text v0.2.0 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
golang.org/x/sys v0.12.0 // indirect
golang.org/x/term v0.12.0 // indirect

2
go.sum

@ -13,6 +13,8 @@ github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZ
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=

25
main.go

@ -4,12 +4,13 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/pelletier/go-toml/v2"
)
const cmdName = "fdroidcl"
@ -43,17 +44,24 @@ func mustData() string {
}
func configPath() string {
return filepath.Join(mustData(), "config.json")
return filepath.Join(mustData(), "config.toml")
}
type repo struct {
ID string `json:"id"`
URL string `json:"url"`
Enabled bool `json:"enabled"`
ID string `toml:"id"`
URL string `toml:"url"`
Enabled bool `toml:"enabled"`
}
type setup struct {
ID string `toml:"id"`
Apps []string `toml:"apps"`
Repos []string `toml:"repos"`
}
type userConfig struct {
Repos []repo `json:"repos"`
Repos []repo `toml:"repos"`
Setups []setup `toml:"setups"`
}
var config = userConfig{
@ -69,6 +77,7 @@ var config = userConfig{
Enabled: false,
},
},
Setups: []setup{},
}
func readConfig() error {
@ -79,7 +88,7 @@ func readConfig() error {
}
defer f.Close()
fileConfig := userConfig{}
err = json.NewDecoder(f).Decode(&fileConfig)
err = toml.NewDecoder(f).Decode(&fileConfig)
if err != nil {
return err
}
@ -168,8 +177,10 @@ var commands = []*Command{
cmdUninstall,
cmdDownload,
cmdDevices,
cmdScan,
cmdList,
cmdRepo,
cmdSetup,
cmdClean,
cmdDefaults,
cmdVersion,

67
scan.go

@ -0,0 +1,67 @@
// Copyright (c) 2024, Thomas Dickson
// See LICENSE for licensing information
package main
import (
"fmt"
"os"
"strings"
"mvdan.cc/fdroidcl/adb"
)
var cmdScan = &Command{
UsageLine: "scan",
Short: "Scan for all recognised apps on a connected device",
}
func init() {
cmdScan.Run = runScan
}
func runScan(args []string) error {
if err := startAdbIfNeeded(); err != nil {
return err
}
devices, err := adb.Devices()
if err != nil {
return fmt.Errorf("could not get devices: %v", err)
}
if len(devices) == 0 {
return fmt.Errorf("no devices found")
}
for _, device := range devices {
fmt.Fprintf(os.Stderr, "Scanning %s - %s (%s)\n", device.ID, device.Model, device.Product)
scanForPackages(device)
fmt.Fprintln(os.Stderr, "Scan completed without error")
}
return nil
}
func scanForPackages(device *adb.Device) {
cmd := device.AdbShell("pm list packages")
out, err := cmd.Output()
if err != nil {
fmt.Println("could not run command: ", err)
}
// fmt.Println(string(out))
// otherwise, print the output from running the command
lines := strings.Split(string(out), "\n")
// fmt.Println(lines)
for _, line := range lines {
if len(line) > 8 {
line = line[8:]
// fmt.Println(line)
apps, err := findApps([]string{line})
if err == nil {
fmt.Println(apps[0].PackageName)
}
}
}
}

292
setup.go

@ -0,0 +1,292 @@
// Copyright (c) 2024, Thomas Dickson
// See LICENSE for licensing information
package main
import (
"fmt"
"os"
"path/filepath"
"github.com/pelletier/go-toml/v2"
)
var cmdSetup = &Command{
UsageLine: "setup",
Short: "Manage setups",
Long: `
List, add, remove, edit, and import/export setups.
Setups allow for mass installs onto an android device, excellent for backups.
List setups:
$ fdroidcl setup Show all setups
$ fdroidcl setup list <NAME> Show details about one setup
Modify setups:
$ fdroidcl setup new <NAME>
$ fdroidcl setup remove <NAME>
$ fdroidcl setup apply <NAME>
$ fdroidcl setup add-app <NAME> <APP-ID>
$ fdroidcl setup rm-app <NAME> <APP-ID>
$ fdroidcl setup add-repo <NAME> <REPO-NAME>
$ fdroidcl setup rm-repo <NAME> <REPO-NAME>
Export setups:
$ fdroidcl setup import <FILENAME>
$ fdroidcl setup export <NAME>
`[1:],
}
func init() {
cmdSetup.Run = runSetup
}
func runSetup(args []string) error {
if len(args) == 0 {
// list repositories
if len(config.Setups) == 0 {
fmt.Println("No setups!")
} else {
for _, value := range config.Setups {
fmt.Printf("Name: %s\n", value.ID)
}
}
return nil
}
switch args[0] {
case "list":
if len(args) != 2 {
return fmt.Errorf("wrong amount of arguments")
}
return listSetup(args[1])
case "new":
if len(args) != 2 {
return fmt.Errorf("wrong amount of arguments")
}
return newSetup(args[1])
case "apply":
if len(args) != 2 {
return fmt.Errorf("wrong amount of arguments")
}
return applySetup(args[1])
case "remove":
if len(args) != 2 {
return fmt.Errorf("wrong amount of arguments")
}
return removeSetup(args[1])
case "add-app":
if len(args) != 3 {
return fmt.Errorf("wrong amount of arguments")
}
return addSetupApp(args[1], args[2])
case "rm-app":
if len(args) != 3 {
return fmt.Errorf("wrong amount of arguments")
}
return removeSetupApp(args[1], args[2])
case "add-repo":
if len(args) != 3 {
return fmt.Errorf("wrong amount of arguments")
}
return removeSetupRepo(args[1], args[2])
case "rm-repo":
if len(args) != 3 {
return fmt.Errorf("wrong amount of arguments")
}
return removeSetupRepo(args[1], args[2])
case "import":
if len(args) != 2 {
return fmt.Errorf("wrong amount of arguments")
}
return importSetup(args[1])
case "export":
if len(args) != 2 {
return fmt.Errorf("wrong amount of arguments")
}
return exportSetup(args[1])
}
return fmt.Errorf("wrong usage")
}
func getIndex(sl []string, str string) int {
index := -1
for i, val := range sl {
if val == str {
index = i
break
}
}
return index
}
func listSetup(name string) error {
index := setupIndex(name)
if index == -1 {
return fmt.Errorf("a setup with the name \"%s\" could not be found", name)
}
setup := config.Setups[index]
fmt.Printf("Name: %s\n", setup.ID)
fmt.Printf("Repos: %s\n", setup.Repos)
fmt.Printf("Apps: %s\n", setup.Apps)
return nil
}
func setupIndex(name string) int {
index := -1
for i, value := range config.Setups {
if value.ID == name {
index = i
break
}
}
return index
}
func newSetup(name string) error {
repos := []string{}
for _, repo := range config.Repos {
repos = append(repos, repo.ID)
}
config.Setups = append(config.Setups, setup{name, []string{}, repos})
return writeConfig(&config)
}
func applySetup(name string) error {
index := setupIndex(name)
if index == -1 {
return fmt.Errorf("a setup with the name \"%s\" could not be found", name)
}
setup := config.Setups[index]
for _, repo := range setup.Repos {
index = repoIndex(repo)
if index == -1 {
return fmt.Errorf("setup contains unknown repo id \"%s\" ", repo)
}
}
fmt.Println("All repos work!")
if len(setup.Apps) == 0 {
return fmt.Errorf("setup has no apps!")
}
for _, app := range setup.Apps {
err := runInstall([]string{app})
if err != nil {
fmt.Println(err)
}
}
return nil
}
func removeSetup(name string) error {
index := setupIndex(name)
if index == -1 {
return fmt.Errorf("a setup with the name \"%s\" could not be found", name)
}
config.Setups = append(config.Setups[:index], config.Setups[index+1:]...)
return writeConfig(&config)
}
func addSetupApp(setupName string, appName string) error {
index := setupIndex(setupName)
if index == -1 {
return fmt.Errorf("a setup with the name \"%s\" could not be found", setupName)
}
config.Setups[index].Apps = append(config.Setups[index].Apps, appName)
return writeConfig(&config)
}
func removeSetupApp(setupName string, appName string) error {
index := setupIndex(setupName)
if index == -1 {
return fmt.Errorf("a setup with the name \"%s\" could not be found", setupName)
}
apps := config.Setups[index].Apps
appIndex := getIndex(apps, appName)
if appIndex == -1 {
return fmt.Errorf("a app with the name \"%s\" could not be found for the setup with the name \"%s\"", appName, setupName)
}
config.Setups[index].Apps = append(apps[:appIndex], apps[appIndex+1:]...)
return writeConfig(&config)
}
func addSetupRepo(setupName string, repoName string) error {
repoIndex := repoIndex(repoName)
if repoIndex == -1 {
return fmt.Errorf("a repo with the name \"%s\" could not be found", repoName)
}
index := setupIndex(setupName)
if index == -1 {
return fmt.Errorf("a setup with the name \"%s\" could not be found", setupName)
}
config.Setups[index].Repos = append(config.Setups[index].Repos, repoName)
return writeConfig(&config)
}
func removeSetupRepo(setupName string, repoName string) error {
index := setupIndex(setupName)
if index == -1 {
return fmt.Errorf("a setup with the name \"%s\" could not be found", setupName)
}
repos := config.Setups[index].Repos
repoIndex := getIndex(repos, repoName)
if repoIndex == -1 {
return fmt.Errorf("a repo with the name \"%s\" could not be found for the setup with the name \"%s\"", repoName, setupName)
}
config.Setups[index].Repos = append(repos[:repoIndex], repos[repoIndex+1:]...)
return writeConfig(&config)
}
func importSetup(filename string) error {
filename, err := filepath.Abs(filename)
if err != nil {
return err
}
f, err := os.Open(filename)
if err != nil {
return fmt.Errorf("file \"%s\" does not exist", filename)
}
defer f.Close()
fileSetup := setup{}
err = toml.NewDecoder(f).Decode(&fileSetup)
if err != nil {
return err
}
config.Setups = append(config.Setups, fileSetup)
return writeConfig(&config)
}
func exportSetup(name string) error {
index := setupIndex(name)
if index == -1 {
return fmt.Errorf("a setup with the name \"%s\" could not be found", name)
}
b, err := toml.Marshal(config.Setups[index])
if err != nil {
return fmt.Errorf("cannot encode config: %v", err)
}
f, err := os.Create(config.Setups[index].ID + ".toml")
if err != nil {
return fmt.Errorf("cannot create config file: %v", err)
}
_, err = f.Write(b)
if cerr := f.Close(); err == nil {
err = cerr
}
return err
}
Loading…
Cancel
Save